v0.2.5-pre.003
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-wallet-lib/Cargo.toml
|
||||
# version: 1
|
||||
# version: 2
|
||||
|
||||
[package]
|
||||
name = "ksp-wallet-lib"
|
||||
@@ -10,6 +10,9 @@ repository.workspace = true
|
||||
[dependencies]
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
base64.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
zeroize.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,7 +1,70 @@
|
||||
// file: crates/ksp-wallet-lib/src/constants.rs
|
||||
// version: 1
|
||||
// version: 3
|
||||
|
||||
//! Wallet-owned constants.
|
||||
|
||||
/// Exact magic string required by every native `.kspwallet` document.
|
||||
pub const KSPWALLET_MAGIC: &str = "KSPWALLET";
|
||||
/// Native Wallet format version implemented by the V1 codec.
|
||||
pub const KSPWALLET_FORMAT_VERSION_V1: u32 = 1;
|
||||
/// Maximum accepted `.kspwallet` document size before JSON parsing.
|
||||
pub const KSPWALLET_MAX_FILE_BYTES: usize = 1024 * 1024;
|
||||
/// Maximum protected alias size in UTF-8 bytes for metadata V1.
|
||||
pub const KSPWALLET_V1_MAX_ALIAS_BYTES: usize = 256;
|
||||
/// Maximum number of protected notes in metadata V1.
|
||||
pub const KSPWALLET_V1_MAX_NOTES: usize = 64;
|
||||
/// Maximum protected note text size in UTF-8 bytes for metadata V1.
|
||||
pub const KSPWALLET_V1_MAX_NOTE_TEXT_BYTES: usize = 8 * 1024;
|
||||
/// Maximum metadata plaintext size before V1 encryption.
|
||||
pub const KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES: usize = 64 * 1024;
|
||||
/// Maximum password size in exact UTF-8 input bytes.
|
||||
pub const KSPWALLET_V1_MAX_PASSWORD_BYTES: usize = 1024;
|
||||
/// Byte length of every V1 key-slot identifier.
|
||||
pub const KSPWALLET_V1_SLOT_ID_BYTES: usize = 16;
|
||||
/// Byte length of an Ed25519 public key used as Wallet format authority.
|
||||
pub const KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES: usize = 32;
|
||||
/// Byte length of an Ed25519 detached state signature.
|
||||
pub const KSPWALLET_V1_ED25519_SIGNATURE_BYTES: usize = 64;
|
||||
/// Byte length of an XChaCha20-Poly1305 nonce.
|
||||
pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
|
||||
/// Byte length of the Poly1305 authentication tag appended to each ciphertext.
|
||||
pub const KSPWALLET_V1_AEAD_TAG_BYTES: usize = 16;
|
||||
/// Argon2 version serialized by V1 key slots.
|
||||
pub const KSPWALLET_V1_ARGON2_VERSION: u32 = 19;
|
||||
/// Minimum accepted Argon2 salt size in bytes.
|
||||
pub const KSPWALLET_V1_MIN_KDF_SALT_BYTES: usize = 16;
|
||||
/// Maximum accepted Argon2 salt size in bytes.
|
||||
pub const KSPWALLET_V1_MAX_KDF_SALT_BYTES: usize = 64;
|
||||
/// Structural V1 ceiling for serialized Argon2 memory cost; creation defaults are benchmarked separately.
|
||||
pub const KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB: u32 = 1024 * 1024;
|
||||
/// Structural V1 ceiling for serialized Argon2 iteration cost; creation defaults are benchmarked separately.
|
||||
pub const KSPWALLET_V1_MAX_ARGON2_ITERATIONS: u32 = 64;
|
||||
/// Structural V1 ceiling for serialized Argon2 parallelism; creation defaults are benchmarked separately.
|
||||
pub const KSPWALLET_V1_MAX_ARGON2_PARALLELISM: u32 = 64;
|
||||
/// Maximum number of key slots understood by format V1: exactly one OWNER plus optional VIEW.
|
||||
pub const KSPWALLET_V1_MAX_KEY_SLOTS: usize = 2;
|
||||
/// Maximum ciphertext size of one wrapped capability payload.
|
||||
pub const KSPWALLET_V1_MAX_KEY_WRAP_CIPHERTEXT_BYTES: usize = 4096;
|
||||
/// Maximum OWNER-control ciphertext size accepted by envelope V1.
|
||||
pub const KSPWALLET_V1_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES: usize = 4096;
|
||||
/// Maximum metadata ciphertext size, including the AEAD tag over the bounded 64-KiB plaintext.
|
||||
pub const KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES + KSPWALLET_V1_AEAD_TAG_BYTES;
|
||||
/// Maximum OWNER-only secret ciphertext size accepted by envelope V1.
|
||||
pub const KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES: usize = 4096;
|
||||
/// Initial protected payload version used independently by control, metadata and secret compartments.
|
||||
pub const KSPWALLET_V1_INITIAL_PAYLOAD_VERSION: u32 = 1;
|
||||
/// Domain separator for the OWNER state-signature transcript.
|
||||
pub const KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V1-STATE";
|
||||
/// Domain separator for OWNER key-slot wrapping AAD.
|
||||
pub const KSPWALLET_V1_OWNER_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-OWNER-SLOT";
|
||||
/// Domain separator for VIEW key-slot wrapping AAD.
|
||||
pub const KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-VIEW-SLOT";
|
||||
/// Domain separator for OWNER-control compartment AAD.
|
||||
pub const KSPWALLET_V1_OWNER_CONTROL_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-OWNER-CONTROL";
|
||||
/// Domain separator for metadata compartment AAD.
|
||||
pub const KSPWALLET_V1_METADATA_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-METADATA";
|
||||
/// Domain separator for OWNER-only secret compartment AAD.
|
||||
pub const KSPWALLET_V1_SECRET_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-SECRET";
|
||||
|
||||
/// Owning tracing target for events emitted by the Wallet crate.
|
||||
pub(crate) const TRACING_TARGET: &str = "ksp-wallet-lib";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/lib.rs
|
||||
// version: 1
|
||||
// version: 3
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -7,9 +7,10 @@
|
||||
//! Autonomous KSP Wallet foundation.
|
||||
//!
|
||||
//! `ksp-wallet-lib` owns the native `.kspwallet` domain, VIEW/OWNER capability model, protected metadata projection, password-secret wrappers and Wallet
|
||||
//! error contract. The `0.2.5-pre.002` foundation deliberately contains no file codec, KDF/AEAD implementation, Solana secret material, persistence,
|
||||
//! network access, Config integration or execution policy. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by
|
||||
//! KSP Core, and behavioral observability uses only `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
|
||||
//! error contract. `0.2.5-pre.003` additionally freezes the strict V1 JSON envelope, canonical Base64url decoding, structural limits and deterministic
|
||||
//! state-transcript/AEAD-AAD byte codecs. It still performs no KDF, encryption, decryption, state-signature verification, Solana signing or filesystem
|
||||
//! persistence. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral observability uses
|
||||
//! only `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
|
||||
|
||||
mod capability;
|
||||
mod constants;
|
||||
@@ -17,10 +18,74 @@ mod error;
|
||||
mod metadata;
|
||||
mod owner;
|
||||
mod password;
|
||||
mod transcript;
|
||||
mod view;
|
||||
mod wire;
|
||||
|
||||
/// Authorized capability represented by an unlocked Wallet handle.
|
||||
pub use self::capability::WalletCapability;
|
||||
/// Native `.kspwallet` V1 format version.
|
||||
pub use self::constants::KSPWALLET_FORMAT_VERSION_V1;
|
||||
/// Exact magic string required by every native `.kspwallet` document.
|
||||
pub use self::constants::KSPWALLET_MAGIC;
|
||||
/// Maximum accepted `.kspwallet` document size before parsing.
|
||||
pub use self::constants::KSPWALLET_MAX_FILE_BYTES;
|
||||
/// Byte length of the AEAD authentication tag appended to V1 ciphertexts.
|
||||
pub use self::constants::KSPWALLET_V1_AEAD_TAG_BYTES;
|
||||
/// Argon2 version serialized by `.kspwallet` V1 key slots.
|
||||
pub use self::constants::KSPWALLET_V1_ARGON2_VERSION;
|
||||
/// Byte length of the Ed25519 format-authority public key.
|
||||
pub use self::constants::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES;
|
||||
/// Byte length of the Ed25519 detached state signature.
|
||||
pub use self::constants::KSPWALLET_V1_ED25519_SIGNATURE_BYTES;
|
||||
/// Initial per-compartment protected payload version.
|
||||
pub use self::constants::KSPWALLET_V1_INITIAL_PAYLOAD_VERSION;
|
||||
/// Maximum alias size in protected metadata V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_ALIAS_BYTES;
|
||||
/// Maximum structural Argon2 iteration count accepted by the V1 parser.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_ARGON2_ITERATIONS;
|
||||
/// Maximum structural Argon2 memory cost accepted by the V1 parser.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB;
|
||||
/// Maximum structural Argon2 parallelism accepted by the V1 parser.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_ARGON2_PARALLELISM;
|
||||
/// Maximum Argon2 salt size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_KDF_SALT_BYTES;
|
||||
/// Maximum number of OWNER/VIEW slots accepted by format V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_KEY_SLOTS;
|
||||
/// Maximum wrapped-key ciphertext size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_KEY_WRAP_CIPHERTEXT_BYTES;
|
||||
/// Maximum protected metadata ciphertext size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES;
|
||||
/// Maximum protected metadata plaintext size defined by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES;
|
||||
/// Maximum protected note text size defined by metadata V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_NOTE_TEXT_BYTES;
|
||||
/// Maximum number of protected notes defined by metadata V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_NOTES;
|
||||
/// Maximum OWNER-control ciphertext size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES;
|
||||
/// Maximum password size in exact UTF-8 bytes defined by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_PASSWORD_BYTES;
|
||||
/// Maximum OWNER-only secret ciphertext size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES;
|
||||
/// Domain separator for metadata-compartment AEAD AAD.
|
||||
pub use self::constants::KSPWALLET_V1_METADATA_AAD_DOMAIN;
|
||||
/// Minimum Argon2 salt size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MIN_KDF_SALT_BYTES;
|
||||
/// Domain separator for OWNER-control compartment AEAD AAD.
|
||||
pub use self::constants::KSPWALLET_V1_OWNER_CONTROL_AAD_DOMAIN;
|
||||
/// Domain separator for OWNER key-slot wrapping AAD.
|
||||
pub use self::constants::KSPWALLET_V1_OWNER_SLOT_AAD_DOMAIN;
|
||||
/// Domain separator for OWNER-only secret compartment AEAD AAD.
|
||||
pub use self::constants::KSPWALLET_V1_SECRET_AAD_DOMAIN;
|
||||
/// Byte length of every V1 key-slot identifier.
|
||||
pub use self::constants::KSPWALLET_V1_SLOT_ID_BYTES;
|
||||
/// Domain separator for the OWNER state-signature transcript.
|
||||
pub use self::constants::KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN;
|
||||
/// Domain separator for VIEW key-slot wrapping AAD.
|
||||
pub use self::constants::KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN;
|
||||
/// Byte length of an XChaCha20-Poly1305 nonce.
|
||||
pub use self::constants::KSPWALLET_V1_XCHACHA_NONCE_BYTES;
|
||||
/// Error code used when an atomic Wallet persistence operation cannot publish a valid replacement.
|
||||
pub use self::error::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED;
|
||||
/// Error code used when an authenticated Wallet structure cannot be verified.
|
||||
@@ -61,6 +126,36 @@ pub use self::password::OwnerPassword;
|
||||
pub use self::password::ViewPassword;
|
||||
/// Authorized VIEW capability handle.
|
||||
pub use self::view::WalletView;
|
||||
/// Strict semantic representation of one parsed native `.kspwallet` V1 envelope.
|
||||
pub use self::wire::KspWalletEnvelopeV1;
|
||||
/// Authenticated-encryption algorithm fixed by native Wallet V1.
|
||||
pub use self::wire::WalletAeadAlgorithmV1;
|
||||
/// Protected compartment kind fixed by native Wallet V1.
|
||||
pub use self::wire::WalletCompartmentKindV1;
|
||||
/// One validated encrypted V1 compartment.
|
||||
pub use self::wire::WalletEncryptedCompartmentV1;
|
||||
/// Password KDF fixed by native Wallet V1.
|
||||
pub use self::wire::WalletKdfAlgorithmV1;
|
||||
/// Parsed Argon2id parameters from one V1 key slot.
|
||||
pub use self::wire::WalletKdfParametersV1;
|
||||
/// Role of one native V1 key slot.
|
||||
pub use self::wire::WalletKeySlotRoleV1;
|
||||
/// One validated OWNER or VIEW V1 key slot.
|
||||
pub use self::wire::WalletKeySlotV1;
|
||||
/// Parsed AEAD wrapped-key payload from one V1 key slot.
|
||||
pub use self::wire::WalletKeyWrapV1;
|
||||
/// State-signature algorithm fixed by native Wallet V1.
|
||||
pub use self::wire::WalletStateSignatureAlgorithmV1;
|
||||
/// Detached OWNER state signature embedded in a V1 envelope.
|
||||
pub use self::wire::WalletStateSignatureV1;
|
||||
/// OWNER-signed stable descriptor of the optional VIEW slot.
|
||||
pub use self::wire::WalletViewDescriptorV1;
|
||||
|
||||
/// Wallet-owned tracing target used by the KSP logging facade.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Internal deterministic compartment-AAD codec shared by Wallet crypto layers.
|
||||
pub(crate) use self::transcript::compartment_aad;
|
||||
/// Internal deterministic key-slot-AAD codec shared by Wallet crypto layers.
|
||||
pub(crate) use self::transcript::slot_aad;
|
||||
/// Internal deterministic OWNER-state transcript codec shared by Wallet crypto layers.
|
||||
pub(crate) use self::transcript::state_transcript;
|
||||
|
||||
131
crates/ksp-wallet-lib/src/transcript.rs
Normal file
131
crates/ksp-wallet-lib/src/transcript.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
// file: crates/ksp-wallet-lib/src/transcript.rs
|
||||
// version: 1
|
||||
|
||||
//! Deterministic `.kspwallet` V1 state-transcript and AEAD-AAD encoding.
|
||||
|
||||
const TAG_MAGIC: u16 = 0x0001;
|
||||
const TAG_FORMAT_VERSION: u16 = 0x0002;
|
||||
const TAG_OWNER_AUTH_PUBLIC_KEY: u16 = 0x0003;
|
||||
const TAG_VIEW_ENABLED: u16 = 0x0010;
|
||||
const TAG_VIEW_ROLE: u16 = 0x0011;
|
||||
const TAG_VIEW_SLOT_ID: u16 = 0x0012;
|
||||
const TAG_SLOT_ID: u16 = 0x0100;
|
||||
const TAG_SLOT_ROLE: u16 = 0x0101;
|
||||
const TAG_KDF_ALGORITHM: u16 = 0x0102;
|
||||
const TAG_KDF_VERSION: u16 = 0x0103;
|
||||
const TAG_KDF_MEMORY_KIB: u16 = 0x0104;
|
||||
const TAG_KDF_ITERATIONS: u16 = 0x0105;
|
||||
const TAG_KDF_PARALLELISM: u16 = 0x0106;
|
||||
const TAG_KDF_SALT: u16 = 0x0107;
|
||||
const TAG_WRAP_ALGORITHM: u16 = 0x0108;
|
||||
const TAG_WRAP_NONCE: u16 = 0x0109;
|
||||
const TAG_WRAP_CIPHERTEXT: u16 = 0x010A;
|
||||
const TAG_COMPARTMENT_KIND: u16 = 0x0200;
|
||||
const TAG_COMPARTMENT_VERSION: u16 = 0x0201;
|
||||
const TAG_COMPARTMENT_ALGORITHM: u16 = 0x0202;
|
||||
const TAG_COMPARTMENT_NONCE: u16 = 0x0203;
|
||||
const TAG_COMPARTMENT_CIPHERTEXT: u16 = 0x0204;
|
||||
const TAG_STATE_SIGNATURE_ALGORITHM: u16 = 0x0500;
|
||||
|
||||
/// Builds the normative OWNER state-signature transcript for one validated V1 envelope.
|
||||
pub(crate) fn state_transcript(envelope: &crate::KspWalletEnvelopeV1) -> std::vec::Vec<u8> {
|
||||
let mut output = start(crate::KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN);
|
||||
push_common(&mut output, envelope);
|
||||
push_bool(&mut output, TAG_VIEW_ENABLED, envelope.view_descriptor().enabled());
|
||||
push_bytes(&mut output, TAG_VIEW_ROLE, crate::WalletKeySlotRoleV1::View.as_str().as_bytes());
|
||||
match envelope.view_descriptor().slot_id() {
|
||||
std::option::Option::Some(slot_id) => push_bytes(&mut output, TAG_VIEW_SLOT_ID, slot_id),
|
||||
std::option::Option::None => push_bytes(&mut output, TAG_VIEW_SLOT_ID, &[]),
|
||||
}
|
||||
|
||||
push_slot(&mut output, envelope.owner_slot(), true);
|
||||
push_compartment(&mut output, envelope.owner_control(), true);
|
||||
push_compartment(&mut output, envelope.metadata(), true);
|
||||
push_compartment(&mut output, envelope.secret(), true);
|
||||
push_bytes(&mut output, TAG_STATE_SIGNATURE_ALGORITHM, envelope.state_signature().algorithm().as_str().as_bytes());
|
||||
return output;
|
||||
}
|
||||
|
||||
/// Builds the normative wrapping AAD for one validated OWNER or VIEW key slot.
|
||||
pub(crate) fn slot_aad(envelope: &crate::KspWalletEnvelopeV1, slot: &crate::WalletKeySlotV1) -> std::vec::Vec<u8> {
|
||||
let domain = match slot.role() {
|
||||
crate::WalletKeySlotRoleV1::Owner => crate::KSPWALLET_V1_OWNER_SLOT_AAD_DOMAIN,
|
||||
crate::WalletKeySlotRoleV1::View => crate::KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN,
|
||||
};
|
||||
let mut output = start(domain);
|
||||
push_common(&mut output, envelope);
|
||||
push_slot(&mut output, slot, false);
|
||||
return output;
|
||||
}
|
||||
|
||||
/// Builds the normative AEAD AAD for one validated encrypted compartment.
|
||||
pub(crate) fn compartment_aad(envelope: &crate::KspWalletEnvelopeV1, compartment: &crate::WalletEncryptedCompartmentV1) -> std::vec::Vec<u8> {
|
||||
let domain = match compartment.kind() {
|
||||
crate::WalletCompartmentKindV1::OwnerControl => crate::KSPWALLET_V1_OWNER_CONTROL_AAD_DOMAIN,
|
||||
crate::WalletCompartmentKindV1::Metadata => crate::KSPWALLET_V1_METADATA_AAD_DOMAIN,
|
||||
crate::WalletCompartmentKindV1::Secret => crate::KSPWALLET_V1_SECRET_AAD_DOMAIN,
|
||||
};
|
||||
let mut output = start(domain);
|
||||
push_common(&mut output, envelope);
|
||||
push_compartment(&mut output, compartment, false);
|
||||
return output;
|
||||
}
|
||||
|
||||
fn start(domain: &[u8]) -> std::vec::Vec<u8> {
|
||||
let mut output = std::vec::Vec::with_capacity(512);
|
||||
output.extend_from_slice(domain);
|
||||
output.push(0);
|
||||
return output;
|
||||
}
|
||||
|
||||
fn push_common(output: &mut std::vec::Vec<u8>, envelope: &crate::KspWalletEnvelopeV1) {
|
||||
push_bytes(output, TAG_MAGIC, crate::KSPWALLET_MAGIC.as_bytes());
|
||||
push_u32(output, TAG_FORMAT_VERSION, envelope.format_version());
|
||||
push_bytes(output, TAG_OWNER_AUTH_PUBLIC_KEY, envelope.owner_auth_public_key());
|
||||
}
|
||||
|
||||
fn push_slot(output: &mut std::vec::Vec<u8>, slot: &crate::WalletKeySlotV1, include_wrap_payload: bool) {
|
||||
push_bytes(output, TAG_SLOT_ID, slot.slot_id());
|
||||
push_bytes(output, TAG_SLOT_ROLE, slot.role().as_str().as_bytes());
|
||||
push_bytes(output, TAG_KDF_ALGORITHM, slot.kdf().algorithm().as_str().as_bytes());
|
||||
push_u32(output, TAG_KDF_VERSION, slot.kdf().version());
|
||||
push_u32(output, TAG_KDF_MEMORY_KIB, slot.kdf().memory_kib());
|
||||
push_u32(output, TAG_KDF_ITERATIONS, slot.kdf().iterations());
|
||||
push_u32(output, TAG_KDF_PARALLELISM, slot.kdf().parallelism());
|
||||
push_bytes(output, TAG_KDF_SALT, slot.kdf().salt());
|
||||
push_bytes(output, TAG_WRAP_ALGORITHM, slot.wrap().algorithm().as_str().as_bytes());
|
||||
if include_wrap_payload {
|
||||
push_bytes(output, TAG_WRAP_NONCE, slot.wrap().nonce());
|
||||
push_bytes(output, TAG_WRAP_CIPHERTEXT, slot.wrap().ciphertext());
|
||||
}
|
||||
}
|
||||
|
||||
fn push_compartment(output: &mut std::vec::Vec<u8>, compartment: &crate::WalletEncryptedCompartmentV1, include_ciphertext: bool) {
|
||||
push_bytes(output, TAG_COMPARTMENT_KIND, compartment.kind().as_str().as_bytes());
|
||||
push_u32(output, TAG_COMPARTMENT_VERSION, compartment.payload_version());
|
||||
push_bytes(output, TAG_COMPARTMENT_ALGORITHM, compartment.algorithm().as_str().as_bytes());
|
||||
if include_ciphertext {
|
||||
push_bytes(output, TAG_COMPARTMENT_NONCE, compartment.nonce());
|
||||
push_bytes(output, TAG_COMPARTMENT_CIPHERTEXT, compartment.ciphertext());
|
||||
}
|
||||
}
|
||||
|
||||
fn push_bool(output: &mut std::vec::Vec<u8>, tag: u16, value: bool) {
|
||||
let byte = if value { 1_u8 } else { 0_u8 };
|
||||
push_bytes(output, tag, &[byte]);
|
||||
}
|
||||
|
||||
fn push_u32(output: &mut std::vec::Vec<u8>, tag: u16, value: u32) {
|
||||
push_bytes(output, tag, value.to_be_bytes().as_slice());
|
||||
}
|
||||
|
||||
fn push_bytes(output: &mut std::vec::Vec<u8>, tag: u16, value: &[u8]) {
|
||||
output.extend_from_slice(tag.to_be_bytes().as_slice());
|
||||
let length = value.len() as u64;
|
||||
output.extend_from_slice(length.to_be_bytes().as_slice());
|
||||
output.extend_from_slice(value);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/transcript.rs"]
|
||||
mod tests;
|
||||
1024
crates/ksp-wallet-lib/src/wire.rs
Normal file
1024
crates/ksp-wallet-lib/src/wire.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/tests/dependency_boundary.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Wallet-specific dependency and ownership canaries.
|
||||
|
||||
@@ -40,6 +40,9 @@ fn wallet_manifest_preserves_dependency_firewall() -> std::io::Result<()> {
|
||||
};
|
||||
assert!(manifest.contains("ksp-core-lib"));
|
||||
assert!(manifest.contains("ksp-logging-lib"));
|
||||
assert!(manifest.contains("base64.workspace = true"));
|
||||
assert!(manifest.contains("serde = { workspace = true, features = [\"derive\"] }"));
|
||||
assert!(manifest.contains("serde_json.workspace = true"));
|
||||
assert!(manifest.contains("zeroize.workspace = true"));
|
||||
for forbidden in [
|
||||
"ksp-config-lib",
|
||||
|
||||
67
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_wire_only.json
vendored
Normal file
67
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_wire_only.json
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"magic": "KSPWALLET",
|
||||
"format_version": 1,
|
||||
"owner_auth_public_key": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
|
||||
"view_descriptor": {
|
||||
"enabled": true,
|
||||
"slot_id": "ICEiIyQlJicoKSorLC0uLw"
|
||||
},
|
||||
"key_slots": [
|
||||
{
|
||||
"slot_id": "EBESExQVFhcYGRobHB0eHw",
|
||||
"role": "owner",
|
||||
"kdf": {
|
||||
"algorithm": "argon2id",
|
||||
"version": 19,
|
||||
"memory_kib": 65536,
|
||||
"iterations": 3,
|
||||
"parallelism": 1,
|
||||
"salt": "MDEyMzQ1Njc4OTo7PD0-Pw"
|
||||
},
|
||||
"wrap": {
|
||||
"algorithm": "xchacha20-poly1305",
|
||||
"nonce": "UFFSU1RVVldYWVpbXF1eX2BhYmNkZWZn",
|
||||
"ciphertext": "gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"slot_id": "ICEiIyQlJicoKSorLC0uLw",
|
||||
"role": "view",
|
||||
"kdf": {
|
||||
"algorithm": "argon2id",
|
||||
"version": 19,
|
||||
"memory_kib": 32768,
|
||||
"iterations": 4,
|
||||
"parallelism": 1,
|
||||
"salt": "QEFCQ0RFRkdISUpLTE1OTw"
|
||||
},
|
||||
"wrap": {
|
||||
"algorithm": "xchacha20-poly1305",
|
||||
"nonce": "aGlqa2xtbm9wcXJzdHV2d3h5ent8fX5_",
|
||||
"ciphertext": "oKGio6SlpqeoqaqrrK2ur7CxsrO0tba3uLm6u7y9vr8"
|
||||
}
|
||||
}
|
||||
],
|
||||
"owner_control": {
|
||||
"control_version": 1,
|
||||
"algorithm": "xchacha20-poly1305",
|
||||
"nonce": "kJGSk5SVlpeYmZqbnJ2en6ChoqOkpaan",
|
||||
"ciphertext": "wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v"
|
||||
},
|
||||
"metadata": {
|
||||
"metadata_version": 1,
|
||||
"algorithm": "xchacha20-poly1305",
|
||||
"nonce": "qKmqq6ytrq-wsbKztLW2t7i5uru8vb6_",
|
||||
"ciphertext": "EBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4_"
|
||||
},
|
||||
"secret": {
|
||||
"secret_version": 1,
|
||||
"algorithm": "xchacha20-poly1305",
|
||||
"nonce": "wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX",
|
||||
"ciphertext": "QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8"
|
||||
},
|
||||
"state_signature": {
|
||||
"algorithm": "ed25519",
|
||||
"signature": "2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_wABAgMEBQYHCAkKCwwNDg8QERITFBUWFw"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/tests/public_api.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Public API canaries for the Wallet foundation.
|
||||
|
||||
@@ -59,3 +59,20 @@ fn wallet_error_codes_are_available_from_crate_root() {
|
||||
assert_eq!(code.domain(), "wallet");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_v1_envelope_and_transcript_are_available_from_crate_root() -> ksp_core_lib::Result<()> {
|
||||
let source = include_bytes!("fixtures/kspwallet_v1_wire_only.json");
|
||||
let envelope = match ksp_wallet_lib::KspWalletEnvelopeV1::parse_json(source) {
|
||||
std::result::Result::Ok(envelope) => envelope,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(envelope.format_version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V1);
|
||||
assert_eq!(envelope.owner_slot().role(), ksp_wallet_lib::WalletKeySlotRoleV1::Owner);
|
||||
assert!(envelope.view_descriptor().enabled());
|
||||
assert!(!envelope.state_transcript().is_empty());
|
||||
assert!(!envelope.owner_slot_aad().is_empty());
|
||||
assert!(envelope.view_slot_aad().is_some());
|
||||
assert!(!envelope.compartment_aad(ksp_wallet_lib::WalletCompartmentKindV1::Metadata).is_empty());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
71
crates/ksp-wallet-lib/unit_tests/transcript.rs
Normal file
71
crates/ksp-wallet-lib/unit_tests/transcript.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/transcript.rs
|
||||
// version: 1
|
||||
|
||||
use base64::Engine as _;
|
||||
|
||||
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
|
||||
|
||||
#[test]
|
||||
fn state_transcript_matches_the_frozen_v1_wire_only_canary() -> ksp_core_lib::Result<()> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(FIXTURE) {
|
||||
std::result::Result::Ok(envelope) => envelope,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(envelope.state_transcript());
|
||||
assert_eq!(
|
||||
encoded,
|
||||
"S1NQV0FMTEVULVYxLVNUQVRFAAABAAAAAAAAAAlLU1BXQUxMRVQAAgAAAAAAAAAEAAAAAQADAAAAAAAAACAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwAQAAAAAAAAAAEBABEAAAAAAAAABHZpZXcAEgAAAAAAAAAQICEiIyQlJicoKSorLC0uLwEAAAAAAAAAABAQERITFBUWFxgZGhscHR4fAQEAAAAAAAAABW93bmVyAQIAAAAAAAAACGFyZ29uMmlkAQMAAAAAAAAABAAAABMBBAAAAAAAAAAEAAEAAAEFAAAAAAAAAAQAAAADAQYAAAAAAAAABAAAAAEBBwAAAAAAAAAQMDEyMzQ1Njc4OTo7PD0-PwEIAAAAAAAAABJ4Y2hhY2hhMjAtcG9seTEzMDUBCQAAAAAAAAAYUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnAQoAAAAAAAAAIICBgoOEhYaHiImKi4yNjo-QkZKTlJWWl5iZmpucnZ6fAgAAAAAAAAAADW93bmVyLWNvbnRyb2wCAQAAAAAAAAAEAAAAAQICAAAAAAAAABJ4Y2hhY2hhMjAtcG9seTEzMDUCAwAAAAAAAAAYkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanAgQAAAAAAAAAMMDBwsPExcbHyMnKy8zNzs_Q0dLT1NXW19jZ2tvc3d7f4OHi4-Tl5ufo6err7O3u7wIAAAAAAAAAAAhtZXRhZGF0YQIBAAAAAAAAAAQAAAABAgIAAAAAAAAAEnhjaGFjaGEyMC1wb2x5MTMwNQIDAAAAAAAAABioqaqrrK2ur7CxsrO0tba3uLm6u7y9vr8CBAAAAAAAAAAwEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4_AgAAAAAAAAAABnNlY3JldAIBAAAAAAAAAAQAAAABAgIAAAAAAAAAEnhjaGFjaGEyMC1wb2x5MTMwNQIDAAAAAAAAABjAwcLDxMXGx8jJysvMzc7P0NHS09TV1tcCBAAAAAAAAABQQEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8FAAAAAAAAAAAHZWQyNTUxOQ"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slot_aad_matches_owner_and_view_canaries() -> ksp_core_lib::Result<()> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(FIXTURE) {
|
||||
std::result::Result::Ok(envelope) => envelope,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(envelope.owner_slot_aad());
|
||||
assert_eq!(
|
||||
owner,
|
||||
"S1NQV0FMTEVULVYxLUFBRC1PV05FUi1TTE9UAAABAAAAAAAAAAlLU1BXQUxMRVQAAgAAAAAAAAAEAAAAAQADAAAAAAAAACAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwEAAAAAAAAAABAQERITFBUWFxgZGhscHR4fAQEAAAAAAAAABW93bmVyAQIAAAAAAAAACGFyZ29uMmlkAQMAAAAAAAAABAAAABMBBAAAAAAAAAAEAAEAAAEFAAAAAAAAAAQAAAADAQYAAAAAAAAABAAAAAEBBwAAAAAAAAAQMDEyMzQ1Njc4OTo7PD0-PwEIAAAAAAAAABJ4Y2hhY2hhMjAtcG9seTEzMDU"
|
||||
);
|
||||
let view_aad_result = envelope.view_slot_aad();
|
||||
assert!(view_aad_result.is_some());
|
||||
let view_aad = match view_aad_result {
|
||||
std::option::Option::Some(view_aad) => view_aad,
|
||||
std::option::Option::None => return std::result::Result::Ok(()),
|
||||
};
|
||||
let view = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(view_aad);
|
||||
assert_eq!(
|
||||
view,
|
||||
"S1NQV0FMTEVULVYxLUFBRC1WSUVXLVNMT1QAAAEAAAAAAAAACUtTUFdBTExFVAACAAAAAAAAAAQAAAABAAMAAAAAAAAAIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fAQAAAAAAAAAAECAhIiMkJSYnKCkqKywtLi8BAQAAAAAAAAAEdmlldwECAAAAAAAAAAhhcmdvbjJpZAEDAAAAAAAAAAQAAAATAQQAAAAAAAAABAAAgAABBQAAAAAAAAAEAAAABAEGAAAAAAAAAAQAAAABAQcAAAAAAAAAEEBBQkNERUZHSElKS0xNTk8BCAAAAAAAAAASeGNoYWNoYTIwLXBvbHkxMzA1"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compartment_aad_is_domain_separated_by_kind() -> ksp_core_lib::Result<()> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(FIXTURE) {
|
||||
std::result::Result::Ok(envelope) => envelope,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner_control = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl));
|
||||
let metadata = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata));
|
||||
let secret = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(envelope.compartment_aad(crate::WalletCompartmentKindV1::Secret));
|
||||
assert_eq!(
|
||||
owner_control,
|
||||
"S1NQV0FMTEVULVYxLUFBRC1PV05FUi1DT05UUk9MAAABAAAAAAAAAAlLU1BXQUxMRVQAAgAAAAAAAAAEAAAAAQADAAAAAAAAACAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwIAAAAAAAAAAA1vd25lci1jb250cm9sAgEAAAAAAAAABAAAAAECAgAAAAAAAAASeGNoYWNoYTIwLXBvbHkxMzA1"
|
||||
);
|
||||
assert_eq!(
|
||||
metadata,
|
||||
"S1NQV0FMTEVULVYxLUFBRC1NRVRBREFUQQAAAQAAAAAAAAAJS1NQV0FMTEVUAAIAAAAAAAAABAAAAAEAAwAAAAAAAAAgAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8CAAAAAAAAAAAIbWV0YWRhdGECAQAAAAAAAAAEAAAAAQICAAAAAAAAABJ4Y2hhY2hhMjAtcG9seTEzMDU"
|
||||
);
|
||||
assert_eq!(
|
||||
secret,
|
||||
"S1NQV0FMTEVULVYxLUFBRC1TRUNSRVQAAAEAAAAAAAAACUtTUFdBTExFVAACAAAAAAAAAAQAAAABAAMAAAAAAAAAIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fAgAAAAAAAAAABnNlY3JldAIBAAAAAAAAAAQAAAABAgIAAAAAAAAAEnhjaGFjaGEyMC1wb2x5MTMwNQ"
|
||||
);
|
||||
assert_ne!(owner_control, metadata);
|
||||
assert_ne!(metadata, secret);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
126
crates/ksp-wallet-lib/unit_tests/wire.rs
Normal file
126
crates/ksp-wallet-lib/unit_tests/wire.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/wire.rs
|
||||
// version: 1
|
||||
|
||||
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
|
||||
|
||||
#[test]
|
||||
fn strict_v1_fixture_parses_and_round_trips_semantically() -> ksp_core_lib::Result<()> {
|
||||
let parsed = match super::KspWalletEnvelopeV1::parse_json(FIXTURE) {
|
||||
std::result::Result::Ok(parsed) => parsed,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(parsed.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1);
|
||||
assert!(parsed.view_descriptor().enabled());
|
||||
assert_eq!(parsed.owner_slot().role(), super::WalletKeySlotRoleV1::Owner);
|
||||
assert_eq!(parsed.view_slot().map(super::WalletKeySlotV1::role), std::option::Option::Some(super::WalletKeySlotRoleV1::View));
|
||||
assert_eq!(parsed.owner_control().payload_version(), 1);
|
||||
assert_eq!(parsed.metadata().payload_version(), 1);
|
||||
assert_eq!(parsed.secret().payload_version(), 1);
|
||||
|
||||
let serialized = match parsed.to_json_bytes() {
|
||||
std::result::Result::Ok(serialized) => serialized,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let reparsed = match super::KspWalletEnvelopeV1::parse_json(serialized.as_slice()) {
|
||||
std::result::Result::Ok(reparsed) => reparsed,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(parsed, reparsed);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_format_version_is_rejected_before_v1_shape_validation() {
|
||||
let source = std::string::String::from_utf8_lossy(FIXTURE).replace("\"format_version\": 1", "\"format_version\": 2");
|
||||
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_top_level_field_is_rejected() {
|
||||
let source = std::string::String::from_utf8_lossy(FIXTURE).replacen("{", "{\n \"unexpected\": true,", 1);
|
||||
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padded_or_noncanonical_base64url_is_rejected() {
|
||||
let source = std::string::String::from_utf8_lossy(FIXTURE)
|
||||
.replace("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8\"", "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=\"");
|
||||
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabled_view_descriptor_must_match_the_view_slot() {
|
||||
let source =
|
||||
std::string::String::from_utf8_lossy(FIXTURE).replacen("\"slot_id\": \"ICEiIyQlJicoKSorLC0uLw\"", "\"slot_id\": \"EBESExQVFhcYGRobHB0eHw\"", 1);
|
||||
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_or_pathological_kdf_parameters_are_rejected_before_crypto() {
|
||||
let zero_source = std::string::String::from_utf8_lossy(FIXTURE).replacen("\"memory_kib\": 65536", "\"memory_kib\": 0", 1);
|
||||
let zero_result = super::KspWalletEnvelopeV1::parse_json(zero_source.as_bytes());
|
||||
assert!(zero_result.is_err());
|
||||
let zero_error = match zero_result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(zero_error.code(), crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID);
|
||||
|
||||
let high_source = std::string::String::from_utf8_lossy(FIXTURE).replacen("\"memory_kib\": 65536", "\"memory_kib\": 1048577", 1);
|
||||
let high_result = super::KspWalletEnvelopeV1::parse_json(high_source.as_bytes());
|
||||
assert!(high_result.is_err());
|
||||
let high_error = match high_result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(high_error.code(), crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_document_is_rejected_before_json_or_crypto() {
|
||||
let oversized = std::vec![b' '; crate::KSPWALLET_MAX_FILE_BYTES + 1];
|
||||
let result = super::KspWalletEnvelopeV1::parse_json(oversized.as_slice());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_debug_does_not_render_ciphertext_contents() -> ksp_core_lib::Result<()> {
|
||||
let parsed = match super::KspWalletEnvelopeV1::parse_json(FIXTURE) {
|
||||
std::result::Result::Ok(parsed) => parsed,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rendered = format!("{parsed:?}");
|
||||
assert!(rendered.contains("ciphertext_bytes"));
|
||||
assert!(!rendered.contains("gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp8"));
|
||||
assert!(!rendered.contains("wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v"));
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
Reference in New Issue
Block a user