// file: crates/ksp-wallet-lib/src/wire_v2.rs // version: 3 //! Strict native `.kspwallet` V2 binary wire envelope. const AEAD_ALGORITHM_XCHACHA20_POLY1305: u8 = 1; const COMPARTMENT_KIND_METADATA: u8 = 2; const COMPARTMENT_KIND_OWNER_CONTROL: u8 = 1; const COMPARTMENT_KIND_SECRET: u8 = 3; const FORMAT_VERSION_WIRE_V2: u16 = 2; const HEADER_FLAG_KNOWN_MASK: u16 = HEADER_FLAG_VIEW_ENABLED; const HEADER_FLAG_VIEW_ENABLED: u16 = 0x0001; const KDF_ALGORITHM_ARGON2ID: u8 = 1; const KEY_SLOT_ROLE_OWNER: u8 = 1; const KEY_SLOT_ROLE_VIEW: u8 = 2; const STATE_SIGNATURE_ALGORITHM_ED25519: u8 = 1; /// Password KDF supported by `.kspwallet` V2 key slots. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WalletKdfAlgorithmV2 { /// Argon2id version 19. Argon2id, } impl WalletKdfAlgorithmV2 { /// Returns the normative one-byte V2 wire identifier. #[must_use] pub const fn wire_id(self) -> u8 { return KDF_ALGORITHM_ARGON2ID; } } /// Authenticated-encryption algorithm supported by `.kspwallet` V2. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WalletAeadAlgorithmV2 { /// XChaCha20-Poly1305 with a 24-byte nonce. XChaCha20Poly1305, } impl WalletAeadAlgorithmV2 { /// Returns the normative one-byte V2 wire identifier. #[must_use] pub const fn wire_id(self) -> u8 { return AEAD_ALGORITHM_XCHACHA20_POLY1305; } } /// State-signature algorithm supported by `.kspwallet` V2. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WalletStateSignatureAlgorithmV2 { /// Ed25519 detached signature. Ed25519, } impl WalletStateSignatureAlgorithmV2 { /// Returns the normative one-byte V2 wire identifier. #[must_use] pub const fn wire_id(self) -> u8 { return STATE_SIGNATURE_ALGORITHM_ED25519; } } /// Role of one V2 password-protected key slot. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WalletKeySlotRoleV2 { /// OWNER slot giving access to the OWNER/root capability. Owner, /// VIEW slot giving access only to protected metadata and self-service VIEW password rotation. View, } impl WalletKeySlotRoleV2 { /// Returns the normative one-byte V2 wire identifier. #[must_use] pub const fn wire_id(self) -> u8 { return match self { Self::Owner => KEY_SLOT_ROLE_OWNER, Self::View => KEY_SLOT_ROLE_VIEW, }; } } /// Protected V2 payload compartment kind. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WalletCompartmentKindV2 { /// OWNER control material, including the format-administration private material. OwnerControl, /// VIEW/OWNER-readable Solana Pubkey, alias and notes. Metadata, /// OWNER-only Solana keypair material. Secret, } impl WalletCompartmentKindV2 { /// Returns the normative one-byte V2 wire identifier. #[must_use] pub const fn wire_id(self) -> u8 { return match self { Self::OwnerControl => COMPARTMENT_KIND_OWNER_CONTROL, Self::Metadata => COMPARTMENT_KIND_METADATA, Self::Secret => COMPARTMENT_KIND_SECRET, }; } } /// Parsed Argon2id parameters embedded in one V2 key slot. #[derive(Clone, Eq, PartialEq)] pub struct WalletKdfParametersV2 { algorithm: WalletKdfAlgorithmV2, version: u32, memory_kib: u32, iterations: u32, parallelism: u32, salt: std::vec::Vec, } impl WalletKdfParametersV2 { /// Creates one crate-owned V2 KDF value after the caller has selected the serialized parameters. pub(crate) fn new_internal(version: u32, memory_kib: u32, iterations: u32, parallelism: u32, salt: std::vec::Vec) -> Self { return Self { algorithm: WalletKdfAlgorithmV2::Argon2id, version, memory_kib, iterations, parallelism, salt }; } /// Creates one V2 KDF value using the release-calibrated default creation profile. pub(crate) fn new_creation(salt: std::vec::Vec) -> Self { return Self::new_internal( crate::KSPWALLET_V2_ARGON2_VERSION, crate::KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB, crate::KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS, crate::KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM, salt, ); } /// Returns the KDF algorithm. #[must_use] pub const fn algorithm(&self) -> WalletKdfAlgorithmV2 { return self.algorithm; } /// Returns the serialized Argon2 version. #[must_use] pub const fn version(&self) -> u32 { return self.version; } /// Returns the Argon2 memory cost in KiB. #[must_use] pub const fn memory_kib(&self) -> u32 { return self.memory_kib; } /// Returns the Argon2 iteration/time cost. #[must_use] pub const fn iterations(&self) -> u32 { return self.iterations; } /// Returns the Argon2 parallelism cost. #[must_use] pub const fn parallelism(&self) -> u32 { return self.parallelism; } /// Returns the public KDF salt bytes. #[must_use] pub fn salt(&self) -> &[u8] { return self.salt.as_slice(); } } impl std::fmt::Debug for WalletKdfParametersV2 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("WalletKdfParametersV2") .field("algorithm", &self.algorithm) .field("version", &self.version) .field("memory_kib", &self.memory_kib) .field("iterations", &self.iterations) .field("parallelism", &self.parallelism) .field("salt_bytes", &self.salt.len()) .finish(); } } /// Parsed AEAD wrapping payload embedded in one V2 key slot. #[derive(Clone, Eq, PartialEq)] pub struct WalletKeyWrapV2 { algorithm: WalletAeadAlgorithmV2, nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec, } impl WalletKeyWrapV2 { /// Creates one crate-owned V2 wrapping value. pub(crate) fn new_internal(nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec) -> Self { return Self { algorithm: WalletAeadAlgorithmV2::XChaCha20Poly1305, nonce, ciphertext }; } /// Returns the wrapping AEAD algorithm. #[must_use] pub const fn algorithm(&self) -> WalletAeadAlgorithmV2 { return self.algorithm; } /// Returns the public AEAD nonce. #[must_use] pub const fn nonce(&self) -> &[u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES] { return &self.nonce; } /// Returns the wrapped-key ciphertext bytes. #[must_use] pub fn ciphertext(&self) -> &[u8] { return self.ciphertext.as_slice(); } } impl std::fmt::Debug for WalletKeyWrapV2 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("WalletKeyWrapV2") .field("algorithm", &self.algorithm) .field("nonce_bytes", &self.nonce.len()) .field("ciphertext_bytes", &self.ciphertext.len()) .finish(); } } /// One validated OWNER or VIEW key slot from a V2 envelope. #[derive(Clone, Eq, PartialEq)] pub struct WalletKeySlotV2 { slot_id: [u8; crate::KSPWALLET_V2_SLOT_ID_BYTES], role: WalletKeySlotRoleV2, kdf: WalletKdfParametersV2, wrap: WalletKeyWrapV2, } impl WalletKeySlotV2 { /// Creates one crate-owned V2 key slot. pub(crate) fn new_internal( slot_id: [u8; crate::KSPWALLET_V2_SLOT_ID_BYTES], role: WalletKeySlotRoleV2, kdf: WalletKdfParametersV2, wrap: WalletKeyWrapV2, ) -> Self { return Self { slot_id, role, kdf, wrap }; } /// Returns the stable 16-byte slot identifier. #[must_use] pub const fn slot_id(&self) -> &[u8; crate::KSPWALLET_V2_SLOT_ID_BYTES] { return &self.slot_id; } /// Returns the key-slot role. #[must_use] pub const fn role(&self) -> WalletKeySlotRoleV2 { return self.role; } /// Returns the serialized password-KDF parameters. #[must_use] pub const fn kdf(&self) -> &WalletKdfParametersV2 { return &self.kdf; } /// Returns the wrapped capability payload. #[must_use] pub const fn wrap(&self) -> &WalletKeyWrapV2 { return &self.wrap; } } impl std::fmt::Debug for WalletKeySlotV2 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("WalletKeySlotV2") .field("slot_id_bytes", &self.slot_id.len()) .field("role", &self.role) .field("kdf", &self.kdf) .field("wrap", &self.wrap) .finish(); } } /// OWNER-signed descriptor binding the optional self-service VIEW slot in V2. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WalletViewDescriptorV2 { enabled: bool, slot_id: std::option::Option<[u8; crate::KSPWALLET_V2_SLOT_ID_BYTES]>, } impl WalletViewDescriptorV2 { /// Creates a disabled V2 VIEW descriptor. pub(crate) const fn disabled() -> Self { return Self { enabled: false, slot_id: std::option::Option::None }; } /// Creates an enabled V2 VIEW descriptor for one slot identifier. pub(crate) const fn enabled_for_slot(slot_id: [u8; crate::KSPWALLET_V2_SLOT_ID_BYTES]) -> Self { return Self { enabled: true, slot_id: std::option::Option::Some(slot_id) }; } /// Reports whether a VIEW slot is enabled. #[must_use] pub const fn enabled(&self) -> bool { return self.enabled; } /// Returns the signed VIEW slot identifier when VIEW is enabled. #[must_use] pub const fn slot_id(&self) -> std::option::Option<&[u8; crate::KSPWALLET_V2_SLOT_ID_BYTES]> { return self.slot_id.as_ref(); } } /// One validated encrypted compartment from a V2 envelope. #[derive(Clone, Eq, PartialEq)] pub struct WalletEncryptedCompartmentV2 { kind: WalletCompartmentKindV2, payload_version: u32, algorithm: WalletAeadAlgorithmV2, nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec, } impl WalletEncryptedCompartmentV2 { /// Creates one crate-owned V2 encrypted compartment. pub(crate) fn new_internal( kind: WalletCompartmentKindV2, payload_version: u32, nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec, ) -> Self { return Self { kind, payload_version, algorithm: WalletAeadAlgorithmV2::XChaCha20Poly1305, nonce, ciphertext }; } /// Returns the compartment kind. #[must_use] pub const fn kind(&self) -> WalletCompartmentKindV2 { return self.kind; } /// Returns the protected payload version for this compartment. #[must_use] pub const fn payload_version(&self) -> u32 { return self.payload_version; } /// Returns the compartment AEAD algorithm. #[must_use] pub const fn algorithm(&self) -> WalletAeadAlgorithmV2 { return self.algorithm; } /// Returns the public AEAD nonce. #[must_use] pub const fn nonce(&self) -> &[u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES] { return &self.nonce; } /// Returns the encrypted payload bytes. #[must_use] pub fn ciphertext(&self) -> &[u8] { return self.ciphertext.as_slice(); } } impl std::fmt::Debug for WalletEncryptedCompartmentV2 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("WalletEncryptedCompartmentV2") .field("kind", &self.kind) .field("payload_version", &self.payload_version) .field("algorithm", &self.algorithm) .field("nonce_bytes", &self.nonce.len()) .field("ciphertext_bytes", &self.ciphertext.len()) .finish(); } } /// Parsed detached OWNER state signature from a V2 envelope. #[derive(Clone, Eq, PartialEq)] pub struct WalletStateSignatureV2 { algorithm: WalletStateSignatureAlgorithmV2, signature: [u8; crate::KSPWALLET_V2_ED25519_SIGNATURE_BYTES], } impl WalletStateSignatureV2 { /// Creates one crate-owned V2 state signature. pub(crate) const fn new_internal(signature: [u8; crate::KSPWALLET_V2_ED25519_SIGNATURE_BYTES]) -> Self { return Self { algorithm: WalletStateSignatureAlgorithmV2::Ed25519, signature }; } /// Returns the state-signature algorithm. #[must_use] pub const fn algorithm(&self) -> WalletStateSignatureAlgorithmV2 { return self.algorithm; } /// Returns the detached signature bytes. #[must_use] pub const fn signature(&self) -> &[u8; crate::KSPWALLET_V2_ED25519_SIGNATURE_BYTES] { return &self.signature; } } impl std::fmt::Debug for WalletStateSignatureV2 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("WalletStateSignatureV2") .field("algorithm", &self.algorithm) .field("signature_bytes", &self.signature.len()) .finish(); } } /// Strict semantic representation of one parsed native `.kspwallet` V2 binary envelope. #[derive(Clone, Eq, PartialEq)] pub struct KspWalletEnvelopeV2 { owner_auth_public_key: [u8; crate::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES], view_descriptor: WalletViewDescriptorV2, owner_slot: WalletKeySlotV2, view_slot: std::option::Option, owner_control: WalletEncryptedCompartmentV2, metadata: WalletEncryptedCompartmentV2, secret: WalletEncryptedCompartmentV2, state_signature: WalletStateSignatureV2, } impl KspWalletEnvelopeV2 { /// Creates one crate-owned V2 envelope from already staged semantic fields. pub(crate) fn new_internal( owner_auth_public_key: [u8; crate::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES], view_descriptor: WalletViewDescriptorV2, owner_slot: WalletKeySlotV2, view_slot: std::option::Option, owner_control: WalletEncryptedCompartmentV2, metadata: WalletEncryptedCompartmentV2, secret: WalletEncryptedCompartmentV2, state_signature: WalletStateSignatureV2, ) -> Self { return Self { owner_auth_public_key, view_descriptor, owner_slot, view_slot, owner_control, metadata, secret, state_signature, }; } /// Parses and structurally validates one complete canonical `.kspwallet` V2 binary document. /// /// This codec validates V2 framing, big-endian integer encoding, algorithm/role/kind identifiers, structural bounds, VIEW invariants and exact /// consumption of the declared document length. Cryptographic authentication/decryption is deliberately owned by later Wallet orchestration. pub fn parse_binary(source: &[u8]) -> ksp_core_lib::Result { if source.len() > crate::KSPWALLET_MAX_FILE_BYTES { return std::result::Result::Err(format_error("Wallet document exceeds the V2 maximum size", "document")); } let mut reader = BinaryReader::new(source); let magic = match reader.read_fixed::<{ crate::KSPWALLET_MAGIC.len() }>("magic") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if magic.as_slice() != crate::KSPWALLET_MAGIC.as_bytes() { return std::result::Result::Err(format_error("Wallet magic is invalid", "magic")); } let format_version = match reader.read_u16("format_version") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if format_version != FORMAT_VERSION_WIRE_V2 { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED, "Wallet format version is not supported") .with_context("format_version", format_version.to_string()), ); } let declared_length = match reader.read_u32("document_length") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let declared_length = match usize::try_from(declared_length) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(format_error("Wallet declared length cannot be represented", "document_length")), }; if declared_length != source.len() { return std::result::Result::Err(format_error("Wallet declared length does not match the input length", "document_length")); } let flags = match reader.read_u16("flags") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if flags & !HEADER_FLAG_KNOWN_MASK != 0 { return std::result::Result::Err(format_error("Wallet V2 header contains unsupported flag bits", "flags")); } let owner_auth_public_key = match reader.read_fixed::<{ crate::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES }>("owner_auth_public_key") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let view_enabled = flags & HEADER_FLAG_VIEW_ENABLED != 0; let view_descriptor = if view_enabled { let slot_id = match reader.read_fixed::<{ crate::KSPWALLET_V2_SLOT_ID_BYTES }>("view_descriptor.slot_id") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; WalletViewDescriptorV2::enabled_for_slot(slot_id) } else { WalletViewDescriptorV2::disabled() }; let owner_slot = match parse_key_slot(&mut reader, WalletKeySlotRoleV2::Owner) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let view_slot = if view_enabled { match parse_key_slot(&mut reader, WalletKeySlotRoleV2::View) { std::result::Result::Ok(value) => std::option::Option::Some(value), std::result::Result::Err(error) => return std::result::Result::Err(error), } } else { std::option::Option::None }; if let (std::option::Option::Some(descriptor_slot_id), std::option::Option::Some(parsed_view_slot)) = (view_descriptor.slot_id(), view_slot.as_ref()) && descriptor_slot_id != parsed_view_slot.slot_id() { return std::result::Result::Err(format_error("VIEW descriptor slot_id does not match the VIEW key slot", "view_descriptor.slot_id")); } let owner_control = match parse_compartment(&mut reader, WalletCompartmentKindV2::OwnerControl) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let metadata = match parse_compartment(&mut reader, WalletCompartmentKindV2::Metadata) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let secret = match parse_compartment(&mut reader, WalletCompartmentKindV2::Secret) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let state_signature = match parse_state_signature(&mut reader) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if !reader.is_finished() { return std::result::Result::Err(format_error("Wallet V2 document contains trailing bytes", "document")); } let envelope = Self::new_internal(owner_auth_public_key, view_descriptor, owner_slot, view_slot, owner_control, metadata, secret, state_signature); ksp_logging_lib::trace!( target: crate::TRACING_TARGET, operation = "wallet_wire_parse", format_version = envelope.format_version(), view_enabled = envelope.view_descriptor().enabled(), "native wallet V2 binary envelope parsed" ); return std::result::Result::Ok(envelope); } /// Serializes this semantic V2 envelope to the canonical KSP binary wire. pub fn to_binary_bytes(&self) -> ksp_core_lib::Result> { let validation = validate_envelope(self); if let std::result::Result::Err(error) = validation { return std::result::Result::Err(error); } let mut output = std::vec::Vec::with_capacity(1024); output.extend_from_slice(crate::KSPWALLET_MAGIC.as_bytes()); output.extend_from_slice(FORMAT_VERSION_WIRE_V2.to_be_bytes().as_slice()); let length_offset = output.len(); output.extend_from_slice(0_u32.to_be_bytes().as_slice()); let flags = if self.view_descriptor.enabled() { HEADER_FLAG_VIEW_ENABLED } else { 0 }; output.extend_from_slice(flags.to_be_bytes().as_slice()); output.extend_from_slice(self.owner_auth_public_key()); if let std::option::Option::Some(slot_id) = self.view_descriptor.slot_id() { output.extend_from_slice(slot_id); } encode_key_slot(&mut output, self.owner_slot()); if let std::option::Option::Some(view_slot) = self.view_slot() { encode_key_slot(&mut output, view_slot); } encode_compartment(&mut output, self.owner_control()); encode_compartment(&mut output, self.metadata()); encode_compartment(&mut output, self.secret()); output.push(self.state_signature.algorithm().wire_id()); output.extend_from_slice(self.state_signature.signature()); if output.len() > crate::KSPWALLET_MAX_FILE_BYTES { return std::result::Result::Err(format_error("Wallet document exceeds the V2 maximum size", "document")); } let total_length = match u32::try_from(output.len()) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(format_error("Wallet V2 document length exceeds the wire range", "document")), }; let length_end = length_offset + std::mem::size_of::(); output[length_offset..length_end].copy_from_slice(total_length.to_be_bytes().as_slice()); return std::result::Result::Ok(output); } /// Returns the fixed native format version. #[must_use] pub const fn format_version(&self) -> u32 { return crate::KSPWALLET_FORMAT_VERSION_V2; } /// Returns the embedded Ed25519 public key that verifies OWNER-controlled state. #[must_use] pub const fn owner_auth_public_key(&self) -> &[u8; crate::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES] { return &self.owner_auth_public_key; } /// Returns the signed VIEW descriptor. #[must_use] pub const fn view_descriptor(&self) -> &WalletViewDescriptorV2 { return &self.view_descriptor; } /// Returns the mandatory OWNER key slot. #[must_use] pub const fn owner_slot(&self) -> &WalletKeySlotV2 { return &self.owner_slot; } /// Returns the optional VIEW key slot. #[must_use] pub const fn view_slot(&self) -> std::option::Option<&WalletKeySlotV2> { return self.view_slot.as_ref(); } /// Returns the OWNER-control encrypted compartment. #[must_use] pub const fn owner_control(&self) -> &WalletEncryptedCompartmentV2 { return &self.owner_control; } /// Returns the protected metadata encrypted compartment. #[must_use] pub const fn metadata(&self) -> &WalletEncryptedCompartmentV2 { return &self.metadata; } /// Returns the OWNER-only secret encrypted compartment. #[must_use] pub const fn secret(&self) -> &WalletEncryptedCompartmentV2 { return &self.secret; } /// Returns the detached OWNER state signature. #[must_use] pub const fn state_signature(&self) -> &WalletStateSignatureV2 { return &self.state_signature; } /// Builds the deterministic binary transcript covered by the V2 OWNER state signature. #[must_use] pub fn state_transcript(&self) -> std::vec::Vec { return crate::state_transcript_v2(self); } /// Builds the deterministic AEAD AAD for the V2 OWNER key slot. #[must_use] pub fn owner_slot_aad(&self) -> std::vec::Vec { return crate::slot_aad_v2(self, self.owner_slot()); } /// Builds the deterministic AEAD AAD for the optional V2 VIEW key slot. #[must_use] pub fn view_slot_aad(&self) -> std::option::Option> { return match self.view_slot() { std::option::Option::Some(slot) => std::option::Option::Some(crate::slot_aad_v2(self, slot)), std::option::Option::None => std::option::Option::None, }; } /// Builds deterministic AEAD AAD for one V2 encrypted compartment. #[must_use] pub fn compartment_aad(&self, kind: WalletCompartmentKindV2) -> std::vec::Vec { let compartment = match kind { WalletCompartmentKindV2::OwnerControl => self.owner_control(), WalletCompartmentKindV2::Metadata => self.metadata(), WalletCompartmentKindV2::Secret => self.secret(), }; return crate::compartment_aad_v2(self, compartment); } } impl std::fmt::Debug for KspWalletEnvelopeV2 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("KspWalletEnvelopeV2") .field("format_version", &crate::KSPWALLET_FORMAT_VERSION_V2) .field("owner_auth_public_key_bytes", &self.owner_auth_public_key.len()) .field("view_descriptor", &self.view_descriptor) .field("owner_slot", &self.owner_slot) .field("view_slot", &self.view_slot) .field("owner_control", &self.owner_control) .field("metadata", &self.metadata) .field("secret", &self.secret) .field("state_signature", &self.state_signature) .finish(); } } fn parse_key_slot(reader: &mut BinaryReader<'_>, expected_role: WalletKeySlotRoleV2) -> ksp_core_lib::Result { let role_id = match reader.read_u8("key_slot.role") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let role = match role_id { KEY_SLOT_ROLE_OWNER => WalletKeySlotRoleV2::Owner, KEY_SLOT_ROLE_VIEW => WalletKeySlotRoleV2::View, _ => return std::result::Result::Err(format_error("Wallet V2 key slot role is unsupported", "key_slot.role")), }; if role != expected_role { return std::result::Result::Err(format_error("Wallet V2 key slots are not in canonical OWNER/VIEW order", "key_slot.role")); } let slot_id = match reader.read_fixed::<{ crate::KSPWALLET_V2_SLOT_ID_BYTES }>("key_slot.slot_id") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let kdf_algorithm = match reader.read_u8("key_slot.kdf.algorithm") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if kdf_algorithm != KDF_ALGORITHM_ARGON2ID { return std::result::Result::Err(crypto_parameter_error("Wallet V2 key slots require Argon2id", "key_slot.kdf.algorithm")); } let version = match reader.read_u32("key_slot.kdf.version") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let memory_kib = match reader.read_u32("key_slot.kdf.memory_kib") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let iterations = match reader.read_u32("key_slot.kdf.iterations") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let parallelism = match reader.read_u32("key_slot.kdf.parallelism") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let salt_length = match reader.read_u8("key_slot.kdf.salt_length") { std::result::Result::Ok(value) => usize::from(value), std::result::Result::Err(error) => return std::result::Result::Err(error), }; let salt = match reader.read_vec(salt_length, "key_slot.kdf.salt") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let kdf = WalletKdfParametersV2::new_internal(version, memory_kib, iterations, parallelism, salt); let kdf_validation = validate_kdf(&kdf); if let std::result::Result::Err(error) = kdf_validation { return std::result::Result::Err(error); } let wrap_algorithm = match reader.read_u8("key_slot.wrap.algorithm") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if wrap_algorithm != AEAD_ALGORITHM_XCHACHA20_POLY1305 { return std::result::Result::Err(crypto_parameter_error("Wallet V2 key wrapping requires XChaCha20-Poly1305", "key_slot.wrap.algorithm")); } let nonce = match reader.read_fixed::<{ crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES }>("key_slot.wrap.nonce") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let ciphertext_length = match reader.read_u16("key_slot.wrap.ciphertext_length") { std::result::Result::Ok(value) => usize::from(value), std::result::Result::Err(error) => return std::result::Result::Err(error), }; let ciphertext = match reader.read_vec(ciphertext_length, "key_slot.wrap.ciphertext") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let wrap = WalletKeyWrapV2::new_internal(nonce, ciphertext); let wrap_validation = validate_wrap(&wrap); if let std::result::Result::Err(error) = wrap_validation { return std::result::Result::Err(error); } return std::result::Result::Ok(WalletKeySlotV2::new_internal(slot_id, role, kdf, wrap)); } fn parse_compartment(reader: &mut BinaryReader<'_>, expected_kind: WalletCompartmentKindV2) -> ksp_core_lib::Result { let kind_id = match reader.read_u8("compartment.kind") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let kind = match kind_id { COMPARTMENT_KIND_OWNER_CONTROL => WalletCompartmentKindV2::OwnerControl, COMPARTMENT_KIND_METADATA => WalletCompartmentKindV2::Metadata, COMPARTMENT_KIND_SECRET => WalletCompartmentKindV2::Secret, _ => return std::result::Result::Err(format_error("Wallet V2 compartment kind is unsupported", "compartment.kind")), }; if kind != expected_kind { return std::result::Result::Err(format_error("Wallet V2 compartments are not in canonical order", "compartment.kind")); } let payload_version = match reader.read_u32("compartment.payload_version") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let algorithm = match reader.read_u8("compartment.algorithm") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if algorithm != AEAD_ALGORITHM_XCHACHA20_POLY1305 { return std::result::Result::Err(crypto_parameter_error("Wallet V2 compartments require XChaCha20-Poly1305", "compartment.algorithm")); } let nonce = match reader.read_fixed::<{ crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES }>("compartment.nonce") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let ciphertext_length = match reader.read_u32("compartment.ciphertext_length") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let ciphertext_length = match usize::try_from(ciphertext_length) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(format_error("Wallet compartment length cannot be represented", "compartment.ciphertext_length")); }, }; let ciphertext = match reader.read_vec(ciphertext_length, "compartment.ciphertext") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let compartment = WalletEncryptedCompartmentV2::new_internal(kind, payload_version, nonce, ciphertext); let validation = validate_compartment(&compartment); if let std::result::Result::Err(error) = validation { return std::result::Result::Err(error); } return std::result::Result::Ok(compartment); } fn parse_state_signature(reader: &mut BinaryReader<'_>) -> ksp_core_lib::Result { let algorithm = match reader.read_u8("state_signature.algorithm") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if algorithm != STATE_SIGNATURE_ALGORITHM_ED25519 { return std::result::Result::Err(crypto_parameter_error("Wallet V2 state authentication requires Ed25519", "state_signature.algorithm")); } let signature = match reader.read_fixed::<{ crate::KSPWALLET_V2_ED25519_SIGNATURE_BYTES }>("state_signature.signature") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(WalletStateSignatureV2::new_internal(signature)); } fn validate_envelope(envelope: &KspWalletEnvelopeV2) -> ksp_core_lib::Result<()> { if envelope.owner_slot.role() != WalletKeySlotRoleV2::Owner { return std::result::Result::Err(format_error("Wallet V2 requires one canonical OWNER key slot", "owner_slot.role")); } let owner_validation = validate_slot(envelope.owner_slot()); if let std::result::Result::Err(error) = owner_validation { return std::result::Result::Err(error); } match (envelope.view_descriptor.enabled(), envelope.view_descriptor.slot_id(), envelope.view_slot()) { (false, std::option::Option::None, std::option::Option::None) => {}, (true, std::option::Option::Some(descriptor_slot_id), std::option::Option::Some(view_slot)) => { if view_slot.role() != WalletKeySlotRoleV2::View || descriptor_slot_id != view_slot.slot_id() { return std::result::Result::Err(format_error("Wallet V2 VIEW descriptor and VIEW key slot do not match", "view_descriptor")); } let view_validation = validate_slot(view_slot); if let std::result::Result::Err(error) = view_validation { return std::result::Result::Err(error); } }, _ => return std::result::Result::Err(format_error("Wallet V2 VIEW descriptor shape is invalid", "view_descriptor")), } for compartment in [envelope.owner_control(), envelope.metadata(), envelope.secret()] { let compartment_validation = validate_compartment(compartment); if let std::result::Result::Err(error) = compartment_validation { return std::result::Result::Err(error); } } return std::result::Result::Ok(()); } fn validate_slot(slot: &WalletKeySlotV2) -> ksp_core_lib::Result<()> { let kdf_validation = validate_kdf(slot.kdf()); if let std::result::Result::Err(error) = kdf_validation { return std::result::Result::Err(error); } return validate_wrap(slot.wrap()); } fn validate_kdf(kdf: &WalletKdfParametersV2) -> ksp_core_lib::Result<()> { if kdf.algorithm() != WalletKdfAlgorithmV2::Argon2id || kdf.version() != crate::KSPWALLET_V2_ARGON2_VERSION { return std::result::Result::Err(crypto_parameter_error("Wallet V2 requires Argon2id version 19", "key_slot.kdf")); } if kdf.memory_kib() == 0 || kdf.memory_kib() > crate::KSPWALLET_V2_MAX_ARGON2_MEMORY_KIB { return std::result::Result::Err(crypto_parameter_error("Wallet Argon2 memory_kib is outside the V2 structural range", "key_slot.kdf.memory_kib")); } if kdf.iterations() == 0 || kdf.iterations() > crate::KSPWALLET_V2_MAX_ARGON2_ITERATIONS { return std::result::Result::Err(crypto_parameter_error("Wallet Argon2 iterations is outside the V2 structural range", "key_slot.kdf.iterations")); } if kdf.parallelism() == 0 || kdf.parallelism() > crate::KSPWALLET_V2_MAX_ARGON2_PARALLELISM { return std::result::Result::Err(crypto_parameter_error("Wallet Argon2 parallelism is outside the V2 structural range", "key_slot.kdf.parallelism")); } if kdf.memory_kib() < kdf.parallelism() * 8 { return std::result::Result::Err(crypto_parameter_error( "Wallet Argon2 memory_kib must provide at least eight 1-KiB blocks per lane", "key_slot.kdf.memory_kib", )); } if kdf.salt().len() < crate::KSPWALLET_V2_MIN_KDF_SALT_BYTES || kdf.salt().len() > crate::KSPWALLET_V2_MAX_KDF_SALT_BYTES { return std::result::Result::Err(crypto_parameter_error("Wallet KDF salt length is outside the V2 range", "key_slot.kdf.salt")); } return std::result::Result::Ok(()); } fn validate_wrap(wrap: &WalletKeyWrapV2) -> ksp_core_lib::Result<()> { if wrap.algorithm() != WalletAeadAlgorithmV2::XChaCha20Poly1305 { return std::result::Result::Err(crypto_parameter_error("Wallet V2 key wrapping requires XChaCha20-Poly1305", "key_slot.wrap.algorithm")); } if wrap.ciphertext().len() < crate::KSPWALLET_V2_AEAD_TAG_BYTES || wrap.ciphertext().len() > crate::KSPWALLET_V2_MAX_KEY_WRAP_CIPHERTEXT_BYTES { return std::result::Result::Err(format_error("Wallet V2 key-wrap ciphertext length is invalid", "key_slot.wrap.ciphertext")); } return std::result::Result::Ok(()); } fn validate_compartment(compartment: &WalletEncryptedCompartmentV2) -> ksp_core_lib::Result<()> { if compartment.payload_version() != crate::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION { return std::result::Result::Err(format_error("Wallet protected payload version is unsupported by V2", "compartment.payload_version")); } if compartment.algorithm() != WalletAeadAlgorithmV2::XChaCha20Poly1305 { return std::result::Result::Err(crypto_parameter_error("Wallet V2 compartments require XChaCha20-Poly1305", "compartment.algorithm")); } let maximum = match compartment.kind() { WalletCompartmentKindV2::OwnerControl => crate::KSPWALLET_V2_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES, WalletCompartmentKindV2::Metadata => crate::KSPWALLET_V2_MAX_METADATA_CIPHERTEXT_BYTES, WalletCompartmentKindV2::Secret => crate::KSPWALLET_V2_MAX_SECRET_CIPHERTEXT_BYTES, }; if compartment.ciphertext().len() < crate::KSPWALLET_V2_AEAD_TAG_BYTES || compartment.ciphertext().len() > maximum { return std::result::Result::Err(format_error("Wallet V2 compartment ciphertext length is invalid", "compartment.ciphertext")); } return std::result::Result::Ok(()); } fn encode_key_slot(output: &mut std::vec::Vec, slot: &WalletKeySlotV2) { output.push(slot.role().wire_id()); output.extend_from_slice(slot.slot_id()); output.push(slot.kdf().algorithm().wire_id()); output.extend_from_slice(slot.kdf().version().to_be_bytes().as_slice()); output.extend_from_slice(slot.kdf().memory_kib().to_be_bytes().as_slice()); output.extend_from_slice(slot.kdf().iterations().to_be_bytes().as_slice()); output.extend_from_slice(slot.kdf().parallelism().to_be_bytes().as_slice()); output.push(slot.kdf().salt().len() as u8); output.extend_from_slice(slot.kdf().salt()); output.push(slot.wrap().algorithm().wire_id()); output.extend_from_slice(slot.wrap().nonce()); output.extend_from_slice((slot.wrap().ciphertext().len() as u16).to_be_bytes().as_slice()); output.extend_from_slice(slot.wrap().ciphertext()); return; } fn encode_compartment(output: &mut std::vec::Vec, compartment: &WalletEncryptedCompartmentV2) { output.push(compartment.kind().wire_id()); output.extend_from_slice(compartment.payload_version().to_be_bytes().as_slice()); output.push(compartment.algorithm().wire_id()); output.extend_from_slice(compartment.nonce()); output.extend_from_slice((compartment.ciphertext().len() as u32).to_be_bytes().as_slice()); output.extend_from_slice(compartment.ciphertext()); return; } struct BinaryReader<'a> { source: &'a [u8], offset: usize, } impl<'a> BinaryReader<'a> { const fn new(source: &'a [u8]) -> Self { return Self { source, offset: 0 }; } fn read_u8(&mut self, field: &'static str) -> ksp_core_lib::Result { let bytes = match self.take(1, field) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(bytes[0]); } fn read_u16(&mut self, field: &'static str) -> ksp_core_lib::Result { let bytes = match self.read_fixed::<2>(field) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(u16::from_be_bytes(bytes)); } fn read_u32(&mut self, field: &'static str) -> ksp_core_lib::Result { let bytes = match self.read_fixed::<4>(field) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(u32::from_be_bytes(bytes)); } fn read_fixed(&mut self, field: &'static str) -> ksp_core_lib::Result<[u8; LENGTH]> { let bytes = match self.take(LENGTH, field) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let converted = <[u8; LENGTH]>::try_from(bytes); return match converted { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(_) => std::result::Result::Err(format_error("Wallet V2 fixed-length field cannot be converted", field)), }; } fn read_vec(&mut self, length: usize, field: &'static str) -> ksp_core_lib::Result> { let bytes = match self.take(length, field) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(bytes.to_vec()); } fn take(&mut self, length: usize, field: &'static str) -> ksp_core_lib::Result<&'a [u8]> { let end = match self.offset.checked_add(length) { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(format_error("Wallet V2 field length overflows the parser range", field)), }; if end > self.source.len() { return std::result::Result::Err(format_error("Wallet V2 document is truncated", field)); } let bytes = &self.source[self.offset..end]; self.offset = end; return std::result::Result::Ok(bytes); } const fn is_finished(&self) -> bool { return self.offset == self.source.len(); } } fn format_error(message: &'static str, field: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message).with_context("field", field); } fn crypto_parameter_error(message: &'static str, field: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID, message).with_context("field", field); } #[cfg(test)] #[path = "../unit_tests/wire_v2.rs"] mod tests;