Files
khadhroony-solana-project/crates/ksp-wallet-lib/src/wire.rs

1096 lines
42 KiB
Rust

// 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<u8>,
}
impl WalletKdfParametersV1 {
/// Creates a new creation value for `WalletKdfParametersV1`.
pub(crate) fn new_creation(salt: std::vec::Vec<u8>) -> 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<u8>,
}
impl WalletKeyWrapV1 {
/// Creates a new `WalletKeyWrapV1` value.
pub(crate) fn new(nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec<u8>) -> 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<u8>,
}
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<u8>) -> 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<WalletKeySlotV1>,
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<WalletKeySlotV1>,
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<Self> {
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::<RawVersionProbe>(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::<RawEnvelopeV1>(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<std::vec::Vec<u8>> {
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<u8> {
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<u8> {
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<std::vec::Vec<u8>> {
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<u8> {
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<RawKeySlotV1>,
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<std::string::String>,
}
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<KspWalletEnvelopeV1> {
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<WalletViewDescriptorV1> {
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<WalletKeySlotV1> {
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<WalletKdfParametersV1> {
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<WalletKeyWrapV1> {
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<WalletEncryptedCompartmentV1> {
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<WalletStateSignatureV1> {
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<const LENGTH: usize>(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<std::vec::Vec<u8>> {
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;