v0.2.6-pre.015

This commit is contained in:
2026-08-22 03:21:49 +02:00
parent bd561cad47
commit d370027c02
27 changed files with 2630 additions and 364 deletions

View File

@@ -1,10 +1,12 @@
// file: crates/ksp-wallet-lib/src/constants.rs
// version: 7
// version: 8
//! Wallet-owned constants.
/// Native Wallet format version implemented by the V1 codec.
pub const KSPWALLET_FORMAT_VERSION_V1: u32 = 1;
/// Native Wallet format version implemented by the V2 binary codec.
pub const KSPWALLET_FORMAT_VERSION_V2: u32 = 2;
/// Exact magic string required by every native `.kspwallet` document.
pub const KSPWALLET_MAGIC: &str = "KSPWALLET";
/// Maximum accepted `.kspwallet` document size before JSON parsing.
@@ -85,6 +87,50 @@ pub const KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V1-STATE";
pub const KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-VIEW-SLOT";
/// Byte length of an XChaCha20-Poly1305 nonce.
pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
/// Byte length of the Poly1305 authentication tag appended to each V2 ciphertext.
pub const KSPWALLET_V2_AEAD_TAG_BYTES: usize = KSPWALLET_V1_AEAD_TAG_BYTES;
/// Argon2 version serialized by V2 key slots.
pub const KSPWALLET_V2_ARGON2_VERSION: u32 = KSPWALLET_V1_ARGON2_VERSION;
/// Byte length of an Ed25519 public key used as V2 Wallet format authority.
pub const KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES: usize = KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES;
/// Byte length of an Ed25519 detached V2 state signature.
pub const KSPWALLET_V2_ED25519_SIGNATURE_BYTES: usize = KSPWALLET_V1_ED25519_SIGNATURE_BYTES;
/// Initial protected payload version used independently by V2 control, metadata and secret compartments.
pub const KSPWALLET_V2_INITIAL_PAYLOAD_VERSION: u32 = KSPWALLET_V1_INITIAL_PAYLOAD_VERSION;
/// Structural V2 ceiling for serialized Argon2 iteration cost.
pub const KSPWALLET_V2_MAX_ARGON2_ITERATIONS: u32 = KSPWALLET_V1_MAX_ARGON2_ITERATIONS;
/// Structural V2 ceiling for serialized Argon2 memory cost.
pub const KSPWALLET_V2_MAX_ARGON2_MEMORY_KIB: u32 = KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB;
/// Structural V2 ceiling for serialized Argon2 parallelism.
pub const KSPWALLET_V2_MAX_ARGON2_PARALLELISM: u32 = KSPWALLET_V1_MAX_ARGON2_PARALLELISM;
/// Maximum Argon2 salt size accepted by V2.
pub const KSPWALLET_V2_MAX_KDF_SALT_BYTES: usize = KSPWALLET_V1_MAX_KDF_SALT_BYTES;
/// Maximum wrapped-key ciphertext size accepted by V2.
pub const KSPWALLET_V2_MAX_KEY_WRAP_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_KEY_WRAP_CIPHERTEXT_BYTES;
/// Maximum protected metadata ciphertext size accepted by V2.
pub const KSPWALLET_V2_MAX_METADATA_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES;
/// Maximum OWNER-control ciphertext size accepted by V2.
pub const KSPWALLET_V2_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES;
/// Maximum OWNER-only secret ciphertext size accepted by V2.
pub const KSPWALLET_V2_MAX_SECRET_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES;
/// Domain separator for V2 metadata-compartment AEAD AAD.
pub const KSPWALLET_V2_METADATA_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-METADATA";
/// Minimum Argon2 salt size accepted by V2.
pub const KSPWALLET_V2_MIN_KDF_SALT_BYTES: usize = KSPWALLET_V1_MIN_KDF_SALT_BYTES;
/// Domain separator for V2 OWNER-control compartment AEAD AAD.
pub const KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNER-CONTROL";
/// Domain separator for V2 OWNER key-slot wrapping AAD.
pub const KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNER-SLOT";
/// Domain separator for V2 OWNER-only secret compartment AEAD AAD.
pub const KSPWALLET_V2_SECRET_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-SECRET";
/// Byte length of every V2 key-slot identifier.
pub const KSPWALLET_V2_SLOT_ID_BYTES: usize = KSPWALLET_V1_SLOT_ID_BYTES;
/// Domain separator for the V2 OWNER state-signature transcript.
pub const KSPWALLET_V2_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V2-STATE";
/// Domain separator for V2 VIEW key-slot wrapping AAD.
pub const KSPWALLET_V2_VIEW_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-VIEW-SLOT";
/// Byte length of an XChaCha20-Poly1305 nonce in V2.
pub const KSPWALLET_V2_XCHACHA_NONCE_BYTES: usize = KSPWALLET_V1_XCHACHA_NONCE_BYTES;
/// Owning tracing target for events emitted by the Wallet crate.
pub(crate) const TRACING_TARGET: &str = "ksp-wallet-lib";

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/lib.rs
// version: 11
// version: 12
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -15,7 +15,8 @@
//! adds Solana message signing, protected metadata administration, password rotation and strong VIEW disable/recreate with capability-bound atomic
//! replacement. `0.2.5-pre.008` adds bounded Solana CLI JSON and canonical full-keypair Base58 import/export adapters with safe inspection and no-clobber
//! native import/export publication. `0.2.5-pre.009` adds adversarial security/compliance canaries and records the final dependency/interop audit before
//! documentation closure. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
//! documentation closure. `0.2.6-pre.015` defines the strict binary `.kspwallet` V2 wire and its bounded canonical codec without yet changing the public
//! persistence default or Wallet Desk dispatch. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
//! observability uses only
//! `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
@@ -29,10 +30,12 @@ mod password;
mod payload;
mod persistence;
mod transcript;
mod transcript_v2;
mod transfer;
mod view;
mod wallet;
mod wire;
mod wire_v2;
#[cfg(test)]
#[path = "../unit_tests/security.rs"]
@@ -42,6 +45,8 @@ mod security_tests;
pub use self::capability::WalletCapability;
/// Native `.kspwallet` V1 format version.
pub use self::constants::KSPWALLET_FORMAT_VERSION_V1;
/// Native `.kspwallet` V2 binary format version.
pub use self::constants::KSPWALLET_FORMAT_VERSION_V2;
/// Exact magic string required by every native `.kspwallet` document.
pub use self::constants::KSPWALLET_MAGIC;
/// Maximum accepted `.kspwallet` document size before parsing.
@@ -122,6 +127,50 @@ pub use self::constants::KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN;
pub use self::constants::KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN;
/// Byte length of an XChaCha20-Poly1305 nonce.
pub use self::constants::KSPWALLET_V1_XCHACHA_NONCE_BYTES;
/// Byte length of the AEAD authentication tag appended to V2 ciphertexts.
pub use self::constants::KSPWALLET_V2_AEAD_TAG_BYTES;
/// Argon2 version serialized by `.kspwallet` V2 key slots.
pub use self::constants::KSPWALLET_V2_ARGON2_VERSION;
/// Byte length of the V2 Ed25519 format-authority public key.
pub use self::constants::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES;
/// Byte length of the V2 Ed25519 detached state signature.
pub use self::constants::KSPWALLET_V2_ED25519_SIGNATURE_BYTES;
/// Initial per-compartment protected payload version for V2.
pub use self::constants::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION;
/// Maximum structural Argon2 iteration count accepted by the V2 parser.
pub use self::constants::KSPWALLET_V2_MAX_ARGON2_ITERATIONS;
/// Maximum structural Argon2 memory cost accepted by the V2 parser.
pub use self::constants::KSPWALLET_V2_MAX_ARGON2_MEMORY_KIB;
/// Maximum structural Argon2 parallelism accepted by the V2 parser.
pub use self::constants::KSPWALLET_V2_MAX_ARGON2_PARALLELISM;
/// Maximum Argon2 salt size accepted by V2.
pub use self::constants::KSPWALLET_V2_MAX_KDF_SALT_BYTES;
/// Maximum wrapped-key ciphertext size accepted by V2.
pub use self::constants::KSPWALLET_V2_MAX_KEY_WRAP_CIPHERTEXT_BYTES;
/// Maximum protected metadata ciphertext size accepted by V2.
pub use self::constants::KSPWALLET_V2_MAX_METADATA_CIPHERTEXT_BYTES;
/// Maximum OWNER-control ciphertext size accepted by V2.
pub use self::constants::KSPWALLET_V2_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES;
/// Maximum OWNER-only secret ciphertext size accepted by V2.
pub use self::constants::KSPWALLET_V2_MAX_SECRET_CIPHERTEXT_BYTES;
/// Domain separator for V2 metadata-compartment AEAD AAD.
pub use self::constants::KSPWALLET_V2_METADATA_AAD_DOMAIN;
/// Minimum Argon2 salt size accepted by V2.
pub use self::constants::KSPWALLET_V2_MIN_KDF_SALT_BYTES;
/// Domain separator for V2 OWNER-control compartment AEAD AAD.
pub use self::constants::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN;
/// Domain separator for V2 OWNER key-slot wrapping AAD.
pub use self::constants::KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN;
/// Domain separator for V2 OWNER-only secret compartment AEAD AAD.
pub use self::constants::KSPWALLET_V2_SECRET_AAD_DOMAIN;
/// Byte length of every V2 key-slot identifier.
pub use self::constants::KSPWALLET_V2_SLOT_ID_BYTES;
/// Domain separator for the V2 OWNER state-signature transcript.
pub use self::constants::KSPWALLET_V2_STATE_TRANSCRIPT_DOMAIN;
/// Domain separator for V2 VIEW key-slot wrapping AAD.
pub use self::constants::KSPWALLET_V2_VIEW_SLOT_AAD_DOMAIN;
/// Byte length of an XChaCha20-Poly1305 nonce in V2.
pub use self::constants::KSPWALLET_V2_XCHACHA_NONCE_BYTES;
/// Error code used when an atomic Wallet persistence operation cannot publish a valid replacement.
pub use self::error::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED;
/// Error code used when an authenticated Wallet structure cannot be verified.
@@ -224,6 +273,30 @@ pub use self::wire::WalletStateSignatureAlgorithmV1;
pub use self::wire::WalletStateSignatureV1;
/// OWNER-signed stable descriptor of the optional VIEW slot.
pub use self::wire::WalletViewDescriptorV1;
/// Strict semantic representation of one parsed native `.kspwallet` V2 binary envelope.
pub use self::wire_v2::KspWalletEnvelopeV2;
/// Authenticated-encryption algorithm fixed by native Wallet V2.
pub use self::wire_v2::WalletAeadAlgorithmV2;
/// Protected compartment kind fixed by native Wallet V2.
pub use self::wire_v2::WalletCompartmentKindV2;
/// One validated encrypted V2 compartment.
pub use self::wire_v2::WalletEncryptedCompartmentV2;
/// Password KDF fixed by native Wallet V2.
pub use self::wire_v2::WalletKdfAlgorithmV2;
/// Parsed Argon2id parameters from one V2 key slot.
pub use self::wire_v2::WalletKdfParametersV2;
/// Role of one native V2 key slot.
pub use self::wire_v2::WalletKeySlotRoleV2;
/// One validated OWNER or VIEW V2 key slot.
pub use self::wire_v2::WalletKeySlotV2;
/// Parsed AEAD wrapped-key payload from one V2 key slot.
pub use self::wire_v2::WalletKeyWrapV2;
/// State-signature algorithm fixed by native Wallet V2.
pub use self::wire_v2::WalletStateSignatureAlgorithmV2;
/// Detached OWNER state signature embedded in a V2 envelope.
pub use self::wire_v2::WalletStateSignatureV2;
/// OWNER-signed stable descriptor of the optional VIEW slot in V2.
pub use self::wire_v2::WalletViewDescriptorV2;
/// Wallet-owned tracing target used by the KSP logging facade.
pub(crate) use self::constants::TRACING_TARGET;
@@ -277,6 +350,12 @@ pub(crate) use self::transcript::compartment_aad;
pub(crate) use self::transcript::slot_aad;
/// Internal deterministic OWNER-state transcript codec shared by Wallet crypto layers.
pub(crate) use self::transcript::state_transcript;
/// Internal deterministic V2 compartment-AAD codec shared by Wallet crypto layers.
pub(crate) use self::transcript_v2::compartment_aad_v2;
/// Internal deterministic V2 key-slot-AAD codec shared by Wallet crypto layers.
pub(crate) use self::transcript_v2::slot_aad_v2;
/// Internal deterministic V2 OWNER-state transcript codec shared by Wallet crypto layers.
pub(crate) use self::transcript_v2::state_transcript_v2;
/// Internal no-clobber transfer-file writer used only by OWNER export.
pub(crate) use self::transfer::write_wallet_transfer_file_v1;
/// Crate-internal `OwnerStateV1` state shared across the owning crate.

