v0.2.5-pre.005

This commit is contained in:
2026-08-19 13:09:16 +02:00
parent 3c069347b7
commit 36b98e0abc
27 changed files with 2305 additions and 113 deletions

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-wallet-lib/Cargo.toml
# version: 4
# version: 5
[package]
name = "ksp-wallet-lib"
@@ -10,12 +10,15 @@ repository.workspace = true
[dependencies]
argon2 = { workspace = true, features = ["alloc", "zeroize"] }
chacha20poly1305 = { workspace = true, features = ["alloc", "zeroize"] }
ed25519-dalek = { workspace = true, features = ["signature", "zeroize"] }
getrandom.workspace = true
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
base64.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
solana-keypair.workspace = true
tokio = { workspace = true, features = ["rt"] }
zeroize.workspace = true
[lints]

View File

@@ -21,6 +21,8 @@ pub const KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES: usize = 64 * 1024;
pub const KSPWALLET_V1_MAX_PASSWORD_BYTES: usize = 1024;
/// Byte length of every V1 key-slot identifier.
pub const KSPWALLET_V1_SLOT_ID_BYTES: usize = 16;
/// Byte length of every protected metadata note identifier.
pub const KSPWALLET_V1_NOTE_ID_BYTES: usize = 16;
/// Byte length of an Ed25519 public key used as Wallet format authority.
pub const KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES: usize = 32;
/// Byte length of an Ed25519 detached state signature.
@@ -31,6 +33,18 @@ pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
pub const KSPWALLET_V1_AEAD_TAG_BYTES: usize = 16;
/// Argon2 version serialized by V1 key slots.
pub const KSPWALLET_V1_ARGON2_VERSION: u32 = 19;
/// Default Argon2id memory cost for newly created V1 key slots, calibrated on the 2026-08-19 operator benchmark.
pub const KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB: u32 = 65_536;
/// Default Argon2id iteration count for newly created V1 key slots.
pub const KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS: u32 = 3;
/// Default Argon2id parallelism for newly created V1 key slots.
pub const KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM: u32 = 1;
/// Salt size generated independently for every newly created V1 key slot.
pub const KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES: usize = 32;
/// Exact plaintext size of the V1 OWNER-control compartment.
pub const KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES: usize = 96;
/// Exact plaintext size of the V1 Solana secret compartment.
pub const KSPWALLET_V1_SECRET_PLAINTEXT_BYTES: usize = 64;
/// Minimum accepted Argon2 salt size in bytes.
pub const KSPWALLET_V1_MIN_KDF_SALT_BYTES: usize = 16;
/// Maximum accepted Argon2 salt size in bytes.
@@ -53,6 +67,7 @@ pub const KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_M
pub const KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES: usize = 4096;
/// Initial protected payload version used independently by control, metadata and secret compartments.
pub const KSPWALLET_V1_INITIAL_PAYLOAD_VERSION: u32 = 1;
/// Domain separator for the OWNER state-signature transcript.
pub const KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V1-STATE";
/// Domain separator for OWNER key-slot wrapping AAD.

View File

