v0.2.5-pre.009
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
// file: crates/ksp-wallet-lib/src/crypto.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! In-memory cryptographic primitives for native `.kspwallet` V1.
|
||||
|
||||
use chacha20poly1305::KeyInit as _;
|
||||
use chacha20poly1305::aead::Aead as _;
|
||||
use chacha20poly1305::KeyInit; // rust-rules: derive-import
|
||||
use chacha20poly1305::aead::Aead; // rust-rules: derive-import
|
||||
|
||||
/// Exact V1 content-key and password-derived-key size in bytes.
|
||||
pub(crate) const SECRET_KEY_BYTES: usize = 32;
|
||||
@@ -14,7 +14,7 @@ pub(crate) struct SecretKeyV1 {
|
||||
bytes: [u8; SECRET_KEY_BYTES],
|
||||
}
|
||||
|
||||
impl SecretKeyV1 {
|
||||
impl crate::SecretKeyV1 {
|
||||
/// Takes ownership of exact 32-byte secret material.
|
||||
pub(crate) const fn from_bytes(bytes: [u8; SECRET_KEY_BYTES]) -> Self {
|
||||
return Self { bytes };
|
||||
@@ -35,13 +35,13 @@ impl SecretKeyV1 {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SecretKeyV1 {
|
||||
impl std::fmt::Debug for crate::SecretKeyV1 {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.write_str("SecretKeyV1(<redacted>)");
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Drop for SecretKeyV1 {
|
||||
impl std::ops::Drop for crate::SecretKeyV1 {
|
||||
fn drop(&mut self) {
|
||||
zeroize::Zeroize::zeroize(&mut self.bytes);
|
||||
}
|
||||
@@ -64,28 +64,28 @@ pub(crate) fn random_nonce() -> ksp_core_lib::Result<[u8; crate::KSPWALLET_V1_XC
|
||||
}
|
||||
|
||||
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
|
||||
pub(crate) fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParametersV1) -> ksp_core_lib::Result<SecretKeyV1> {
|
||||
pub(crate) fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::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.
|
||||
pub(crate) fn wrap_key(
|
||||
wrapping_key: &SecretKeyV1,
|
||||
key_to_wrap: &SecretKeyV1,
|
||||
wrapping_key: &crate::SecretKeyV1,
|
||||
key_to_wrap: &crate::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());
|
||||
return crate::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.
|
||||
pub(crate) fn unwrap_key(
|
||||
wrapping_key: &SecretKeyV1,
|
||||
wrapping_key: &crate::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);
|
||||
) -> ksp_core_lib::Result<crate::SecretKeyV1> {
|
||||
let plaintext_result = crate::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),
|
||||
@@ -96,7 +96,7 @@ pub(crate) fn unwrap_key(
|
||||
}
|
||||
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::Ok(key) => crate::SecretKeyV1::from_bytes(key),
|
||||
std::result::Result::Err(_) => {
|
||||
zeroize::Zeroize::zeroize(plaintext.as_mut_slice());
|
||||
return std::result::Result::Err(authentication_error());
|
||||
@@ -108,7 +108,7 @@ pub(crate) fn unwrap_key(
|
||||
|
||||
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
|
||||
pub(crate) fn encrypt_bytes(
|
||||
key: &SecretKeyV1,
|
||||
key: &crate::SecretKeyV1,
|
||||
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
aad: &[u8],
|
||||
plaintext: &[u8],
|
||||
@@ -129,7 +129,7 @@ pub(crate) fn encrypt_bytes(
|
||||
|
||||
/// Decrypts authenticated ciphertext bytes and returns a generic authentication error on tag failure.
|
||||
pub(crate) fn decrypt_bytes(
|
||||
key: &SecretKeyV1,
|
||||
key: &crate::SecretKeyV1,
|
||||
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
aad: &[u8],
|
||||
ciphertext: &[u8],
|
||||
@@ -148,7 +148,7 @@ pub(crate) fn decrypt_bytes(
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u32, parallelism: u32) -> ksp_core_lib::Result<SecretKeyV1> {
|
||||
fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u32, parallelism: u32) -> ksp_core_lib::Result<crate::SecretKeyV1> {
|
||||
if password.is_empty() || password.len() > crate::KSPWALLET_V1_MAX_PASSWORD_BYTES {
|
||||
return std::result::Result::Err(crypto_parameter_error());
|
||||
}
|
||||
@@ -178,7 +178,7 @@ pub(crate) fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, ite
|
||||
zeroize::Zeroize::zeroize(&mut output);
|
||||
return std::result::Result::Err(crypto_parameter_error());
|
||||
}
|
||||
return std::result::Result::Ok(SecretKeyV1::from_bytes(output));
|
||||
return std::result::Result::Ok(crate::SecretKeyV1::from_bytes(output));
|
||||
}
|
||||
|
||||
fn randomness_error() -> ksp_core_lib::Error {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// file: crates/ksp-wallet-lib/src/lib.rs
|
||||
// version: 8
|
||||
// version: 10
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -13,7 +14,8 @@
|
||||
//! `0.2.5-pre.006` adds bounded async-first filesystem reads plus same-directory synchronized no-clobber publication for new native files. `0.2.5-pre.007`
|
||||
//! adds Solana message signing, protected metadata administration, password rotation and strong VIEW disable/recreate with capability-bound atomic
|
||||
//! replacement. `0.2.5-pre.008` adds bounded Solana CLI JSON and canonical full-keypair Base58 import/export adapters with safe inspection and no-clobber
|
||||
//! native import/export publication. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
|
||||
//! native import/export publication. `0.2.5-pre.009` adds adversarial security/compliance canaries and records the final dependency/interop audit before
|
||||
//! documentation closure. 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`.
|
||||
|
||||
@@ -32,6 +34,10 @@ mod view;
|
||||
mod wallet;
|
||||
mod wire;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/security.rs"]
|
||||
mod security_tests;
|
||||
|
||||
/// Authorized capability represented by an unlocked Wallet handle.
|
||||
pub use self::capability::WalletCapability;
|
||||
/// Native `.kspwallet` V1 format version.
|
||||
@@ -221,6 +227,26 @@ pub use self::wire::WalletViewDescriptorV1;
|
||||
|
||||
/// Wallet-owned tracing target used by the KSP logging facade.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Exact V1 content-key and password-derived-key size in bytes.
|
||||
pub(crate) use self::crypto::SECRET_KEY_BYTES;
|
||||
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
|
||||
pub(crate) use self::crypto::SecretKeyV1;
|
||||
/// Decrypts authenticated ciphertext bytes and returns a generic authentication error on tag failure.
|
||||
pub(crate) use self::crypto::decrypt_bytes;
|
||||
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
|
||||
pub(crate) use self::crypto::derive_password_key;
|
||||
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
|
||||
pub(crate) use self::crypto::encrypt_bytes;
|
||||
/// Generates a fresh fixed-size byte array from the operating-system CSPRNG.
|
||||
pub(crate) use self::crypto::random_bytes;
|
||||
/// Generates a fresh XChaCha20-Poly1305 nonce from the operating-system CSPRNG.
|
||||
pub(crate) use self::crypto::random_nonce;
|
||||
/// Unwraps one 32-byte content key and maps every AEAD authentication failure to the generic Wallet authentication error.
|
||||
pub(crate) use self::crypto::unwrap_key;
|
||||
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
|
||||
pub(crate) use self::crypto::wrap_key;
|
||||
pub(crate) use self::payload::MetadataPayloadV1;
|
||||
pub(crate) use self::payload::decode_metadata_payload;
|
||||
/// Internal no-clobber native persistence path shared by transfer adapters.
|
||||
pub(crate) use self::persistence::persist_new_wallet_content_v1;
|
||||
/// Internal deterministic compartment-AAD codec shared by Wallet crypto layers.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/owner.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
/// Authorized OWNER capability handle.
|
||||
///
|
||||
@@ -119,7 +119,7 @@ impl WalletOwner {
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
alias: std::option::Option<std::string::String>,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
|
||||
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
|
||||
let mutation_result = payload.set_alias(alias);
|
||||
if let std::result::Result::Err(error) = mutation_result {
|
||||
return std::result::Result::Err(error);
|
||||
@@ -133,7 +133,7 @@ impl WalletOwner {
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
text: std::string::String,
|
||||
) -> ksp_core_lib::Result<std::string::String> {
|
||||
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
|
||||
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
|
||||
let note_id = match payload.add_note(text) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -152,7 +152,7 @@ impl WalletOwner {
|
||||
note_id: &str,
|
||||
text: std::string::String,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
|
||||
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
|
||||
let mutation_result = payload.update_note(note_id, text);
|
||||
if let std::result::Result::Err(error) = mutation_result {
|
||||
return std::result::Result::Err(error);
|
||||
@@ -162,7 +162,7 @@ impl WalletOwner {
|
||||
|
||||
/// Deletes one protected note selected by its stable identifier.
|
||||
pub async fn delete_note(&mut self, destination: impl std::convert::AsRef<std::path::Path>, note_id: &str) -> ksp_core_lib::Result<()> {
|
||||
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
|
||||
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
|
||||
let mutation_result = payload.delete_note(note_id);
|
||||
if let std::result::Result::Err(error) = mutation_result {
|
||||
return std::result::Result::Err(error);
|
||||
@@ -274,7 +274,7 @@ impl WalletOwner {
|
||||
async fn persist_metadata_payload(
|
||||
&mut self,
|
||||
destination: std::path::PathBuf,
|
||||
payload: crate::payload::MetadataPayloadV1,
|
||||
payload: crate::MetadataPayloadV1,
|
||||
operation: &'static str,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let (envelope, info) = match self.state.stage_metadata_payload(payload) {
|
||||
@@ -292,6 +292,12 @@ impl WalletOwner {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WalletOwner {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("WalletOwner").field("info", &self.info).field("unlocked_state", &"<redacted>").finish();
|
||||
}
|
||||
}
|
||||
|
||||
async fn persist_staged(
|
||||
destination: std::path::PathBuf,
|
||||
expected_current: &crate::KspWalletEnvelopeV1,
|
||||
@@ -304,12 +310,6 @@ async fn persist_staged(
|
||||
return crate::persistence::replace_wallet_file_v1(destination, expected_current.clone(), serialized).await;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WalletOwner {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("WalletOwner").field("info", &self.info).field("unlocked_state", &"<redacted>").finish();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/administration.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: crates/ksp-wallet-lib/src/payload.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Plaintext payload codecs protected inside native `.kspwallet` V1 compartments.
|
||||
|
||||
use base64::Engine as _;
|
||||
use std::str::FromStr as _;
|
||||
use base64::Engine; // rust-rules: derive-import
|
||||
use std::str::FromStr; // rust-rules: derive-import
|
||||
|
||||
pub(crate) struct MetadataPayloadV1 {
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
@@ -12,7 +12,7 @@ pub(crate) struct MetadataPayloadV1 {
|
||||
notes: std::vec::Vec<crate::WalletNote>,
|
||||
}
|
||||
|
||||
impl MetadataPayloadV1 {
|
||||
impl crate::MetadataPayloadV1 {
|
||||
pub(crate) const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.pubkey;
|
||||
}
|
||||
@@ -117,16 +117,16 @@ impl MetadataPayloadV1 {
|
||||
|
||||
pub(crate) struct OwnerControlMaterialV1 {
|
||||
admin_signing_secret: [u8; crate::crypto::SECRET_KEY_BYTES],
|
||||
metadata_key: crate::crypto::SecretKeyV1,
|
||||
secret_key: crate::crypto::SecretKeyV1,
|
||||
metadata_key: crate::SecretKeyV1,
|
||||
secret_key: crate::SecretKeyV1,
|
||||
}
|
||||
|
||||
impl OwnerControlMaterialV1 {
|
||||
pub(crate) fn into_parts(mut self) -> ([u8; crate::crypto::SECRET_KEY_BYTES], crate::crypto::SecretKeyV1, crate::crypto::SecretKeyV1) {
|
||||
pub(crate) fn into_parts(mut self) -> ([u8; crate::crypto::SECRET_KEY_BYTES], crate::SecretKeyV1, crate::SecretKeyV1) {
|
||||
let mut admin_signing_secret = [0_u8; crate::crypto::SECRET_KEY_BYTES];
|
||||
std::mem::swap(&mut admin_signing_secret, &mut self.admin_signing_secret);
|
||||
let metadata_key = std::mem::replace(&mut self.metadata_key, crate::crypto::SecretKeyV1::from_bytes([0_u8; crate::crypto::SECRET_KEY_BYTES]));
|
||||
let secret_key = std::mem::replace(&mut self.secret_key, crate::crypto::SecretKeyV1::from_bytes([0_u8; crate::crypto::SECRET_KEY_BYTES]));
|
||||
let metadata_key = std::mem::replace(&mut self.metadata_key, crate::SecretKeyV1::from_bytes([0_u8; crate::crypto::SECRET_KEY_BYTES]));
|
||||
let secret_key = std::mem::replace(&mut self.secret_key, crate::SecretKeyV1::from_bytes([0_u8; crate::crypto::SECRET_KEY_BYTES]));
|
||||
return (admin_signing_secret, metadata_key, secret_key);
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ struct RawMetadataNoteV1 {
|
||||
pub(crate) fn encode_initial_metadata_payload(
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
metadata: crate::WalletCreateMetadataV1,
|
||||
) -> ksp_core_lib::Result<(std::vec::Vec<u8>, MetadataPayloadV1)> {
|
||||
) -> ksp_core_lib::Result<(std::vec::Vec<u8>, crate::MetadataPayloadV1)> {
|
||||
let (alias, note_texts) = metadata.into_parts();
|
||||
let validation_result = validate_alias(alias.as_deref());
|
||||
if let std::result::Result::Err(error) = validation_result {
|
||||
@@ -183,7 +183,7 @@ pub(crate) fn encode_initial_metadata_payload(
|
||||
notes.push(crate::WalletNote::new(note_id, text));
|
||||
}
|
||||
|
||||
let payload = MetadataPayloadV1 { pubkey, alias, notes };
|
||||
let payload = crate::MetadataPayloadV1 { pubkey, alias, notes };
|
||||
let serialized = match payload.encode() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -191,7 +191,7 @@ pub(crate) fn encode_initial_metadata_payload(
|
||||
return std::result::Result::Ok((serialized, payload));
|
||||
}
|
||||
|
||||
pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<MetadataPayloadV1> {
|
||||
pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<crate::MetadataPayloadV1> {
|
||||
if source.len() > crate::KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES {
|
||||
return std::result::Result::Err(metadata_error("Wallet metadata payload exceeds the V1 plaintext limit", "metadata"));
|
||||
}
|
||||
@@ -236,13 +236,13 @@ pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<Met
|
||||
}
|
||||
notes.push(crate::WalletNote::new(raw_note.id, raw_note.text));
|
||||
}
|
||||
return std::result::Result::Ok(MetadataPayloadV1 { pubkey, alias: raw.alias, notes });
|
||||
return std::result::Result::Ok(crate::MetadataPayloadV1 { pubkey, alias: raw.alias, notes });
|
||||
}
|
||||
|
||||
pub(crate) fn encode_owner_control_payload(
|
||||
admin_signing_secret: &[u8; crate::crypto::SECRET_KEY_BYTES],
|
||||
metadata_key: &crate::crypto::SecretKeyV1,
|
||||
secret_key: &crate::crypto::SecretKeyV1,
|
||||
metadata_key: &crate::SecretKeyV1,
|
||||
secret_key: &crate::SecretKeyV1,
|
||||
) -> [u8; crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES] {
|
||||
let mut output = [0_u8; crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES];
|
||||
output[0..32].copy_from_slice(admin_signing_secret);
|
||||
@@ -263,8 +263,8 @@ pub(crate) fn decode_owner_control_payload(source: &[u8]) -> ksp_core_lib::Resul
|
||||
secret_key.copy_from_slice(&source[64..96]);
|
||||
return std::result::Result::Ok(OwnerControlMaterialV1 {
|
||||
admin_signing_secret,
|
||||
metadata_key: crate::crypto::SecretKeyV1::from_bytes(metadata_key),
|
||||
secret_key: crate::crypto::SecretKeyV1::from_bytes(secret_key),
|
||||
metadata_key: crate::SecretKeyV1::from_bytes(metadata_key),
|
||||
secret_key: crate::SecretKeyV1::from_bytes(secret_key),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// file: crates/ksp-wallet-lib/src/persistence.rs
|
||||
// version: 4
|
||||
// version: 6
|
||||
|
||||
//! Async-first native Wallet V1 filesystem persistence.
|
||||
|
||||
use std::io::{Read as _, Write as _};
|
||||
use std::io::Read; // rust-rules: derive-import
|
||||
use std::io::Write; // rust-rules: derive-import
|
||||
|
||||
/// Creates a new native `.kspwallet` V1 at `destination` without overwriting an existing path.
|
||||
///
|
||||
@@ -82,26 +83,10 @@ pub async fn inspect_locked_wallet_file_v1(source: impl std::convert::AsRef<std:
|
||||
return crate::inspect_locked_wallet_v1(bytes.as_slice());
|
||||
}
|
||||
|
||||
async fn read_wallet_file_async(source: std::path::PathBuf) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
let task = tokio::task::spawn_blocking(move || return read_wallet_file_blocking(source.as_path()));
|
||||
return match task.await {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(error) => std::result::Result::Err(blocking_io_error("read_task", error)),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) async fn persist_new_wallet_content_v1(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||
return persist_new_wallet_async(destination, content).await;
|
||||
}
|
||||
|
||||
async fn persist_new_wallet_async(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||
let task = tokio::task::spawn_blocking(move || return persist_new_wallet_blocking(destination.as_path(), content.as_slice()));
|
||||
return match task.await {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(error) => std::result::Result::Err(blocking_atomic_error("create_task", error)),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) async fn replace_wallet_file_v1(
|
||||
destination: std::path::PathBuf,
|
||||
expected_current: crate::KspWalletEnvelopeV1,
|
||||
@@ -116,6 +101,22 @@ pub(crate) async fn replace_wallet_file_v1(
|
||||
};
|
||||
}
|
||||
|
||||
async fn read_wallet_file_async(source: std::path::PathBuf) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
let task = tokio::task::spawn_blocking(move || return read_wallet_file_blocking(source.as_path()));
|
||||
return match task.await {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(error) => std::result::Result::Err(blocking_io_error("read_task", error)),
|
||||
};
|
||||
}
|
||||
|
||||
async fn persist_new_wallet_async(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||
let task = tokio::task::spawn_blocking(move || return persist_new_wallet_blocking(destination.as_path(), content.as_slice()));
|
||||
return match task.await {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(error) => std::result::Result::Err(blocking_atomic_error("create_task", error)),
|
||||
};
|
||||
}
|
||||
|
||||
fn read_wallet_file_blocking(source: &std::path::Path) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
let opened = std::fs::File::open(source);
|
||||
let file = match opened {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// file: crates/ksp-wallet-lib/src/transfer.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Explicit OWNER-only Solana keypair import/export adapters.
|
||||
|
||||
use std::io::{Read as _, Write as _};
|
||||
use zeroize::Zeroize as _;
|
||||
use std::io::Read; // rust-rules: derive-import
|
||||
use std::io::Write; // rust-rules: derive-import
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt; // rust-rules: derive-import
|
||||
use zeroize::Zeroize; // rust-rules: derive-import
|
||||
|
||||
/// Explicit secret-transfer formats supported by Wallet `0.2.5`.
|
||||
#[non_exhaustive]
|
||||
@@ -317,8 +320,6 @@ fn destination_parent(destination: &std::path::Path) -> ksp_core_lib::Result<&st
|
||||
|
||||
#[cfg(unix)]
|
||||
fn set_private_permissions_best_effort(file: &std::fs::File) {
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
|
||||
let permissions = std::fs::Permissions::from_mode(0o600);
|
||||
if let std::result::Result::Err(error) = file.set_permissions(permissions) {
|
||||
ksp_logging_lib::warn!(
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// file: crates/ksp-wallet-lib/src/wallet.rs
|
||||
// version: 5
|
||||
// version: 7
|
||||
|
||||
//! In-memory native Wallet V1 create/open orchestration.
|
||||
|
||||
use ed25519_dalek::Signer as _;
|
||||
use zeroize::Zeroize as _;
|
||||
use ed25519_dalek::Signer; // rust-rules: derive-import
|
||||
use zeroize::Zeroize; // rust-rules: derive-import
|
||||
|
||||
pub(crate) struct OwnerStateV1 {
|
||||
envelope: crate::KspWalletEnvelopeV1,
|
||||
owner_root: std::option::Option<crate::crypto::SecretKeyV1>,
|
||||
metadata_key: std::option::Option<crate::crypto::SecretKeyV1>,
|
||||
secret_key: std::option::Option<crate::crypto::SecretKeyV1>,
|
||||
owner_root: std::option::Option<crate::SecretKeyV1>,
|
||||
metadata_key: std::option::Option<crate::SecretKeyV1>,
|
||||
secret_key: std::option::Option<crate::SecretKeyV1>,
|
||||
admin_signing_key: std::option::Option<ed25519_dalek::SigningKey>,
|
||||
solana_keypair: std::option::Option<solana_keypair::Keypair>,
|
||||
}
|
||||
@@ -18,9 +18,9 @@ pub(crate) struct OwnerStateV1 {
|
||||
impl OwnerStateV1 {
|
||||
pub(crate) fn new(
|
||||
envelope: crate::KspWalletEnvelopeV1,
|
||||
owner_root: crate::crypto::SecretKeyV1,
|
||||
metadata_key: crate::crypto::SecretKeyV1,
|
||||
secret_key: crate::crypto::SecretKeyV1,
|
||||
owner_root: crate::SecretKeyV1,
|
||||
metadata_key: crate::SecretKeyV1,
|
||||
secret_key: crate::SecretKeyV1,
|
||||
admin_signing_key: ed25519_dalek::SigningKey,
|
||||
solana_keypair: solana_keypair::Keypair,
|
||||
) -> Self {
|
||||
@@ -43,7 +43,7 @@ impl OwnerStateV1 {
|
||||
return;
|
||||
}
|
||||
|
||||
pub(crate) fn apply_strong_view_state(&mut self, envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::crypto::SecretKeyV1) {
|
||||
pub(crate) fn apply_strong_view_state(&mut self, envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::SecretKeyV1) {
|
||||
self.envelope = envelope;
|
||||
let previous = self.metadata_key.replace(metadata_key);
|
||||
drop(previous);
|
||||
@@ -80,8 +80,8 @@ impl OwnerStateV1 {
|
||||
std::option::Option::None => return std::result::Result::Err(signing_error()),
|
||||
};
|
||||
let mut keypair_bytes = keypair.to_bytes();
|
||||
let mut seed = [0_u8; crate::crypto::SECRET_KEY_BYTES];
|
||||
seed.copy_from_slice(&keypair_bytes[0..crate::crypto::SECRET_KEY_BYTES]);
|
||||
let mut seed = [0_u8; crate::SECRET_KEY_BYTES];
|
||||
seed.copy_from_slice(&keypair_bytes[0..crate::SECRET_KEY_BYTES]);
|
||||
let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
|
||||
let signature = signing_key.sign(message).to_bytes();
|
||||
seed.zeroize();
|
||||
@@ -89,10 +89,7 @@ impl OwnerStateV1 {
|
||||
return std::result::Result::Ok(signature);
|
||||
}
|
||||
|
||||
pub(crate) fn stage_metadata_payload(
|
||||
&self,
|
||||
payload: crate::payload::MetadataPayloadV1,
|
||||
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::WalletInfo)> {
|
||||
pub(crate) fn stage_metadata_payload(&self, payload: crate::MetadataPayloadV1) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::WalletInfo)> {
|
||||
let metadata_key = match self.metadata_key.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crypto_operation_error()),
|
||||
@@ -105,14 +102,14 @@ impl OwnerStateV1 {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let nonce = match crate::crypto::random_nonce() {
|
||||
let nonce = match crate::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
plaintext.zeroize();
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
let encrypted = crate::crypto::encrypt_bytes(
|
||||
let encrypted = crate::encrypt_bytes(
|
||||
metadata_key,
|
||||
&nonce,
|
||||
self.envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
|
||||
@@ -204,7 +201,7 @@ impl OwnerStateV1 {
|
||||
return verify_and_return(envelope);
|
||||
}
|
||||
|
||||
pub(crate) fn stage_disable_view(&self) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::SecretKeyV1)> {
|
||||
pub(crate) fn stage_disable_view(&self) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> {
|
||||
if !self.envelope.view_descriptor().enabled() {
|
||||
return std::result::Result::Err(capability_error("Wallet VIEW capability is already disabled"));
|
||||
}
|
||||
@@ -214,12 +211,12 @@ impl OwnerStateV1 {
|
||||
pub(crate) async fn stage_recreate_view(
|
||||
&self,
|
||||
new_password: crate::ViewPassword,
|
||||
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::SecretKeyV1)> {
|
||||
let slot_id = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
|
||||
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> {
|
||||
let slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let new_metadata_key = match crate::crypto::SecretKeyV1::random() {
|
||||
let new_metadata_key = match crate::SecretKeyV1::random() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -234,8 +231,8 @@ impl OwnerStateV1 {
|
||||
fn stage_strong_view_change(
|
||||
&self,
|
||||
view_slot: std::option::Option<crate::WalletKeySlotV1>,
|
||||
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::SecretKeyV1)> {
|
||||
let new_metadata_key = match crate::crypto::SecretKeyV1::random() {
|
||||
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> {
|
||||
let new_metadata_key = match crate::SecretKeyV1::random() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -245,8 +242,8 @@ impl OwnerStateV1 {
|
||||
fn stage_strong_view_change_with_key(
|
||||
&self,
|
||||
view_slot: std::option::Option<crate::WalletKeySlotV1>,
|
||||
new_metadata_key: crate::crypto::SecretKeyV1,
|
||||
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::SecretKeyV1)> {
|
||||
new_metadata_key: crate::SecretKeyV1,
|
||||
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> {
|
||||
let current_metadata_key = match self.metadata_key.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crypto_operation_error()),
|
||||
@@ -264,7 +261,7 @@ impl OwnerStateV1 {
|
||||
std::option::Option::None => return std::result::Result::Err(crypto_operation_error()),
|
||||
};
|
||||
|
||||
let current_plaintext_result = crate::crypto::decrypt_bytes(
|
||||
let current_plaintext_result = crate::decrypt_bytes(
|
||||
current_metadata_key,
|
||||
self.envelope.metadata().nonce(),
|
||||
self.envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
|
||||
@@ -274,14 +271,14 @@ impl OwnerStateV1 {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
|
||||
};
|
||||
let metadata_nonce = match crate::crypto::random_nonce() {
|
||||
let metadata_nonce = match crate::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
metadata_plaintext.zeroize();
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
let metadata_ciphertext_result = crate::crypto::encrypt_bytes(
|
||||
let metadata_ciphertext_result = crate::encrypt_bytes(
|
||||
&new_metadata_key,
|
||||
&metadata_nonce,
|
||||
self.envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
|
||||
@@ -297,14 +294,14 @@ impl OwnerStateV1 {
|
||||
let mut admin_secret = admin_signing_key.to_bytes();
|
||||
let mut owner_control_plaintext = crate::payload::encode_owner_control_payload(&admin_secret, &new_metadata_key, secret_key);
|
||||
admin_secret.zeroize();
|
||||
let owner_control_nonce = match crate::crypto::random_nonce() {
|
||||
let owner_control_nonce = match crate::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
owner_control_plaintext.zeroize();
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
let owner_control_ciphertext_result = crate::crypto::encrypt_bytes(
|
||||
let owner_control_ciphertext_result = crate::encrypt_bytes(
|
||||
owner_root,
|
||||
&owner_control_nonce,
|
||||
self.envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
|
||||
@@ -357,11 +354,11 @@ impl std::ops::Drop for OwnerStateV1 {
|
||||
|
||||
pub(crate) struct ViewStateV1 {
|
||||
envelope: crate::KspWalletEnvelopeV1,
|
||||
metadata_key: std::option::Option<crate::crypto::SecretKeyV1>,
|
||||
metadata_key: std::option::Option<crate::SecretKeyV1>,
|
||||
}
|
||||
|
||||
impl ViewStateV1 {
|
||||
pub(crate) fn new(envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::crypto::SecretKeyV1) -> Self {
|
||||
pub(crate) fn new(envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::SecretKeyV1) -> Self {
|
||||
return Self { envelope, metadata_key: std::option::Option::Some(metadata_key) };
|
||||
}
|
||||
|
||||
@@ -411,6 +408,18 @@ impl std::ops::Drop for ViewStateV1 {
|
||||
}
|
||||
}
|
||||
|
||||
enum PasswordRotationV1 {
|
||||
Owner(crate::OwnerPassword),
|
||||
View(crate::ViewPassword),
|
||||
}
|
||||
|
||||
struct ViewCreationMaterialV1 {
|
||||
slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
|
||||
kdf: crate::WalletKdfParametersV1,
|
||||
wrap_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
password: crate::ViewPassword,
|
||||
}
|
||||
|
||||
/// Creates a new in-memory native `.kspwallet` V1 with a fresh Solana keypair.
|
||||
///
|
||||
/// The optional VIEW password creates an independent VIEW slot. This function performs no filesystem I/O; use [`crate::create_wallet_file_v1`] when
|
||||
@@ -420,7 +429,7 @@ pub async fn create_wallet_v1(
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadataV1,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let mut solana_secret = match crate::crypto::random_bytes::<32>() {
|
||||
let mut solana_secret = match crate::random_bytes::<32>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -429,26 +438,204 @@ pub async fn create_wallet_v1(
|
||||
return crate::create_wallet_v1_from_keypair(owner_password, view_password, metadata, solana_keypair).await;
|
||||
}
|
||||
|
||||
/// Opens the VIEW capability from a native `.kspwallet` V1 JSON document.
|
||||
pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result<crate::WalletView> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verify_result = verify_state_signature(&envelope);
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let view_slot = match envelope.view_slot() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
|
||||
};
|
||||
let derived_result = derive_view_password_key_async(password, view_slot.kdf().clone()).await;
|
||||
let derived = match derived_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let aad = envelope.view_slot_aad();
|
||||
let view_aad = match aad {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
|
||||
};
|
||||
let metadata_key_result = crate::unwrap_key(&derived, view_slot.wrap().nonce(), view_aad.as_slice(), view_slot.wrap().ciphertext());
|
||||
let metadata_key = match metadata_key_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
|
||||
};
|
||||
let metadata_plaintext_result = crate::decrypt_bytes(
|
||||
&metadata_key,
|
||||
envelope.metadata().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
|
||||
envelope.metadata().ciphertext(),
|
||||
);
|
||||
let mut metadata_plaintext = match metadata_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
|
||||
};
|
||||
let metadata_payload_result = crate::decode_metadata_payload(metadata_plaintext.as_slice());
|
||||
metadata_plaintext.zeroize();
|
||||
let metadata_payload = match metadata_payload_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let info = metadata_payload.into_info(crate::WalletCapability::View);
|
||||
let state = ViewStateV1::new(envelope, metadata_key);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_open_view",
|
||||
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
||||
capability = "view",
|
||||
"native wallet VIEW capability opened"
|
||||
);
|
||||
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, state));
|
||||
}
|
||||
|
||||
/// Opens the OWNER capability from a native `.kspwallet` V1 JSON document.
|
||||
pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verify_result = verify_state_signature(&envelope);
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let owner_slot = envelope.owner_slot();
|
||||
let derived_result = derive_owner_password_key_async(password, owner_slot.kdf().clone()).await;
|
||||
let derived = match derived_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner_root_result = crate::unwrap_key(&derived, owner_slot.wrap().nonce(), envelope.owner_slot_aad().as_slice(), owner_slot.wrap().ciphertext());
|
||||
let owner_root = match owner_root_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()),
|
||||
};
|
||||
let owner_control_plaintext_result = crate::decrypt_bytes(
|
||||
&owner_root,
|
||||
envelope.owner_control().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
|
||||
envelope.owner_control().ciphertext(),
|
||||
);
|
||||
let mut owner_control_plaintext = match owner_control_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()),
|
||||
};
|
||||
let control_result = crate::payload::decode_owner_control_payload(owner_control_plaintext.as_slice());
|
||||
owner_control_plaintext.zeroize();
|
||||
let control = match control_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let (mut admin_secret, metadata_key, secret_key) = control.into_parts();
|
||||
let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret);
|
||||
admin_secret.zeroize();
|
||||
if admin_signing_key.verifying_key().to_bytes() != *envelope.owner_auth_public_key() {
|
||||
return std::result::Result::Err(authentication_error());
|
||||
}
|
||||
|
||||
let metadata_plaintext_result = crate::decrypt_bytes(
|
||||
&metadata_key,
|
||||
envelope.metadata().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
|
||||
envelope.metadata().ciphertext(),
|
||||
);
|
||||
let mut metadata_plaintext = match metadata_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
|
||||
};
|
||||
let metadata_payload_result = crate::decode_metadata_payload(metadata_plaintext.as_slice());
|
||||
metadata_plaintext.zeroize();
|
||||
let metadata_payload = match metadata_payload_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let secret_plaintext_result = crate::decrypt_bytes(
|
||||
&secret_key,
|
||||
envelope.secret().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(),
|
||||
envelope.secret().ciphertext(),
|
||||
);
|
||||
let mut secret_plaintext = match secret_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
|
||||
};
|
||||
if secret_plaintext.len() != crate::KSPWALLET_V1_SECRET_PLAINTEXT_BYTES {
|
||||
secret_plaintext.zeroize();
|
||||
return std::result::Result::Err(key_material_error());
|
||||
}
|
||||
let keypair_result = solana_keypair::Keypair::try_from(secret_plaintext.as_slice());
|
||||
let solana_keypair = match keypair_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
secret_plaintext.zeroize();
|
||||
return std::result::Result::Err(key_material_error());
|
||||
},
|
||||
};
|
||||
let secret_pubkey_result = pubkey_from_keypair_bytes(secret_plaintext.as_slice());
|
||||
let secret_pubkey = match secret_pubkey_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
secret_plaintext.zeroize();
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
secret_plaintext.zeroize();
|
||||
if &secret_pubkey != metadata_payload.pubkey() {
|
||||
return std::result::Result::Err(key_material_error());
|
||||
}
|
||||
|
||||
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
|
||||
let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_open_owner",
|
||||
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
||||
capability = "owner",
|
||||
"native wallet OWNER capability opened"
|
||||
);
|
||||
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
|
||||
}
|
||||
|
||||
/// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material.
|
||||
pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verify_result = verify_state_signature(&envelope);
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(crate::LockedWalletInfo::new(envelope.view_descriptor().enabled()));
|
||||
}
|
||||
|
||||
pub(crate) async fn create_wallet_v1_from_keypair(
|
||||
owner_password: crate::OwnerPassword,
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadataV1,
|
||||
solana_keypair: solana_keypair::Keypair,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let owner_root = match crate::crypto::SecretKeyV1::random() {
|
||||
let owner_root = match crate::SecretKeyV1::random() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let metadata_key = match crate::crypto::SecretKeyV1::random() {
|
||||
let metadata_key = match crate::SecretKeyV1::random() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let secret_key = match crate::crypto::SecretKeyV1::random() {
|
||||
let secret_key = match crate::SecretKeyV1::random() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let mut admin_secret = match crate::crypto::random_bytes::<{ crate::crypto::SECRET_KEY_BYTES }>() {
|
||||
let mut admin_secret = match crate::random_bytes::<{ crate::SECRET_KEY_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -478,20 +665,20 @@ pub(crate) async fn create_wallet_v1_from_keypair(
|
||||
let mut owner_control_plaintext = crate::payload::encode_owner_control_payload(&admin_secret, &metadata_key, &secret_key);
|
||||
admin_secret.zeroize();
|
||||
|
||||
let owner_slot_id = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
|
||||
let owner_slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
let owner_salt = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
|
||||
let owner_salt = match crate::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
let owner_kdf = crate::WalletKdfParametersV1::new_creation(owner_salt.to_vec());
|
||||
let owner_wrap_nonce = match crate::crypto::random_nonce() {
|
||||
let owner_wrap_nonce = match crate::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
@@ -506,19 +693,19 @@ pub(crate) async fn create_wallet_v1_from_keypair(
|
||||
},
|
||||
};
|
||||
|
||||
let owner_control_nonce = match crate::crypto::random_nonce() {
|
||||
let owner_control_nonce = match crate::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
let metadata_nonce = match crate::crypto::random_nonce() {
|
||||
let metadata_nonce = match crate::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
let secret_nonce = match crate::crypto::random_nonce() {
|
||||
let secret_nonce = match crate::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
@@ -543,7 +730,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
let owner_wrapped_result = crate::crypto::wrap_key(&owner_derived, &owner_root, &owner_wrap_nonce, provisional.owner_slot_aad().as_slice());
|
||||
let owner_wrapped_result = crate::wrap_key(&owner_derived, &owner_root, &owner_wrap_nonce, provisional.owner_slot_aad().as_slice());
|
||||
let owner_wrapped = match owner_wrapped_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
@@ -559,7 +746,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
|
||||
},
|
||||
};
|
||||
|
||||
let owner_control_ciphertext_result = crate::crypto::encrypt_bytes(
|
||||
let owner_control_ciphertext_result = crate::encrypt_bytes(
|
||||
&owner_root,
|
||||
&owner_control_nonce,
|
||||
provisional.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
|
||||
@@ -573,7 +760,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
|
||||
};
|
||||
owner_control_plaintext.zeroize();
|
||||
|
||||
let metadata_ciphertext_result = crate::crypto::encrypt_bytes(
|
||||
let metadata_ciphertext_result = crate::encrypt_bytes(
|
||||
&metadata_key,
|
||||
&metadata_nonce,
|
||||
provisional.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
|
||||
@@ -587,7 +774,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
|
||||
};
|
||||
metadata_plaintext.zeroize();
|
||||
|
||||
let secret_ciphertext_result = crate::crypto::encrypt_bytes(
|
||||
let secret_ciphertext_result = crate::encrypt_bytes(
|
||||
&secret_key,
|
||||
&secret_nonce,
|
||||
provisional.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(),
|
||||
@@ -648,185 +835,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
|
||||
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
|
||||
}
|
||||
|
||||
/// Opens the VIEW capability from a native `.kspwallet` V1 JSON document.
|
||||
pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result<crate::WalletView> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verify_result = verify_state_signature(&envelope);
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let view_slot = match envelope.view_slot() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
|
||||
};
|
||||
let derived_result = derive_view_password_key_async(password, view_slot.kdf().clone()).await;
|
||||
let derived = match derived_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let aad = envelope.view_slot_aad();
|
||||
let view_aad = match aad {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
|
||||
};
|
||||
let metadata_key_result = crate::crypto::unwrap_key(&derived, view_slot.wrap().nonce(), view_aad.as_slice(), view_slot.wrap().ciphertext());
|
||||
let metadata_key = match metadata_key_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
|
||||
};
|
||||
let metadata_plaintext_result = crate::crypto::decrypt_bytes(
|
||||
&metadata_key,
|
||||
envelope.metadata().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
|
||||
envelope.metadata().ciphertext(),
|
||||
);
|
||||
let mut metadata_plaintext = match metadata_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
|
||||
};
|
||||
let metadata_payload_result = crate::payload::decode_metadata_payload(metadata_plaintext.as_slice());
|
||||
metadata_plaintext.zeroize();
|
||||
let metadata_payload = match metadata_payload_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let info = metadata_payload.into_info(crate::WalletCapability::View);
|
||||
let state = ViewStateV1::new(envelope, metadata_key);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_open_view",
|
||||
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
||||
capability = "view",
|
||||
"native wallet VIEW capability opened"
|
||||
);
|
||||
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, state));
|
||||
}
|
||||
|
||||
/// Opens the OWNER capability from a native `.kspwallet` V1 JSON document.
|
||||
pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verify_result = verify_state_signature(&envelope);
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let owner_slot = envelope.owner_slot();
|
||||
let derived_result = derive_owner_password_key_async(password, owner_slot.kdf().clone()).await;
|
||||
let derived = match derived_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner_root_result =
|
||||
crate::crypto::unwrap_key(&derived, owner_slot.wrap().nonce(), envelope.owner_slot_aad().as_slice(), owner_slot.wrap().ciphertext());
|
||||
let owner_root = match owner_root_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()),
|
||||
};
|
||||
let owner_control_plaintext_result = crate::crypto::decrypt_bytes(
|
||||
&owner_root,
|
||||
envelope.owner_control().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
|
||||
envelope.owner_control().ciphertext(),
|
||||
);
|
||||
let mut owner_control_plaintext = match owner_control_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()),
|
||||
};
|
||||
let control_result = crate::payload::decode_owner_control_payload(owner_control_plaintext.as_slice());
|
||||
owner_control_plaintext.zeroize();
|
||||
let control = match control_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let (mut admin_secret, metadata_key, secret_key) = control.into_parts();
|
||||
let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret);
|
||||
admin_secret.zeroize();
|
||||
if admin_signing_key.verifying_key().to_bytes() != *envelope.owner_auth_public_key() {
|
||||
return std::result::Result::Err(authentication_error());
|
||||
}
|
||||
|
||||
let metadata_plaintext_result = crate::crypto::decrypt_bytes(
|
||||
&metadata_key,
|
||||
envelope.metadata().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
|
||||
envelope.metadata().ciphertext(),
|
||||
);
|
||||
let mut metadata_plaintext = match metadata_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
|
||||
};
|
||||
let metadata_payload_result = crate::payload::decode_metadata_payload(metadata_plaintext.as_slice());
|
||||
metadata_plaintext.zeroize();
|
||||
let metadata_payload = match metadata_payload_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let secret_plaintext_result = crate::crypto::decrypt_bytes(
|
||||
&secret_key,
|
||||
envelope.secret().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(),
|
||||
envelope.secret().ciphertext(),
|
||||
);
|
||||
let mut secret_plaintext = match secret_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
|
||||
};
|
||||
if secret_plaintext.len() != crate::KSPWALLET_V1_SECRET_PLAINTEXT_BYTES {
|
||||
secret_plaintext.zeroize();
|
||||
return std::result::Result::Err(key_material_error());
|
||||
}
|
||||
let keypair_result = solana_keypair::Keypair::try_from(secret_plaintext.as_slice());
|
||||
let solana_keypair = match keypair_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
secret_plaintext.zeroize();
|
||||
return std::result::Result::Err(key_material_error());
|
||||
},
|
||||
};
|
||||
let secret_pubkey_result = pubkey_from_keypair_bytes(secret_plaintext.as_slice());
|
||||
let secret_pubkey = match secret_pubkey_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
secret_plaintext.zeroize();
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
secret_plaintext.zeroize();
|
||||
if &secret_pubkey != metadata_payload.pubkey() {
|
||||
return std::result::Result::Err(key_material_error());
|
||||
}
|
||||
|
||||
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
|
||||
let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_open_owner",
|
||||
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
||||
capability = "owner",
|
||||
"native wallet OWNER capability opened"
|
||||
);
|
||||
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
|
||||
}
|
||||
|
||||
/// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material.
|
||||
pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verify_result = verify_state_signature(&envelope);
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(crate::LockedWalletInfo::new(envelope.view_descriptor().enabled()));
|
||||
}
|
||||
|
||||
pub(crate) fn verify_state_signature(envelope: &crate::KspWalletEnvelopeV1) -> ksp_core_lib::Result<()> {
|
||||
let verifying_key_result = ed25519_dalek::VerifyingKey::from_bytes(envelope.owner_auth_public_key());
|
||||
let verifying_key = match verifying_key_result {
|
||||
@@ -841,24 +849,19 @@ pub(crate) fn verify_state_signature(envelope: &crate::KspWalletEnvelopeV1) -> k
|
||||
};
|
||||
}
|
||||
|
||||
enum PasswordRotationV1 {
|
||||
Owner(crate::OwnerPassword),
|
||||
View(crate::ViewPassword),
|
||||
}
|
||||
|
||||
async fn rewrap_slot(
|
||||
envelope: &crate::KspWalletEnvelopeV1,
|
||||
slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
|
||||
role: crate::WalletKeySlotRoleV1,
|
||||
capability_key: &crate::crypto::SecretKeyV1,
|
||||
capability_key: &crate::SecretKeyV1,
|
||||
password: PasswordRotationV1,
|
||||
) -> ksp_core_lib::Result<crate::WalletKeySlotV1> {
|
||||
let salt = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
|
||||
let salt = match crate::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let kdf = crate::WalletKdfParametersV1::new_creation(salt.to_vec());
|
||||
let nonce = match crate::crypto::random_nonce() {
|
||||
let nonce = match crate::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -874,10 +877,10 @@ async fn rewrap_slot(
|
||||
slot_id,
|
||||
role,
|
||||
kdf.clone(),
|
||||
crate::WalletKeyWrapV1::new(nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
crate::WalletKeyWrapV1::new(nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
);
|
||||
let aad = crate::slot_aad(envelope, &provisional);
|
||||
let wrapped = match crate::crypto::wrap_key(&derived, capability_key, &nonce, aad.as_slice()) {
|
||||
let wrapped = match crate::wrap_key(&derived, capability_key, &nonce, aad.as_slice()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -925,27 +928,20 @@ fn verify_and_return(envelope: crate::KspWalletEnvelopeV1) -> ksp_core_lib::Resu
|
||||
return std::result::Result::Ok(envelope);
|
||||
}
|
||||
|
||||
struct ViewCreationMaterialV1 {
|
||||
slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
|
||||
kdf: crate::WalletKdfParametersV1,
|
||||
wrap_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
password: crate::ViewPassword,
|
||||
}
|
||||
|
||||
fn prepare_view_creation(password: std::option::Option<crate::ViewPassword>) -> ksp_core_lib::Result<std::option::Option<ViewCreationMaterialV1>> {
|
||||
let password = match password {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
let slot_id = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
|
||||
let slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let salt = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
|
||||
let salt = match crate::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wrap_nonce = match crate::crypto::random_nonce() {
|
||||
let wrap_nonce = match crate::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -960,7 +956,7 @@ fn prepare_view_creation(password: std::option::Option<crate::ViewPassword>) ->
|
||||
async fn seal_view_slot(
|
||||
envelope: &crate::KspWalletEnvelopeV1,
|
||||
material: std::option::Option<ViewCreationMaterialV1>,
|
||||
metadata_key: &crate::crypto::SecretKeyV1,
|
||||
metadata_key: &crate::SecretKeyV1,
|
||||
) -> ksp_core_lib::Result<std::option::Option<crate::WalletKeySlotV1>> {
|
||||
let material = match material {
|
||||
std::option::Option::Some(value) => value,
|
||||
@@ -975,10 +971,10 @@ async fn seal_view_slot(
|
||||
material.slot_id,
|
||||
crate::WalletKeySlotRoleV1::View,
|
||||
material.kdf.clone(),
|
||||
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
);
|
||||
let aad = crate::slot_aad(envelope, &provisional_slot);
|
||||
let wrapped_result = crate::crypto::wrap_key(&derived, metadata_key, &material.wrap_nonce, aad.as_slice());
|
||||
let wrapped_result = crate::wrap_key(&derived, metadata_key, &material.wrap_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),
|
||||
@@ -1005,7 +1001,7 @@ fn provisional_envelope(
|
||||
owner_slot_id,
|
||||
crate::WalletKeySlotRoleV1::Owner,
|
||||
owner_kdf,
|
||||
crate::WalletKeyWrapV1::new(owner_wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
crate::WalletKeyWrapV1::new(owner_wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
);
|
||||
let (view_descriptor, view_slot) = match view_material {
|
||||
std::option::Option::Some(material) => (
|
||||
@@ -1014,7 +1010,7 @@ fn provisional_envelope(
|
||||
material.slot_id,
|
||||
crate::WalletKeySlotRoleV1::View,
|
||||
material.kdf.clone(),
|
||||
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
)),
|
||||
),
|
||||
std::option::Option::None => (crate::WalletViewDescriptorV1::disabled(), std::option::Option::None),
|
||||
@@ -1035,19 +1031,16 @@ fn provisional_envelope(
|
||||
);
|
||||
}
|
||||
|
||||
async fn derive_owner_password_key_async(
|
||||
password: crate::OwnerPassword,
|
||||
kdf: crate::WalletKdfParametersV1,
|
||||
) -> ksp_core_lib::Result<crate::crypto::SecretKeyV1> {
|
||||
let task = tokio::task::spawn_blocking(move || return crate::crypto::derive_password_key(password.as_bytes(), &kdf));
|
||||
async fn derive_owner_password_key_async(password: crate::OwnerPassword, kdf: crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::SecretKeyV1> {
|
||||
let task = tokio::task::spawn_blocking(move || return crate::derive_password_key(password.as_bytes(), &kdf));
|
||||
return match task.await {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()),
|
||||
};
|
||||
}
|
||||
|
||||
async fn derive_view_password_key_async(password: crate::ViewPassword, kdf: crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::crypto::SecretKeyV1> {
|
||||
let task = tokio::task::spawn_blocking(move || return crate::crypto::derive_password_key(password.as_bytes(), &kdf));
|
||||
async fn derive_view_password_key_async(password: crate::ViewPassword, kdf: crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::SecretKeyV1> {
|
||||
let task = tokio::task::spawn_blocking(move || return crate::derive_password_key(password.as_bytes(), &kdf));
|
||||
return match task.await {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-wallet-lib/src/wire.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Strict native `.kspwallet` V1 wire envelope.
|
||||
|
||||
use base64::Engine as _;
|
||||
use base64::Engine; // rust-rules: derive-import
|
||||
|
||||
/// Password KDF supported by `.kspwallet` V1 key slots.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/tests/dependency_boundary.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Wallet-specific dependency and ownership canaries.
|
||||
|
||||
@@ -60,6 +60,8 @@ fn wallet_manifest_preserves_dependency_firewall() -> std::io::Result<()> {
|
||||
"tauri",
|
||||
"tracing =",
|
||||
"solana-pubkey",
|
||||
"solana-signer",
|
||||
"solana-signature",
|
||||
"bs58",
|
||||
] {
|
||||
assert!(!manifest.contains(forbidden), "forbidden direct Wallet dependency detected: {forbidden}");
|
||||
@@ -90,3 +92,26 @@ fn wallet_sources_use_core_pubkey_logging_facade_and_no_environment() -> std::io
|
||||
assert!(!all_source.contains(direct_tracing_path.as_str()));
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_public_surface_does_not_reexport_secret_or_signer_types() -> std::io::Result<()> {
|
||||
let lib_source = match std::fs::read_to_string(crate_root().join("src/lib.rs")) {
|
||||
std::result::Result::Ok(source) => source,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
for forbidden in ["pub use solana_keypair", "pub use solana_signer", "pub use solana_signature"] {
|
||||
assert!(!lib_source.contains(forbidden), "Wallet public surface reexports secret/signing implementation type: {forbidden}");
|
||||
}
|
||||
let view_source = match std::fs::read_to_string(crate_root().join("src/view.rs")) {
|
||||
std::result::Result::Ok(source) => source,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert!(!view_source.contains("export_transfer("));
|
||||
assert!(!view_source.contains("sign_message("));
|
||||
let transfer_source = match std::fs::read_to_string(crate_root().join("src/transfer.rs")) {
|
||||
std::result::Result::Ok(source) => source,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert!(transfer_source.contains("#[non_exhaustive]"));
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/capability.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn capability_variants_are_distinct_and_stable() {
|
||||
assert_ne!(super::WalletCapability::View, super::WalletCapability::Owner);
|
||||
assert_eq!(format!("{:?}", super::WalletCapability::View), "View");
|
||||
assert_eq!(format!("{:?}", super::WalletCapability::Owner), "Owner");
|
||||
assert_ne!(crate::WalletCapability::View, crate::WalletCapability::Owner);
|
||||
assert_eq!(format!("{:?}", crate::WalletCapability::View), "View");
|
||||
assert_eq!(format!("{:?}", crate::WalletCapability::Owner), "Owner");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/crypto.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
use base64::Engine as _;
|
||||
use base64::Engine; // rust-rules: derive-import
|
||||
|
||||
const VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_crypto_vectors.json");
|
||||
|
||||
@@ -70,15 +70,15 @@ fn deterministic_argon2id_and_xchacha_wrap_vector_matches_external_canary() -> k
|
||||
};
|
||||
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 content = crate::SecretKeyV1::from_bytes(content_key);
|
||||
let wrapped_result = crate::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_result = crate::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),
|
||||
@@ -89,16 +89,16 @@ fn deterministic_argon2id_and_xchacha_wrap_vector_matches_external_canary() -> k
|
||||
|
||||
#[test]
|
||||
fn xchacha_tampering_is_reported_as_generic_authentication_failure() -> ksp_core_lib::Result<()> {
|
||||
let key = super::SecretKeyV1::from_bytes([0x11_u8; 32]);
|
||||
let key = crate::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 encrypted_result = crate::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());
|
||||
let result = crate::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,
|
||||
@@ -110,12 +110,12 @@ fn xchacha_tampering_is_reported_as_generic_authentication_failure() -> ksp_core
|
||||
|
||||
#[test]
|
||||
fn secret_key_debug_is_redacted_and_random_sources_are_callable() -> ksp_core_lib::Result<()> {
|
||||
let key_result = super::SecretKeyV1::random();
|
||||
let key_result = crate::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_result = crate::random_nonce();
|
||||
let nonce = match nonce_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/metadata.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
fn test_pubkey() -> ksp_core_lib::Pubkey {
|
||||
return ksp_core_lib::PRGIDPK_SOLANA_SYSTEM;
|
||||
@@ -7,8 +7,8 @@ fn test_pubkey() -> ksp_core_lib::Pubkey {
|
||||
|
||||
#[test]
|
||||
fn authorized_info_exposes_metadata_after_authorization() {
|
||||
let note = super::WalletNote { id: std::string::String::from("purpose"), text: std::string::String::from("devnet test wallet") };
|
||||
let info = super::WalletInfo {
|
||||
let note = crate::WalletNote { id: std::string::String::from("purpose"), text: std::string::String::from("devnet test wallet") };
|
||||
let info = crate::WalletInfo {
|
||||
format_version: 1,
|
||||
capability: crate::WalletCapability::View,
|
||||
pubkey: test_pubkey(),
|
||||
@@ -25,11 +25,11 @@ fn authorized_info_exposes_metadata_after_authorization() {
|
||||
|
||||
#[test]
|
||||
fn authorized_info_debug_redacts_alias_and_note_text() {
|
||||
let note = super::WalletNote {
|
||||
let note = crate::WalletNote {
|
||||
id: std::string::String::from("NOTE-ID-CANARY"),
|
||||
text: std::string::String::from("NOTE-SECRET-CANARY"),
|
||||
};
|
||||
let info = super::WalletInfo {
|
||||
let info = crate::WalletInfo {
|
||||
format_version: 1,
|
||||
capability: crate::WalletCapability::Owner,
|
||||
pubkey: test_pubkey(),
|
||||
@@ -46,7 +46,7 @@ fn authorized_info_debug_redacts_alias_and_note_text() {
|
||||
|
||||
#[test]
|
||||
fn locked_info_does_not_carry_authorized_identity_or_metadata() {
|
||||
let info = super::LockedWalletInfo { format_version: 1, view_enabled: true };
|
||||
let info = crate::LockedWalletInfo { format_version: 1, view_enabled: true };
|
||||
assert_eq!(info.format_version(), 1);
|
||||
assert!(info.view_enabled());
|
||||
let rendered = format!("{info:?}");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/password.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn view_password_debug_is_redacted() {
|
||||
let password = super::ViewPassword::new(std::string::String::from("VIEW-SECRET-CANARY"));
|
||||
let password = crate::ViewPassword::new(std::string::String::from("VIEW-SECRET-CANARY"));
|
||||
let rendered = format!("{password:?}");
|
||||
assert_eq!(rendered, "ViewPassword(<redacted>)");
|
||||
assert!(!rendered.contains("VIEW-SECRET-CANARY"));
|
||||
@@ -11,7 +11,7 @@ fn view_password_debug_is_redacted() {
|
||||
|
||||
#[test]
|
||||
fn owner_password_debug_is_redacted() {
|
||||
let password = super::OwnerPassword::new(std::string::String::from("OWNER-SECRET-CANARY"));
|
||||
let password = crate::OwnerPassword::new(std::string::String::from("OWNER-SECRET-CANARY"));
|
||||
let rendered = format!("{password:?}");
|
||||
assert_eq!(rendered, "OwnerPassword(<redacted>)");
|
||||
assert!(!rendered.contains("OWNER-SECRET-CANARY"));
|
||||
@@ -19,6 +19,6 @@ fn owner_password_debug_is_redacted() {
|
||||
|
||||
#[test]
|
||||
fn password_wrappers_require_drop_for_zeroization() {
|
||||
assert!(std::mem::needs_drop::<super::ViewPassword>());
|
||||
assert!(std::mem::needs_drop::<super::OwnerPassword>());
|
||||
assert!(std::mem::needs_drop::<crate::ViewPassword>());
|
||||
assert!(std::mem::needs_drop::<crate::OwnerPassword>());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/payload.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
use base64::Engine as _;
|
||||
use base64::Engine; // rust-rules: derive-import
|
||||
|
||||
#[test]
|
||||
fn metadata_payload_rejects_duplicate_note_identifiers() -> ksp_core_lib::Result<()> {
|
||||
@@ -18,7 +18,7 @@ fn metadata_payload_rejects_duplicate_note_identifiers() -> ksp_core_lib::Result
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(test_error("test metadata payload could not be serialized")),
|
||||
};
|
||||
let result = super::decode_metadata_payload(encoded.as_slice());
|
||||
let result = crate::decode_metadata_payload(encoded.as_slice());
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("duplicate note identifiers unexpectedly parsed")),
|
||||
std::result::Result::Err(error) => error,
|
||||
|
||||
86
crates/ksp-wallet-lib/unit_tests/security.rs
Normal file
86
crates/ksp-wallet-lib/unit_tests/security.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/security.rs
|
||||
// version: 2
|
||||
|
||||
//! Adversarial security canaries for native `.kspwallet` V1.
|
||||
|
||||
use base64::Engine; // rust-rules: derive-import
|
||||
|
||||
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
|
||||
|
||||
#[test]
|
||||
fn owner_signed_regions_reject_canonical_tampering_before_unlock() {
|
||||
for pointer in [
|
||||
"/key_slots/0/kdf/salt",
|
||||
"/key_slots/0/wrap/ciphertext",
|
||||
"/owner_control/ciphertext",
|
||||
"/metadata/ciphertext",
|
||||
"/secret/ciphertext",
|
||||
"/state_signature/signature",
|
||||
] {
|
||||
let tampered = tamper_base64url_field(FULL_VECTOR, pointer);
|
||||
let error = crate::inspect_locked_wallet_v1(tampered.as_slice()).expect_err("OWNER-signed canonical tampering must be rejected");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_AUTHENTICATION_FAILED, "unexpected error code for tampered field {pointer}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owner_signature_verification_precedes_owner_and_view_password_kdf() {
|
||||
let tampered = tamper_base64url_field(FULL_VECTOR, "/metadata/ciphertext");
|
||||
let runtime = runtime();
|
||||
|
||||
let owner = runtime.block_on(crate::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(std::string::String::new())));
|
||||
assert_eq!(owner.expect_err("tampered state must fail before an empty OWNER password reaches Argon2").code(), crate::ERROR_CODE_AUTHENTICATION_FAILED);
|
||||
|
||||
let view = runtime.block_on(crate::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(std::string::String::new())));
|
||||
assert_eq!(view.expect_err("tampered state must fail before an empty VIEW password reaches Argon2").code(), crate::ERROR_CODE_AUTHENTICATION_FAILED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_credential_tampering_does_not_forge_owner_state_and_cannot_unlock_view() {
|
||||
let tampered = tamper_base64url_field(FULL_VECTOR, "/key_slots/1/wrap/ciphertext");
|
||||
let locked = crate::inspect_locked_wallet_v1(tampered.as_slice()).expect("VIEW wrapping credentials are intentionally outside the OWNER state transcript");
|
||||
assert!(locked.view_enabled());
|
||||
|
||||
let runtime = runtime();
|
||||
let owner =
|
||||
runtime.block_on(crate::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))));
|
||||
assert!(owner.is_ok(), "VIEW credential tampering must not invalidate OWNER unlock");
|
||||
|
||||
let view = runtime.block_on(crate::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
|
||||
assert_eq!(view.expect_err("tampered VIEW wrapping must not unlock metadata").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlock_failures_do_not_echo_password_material() {
|
||||
let owner_canary = "OWNER-PASSWORD-LEAK-CANARY";
|
||||
let view_canary = "VIEW-PASSWORD-LEAK-CANARY";
|
||||
let runtime = runtime();
|
||||
|
||||
let owner = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from(owner_canary))));
|
||||
let owner_error = owner.expect_err("wrong OWNER password must fail");
|
||||
assert_eq!(owner_error.code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
|
||||
assert!(!owner_error.to_string().contains(owner_canary));
|
||||
assert!(!format!("{owner_error:?}").contains(owner_canary));
|
||||
|
||||
let view = runtime.block_on(crate::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(std::string::String::from(view_canary))));
|
||||
let view_error = view.expect_err("wrong VIEW password must fail");
|
||||
assert_eq!(view_error.code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
|
||||
assert!(!view_error.to_string().contains(view_canary));
|
||||
assert!(!format!("{view_error:?}").contains(view_canary));
|
||||
}
|
||||
|
||||
fn tamper_base64url_field(source: &[u8], pointer: &str) -> std::vec::Vec<u8> {
|
||||
let mut value: serde_json::Value = serde_json::from_slice(source).expect("security fixture must be valid JSON");
|
||||
let target = value.pointer_mut(pointer).expect("security fixture pointer must exist");
|
||||
let encoded = target.as_str().expect("security fixture target must be a Base64url string");
|
||||
let mut decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded.as_bytes()).expect("security fixture Base64url must decode");
|
||||
let first = decoded.first_mut().expect("security fixture Base64url field must not be empty");
|
||||
*first ^= 0x01;
|
||||
*target = serde_json::Value::String(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(decoded.as_slice()));
|
||||
zeroize::Zeroize::zeroize(decoded.as_mut_slice());
|
||||
return serde_json::to_vec(&value).expect("tampered security fixture must serialize");
|
||||
}
|
||||
|
||||
fn runtime() -> tokio::runtime::Runtime {
|
||||
return tokio::runtime::Builder::new_current_thread().build().expect("security test runtime must be creatable");
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/transcript.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
use base64::Engine as _;
|
||||
use base64::Engine; // rust-rules: derive-import
|
||||
|
||||
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/transfer.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
use zeroize::Zeroize as _;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt; // rust-rules: derive-import
|
||||
use zeroize::Zeroize; // rust-rules: derive-import
|
||||
|
||||
fn runtime() -> tokio::runtime::Runtime {
|
||||
return tokio::runtime::Builder::new_current_thread().build().expect("Wallet transfer test runtime must build");
|
||||
@@ -234,7 +236,6 @@ fn owner_transfer_file_export_is_no_clobber() {
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
let mode = std::fs::metadata(export_path.as_path()).expect("export metadata must be readable").permissions().mode() & 0o777;
|
||||
assert_eq!(mode, 0o600);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/wallet.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
|
||||
const FULL_VECTOR_META: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector_meta.json");
|
||||
@@ -26,7 +26,7 @@ fn externally_generated_full_vector_opens_view_and_owner_independently() -> ksp_
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let view_result = runtime.block_on(super::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(vector.view_password_utf8)));
|
||||
let view_result = runtime.block_on(crate::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(vector.view_password_utf8)));
|
||||
let view = match view_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -36,7 +36,7 @@ fn externally_generated_full_vector_opens_view_and_owner_independently() -> ksp_
|
||||
assert_eq!(view.alias(), std::option::Option::Some(vector.expected_alias.as_str()));
|
||||
assert_note_texts(view.notes(), vector.expected_notes.as_slice());
|
||||
|
||||
let owner_result = runtime.block_on(super::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(vector.owner_password_utf8)));
|
||||
let owner_result = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(vector.owner_password_utf8)));
|
||||
let owner = match owner_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -59,21 +59,21 @@ fn wrong_passwords_do_not_cross_unlock_capabilities() -> ksp_core_lib::Result<()
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let wrong_view = runtime.block_on(super::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(std::string::String::from("wrong-view-password"))));
|
||||
let wrong_view = runtime.block_on(crate::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(std::string::String::from("wrong-view-password"))));
|
||||
let wrong_view_error = match wrong_view {
|
||||
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("wrong VIEW password unexpectedly unlocked Wallet")),
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(wrong_view_error.code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
|
||||
|
||||
let wrong_owner = runtime.block_on(super::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from("wrong-owner-password"))));
|
||||
let wrong_owner = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from("wrong-owner-password"))));
|
||||
let wrong_owner_error = match wrong_owner {
|
||||
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("wrong OWNER password unexpectedly unlocked Wallet")),
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(wrong_owner_error.code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
|
||||
|
||||
let view_as_owner = runtime.block_on(super::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(vector.view_password_utf8)));
|
||||
let view_as_owner = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(vector.view_password_utf8)));
|
||||
let view_as_owner_error = match view_as_owner {
|
||||
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("VIEW password unexpectedly unlocked OWNER")),
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -88,7 +88,7 @@ fn owner_signed_metadata_tampering_is_rejected_before_unlock() -> ksp_core_lib::
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let inspect_result = super::inspect_locked_wallet_v1(tampered.as_slice());
|
||||
let inspect_result = crate::inspect_locked_wallet_v1(tampered.as_slice());
|
||||
let error = match inspect_result {
|
||||
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("tampered OWNER-signed metadata unexpectedly verified")),
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -107,7 +107,7 @@ fn view_wrap_can_change_without_breaking_owner_authenticated_state() -> ksp_core
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let inspected = match super::inspect_locked_wallet_v1(tampered.as_slice()) {
|
||||
let inspected = match crate::inspect_locked_wallet_v1(tampered.as_slice()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -117,12 +117,12 @@ fn view_wrap_can_change_without_breaking_owner_authenticated_state() -> ksp_core
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner_result = runtime.block_on(super::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(vector.owner_password_utf8)));
|
||||
let owner_result = runtime.block_on(crate::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(vector.owner_password_utf8)));
|
||||
if let std::result::Result::Err(error) = owner_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
|
||||
let view_result = runtime.block_on(super::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(vector.view_password_utf8)));
|
||||
let view_result = runtime.block_on(crate::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(vector.view_password_utf8)));
|
||||
let view_error = match view_result {
|
||||
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("modified VIEW wrapping unexpectedly unlocked metadata")),
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -147,7 +147,7 @@ fn locked_full_vector_contains_no_authorized_identity_or_metadata_plaintext() ->
|
||||
for note in vector.expected_notes {
|
||||
assert!(!document.contains(note.as_str()));
|
||||
}
|
||||
let locked = match super::inspect_locked_wallet_v1(FULL_VECTOR) {
|
||||
let locked = match crate::inspect_locked_wallet_v1(FULL_VECTOR) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -181,7 +181,7 @@ fn create_uses_calibrated_defaults_and_keeps_locked_projection_private() -> ksp_
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner_result = runtime.block_on(super::create_wallet_v1(
|
||||
let owner_result = runtime.block_on(crate::create_wallet_v1(
|
||||
crate::OwnerPassword::new(std::string::String::from("pre005-create-owner-password")),
|
||||
std::option::Option::None,
|
||||
metadata,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/wire.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
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) {
|
||||
let parsed = match crate::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_slot().role(), crate::WalletKeySlotRoleV1::Owner);
|
||||
assert_eq!(parsed.view_slot().map(crate::WalletKeySlotV1::role), std::option::Option::Some(crate::WalletKeySlotRoleV1::View));
|
||||
assert_eq!(parsed.owner_control().payload_version(), 1);
|
||||
assert_eq!(parsed.metadata().payload_version(), 1);
|
||||
assert_eq!(parsed.secret().payload_version(), 1);
|
||||
@@ -21,7 +21,7 @@ fn strict_v1_fixture_parses_and_round_trips_semantically() -> ksp_core_lib::Resu
|
||||
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()) {
|
||||
let reparsed = match crate::KspWalletEnvelopeV1::parse_json(serialized.as_slice()) {
|
||||
std::result::Result::Ok(reparsed) => reparsed,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -32,7 +32,7 @@ fn strict_v1_fixture_parses_and_round_trips_semantically() -> ksp_core_lib::Resu
|
||||
#[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());
|
||||
let result = crate::KspWalletEnvelopeV1::parse_json(source.as_bytes());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -44,7 +44,7 @@ fn unknown_format_version_is_rejected_before_v1_shape_validation() {
|
||||
#[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());
|
||||
let result = crate::KspWalletEnvelopeV1::parse_json(source.as_bytes());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -57,7 +57,7 @@ fn unknown_top_level_field_is_rejected() {
|
||||
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());
|
||||
let result = crate::KspWalletEnvelopeV1::parse_json(source.as_bytes());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -70,7 +70,7 @@ fn padded_or_noncanonical_base64url_is_rejected() {
|
||||
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());
|
||||
let result = crate::KspWalletEnvelopeV1::parse_json(source.as_bytes());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -82,7 +82,7 @@ fn enabled_view_descriptor_must_match_the_view_slot() {
|
||||
#[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());
|
||||
let zero_result = crate::KspWalletEnvelopeV1::parse_json(zero_source.as_bytes());
|
||||
assert!(zero_result.is_err());
|
||||
let zero_error = match zero_result {
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -91,7 +91,7 @@ fn zero_or_pathological_kdf_parameters_are_rejected_before_crypto() {
|
||||
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());
|
||||
let high_result = crate::KspWalletEnvelopeV1::parse_json(high_source.as_bytes());
|
||||
assert!(high_result.is_err());
|
||||
let high_error = match high_result {
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -107,7 +107,7 @@ fn argon2_memory_must_cover_all_lanes_before_crypto() {
|
||||
"\"parallelism\": 2",
|
||||
1,
|
||||
);
|
||||
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
|
||||
let result = crate::KspWalletEnvelopeV1::parse_json(source.as_bytes());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -119,7 +119,7 @@ fn argon2_memory_must_cover_all_lanes_before_crypto() {
|
||||
#[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());
|
||||
let result = crate::KspWalletEnvelopeV1::parse_json(oversized.as_slice());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -130,7 +130,7 @@ fn oversized_document_is_rejected_before_json_or_crypto() {
|
||||
|
||||
#[test]
|
||||
fn envelope_debug_does_not_render_ciphertext_contents() -> ksp_core_lib::Result<()> {
|
||||
let parsed = match super::KspWalletEnvelopeV1::parse_json(FIXTURE) {
|
||||
let parsed = match crate::KspWalletEnvelopeV1::parse_json(FIXTURE) {
|
||||
std::result::Result::Ok(parsed) => parsed,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user