v0.2.5-pre.005

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

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 150
# version: 151
[workspace]
resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package]
version = "0.2.5-pre.4.fix.2"
version = "0.2.5-pre.5"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
@@ -16,6 +16,7 @@ publish = false
[workspace.dependencies]
argon2 = { version = "^0.5", default-features = false }
chacha20poly1305 = { version = "^0.11", default-features = false }
ed25519-dalek = { version = "^2.2", default-features = false }
getrandom = { version = "^0.4", default-features = false }
base64 = { version = "^0.23" }
fs2 = { version = "^0.4" }
@@ -23,6 +24,7 @@ serde = { version = "^1.0" }
serde_json = { version = "^1.0" }
jsonschema = { version = "^0.49", default-features = false }
reqwest = { version = "^0.13", default-features = false }
solana-keypair = { version = "^3.1", default-features = false }
solana-pubkey = { version = "^4.3", default-features = false }
tracing = { version = "^0.1", default-features = false }
tracing-subscriber = { version = "^0.3", default-features = false }

View File

@@ -49,7 +49,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
- [X] `0.2.2` — HTTP Accounts + Tokens + Cluster : 22 wrappers typés (5 Accounts + 5 Tokens + 12 Cluster), canaries de complétude 52+14, smoke Devnet Transport pur et smoke historique Config -> Transport validés, documentation durable et prompt `0.2.3` publiés stables.
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
- [/] `0.2.5` — Wallet foundation : `pre.001` fixe le threat model et le format V1 autonome ; `pre.002` crée `ksp-wallet-lib` avec capabilities VIEW/OWNER et frontières ; `pre.003` fige lenveloppe JSON stricte, transcript OWNER/AAD et la spécification externe initiale ; `pre.004` ajoute les primitives in-memory Argon2id v19 + XChaCha20-Poly1305 + CSPRNG OS, le wrapping de content keys et un vecteur crypto interopérable. Le choix du profil Argon2 de création reste bloqué sur le benchmark opérateur explicite livré par `pre.004`. `Pubkey` reste consommée uniquement via `ksp-core-lib`; Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet. Persistence, state signature, signature Solana, rotations et import/export restent réparties jusquà `pre.010` ; `WalletPolicy` reste exclu.
- [/] `0.2.5` — Wallet foundation : `pre.001` fixe le threat model et le format V1 autonome ; `pre.002` crée `ksp-wallet-lib` avec capabilities VIEW/OWNER et frontières ; `pre.003` fige le wire strict, key slots, transcript OWNER/AAD et la spécification externe ; `pre.004` matérialise Argon2id/XChaCha20-Poly1305/CSPRNG et le wrapping ; `pre.005` fixe le profil de création KSP issu du benchmark, les payloads owner-control/metadata/secret, lautorité Ed25519 séparée et les flux in-memory create/open VIEW/OWNER avec vecteur complet interopérable. `Pubkey` reste consommée uniquement via `ksp-core-lib`; Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet. `pre.006` porte ensuite la persistence atomique/no-clobber ; signature Solana publique, administration/rotations puis import/export restent répartis jusquà `pre.010` ; `WalletPolicy` reste exclu.
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet.
- [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,682 @@
// file: crates/ksp-wallet-lib/src/wallet.rs
// version: 2
//! In-memory native Wallet V1 create/open orchestration.
use ed25519_dalek::Signer as _;
use zeroize::Zeroize as _;
pub(crate) struct OwnerStateV1 {
envelope: crate::KspWalletEnvelopeV1,
owner_root: std::option::Option<crate::crypto::SecretKeyV1>,
metadata_key: std::option::Option<crate::crypto::SecretKeyV1>,
secret_key: std::option::Option<crate::crypto::SecretKeyV1>,
admin_signing_key: std::option::Option<ed25519_dalek::SigningKey>,
solana_keypair: std::option::Option<solana_keypair::Keypair>,
}
impl OwnerStateV1 {
pub(crate) fn new(
envelope: crate::KspWalletEnvelopeV1,
owner_root: crate::crypto::SecretKeyV1,
metadata_key: crate::crypto::SecretKeyV1,
secret_key: crate::crypto::SecretKeyV1,
admin_signing_key: ed25519_dalek::SigningKey,
solana_keypair: solana_keypair::Keypair,
) -> Self {
return Self {
envelope,
owner_root: std::option::Option::Some(owner_root),
metadata_key: std::option::Option::Some(metadata_key),
secret_key: std::option::Option::Some(secret_key),
admin_signing_key: std::option::Option::Some(admin_signing_key),
solana_keypair: std::option::Option::Some(solana_keypair),
};
}
pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 {
return &self.envelope;
}
}
impl std::ops::Drop for OwnerStateV1 {
fn drop(&mut self) {
let owner_root = self.owner_root.take();
let metadata_key = self.metadata_key.take();
let secret_key = self.secret_key.take();
let admin_signing_key = self.admin_signing_key.take();
let solana_keypair = self.solana_keypair.take();
drop(owner_root);
drop(metadata_key);
drop(secret_key);
drop(admin_signing_key);
drop(solana_keypair);
}
}
pub(crate) struct ViewStateV1 {
envelope: crate::KspWalletEnvelopeV1,
metadata_key: std::option::Option<crate::crypto::SecretKeyV1>,
}
impl ViewStateV1 {
pub(crate) fn new(envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::crypto::SecretKeyV1) -> Self {
return Self { envelope, metadata_key: std::option::Option::Some(metadata_key) };
}
pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 {
return &self.envelope;
}
}
impl std::ops::Drop for ViewStateV1 {
fn drop(&mut self) {
let metadata_key = self.metadata_key.take();
drop(metadata_key);
}
}
/// Creates a new in-memory native `.kspwallet` V1 with a fresh Solana keypair.
///
/// The optional VIEW password creates an independent VIEW slot. This function performs no filesystem I/O; use the OWNER handle's
/// `to_json_bytes()` projection for the complete locked document until `pre.006` adds atomic persistence.
pub async fn create_wallet_v1(
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadataV1,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let owner_root = match crate::crypto::SecretKeyV1::random() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let metadata_key = match crate::crypto::SecretKeyV1::random() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let secret_key = match crate::crypto::SecretKeyV1::random() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut admin_secret = match crate::crypto::random_bytes::<{ crate::crypto::SECRET_KEY_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret);
let owner_auth_public_key = admin_signing_key.verifying_key().to_bytes();
let mut solana_secret = match crate::crypto::random_bytes::<32>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
admin_secret.zeroize();
return std::result::Result::Err(error);
},
};
let solana_keypair = solana_keypair::Keypair::new_from_array(solana_secret);
solana_secret.zeroize();
let mut solana_keypair_bytes = solana_keypair.to_bytes();
let pubkey_result = pubkey_from_keypair_bytes(&solana_keypair_bytes);
let pubkey = match pubkey_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
admin_secret.zeroize();
solana_keypair_bytes.zeroize();
return std::result::Result::Err(error);
},
};
let metadata_encoded_result = crate::payload::encode_initial_metadata_payload(pubkey, metadata);
let (mut metadata_plaintext, metadata_payload) = match metadata_encoded_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
admin_secret.zeroize();
solana_keypair_bytes.zeroize();
return std::result::Result::Err(error);
},
};
let mut owner_control_plaintext = crate::payload::encode_owner_control_payload(&admin_secret, &metadata_key, &secret_key);
admin_secret.zeroize();
let owner_slot_id = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let owner_salt = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let owner_kdf = crate::WalletKdfParametersV1::new_creation(owner_salt.to_vec());
let owner_wrap_nonce = match crate::crypto::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let view_material_result = prepare_view_creation(view_password);
let view_material = match view_material_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let owner_control_nonce = match crate::crypto::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let metadata_nonce = match crate::crypto::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let secret_nonce = match crate::crypto::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let provisional = provisional_envelope(
owner_auth_public_key,
owner_slot_id,
owner_kdf.clone(),
owner_wrap_nonce,
view_material.as_ref(),
owner_control_nonce,
metadata_nonce,
secret_nonce,
);
let owner_derived_result = derive_owner_password_key_async(owner_password, owner_kdf.clone()).await;
let owner_derived = match owner_derived_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let owner_wrapped_result = crate::crypto::wrap_key(&owner_derived, &owner_root, &owner_wrap_nonce, provisional.owner_slot_aad().as_slice());
let owner_wrapped = match owner_wrapped_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let view_slot_result = seal_view_slot(&provisional, view_material, &metadata_key).await;
let view_slot = match view_slot_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let owner_control_ciphertext_result = crate::crypto::encrypt_bytes(
&owner_root,
&owner_control_nonce,
provisional.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
owner_control_plaintext.as_slice(),
);
let owner_control_ciphertext = match owner_control_ciphertext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
owner_control_plaintext.zeroize();
let metadata_ciphertext_result = crate::crypto::encrypt_bytes(
&metadata_key,
&metadata_nonce,
provisional.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
metadata_plaintext.as_slice(),
);
let metadata_ciphertext = match metadata_ciphertext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
metadata_plaintext.zeroize();
let secret_ciphertext_result = crate::crypto::encrypt_bytes(
&secret_key,
&secret_nonce,
provisional.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(),
solana_keypair_bytes.as_slice(),
);
let secret_ciphertext = match secret_ciphertext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
solana_keypair_bytes.zeroize();
let owner_slot =
crate::WalletKeySlotV1::new(owner_slot_id, crate::WalletKeySlotRoleV1::Owner, owner_kdf, crate::WalletKeyWrapV1::new(owner_wrap_nonce, owner_wrapped));
let view_descriptor = match view_slot.as_ref() {
std::option::Option::Some(slot) => crate::WalletViewDescriptorV1::enabled(*slot.slot_id()),
std::option::Option::None => crate::WalletViewDescriptorV1::disabled(),
};
let owner_control = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::OwnerControl, owner_control_nonce, owner_control_ciphertext);
let metadata_compartment = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Metadata, metadata_nonce, metadata_ciphertext);
let secret_compartment = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Secret, secret_nonce, secret_ciphertext);
let unsigned = crate::KspWalletEnvelopeV1::new_internal(
owner_auth_public_key,
view_descriptor,
owner_slot.clone(),
view_slot.clone(),
owner_control.clone(),
metadata_compartment.clone(),
secret_compartment.clone(),
crate::WalletStateSignatureV1::new([0_u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]),
);
let signature = admin_signing_key.sign(unsigned.state_transcript().as_slice()).to_bytes();
let envelope = crate::KspWalletEnvelopeV1::new_internal(
owner_auth_public_key,
view_descriptor,
owner_slot,
view_slot,
owner_control,
metadata_compartment,
secret_compartment,
crate::WalletStateSignatureV1::new(signature),
);
let verify_result = verify_state_signature(&envelope);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_create",
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
view_enabled = state.envelope().view_descriptor().enabled(),
"native wallet created in memory"
);
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
}
/// Opens the VIEW capability from a native `.kspwallet` V1 JSON document.
pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result<crate::WalletView> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_result = verify_state_signature(&envelope);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
let view_slot = match envelope.view_slot() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
};
let derived_result = derive_view_password_key_async(password, view_slot.kdf().clone()).await;
let derived = match derived_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let aad = envelope.view_slot_aad();
let view_aad = match aad {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
};
let metadata_key_result = crate::crypto::unwrap_key(&derived, view_slot.wrap().nonce(), view_aad.as_slice(), view_slot.wrap().ciphertext());
let metadata_key = match metadata_key_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
};
let metadata_plaintext_result = crate::crypto::decrypt_bytes(
&metadata_key,
envelope.metadata().nonce(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
envelope.metadata().ciphertext(),
);
let mut metadata_plaintext = match metadata_plaintext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
};
let metadata_payload_result = crate::payload::decode_metadata_payload(metadata_plaintext.as_slice());
metadata_plaintext.zeroize();
let metadata_payload = match metadata_payload_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let info = metadata_payload.into_info(crate::WalletCapability::View);
let state = ViewStateV1::new(envelope, metadata_key);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_open_view",
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
capability = "view",
"native wallet VIEW capability opened"
);
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, state));
}
/// Opens the OWNER capability from a native `.kspwallet` V1 JSON document.
pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result<crate::WalletOwner> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_result = verify_state_signature(&envelope);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
let owner_slot = envelope.owner_slot();
let derived_result = derive_owner_password_key_async(password, owner_slot.kdf().clone()).await;
let derived = match derived_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner_root_result =
crate::crypto::unwrap_key(&derived, owner_slot.wrap().nonce(), envelope.owner_slot_aad().as_slice(), owner_slot.wrap().ciphertext());
let owner_root = match owner_root_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()),
};
let owner_control_plaintext_result = crate::crypto::decrypt_bytes(
&owner_root,
envelope.owner_control().nonce(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
envelope.owner_control().ciphertext(),
);
let mut owner_control_plaintext = match owner_control_plaintext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()),
};
let control_result = crate::payload::decode_owner_control_payload(owner_control_plaintext.as_slice());
owner_control_plaintext.zeroize();
let control = match control_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let (mut admin_secret, metadata_key, secret_key) = control.into_parts();
let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret);
admin_secret.zeroize();
if admin_signing_key.verifying_key().to_bytes() != *envelope.owner_auth_public_key() {
return std::result::Result::Err(authentication_error());
}
let metadata_plaintext_result = crate::crypto::decrypt_bytes(
&metadata_key,
envelope.metadata().nonce(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
envelope.metadata().ciphertext(),
);
let mut metadata_plaintext = match metadata_plaintext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
};
let metadata_payload_result = crate::payload::decode_metadata_payload(metadata_plaintext.as_slice());
metadata_plaintext.zeroize();
let metadata_payload = match metadata_payload_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let secret_plaintext_result = crate::crypto::decrypt_bytes(
&secret_key,
envelope.secret().nonce(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(),
envelope.secret().ciphertext(),
);
let mut secret_plaintext = match secret_plaintext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
};
if secret_plaintext.len() != crate::KSPWALLET_V1_SECRET_PLAINTEXT_BYTES {
secret_plaintext.zeroize();
return std::result::Result::Err(key_material_error());
}
let keypair_result = solana_keypair::Keypair::try_from(secret_plaintext.as_slice());
let solana_keypair = match keypair_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
secret_plaintext.zeroize();
return std::result::Result::Err(key_material_error());
},
};
let secret_pubkey_result = pubkey_from_keypair_bytes(secret_plaintext.as_slice());
let secret_pubkey = match secret_pubkey_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
secret_plaintext.zeroize();
return std::result::Result::Err(error);
},
};
secret_plaintext.zeroize();
if &secret_pubkey != metadata_payload.pubkey() {
return std::result::Result::Err(key_material_error());
}
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_open_owner",
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
capability = "owner",
"native wallet OWNER capability opened"
);
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
}
/// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material.
pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_result = verify_state_signature(&envelope);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(crate::LockedWalletInfo::new(envelope.view_descriptor().enabled()));
}
pub(crate) fn verify_state_signature(envelope: &crate::KspWalletEnvelopeV1) -> ksp_core_lib::Result<()> {
let verifying_key_result = ed25519_dalek::VerifyingKey::from_bytes(envelope.owner_auth_public_key());
let verifying_key = match verifying_key_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
};
let signature = ed25519_dalek::Signature::from_bytes(envelope.state_signature().signature());
let verify_result = verifying_key.verify_strict(envelope.state_transcript().as_slice(), &signature);
return match verify_result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(_) => std::result::Result::Err(authentication_error()),
};
}
struct ViewCreationMaterialV1 {
slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
kdf: crate::WalletKdfParametersV1,
wrap_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
password: crate::ViewPassword,
}
fn prepare_view_creation(password: std::option::Option<crate::ViewPassword>) -> ksp_core_lib::Result<std::option::Option<ViewCreationMaterialV1>> {
let password = match password {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let slot_id = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let salt = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wrap_nonce = match crate::crypto::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::option::Option::Some(ViewCreationMaterialV1 {
slot_id,
kdf: crate::WalletKdfParametersV1::new_creation(salt.to_vec()),
wrap_nonce,
password,
}));
}
async fn seal_view_slot(
envelope: &crate::KspWalletEnvelopeV1,
material: std::option::Option<ViewCreationMaterialV1>,
metadata_key: &crate::crypto::SecretKeyV1,
) -> ksp_core_lib::Result<std::option::Option<crate::WalletKeySlotV1>> {
let material = match material {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let derived_result = derive_view_password_key_async(material.password, material.kdf.clone()).await;
let derived = match derived_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provisional_slot = crate::WalletKeySlotV1::new(
material.slot_id,
crate::WalletKeySlotRoleV1::View,
material.kdf.clone(),
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
);
let aad = crate::slot_aad(envelope, &provisional_slot);
let wrapped_result = crate::crypto::wrap_key(&derived, metadata_key, &material.wrap_nonce, aad.as_slice());
let wrapped = match wrapped_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::option::Option::Some(crate::WalletKeySlotV1::new(
material.slot_id,
crate::WalletKeySlotRoleV1::View,
material.kdf,
crate::WalletKeyWrapV1::new(material.wrap_nonce, wrapped),
)));
}
fn provisional_envelope(
owner_auth_public_key: [u8; crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES],
owner_slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
owner_kdf: crate::WalletKdfParametersV1,
owner_wrap_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
view_material: std::option::Option<&ViewCreationMaterialV1>,
owner_control_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
metadata_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
secret_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
) -> crate::KspWalletEnvelopeV1 {
let owner_slot = crate::WalletKeySlotV1::new(
owner_slot_id,
crate::WalletKeySlotRoleV1::Owner,
owner_kdf,
crate::WalletKeyWrapV1::new(owner_wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
);
let (view_descriptor, view_slot) = match view_material {
std::option::Option::Some(material) => (
crate::WalletViewDescriptorV1::enabled(material.slot_id),
std::option::Option::Some(crate::WalletKeySlotV1::new(
material.slot_id,
crate::WalletKeySlotRoleV1::View,
material.kdf.clone(),
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
)),
),
std::option::Option::None => (crate::WalletViewDescriptorV1::disabled(), std::option::Option::None),
};
return crate::KspWalletEnvelopeV1::new_internal(
owner_auth_public_key,
view_descriptor,
owner_slot,
view_slot,
crate::WalletEncryptedCompartmentV1::new(
crate::WalletCompartmentKindV1::OwnerControl,
owner_control_nonce,
std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES],
),
crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Metadata, metadata_nonce, std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Secret, secret_nonce, std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
crate::WalletStateSignatureV1::new([0_u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]),
);
}
async fn derive_owner_password_key_async(
password: crate::OwnerPassword,
kdf: crate::WalletKdfParametersV1,
) -> ksp_core_lib::Result<crate::crypto::SecretKeyV1> {
let task = tokio::task::spawn_blocking(move || return crate::crypto::derive_password_key(password.as_bytes(), &kdf));
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()),
};
}
async fn derive_view_password_key_async(password: crate::ViewPassword, kdf: crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::crypto::SecretKeyV1> {
let task = tokio::task::spawn_blocking(move || return crate::crypto::derive_password_key(password.as_bytes(), &kdf));
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()),
};
}
fn pubkey_from_keypair_bytes(bytes: &[u8]) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
let public_slice = match bytes.get(32..64) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(key_material_error()),
};
let public_array = match <[u8; 32]>::try_from(public_slice) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(key_material_error()),
};
return std::result::Result::Ok(ksp_core_lib::Pubkey::new_from_array(public_array));
}
fn cleanup_create_error<T>(
error: ksp_core_lib::Error,
owner_control_plaintext: &mut [u8],
metadata_plaintext: &mut [u8],
solana_keypair_bytes: &mut [u8],
) -> ksp_core_lib::Result<T> {
owner_control_plaintext.zeroize();
metadata_plaintext.zeroize();
solana_keypair_bytes.zeroize();
return std::result::Result::Err(error);
}
fn authentication_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_AUTHENTICATION_FAILED, "Wallet OWNER-authenticated state verification failed");
}
fn crypto_operation_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_CRYPTO_OPERATION_FAILED, "Wallet blocking cryptographic operation could not complete");
}
fn view_unlock_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_VIEW_UNLOCK_FAILED, "Wallet VIEW capability could not be unlocked");
}
fn owner_unlock_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_OWNER_UNLOCK_FAILED, "Wallet OWNER capability could not be unlocked");
}
fn capability_error(message: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_CAPABILITY_INSUFFICIENT, message);
}
fn key_material_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_KEY_MATERIAL_INVALID, "Wallet Solana key material is invalid or inconsistent");
}
#[cfg(test)]
#[path = "../unit_tests/wallet.rs"]
mod tests;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

