v0.2.5-pre.004

This commit is contained in:
2026-08-19 11:29:05 +02:00
parent d5536b0778
commit 6c1172efc3
17 changed files with 859 additions and 27 deletions

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-wallet-lib/Cargo.toml
# version: 2
# version: 3
[package]
name = "ksp-wallet-lib"
@@ -8,6 +8,9 @@ edition.workspace = true
repository.workspace = true
[dependencies]
argon2 = { workspace = true, features = ["zeroize"] }
chacha20poly1305 = { workspace = true, features = ["alloc", "zeroize"] }
getrandom.workspace = true
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
base64.workspace = true

View File

@@ -0,0 +1,199 @@
// file: crates/ksp-wallet-lib/src/crypto.rs
// version: 1
//! In-memory cryptographic primitives for native `.kspwallet` V1.
//!
//! `pre.004` intentionally lands these crate-private primitives one tranche before `pre.005` wires them into create/open capability flows.
#![allow(dead_code, reason = "pre.004 stages private Wallet crypto primitives before their pre.005 production callers")]
use chacha20poly1305::KeyInit as _;
use chacha20poly1305::aead::Aead as _;
/// Exact V1 content-key and password-derived-key size in bytes.
const SECRET_KEY_BYTES: usize = 32;
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
struct SecretKeyV1 {
bytes: [u8; SECRET_KEY_BYTES],
}
impl SecretKeyV1 {
/// Takes ownership of exact 32-byte secret material.
const fn from_bytes(bytes: [u8; SECRET_KEY_BYTES]) -> Self {
return Self { bytes };
}
/// Generates a fresh secret key from the operating-system CSPRNG.
fn random() -> ksp_core_lib::Result<Self> {
let mut bytes = [0_u8; SECRET_KEY_BYTES];
let fill_result = getrandom::fill(bytes.as_mut_slice());
if fill_result.is_err() {
zeroize::Zeroize::zeroize(&mut bytes);
return std::result::Result::Err(randomness_error());
}
return std::result::Result::Ok(Self { bytes });
}
/// Borrows secret bytes internally without allocating or cloning.
const fn as_bytes(&self) -> &[u8; SECRET_KEY_BYTES] {
return &self.bytes;
}
}
impl std::fmt::Debug for SecretKeyV1 {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("SecretKeyV1(<redacted>)");
}
}
impl std::ops::Drop for SecretKeyV1 {
fn drop(&mut self) {
zeroize::Zeroize::zeroize(&mut self.bytes);
}
}
/// Generates a fresh XChaCha20-Poly1305 nonce from the operating-system CSPRNG.
fn random_nonce() -> ksp_core_lib::Result<[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES]> {
let mut nonce = [0_u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES];
let fill_result = getrandom::fill(nonce.as_mut_slice());
if fill_result.is_err() {
zeroize::Zeroize::zeroize(&mut nonce);
return std::result::Result::Err(randomness_error());
}
return std::result::Result::Ok(nonce);
}
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParametersV1) -> ksp_core_lib::Result<SecretKeyV1> {
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
}
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
fn wrap_key(
wrapping_key: &SecretKeyV1,
key_to_wrap: &SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return encrypt_bytes(wrapping_key, nonce, aad, key_to_wrap.as_bytes());
}
/// Unwraps one 32-byte content key and maps every AEAD authentication failure to the generic Wallet authentication error.
fn unwrap_key(
wrapping_key: &SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
ciphertext: &[u8],
) -> ksp_core_lib::Result<SecretKeyV1> {
let plaintext_result = decrypt_bytes(wrapping_key, nonce, aad, ciphertext);
let mut plaintext = match plaintext_result {
std::result::Result::Ok(plaintext) => plaintext,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if plaintext.len() != SECRET_KEY_BYTES {
zeroize::Zeroize::zeroize(plaintext.as_mut_slice());
return std::result::Result::Err(authentication_error());
}
let converted = <[u8; SECRET_KEY_BYTES]>::try_from(plaintext.as_slice());
let key = match converted {
std::result::Result::Ok(key) => SecretKeyV1::from_bytes(key),
std::result::Result::Err(_) => {
zeroize::Zeroize::zeroize(plaintext.as_mut_slice());
return std::result::Result::Err(authentication_error());
},
};
zeroize::Zeroize::zeroize(plaintext.as_mut_slice());
return std::result::Result::Ok(key);
}
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
fn encrypt_bytes(
key: &SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
plaintext: &[u8],
) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let cipher_result = chacha20poly1305::XChaCha20Poly1305::new_from_slice(key.as_bytes());
let cipher = match cipher_result {
std::result::Result::Ok(cipher) => cipher,
std::result::Result::Err(_) => return std::result::Result::Err(crypto_parameter_error()),
};
let nonce_value = chacha20poly1305::XNonce::from(*nonce);
let payload = chacha20poly1305::aead::Payload { msg: plaintext, aad };
let encrypted = cipher.encrypt(&nonce_value, payload);
return match encrypted {
std::result::Result::Ok(ciphertext) => std::result::Result::Ok(ciphertext),
std::result::Result::Err(_) => std::result::Result::Err(crypto_parameter_error()),
};
}
/// Decrypts authenticated ciphertext bytes and returns a generic authentication error on tag failure.
fn decrypt_bytes(
key: &SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
ciphertext: &[u8],
) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let cipher_result = chacha20poly1305::XChaCha20Poly1305::new_from_slice(key.as_bytes());
let cipher = match cipher_result {
std::result::Result::Ok(cipher) => cipher,
std::result::Result::Err(_) => return std::result::Result::Err(crypto_parameter_error()),
};
let nonce_value = chacha20poly1305::XNonce::from(*nonce);
let payload = chacha20poly1305::aead::Payload { msg: ciphertext, aad };
let decrypted = cipher.decrypt(&nonce_value, payload);
return match decrypted {
std::result::Result::Ok(plaintext) => std::result::Result::Ok(plaintext),
std::result::Result::Err(_) => std::result::Result::Err(authentication_error()),
};
}
fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u32, parallelism: u32) -> ksp_core_lib::Result<SecretKeyV1> {
if password.is_empty() || password.len() > crate::KSPWALLET_V1_MAX_PASSWORD_BYTES {
return std::result::Result::Err(crypto_parameter_error());
}
if salt.len() < crate::KSPWALLET_V1_MIN_KDF_SALT_BYTES || salt.len() > crate::KSPWALLET_V1_MAX_KDF_SALT_BYTES {
return std::result::Result::Err(crypto_parameter_error());
}
if memory_kib == 0
|| memory_kib > crate::KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB
|| iterations == 0
|| iterations > crate::KSPWALLET_V1_MAX_ARGON2_ITERATIONS
|| parallelism == 0
|| parallelism > crate::KSPWALLET_V1_MAX_ARGON2_PARALLELISM
|| memory_kib < parallelism * 8
{
return std::result::Result::Err(crypto_parameter_error());
}
let params_result = argon2::Params::new(memory_kib, iterations, parallelism, std::option::Option::Some(SECRET_KEY_BYTES));
let params = match params_result {
std::result::Result::Ok(params) => params,
std::result::Result::Err(_) => return std::result::Result::Err(crypto_parameter_error()),
};
let argon2 = argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut output = [0_u8; SECRET_KEY_BYTES];
let derive_result = argon2.hash_password_into(password, salt, output.as_mut_slice());
if derive_result.is_err() {
zeroize::Zeroize::zeroize(&mut output);
return std::result::Result::Err(crypto_parameter_error());
}
return std::result::Result::Ok(SecretKeyV1::from_bytes(output));
}
fn randomness_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_RANDOMNESS_FAILED, "Wallet cryptographic randomness could not be obtained");
}
fn crypto_parameter_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID, "Wallet cryptographic parameters are invalid or unsupported");
}
fn authentication_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_AUTHENTICATION_FAILED, "Wallet cryptographic authentication failed");
}
#[cfg(test)]
#[path = "../unit_tests/crypto.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/error.rs
// version: 1
// version: 2
/// Error code used when a native Wallet structure is invalid.
pub const ERROR_CODE_FORMAT_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "format_invalid");
@@ -7,6 +7,8 @@ pub const ERROR_CODE_FORMAT_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::Err
pub const ERROR_CODE_FORMAT_VERSION_UNSUPPORTED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "format_version_unsupported");
/// Error code used when serialized cryptographic parameters are invalid or unsupported.
pub const ERROR_CODE_CRYPTO_PARAMETERS_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "crypto_parameters_invalid");
/// Error code used when the operating-system cryptographic random source cannot provide bytes.
pub const ERROR_CODE_RANDOMNESS_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "randomness_failed");
/// Error code used when an authenticated Wallet structure cannot be verified.
pub const ERROR_CODE_AUTHENTICATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "authentication_failed");
/// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/lib.rs
// version: 3
// version: 4
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -7,13 +7,15 @@
//! 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. `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`.
//! error contract. `0.2.5-pre.003` freezes the strict V1 JSON envelope, canonical Base64url decoding, structural limits and deterministic
//! state-transcript/AEAD-AAD byte codecs. `0.2.5-pre.004` adds the in-memory Argon2id/XChaCha20-Poly1305/CSPRNG primitives and deterministic crypto
//! vectors used by later create/open operations. It still performs no 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;
mod crypto;
mod error;
mod metadata;
mod owner;
@@ -106,6 +108,8 @@ pub use self::error::ERROR_CODE_IO_FAILED;
pub use self::error::ERROR_CODE_KEY_MATERIAL_INVALID;
/// Error code used when an OWNER unlock attempt fails without exposing a finer cryptographic oracle.
pub use self::error::ERROR_CODE_OWNER_UNLOCK_FAILED;
/// Error code used when the operating-system cryptographic random source cannot provide bytes.
pub use self::error::ERROR_CODE_RANDOMNESS_FAILED;
/// Error code used when a Wallet signing operation fails.
pub use self::error::ERROR_CODE_SIGNATURE_FAILED;
/// Error code used when an import/export transfer format is unsupported.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/wire.rs
// version: 1
// version: 3
//! Strict native `.kspwallet` V1 wire envelope.
@@ -893,6 +893,12 @@ fn parse_kdf(raw: RawKdfV1) -> ksp_core_lib::Result<WalletKdfParametersV1> {
if raw.parallelism == 0 || raw.parallelism > crate::KSPWALLET_V1_MAX_ARGON2_PARALLELISM {
return std::result::Result::Err(crypto_parameter_error("Wallet Argon2 parallelism is outside the V1 structural range", "key_slots.kdf.parallelism"));
}
if raw.memory_kib < raw.parallelism * 8 {
return std::result::Result::Err(crypto_parameter_error(
"Wallet Argon2 memory_kib must provide at least eight 1-KiB blocks per lane",
"key_slots.kdf.memory_kib",
));
}
let salt = match decode_base64url(raw.salt.as_str(), "key_slots.kdf.salt") {
std::result::Result::Ok(salt) => salt,
std::result::Result::Err(error) => return std::result::Result::Err(error),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/tests/dependency_boundary.rs
// version: 3
// version: 4
//! 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("argon2 = { workspace = true, features = [\"zeroize\"] }"));
assert!(manifest.contains("chacha20poly1305 = { workspace = true, features = [\"alloc\", \"zeroize\"] }"));
assert!(manifest.contains("getrandom.workspace = true"));
assert!(manifest.contains("base64.workspace = true"));
assert!(manifest.contains("serde = { workspace = true, features = [\"derive\"] }"));
assert!(manifest.contains("serde_json.workspace = true"));

View File

@@ -0,0 +1,16 @@
{
"vector_version": 1,
"warning": "TEST ONLY. These passwords and keys are public fixtures and MUST NOT be used for a real wallet.",
"argon2id_xchacha20poly1305_wrap": {
"password_utf8": "kspwallet-pre004-vector-owner",
"salt": "AAECAwQFBgcICQoLDA0ODw",
"memory_kib": 32,
"iterations": 2,
"parallelism": 1,
"derived_key": "zEky0GdSA4qbfHhMWRHXUcTTGj-nRkMcblqitBW9miU",
"content_key": "oKGio6SlpqeoqaqrrK2ur7CxsrO0tba3uLm6u7y9vr8",
"nonce": "QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZX",
"aad": "S1NQV0FMTEVULVYxLVBSRTAwNC1XUkFQAA",
"wrapped_key": "8SgqlNTyE033pmdv02owxOI3IFgxl0fwu-2ud609ngW_hzmXrrxRldY5YqJ5NW90"
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/tests/public_api.rs
// version: 2
// version: 3
//! Public API canaries for the Wallet foundation.
@@ -43,6 +43,7 @@ fn wallet_error_codes_are_available_from_crate_root() {
ksp_wallet_lib::ERROR_CODE_FORMAT_INVALID,
ksp_wallet_lib::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED,
ksp_wallet_lib::ERROR_CODE_CRYPTO_PARAMETERS_INVALID,
ksp_wallet_lib::ERROR_CODE_RANDOMNESS_FAILED,
ksp_wallet_lib::ERROR_CODE_AUTHENTICATION_FAILED,
ksp_wallet_lib::ERROR_CODE_VIEW_UNLOCK_FAILED,
ksp_wallet_lib::ERROR_CODE_OWNER_UNLOCK_FAILED,
@@ -54,7 +55,7 @@ fn wallet_error_codes_are_available_from_crate_root() {
ksp_wallet_lib::ERROR_CODE_KEY_MATERIAL_INVALID,
ksp_wallet_lib::ERROR_CODE_SIGNATURE_FAILED,
];
assert_eq!(codes.len(), 13);
assert_eq!(codes.len(), 14);
for code in codes {
assert_eq!(code.domain(), "wallet");
}

View File

@@ -0,0 +1,175 @@
// file: crates/ksp-wallet-lib/unit_tests/crypto.rs
// version: 1
use base64::Engine as _;
const VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_crypto_vectors.json");
#[derive(serde::Deserialize)]
struct CryptoVector {
password_utf8: std::string::String,
salt: std::string::String,
memory_kib: u32,
iterations: u32,
parallelism: u32,
derived_key: std::string::String,
content_key: std::string::String,
nonce: std::string::String,
aad: std::string::String,
wrapped_key: std::string::String,
}
#[derive(serde::Deserialize)]
struct CryptoVectors {
vector_version: u32,
warning: std::string::String,
argon2id_xchacha20poly1305_wrap: CryptoVector,
}
#[test]
fn deterministic_argon2id_and_xchacha_wrap_vector_matches_external_canary() -> ksp_core_lib::Result<()> {
let vectors_result = serde_json::from_slice::<CryptoVectors>(VECTOR);
let vectors = match vectors_result {
std::result::Result::Ok(vectors) => vectors,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet crypto test vector JSON is invalid"));
},
};
assert_eq!(vectors.vector_version, 1);
assert!(vectors.warning.contains("TEST ONLY"));
let vector = vectors.argon2id_xchacha20poly1305_wrap;
let salt = match decode(vector.salt.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let expected_derived = match decode_fixed::<32>(vector.derived_key.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let content_key = match decode_fixed::<32>(vector.content_key.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let nonce = match decode_fixed::<{ crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES }>(vector.nonce.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let aad = match decode(vector.aad.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let expected_wrapped = match decode(vector.wrapped_key.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let derived_result = super::derive_argon2id(vector.password_utf8.as_bytes(), salt.as_slice(), vector.memory_kib, vector.iterations, vector.parallelism);
let derived = match derived_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(derived.as_bytes(), &expected_derived);
let content = super::SecretKeyV1::from_bytes(content_key);
let wrapped_result = super::wrap_key(&derived, &content, &nonce, aad.as_slice());
let wrapped = match wrapped_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(wrapped, expected_wrapped);
let unwrapped_result = super::unwrap_key(&derived, &nonce, aad.as_slice(), wrapped.as_slice());
let unwrapped = match unwrapped_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(unwrapped.as_bytes(), content.as_bytes());
return std::result::Result::Ok(());
}
#[test]
fn xchacha_tampering_is_reported_as_generic_authentication_failure() -> ksp_core_lib::Result<()> {
let key = super::SecretKeyV1::from_bytes([0x11_u8; 32]);
let nonce = [0x22_u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES];
let plaintext = [0x33_u8; 32];
let encrypted_result = super::encrypt_bytes(&key, &nonce, b"kspwallet-test-aad", plaintext.as_slice());
let mut encrypted = match encrypted_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
encrypted[0] ^= 1;
let result = super::decrypt_bytes(&key, &nonce, b"kspwallet-test-aad", encrypted.as_slice());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
std::result::Result::Ok(_) => return std::result::Result::Ok(()),
};
assert_eq!(error.code(), crate::ERROR_CODE_AUTHENTICATION_FAILED);
return std::result::Result::Ok(());
}
#[test]
fn secret_key_debug_is_redacted_and_random_sources_are_callable() -> ksp_core_lib::Result<()> {
let key_result = super::SecretKeyV1::random();
let key = match key_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let nonce_result = super::random_nonce();
let nonce = match nonce_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rendered = format!("{key:?}");
assert_eq!(rendered, "SecretKeyV1(<redacted>)");
assert_eq!(nonce.len(), crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES);
return std::result::Result::Ok(());
}
#[test]
#[ignore = "operator-only Argon2 calibration; run single-threaded with --nocapture"]
fn benchmark_argon2_creation_candidates() -> ksp_core_lib::Result<()> {
let password = b"KSPWALLET-BENCHMARK-TEST-ONLY-PASSWORD";
let salt = [0x5a_u8; 16];
let candidates = [(65_536_u32, 3_u32, 1_u32), (131_072_u32, 3_u32, 1_u32), (262_144_u32, 3_u32, 1_u32)];
for candidate in candidates {
let started = std::time::Instant::now();
let result = super::derive_argon2id(password, salt.as_slice(), candidate.0, candidate.1, candidate.2);
match result {
std::result::Result::Ok(_) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
println!(
"KSP Argon2id candidate memory_kib={} iterations={} parallelism={} elapsed_ms={}",
candidate.0,
candidate.1,
candidate.2,
started.elapsed().as_millis()
);
}
return std::result::Result::Ok(());
}
fn decode(value: &str) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(value.as_bytes());
return match decoded {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => {
std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet crypto test vector Base64url is invalid"))
},
};
}
fn decode_fixed<const LENGTH: usize>(value: &str) -> ksp_core_lib::Result<[u8; LENGTH]> {
let decoded = match decode(value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let converted = <[u8; LENGTH]>::try_from(decoded.as_slice());
return match converted {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => {
std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet crypto test vector has an invalid fixed-size field"))
},
};
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/unit_tests/wire.rs
// version: 1
// version: 2
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
@@ -100,6 +100,22 @@ fn zero_or_pathological_kdf_parameters_are_rejected_before_crypto() {
assert_eq!(high_error.code(), crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID);
}
#[test]
fn argon2_memory_must_cover_all_lanes_before_crypto() {
let source = std::string::String::from_utf8_lossy(FIXTURE).replacen("\"memory_kib\": 65536", "\"memory_kib\": 8", 1).replacen(
"\"parallelism\": 1",
"\"parallelism\": 2",
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_CRYPTO_PARAMETERS_INVALID);
}
#[test]
fn oversized_document_is_rejected_before_json_or_crypto() {
let oversized = std::vec![b' '; crate::KSPWALLET_MAX_FILE_BYTES + 1];