@@ -1,42 +1,36 @@
// file: crates/ksp-wallet-lib/src/crypto.rs
// version: 1
// version: 2
//! In-memory cryptographic primitives for native `.kspwallet` V1.
//!
//! `pre.004` intentionally lands these crate-private primitives one tranche before `pre.005` wires them into create/open capability flows.
#![allow(dead_code, reason = "pre.004 stages private Wallet crypto primitives before their pre.005 production callers")]
use chacha20poly1305::KeyInit as _;
use chacha20poly1305::aead::Aead as _;
/// Exact V1 content-key and password-derived-key size in bytes.
const SECRET_KEY_BYTES: usize = 32;
pub(crate) const SECRET_KEY_BYTES: usize = 32;
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
struct SecretKeyV1 {
pub(crate) struct SecretKeyV1 {
bytes: [u8; SECRET_KEY_BYTES],
}
impl SecretKeyV1 {
/// Takes ownership of exact 32-byte secret material.
const fn from_bytes(bytes: [u8; SECRET_KEY_BYTES]) -> Self {
pub(crate) const fn from_bytes(bytes: [u8; SECRET_KEY_BYTES]) -> Self {
return Self { bytes };
}
/// Generates a fresh secret key from the operating-system CSPRNG.
fn random() -> ksp_core_lib::Result<Self> {
let mut bytes = [0_u8; SECRET_KEY_BYTES];
let fill_result = getrandom::fill(bytes.as_mut_slice());
if fill_result.is_err() {
zeroize::Zeroize::zeroize(&mut bytes);
return std::result::Result::Err(randomness_error());
}
pub(crate) fn random() -> ksp_core_lib::Result<Self> {
let bytes = match random_bytes::<SECRET_KEY_BYTES>() {
std::result::Result::Ok(bytes) => bytes,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(Self { bytes });
}
/// Borrows secret bytes internally without allocating or cloning.
const fn as_bytes(&self) -> &[u8; SECRET_KEY_BYTES] {
pub(crate) const fn as_bytes(&self) -> &[u8; SECRET_KEY_BYTES] {
return &self.bytes;
}
}
@@ -53,24 +47,29 @@ impl std::ops::Drop for SecretKeyV1 {
}
}
/// Generates a fresh XChaCha20-Poly1305 nonce from the operating-system CSPRNG.
fn random_nonce() -> ksp_core_lib::Result<[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES]> {
let mut nonce = [0_u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES];
let fill_result = getrandom::fill(nonce.as_mut_slice());
/// Generates a fresh fixed-size byte array from the operating-system CSPRNG.
pub(crate) fn random_bytes<const LENGTH: usize>() -> ksp_core_lib::Result<[u8; LENGTH]> {
let mut bytes = [0_u8; LENGTH];
let fill_result = getrandom::fill(bytes.as_mut_slice());
if fill_result.is_err() {
zeroize::Zeroize::zeroize(&mut nonce);
zeroize::Zeroize::zeroize(&mut bytes);
return std::result::Result::Err(randomness_error());
}
return std::result::Result::Ok(nonce);
return std::result::Result::Ok(bytes);
}
/// Generates a fresh XChaCha20-Poly1305 nonce from the operating-system CSPRNG.
pub(crate) fn random_nonce() -> ksp_core_lib::Result<[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES]> {
return random_bytes::<{ crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES }>();
}
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
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<SecretKeyV1> {
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
}
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
fn wrap_key(
pub(crate) fn wrap_key(
wrapping_key: &SecretKeyV1,
key_to_wrap: &SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
@@ -80,7 +79,7 @@ fn wrap_key(
}
/// Unwraps one 32-byte content key and maps every AEAD authentication failure to the generic Wallet authentication error.
fn unwrap_key(
pub(crate) fn unwrap_key(
wrapping_key: &SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
@@ -108,7 +107,7 @@ fn unwrap_key(
}
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
fn encrypt_bytes(
pub(crate) fn encrypt_bytes(
key: &SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
@@ -129,7 +128,7 @@ fn encrypt_bytes(
}
/// Decrypts authenticated ciphertext bytes and returns a generic authentication error on tag failure.
fn decrypt_bytes(
pub(crate) fn decrypt_bytes(
key: &SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
@@ -149,7 +148,7 @@ fn decrypt_bytes(
};
}
fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u32, parallelism: u32) -> ksp_core_lib::Result<SecretKeyV1> {
pub(crate) fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u32, parallelism: u32) -> ksp_core_lib::Result<SecretKeyV1> {
if password.is_empty() || password.len() > crate::KSPWALLET_V1_MAX_PASSWORD_BYTES {
return std::result::Result::Err(crypto_parameter_error());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/error.rs
// version: 2
// version: 3
/// Error code used when a native Wallet structure is invalid.
pub const ERROR_CODE_FORMAT_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "format_invalid");
@@ -11,6 +11,8 @@ pub const ERROR_CODE_CRYPTO_PARAMETERS_INVALID: ksp_core_lib::ErrorCode = ksp_co
pub const ERROR_CODE_RANDOMNESS_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "randomness_failed");
/// Error code used when an authenticated Wallet structure cannot be verified.
pub const ERROR_CODE_AUTHENTICATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "authentication_failed");
/// Error code used when an internal blocking cryptographic operation cannot complete.
pub const ERROR_CODE_CRYPTO_OPERATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "crypto_operation_failed");
/// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle.
pub const ERROR_CODE_VIEW_UNLOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "view_unlock_failed");
/// Error code used when an OWNER unlock attempt fails without exposing a finer cryptographic oracle.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/lib.rs
// version: 4
// version: 5
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -9,7 +9,8 @@
//! `ksp-wallet-lib` owns the native `.kspwallet` domain, VIEW/OWNER capability model, protected metadata projection, password-secret wrappers and Wallet
//! error contract. `0.2.5-pre.003` freezes the strict V1 JSON envelope, canonical Base64url decoding, structural limits and deterministic
//! state-transcript/AEAD-AAD byte codecs. `0.2.5-pre.004` adds the in-memory Argon2id/XChaCha20-Poly1305/CSPRNG primitives and deterministic crypto
//! vectors used by later create/open operations. It still performs no state-signature verification, Solana signing or filesystem persistence. Public
//! vectors. `0.2.5-pre.005` adds exact protected payloads, OWNER Ed25519 state authentication and async in-memory create/open flows for VIEW and OWNER.
//! Solana transaction signing and filesystem persistence remain outside this tranche. 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`.
@@ -20,8 +21,10 @@ mod error;
mod metadata;
mod owner;
mod password;
mod payload;
mod transcript;
mod view;
mod wallet;
mod wire;
/// Authorized capability represented by an unlocked Wallet handle.
@@ -36,6 +39,14 @@ pub use self::constants::KSPWALLET_MAX_FILE_BYTES;
pub use self::constants::KSPWALLET_V1_AEAD_TAG_BYTES;
/// Argon2 version serialized by `.kspwallet` V1 key slots.
pub use self::constants::KSPWALLET_V1_ARGON2_VERSION;
/// Default Argon2id iteration count for newly created V1 slots.
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS;
/// Default Argon2id memory cost for newly created V1 slots.
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB;
/// Default Argon2id parallelism for newly created V1 slots.
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM;
/// Default KDF salt size generated for newly created V1 slots.
pub use self::constants::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES;
/// Byte length of the Ed25519 format-authority public key.
pub use self::constants::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES;
/// Byte length of the Ed25519 detached state signature.
@@ -74,12 +85,18 @@ pub use self::constants::KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES;
pub use self::constants::KSPWALLET_V1_METADATA_AAD_DOMAIN;
/// Minimum Argon2 salt size accepted by V1.
pub use self::constants::KSPWALLET_V1_MIN_KDF_SALT_BYTES;
/// Byte length of every protected metadata note identifier.
pub use self::constants::KSPWALLET_V1_NOTE_ID_BYTES;
/// Domain separator for OWNER-control compartment AEAD AAD.
pub use self::constants::KSPWALLET_V1_OWNER_CONTROL_AAD_DOMAIN;
/// Exact OWNER-control plaintext size fixed by V1.
pub use self::constants::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES;
/// Domain separator for OWNER key-slot wrapping AAD.
pub use self::constants::KSPWALLET_V1_OWNER_SLOT_AAD_DOMAIN;
/// Domain separator for OWNER-only secret compartment AEAD AAD.
pub use self::constants::KSPWALLET_V1_SECRET_AAD_DOMAIN;
/// Exact Solana keypair plaintext size fixed by V1.
pub use self::constants::KSPWALLET_V1_SECRET_PLAINTEXT_BYTES;
/// Byte length of every V1 key-slot identifier.
pub use self::constants::KSPWALLET_V1_SLOT_ID_BYTES;
/// Domain separator for the OWNER state-signature transcript.
@@ -94,6 +111,8 @@ pub use self::error::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED;
pub use self::error::ERROR_CODE_AUTHENTICATION_FAILED;
/// Error code used when an operation requires a capability that the caller does not own.
pub use self::error::ERROR_CODE_CAPABILITY_INSUFFICIENT;
/// Error code used when a blocking cryptographic task cannot complete.
pub use self::error::ERROR_CODE_CRYPTO_OPERATION_FAILED;
/// Error code used when serialized cryptographic parameters are invalid or unsupported.
pub use self::error::ERROR_CODE_CRYPTO_PARAMETERS_INVALID;
/// Error code used when a no-clobber create or import destination already exists.
@@ -118,6 +137,8 @@ pub use self::error::ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED;
pub use self::error::ERROR_CODE_VIEW_UNLOCK_FAILED;
/// Minimal non-secret information available while a native Wallet remains locked.
pub use self::metadata::LockedWalletInfo;
/// Protected initial metadata supplied to native Wallet creation.
pub use self::metadata::WalletCreateMetadataV1;
/// Safe metadata projection produced after VIEW or OWNER authorization.
pub use self::metadata::WalletInfo;
/// One protected Wallet note exposed only after authorization.
@@ -130,6 +151,14 @@ pub use self::password::OwnerPassword;
pub use self::password::ViewPassword;
/// Authorized VIEW capability handle.
pub use self::view::WalletView;
/// Creates a new in-memory native Wallet V1.
pub use self::wallet::create_wallet_v1;
/// Parses and verifies locked native Wallet state without unlocking protected metadata.
pub use self::wallet::inspect_locked_wallet_v1;
/// Opens the OWNER capability from a native Wallet V1 document.
pub use self::wallet::open_wallet_owner_v1;
/// Opens the VIEW capability from a native Wallet V1 document.
pub use self::wallet::open_wallet_view_v1;
/// Strict semantic representation of one parsed native `.kspwallet` V1 envelope.
pub use self::wire::KspWalletEnvelopeV1;
/// Authenticated-encryption algorithm fixed by native Wallet V1.

View File

@@ -1,5 +1,36 @@
// file: crates/ksp-wallet-lib/src/metadata.rs
// version: 2
// version: 3
/// Protected metadata requested when creating a new native Wallet.
///
/// Alias and note texts are never rendered by `Debug`. Wallet generates stable random note identifiers when the metadata is sealed into the V1 payload.
#[derive(Default, Eq, PartialEq)]
pub struct WalletCreateMetadataV1 {
alias: std::option::Option<std::string::String>,
note_texts: std::vec::Vec<std::string::String>,
}
impl WalletCreateMetadataV1 {
/// Creates protected initial Wallet metadata.
#[must_use]
pub fn new(alias: std::option::Option<std::string::String>, note_texts: std::vec::Vec<std::string::String>) -> Self {
return Self { alias, note_texts };
}
pub(crate) fn into_parts(self) -> (std::option::Option<std::string::String>, std::vec::Vec<std::string::String>) {
return (self.alias, self.note_texts);
}
}
impl std::fmt::Debug for WalletCreateMetadataV1 {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("WalletCreateMetadataV1")
.field("alias", &self.alias.as_ref().map(|_| return "<redacted>"))
.field("note_count", &self.note_texts.len())
.finish();
}
}
/// One protected Wallet note exposed only after VIEW or OWNER authorization.
#[derive(Clone, Eq, PartialEq)]
@@ -9,6 +40,10 @@ pub struct WalletNote {
}
impl WalletNote {
pub(crate) fn new(id: std::string::String, text: std::string::String) -> Self {
return Self { id, text };
}
/// Returns the stable note identifier used by Wallet administration operations.
#[must_use]
pub fn id(&self) -> &str {
@@ -39,6 +74,15 @@ pub struct WalletInfo {
}
impl WalletInfo {
pub(crate) fn new(
capability: crate::WalletCapability,
pubkey: ksp_core_lib::Pubkey,
alias: std::option::Option<std::string::String>,
notes: std::vec::Vec<crate::WalletNote>,
) -> Self {
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, capability, pubkey, alias, notes };
}
/// Returns the native Wallet format version parsed for this projection.
#[must_use]
pub const fn format_version(&self) -> u32 {
@@ -91,6 +135,10 @@ pub struct LockedWalletInfo {
}
impl LockedWalletInfo {
pub(crate) const fn new(view_enabled: bool) -> Self {
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, view_enabled };
}
/// Returns the native Wallet format version.
#[must_use]
pub const fn format_version(&self) -> u32 {

View File

@@ -1,15 +1,20 @@
// file: crates/ksp-wallet-lib/src/owner.rs
// version: 1
// version: 2
/// Authorized OWNER capability handle.
///
/// OWNER exposes all authorized metadata and will receive signing plus Wallet administration operations in later `0.2.5` tranches without exposing a
/// general-purpose secret-key getter.
/// OWNER exposes protected metadata and retains the authenticated in-memory state required by later signing and administration tranches without
/// exposing a general-purpose secret-key getter.
pub struct WalletOwner {
info: crate::WalletInfo,
state: crate::wallet::OwnerStateV1,
}
impl WalletOwner {
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::wallet::OwnerStateV1) -> Self {
return Self { info, state };
}
/// Returns the authorization capability represented by this handle.
#[must_use]
pub const fn capability(&self) -> crate::WalletCapability {
@@ -45,10 +50,15 @@ impl WalletOwner {
);
return &self.info;
}
/// Serializes the complete locked `.kspwallet` V1 document without exposing any unlocked secret material.
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.envelope().to_json_bytes();
}
}
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).finish();
return formatter.debug_struct("WalletOwner").field("info", &self.info).field("unlocked_state", &"<redacted>").finish();
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/password.rs
// version: 1
// version: 2
/// Owned VIEW password material.
///
@@ -21,6 +21,11 @@ impl ViewPassword {
pub fn new(value: std::string::String) -> Self {
return Self { value };
}
/// Borrows exact UTF-8 password bytes inside Wallet cryptographic operations.
pub(crate) fn as_bytes(&self) -> &[u8] {
return self.value.as_bytes();
}
}
impl std::fmt::Debug for ViewPassword {
@@ -55,6 +60,11 @@ impl OwnerPassword {
pub fn new(value: std::string::String) -> Self {
return Self { value };
}
/// Borrows exact UTF-8 password bytes inside Wallet cryptographic operations.
pub(crate) fn as_bytes(&self) -> &[u8] {
return self.value.as_bytes();
}
}
impl std::fmt::Debug for OwnerPassword {

View File

@@ -0,0 +1,207 @@
// file: crates/ksp-wallet-lib/src/payload.rs
// version: 2
//! Plaintext payload codecs protected inside native `.kspwallet` V1 compartments.
use base64::Engine as _;
use std::str::FromStr as _;
pub(crate) struct MetadataPayloadV1 {
pubkey: ksp_core_lib::Pubkey,
alias: std::option::Option<std::string::String>,
notes: std::vec::Vec<crate::WalletNote>,
}
impl MetadataPayloadV1 {
pub(crate) const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
return &self.pubkey;
}
pub(crate) fn into_info(self, capability: crate::WalletCapability) -> crate::WalletInfo {
return crate::WalletInfo::new(capability, self.pubkey, self.alias, self.notes);
}
}
pub(crate) struct OwnerControlMaterialV1 {
admin_signing_secret: [u8; crate::crypto::SECRET_KEY_BYTES],
metadata_key: crate::crypto::SecretKeyV1,
secret_key: crate::crypto::SecretKeyV1,
}
impl OwnerControlMaterialV1 {
pub(crate) fn into_parts(mut self) -> ([u8; crate::crypto::SECRET_KEY_BYTES], crate::crypto::SecretKeyV1, crate::crypto::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]));
return (admin_signing_secret, metadata_key, secret_key);
}
}
impl std::ops::Drop for OwnerControlMaterialV1 {
fn drop(&mut self) {
zeroize::Zeroize::zeroize(&mut self.admin_signing_secret);
}
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
struct RawMetadataPayloadV1 {
pubkey: std::string::String,
alias: std::option::Option<std::string::String>,
notes: std::vec::Vec<RawMetadataNoteV1>,
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
struct RawMetadataNoteV1 {
id: std::string::String,
text: std::string::String,
}
pub(crate) fn encode_initial_metadata_payload(
pubkey: ksp_core_lib::Pubkey,
metadata: crate::WalletCreateMetadataV1,
) -> ksp_core_lib::Result<(std::vec::Vec<u8>, MetadataPayloadV1)> {
let (alias, note_texts) = metadata.into_parts();
if let std::option::Option::Some(alias_value) = alias.as_ref()
&& alias_value.len() > crate::KSPWALLET_V1_MAX_ALIAS_BYTES
{
return std::result::Result::Err(metadata_error("Wallet alias exceeds the V1 UTF-8 byte limit", "metadata.alias"));
}
if note_texts.len() > crate::KSPWALLET_V1_MAX_NOTES {
return std::result::Result::Err(metadata_error("Wallet note count exceeds the V1 limit", "metadata.notes"));
}
let mut raw_notes = std::vec::Vec::with_capacity(note_texts.len());
let mut notes = std::vec::Vec::with_capacity(note_texts.len());
let mut note_ids = std::collections::BTreeSet::new();
for text in note_texts {
if text.len() > crate::KSPWALLET_V1_MAX_NOTE_TEXT_BYTES {
return std::result::Result::Err(metadata_error("Wallet note text exceeds the V1 UTF-8 byte limit", "metadata.notes.text"));
}
let note_id_bytes = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_NOTE_ID_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let note_id = encode_base64url(note_id_bytes.as_slice());
if !note_ids.insert(note_id.clone()) {
return std::result::Result::Err(random_note_identifier_error());
}
raw_notes.push(RawMetadataNoteV1 { id: note_id.clone(), text: text.clone() });
notes.push(crate::WalletNote::new(note_id, text));
}
let raw = RawMetadataPayloadV1 { pubkey: pubkey.to_string(), alias: alias.clone(), notes: raw_notes };
let serialized_result = serde_json::to_vec(&raw);
let serialized = match serialized_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata payload cannot be serialized", "metadata")),
};
if serialized.len() > crate::KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES {
return std::result::Result::Err(metadata_error("Wallet metadata payload exceeds the V1 plaintext limit", "metadata"));
}
return std::result::Result::Ok((serialized, MetadataPayloadV1 { pubkey, alias, notes }));
}
pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<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"));
}
let raw_result = serde_json::from_slice::<RawMetadataPayloadV1>(source);
let raw = match raw_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata payload is invalid", "metadata")),
};
if let std::option::Option::Some(alias) = raw.alias.as_ref()
&& alias.len() > crate::KSPWALLET_V1_MAX_ALIAS_BYTES
{
return std::result::Result::Err(metadata_error("Wallet alias exceeds the V1 UTF-8 byte limit", "metadata.alias"));
}
if raw.notes.len() > crate::KSPWALLET_V1_MAX_NOTES {
return std::result::Result::Err(metadata_error("Wallet note count exceeds the V1 limit", "metadata.notes"));
}
let pubkey_result = ksp_core_lib::Pubkey::from_str(raw.pubkey.as_str());
let pubkey = match pubkey_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata Pubkey is invalid", "metadata.pubkey")),
};
if pubkey.to_string() != raw.pubkey {
return std::result::Result::Err(metadata_error("Wallet metadata Pubkey is not canonical Base58", "metadata.pubkey"));
}
let mut notes = std::vec::Vec::with_capacity(raw.notes.len());
let mut note_ids = std::collections::BTreeSet::new();
for raw_note in raw.notes {
if raw_note.text.len() > crate::KSPWALLET_V1_MAX_NOTE_TEXT_BYTES {
return std::result::Result::Err(metadata_error("Wallet note text exceeds the V1 UTF-8 byte limit", "metadata.notes.text"));
}
let id_bytes = match decode_base64url(raw_note.id.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if id_bytes.len() != crate::KSPWALLET_V1_NOTE_ID_BYTES || encode_base64url(id_bytes.as_slice()) != raw_note.id {
return std::result::Result::Err(metadata_error("Wallet note identifier is not canonical V1 Base64url", "metadata.notes.id"));
}
if !note_ids.insert(raw_note.id.clone()) {
return std::result::Result::Err(metadata_error("Wallet note identifiers must be unique", "metadata.notes.id"));
}
notes.push(crate::WalletNote::new(raw_note.id, raw_note.text));
}
return std::result::Result::Ok(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,
) -> [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);
output[32..64].copy_from_slice(metadata_key.as_bytes());
output[64..96].copy_from_slice(secret_key.as_bytes());
return output;
}
pub(crate) fn decode_owner_control_payload(source: &[u8]) -> ksp_core_lib::Result<OwnerControlMaterialV1> {
if source.len() != crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES {
return std::result::Result::Err(metadata_error("Wallet OWNER-control payload length is invalid", "owner_control"));
}
let mut admin_signing_secret = [0_u8; crate::crypto::SECRET_KEY_BYTES];
admin_signing_secret.copy_from_slice(&source[0..32]);
let mut metadata_key = [0_u8; crate::crypto::SECRET_KEY_BYTES];
metadata_key.copy_from_slice(&source[32..64]);
let mut secret_key = [0_u8; crate::crypto::SECRET_KEY_BYTES];
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),
});
}
fn encode_base64url(bytes: &[u8]) -> std::string::String {
return base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
}
fn decode_base64url(encoded: &str) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded.as_bytes());
let value = match decoded {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata Base64url field is invalid", "metadata.notes.id")),
};
return std::result::Result::Ok(value);
}
fn random_note_identifier_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_RANDOMNESS_FAILED, "Wallet generated a duplicate protected note identifier");
}
fn metadata_error(message: &'static str, field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message).with_context("field", field);
}
#[cfg(test)]
#[path = "../unit_tests/payload.rs"]
mod tests;

