// file: crates/ksp-wallet-lib/src/wire.rs // version: 7 //! Strict native `.kspwallet` V1 wire envelope. use base64::Engine; // rust-rules: trait-import /// Password KDF supported by `.kspwallet` V1 key slots. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WalletKdfAlgorithmV1 { /// Argon2id version 19. Argon2id, } impl WalletKdfAlgorithmV1 { /// Returns the normative wire identifier. #[must_use] pub const fn as_str(self) -> &'static str { return "argon2id"; } } /// Authenticated-encryption algorithm supported by `.kspwallet` V1. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WalletAeadAlgorithmV1 { /// XChaCha20-Poly1305 with a 24-byte nonce. XChaCha20Poly1305, } impl WalletAeadAlgorithmV1 { /// Returns the normative wire identifier. #[must_use] pub const fn as_str(self) -> &'static str { return "xchacha20-poly1305"; } } /// State-signature algorithm supported by `.kspwallet` V1. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WalletStateSignatureAlgorithmV1 { /// Ed25519 detached signature. Ed25519, } impl WalletStateSignatureAlgorithmV1 { /// Returns the normative wire identifier. #[must_use] pub const fn as_str(self) -> &'static str { return "ed25519"; } } /// Role of one V1 password-protected key slot. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WalletKeySlotRoleV1 { /// 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 WalletKeySlotRoleV1 { /// Returns the normative wire identifier. #[must_use] pub const fn as_str(self) -> &'static str { return match self { Self::Owner => "owner", Self::View => "view", }; } } /// Protected V1 payload compartment kind. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WalletCompartmentKindV1 { /// 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 WalletCompartmentKindV1 { /// Returns the stable transcript/AAD identifier. #[must_use] pub const fn as_str(self) -> &'static str { return match self { Self::OwnerControl => "owner-control", Self::Metadata => "metadata", Self::Secret => "secret", }; } } /// Parsed Argon2id parameters embedded in one V1 key slot. #[derive(Clone, Eq, PartialEq)] pub struct WalletKdfParametersV1 { algorithm: WalletKdfAlgorithmV1, version: u32, memory_kib: u32, iterations: u32, parallelism: u32, salt: std::vec::Vec, } impl WalletKdfParametersV1 { /// Creates a new creation value for `WalletKdfParametersV1`. pub(crate) fn new_creation(salt: std::vec::Vec) -> Self { return Self { algorithm: WalletKdfAlgorithmV1::Argon2id, version: crate::KSPWALLET_V1_ARGON2_VERSION, memory_kib: crate::KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB, iterations: crate::KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS, parallelism: crate::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM, salt, }; } /// Returns the KDF algorithm. #[must_use] pub const fn algorithm(&self) -> WalletKdfAlgorithmV1 { 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 WalletKdfParametersV1 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("WalletKdfParametersV1") .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 V1 key slot. #[derive(Clone, Eq, PartialEq)] pub struct WalletKeyWrapV1 { algorithm: WalletAeadAlgorithmV1, nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec, } impl WalletKeyWrapV1 { /// Creates a new `WalletKeyWrapV1` value. pub(crate) fn new(nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec) -> Self { return Self { algorithm: WalletAeadAlgorithmV1::XChaCha20Poly1305, nonce, ciphertext }; } /// Returns the wrapping AEAD algorithm. #[must_use] pub const fn algorithm(&self) -> WalletAeadAlgorithmV1 { return self.algorithm; } /// Returns the public AEAD nonce. #[must_use] pub const fn nonce(&self) -> &[u8; crate::KSPWALLET_V1_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 WalletKeyWrapV1 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("WalletKeyWrapV1") .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 V1 envelope. #[derive(Clone, Eq, PartialEq)] pub struct WalletKeySlotV1 { slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES], role: WalletKeySlotRoleV1, kdf: WalletKdfParametersV1, wrap: WalletKeyWrapV1, } impl WalletKeySlotV1 { /// Creates a new `WalletKeySlotV1` value. pub(crate) fn new(slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES], role: WalletKeySlotRoleV1, kdf: WalletKdfParametersV1, wrap: WalletKeyWrapV1) -> 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_V1_SLOT_ID_BYTES] { return &self.slot_id; } /// Returns the key-slot role. #[must_use] pub const fn role(&self) -> WalletKeySlotRoleV1 { return self.role; } /// Returns the serialized password-KDF parameters. #[must_use] pub const fn kdf(&self) -> &WalletKdfParametersV1 { return &self.kdf; } /// Returns the wrapped capability payload. #[must_use] pub const fn wrap(&self) -> &WalletKeyWrapV1 { return &self.wrap; } } impl std::fmt::Debug for WalletKeySlotV1 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("WalletKeySlotV1") .field("slot_id_bytes", &self.slot_id.len()) .field("role", &self.role) .field("kdf", &self.kdf) .field("wrap", &self.wrap) .finish(); } } /// OWNER-signed descriptor that binds the optional self-service VIEW slot. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WalletViewDescriptorV1 { enabled: bool, slot_id: std::option::Option<[u8; crate::KSPWALLET_V1_SLOT_ID_BYTES]>, } impl WalletViewDescriptorV1 { /// Executes the crate-internal disabled operation for `WalletViewDescriptorV1`. pub(crate) const fn disabled() -> Self { return Self { enabled: false, slot_id: std::option::Option::None }; } /// Executes the crate-internal enabled for slot operation for `WalletViewDescriptorV1`. pub(crate) const fn enabled_for_slot(slot_id: [u8; crate::KSPWALLET_V1_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_V1_SLOT_ID_BYTES]> { return self.slot_id.as_ref(); } } /// One validated encrypted compartment from a V1 envelope. #[derive(Clone, Eq, PartialEq)] pub struct WalletEncryptedCompartmentV1 { kind: WalletCompartmentKindV1, payload_version: u32, algorithm: WalletAeadAlgorithmV1, nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec, } impl WalletEncryptedCompartmentV1 { /// Creates a new `WalletEncryptedCompartmentV1` value. pub(crate) fn new(kind: WalletCompartmentKindV1, nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec) -> Self { return Self { kind, payload_version: crate::KSPWALLET_V1_INITIAL_PAYLOAD_VERSION, algorithm: WalletAeadAlgorithmV1::XChaCha20Poly1305, nonce, ciphertext, }; } /// Returns the compartment kind. #[must_use] pub const fn kind(&self) -> WalletCompartmentKindV1 { 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) -> WalletAeadAlgorithmV1 { return self.algorithm; } /// Returns the public AEAD nonce. #[must_use] pub const fn nonce(&self) -> &[u8; crate::KSPWALLET_V1_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 WalletEncryptedCompartmentV1 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("WalletEncryptedCompartmentV1") .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 V1 envelope. #[derive(Clone, Eq, PartialEq)] pub struct WalletStateSignatureV1 { algorithm: WalletStateSignatureAlgorithmV1, signature: [u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES], } impl WalletStateSignatureV1 { /// Creates a new `WalletStateSignatureV1` value. pub(crate) const fn new(signature: [u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]) -> Self { return Self { algorithm: WalletStateSignatureAlgorithmV1::Ed25519, signature }; } /// Returns the state-signature algorithm. #[must_use] pub const fn algorithm(&self) -> WalletStateSignatureAlgorithmV1 { return self.algorithm; } /// Returns the detached signature bytes. #[must_use] pub const fn signature(&self) -> &[u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES] { return &self.signature; } } impl std::fmt::Debug for WalletStateSignatureV1 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("WalletStateSignatureV1") .field("algorithm", &self.algorithm) .field("signature_bytes", &self.signature.len()) .finish(); } } /// Strict semantic representation of a parsed `.kspwallet` V1 envelope. #[derive(Clone, Eq, PartialEq)] pub struct KspWalletEnvelopeV1 { owner_auth_public_key: [u8; crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES], view_descriptor: WalletViewDescriptorV1, owner_slot: WalletKeySlotV1, view_slot: std::option::Option, owner_control: WalletEncryptedCompartmentV1, metadata: WalletEncryptedCompartmentV1, secret: WalletEncryptedCompartmentV1, state_signature: WalletStateSignatureV1, } impl KspWalletEnvelopeV1 { /// Creates a new internal value for `KspWalletEnvelopeV1`. pub(crate) fn new_internal( owner_auth_public_key: [u8; crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES], view_descriptor: WalletViewDescriptorV1, owner_slot: WalletKeySlotV1, view_slot: std::option::Option, owner_control: WalletEncryptedCompartmentV1, metadata: WalletEncryptedCompartmentV1, secret: WalletEncryptedCompartmentV1, state_signature: WalletStateSignatureV1, ) -> Self { return Self { owner_auth_public_key, view_descriptor, owner_slot, view_slot, owner_control, metadata, secret, state_signature, }; } /// Parses and semantically validates one complete `.kspwallet` V1 JSON document. /// /// This function validates only the V1 wire grammar, identifiers, canonical Base64url representation, structural bounds and key-slot invariants. /// Cryptographic authentication/decryption is deliberately implemented by later Wallet layers. pub fn parse_json(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 V1 maximum size", "document")); } let probe_result = serde_json::from_slice::(source); let probe = match probe_result { std::result::Result::Ok(probe) => probe, std::result::Result::Err(error) => return std::result::Result::Err(json_parse_error(error)), }; if probe.magic != crate::KSPWALLET_MAGIC { return std::result::Result::Err(format_error("Wallet magic is invalid", "magic")); } if probe.format_version != crate::KSPWALLET_FORMAT_VERSION_V1 { 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", probe.format_version.to_string()), ); } let raw_result = serde_json::from_slice::(source); let raw = match raw_result { std::result::Result::Ok(raw) => raw, std::result::Result::Err(error) => return std::result::Result::Err(json_parse_error(error)), }; let envelope = match parse_raw_envelope(raw) { std::result::Result::Ok(envelope) => envelope, std::result::Result::Err(error) => return std::result::Result::Err(error), }; 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 V1 envelope parsed" ); return std::result::Result::Ok(envelope); } /// Serializes this already-validated semantic envelope to canonical KSP pretty JSON plus one trailing newline. pub fn to_json_bytes(&self) -> ksp_core_lib::Result> { let raw = RawEnvelopeV1::from_envelope(self); let serialized_result = serde_json::to_vec_pretty(&raw); let mut serialized = match serialized_result { std::result::Result::Ok(serialized) => serialized, std::result::Result::Err(_) => { return std::result::Result::Err(format_error("Validated Wallet envelope cannot be serialized", "document")); }, }; serialized.push(b'\n'); return std::result::Result::Ok(serialized); } /// Returns the fixed native format version. #[must_use] pub const fn format_version(&self) -> u32 { return crate::KSPWALLET_FORMAT_VERSION_V1; } /// Returns the embedded Ed25519 public key that verifies OWNER-controlled state. #[must_use] pub const fn owner_auth_public_key(&self) -> &[u8; crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES] { return &self.owner_auth_public_key; } /// Returns the OWNER-signed VIEW descriptor. #[must_use] pub const fn view_descriptor(&self) -> &WalletViewDescriptorV1 { return &self.view_descriptor; } /// Returns the unique OWNER slot. #[must_use] pub const fn owner_slot(&self) -> &WalletKeySlotV1 { return &self.owner_slot; } /// Returns the VIEW slot when enabled. #[must_use] pub const fn view_slot(&self) -> std::option::Option<&WalletKeySlotV1> { return self.view_slot.as_ref(); } /// Returns the OWNER-control encrypted compartment. #[must_use] pub const fn owner_control(&self) -> &WalletEncryptedCompartmentV1 { return &self.owner_control; } /// Returns the protected metadata encrypted compartment. #[must_use] pub const fn metadata(&self) -> &WalletEncryptedCompartmentV1 { return &self.metadata; } /// Returns the OWNER-only Solana secret encrypted compartment. #[must_use] pub const fn secret(&self) -> &WalletEncryptedCompartmentV1 { return &self.secret; } /// Returns the detached OWNER state signature. #[must_use] pub const fn state_signature(&self) -> &WalletStateSignatureV1 { return &self.state_signature; } /// Builds the deterministic binary transcript covered by the OWNER state signature. #[must_use] pub fn state_transcript(&self) -> std::vec::Vec { return crate::state_transcript(self); } /// Builds the deterministic AEAD AAD for the OWNER key slot. #[must_use] pub fn owner_slot_aad(&self) -> std::vec::Vec { return crate::slot_aad(self, self.owner_slot()); } /// Builds the deterministic AEAD AAD for the self-service VIEW slot when enabled. #[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(self, slot)), std::option::Option::None => std::option::Option::None, }; } /// Builds deterministic AEAD AAD for one encrypted compartment. #[must_use] pub fn compartment_aad(&self, kind: WalletCompartmentKindV1) -> std::vec::Vec { let compartment = match kind { WalletCompartmentKindV1::OwnerControl => self.owner_control(), WalletCompartmentKindV1::Metadata => self.metadata(), WalletCompartmentKindV1::Secret => self.secret(), }; return crate::compartment_aad(self, compartment); } } impl std::fmt::Debug for KspWalletEnvelopeV1 { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("KspWalletEnvelopeV1") .field("format_version", &crate::KSPWALLET_FORMAT_VERSION_V1) .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(); } } #[derive(serde::Deserialize)] struct RawVersionProbe { magic: std::string::String, format_version: u32, } #[derive(serde::Deserialize, serde::Serialize)] #[serde(deny_unknown_fields)] struct RawEnvelopeV1 { magic: std::string::String, format_version: u32, owner_auth_public_key: std::string::String, view_descriptor: RawViewDescriptorV1, key_slots: std::vec::Vec, owner_control: RawOwnerControlV1, metadata: RawMetadataV1, secret: RawSecretV1, state_signature: RawStateSignatureV1, } impl RawEnvelopeV1 { fn from_envelope(envelope: &KspWalletEnvelopeV1) -> Self { let mut key_slots = std::vec::Vec::with_capacity(if envelope.view_slot().is_some() { 2 } else { 1 }); key_slots.push(RawKeySlotV1::from_slot(envelope.owner_slot())); if let std::option::Option::Some(view_slot) = envelope.view_slot() { key_slots.push(RawKeySlotV1::from_slot(view_slot)); } return Self { magic: std::string::String::from(crate::KSPWALLET_MAGIC), format_version: crate::KSPWALLET_FORMAT_VERSION_V1, owner_auth_public_key: encode_base64url(envelope.owner_auth_public_key()), view_descriptor: RawViewDescriptorV1::from_descriptor(envelope.view_descriptor()), key_slots, owner_control: RawOwnerControlV1::from_compartment(envelope.owner_control()), metadata: RawMetadataV1::from_compartment(envelope.metadata()), secret: RawSecretV1::from_compartment(envelope.secret()), state_signature: RawStateSignatureV1::from_signature(envelope.state_signature()), }; } } #[derive(serde::Deserialize, serde::Serialize)] #[serde(deny_unknown_fields)] struct RawViewDescriptorV1 { enabled: bool, slot_id: std::option::Option, } impl RawViewDescriptorV1 { fn from_descriptor(descriptor: &WalletViewDescriptorV1) -> Self { let slot_id = match descriptor.slot_id() { std::option::Option::Some(slot_id) => std::option::Option::Some(encode_base64url(slot_id)), std::option::Option::None => std::option::Option::None, }; return Self { enabled: descriptor.enabled(), slot_id }; } } #[derive(serde::Deserialize, serde::Serialize)] #[serde(deny_unknown_fields)] struct RawKeySlotV1 { slot_id: std::string::String, role: std::string::String, kdf: RawKdfV1, wrap: RawWrapV1, } impl RawKeySlotV1 { fn from_slot(slot: &WalletKeySlotV1) -> Self { return Self { slot_id: encode_base64url(slot.slot_id()), role: std::string::String::from(slot.role().as_str()), kdf: RawKdfV1::from_kdf(slot.kdf()), wrap: RawWrapV1::from_wrap(slot.wrap()), }; } } #[derive(serde::Deserialize, serde::Serialize)] #[serde(deny_unknown_fields)] struct RawKdfV1 { algorithm: std::string::String, version: u32, memory_kib: u32, iterations: u32, parallelism: u32, salt: std::string::String, } impl RawKdfV1 { fn from_kdf(kdf: &WalletKdfParametersV1) -> Self { return Self { algorithm: std::string::String::from(kdf.algorithm().as_str()), version: kdf.version(), memory_kib: kdf.memory_kib(), iterations: kdf.iterations(), parallelism: kdf.parallelism(), salt: encode_base64url(kdf.salt()), }; } } #[derive(serde::Deserialize, serde::Serialize)] #[serde(deny_unknown_fields)] struct RawWrapV1 { algorithm: std::string::String, nonce: std::string::String, ciphertext: std::string::String, } impl RawWrapV1 { fn from_wrap(wrap: &WalletKeyWrapV1) -> Self { return Self { algorithm: std::string::String::from(wrap.algorithm().as_str()), nonce: encode_base64url(wrap.nonce()), ciphertext: encode_base64url(wrap.ciphertext()), }; } } #[derive(serde::Deserialize, serde::Serialize)] #[serde(deny_unknown_fields)] struct RawOwnerControlV1 { control_version: u32, algorithm: std::string::String, nonce: std::string::String, ciphertext: std::string::String, } impl RawOwnerControlV1 { fn from_compartment(compartment: &WalletEncryptedCompartmentV1) -> Self { return Self { control_version: compartment.payload_version(), algorithm: std::string::String::from(compartment.algorithm().as_str()), nonce: encode_base64url(compartment.nonce()), ciphertext: encode_base64url(compartment.ciphertext()), }; } } #[derive(serde::Deserialize, serde::Serialize)] #[serde(deny_unknown_fields)] struct RawMetadataV1 { metadata_version: u32, algorithm: std::string::String, nonce: std::string::String, ciphertext: std::string::String, } impl RawMetadataV1 { fn from_compartment(compartment: &WalletEncryptedCompartmentV1) -> Self { return Self { metadata_version: compartment.payload_version(), algorithm: std::string::String::from(compartment.algorithm().as_str()), nonce: encode_base64url(compartment.nonce()), ciphertext: encode_base64url(compartment.ciphertext()), }; } } #[derive(serde::Deserialize, serde::Serialize)] #[serde(deny_unknown_fields)] struct RawSecretV1 { secret_version: u32, algorithm: std::string::String, nonce: std::string::String, ciphertext: std::string::String, } impl RawSecretV1 { fn from_compartment(compartment: &WalletEncryptedCompartmentV1) -> Self { return Self { secret_version: compartment.payload_version(), algorithm: std::string::String::from(compartment.algorithm().as_str()), nonce: encode_base64url(compartment.nonce()), ciphertext: encode_base64url(compartment.ciphertext()), }; } } #[derive(serde::Deserialize, serde::Serialize)] #[serde(deny_unknown_fields)] struct RawStateSignatureV1 { algorithm: std::string::String, signature: std::string::String, } impl RawStateSignatureV1 { fn from_signature(signature: &WalletStateSignatureV1) -> Self { return Self { algorithm: std::string::String::from(signature.algorithm().as_str()), signature: encode_base64url(signature.signature()), }; } } fn parse_raw_envelope(raw: RawEnvelopeV1) -> ksp_core_lib::Result { if raw.magic != crate::KSPWALLET_MAGIC { return std::result::Result::Err(format_error("Wallet magic is invalid", "magic")); } if raw.format_version != crate::KSPWALLET_FORMAT_VERSION_V1 { 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", raw.format_version.to_string()), ); } let owner_auth_public_key = match decode_fixed::<{ crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES }>(raw.owner_auth_public_key.as_str(), "owner_auth_public_key") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let view_descriptor = match parse_view_descriptor(raw.view_descriptor) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if raw.key_slots.is_empty() || raw.key_slots.len() > crate::KSPWALLET_V1_MAX_KEY_SLOTS { return std::result::Result::Err(format_error("Wallet key_slots count is invalid for V1", "key_slots")); } let mut owner_slot = std::option::Option::None; let mut view_slot = std::option::Option::None; for raw_slot in raw.key_slots { let slot = match parse_key_slot(raw_slot) { std::result::Result::Ok(slot) => slot, std::result::Result::Err(error) => return std::result::Result::Err(error), }; match slot.role() { WalletKeySlotRoleV1::Owner => { if owner_slot.is_some() { return std::result::Result::Err(format_error("Wallet contains duplicate OWNER key slots", "key_slots")); } owner_slot = std::option::Option::Some(slot); }, WalletKeySlotRoleV1::View => { if view_slot.is_some() { return std::result::Result::Err(format_error("Wallet contains duplicate VIEW key slots", "key_slots")); } view_slot = std::option::Option::Some(slot); }, } } let owner_slot = match owner_slot { std::option::Option::Some(slot) => slot, std::option::Option::None => return std::result::Result::Err(format_error("Wallet OWNER key slot is missing", "key_slots")), }; if let std::option::Option::Some(view_slot_ref) = view_slot.as_ref() && owner_slot.slot_id() == view_slot_ref.slot_id() { return std::result::Result::Err(format_error("Wallet key slot identifiers must be unique", "key_slots")); } if view_descriptor.enabled() { let descriptor_slot_id = match view_descriptor.slot_id() { std::option::Option::Some(slot_id) => slot_id, std::option::Option::None => return std::result::Result::Err(format_error("Enabled VIEW descriptor is missing slot_id", "view_descriptor")), }; let parsed_view_slot = match view_slot.as_ref() { std::option::Option::Some(slot) => slot, std::option::Option::None => return std::result::Result::Err(format_error("Enabled VIEW descriptor requires one VIEW slot", "key_slots")), }; if 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")); } } else if view_slot.is_some() { return std::result::Result::Err(format_error("Disabled VIEW descriptor forbids a VIEW key slot", "key_slots")); } let owner_control = match parse_compartment( WalletCompartmentKindV1::OwnerControl, raw.owner_control.control_version, raw.owner_control.algorithm, raw.owner_control.nonce, raw.owner_control.ciphertext, crate::KSPWALLET_V1_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let metadata = match parse_compartment( WalletCompartmentKindV1::Metadata, raw.metadata.metadata_version, raw.metadata.algorithm, raw.metadata.nonce, raw.metadata.ciphertext, crate::KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let secret = match parse_compartment( WalletCompartmentKindV1::Secret, raw.secret.secret_version, raw.secret.algorithm, raw.secret.nonce, raw.secret.ciphertext, crate::KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let state_signature = match parse_state_signature(raw.state_signature) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(KspWalletEnvelopeV1 { owner_auth_public_key, view_descriptor, owner_slot, view_slot, owner_control, metadata, secret, state_signature, }); } fn parse_view_descriptor(raw: RawViewDescriptorV1) -> ksp_core_lib::Result { if raw.enabled { let encoded_slot_id = match raw.slot_id { std::option::Option::Some(slot_id) => slot_id, std::option::Option::None => return std::result::Result::Err(format_error("Enabled VIEW descriptor requires slot_id", "view_descriptor.slot_id")), }; let slot_id = match decode_fixed::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>(encoded_slot_id.as_str(), "view_descriptor.slot_id") { std::result::Result::Ok(slot_id) => slot_id, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(WalletViewDescriptorV1 { enabled: true, slot_id: std::option::Option::Some(slot_id) }); } if raw.slot_id.is_some() { return std::result::Result::Err(format_error("Disabled VIEW descriptor requires null slot_id", "view_descriptor.slot_id")); } return std::result::Result::Ok(WalletViewDescriptorV1 { enabled: false, slot_id: std::option::Option::None }); } fn parse_key_slot(raw: RawKeySlotV1) -> ksp_core_lib::Result { let slot_id = match decode_fixed::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>(raw.slot_id.as_str(), "key_slots.slot_id") { std::result::Result::Ok(slot_id) => slot_id, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let role = match raw.role.as_str() { "owner" => WalletKeySlotRoleV1::Owner, "view" => WalletKeySlotRoleV1::View, _ => return std::result::Result::Err(format_error("Wallet key slot role is unsupported", "key_slots.role")), }; let kdf = match parse_kdf(raw.kdf) { std::result::Result::Ok(kdf) => kdf, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let wrap = match parse_wrap(raw.wrap) { std::result::Result::Ok(wrap) => wrap, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(WalletKeySlotV1 { slot_id, role, kdf, wrap }); } fn parse_kdf(raw: RawKdfV1) -> ksp_core_lib::Result { if raw.algorithm != "argon2id" || raw.version != crate::KSPWALLET_V1_ARGON2_VERSION { return std::result::Result::Err(crypto_parameter_error("Wallet V1 requires Argon2id version 19", "key_slots.kdf")); } if raw.memory_kib == 0 || raw.memory_kib > crate::KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB { return std::result::Result::Err(crypto_parameter_error("Wallet Argon2 memory_kib is outside the V1 structural range", "key_slots.kdf.memory_kib")); } if raw.iterations == 0 || raw.iterations > crate::KSPWALLET_V1_MAX_ARGON2_ITERATIONS { return std::result::Result::Err(crypto_parameter_error("Wallet Argon2 iterations is outside the V1 structural range", "key_slots.kdf.iterations")); } if raw.parallelism == 0 || raw.parallelism > crate::KSPWALLET_V1_MAX_ARGON2_PARALLELISM { return std::result::Result::Err(crypto_parameter_error("Wallet Argon2 parallelism is outside the V1 structural range", "key_slots.kdf.parallelism")); } if raw.memory_kib < raw.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_slots.kdf.memory_kib", )); } let salt = match decode_base64url(raw.salt.as_str(), "key_slots.kdf.salt") { std::result::Result::Ok(salt) => salt, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if salt.len() < crate::KSPWALLET_V1_MIN_KDF_SALT_BYTES || salt.len() > crate::KSPWALLET_V1_MAX_KDF_SALT_BYTES { return std::result::Result::Err(crypto_parameter_error("Wallet KDF salt length is outside the V1 range", "key_slots.kdf.salt")); } return std::result::Result::Ok(WalletKdfParametersV1 { algorithm: WalletKdfAlgorithmV1::Argon2id, version: raw.version, memory_kib: raw.memory_kib, iterations: raw.iterations, parallelism: raw.parallelism, salt, }); } fn parse_wrap(raw: RawWrapV1) -> ksp_core_lib::Result { if raw.algorithm != "xchacha20-poly1305" { return std::result::Result::Err(crypto_parameter_error("Wallet V1 key wrapping requires XChaCha20-Poly1305", "key_slots.wrap.algorithm")); } let nonce = match decode_fixed::<{ crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES }>(raw.nonce.as_str(), "key_slots.wrap.nonce") { std::result::Result::Ok(nonce) => nonce, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let ciphertext = match decode_base64url(raw.ciphertext.as_str(), "key_slots.wrap.ciphertext") { std::result::Result::Ok(ciphertext) => ciphertext, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if ciphertext.len() < crate::KSPWALLET_V1_AEAD_TAG_BYTES || ciphertext.len() > crate::KSPWALLET_V1_MAX_KEY_WRAP_CIPHERTEXT_BYTES { return std::result::Result::Err(format_error("Wallet key-wrap ciphertext length is invalid", "key_slots.wrap.ciphertext")); } return std::result::Result::Ok(WalletKeyWrapV1 { algorithm: WalletAeadAlgorithmV1::XChaCha20Poly1305, nonce, ciphertext }); } fn parse_compartment( kind: WalletCompartmentKindV1, payload_version: u32, algorithm: std::string::String, encoded_nonce: std::string::String, encoded_ciphertext: std::string::String, max_ciphertext_bytes: usize, ) -> ksp_core_lib::Result { if payload_version != crate::KSPWALLET_V1_INITIAL_PAYLOAD_VERSION { return std::result::Result::Err(format_error("Wallet protected payload version is unsupported by V1", "compartment.version")); } if algorithm != "xchacha20-poly1305" { return std::result::Result::Err(crypto_parameter_error("Wallet V1 compartments require XChaCha20-Poly1305", "compartment.algorithm")); } let nonce = match decode_fixed::<{ crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES }>(encoded_nonce.as_str(), "compartment.nonce") { std::result::Result::Ok(nonce) => nonce, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let ciphertext = match decode_base64url(encoded_ciphertext.as_str(), "compartment.ciphertext") { std::result::Result::Ok(ciphertext) => ciphertext, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if ciphertext.len() < crate::KSPWALLET_V1_AEAD_TAG_BYTES || ciphertext.len() > max_ciphertext_bytes { return std::result::Result::Err(format_error("Wallet compartment ciphertext length is invalid", "compartment.ciphertext")); } return std::result::Result::Ok(WalletEncryptedCompartmentV1 { kind, payload_version, algorithm: WalletAeadAlgorithmV1::XChaCha20Poly1305, nonce, ciphertext, }); } fn parse_state_signature(raw: RawStateSignatureV1) -> ksp_core_lib::Result { if raw.algorithm != "ed25519" { return std::result::Result::Err(crypto_parameter_error("Wallet V1 state authentication requires Ed25519", "state_signature.algorithm")); } let signature = match decode_fixed::<{ crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES }>(raw.signature.as_str(), "state_signature.signature") { std::result::Result::Ok(signature) => signature, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(WalletStateSignatureV1 { algorithm: WalletStateSignatureAlgorithmV1::Ed25519, signature }); } fn decode_fixed(encoded: &str, field: &'static str) -> ksp_core_lib::Result<[u8; LENGTH]> { let decoded = match decode_base64url(encoded, field) { std::result::Result::Ok(decoded) => decoded, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if decoded.len() != LENGTH { return std::result::Result::Err(format_error("Wallet binary field has an invalid decoded length", field)); } let converted = <[u8; LENGTH]>::try_from(decoded.as_slice()); return match converted { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(_) => std::result::Result::Err(format_error("Wallet binary field cannot be converted to its fixed length", field)), }; } fn decode_base64url(encoded: &str, field: &'static str) -> ksp_core_lib::Result> { let decoded_result = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded.as_bytes()); let decoded = match decoded_result { std::result::Result::Ok(decoded) => decoded, std::result::Result::Err(_) => return std::result::Result::Err(format_error("Wallet binary field is not canonical Base64url without padding", field)), }; let canonical = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(decoded.as_slice()); if canonical != encoded { return std::result::Result::Err(format_error("Wallet binary field is not canonical Base64url without padding", field)); } return std::result::Result::Ok(decoded); } fn encode_base64url(bytes: &[u8]) -> std::string::String { return base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes); } fn json_parse_error(error: serde_json::Error) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet JSON document is invalid or violates the strict V1 grammar") .with_context("line", error.line().to_string()) .with_context("column", error.column().to_string()); } 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.rs"] mod tests;