v0.2.5-pre.005
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
207
crates/ksp-wallet-lib/src/payload.rs
Normal file
207
crates/ksp-wallet-lib/src/payload.rs
Normal 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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
682
crates/ksp-wallet-lib/src/wallet.rs
Normal file
682
crates/ksp-wallet-lib/src/wallet.rs
Normal 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;
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
67
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector.json
vendored
Normal file
67
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
24
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector_meta.json
vendored
Normal file
24
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector_meta.json
vendored
Normal 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"
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
32
crates/ksp-wallet-lib/unit_tests/payload.rs
Normal file
32
crates/ksp-wallet-lib/unit_tests/payload.rs
Normal 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);
|
||||
}
|
||||
316
crates/ksp-wallet-lib/unit_tests/wallet.rs
Normal file
316
crates/ksp-wallet-lib/unit_tests/wallet.rs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user