View File

@@ -1,15 +1,20 @@
// file: crates/ksp-wallet-lib/src/view.rs
// version: 1
// version: 2
/// Authorized VIEW capability handle.
///
/// VIEW exposes protected metadata and, in later `0.2.5` tranches, will expose only self-rotation of its VIEW password. It never owns the Solana secret or
/// OWNER administration material.
/// VIEW exposes protected metadata and retains only the authenticated metadata capability needed for its future self-service password rotation. It never
/// owns the Solana secret or OWNER administration material.
pub struct WalletView {
info: crate::WalletInfo,
state: crate::wallet::ViewStateV1,
}
impl WalletView {
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::wallet::ViewStateV1) -> Self {
return Self { info, state };
}
/// Returns the authorization capability represented by this handle.
#[must_use]
pub const fn capability(&self) -> crate::WalletCapability {
@@ -45,10 +50,15 @@ impl WalletView {
);
return &self.info;
}
/// Serializes the unchanged locked `.kspwallet` V1 document without exposing the metadata content key.
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.envelope().to_json_bytes();
}
}
impl std::fmt::Debug for WalletView {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("WalletView").field("info", &self.info).finish();
return formatter.debug_struct("WalletView").field("info", &self.info).field("unlocked_state", &"<redacted>").finish();
}
}

