1049 lines
44 KiB
Rust
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;
|