View File

@@ -0,0 +1,141 @@
// file: crates/ksp-wallet-lib/src/transcript_v2.rs
// version: 1
//! Deterministic `.kspwallet` V2 state-transcript and AEAD-AAD encoding.
const TAG_COMPARTMENT_ALGORITHM: u16 = 0x0202;
const TAG_COMPARTMENT_CIPHERTEXT: u16 = 0x0204;
const TAG_COMPARTMENT_KIND: u16 = 0x0200;
const TAG_COMPARTMENT_NONCE: u16 = 0x0203;
const TAG_COMPARTMENT_VERSION: u16 = 0x0201;
const TAG_FORMAT_VERSION: u16 = 0x0002;
const TAG_KDF_ALGORITHM: u16 = 0x0102;
const TAG_KDF_ITERATIONS: u16 = 0x0105;
const TAG_KDF_MEMORY_KIB: u16 = 0x0104;
const TAG_KDF_PARALLELISM: u16 = 0x0106;
const TAG_KDF_SALT: u16 = 0x0107;
const TAG_KDF_VERSION: u16 = 0x0103;
const TAG_MAGIC: u16 = 0x0001;
const TAG_OWNER_AUTH_PUBLIC_KEY: u16 = 0x0003;
const TAG_SLOT_ID: u16 = 0x0100;
const TAG_SLOT_ROLE: u16 = 0x0101;
const TAG_STATE_SIGNATURE_ALGORITHM: u16 = 0x0500;
const TAG_VIEW_ENABLED: u16 = 0x0010;
const TAG_VIEW_ROLE: u16 = 0x0011;
const TAG_VIEW_SLOT_ID: u16 = 0x0012;
const TAG_WRAP_ALGORITHM: u16 = 0x0108;
const TAG_WRAP_CIPHERTEXT: u16 = 0x010A;
const TAG_WRAP_NONCE: u16 = 0x0109;
/// Builds the normative OWNER state-signature transcript for one validated V2 envelope.
pub(crate) fn state_transcript_v2(envelope: &crate::KspWalletEnvelopeV2) -> std::vec::Vec<u8> {
let mut output = start(crate::KSPWALLET_V2_STATE_TRANSCRIPT_DOMAIN);
push_common(&mut output, envelope);
push_bool(&mut output, TAG_VIEW_ENABLED, envelope.view_descriptor().enabled());
push_u8(&mut output, TAG_VIEW_ROLE, crate::WalletKeySlotRoleV2::View.wire_id());
match envelope.view_descriptor().slot_id() {
std::option::Option::Some(slot_id) => push_bytes(&mut output, TAG_VIEW_SLOT_ID, slot_id),
std::option::Option::None => push_bytes(&mut output, TAG_VIEW_SLOT_ID, &[]),
}
push_slot(&mut output, envelope.owner_slot(), true);
push_compartment(&mut output, envelope.owner_control(), true);
push_compartment(&mut output, envelope.metadata(), true);
push_compartment(&mut output, envelope.secret(), true);
push_u8(&mut output, TAG_STATE_SIGNATURE_ALGORITHM, envelope.state_signature().algorithm().wire_id());
return output;
}
/// Builds the normative wrapping AAD for one validated V2 OWNER or VIEW key slot.
pub(crate) fn slot_aad_v2(envelope: &crate::KspWalletEnvelopeV2, slot: &crate::WalletKeySlotV2) -> std::vec::Vec<u8> {
let domain = match slot.role() {
crate::WalletKeySlotRoleV2::Owner => crate::KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN,
crate::WalletKeySlotRoleV2::View => crate::KSPWALLET_V2_VIEW_SLOT_AAD_DOMAIN,
};
let mut output = start(domain);
push_common(&mut output, envelope);
push_slot(&mut output, slot, false);
return output;
}
/// Builds the normative AEAD AAD for one validated V2 encrypted compartment.
pub(crate) fn compartment_aad_v2(envelope: &crate::KspWalletEnvelopeV2, compartment: &crate::WalletEncryptedCompartmentV2) -> std::vec::Vec<u8> {
let domain = match compartment.kind() {
crate::WalletCompartmentKindV2::OwnerControl => crate::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN,
crate::WalletCompartmentKindV2::Metadata => crate::KSPWALLET_V2_METADATA_AAD_DOMAIN,
crate::WalletCompartmentKindV2::Secret => crate::KSPWALLET_V2_SECRET_AAD_DOMAIN,
};
let mut output = start(domain);
push_common(&mut output, envelope);
push_compartment(&mut output, compartment, false);
return output;
}
fn start(domain: &[u8]) -> std::vec::Vec<u8> {
let mut output = std::vec::Vec::with_capacity(512);
output.extend_from_slice(domain);
output.push(0);
return output;
}
fn push_bool(output: &mut std::vec::Vec<u8>, tag: u16, value: bool) {
let byte = if value { 1_u8 } else { 0_u8 };
push_bytes(output, tag, &[byte]);
return;
}
fn push_bytes(output: &mut std::vec::Vec<u8>, tag: u16, value: &[u8]) {
output.extend_from_slice(tag.to_be_bytes().as_slice());
let length = value.len() as u64;
output.extend_from_slice(length.to_be_bytes().as_slice());
output.extend_from_slice(value);
return;
}
fn push_common(output: &mut std::vec::Vec<u8>, envelope: &crate::KspWalletEnvelopeV2) {
push_bytes(output, TAG_MAGIC, crate::KSPWALLET_MAGIC.as_bytes());
push_u32(output, TAG_FORMAT_VERSION, envelope.format_version());
push_bytes(output, TAG_OWNER_AUTH_PUBLIC_KEY, envelope.owner_auth_public_key());
return;
}
fn push_compartment(output: &mut std::vec::Vec<u8>, compartment: &crate::WalletEncryptedCompartmentV2, include_ciphertext: bool) {
push_u8(output, TAG_COMPARTMENT_KIND, compartment.kind().wire_id());
push_u32(output, TAG_COMPARTMENT_VERSION, compartment.payload_version());
push_u8(output, TAG_COMPARTMENT_ALGORITHM, compartment.algorithm().wire_id());
if include_ciphertext {
push_bytes(output, TAG_COMPARTMENT_NONCE, compartment.nonce());
push_bytes(output, TAG_COMPARTMENT_CIPHERTEXT, compartment.ciphertext());
}
return;
}
fn push_slot(output: &mut std::vec::Vec<u8>, slot: &crate::WalletKeySlotV2, include_wrap_payload: bool) {
push_bytes(output, TAG_SLOT_ID, slot.slot_id());
push_u8(output, TAG_SLOT_ROLE, slot.role().wire_id());
push_u8(output, TAG_KDF_ALGORITHM, slot.kdf().algorithm().wire_id());
push_u32(output, TAG_KDF_VERSION, slot.kdf().version());
push_u32(output, TAG_KDF_MEMORY_KIB, slot.kdf().memory_kib());
push_u32(output, TAG_KDF_ITERATIONS, slot.kdf().iterations());
push_u32(output, TAG_KDF_PARALLELISM, slot.kdf().parallelism());
push_bytes(output, TAG_KDF_SALT, slot.kdf().salt());
push_u8(output, TAG_WRAP_ALGORITHM, slot.wrap().algorithm().wire_id());
if include_wrap_payload {
push_bytes(output, TAG_WRAP_NONCE, slot.wrap().nonce());
push_bytes(output, TAG_WRAP_CIPHERTEXT, slot.wrap().ciphertext());
}
return;
}
fn push_u32(output: &mut std::vec::Vec<u8>, tag: u16, value: u32) {
push_bytes(output, tag, value.to_be_bytes().as_slice());
return;
}
fn push_u8(output: &mut std::vec::Vec<u8>, tag: u16, value: u8) {
push_bytes(output, tag, &[value]);
return;
}
#[cfg(test)]
#[path = "../unit_tests/transcript_v2.rs"]
mod tests;

File diff suppressed because it is too large Load Diff