View File

@@ -0,0 +1,682 @@
// file: crates/ksp-wallet-lib/src/wallet.rs
// version: 2
//! In-memory native Wallet V1 create/open orchestration.
use ed25519_dalek::Signer as _;
use zeroize::Zeroize as _;
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>,
admin_signing_key: std::option::Option<ed25519_dalek::SigningKey>,
solana_keypair: std::option::Option<solana_keypair::Keypair>,
}
impl OwnerStateV1 {
pub(crate) fn new(
envelope: crate::KspWalletEnvelopeV1,
owner_root: crate::crypto::SecretKeyV1,
metadata_key: crate::crypto::SecretKeyV1,
secret_key: crate::crypto::SecretKeyV1,
admin_signing_key: ed25519_dalek::SigningKey,
solana_keypair: solana_keypair::Keypair,
) -> Self {
return Self {
envelope,
owner_root: std::option::Option::Some(owner_root),
metadata_key: std::option::Option::Some(metadata_key),
secret_key: std::option::Option::Some(secret_key),
admin_signing_key: std::option::Option::Some(admin_signing_key),
solana_keypair: std::option::Option::Some(solana_keypair),
};
}
pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 {
return &self.envelope;
}
}
impl std::ops::Drop for OwnerStateV1 {
fn drop(&mut self) {
let owner_root = self.owner_root.take();
let metadata_key = self.metadata_key.take();
let secret_key = self.secret_key.take();
let admin_signing_key = self.admin_signing_key.take();
let solana_keypair = self.solana_keypair.take();
drop(owner_root);
drop(metadata_key);
drop(secret_key);
drop(admin_signing_key);
drop(solana_keypair);
}
}
pub(crate) struct ViewStateV1 {
envelope: crate::KspWalletEnvelopeV1,
metadata_key: std::option::Option<crate::crypto::SecretKeyV1>,
}
impl ViewStateV1 {
pub(crate) fn new(envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::crypto::SecretKeyV1) -> Self {
return Self { envelope, metadata_key: std::option::Option::Some(metadata_key) };
}
pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 {
return &self.envelope;
}
}
impl std::ops::Drop for ViewStateV1 {
fn drop(&mut self) {
let metadata_key = self.metadata_key.take();
drop(metadata_key);
}
}
/// 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 the OWNER handle's
/// `to_json_bytes()` projection for the complete locked document until `pre.006` adds atomic persistence.
pub async fn create_wallet_v1(
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadataV1,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let owner_root = match crate::crypto::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() {
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() {
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 }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret);
let owner_auth_public_key = admin_signing_key.verifying_key().to_bytes();
let mut solana_secret = match crate::crypto::random_bytes::<32>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
admin_secret.zeroize();
return std::result::Result::Err(error);
},
};
let solana_keypair = solana_keypair::Keypair::new_from_array(solana_secret);
solana_secret.zeroize();
let mut solana_keypair_bytes = solana_keypair.to_bytes();
let pubkey_result = pubkey_from_keypair_bytes(&solana_keypair_bytes);
let pubkey = match pubkey_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
admin_secret.zeroize();
solana_keypair_bytes.zeroize();
return std::result::Result::Err(error);
},
};
let metadata_encoded_result = crate::payload::encode_initial_metadata_payload(pubkey, metadata);
let (mut metadata_plaintext, metadata_payload) = match metadata_encoded_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
admin_secret.zeroize();
solana_keypair_bytes.zeroize();
return std::result::Result::Err(error);
},
};
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 }>() {
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 }>() {
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() {
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 view_material_result = prepare_view_creation(view_password);
let view_material = match view_material_result {
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_control_nonce = match crate::crypto::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() {
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() {
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 provisional = provisional_envelope(
owner_auth_public_key,
owner_slot_id,
owner_kdf.clone(),
owner_wrap_nonce,
view_material.as_ref(),
owner_control_nonce,
metadata_nonce,
secret_nonce,
);
let owner_derived_result = derive_owner_password_key_async(owner_password, owner_kdf.clone()).await;
let owner_derived = match owner_derived_result {
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_wrapped_result = crate::crypto::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) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let view_slot_result = seal_view_slot(&provisional, view_material, &metadata_key).await;
let view_slot = match view_slot_result {
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_control_ciphertext_result = crate::crypto::encrypt_bytes(
&owner_root,
&owner_control_nonce,
provisional.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
owner_control_plaintext.as_slice(),
);
let owner_control_ciphertext = match owner_control_ciphertext_result {
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);
},
};
owner_control_plaintext.zeroize();
let metadata_ciphertext_result = crate::crypto::encrypt_bytes(
&metadata_key,
&metadata_nonce,
provisional.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
metadata_plaintext.as_slice(),
);
let metadata_ciphertext = match metadata_ciphertext_result {
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);
},
};
metadata_plaintext.zeroize();
let secret_ciphertext_result = crate::crypto::encrypt_bytes(
&secret_key,
&secret_nonce,
provisional.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(),
solana_keypair_bytes.as_slice(),
);
let secret_ciphertext = match secret_ciphertext_result {
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);
},
};
solana_keypair_bytes.zeroize();
let owner_slot =
crate::WalletKeySlotV1::new(owner_slot_id, crate::WalletKeySlotRoleV1::Owner, owner_kdf, crate::WalletKeyWrapV1::new(owner_wrap_nonce, owner_wrapped));
let view_descriptor = match view_slot.as_ref() {
std::option::Option::Some(slot) => crate::WalletViewDescriptorV1::enabled(*slot.slot_id()),
std::option::Option::None => crate::WalletViewDescriptorV1::disabled(),
};
let owner_control = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::OwnerControl, owner_control_nonce, owner_control_ciphertext);
let metadata_compartment = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Metadata, metadata_nonce, metadata_ciphertext);
let secret_compartment = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Secret, secret_nonce, secret_ciphertext);
let unsigned = crate::KspWalletEnvelopeV1::new_internal(
owner_auth_public_key,
view_descriptor,
owner_slot.clone(),
view_slot.clone(),
owner_control.clone(),
metadata_compartment.clone(),
secret_compartment.clone(),
crate::WalletStateSignatureV1::new([0_u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]),
);
let signature = admin_signing_key.sign(unsigned.state_transcript().as_slice()).to_bytes();
let envelope = crate::KspWalletEnvelopeV1::new_internal(
owner_auth_public_key,
view_descriptor,
owner_slot,
view_slot,
owner_control,
metadata_compartment,
secret_compartment,
crate::WalletStateSignatureV1::new(signature),
);
let verify_result = verify_state_signature(&envelope);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(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_create",
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
view_enabled = state.envelope().view_descriptor().enabled(),
"native wallet created in memory"
);
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 {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
};
let signature = ed25519_dalek::Signature::from_bytes(envelope.state_signature().signature());
let verify_result = verifying_key.verify_strict(envelope.state_transcript().as_slice(), &signature);
return match verify_result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(_) => std::result::Result::Err(authentication_error()),
};
}
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 }>() {
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 }>() {
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() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::option::Option::Some(ViewCreationMaterialV1 {
slot_id,
kdf: crate::WalletKdfParametersV1::new_creation(salt.to_vec()),
wrap_nonce,
password,
}));
}
async fn seal_view_slot(
envelope: &crate::KspWalletEnvelopeV1,
material: std::option::Option<ViewCreationMaterialV1>,
metadata_key: &crate::crypto::SecretKeyV1,
) -> ksp_core_lib::Result<std::option::Option<crate::WalletKeySlotV1>> {
let material = match material {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let derived_result = derive_view_password_key_async(material.password, material.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 provisional_slot = crate::WalletKeySlotV1::new(
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]),
);
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 = match wrapped_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::option::Option::Some(crate::WalletKeySlotV1::new(
material.slot_id,
crate::WalletKeySlotRoleV1::View,
material.kdf,
crate::WalletKeyWrapV1::new(material.wrap_nonce, wrapped),
)));
}
fn provisional_envelope(
owner_auth_public_key: [u8; crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES],
owner_slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
owner_kdf: crate::WalletKdfParametersV1,
owner_wrap_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
view_material: std::option::Option<&ViewCreationMaterialV1>,
owner_control_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
metadata_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
secret_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
) -> crate::KspWalletEnvelopeV1 {
let owner_slot = crate::WalletKeySlotV1::new(
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]),
);
let (view_descriptor, view_slot) = match view_material {
std::option::Option::Some(material) => (
crate::WalletViewDescriptorV1::enabled(material.slot_id),
std::option::Option::Some(crate::WalletKeySlotV1::new(
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]),
)),
),
std::option::Option::None => (crate::WalletViewDescriptorV1::disabled(), std::option::Option::None),
};
return crate::KspWalletEnvelopeV1::new_internal(
owner_auth_public_key,
view_descriptor,
owner_slot,
view_slot,
crate::WalletEncryptedCompartmentV1::new(
crate::WalletCompartmentKindV1::OwnerControl,
owner_control_nonce,
std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES],
),
crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Metadata, metadata_nonce, std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Secret, secret_nonce, std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
crate::WalletStateSignatureV1::new([0_u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]),
);
}
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));
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));
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()),
};
}
fn pubkey_from_keypair_bytes(bytes: &[u8]) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
let public_slice = match bytes.get(32..64) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(key_material_error()),
};
let public_array = match <[u8; 32]>::try_from(public_slice) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(key_material_error()),
};
return std::result::Result::Ok(ksp_core_lib::Pubkey::new_from_array(public_array));
}
fn cleanup_create_error<T>(
error: ksp_core_lib::Error,
owner_control_plaintext: &mut [u8],
metadata_plaintext: &mut [u8],
solana_keypair_bytes: &mut [u8],
) -> ksp_core_lib::Result<T> {
owner_control_plaintext.zeroize();
metadata_plaintext.zeroize();
solana_keypair_bytes.zeroize();
return std::result::Result::Err(error);
}
fn authentication_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_AUTHENTICATION_FAILED, "Wallet OWNER-authenticated state verification failed");
}
fn crypto_operation_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_CRYPTO_OPERATION_FAILED, "Wallet blocking cryptographic operation could not complete");
}
fn view_unlock_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_VIEW_UNLOCK_FAILED, "Wallet VIEW capability could not be unlocked");
}
fn owner_unlock_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_OWNER_UNLOCK_FAILED, "Wallet OWNER capability could not be unlocked");
}
fn capability_error(message: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_CAPABILITY_INSUFFICIENT, message);
}
fn key_material_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_KEY_MATERIAL_INVALID, "Wallet Solana key material is invalid or inconsistent");
}
#[cfg(test)]
#[path = "../unit_tests/wallet.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/wire.rs
// version: 3
// version: 4
//! Strict native `.kspwallet` V1 wire envelope.
@@ -105,6 +105,17 @@ pub struct WalletKdfParametersV1 {
}
impl WalletKdfParametersV1 {
pub(crate) fn new_creation(salt: std::vec::Vec<u8>) -> Self {
return Self {
algorithm: WalletKdfAlgorithmV1::Argon2id,
version: crate::KSPWALLET_V1_ARGON2_VERSION,
memory_kib: crate::KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB,
iterations: crate::KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS,
parallelism: crate::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM,
salt,
};
}
/// Returns the KDF algorithm.
#[must_use]
pub const fn algorithm(&self) -> WalletKdfAlgorithmV1 {
@@ -165,6 +176,10 @@ pub struct WalletKeyWrapV1 {
}
impl WalletKeyWrapV1 {
pub(crate) fn new(nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec<u8>) -> Self {
return Self { algorithm: WalletAeadAlgorithmV1::XChaCha20Poly1305, nonce, ciphertext };
}
/// Returns the wrapping AEAD algorithm.
#[must_use]
pub const fn algorithm(&self) -> WalletAeadAlgorithmV1 {
@@ -205,6 +220,10 @@ pub struct WalletKeySlotV1 {
}
impl WalletKeySlotV1 {
pub(crate) fn new(slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES], role: WalletKeySlotRoleV1, kdf: WalletKdfParametersV1, wrap: WalletKeyWrapV1) -> Self {
return Self { slot_id, role, kdf, wrap };
}
/// Returns the stable 16-byte slot identifier.
#[must_use]
pub const fn slot_id(&self) -> &[u8; crate::KSPWALLET_V1_SLOT_ID_BYTES] {
@@ -250,6 +269,14 @@ pub struct WalletViewDescriptorV1 {
}
impl WalletViewDescriptorV1 {
pub(crate) const fn disabled() -> Self {
return Self { enabled: false, slot_id: std::option::Option::None };
}
pub(crate) const fn enabled(slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES]) -> Self {
return Self { enabled: true, slot_id: std::option::Option::Some(slot_id) };
}
/// Reports whether a VIEW slot is enabled.
#[must_use]
pub const fn enabled(&self) -> bool {
@@ -274,6 +301,16 @@ pub struct WalletEncryptedCompartmentV1 {
}
impl WalletEncryptedCompartmentV1 {
pub(crate) fn new(kind: WalletCompartmentKindV1, nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec<u8>) -> Self {
return Self {
kind,
payload_version: crate::KSPWALLET_V1_INITIAL_PAYLOAD_VERSION,
algorithm: WalletAeadAlgorithmV1::XChaCha20Poly1305,
nonce,
ciphertext,
};
}
/// Returns the compartment kind.
#[must_use]
pub const fn kind(&self) -> WalletCompartmentKindV1 {
@@ -326,6 +363,10 @@ pub struct WalletStateSignatureV1 {
}
impl WalletStateSignatureV1 {
pub(crate) const fn new(signature: [u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]) -> Self {
return Self { algorithm: WalletStateSignatureAlgorithmV1::Ed25519, signature };
}
/// Returns the state-signature algorithm.
#[must_use]
pub const fn algorithm(&self) -> WalletStateSignatureAlgorithmV1 {
@@ -363,6 +404,28 @@ pub struct KspWalletEnvelopeV1 {
}
impl KspWalletEnvelopeV1 {
pub(crate) fn new_internal(
owner_auth_public_key: [u8; crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES],
view_descriptor: WalletViewDescriptorV1,
owner_slot: WalletKeySlotV1,
view_slot: std::option::Option<WalletKeySlotV1>,
owner_control: WalletEncryptedCompartmentV1,
metadata: WalletEncryptedCompartmentV1,
secret: WalletEncryptedCompartmentV1,
state_signature: WalletStateSignatureV1,
) -> Self {
return Self {
owner_auth_public_key,
view_descriptor,
owner_slot,
view_slot,
owner_control,
metadata,
secret,
state_signature,
};
}
/// Parses and semantically validates one complete `.kspwallet` V1 JSON document.
///
/// This function validates only the V1 wire grammar, identifiers, canonical Base64url representation, structural bounds and key-slot invariants.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/tests/dependency_boundary.rs
// version: 6
// version: 7
//! Wallet-specific dependency and ownership canaries.
@@ -42,10 +42,13 @@ fn wallet_manifest_preserves_dependency_firewall() -> std::io::Result<()> {
assert!(manifest.contains("ksp-logging-lib"));
assert!(manifest.contains("argon2 = { workspace = true, features = [\"alloc\", \"zeroize\"] }"));
assert!(manifest.contains("chacha20poly1305 = { workspace = true, features = [\"alloc\", \"zeroize\"] }"));
assert!(manifest.contains("ed25519-dalek = { workspace = true, features = [\"signature\", \"zeroize\"] }"));
assert!(manifest.contains("getrandom.workspace = true"));
assert!(manifest.contains("base64.workspace = true"));
assert!(manifest.contains("serde = { workspace = true, features = [\"derive\"] }"));
assert!(manifest.contains("serde_json.workspace = true"));
assert!(manifest.contains("solana-keypair.workspace = true"));
assert!(manifest.contains("tokio = { workspace = true, features = [\"rt\"] }"));
assert!(manifest.contains("zeroize.workspace = true"));
for forbidden in [
"ksp-config-lib",

View File

@@ -0,0 +1,67 @@
{
"magic": "KSPWALLET",
"format_version": 1,
"owner_auth_public_key": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ",
"view_descriptor": {
"enabled": true,
"slot_id": "sbKztLW2t7i5uru8vb6_wA"
},
"key_slots": [
{
"slot_id": "oaKjpKWmp6ipqqusra6vsA",
"role": "owner",
"kdf": {
"algorithm": "argon2id",
"version": 19,
"memory_kib": 32,
"iterations": 2,
"parallelism": 1,
"salt": "wcLDxMXGx8jJysvMzc7P0A"
},
"wrap": {
"algorithm": "xchacha20-poly1305",
"nonce": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY",
"ciphertext": "Ty633tO_mYpLTEMC1FjBp0Nt95oA2hggQV34DxdNG9pRqnHSD_BMoms3s8G6lSUO"
}
},
{
"slot_id": "sbKztLW2t7i5uru8vb6_wA",
"role": "view",
"kdf": {
"algorithm": "argon2id",
"version": 19,
"memory_kib": 32,
"iterations": 2,
"parallelism": 1,
"salt": "0dLT1NXW19jZ2tvc3d7f4A"
},
"wrap": {
"algorithm": "xchacha20-poly1305",
"nonce": "GRobHB0eHyAhIiMkJSYnKCkqKywtLi8w",
"ciphertext": "lFemQ6rPpYGl6wQBbkDfnHkitlNEsEfq2Us2Sm3dgQ14ZUe_T6EGufHWtCtrXDwr"
}
}
],
"owner_control": {
"control_version": 1,
"algorithm": "xchacha20-poly1305",
"nonce": "MTIzNDU2Nzg5Ojs8PT4_QEFCQ0RFRkdI",
"ciphertext": "r79zNsVtzi7tWr55hD_J5bnJbtODUMpFChLVlpsgJEEVaZOB1oXiJkkYGFmGt9tgI7X0Szt66pHZ7z1IcmuHPjgVPxEOkD4VxvomfLQpdFKZP8dcGkIGZhqXNt9kcK7iMsu6q-DcjTv4vUfWp6S5sw"
},
"metadata": {
"metadata_version": 1,
"algorithm": "xchacha20-poly1305",
"nonce": "SUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9g",
"ciphertext": "uKyxG7n0baGnTwogplINSiirhUOj2mh7-FOv-73ch91NiCnoo1rkEl35ezO1niAfpQyk7QjGRlwxXlAGmH0ppgR5vx0D9M0LPVTNyb9Z2sm6XXckaJZQh-Nlc9Jo1cZwMkm6LHzSiwiLt5IR1BMjYvdIcctYztnpimVI3WI2VANTZre1mInyt1g_T7ZlsfcuNtrJFRqFhyJvAu-5EwxcIqR5ZIKi8hsI9BnAxqPrbPsLbFWFnoac43GcWlEuG2GJRp92rKAI5QEi9zOSMtssjZJ2B-A1dE-wU9tsCga1-iVXU7gs9xfNL6xrlhn3eWNBgzUWFlXGSUqL8Z1I"
},
"secret": {
"secret_version": 1,
"algorithm": "xchacha20-poly1305",
"nonce": "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4",
"ciphertext": "VVZmG6-XrlNMoUSx10Z5Utn0DDmJ7zt1IYN4qTuX1uMkR04ZAOR_02vumg9MFaY66RHt_hGmtut2PNJxFff4sXSzceJb_JDeu1iYOE4-KH4"
},
"state_signature": {
"algorithm": "ed25519",
"signature": "RDH52zfvMvAcHTmFChhzxIC-eY5N6JU8kE_B9scQ25Y1NPwVgElmnlSXVUig4o0PPXJsGAQ4sd0Lij2FuGIyCw"
}
}

View File

@@ -0,0 +1,24 @@
{
"vector_version": 1,
"warning": "TEST ONLY. Passwords, keys and wallet bytes are public fixtures and MUST NOT be used for a real wallet.",
"owner_password_utf8": "pre005-owner-password",
"view_password_utf8": "pre005-view-password",
"expected_pubkey": "8zH45w576QJUEGtpXqZvEi6UPddmMfKopLatocZGDw6",
"expected_alias": "pre005-vector-wallet",
"expected_notes": [
"first public test-only note",
"second public test-only note"
],
"admin_signing_secret": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA",
"owner_root_key": "ISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0A",
"metadata_key": "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2A",
"secret_key": "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1-f4A",
"solana_secret": "gYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6A",
"solana_keypair": "gYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ACC9QnRGtyNCTYDSytNSuj3zZJ0O-PquDKfrJUQ5QbKQ",
"metadata_plaintext_utf8": "{\"pubkey\":\"8zH45w576QJUEGtpXqZvEi6UPddmMfKopLatocZGDw6\",\"alias\":\"pre005-vector-wallet\",\"notes\":[{\"id\":\"eXp7fH1-f4CBgoOEhYaHiA\",\"text\":\"first public test-only note\"},{\"id\":\"iYqLjI2Oj5CRkpOUlZaXmA\",\"text\":\"second public test-only note\"}]}",
"owner_control_plaintext": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyBBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-A",
"owner_derived_key": "o0_1L9S1E1t_-b-0mQT8lY5AVqdv00GaMAxDM914wBU",
"view_derived_key": "tzZ_VEogXNP1gdmKko2Xth8wZDMwe0VG4iIe6qYV-8A",
"state_transcript": "S1NQV0FMTEVULVYxLVNUQVRFAAABAAAAAAAAAAlLU1BXQUxMRVQAAgAAAAAAAAAEAAAAAQADAAAAAAAAACB5tVYuj-ZU-UB4sRLoqYunkB-FOuaVvtfg45ELrQSWZAAQAAAAAAAAAAEBABEAAAAAAAAABHZpZXcAEgAAAAAAAAAQsbKztLW2t7i5uru8vb6_wAEAAAAAAAAAABChoqOkpaanqKmqq6ytrq-wAQEAAAAAAAAABW93bmVyAQIAAAAAAAAACGFyZ29uMmlkAQMAAAAAAAAABAAAABMBBAAAAAAAAAAEAAAAIAEFAAAAAAAAAAQAAAACAQYAAAAAAAAABAAAAAEBBwAAAAAAAAAQwcLDxMXGx8jJysvMzc7P0AEIAAAAAAAAABJ4Y2hhY2hhMjAtcG9seTEzMDUBCQAAAAAAAAAYAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYAQoAAAAAAAAAME8ut97Tv5mKS0xDAtRYwadDbfeaANoYIEFd-A8XTRvaUapx0g_wTKJrN7PBupUlDgIAAAAAAAAAAA1vd25lci1jb250cm9sAgEAAAAAAAAABAAAAAECAgAAAAAAAAASeGNoYWNoYTIwLXBvbHkxMzA1AgMAAAAAAAAAGDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSAIEAAAAAAAAAHCvv3M2xW3OLu1avnmEP8nluclu04NQykUKEtWWmyAkQRVpk4HWheImSRgYWYa322AjtfRLO3rqkdnvPUhya4c-OBU_EQ6QPhXG-iZ8tCl0Upk_x1waQgZmGpc232RwruIyy7qr4NyNO_i9R9anpLmzAgAAAAAAAAAACG1ldGFkYXRhAgEAAAAAAAAABAAAAAECAgAAAAAAAAASeGNoYWNoYTIwLXBvbHkxMzA1AgMAAAAAAAAAGElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYAIEAAAAAAAAAPy4rLEbufRtoadPCiCmUg1KKKuFQ6PaaHv4U6_7vdyH3U2IKeijWuQSXfl7M7WeIB-lDKTtCMZGXDFeUAaYfSmmBHm_HQP0zQs9VM3Jv1naybpddyRollCH42Vz0mjVxnAySbosfNKLCIu3khHUEyNi90hxy1jO2emKZUjdYjZUA1Nmt7WYifK3WD9PtmWx9y422skVGoWHIm8C77kTDFwipHlkgqLyGwj0GcDGo-ts-wtsVYWehpzjcZxaUS4bYYlGn3asoAjlASL3M5Iy2yyNknYH4DV0T7BT22wKBrX6JVdTuCz3F80vrGuWGfd5Y0GDNRYWVcZJSovxnUgCAAAAAAAAAAAGc2VjcmV0AgEAAAAAAAAABAAAAAECAgAAAAAAAAASeGNoYWNoYTIwLXBvbHkxMzA1AgMAAAAAAAAAGGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eAIEAAAAAAAAAFBVVmYbr5euU0yhRLHXRnlS2fQMOYnvO3Uhg3ipO5fW4yRHThkA5H_Ta-6aD0wVpjrpEe3-Eaa263Y80nEV9_ixdLNx4lv8kN67WJg4Tj4ofgUAAAAAAAAAAAdlZDI1NTE5",
"state_signature": "RDH52zfvMvAcHTmFChhzxIC-eY5N6JU8kE_B9scQ25Y1NPwVgElmnlSXVUig4o0PPXJsGAQ4sd0Lij2FuGIyCw"
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/tests/public_api.rs
// version: 3
// version: 4
//! Public API canaries for the Wallet foundation.
@@ -43,6 +43,7 @@ fn wallet_error_codes_are_available_from_crate_root() {
ksp_wallet_lib::ERROR_CODE_FORMAT_INVALID,
ksp_wallet_lib::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED,
ksp_wallet_lib::ERROR_CODE_CRYPTO_PARAMETERS_INVALID,
ksp_wallet_lib::ERROR_CODE_CRYPTO_OPERATION_FAILED,
ksp_wallet_lib::ERROR_CODE_RANDOMNESS_FAILED,
ksp_wallet_lib::ERROR_CODE_AUTHENTICATION_FAILED,
ksp_wallet_lib::ERROR_CODE_VIEW_UNLOCK_FAILED,
@@ -55,7 +56,7 @@ fn wallet_error_codes_are_available_from_crate_root() {
ksp_wallet_lib::ERROR_CODE_KEY_MATERIAL_INVALID,
ksp_wallet_lib::ERROR_CODE_SIGNATURE_FAILED,
];
assert_eq!(codes.len(), 14);
assert_eq!(codes.len(), 15);
for code in codes {
assert_eq!(code.domain(), "wallet");
}
@@ -77,3 +78,17 @@ fn strict_v1_envelope_and_transcript_are_available_from_crate_root() -> ksp_core
assert!(!envelope.compartment_aad(ksp_wallet_lib::WalletCompartmentKindV1::Metadata).is_empty());
return std::result::Result::Ok(());
}
#[test]
fn public_pre_005_create_open_and_calibrated_defaults_are_available_from_crate_root() {
let _ = ksp_wallet_lib::create_wallet_v1;
let _ = ksp_wallet_lib::open_wallet_view_v1;
let _ = ksp_wallet_lib::open_wallet_owner_v1;
let _ = ksp_wallet_lib::inspect_locked_wallet_v1;
let metadata = ksp_wallet_lib::WalletCreateMetadataV1::default();
assert_eq!(format!("{metadata:?}"), "WalletCreateMetadataV1 { alias: None, note_count: 0 }");
assert_eq!(ksp_wallet_lib::KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB, 65_536);
assert_eq!(ksp_wallet_lib::KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS, 3);
assert_eq!(ksp_wallet_lib::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM, 1);
assert_eq!(ksp_wallet_lib::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES, 32);
}

View File

@@ -0,0 +1,32 @@
// file: crates/ksp-wallet-lib/unit_tests/payload.rs
// version: 1
use base64::Engine as _;
#[test]
fn metadata_payload_rejects_duplicate_note_identifiers() -> ksp_core_lib::Result<()> {
let duplicate_id = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([7_u8; crate::KSPWALLET_V1_NOTE_ID_BYTES]);
let payload = serde_json::json!({
"pubkey": "11111111111111111111111111111111",
"alias": null,
"notes": [
{"id": duplicate_id.clone(), "text": "first"},
{"id": duplicate_id, "text": "second"}
]
});
let encoded = match serde_json::to_vec(&payload) {
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 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,
};
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
return std::result::Result::Ok(());
}
fn test_error(message: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message);
}

View File

@@ -0,0 +1,316 @@
// file: crates/ksp-wallet-lib/unit_tests/wallet.rs
// version: 2
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");
#[derive(serde::Deserialize)]
struct FullVectorMeta {
warning: std::string::String,
owner_password_utf8: std::string::String,
view_password_utf8: std::string::String,
expected_pubkey: std::string::String,
expected_alias: std::string::String,
expected_notes: std::vec::Vec<std::string::String>,
}
#[test]
fn externally_generated_full_vector_opens_view_and_owner_independently() -> ksp_core_lib::Result<()> {
let vector = match full_vector_meta() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(vector.warning.contains("TEST ONLY"));
let runtime = match test_runtime() {
std::result::Result::Ok(value) => value,
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 = match view_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(view.capability(), crate::WalletCapability::View);
assert_eq!(view.pubkey().to_string(), vector.expected_pubkey);
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 = match owner_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(owner.capability(), crate::WalletCapability::Owner);
assert_eq!(owner.pubkey(), view.pubkey());
assert_eq!(owner.alias(), view.alias());
assert_eq!(owner.notes(), view.notes());
return std::result::Result::Ok(());
}
#[test]
fn wrong_passwords_do_not_cross_unlock_capabilities() -> ksp_core_lib::Result<()> {
let vector = match full_vector_meta() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let runtime = match test_runtime() {
std::result::Result::Ok(value) => value,
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_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_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_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,
};
assert_eq!(view_as_owner_error.code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
return std::result::Result::Ok(());
}
#[test]
fn owner_signed_metadata_tampering_is_rejected_before_unlock() -> ksp_core_lib::Result<()> {
let tampered = match replace_string_field(FULL_VECTOR, &["metadata", "ciphertext"], "AAAAAAAAAAAAAAAAAAAAAA") {
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 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,
};
assert_eq!(error.code(), crate::ERROR_CODE_AUTHENTICATION_FAILED);
return std::result::Result::Ok(());
}
#[test]
fn view_wrap_can_change_without_breaking_owner_authenticated_state() -> ksp_core_lib::Result<()> {
let vector = match full_vector_meta() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let tampered = match replace_view_wrap_ciphertext(FULL_VECTOR, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") {
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()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(inspected.view_enabled());
let runtime = match test_runtime() {
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)));
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_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,
};
assert_eq!(view_error.code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
return std::result::Result::Ok(());
}
#[test]
fn locked_full_vector_contains_no_authorized_identity_or_metadata_plaintext() -> ksp_core_lib::Result<()> {
let vector = match full_vector_meta() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let document_result = std::str::from_utf8(FULL_VECTOR);
let document = match document_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(test_error("full Wallet vector is not UTF-8")),
};
assert!(!document.contains(vector.expected_pubkey.as_str()));
assert!(!document.contains(vector.expected_alias.as_str()));
for note in vector.expected_notes {
assert!(!document.contains(note.as_str()));
}
let locked = match super::inspect_locked_wallet_v1(FULL_VECTOR) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(locked.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1);
assert!(locked.view_enabled());
return std::result::Result::Ok(());
}
#[test]
fn owner_and_view_slots_use_independent_kdf_material() -> ksp_core_lib::Result<()> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(FULL_VECTOR) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => 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(test_error("full Wallet vector is missing VIEW slot")),
};
assert_ne!(envelope.owner_slot().slot_id(), view_slot.slot_id());
assert_ne!(envelope.owner_slot().kdf().salt(), view_slot.kdf().salt());
assert_ne!(envelope.owner_slot().wrap().nonce(), view_slot.wrap().nonce());
return std::result::Result::Ok(());
}
#[test]
fn create_uses_calibrated_defaults_and_keeps_locked_projection_private() -> ksp_core_lib::Result<()> {
let alias = std::string::String::from("CREATE-ALIAS-PLAINTEXT-CANARY");
let note = std::string::String::from("CREATE-NOTE-PLAINTEXT-CANARY");
let metadata = crate::WalletCreateMetadataV1::new(std::option::Option::Some(alias.clone()), std::vec![note.clone()]);
let runtime = match test_runtime() {
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(
crate::OwnerPassword::new(std::string::String::from("pre005-create-owner-password")),
std::option::Option::None,
metadata,
));
let owner = match owner_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(owner.alias(), std::option::Option::Some(alias.as_str()));
assert_eq!(owner.notes().len(), 1);
let created_note = match owner.notes().first() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("created Wallet is missing initial note")),
};
assert_eq!(created_note.text(), note.as_str());
let locked_bytes = match owner.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let locked_text = match std::str::from_utf8(locked_bytes.as_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(test_error("created Wallet JSON is not UTF-8")),
};
assert!(!locked_text.contains(alias.as_str()));
assert!(!locked_text.contains(note.as_str()));
assert!(!locked_text.contains(owner.pubkey().to_string().as_str()));
let envelope = match crate::KspWalletEnvelopeV1::parse_json(locked_bytes.as_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(envelope.owner_slot().kdf().memory_kib(), crate::KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB);
assert_eq!(envelope.owner_slot().kdf().iterations(), crate::KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS);
assert_eq!(envelope.owner_slot().kdf().parallelism(), crate::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM);
assert_eq!(envelope.owner_slot().kdf().salt().len(), crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES);
assert!(!envelope.view_descriptor().enabled());
assert!(envelope.view_slot().is_none());
return std::result::Result::Ok(());
}
fn full_vector_meta() -> ksp_core_lib::Result<FullVectorMeta> {
let parsed = serde_json::from_slice::<FullVectorMeta>(FULL_VECTOR_META);
return match parsed {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(test_error("full Wallet vector metadata JSON is invalid")),
};
}
fn test_runtime() -> ksp_core_lib::Result<tokio::runtime::Runtime> {
let built = tokio::runtime::Builder::new_current_thread().build();
return match built {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(test_error("Wallet unit-test Tokio runtime could not be built").with_source(error)),
};
}
fn assert_note_texts(notes: &[crate::WalletNote], expected: &[std::string::String]) {
assert_eq!(notes.len(), expected.len());
for (note, expected_text) in notes.iter().zip(expected.iter()) {
assert_eq!(note.text(), expected_text.as_str());
}
}
fn replace_string_field(source: &[u8], path: &[&str], replacement: &str) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let parsed = serde_json::from_slice::<serde_json::Value>(source);
let mut value = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(test_error("Wallet test JSON cannot be parsed")),
};
let mut current = &mut value;
for key in path {
let object = match current.as_object_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test JSON path does not reference an object")),
};
current = match object.get_mut(*key) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test JSON path is missing")),
};
}
*current = serde_json::Value::String(std::string::String::from(replacement));
return serialize_test_json(&value);
}
fn replace_view_wrap_ciphertext(source: &[u8], replacement: &str) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let parsed = serde_json::from_slice::<serde_json::Value>(source);
let mut value = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(test_error("Wallet test JSON cannot be parsed")),
};
let object = match value.as_object_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test JSON root is not an object")),
};
let slots = match object.get_mut("key_slots") {
std::option::Option::Some(value) => match value.as_array_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test key_slots is not an array")),
},
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test key_slots is missing")),
};
let view_slot = match slots.get_mut(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test VIEW slot is missing")),
};
let view_object = match view_slot.as_object_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test VIEW slot is not an object")),
};
let wrap = match view_object.get_mut("wrap") {
std::option::Option::Some(value) => match value.as_object_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test VIEW wrap is not an object")),
},
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test VIEW wrap is missing")),
};
wrap.insert(std::string::String::from("ciphertext"), serde_json::Value::String(std::string::String::from(replacement)));
return serialize_test_json(&value);
}
fn serialize_test_json(value: &serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let serialized = serde_json::to_vec(value);
return match serialized {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(test_error("Wallet test JSON cannot be serialized")),
};
}
fn test_error(message: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message);
}