Files
khadhroony-solana-project/crates/ksp-wallet-lib/src/wire_v2.rs
2026-08-22 08:18:38 +02:00

1049 lines
44 KiB
Rust

// 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<u8>,
}
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<u8>) -> 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<u8>) -> 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<u8>,
}
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<u8>) -> 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<u8>,
}
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<u8>,
) -> 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<WalletKeySlotV2>,
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<WalletKeySlotV2>,
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<Self> {
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<std::vec::Vec<u8>> {
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::<u32>();
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<u8> {
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<u8> {
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<std::vec::Vec<u8>> {
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<u8> {
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<WalletKeySlotV2> {
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<WalletEncryptedCompartmentV2> {
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<WalletStateSignatureV2> {
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<u8>, 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<u8>, 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<u8> {
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<u16> {
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<u32> {
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<const LENGTH: usize>(&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<std::vec::Vec<u8>> {
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;