487
deltas/0.2.5/pre.005.md Normal file
View File

@@ -0,0 +1,487 @@
<!-- file: deltas/0.2.5/pre.005.md -->
<!-- version: 1 -->
# Delta `0.2.5-pre.005` — payloads V1 + create/open VIEW/OWNER + autorité OWNER Ed25519
## Base requise
```text
livraison : 0.2.5-pre.004-fix.002
workspace.package.version = "0.2.5-pre.4.fix.2"
```
Les validations opérateur de cette base sont propres :
```text
cargo fmt --all OK
cargo check --workspace OK
cargo clippy --workspace --all-targets OK
cargo test -p ksp-wallet-lib OK : 22 passés, 1 benchmark ignoré
+ 2 dependency-boundary
+ 5 public API
+ 2 doc-tests compile_fail
cargo test -p ksp-logging-lib --test ownership OK : 2/2
cargo test --workspace OK
```
Le benchmark Argon2 opérateur acquis avant cette tranche donne :
```text
64 MiB / 3 passes / 1 lane -> 1742 ms
128 MiB / 3 passes / 1 lane -> 3459 ms
256 MiB / 3 passes / 1 lane -> 6925 ms
```
## Objectif
Matérialiser les compartiments protégés et les capabilities in-memory complètes de lecture sans ouvrir encore la persistence filesystem ni l'administration :
```text
profil de création KSP V1 issu du benchmark
payload owner_control exact
payload metadata exact
payload secret Solana exact
autorité de format Ed25519 distincte de la keypair Solana
state_signature OWNER vérifiée avant KDF
create V1 in-memory
open VIEW indépendant
open OWNER indépendant
inspection locked authentifiée
vecteur .kspwallet complet interopérable
```
Restent explicitement hors tranche :
```text
filesystem / persistence atomique / no-clobber
signature Solana publique
export du secret
modification alias/notes
rotation password VIEW par VIEW
rotation password VIEW/OWNER par OWNER
disable/recreate VIEW
révocation VIEW forte
import/export
```
Ces opérations restent prévues dans `pre.006+` sans déplacer de responsabilité vers Config ou Transport.
## Version Cargo
Conformément au séquencement des prereleases :
```text
0.2.5-pre.4.fix.2 -> 0.2.5-pre.5
```
`workspace.package.version` reste le signal technique de la tranche.
## Profil Argon2 de création KSP
À partir des mesures opérateur, les créations KSP V1 utilisent initialement :
```text
algorithm Argon2id
version 19
memory_kib 65_536
iterations 3
parallelism 1
salt 32 octets CSPRNG indépendants par slot
output KEK 32 octets
```
Le choix vise un coût d'environ 1,7 s sur la machine de calibration plutôt que les profils 128/256 MiB mesurés à environ 3,5/6,9 s.
Ce profil est un **default de création KSP**, jamais une condition de lecture du format : chaque slot sérialise toujours ses paramètres KDF complets. Un ancien wallet reste donc lisible après un futur durcissement des defaults.
## Dépendances introduites
Nouvelles dépendances communes sous `[workspace.dependencies]` :
```toml
ed25519-dalek = { version = "^2.2", default-features = false }
solana-keypair = { version = "^3.1", default-features = false }
```
Le membre Wallet consomme :
```toml
ed25519-dalek = { workspace = true, features = ["signature", "zeroize"] }
solana-keypair.workspace = true
tokio = { workspace = true, features = ["rt"] }
```
`tokio` était déjà une dépendance commune du workspace ; seule la feature locale nécessaire à `spawn_blocking` est activée.
Le choix direct `ed25519-dalek ^2.2` est volontaire : `solana-keypair 3.1.2` dépend lui-même de `ed25519-dalek ^2.1.1`. Rester dans la génération 2.x doit permettre à Cargo d'unifier Dalek au lieu d'ajouter une branche 3.x concurrente. La feature `zeroize` est activée explicitement.
Aucune dépendance directe `solana-pubkey`, `solana-signer` ou `solana-signature` n'est ajoutée. La Pubkey du domaine Wallet reste exclusivement `ksp_core_lib::Pubkey`.
Un `cargo tree` opérateur est obligatoire pour confirmer la résolution réelle après application.
## Content keys et autorité OWNER
La création génère indépendamment :
```text
K_owner_root 32 octets
K_metadata 32 octets
K_secret 32 octets
admin seed 32 octets Ed25519
Solana secret 32 octets Ed25519 distincts
```
La clé d'administration du format reste distincte de la keypair Solana.
Le slot OWNER wrappe uniquement `K_owner_root`.
Le slot VIEW optionnel wrappe uniquement `K_metadata`.
OWNER ne dépend donc jamais de VIEW et VIEW ne reçoit jamais `K_owner_root` ni `K_secret`.
## Payload `owner_control`
Le plaintext V1 est fixé à exactement 96 octets :
```text
offset 0..32 admin Ed25519 secret seed
offset 32..64 K_metadata
offset 64..96 K_secret
```
Il est chiffré sous `K_owner_root` avec l'AAD compartment figé en `pre.003`.
À l'ouverture OWNER, la seed admin redérive obligatoirement la `owner_auth_public_key` de l'enveloppe. Un mismatch est rejeté.
## Payload metadata
Le plaintext metadata est un JSON UTF-8 strict et protégé :
```json
{
"pubkey": "<canonical Solana Pubkey Base58>",
"alias": null,
"notes": [
{ "id": "<16 bytes Base64url-no-pad>", "text": "..." }
]
}
```
Invariants :
```text
Pubkey type ksp_core_lib::Pubkey après parsing
alias optionnel, <= 256 octets UTF-8
notes maximum 64
note.id exactement 16 octets, unique dans le payload
note.text <= 8192 octets UTF-8
payload metadata <= 65 536 octets
```
Les IDs de note sont générés au CSPRNG. Une collision pendant la création est traitée comme une défaillance de génération plutôt que de produire des IDs ambigus. Un payload décodé avec IDs dupliqués est rejeté.
## Payload secret Solana
Le plaintext secret est exactement le keypair Ed25519 Solana 64 octets :
```text
offset 0..32 secret Ed25519
offset 32..64 public Ed25519
```
À l'ouverture OWNER :
```text
1. le payload doit faire exactement 64 octets ;
2. solana-keypair valide la cohérence secret/public ;
3. les 32 octets publics construisent un ksp_core_lib::Pubkey ;
4. cette Pubkey doit correspondre exactement à la Pubkey metadata.
```
Un mismatch n'est jamais accepté comme changement d'identité implicite.
## `state_signature` OWNER
La signature d'état V1 est désormais effective :
```text
algorithm Ed25519
public key owner_auth_public_key dans l'enveloppe
message state_transcript TLV figé en pre.003
signature 64 octets
```
La vérification stricte de cette signature intervient **avant toute dérivation Argon2** lors de `inspect`, `open VIEW` et `open OWNER`.
Le transcript continue d'exclure le matériau self-service rotatable du slot VIEW (`KDF/salt/wrap`) et d'authentifier son descripteur stable OWNER-controlled. Cette frontière préserve la future rotation du propre password VIEW sans donner à VIEW la capacité de modifier metadata/secret/OWNER-state.
## API in-memory
La crate expose désormais :
```rust
create_wallet_v1(owner_password, optional_view_password, metadata).await
open_wallet_view_v1(source, view_password).await
open_wallet_owner_v1(source, owner_password).await
inspect_locked_wallet_v1(source)
```
`create_wallet_v1` crée une nouvelle identité Solana et une autorité admin séparée, mais **n'effectue aucun I/O filesystem**.
`WalletOwner::to_json_bytes()` et `WalletView::to_json_bytes()` permettent seulement de projeter l'enveloppe verrouillée complète en mémoire en attendant `pre.006`.
### VIEW
L'ouverture VIEW :
```text
parse strict
verify state_signature OWNER
Argon2 du seul slot VIEW via spawn_blocking
unwrap K_metadata
decrypt metadata
retour WalletView
```
Elle ne déchiffre jamais `owner_control` ou `secret`, ne construit jamais la keypair Solana et ne matérialise jamais `K_secret`.
### OWNER
L'ouverture OWNER :
```text
parse strict
verify state_signature OWNER
Argon2 du slot OWNER via spawn_blocking
unwrap K_owner_root
decrypt owner_control
valider admin secret -> owner_auth_public_key
récupérer K_metadata / K_secret
decrypt metadata
decrypt et valider keypair Solana 64 octets
vérifier Pubkey keypair == Pubkey metadata
retour WalletOwner
```
OWNER reste fonctionnel même si VIEW est absent, inconnu ou inutilisable.
## Secret memory
Les invariants précédents sont conservés :
```text
password wrappers non-Clone et zeroized au Drop
content keys non-Copy/non-Clone et zeroized au Drop
buffers plaintext owner-control/metadata/secret zeroized après consommation raisonnable
admin SigningKey avec feature zeroize
solana-keypair conservé uniquement dans l'état OWNER opaque
aucun Debug/Display public de secret
aucun secret/password/alias/note dans les logs
```
Les limites habituelles de zeroization Rust restent documentées : aucune garantie absolue n'est revendiquée contre toutes les copies temporaires possibles du compilateur/runtime.
## Logging
Les nouveaux flux utilisent exclusivement `ksp-logging-lib` avec :
```text
target = crate::TRACING_TARGET = "ksp-wallet-lib"
```
Les événements ne contiennent que l'opération/capability/version non secrète et l'issue catégorisée. Aucun path, password, secret, metadata ou payload arbitraire n'est journalisé.
## Vecteur complet d'interopérabilité
Nouveaux fixtures publics TEST ONLY :
```text
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector.json
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector_meta.json
```
Ils fixent de manière auto-contenue :
```text
password OWNER connu
password VIEW connu
salts / slot IDs / nonces
content keys test-only
admin key test-only
keypair Solana test-only
Pubkey / alias / notes attendus
ciphertexts owner-control / metadata / secret
wrapped keys OWNER / VIEW
state transcript
state signature Ed25519
```
Le vecteur utilise volontairement un KDF faible/test-fast (`32 KiB / 2 / 1`) et ne constitue jamais un wallet ni un profil de production recommandé.
Une implémentation indépendante du code Rust a revérifié :
```text
state transcript exact : 1182 octets
signature Ed25519 valide
dérivations OWNER/VIEW exactes
unwrap K_owner_root / K_metadata exact
déchiffrement owner_control exact
déchiffrement metadata exact
déchiffrement secret exact : 64 octets
admin seed -> owner_auth_public_key exacte
secret Solana -> public half cohérente
```
## Tests
Les tests Wallet ajoutent notamment :
```text
vecteur complet ouvre VIEW et OWNER indépendamment
password VIEW ne déverrouille pas OWNER
password OWNER ne déverrouille pas VIEW
mutation OWNER-authentifiée metadata rejetée
mutation du seul wrapping VIEW ne casse pas OWNER mais invalide VIEW
fichier verrouillé ne contient pas Pubkey/alias/notes en clair
salts / slot IDs / nonces OWNER/VIEW indépendants
create utilise le profil KSP benchmarké
projection locked reste privée
duplicate note IDs rejetés
```
Les tests d'architecture continuent d'imposer :
```text
Wallet -X-> Config
Wallet -X-> Transport
Wallet -X-> ExecutionPolicy
Wallet -X-> Store
Wallet -X-> Tauri
Wallet -X-> tracing direct
Wallet -X-> solana-pubkey direct
Wallet -X-> accès environnement direct
```
## Documentation
Mises à jour :
```text
ROADMAP.md
docs/000-README.md
docs/formats/000-README.md
docs/formats/KSPWALLET_V1.md
docs/plans/000-README.md
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md
```
`KSPWALLET_V1.md` devient suffisamment précis pour les payloads et procédures `pre.005`; la persistence et les opérations d'administration restent explicitement futures.
`CHANGELOG.md` reste inchangé : la policy KSP réserve les entrées de release stable à la clôture/publication, les prereleases étant décrites par plans/deltas/roadmap.
## Fichiers ajoutés
```text
crates/ksp-wallet-lib/src/payload.rs
crates/ksp-wallet-lib/src/wallet.rs
crates/ksp-wallet-lib/unit_tests/payload.rs
crates/ksp-wallet-lib/unit_tests/wallet.rs
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector.json
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector_meta.json
deltas/0.2.5/pre.005.md
```
## Fichiers modifiés
```text
Cargo.toml
ROADMAP.md
crates/ksp-wallet-lib/Cargo.toml
crates/ksp-wallet-lib/src/constants.rs
crates/ksp-wallet-lib/src/crypto.rs
crates/ksp-wallet-lib/src/error.rs
crates/ksp-wallet-lib/src/lib.rs
crates/ksp-wallet-lib/src/metadata.rs
crates/ksp-wallet-lib/src/owner.rs
crates/ksp-wallet-lib/src/password.rs
crates/ksp-wallet-lib/src/view.rs
crates/ksp-wallet-lib/src/wire.rs
crates/ksp-wallet-lib/tests/dependency_boundary.rs
crates/ksp-wallet-lib/tests/public_api.rs
docs/000-README.md
docs/formats/000-README.md
docs/formats/KSPWALLET_V1.md
docs/plans/000-README.md
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md
```
Aucun fichier hors de cette différence avec `pre.004-fix.002` ne doit être embarqué.
## Fichiers supprimés
Aucun.
## Validation attendue après application
```bash
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-wallet-lib
cargo test --workspace
```
Audit de dépendances obligatoire :
```bash
cargo tree -p ksp-wallet-lib
cargo tree -p ksp-wallet-lib -d
cargo tree -e features -p ksp-wallet-lib
cargo tree -i ed25519-dalek@2.2.0
cargo tree -i solana-keypair@3.1.2
cargo tree -i solana-address@2.7.0
```
Points à confirmer :
```text
une seule génération ed25519-dalek dans le sous-arbre Wallet
pas de duplication crypto injustifiée introduite par Wallet
solana-address résolu sans second type Pubkey KSP
aucune dépendance Config/Transport/Tauri depuis Wallet
zeroize effectivement activé sur ed25519-dalek
```
## Décisions
- retenir `65 536 KiB / 3 / 1` comme profil initial de création KSP V1 à partir du benchmark opérateur ;
- conserver les paramètres KDF sérialisés comme autorité de lecture du wallet ;
- utiliser une autorité Ed25519 de format distincte de la keypair Solana ;
- vérifier la state signature avant tout KDF ;
- garder `ed25519-dalek` en génération `2.x` pour s'aligner sur `solana-keypair 3.1.2` ;
- ne jamais dépendre directement de `solana-pubkey` dans Wallet ;
- ouvrir VIEW sans matérialiser le secret Solana ;
- garder OWNER indépendant de VIEW ;
- ne pas introduire persistence/signature/export/admin avant leurs tranches dédiées.
## Questions ouvertes
Aucune question de format V1 nouvelle n'est introduite par cette tranche.
`pre.006` doit choisir et prouver la stratégie filesystem portable pour :
```text
create destination nouvelle/no-clobber
atomic replace des futures mutations OWNER/VIEW
fsync/durability raisonnable selon plateforme
fault injection
concurrence
récupération après interruption
```
## Commit attendu
```text
v0.2.5-pre.005
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/000-README.md -->
<!-- version: 35 -->
<!-- version: 36 -->
# Documentation KSP
@@ -73,11 +73,11 @@ D'autres sous-répertoires seront ajoutés uniquement lorsque leur rôle aura é
## Documents de planification
Le plan historique de la phase fondatrice clôturée est conservé dans [`plans/001-V0_0_3_PLAN.md`](plans/001-V0_0_3_PLAN.md). La séquence active des premières releases fonctionnelles est définie dans [`plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md`](plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md). Le plan détaillé de la release stable `0.1.1` est conservé comme historique clôturé dans [`plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md`](plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.2` est conservé comme historique clôturé dans [`plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.3 — Configuration foundation` est conservé comme historique clôturé dans [`plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.4 — ksp-app-config-desk` est conservé comme historique clôturé dans [`plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md`](plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md), avec sa matrice finale [`validation/001-V0_1_4_CONFIG_DESKTOP.md`](validation/001-V0_1_4_CONFIG_DESKTOP.md). Son prompt d'ouverture historique reste [`../prompts/004-V0_1_4_START_PROMPT.md`](../prompts/004-V0_1_4_START_PROMPT.md). La release stable `0.2.0` clôt l'audit de bot3 et le découpage de la série. Son plan directeur est conservé comme historique clôturé dans [`plans/007-V0_2_0_SERIES_PLANNING.md`](plans/007-V0_2_0_SERIES_PLANNING.md), avec sa matrice finale [`validation/002-V0_2_0_SERIES_PLANNING.md`](validation/002-V0_2_0_SERIES_PLANNING.md). La release stable `0.2.1 — HTTP Solana foundation` a été ouverte par [`../prompts/006-V0_2_1_START_PROMPT.md`](../prompts/006-V0_2_1_START_PROMPT.md). Son gate de sizing et sa matrice exhaustive sont conservés dans [`plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md), avec la validation finale [`validation/003-V0_2_1_ONCHAIN_HTTP.md`](validation/003-V0_2_1_ONCHAIN_HTTP.md), README/USAGE Transport et le smoke Devnet opt-in de composition Config -> Transport. Le prompt [`../prompts/007-V0_2_2_START_PROMPT.md`](../prompts/007-V0_2_2_START_PROMPT.md) a ouvert la release stable `0.2.2 — HTTP Accounts + Tokens + Cluster`. Son plan clôturé [`plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) conserve l'audit et l'implémentation des 22 wrappers typés, tandis que [`validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) enregistre les validations déterministes, les graphes Cargo et les deux smokes Devnet passés avant publication. Le prompt [`../prompts/008-V0_2_3_START_PROMPT.md`](../prompts/008-V0_2_3_START_PROMPT.md) a ouvert la release stable `0.2.3 — HTTP Transactions`. Son plan clôturé [`plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) conserve l'audit et l'implémentation des 11 wrappers ; le réaudit [`validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md`](validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md) confirme la complétude des 37 wrappers HTTP typés et [`validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](validation/006-V0_2_3_HTTP_TRANSACTIONS.md) enregistre les validations finales, graphes Cargo et deux smokes Devnet passés avant publication. Le prompt [`../prompts/009-V0_2_4_START_PROMPT.md`](../prompts/009-V0_2_4_START_PROMPT.md) a ouvert la release stable `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale`. Son plan clôturé [`plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) conserve limplémentation des 15 wrappers et la compliance `52/52 + 14/14`; la matrice finale [`validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md`](validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md) enregistre le réaudit SIMD/inventaire, les canaries globales et les preuves opérateur avant publication. Le prompt [`../prompts/010-V0_2_5_START_PROMPT.md`](../prompts/010-V0_2_5_START_PROMPT.md), finalisé par `0.2.4-pre.009-fix.001`, ouvre `0.2.5 — Wallet foundation` sur la base stable `v0.2.4`. Son gate `0.2.5-pre.001` est conservé dans [`plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md) : il réaudite bot2/bot3 et les crates actuelles, formalise le threat model offline, retient les capacités VIEW/OWNER indépendantes et le niveau B read-only, cadre le format interopérable `.kspwallet` V1 et redimensionne la release avant implémentation cryptographique.
Le plan historique de la phase fondatrice clôturée est conservé dans [`plans/001-V0_0_3_PLAN.md`](plans/001-V0_0_3_PLAN.md). La séquence active des premières releases fonctionnelles est définie dans [`plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md`](plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md). Le plan détaillé de la release stable `0.1.1` est conservé comme historique clôturé dans [`plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md`](plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.2` est conservé comme historique clôturé dans [`plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.3 — Configuration foundation` est conservé comme historique clôturé dans [`plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.4 — ksp-app-config-desk` est conservé comme historique clôturé dans [`plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md`](plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md), avec sa matrice finale [`validation/001-V0_1_4_CONFIG_DESKTOP.md`](validation/001-V0_1_4_CONFIG_DESKTOP.md). Son prompt d'ouverture historique reste [`../prompts/004-V0_1_4_START_PROMPT.md`](../prompts/004-V0_1_4_START_PROMPT.md). La release stable `0.2.0` clôt l'audit de bot3 et le découpage de la série. Son plan directeur est conservé comme historique clôturé dans [`plans/007-V0_2_0_SERIES_PLANNING.md`](plans/007-V0_2_0_SERIES_PLANNING.md), avec sa matrice finale [`validation/002-V0_2_0_SERIES_PLANNING.md`](validation/002-V0_2_0_SERIES_PLANNING.md). La release stable `0.2.1 — HTTP Solana foundation` a été ouverte par [`../prompts/006-V0_2_1_START_PROMPT.md`](../prompts/006-V0_2_1_START_PROMPT.md). Son gate de sizing et sa matrice exhaustive sont conservés dans [`plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md), avec la validation finale [`validation/003-V0_2_1_ONCHAIN_HTTP.md`](validation/003-V0_2_1_ONCHAIN_HTTP.md), README/USAGE Transport et le smoke Devnet opt-in de composition Config -> Transport. Le prompt [`../prompts/007-V0_2_2_START_PROMPT.md`](../prompts/007-V0_2_2_START_PROMPT.md) a ouvert la release stable `0.2.2 — HTTP Accounts + Tokens + Cluster`. Son plan clôturé [`plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) conserve l'audit et l'implémentation des 22 wrappers typés, tandis que [`validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) enregistre les validations déterministes, les graphes Cargo et les deux smokes Devnet passés avant publication. Le prompt [`../prompts/008-V0_2_3_START_PROMPT.md`](../prompts/008-V0_2_3_START_PROMPT.md) a ouvert la release stable `0.2.3 — HTTP Transactions`. Son plan clôturé [`plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) conserve l'audit et l'implémentation des 11 wrappers ; le réaudit [`validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md`](validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md) confirme la complétude des 37 wrappers HTTP typés et [`validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](validation/006-V0_2_3_HTTP_TRANSACTIONS.md) enregistre les validations finales, graphes Cargo et deux smokes Devnet passés avant publication. Le prompt [`../prompts/009-V0_2_4_START_PROMPT.md`](../prompts/009-V0_2_4_START_PROMPT.md) a ouvert la release stable `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale`. Son plan clôturé [`plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) conserve limplémentation des 15 wrappers et la compliance `52/52 + 14/14`; la matrice finale [`validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md`](validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md) enregistre le réaudit SIMD/inventaire, les canaries globales et les preuves opérateur avant publication. Le prompt [`../prompts/010-V0_2_5_START_PROMPT.md`](../prompts/010-V0_2_5_START_PROMPT.md), finalisé par `0.2.4-pre.009-fix.001`, ouvre `0.2.5 — Wallet foundation` sur la base stable `v0.2.4`. Son plan actif [`plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md) part du gate `pre.001` (héritage, threat model offline, VIEW/OWNER indépendants et niveau B read-only), puis matérialise la crate en `pre.002`, le wire/transcript en `pre.003`, les primitives Argon2id/XChaCha20-Poly1305 en `pre.004` et, en `pre.005`, les payloads protégés, le profil de création benchmarké, lautorité Ed25519 séparée ainsi que les flux in-memory create/open VIEW/OWNER avec vecteur complet interopérable. `pre.006` porte la persistence atomique/no-clobber.
## Spécifications de formats
Les formats durables, interopérables et destinés à être réimplémentables hors de KSP sont indexés depuis [`formats/000-README.md`](formats/000-README.md). Le premier format natif publié dans cette famille est [`.kspwallet` V1](formats/KSPWALLET_V1.md), dont `0.2.5-pre.003` fige l'enveloppe JSON stricte, les key slots, les limites structurelles, le transcript OWNER et les AAD indépendamment de l'implémentation Rust.
Les formats durables, interopérables et destinés à être réimplémentables hors de KSP sont indexés depuis [`formats/000-README.md`](formats/000-README.md). Le premier format natif publié dans cette famille est [`.kspwallet` V1](formats/KSPWALLET_V1.md) : `pre.003` en fige le wire/transcript/AAD, `pre.004` les primitives KDF/AEAD et `pre.005` les payloads plaintext, lautorité Ed25519 OWNER, les procédures create/open VIEW/OWNER, le profil de création KSP issu du benchmark et un vecteur complet interopérable indépendant du code Rust.
`IDEAS.md` conserve les pistes et questions qui ne sont pas encore des engagements du roadmap ni des décisions architecturales.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/formats/000-README.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# Formats KSP
@@ -9,4 +9,4 @@ Une spécification de format décrit le wire exact, les encodages, les limites,
## Formats actifs
- [`KSPWALLET_V1.md`](KSPWALLET_V1.md) — spécification du format natif autonome `.kspwallet` V1. `0.2.5-pre.003` fige son enveloppe JSON stricte, ses limites structurelles, ses key slots, son transcript OWNER et ses AAD ; `0.2.5-pre.004` ajoute Argon2id/XChaCha20-Poly1305/CSPRNG OS, le wrapping de content keys et le premier vecteur crypto déterministe. Le default Argon2 de création reste volontairement en attente du benchmark opérateur livré par cette tranche.
- [`KSPWALLET_V1.md`](KSPWALLET_V1.md) — spécification du format natif autonome `.kspwallet` V1. `0.2.5-pre.003` fige l'enveloppe/wire et les transcripts/AAD, `pre.004` ajoute Argon2id/XChaCha20-Poly1305/CSPRNG OS et `pre.005` fixe les payloads plaintext, le profil de création KSP calibré, l'autorité Ed25519 OWNER, les procédures create/open VIEW/OWNER et un vecteur complet interopérable test-only.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/formats/KSPWALLET_V1.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# `.kspwallet` V1 — spécification du format natif Wallet KSP
@@ -20,7 +20,7 @@ AAD des compartiments owner-control / metadata / secret
règles unknown-field / unknown-version
```
`0.2.5-pre.004` complète maintenant les primitives KDF/AEAD normatives et un premier vecteur cryptographique public. Le **default de création Argon2id** reste volontairement non normatif tant que le benchmark opérateur de `pre.004` n'a pas été exécuté. Les prereleases suivantes complètent les payloads plaintext exacts, l'autorité Ed25519, les opérations create/open et les vecteurs de wallet complets. Toute évolution qui modifie un élément déjà déclaré **figé** par cette spécification exige une évolution explicitement tracée avant la release stable ; après publication de V1, une incompatibilité de wire exige un nouveau `format_version`.
`0.2.5-pre.004` ajoute les primitives KDF/AEAD normatives et un premier vecteur cryptographique public. `0.2.5-pre.005` fixe maintenant les payloads plaintext V1, l'autorité Ed25519 OWNER, les procédures de création et d'ouverture VIEW/OWNER, le profil de création KSP issu du benchmark opérateur et un vecteur `.kspwallet` complet généré indépendamment du code Rust. La persistence filesystem, les mutations administratives, la signature Solana publique et les adapters import/export restent dans les tranches suivantes. Toute évolution qui modifie un élément déjà déclaré **figé** par cette spécification exige une évolution explicitement tracée avant la release stable ; après publication de V1, une incompatibilité de wire exige un nouveau `format_version`.
Le but final est qu'une implémentation indépendante en Rust, Python, Go, C/C++, Java ou autre puisse créer, parser, vérifier et ouvrir un `.kspwallet` sans lire le code source de `ksp-wallet-lib`.
@@ -166,7 +166,7 @@ La forme V1 est :
}
```
Les valeurs Argon2 chiffrées dans cet exemple sont **des valeurs de fixture structurelle**, pas les defaults de création V1. Les defaults ne deviennent normatifs qu'après benchmark de `pre.004`.
Les valeurs Argon2 montrées dans l'exemple OWNER correspondent au profil de création KSP retenu en `pre.005`. L'exemple VIEW reste seulement illustratif : chaque key slot sérialise ses propres paramètres et toute combinaison respectant les bornes V1 reste lisible. Le profil par défaut KSP ne constitue donc pas une contrainte imposée aux implémentations externes conformes.
## 6. `owner_auth_public_key`
@@ -223,9 +223,20 @@ memory_kib >= 8 * parallelism
salt : 16 .. 64 octets
```
Ces plafonds sont des bornes de format/rejet hostile ; ils ne définissent pas les paramètres de **création par défaut**. Ceux-ci sont benchmarkés séparément.
Ces plafonds sont des bornes de format/rejet hostile ; ils ne définissent pas à eux seuls les paramètres de création.
Le password KDF futur est la séquence exacte des octets UTF-8 fournis, sans normalisation Unicode implicite, avec une longueur maximale de 1024 octets et un password vide refusé à la création.
Le profil de création KSP V1 retenu en `0.2.5-pre.005` est :
```text
memory_kib = 65 536
iterations = 3
parallelism = 1
salt = 32 octets CSPRNG neufs par slot
```
Le benchmark opérateur du 2026-08-19, exécuté avec le test calibrateur livré par `pre.004`, a mesuré environ 1742 ms pour `64 MiB / 3 / 1`, 3459 ms pour `128 MiB / 3 / 1` et 6925 ms pour `256 MiB / 3 / 1`. Le premier candidat est retenu comme équilibre initial ; ces mesures caractérisent la machine/profil testés et ne sont pas une promesse de latence portable. Les paramètres étant sérialisés dans chaque slot, KSP pourra durcir les defaults futurs sans rendre les wallets existants illisibles.
Le password KDF est la séquence exacte des octets UTF-8 fournis, sans normalisation Unicode implicite, avec une longueur maximale de 1024 octets et un password vide refusé.
### 8.2 Wrapping
@@ -240,7 +251,14 @@ ciphertext <= 4096 octets
Le ciphertext contient le tag Poly1305 de 16 octets produit par l'AEAD.
Le contenu plaintext exact des wrapped capabilities est figé avec la couche crypto/payload suivante ; la grammaire envelope/key-slot et son AAD sont déjà figés ici.
Le plaintext wrappé est exactement **32 octets** :
```text
OWNER slot -> K_owner_root (32 octets)
VIEW slot -> K_metadata (32 octets)
```
OWNER et VIEW ont des KDF/salts/nonces indépendants. OWNER n'a jamais besoin du slot VIEW pour accéder à `K_metadata`, puisque cette clé est également contenue dans `owner_control` sous `K_owner_root`.
## 9. Compartiments chiffrés
@@ -275,19 +293,57 @@ metadata : 16 .. 65 552 octets
secret : 16 .. 4096 octets
```
La metadata plaintext V1 reste bornée à 65 536 octets. Les payloads plaintext exacts sont consolidés dans la tranche dédiée, mais les champs wire ci-dessus ne changent pas.
La metadata plaintext V1 reste bornée à 65 536 octets. `pre.005` fixe les trois plaintexts :
Le compartiment metadata contient à terme au minimum :
### 9.1 `owner_control`
Plaintext binaire de **96 octets exacts**, dans cet ordre :
```text
Pubkey Solana Base58 canonique
alias optionnel <= 256 octets UTF-8
maximum 64 notes
texte note <= 8192 octets UTF-8
id de note = 16 octets aléatoires
offset 0..32 : admin_signing_secret Ed25519 (32 octets)
offset 32..64 : K_metadata (32 octets)
offset 64..96 : K_secret (32 octets)
```
Le compartiment secret contient la keypair Solana exacte nécessaire à la signature OWNER.
`admin_signing_secret` est la seed privée Ed25519 correspondant à `owner_auth_public_key`. Cette autorité est distincte de la keypair Solana.
### 9.2 `metadata`
Plaintext JSON UTF-8, objet strict sans champs inconnus :
```json
{
"pubkey": "<Pubkey Solana Base58 canonique>",
"alias": "<string ou null>",
"notes": [
{"id": "<16 octets Base64url sans padding>", "text": "<UTF-8>"}
]
}
```
Contraintes :
```text
pubkey = Base58 canonique d'une Pubkey Solana 32 octets
alias = null ou <= 256 octets UTF-8
notes = maximum 64
note.id = exactement 16 octets, Base64url canonique sans padding, unique dans le payload
note.text = <= 8192 octets UTF-8
payload total <= 65 536 octets
```
L'ordre des propriétés JSON du plaintext metadata n'est pas cryptographiquement canonicalisé : l'AEAD protège les octets plaintext réellement choisis par le créateur, et la signature OWNER protège ensuite le ciphertext. Le serializer KSP émet un JSON compact dans l'ordre `pubkey`, `alias`, `notes`, puis `id`, `text` pour chaque note.
### 9.3 `secret`
Plaintext binaire de **64 octets exacts**, compatible avec la représentation Solana/Ed25519 standard :
```text
offset 0..32 : secret Ed25519 Solana
offset 32..64 : public Ed25519 Solana
```
À l'ouverture OWNER, l'implémentation doit valider que la moitié publique correspond au secret et que la Pubkey dérivée est exactement celle du payload metadata. Un mismatch est du key material invalide, jamais une nouvelle identité acceptée silencieusement.
## 10. Signature d'état OWNER
@@ -304,6 +360,8 @@ La signature porte sur le **transcript sémantique OWNER-controlled**, jamais su
Les paramètres/salt/nonce/ciphertext du slot VIEW self-service sont exclus de la signature OWNER ; le descripteur stable VIEW est inclus.
La vérification Ed25519 de l'état OWNER-controlled doit précéder toute dérivation de password. `owner_auth_public_key` est parsée comme clé de vérification Ed25519 et la signature est vérifiée en mode strict sur le transcript exact de la section 12. Une mutation de metadata/secret/owner-control/slot OWNER sans clé privée d'administration est donc rejetée avant Argon2. Lors d'une ouverture OWNER, la seed d'administration déchiffrée depuis `owner_control` doit en plus redériver exactement `owner_auth_public_key`.
## 11. Codec binaire transcript/AAD
### 11.1 Préfixe de domaine
@@ -603,19 +661,20 @@ AAD exacts
round-trip du codec
```
Le premier vecteur cryptographique public KDF+wrapping est ajouté par `pre.004`. Les vecteurs de wallet complets incluant payloads et state signature sont ajoutés après implémentation des compartiments et de l'autorité OWNER.
Le premier vecteur cryptographique public KDF+wrapping est ajouté par `pre.004`. `pre.005` ajoute en plus un `.kspwallet` complet cryptographiquement valide et ses métadonnées de contrôle test-only, décrits en section 22.
## 19. Invariants encore à compléter sans modifier le wire figé
Les tranches suivantes doivent compléter :
Après `pre.005`, les tranches restantes portent sur les opérations autour du format déjà défini :
```text
pre.004 : sélection finale du default Argon2 après benchmark opérateur (KDF/AEAD/wrapping/vecteur déjà implémentés)
pre.005 : payloads owner-control/metadata/secret + create/open VIEW/OWNER + state signature effective
pre.006+ : persistence/administration/signature/import-export selon le plan Wallet
pre.006 : persistence async/atomique/no-clobber
pre.007 : signature Solana, metadata admin, rotations OWNER/VIEW et révocation forte VIEW
pre.008 : import/export
pre.009+ : audit adversarial, compliance et documentation de clôture
```
Toute découverte imposant de modifier la grammaire, les tags, l'ordre transcript ou les domain separators définis dans ce document doit être traitée explicitement avant la publication stable, jamais masquée par une tolérance du parseur.
Toute découverte imposant de modifier la grammaire, les payloads plaintext, les tags, l'ordre transcript ou les domain separators définis dans ce document doit être traitée explicitement avant la publication stable, jamais masquée par une tolérance du parseur.
## 20. Primitives cryptographiques effectives depuis `pre.004`
@@ -638,15 +697,15 @@ secret Argon2 externe = aucun
Un password vide est rejeté. V1 limite l'entrée password à 1024 octets UTF-8. Le KDF est une opération CPU/mémoire coûteuse ; les futures API async create/open l'exécuteront hors du thread executor conformément au plan Wallet.
Le default de création n'est **pas** déterminé par les defaults de la crate RustCrypto. Le benchmark opérateur compare explicitement :
Le default de création n'est **pas** déterminé par les defaults de la crate RustCrypto. Le benchmark opérateur a comparé :
```text
64 MiB / 3 passes / 1 lane
128 MiB / 3 passes / 1 lane
256 MiB / 3 passes / 1 lane
64 MiB / 3 passes / 1 lane -> 1742 ms
128 MiB / 3 passes / 1 lane -> 3459 ms
256 MiB / 3 passes / 1 lane -> 6925 ms
```
Le profil retenu sera enregistré après mesure sur machine cible. Un wallet conserve toujours ses propres paramètres sérialisés, indépendamment des defaults futurs.
KSP retient donc initialement `64 MiB / 3 / 1` avec un salt CSPRNG de 32 octets par slot. Un wallet conserve toujours ses propres paramètres sérialisés, indépendamment des defaults futurs.
### 20.2 XChaCha20-Poly1305
@@ -685,3 +744,89 @@ XChaCha20-Poly1305(derived_key, nonce, AAD, content_key) -> wrapped_key attendu
```
Le vecteur a été recalculé indépendamment de l'implémentation Rust avec Argon2id puis la construction XChaCha20-Poly1305 `HChaCha20 + ChaCha20-Poly1305 IETF`. Ces valeurs ne constituent jamais des secrets de production.
## 21. Procédures V1 matérialisées par `pre.005`
### 21.1 Création
Une création KSP V1 en mémoire suit conceptuellement :
```text
1. générer K_owner_root, K_metadata et K_secret indépendants (32 octets chacun) ;
2. générer une seed Ed25519 d'administration de format indépendante de la keypair Solana ;
3. générer une nouvelle keypair Solana Ed25519 et dériver la Pubkey metadata ;
4. encoder owner_control, metadata et secret selon la section 9 ;
5. générer slot_id/salt/nonce OWNER et, si demandé, slot_id/salt/nonce VIEW indépendants ;
6. Argon2id(password OWNER) -> KEK_OWNER -> wrap K_owner_root ;
7. si VIEW existe : Argon2id(password VIEW) -> KEK_VIEW -> wrap K_metadata ;
8. chiffrer owner_control avec K_owner_root, metadata avec K_metadata, secret avec K_secret ;
9. construire le transcript OWNER et le signer avec l'autorité Ed25519 de format ;
10. vérifier immédiatement la signature produite avant de retourner le handle OWNER.
```
Les opérations Argon2id sont exécutées hors du thread executor async par une frontière blocking dédiée. La création `pre.005` est **in-memory** : la persistence arrive en `pre.006`.
### 21.2 Ouverture VIEW
```text
1. parser/rejeter strictement l'enveloppe ;
2. vérifier la state signature OWNER avant tout KDF ;
3. exiger un slot VIEW cohérent avec view_descriptor ;
4. Argon2id(password VIEW, paramètres du slot VIEW) ;
5. unwrap K_metadata avec l'AAD VIEW courant ;
6. déchiffrer uniquement metadata ;
7. valider le payload metadata ;
8. retourner Pubkey/alias/notes + capability VIEW.
```
Une ouverture VIEW ne déchiffre **jamais** `owner_control` ni `secret` et ne matérialise ni `K_owner_root`, ni `K_secret`, ni la keypair Solana, ni la seed Ed25519 d'administration.
### 21.3 Ouverture OWNER
```text
1. parser/rejeter strictement l'enveloppe ;
2. vérifier la state signature OWNER avant tout KDF ;
3. Argon2id(password OWNER, paramètres slot OWNER) ;
4. unwrap K_owner_root ;
5. déchiffrer owner_control et récupérer admin_signing_secret, K_metadata, K_secret ;
6. vérifier que admin_signing_secret redérive owner_auth_public_key ;
7. déchiffrer/valider metadata ;
8. déchiffrer secret, reconstruire strictement la keypair Solana 64 octets ;
9. vérifier cohérence secret/public et égalité avec la Pubkey metadata ;
10. retourner capability OWNER en conservant les secrets seulement dans l'état OWNER opaque.
```
Aucun getter public de secret n'est introduit par cette procédure.
## 22. Vecteur complet interopérable `pre.005`
Le dépôt publie :
```text
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector.json
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector_meta.json
```
Ces fichiers sont **TEST ONLY**. Ils publient volontairement passwords, secrets, clés et résultats attendus et ne doivent jamais servir de wallet réel ni d'exemple de paramètres sécurisés. Pour garder les tests rapides, leurs key slots utilisent `32 KiB / 2 / 1`, très en dessous du profil de création KSP.
Le vector fixe notamment :
```text
password OWNER = pre005-owner-password
password VIEW = pre005-view-password
Pubkey attendue = 8zH45w576QJUEGtpXqZvEi6UPddmMfKopLatocZGDw6
alias attendu = pre005-vector-wallet
2 notes test-only
owner/view derived keys
autorité Ed25519
K_owner_root / K_metadata / K_secret
owner-control plaintext
metadata plaintext
keypair Solana 64 octets
state transcript exact
state signature exacte
wallet JSON complet
```
Le vecteur a été généré et revérifié indépendamment du code Rust avec Argon2id, XChaCha20-Poly1305 construit via HChaCha20 + ChaCha20-Poly1305 IETF et Ed25519. Une implémentation externe conforme doit pouvoir reproduire les mêmes dérivations, déchiffrements et vérifications à partir des deux fichiers sans dépendre d'un type Rust KSP.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/000-README.md -->
<!-- version: 42 -->
<!-- version: 43 -->
# Plans KSP
@@ -20,7 +20,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
- [`009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) — plan clôturé de la release stable `0.2.2`, établi par `pre.001`, corrigé après réaudit Agave v4.2.1 puis exécuté jusqu'à `pre.007-fix.002`; il couvre les 22 wrappers Accounts/Tokens/Cluster, le smoke Transport opt-in et la préparation de `0.2.3`.
- [`010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) — plan historique clôturé de la release stable `0.2.3 — HTTP Transactions`, ouvert par `pre.001`, exécuté jusqu'à `pre.009` puis publié par `rel.001`; il couvre les 11 méthodes, la classification `8 Read / 2 WriteSubmission / 1 Simulation`, `KSP-TRANSPORT-007`, le no-resend et la préparation de `0.2.4`.
- [`011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) — plan historique clôturé de la release stable `0.2.4`, ouvert par `pre.001`, exécuté jusquà `pre.009`, complété par le fix documentaire Wallet `pre.009-fix.001` puis publié par `rel.001`; il couvre les 10 Blocks + 5 Economics et la compliance finale `52/52 + 14/14` sous `KSP-TRANSPORT-007`.
- [`012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan actif de `0.2.5 — Wallet foundation`, ouvert par `pre.001`; `pre.002` matérialise la crate et `pre.003` fige le wire JSON V1, ses key slots, limites, transcript/AAD et la première spécification interopérable `docs/formats/KSPWALLET_V1.md`, avant la cryptographie effective de `pre.004+`.
- [`012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan actif de `0.2.5 — Wallet foundation`, ouvert par `pre.001`; `pre.002` matérialise la crate, `pre.003` fige le wire JSON V1/transcript/AAD, `pre.004` ajoute Argon2id/XChaCha20-Poly1305/CSPRNG et `pre.005` fixe les payloads, le profil de création benchmarké, lautorité Ed25519 séparée et les flux in-memory create/open VIEW/OWNER avec vecteur complet interopérable. `pre.006` porte la persistence atomique/no-clobber.
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
<!-- version: 49 -->
<!-- version: 50 -->
# Séquence des releases fonctionnelles KSP
@@ -414,6 +414,8 @@ La release doit fournir `docs/formats/KSPWALLET_V1.md` comme spécification sép
`0.2.5-pre.004` matérialise les primitives cryptographiques in-memory sans encore créer/ouvrir un wallet complet : Argon2id version 19 dérive une KEK de 32 octets depuis le password et les paramètres sérialisés, XChaCha20-Poly1305 wrappe/déwrappe les content keys avec les AAD figés, `getrandom` fournit le CSPRNG OS et les buffers secrets possédés sont zeroized. Un vecteur public test-only fixe l'interop KDF+AEAD. Le profil Argon2 de création n'est pas inventé : trois candidats sont benchmarkables par un test `#[ignore]` et le gate `pre.005` attend le résultat opérateur avant de figer le default.
`0.2.5-pre.005` consomme ce benchmark (`64 MiB / 3 / 1 ≈ 1742 ms`, `128 MiB ≈ 3459 ms`, `256 MiB ≈ 6925 ms` sur la machine opérateur) et retient pour les créations KSP V1 `65 536 KiB / 3 / 1` avec salt 32 octets, tout en conservant les paramètres sérialisés comme autorité de lecture de chaque wallet. La tranche fixe les payloads `owner_control` (seed Ed25519 dadministration + `K_metadata` + `K_secret`), metadata strictes et secret Solana 64 octets, ajoute une autorité Ed25519 séparée de la keypair Solana et vérifie sa signature détat avant tout KDF. Les API in-memory `create_wallet_v1`, `open_wallet_view_v1`, `open_wallet_owner_v1` et `inspect_locked_wallet_v1` matérialisent lindépendance VIEW/OWNER : VIEW ne déchiffre jamais `owner_control`/secret, OWNER ne dépend pas de VIEW et la Pubkey reste un `ksp_core_lib::Pubkey`. Un vecteur `.kspwallet` complet test-only, revérifiable hors Rust, ferme linterop de cette tranche. La persistence filesystem reste explicitement `pre.006`; signature Solana publique et mutations/rotations restent `pre.007+`.
## `0.2.6` — Wallet Desk
Mission : valider Config composite + `.kspwallet` + transport HTTP dans une application Tauri mince.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# Plan `0.2.5` — Wallet foundation
@@ -363,7 +363,7 @@ Il faut distinguer :
**keypair Solana** : immuable dans V1 après création/import. Aucun `replace_keypair` n'est exposé. Toute tentative de modifier `secret` ou la Pubkey metadata sans une réécriture OWNER valide échoue sur la `state_signature`; même avec OWNER, l'import d'une autre keypair crée un nouveau `.kspwallet` au lieu de muter l'identité cryptographique existante.
## 8. Primitives et dépendances réauditées le 2026-08-18
## 8. Primitives et dépendances réauditées jusquau 2026-08-19
Aucune dépendance ci-dessous n'est ajoutée dans `pre.001`. Ce sont les candidates pour les tranches qui les consommeront réellement.
@@ -378,9 +378,9 @@ Aucune dépendance ci-dessous n'est ajoutée dans `pre.001`. Ce sont les candida
`solana-keypair` fournit le keypair Ed25519, la conversion stricte depuis 64 octets, la signature, le format JSON de 64 entiers et une représentation Base58 complète. Aucun client RPC Solana n'est nécessaire.
La génération SDK récente définit `Pubkey` comme alias du type `Address`, mais l'écosystème a déjà connu des incompatibilités lorsque plusieurs générations de `solana-address` coexistent. `pre.002` verrouille donc la surface publique Wallet sur **`ksp_core_lib::Pubkey` exclusivement** et n'ajoute aucune dépendance directe `solana-pubkey`. Lorsque `solana-keypair` sera réellement introduit, la tranche concernée devra prouver par compilation/cargo-tree que son `Address/Pubkey` est compatible avec la génération possédée par Core avant toute conversion interne. Aucun second type d'adresse KSP n'est introduit pour contourner un mismatch.
La génération SDK récente définit `Pubkey` comme alias du type `Address`, mais l'écosystème a déjà connu des incompatibilités lorsque plusieurs générations de `solana-address` coexistent. `pre.002` verrouille donc la surface publique Wallet sur **`ksp_core_lib::Pubkey` exclusivement** et n'ajoute aucune dépendance directe `solana-pubkey`. `pre.005` introduit réellement `solana-keypair 3.1.2` uniquement pour posséder/valider la keypair Ed25519 ; la Pubkey Wallet reste construite via `ksp_core_lib::Pubkey` depuis les 32 octets publics du keypair, sans exposer un second type d'adresse KSP.
Un `cargo tree` est obligatoire lors de l'introduction réelle afin de contrôler les versions `ed25519-dalek`, `solana-signature`, `rand/getrandom` et éviter des duplications évitables.
Un `cargo tree` est obligatoire avec `pre.005` afin de confirmer l'unification de `solana-address`, `ed25519-dalek`, `rand/getrandom` et d'éviter des duplications crypto injustifiées.
Audit source notable : `solana-keypair 3.1.2` contient un bloc `unsafe` interne dans sa conversion Base58 vers `String`. Cela ne modifie pas la règle `#![forbid(unsafe_code)]` du code KSP, mais doit rester visible dans l'audit des transitifs ; `pre.008` réévaluera le chemin Base58 réellement appelé et les alternatives avant de figer l'adapter.
@@ -392,9 +392,9 @@ Audit source notable : `solana-keypair 3.1.2` contient un bloc `unsafe` interne
| scrypt | `0.12.0` | alternative maintenue, non ajoutée |
| PBKDF2 | `0.13.0` | compatibilité/legacy seulement, non ajouté |
Les paramètres Argon2 de création sont benchmarkés sur les machines cibles. `pre.004` livre trois candidats explicites (64 MiB / 128 MiB / 256 MiB, trois passes, une lane) et un test opérateur ignoré par défaut ; **aucun candidat n'est déclaré default avant retour du benchmark opérateur**. Ils ne sont pas copiés de bot3, d'un RFC ou d'un default de crate. Le fichier sérialise tous les paramètres nécessaires afin qu'un ancien wallet conserve son profil historique.
Les paramètres Argon2 de création ont été mesurés avec le benchmark opérateur de `pre.004` : `64 MiB / 3 / 1 = 1742 ms`, `128 MiB / 3 / 1 = 3459 ms`, `256 MiB / 3 / 1 = 6925 ms` sur la machine/profil testés le 2026-08-19. `pre.005` retient donc **64 MiB / 3 passes / 1 lane** comme profil initial de création KSP, avec un salt CSPRNG indépendant de 32 octets par slot. Ce choix n'est copié ni de bot3, ni d'un RFC, ni d'un default de crate. Le fichier sérialise tous les paramètres nécessaires afin qu'un ancien wallet conserve son profil historique même lorsque les defaults KSP seront durcis.
Le parseur impose des **bornes maximales** avant de lancer le KDF, afin qu'un fichier hostile ne puisse demander arbitrairement mémoire/CPU. `pre.004` ajoute également l'invariant Argon2 `memory_kib >= 8 * parallelism` avant tout calcul coûteux. Les bornes structurelles restent indépendantes du default de création ; celui-ci est figé seulement après le benchmark opérateur de `pre.004`.
Le parseur impose des **bornes maximales** avant de lancer le KDF, afin qu'un fichier hostile ne puisse demander arbitrairement mémoire/CPU. `pre.004` ajoute également l'invariant Argon2 `memory_kib >= 8 * parallelism` avant tout calcul coûteux. Les bornes structurelles restent indépendantes du profil de création KSP.
### 8.3 AEAD
@@ -409,13 +409,13 @@ AES-GCM-SIV apporte une meilleure tolérance à la réutilisation accidentelle d
### 8.4 CSPRNG
`getrandom 0.4.3` est retenu comme candidate low-level pour les octets aléatoires propres au format. Les API de génération de keypair Solana restent libres d'utiliser leur CSPRNG interne maintenu.
`getrandom 0.4.3` est retenu depuis `pre.004` comme primitive low-level pour les octets aléatoires propres au format. `pre.005` l'utilise également pour les seeds/slot IDs/salts/nonces de création. Les API de génération de keypair Solana ne deviennent pas pour autant propriétaires du CSPRNG du format.
### 8.5 Secret memory
`zeroize 1.9.0` est retenu et devient la seule nouvelle dépendance tierce de `pre.002`, car les wrappers `ViewPassword` / `OwnerPassword` lutilisent immédiatement pour leur nettoyage au `Drop`. `secrecy` n'est pas ajouté tant qu'un besoin ergonomique concret n'est pas démontré ; des types KSP simples imposent eux-mêmes redaction/non-Clone et utilisent `zeroize`.
Pour la clé admin Ed25519 distincte, `ed25519-dalek 3.0.0` est une candidate standard, mais son ajout direct est **conditionné au cargo-tree** de la tranche qui implémente l'authentification afin d'éviter une génération concurrente inutile avec celle déjà tirée par `solana-keypair`.
`pre.005` introduit directement **`ed25519-dalek ^2.2`** avec `default-features = false` et les features `signature` + `zeroize`. La génération `3.0.0`, bien que plus récente, n'est volontairement pas ajoutée : `solana-keypair 3.1.2` dépend de `ed25519-dalek ^2.1.1`, donc la branche directe `2.2` permet à Cargo d'unifier une seule génération Dalek et d'activer `zeroize` sur la `SigningKey` utilisée à la fois par l'autorité de format KSP et par le wrapper Solana. Une duplication `2.x + 3.x` n'apporterait aucune capacité nécessaire à V1.
### 8.6 État des audits de sécurité publics
@@ -432,14 +432,14 @@ L'absence d'une ligne « audited » dans ce document ne signifie donc pas « sû
### 8.7 Encodage et persistence
Candidates :
État au terme de `pre.005` :
```text
base64 0.23.1 pour Base64url sans padding des champs binaires JSON
tempfile 3.27.0 pour temp files same-directory + persist/persist_noclobber
base64 0.23.1 acquis depuis pre.003 pour Base64url sans padding des champs binaires JSON
tempfile 3.27.0 candidate pre.006 pour temp files same-directory + persist/persist_noclobber
```
Elles ne sont ajoutées que lorsqu'elles sont réellement consommées.
`tempfile` ne sera ajouté que si `pre.006` confirme sa sémantique portable et son besoin réel pour l'atomicité/no-clobber.
## 9. Format natif `.kspwallet` V1
@@ -516,7 +516,7 @@ Structure V1 figée par le codec `pre.003` (les longueurs/encodages exacts sont
}
```
Les `0` ci-dessus signifient « valeur déterminée par le benchmark futur », pas des paramètres valides.
Les paramètres de création KSP V1 sont désormais `memory_kib = 65536`, `iterations = 3`, `parallelism = 1`, avec un salt CSPRNG indépendant de 32 octets par slot. Le wire continue toutefois d'accepter tout profil V1 valide dans les bornes documentées, car les paramètres sont sérialisés par wallet.
### 9.3 Informations visibles wallet verrouillé
@@ -1031,6 +1031,26 @@ L'injection de random déterministe reste privée aux tests/codec fixtures ; l'A
- aucun `unsafe` KSP ;
- cargo-tree sans duplication crypto injustifiée.
### 19.7 État acquis après `pre.005`
`pre.005` matérialise désormais les invariants crypto centraux sans filesystem :
```text
K_owner_root, K_metadata, K_secret indépendants
OWNER slot -> K_owner_root
VIEW slot -> K_metadata
owner_control = admin Ed25519 secret || K_metadata || K_secret (96 octets)
metadata = JSON protégé Pubkey/alias/notes avec note IDs 16 octets uniques
secret = keypair Solana 64 octets secret||public
state_signature Ed25519 vérifiée avant tout KDF
open VIEW ne matérialise jamais owner-control/secret
open OWNER vérifie autorité admin + cohérence keypair + Pubkey metadata
create/open KDF via spawn_blocking
vecteur complet test-only généré et revérifié indépendamment du Rust
```
La persistence, la signature Solana publique, les mutations metadata et les rotations restent volontairement absentes de cette tranche.
## 20. Sizing
| Domaine | Taille | Risque principal |
@@ -1070,31 +1090,32 @@ rel.001 publication strictement publicationnelle
Une `fix` ou tranche supplémentaire est préférable à la suppression d'une garantie sécurité si un des gates révèle une incompatibilité.
## 22. Dépendances candidates par tranche
## 22. Dépendances par tranche
`pre.001` najoutait aucune dépendance. `pre.002` ajoute uniquement :
État réellement acquis au terme de `pre.005` :
```text
zeroize ^1.9
pre.002 zeroize ^1.9
pre.003 base64 ^0.23
pre.004 argon2 ^0.5
chacha20poly1305 ^0.11
getrandom ^0.4
pre.005 ed25519-dalek ^2.2
solana-keypair ^3.1
tokio déjà workspace, feature locale rt pour spawn_blocking
```
La dépendance est centralisée sous `[workspace.dependencies]` puis consommée avec `zeroize.workspace = true`. Elle est utilisée immédiatement par les wrappers de password ; aucune crate crypto/KDF/Solana supplémentaire nest ajoutée par anticipation. La génération courante `zeroize 1.9.0` a été réauditée avant insertion.
Toutes les dépendances tierces communes restent centralisées sous `[workspace.dependencies]`; le membre Wallet active uniquement les features nécessaires. `ed25519-dalek ^2.2` est volontairement aligné avec la contrainte `^2.1.1` de `solana-keypair 3.1.2` afin de permettre une seule génération Dalek et d'activer `zeroize` sur la `SigningKey` partagée par résolution Cargo.
Liste de travail restante, à réauditer juste avant insertion sous `[workspace.dependencies]` :
Candidates restantes, à réauditer juste avant insertion :
```text
argon2 ^0.5
base64 ^0.23
chacha20poly1305 ^0.11
ed25519-dalek ^3.0 # seulement si cargo-tree justifie le direct
getrandom ^0.4
solana-keypair ^3.1
solana-signer ^3.0 # si réellement nécessaire directement
tempfile ^3.27 # pre.006 si la sémantique atomic/no-clobber est confirmée
solana-signer ^3.0 # seulement si un contrat public/impl l'exige réellement
solana-signature ^3.5 # seulement si le type public l'exige
tempfile ^3.27
```
Déjà présents et réutilisables :
Déjà présents et réutilisés :
```text
serde
@@ -1122,7 +1143,7 @@ Le fait que `bs58` ne soit pas requis sera révalidé lorsque les adapters sont
## 23. Sources externes réauditées
Sources primaires consultées le 2026-08-18 :
Sources primaires consultées initialement le 2026-08-18 puis réauditées le 2026-08-19 pour les dépendances effectivement introduites par `pre.005` :
```text
https://docs.rs/solana-keypair/3.1.2/
@@ -1136,12 +1157,12 @@ https://docs.rs/chacha20poly1305/0.11.0/
https://docs.rs/aes-gcm-siv/0.12.0/
https://docs.rs/getrandom/0.4.3/
https://docs.rs/zeroize/1.9.0/
https://docs.rs/ed25519-dalek/3.0.0/
https://docs.rs/ed25519-dalek/2.2.0/
https://docs.rs/base64/0.23.1/
https://docs.rs/tempfile/3.27.0/
```
Les versions sont des constats d'audit du gate, pas des dépendances ajoutées par ce delta. Chaque tranche réaudite sa candidate au moment où elle devient réellement consommée.
Les versions sont des constats d'audit successifs : elles ne signifient pas que toutes les crates listées ont été ajoutées dans une même tranche. Chaque dépendance sensible est réauditée au moment où elle devient réellement consommée.
## 24. Critères de sortie `0.2.5`
@@ -1197,4 +1218,4 @@ Une future `format_version >= 2` pourra réétudier des facteurs/ancrages extern
## 26. Suite immédiate
`0.2.5-pre.003` fige le codec JSON strict, les limites structurelles, `slot_id` 16 octets, le descripteur VIEW, les DTOs denveloppe/key slots, les TLV transcript/AAD et la première spécification `docs/formats/KSPWALLET_V1.md`. `0.2.5-pre.004` ajoute les primitives effectives Argon2id v19, XChaCha20-Poly1305, CSPRNG OS, wrapping de content keys, zeroization des clés possédées et un vecteur crypto public déterministe. La seule partie de `pre.004` qui reste volontairement à confirmer par lopérateur est le choix du **default de création Argon2**, via le benchmark candidat livré et ignoré par défaut. `pre.005` ne doit pas figer ce default sans ce résultat.
`0.2.5-pre.003` fige le codec JSON strict, les limites structurelles, `slot_id` 16 octets, le descripteur VIEW, les DTOs denveloppe/key slots, les TLV transcript/AAD et la première spécification `docs/formats/KSPWALLET_V1.md`. `pre.004` ajoute Argon2id/XChaCha20-Poly1305/CSPRNG OS et le wrapping de content keys. Le benchmark opérateur a ensuite permis à `pre.005` de retenir le profil initial `64 MiB / 3 / 1`, de figer les payloads `owner_control`/metadata/secret, d'introduire l'autorité Ed25519 OWNER distincte de la keypair Solana, de créer/ouvrir réellement VIEW et OWNER en mémoire et de publier un vecteur `.kspwallet` complet interopérable. **La suite immédiate est `pre.006` : persistence async/atomique/no-clobber**, sans déplacer de logique filesystem dans Config.