From d370027c02722ed5fbfc9cc5c1bbc5ca62a917d5 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 22 Aug 2026 03:21:49 +0200 Subject: [PATCH] v0.2.6-pre.015 --- Cargo.toml | 4 +- ROADMAP.md | 19 +- crates/ksp-app-wallet-desk/src/app_state.rs | 20 +- crates/ksp-wallet-lib/README.md | 9 +- crates/ksp-wallet-lib/USAGE.md | 13 +- crates/ksp-wallet-lib/src/constants.rs | 48 +- crates/ksp-wallet-lib/src/lib.rs | 83 +- crates/ksp-wallet-lib/src/transcript_v2.rs | 141 +++ crates/ksp-wallet-lib/src/wire_v2.rs | 1037 +++++++++++++++++ .../tests/fixtures/kspwallet_v2_wire_only.bin | Bin 0 -> 628 bytes crates/ksp-wallet-lib/tests/public_api.rs | 21 +- .../unit_tests/transcript_v2.rs | 55 + crates/ksp-wallet-lib/unit_tests/wire_v2.rs | 136 +++ deltas/0.2.6/pre.015.md | 435 +++++++ docs/000-README.md | 4 +- docs/IDEAS.md | 10 +- docs/architecture/004-COMPONENT_INVENTORY.md | 18 +- docs/formats/000-README.md | 3 +- docs/formats/KSPWALLET_V1.md | 4 +- docs/formats/KSPWALLET_V2.md | 441 +++++++ docs/plans/000-README.md | 4 +- docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md | 44 +- docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md | 114 +- .../009-V0_2_6_WALLET_DESK_COMPLIANCE.md | 37 +- prompts/000-README.md | 5 +- prompts/011-V0_2_6_START_PROMPT.md | 17 +- prompts/012-V0_2_7_START_PROMPT.md | 272 +---- 27 files changed, 2630 insertions(+), 364 deletions(-) create mode 100644 crates/ksp-wallet-lib/src/transcript_v2.rs create mode 100644 crates/ksp-wallet-lib/src/wire_v2.rs create mode 100644 crates/ksp-wallet-lib/tests/fixtures/kspwallet_v2_wire_only.bin create mode 100644 crates/ksp-wallet-lib/unit_tests/transcript_v2.rs create mode 100644 crates/ksp-wallet-lib/unit_tests/wire_v2.rs create mode 100644 deltas/0.2.6/pre.015.md create mode 100644 docs/formats/KSPWALLET_V2.md diff --git a/Cargo.toml b/Cargo.toml index aede7cb..c3d545f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 187 +# version: 188 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-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.6-pre.14.fix.1" +version = "0.2.6-pre.15" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/ROADMAP.md b/ROADMAP.md index 8a094a4..711c08a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # Roadmap KSP @@ -50,15 +50,14 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U - [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. - [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001`–`fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe l’audit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet. -- [/] `0.2.6` — `ksp-app-wallet-desk` : surface fonctionnelle Wallet Desk acquise ; `pre.014` harmonise le gabarit/splash/Tauri multi-app puis `pre.015` clôture documentation et build final. Plan détaillé : `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`. -- [ ] `0.2.7` — Durcir la persistence `.kspwallet` dans `ksp-wallet-lib` avec un conteneur binaire versionné rétrocompatible ; conserver les semantics cryptographiques V1 et rejouer Wallet Desk comme consommateur sans changement fonctionnel attendu. -- [ ] `0.2.8` — É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.9` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client. -- [ ] `0.2.10` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle. -- [ ] `0.2.11` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR. -- [ ] `0.2.12` — Introduire une petite application desk de visualisation/validation des prix offchain, puis intégrer cette capacité dans `ksp-app-wallet-desk` sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé. -- [ ] `0.2.13` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes. -- [ ] `0.2.14` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet. +- [/] `0.2.6` — `ksp-app-wallet-desk` + évolution `.kspwallet` V2 : surface Desk acquise ; `pre.014` polish, `pre.015` wire binaire V2, `pre.016` APIs génériques/versionnées + V2, `pre.017` migration/canaris, `pre.018` candidate finale. Plan détaillé : `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`. +- [ ] `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. +- [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle. +- [ ] `0.2.10` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR. +- [ ] `0.2.11` — Introduire une petite application desk de visualisation/validation des prix offchain, puis intégrer cette capacité dans `ksp-app-wallet-desk` sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé. +- [ ] `0.2.12` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes. +- [ ] `0.2.13` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet. ### Règles Transport pour toute la série diff --git a/crates/ksp-app-wallet-desk/src/app_state.rs b/crates/ksp-app-wallet-desk/src/app_state.rs index 06b55ab..0ae012f 100644 --- a/crates/ksp-app-wallet-desk/src/app_state.rs +++ b/crates/ksp-app-wallet-desk/src/app_state.rs @@ -914,23 +914,23 @@ impl AppState { std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); - match previous { + return match previous { crate::WalletSession::ViewOperation { wallet_id: reserved_wallet_id, path: reserved_path, pubkey: reserved_pubkey } if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey => { *session = crate::WalletSession::View { wallet_id: wallet_id.clone(), path, wallet: view }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet VIEW privileged operation completed"); - return std::result::Result::Ok(dto); + std::result::Result::Ok(dto) }, other => { *session = other; drop(view); - return std::result::Result::Err(ksp_core_lib::Error::new( + std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet VIEW operation completion no longer owns the selected session", - )); + )) }, - } + }; } fn restore_view_after_operation_failure(&self, context: ViewOperationContext) { @@ -1039,7 +1039,7 @@ impl AppState { std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); - match previous { + return match previous { crate::WalletSession::OwnerOperation { wallet_id: reserved_wallet_id, path: reserved_path, @@ -1048,17 +1048,17 @@ impl AppState { } if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey && reserved_view_enabled == view_enabled => { *session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled: result_view_enabled, wallet: owner }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER privileged operation completed"); - return std::result::Result::Ok(dto); + std::result::Result::Ok(dto) }, other => { *session = other; drop(owner); - return std::result::Result::Err(ksp_core_lib::Error::new( + std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet OWNER operation completion no longer owns the selected session", - )); + )) }, - } + }; } fn restore_owner_after_operation_failure(&self, context: OwnerOperationContext) { diff --git a/crates/ksp-wallet-lib/README.md b/crates/ksp-wallet-lib/README.md index 9b9cb14..1f9df36 100644 --- a/crates/ksp-wallet-lib/README.md +++ b/crates/ksp-wallet-lib/README.md @@ -1,11 +1,11 @@ - + # `ksp-wallet-lib` Statut : **stable depuis KSP `0.2.5`**. -`ksp-wallet-lib` est la bibliothèque KSP propriétaire du Wallet Solana natif. Elle possède le format autonome `.kspwallet` V1, les capacités indépendantes VIEW/OWNER, la protection du secret Solana, la signature, l'administration des metadata, les rotations de credentials, la persistence native et les adapters d'import/export explicitement supportés. +`ksp-wallet-lib` est la bibliothèque KSP propriétaire du Wallet Solana natif. Elle possède le format autonome `.kspwallet` V1 et, depuis `0.2.6-pre.015`, le wire binaire V2 canonique, les capacités indépendantes VIEW/OWNER, la protection du secret Solana, la signature, l'administration des metadata, les rotations de credentials, la persistence native et les adapters d'import/export explicitement supportés. La crate est volontairement indépendante de Config, du réseau et de Tauri. Un consumer fournit les chemins, passwords et metadata ; Wallet ouvre, protège, signe et persiste sans décider d'une policy de dépense ni contacter un RPC. @@ -14,6 +14,7 @@ La crate est volontairement indépendante de Config, du réseau et de Tauri. Un La crate possède : - le format natif `.kspwallet` V1 et son parser JSON strict ; +- le wire binaire `.kspwallet` V2, son codec borné/canonique et ses domains/transcripts distincts ; - les key slots OWNER/VIEW indépendants ; - Argon2id pour les KDF de passwords ; - XChaCha20-Poly1305 pour le wrapping et les compartiments ; @@ -192,3 +193,7 @@ Elles couvrent le wire, Argon2id/XChaCha20-Poly1305, l'ouverture VIEW/OWNER, la - [`../../docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](../../docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan historique et threat model de `0.2.5` ; - [`../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) — matrice de sécurité/interoperabilité/compliance ; - [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_START_PROMPT.md) — reprise vers Wallet Desk après publication stable de `0.2.5`. + +## V2 en `0.2.6-pre.015` + +`pre.015` ajoute uniquement le codec structurel V2 et sa spécification. Les APIs de persistence runtime restent V1 jusqu’à `pre.016`, qui ajoute `create_wallet_file(...)` default V2, les variantes `_v1`/`_v2` et le dispatch générique de lecture. Le default est une décision explicite et ne suit pas automatiquement une future V3. diff --git a/crates/ksp-wallet-lib/USAGE.md b/crates/ksp-wallet-lib/USAGE.md index 64e5eaf..c1ba753 100644 --- a/crates/ksp-wallet-lib/USAGE.md +++ b/crates/ksp-wallet-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de `ksp-wallet-lib` @@ -277,3 +277,14 @@ ksp-onchain-transport-lib ``` Cette composition est le rôle de `0.2.6 — ksp-app-wallet-desk`, pas de `ksp-wallet-lib`. + +## Wire V2 (`0.2.6-pre.015`) + +Le codec structurel V2 peut être utilisé pour analyser une fixture/document V2 déjà produit : + +```rust +let envelope = ksp_wallet_lib::KspWalletEnvelopeV2::parse_binary(bytes)?; +let canonical = envelope.to_binary_bytes()?; +``` + +La persistence applicative ne doit pas encore appeler ce codec directement pour créer un wallet : `pre.016` introduit les APIs génériques/versionnées et le default V2. diff --git a/crates/ksp-wallet-lib/src/constants.rs b/crates/ksp-wallet-lib/src/constants.rs index 993bea6..4a71444 100644 --- a/crates/ksp-wallet-lib/src/constants.rs +++ b/crates/ksp-wallet-lib/src/constants.rs @@ -1,10 +1,12 @@ // file: crates/ksp-wallet-lib/src/constants.rs -// version: 7 +// version: 8 //! Wallet-owned constants. /// Native Wallet format version implemented by the V1 codec. pub const KSPWALLET_FORMAT_VERSION_V1: u32 = 1; +/// Native Wallet format version implemented by the V2 binary codec. +pub const KSPWALLET_FORMAT_VERSION_V2: u32 = 2; /// Exact magic string required by every native `.kspwallet` document. pub const KSPWALLET_MAGIC: &str = "KSPWALLET"; /// Maximum accepted `.kspwallet` document size before JSON parsing. @@ -85,6 +87,50 @@ pub const KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V1-STATE"; pub const KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-VIEW-SLOT"; /// Byte length of an XChaCha20-Poly1305 nonce. pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24; +/// Byte length of the Poly1305 authentication tag appended to each V2 ciphertext. +pub const KSPWALLET_V2_AEAD_TAG_BYTES: usize = KSPWALLET_V1_AEAD_TAG_BYTES; +/// Argon2 version serialized by V2 key slots. +pub const KSPWALLET_V2_ARGON2_VERSION: u32 = KSPWALLET_V1_ARGON2_VERSION; +/// Byte length of an Ed25519 public key used as V2 Wallet format authority. +pub const KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES: usize = KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES; +/// Byte length of an Ed25519 detached V2 state signature. +pub const KSPWALLET_V2_ED25519_SIGNATURE_BYTES: usize = KSPWALLET_V1_ED25519_SIGNATURE_BYTES; +/// Initial protected payload version used independently by V2 control, metadata and secret compartments. +pub const KSPWALLET_V2_INITIAL_PAYLOAD_VERSION: u32 = KSPWALLET_V1_INITIAL_PAYLOAD_VERSION; +/// Structural V2 ceiling for serialized Argon2 iteration cost. +pub const KSPWALLET_V2_MAX_ARGON2_ITERATIONS: u32 = KSPWALLET_V1_MAX_ARGON2_ITERATIONS; +/// Structural V2 ceiling for serialized Argon2 memory cost. +pub const KSPWALLET_V2_MAX_ARGON2_MEMORY_KIB: u32 = KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB; +/// Structural V2 ceiling for serialized Argon2 parallelism. +pub const KSPWALLET_V2_MAX_ARGON2_PARALLELISM: u32 = KSPWALLET_V1_MAX_ARGON2_PARALLELISM; +/// Maximum Argon2 salt size accepted by V2. +pub const KSPWALLET_V2_MAX_KDF_SALT_BYTES: usize = KSPWALLET_V1_MAX_KDF_SALT_BYTES; +/// Maximum wrapped-key ciphertext size accepted by V2. +pub const KSPWALLET_V2_MAX_KEY_WRAP_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_KEY_WRAP_CIPHERTEXT_BYTES; +/// Maximum protected metadata ciphertext size accepted by V2. +pub const KSPWALLET_V2_MAX_METADATA_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES; +/// Maximum OWNER-control ciphertext size accepted by V2. +pub const KSPWALLET_V2_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES; +/// Maximum OWNER-only secret ciphertext size accepted by V2. +pub const KSPWALLET_V2_MAX_SECRET_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES; +/// Domain separator for V2 metadata-compartment AEAD AAD. +pub const KSPWALLET_V2_METADATA_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-METADATA"; +/// Minimum Argon2 salt size accepted by V2. +pub const KSPWALLET_V2_MIN_KDF_SALT_BYTES: usize = KSPWALLET_V1_MIN_KDF_SALT_BYTES; +/// Domain separator for V2 OWNER-control compartment AEAD AAD. +pub const KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNER-CONTROL"; +/// Domain separator for V2 OWNER key-slot wrapping AAD. +pub const KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNER-SLOT"; +/// Domain separator for V2 OWNER-only secret compartment AEAD AAD. +pub const KSPWALLET_V2_SECRET_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-SECRET"; +/// Byte length of every V2 key-slot identifier. +pub const KSPWALLET_V2_SLOT_ID_BYTES: usize = KSPWALLET_V1_SLOT_ID_BYTES; +/// Domain separator for the V2 OWNER state-signature transcript. +pub const KSPWALLET_V2_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V2-STATE"; +/// Domain separator for V2 VIEW key-slot wrapping AAD. +pub const KSPWALLET_V2_VIEW_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-VIEW-SLOT"; +/// Byte length of an XChaCha20-Poly1305 nonce in V2. +pub const KSPWALLET_V2_XCHACHA_NONCE_BYTES: usize = KSPWALLET_V1_XCHACHA_NONCE_BYTES; /// Owning tracing target for events emitted by the Wallet crate. pub(crate) const TRACING_TARGET: &str = "ksp-wallet-lib"; diff --git a/crates/ksp-wallet-lib/src/lib.rs b/crates/ksp-wallet-lib/src/lib.rs index 1cf992a..482d200 100644 --- a/crates/ksp-wallet-lib/src/lib.rs +++ b/crates/ksp-wallet-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-wallet-lib/src/lib.rs -// version: 11 +// version: 12 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -15,7 +15,8 @@ //! adds Solana message signing, protected metadata administration, password rotation and strong VIEW disable/recreate with capability-bound atomic //! replacement. `0.2.5-pre.008` adds bounded Solana CLI JSON and canonical full-keypair Base58 import/export adapters with safe inspection and no-clobber //! native import/export publication. `0.2.5-pre.009` adds adversarial security/compliance canaries and records the final dependency/interop audit before -//! documentation closure. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral +//! documentation closure. `0.2.6-pre.015` defines the strict binary `.kspwallet` V2 wire and its bounded canonical codec without yet changing the public +//! persistence default or Wallet Desk dispatch. 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`. @@ -29,10 +30,12 @@ mod password; mod payload; mod persistence; mod transcript; +mod transcript_v2; mod transfer; mod view; mod wallet; mod wire; +mod wire_v2; #[cfg(test)] #[path = "../unit_tests/security.rs"] @@ -42,6 +45,8 @@ mod security_tests; pub use self::capability::WalletCapability; /// Native `.kspwallet` V1 format version. pub use self::constants::KSPWALLET_FORMAT_VERSION_V1; +/// Native `.kspwallet` V2 binary format version. +pub use self::constants::KSPWALLET_FORMAT_VERSION_V2; /// Exact magic string required by every native `.kspwallet` document. pub use self::constants::KSPWALLET_MAGIC; /// Maximum accepted `.kspwallet` document size before parsing. @@ -122,6 +127,50 @@ pub use self::constants::KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN; pub use self::constants::KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN; /// Byte length of an XChaCha20-Poly1305 nonce. pub use self::constants::KSPWALLET_V1_XCHACHA_NONCE_BYTES; +/// Byte length of the AEAD authentication tag appended to V2 ciphertexts. +pub use self::constants::KSPWALLET_V2_AEAD_TAG_BYTES; +/// Argon2 version serialized by `.kspwallet` V2 key slots. +pub use self::constants::KSPWALLET_V2_ARGON2_VERSION; +/// Byte length of the V2 Ed25519 format-authority public key. +pub use self::constants::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES; +/// Byte length of the V2 Ed25519 detached state signature. +pub use self::constants::KSPWALLET_V2_ED25519_SIGNATURE_BYTES; +/// Initial per-compartment protected payload version for V2. +pub use self::constants::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION; +/// Maximum structural Argon2 iteration count accepted by the V2 parser. +pub use self::constants::KSPWALLET_V2_MAX_ARGON2_ITERATIONS; +/// Maximum structural Argon2 memory cost accepted by the V2 parser. +pub use self::constants::KSPWALLET_V2_MAX_ARGON2_MEMORY_KIB; +/// Maximum structural Argon2 parallelism accepted by the V2 parser. +pub use self::constants::KSPWALLET_V2_MAX_ARGON2_PARALLELISM; +/// Maximum Argon2 salt size accepted by V2. +pub use self::constants::KSPWALLET_V2_MAX_KDF_SALT_BYTES; +/// Maximum wrapped-key ciphertext size accepted by V2. +pub use self::constants::KSPWALLET_V2_MAX_KEY_WRAP_CIPHERTEXT_BYTES; +/// Maximum protected metadata ciphertext size accepted by V2. +pub use self::constants::KSPWALLET_V2_MAX_METADATA_CIPHERTEXT_BYTES; +/// Maximum OWNER-control ciphertext size accepted by V2. +pub use self::constants::KSPWALLET_V2_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES; +/// Maximum OWNER-only secret ciphertext size accepted by V2. +pub use self::constants::KSPWALLET_V2_MAX_SECRET_CIPHERTEXT_BYTES; +/// Domain separator for V2 metadata-compartment AEAD AAD. +pub use self::constants::KSPWALLET_V2_METADATA_AAD_DOMAIN; +/// Minimum Argon2 salt size accepted by V2. +pub use self::constants::KSPWALLET_V2_MIN_KDF_SALT_BYTES; +/// Domain separator for V2 OWNER-control compartment AEAD AAD. +pub use self::constants::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN; +/// Domain separator for V2 OWNER key-slot wrapping AAD. +pub use self::constants::KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN; +/// Domain separator for V2 OWNER-only secret compartment AEAD AAD. +pub use self::constants::KSPWALLET_V2_SECRET_AAD_DOMAIN; +/// Byte length of every V2 key-slot identifier. +pub use self::constants::KSPWALLET_V2_SLOT_ID_BYTES; +/// Domain separator for the V2 OWNER state-signature transcript. +pub use self::constants::KSPWALLET_V2_STATE_TRANSCRIPT_DOMAIN; +/// Domain separator for V2 VIEW key-slot wrapping AAD. +pub use self::constants::KSPWALLET_V2_VIEW_SLOT_AAD_DOMAIN; +/// Byte length of an XChaCha20-Poly1305 nonce in V2. +pub use self::constants::KSPWALLET_V2_XCHACHA_NONCE_BYTES; /// Error code used when an atomic Wallet persistence operation cannot publish a valid replacement. pub use self::error::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED; /// Error code used when an authenticated Wallet structure cannot be verified. @@ -224,6 +273,30 @@ pub use self::wire::WalletStateSignatureAlgorithmV1; pub use self::wire::WalletStateSignatureV1; /// OWNER-signed stable descriptor of the optional VIEW slot. pub use self::wire::WalletViewDescriptorV1; +/// Strict semantic representation of one parsed native `.kspwallet` V2 binary envelope. +pub use self::wire_v2::KspWalletEnvelopeV2; +/// Authenticated-encryption algorithm fixed by native Wallet V2. +pub use self::wire_v2::WalletAeadAlgorithmV2; +/// Protected compartment kind fixed by native Wallet V2. +pub use self::wire_v2::WalletCompartmentKindV2; +/// One validated encrypted V2 compartment. +pub use self::wire_v2::WalletEncryptedCompartmentV2; +/// Password KDF fixed by native Wallet V2. +pub use self::wire_v2::WalletKdfAlgorithmV2; +/// Parsed Argon2id parameters from one V2 key slot. +pub use self::wire_v2::WalletKdfParametersV2; +/// Role of one native V2 key slot. +pub use self::wire_v2::WalletKeySlotRoleV2; +/// One validated OWNER or VIEW V2 key slot. +pub use self::wire_v2::WalletKeySlotV2; +/// Parsed AEAD wrapped-key payload from one V2 key slot. +pub use self::wire_v2::WalletKeyWrapV2; +/// State-signature algorithm fixed by native Wallet V2. +pub use self::wire_v2::WalletStateSignatureAlgorithmV2; +/// Detached OWNER state signature embedded in a V2 envelope. +pub use self::wire_v2::WalletStateSignatureV2; +/// OWNER-signed stable descriptor of the optional VIEW slot in V2. +pub use self::wire_v2::WalletViewDescriptorV2; /// Wallet-owned tracing target used by the KSP logging facade. pub(crate) use self::constants::TRACING_TARGET; @@ -277,6 +350,12 @@ pub(crate) use self::transcript::compartment_aad; pub(crate) use self::transcript::slot_aad; /// Internal deterministic OWNER-state transcript codec shared by Wallet crypto layers. pub(crate) use self::transcript::state_transcript; +/// Internal deterministic V2 compartment-AAD codec shared by Wallet crypto layers. +pub(crate) use self::transcript_v2::compartment_aad_v2; +/// Internal deterministic V2 key-slot-AAD codec shared by Wallet crypto layers. +pub(crate) use self::transcript_v2::slot_aad_v2; +/// Internal deterministic V2 OWNER-state transcript codec shared by Wallet crypto layers. +pub(crate) use self::transcript_v2::state_transcript_v2; /// Internal no-clobber transfer-file writer used only by OWNER export. pub(crate) use self::transfer::write_wallet_transfer_file_v1; /// Crate-internal `OwnerStateV1` state shared across the owning crate. diff --git a/crates/ksp-wallet-lib/src/transcript_v2.rs b/crates/ksp-wallet-lib/src/transcript_v2.rs new file mode 100644 index 0000000..4af4a4e --- /dev/null +++ b/crates/ksp-wallet-lib/src/transcript_v2.rs @@ -0,0 +1,141 @@ +// file: crates/ksp-wallet-lib/src/transcript_v2.rs +// version: 1 + +//! Deterministic `.kspwallet` V2 state-transcript and AEAD-AAD encoding. + +const TAG_COMPARTMENT_ALGORITHM: u16 = 0x0202; +const TAG_COMPARTMENT_CIPHERTEXT: u16 = 0x0204; +const TAG_COMPARTMENT_KIND: u16 = 0x0200; +const TAG_COMPARTMENT_NONCE: u16 = 0x0203; +const TAG_COMPARTMENT_VERSION: u16 = 0x0201; +const TAG_FORMAT_VERSION: u16 = 0x0002; +const TAG_KDF_ALGORITHM: u16 = 0x0102; +const TAG_KDF_ITERATIONS: u16 = 0x0105; +const TAG_KDF_MEMORY_KIB: u16 = 0x0104; +const TAG_KDF_PARALLELISM: u16 = 0x0106; +const TAG_KDF_SALT: u16 = 0x0107; +const TAG_KDF_VERSION: u16 = 0x0103; +const TAG_MAGIC: u16 = 0x0001; +const TAG_OWNER_AUTH_PUBLIC_KEY: u16 = 0x0003; +const TAG_SLOT_ID: u16 = 0x0100; +const TAG_SLOT_ROLE: u16 = 0x0101; +const TAG_STATE_SIGNATURE_ALGORITHM: u16 = 0x0500; +const TAG_VIEW_ENABLED: u16 = 0x0010; +const TAG_VIEW_ROLE: u16 = 0x0011; +const TAG_VIEW_SLOT_ID: u16 = 0x0012; +const TAG_WRAP_ALGORITHM: u16 = 0x0108; +const TAG_WRAP_CIPHERTEXT: u16 = 0x010A; +const TAG_WRAP_NONCE: u16 = 0x0109; + +/// Builds the normative OWNER state-signature transcript for one validated V2 envelope. +pub(crate) fn state_transcript_v2(envelope: &crate::KspWalletEnvelopeV2) -> std::vec::Vec { + let mut output = start(crate::KSPWALLET_V2_STATE_TRANSCRIPT_DOMAIN); + push_common(&mut output, envelope); + push_bool(&mut output, TAG_VIEW_ENABLED, envelope.view_descriptor().enabled()); + push_u8(&mut output, TAG_VIEW_ROLE, crate::WalletKeySlotRoleV2::View.wire_id()); + match envelope.view_descriptor().slot_id() { + std::option::Option::Some(slot_id) => push_bytes(&mut output, TAG_VIEW_SLOT_ID, slot_id), + std::option::Option::None => push_bytes(&mut output, TAG_VIEW_SLOT_ID, &[]), + } + push_slot(&mut output, envelope.owner_slot(), true); + push_compartment(&mut output, envelope.owner_control(), true); + push_compartment(&mut output, envelope.metadata(), true); + push_compartment(&mut output, envelope.secret(), true); + push_u8(&mut output, TAG_STATE_SIGNATURE_ALGORITHM, envelope.state_signature().algorithm().wire_id()); + return output; +} + +/// Builds the normative wrapping AAD for one validated V2 OWNER or VIEW key slot. +pub(crate) fn slot_aad_v2(envelope: &crate::KspWalletEnvelopeV2, slot: &crate::WalletKeySlotV2) -> std::vec::Vec { + let domain = match slot.role() { + crate::WalletKeySlotRoleV2::Owner => crate::KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN, + crate::WalletKeySlotRoleV2::View => crate::KSPWALLET_V2_VIEW_SLOT_AAD_DOMAIN, + }; + let mut output = start(domain); + push_common(&mut output, envelope); + push_slot(&mut output, slot, false); + return output; +} + +/// Builds the normative AEAD AAD for one validated V2 encrypted compartment. +pub(crate) fn compartment_aad_v2(envelope: &crate::KspWalletEnvelopeV2, compartment: &crate::WalletEncryptedCompartmentV2) -> std::vec::Vec { + let domain = match compartment.kind() { + crate::WalletCompartmentKindV2::OwnerControl => crate::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN, + crate::WalletCompartmentKindV2::Metadata => crate::KSPWALLET_V2_METADATA_AAD_DOMAIN, + crate::WalletCompartmentKindV2::Secret => crate::KSPWALLET_V2_SECRET_AAD_DOMAIN, + }; + let mut output = start(domain); + push_common(&mut output, envelope); + push_compartment(&mut output, compartment, false); + return output; +} + +fn start(domain: &[u8]) -> std::vec::Vec { + let mut output = std::vec::Vec::with_capacity(512); + output.extend_from_slice(domain); + output.push(0); + return output; +} + +fn push_bool(output: &mut std::vec::Vec, tag: u16, value: bool) { + let byte = if value { 1_u8 } else { 0_u8 }; + push_bytes(output, tag, &[byte]); + return; +} + +fn push_bytes(output: &mut std::vec::Vec, tag: u16, value: &[u8]) { + output.extend_from_slice(tag.to_be_bytes().as_slice()); + let length = value.len() as u64; + output.extend_from_slice(length.to_be_bytes().as_slice()); + output.extend_from_slice(value); + return; +} + +fn push_common(output: &mut std::vec::Vec, envelope: &crate::KspWalletEnvelopeV2) { + push_bytes(output, TAG_MAGIC, crate::KSPWALLET_MAGIC.as_bytes()); + push_u32(output, TAG_FORMAT_VERSION, envelope.format_version()); + push_bytes(output, TAG_OWNER_AUTH_PUBLIC_KEY, envelope.owner_auth_public_key()); + return; +} + +fn push_compartment(output: &mut std::vec::Vec, compartment: &crate::WalletEncryptedCompartmentV2, include_ciphertext: bool) { + push_u8(output, TAG_COMPARTMENT_KIND, compartment.kind().wire_id()); + push_u32(output, TAG_COMPARTMENT_VERSION, compartment.payload_version()); + push_u8(output, TAG_COMPARTMENT_ALGORITHM, compartment.algorithm().wire_id()); + if include_ciphertext { + push_bytes(output, TAG_COMPARTMENT_NONCE, compartment.nonce()); + push_bytes(output, TAG_COMPARTMENT_CIPHERTEXT, compartment.ciphertext()); + } + return; +} + +fn push_slot(output: &mut std::vec::Vec, slot: &crate::WalletKeySlotV2, include_wrap_payload: bool) { + push_bytes(output, TAG_SLOT_ID, slot.slot_id()); + push_u8(output, TAG_SLOT_ROLE, slot.role().wire_id()); + push_u8(output, TAG_KDF_ALGORITHM, slot.kdf().algorithm().wire_id()); + push_u32(output, TAG_KDF_VERSION, slot.kdf().version()); + push_u32(output, TAG_KDF_MEMORY_KIB, slot.kdf().memory_kib()); + push_u32(output, TAG_KDF_ITERATIONS, slot.kdf().iterations()); + push_u32(output, TAG_KDF_PARALLELISM, slot.kdf().parallelism()); + push_bytes(output, TAG_KDF_SALT, slot.kdf().salt()); + push_u8(output, TAG_WRAP_ALGORITHM, slot.wrap().algorithm().wire_id()); + if include_wrap_payload { + push_bytes(output, TAG_WRAP_NONCE, slot.wrap().nonce()); + push_bytes(output, TAG_WRAP_CIPHERTEXT, slot.wrap().ciphertext()); + } + return; +} + +fn push_u32(output: &mut std::vec::Vec, tag: u16, value: u32) { + push_bytes(output, tag, value.to_be_bytes().as_slice()); + return; +} + +fn push_u8(output: &mut std::vec::Vec, tag: u16, value: u8) { + push_bytes(output, tag, &[value]); + return; +} + +#[cfg(test)] +#[path = "../unit_tests/transcript_v2.rs"] +mod tests; diff --git a/crates/ksp-wallet-lib/src/wire_v2.rs b/crates/ksp-wallet-lib/src/wire_v2.rs new file mode 100644 index 0000000..a9eb506 --- /dev/null +++ b/crates/ksp-wallet-lib/src/wire_v2.rs @@ -0,0 +1,1037 @@ +// file: crates/ksp-wallet-lib/src/wire_v2.rs +// version: 2 + +//! Strict native `.kspwallet` V2 binary wire envelope. + +const AEAD_ALGORITHM_XCHACHA20_POLY1305: u8 = 1; +const COMPARTMENT_KIND_METADATA: u8 = 2; +const COMPARTMENT_KIND_OWNER_CONTROL: u8 = 1; +const COMPARTMENT_KIND_SECRET: u8 = 3; +const FORMAT_VERSION_WIRE_V2: u16 = 2; +const HEADER_FLAG_KNOWN_MASK: u16 = HEADER_FLAG_VIEW_ENABLED; +const HEADER_FLAG_VIEW_ENABLED: u16 = 0x0001; +const KDF_ALGORITHM_ARGON2ID: u8 = 1; +const KEY_SLOT_ROLE_OWNER: u8 = 1; +const KEY_SLOT_ROLE_VIEW: u8 = 2; +const STATE_SIGNATURE_ALGORITHM_ED25519: u8 = 1; + +/// Password KDF supported by `.kspwallet` V2 key slots. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WalletKdfAlgorithmV2 { + /// Argon2id version 19. + Argon2id, +} + +impl WalletKdfAlgorithmV2 { + /// Returns the normative one-byte V2 wire identifier. + #[must_use] + pub const fn wire_id(self) -> u8 { + return KDF_ALGORITHM_ARGON2ID; + } +} + +/// Authenticated-encryption algorithm supported by `.kspwallet` V2. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WalletAeadAlgorithmV2 { + /// XChaCha20-Poly1305 with a 24-byte nonce. + XChaCha20Poly1305, +} + +impl WalletAeadAlgorithmV2 { + /// Returns the normative one-byte V2 wire identifier. + #[must_use] + pub const fn wire_id(self) -> u8 { + return AEAD_ALGORITHM_XCHACHA20_POLY1305; + } +} + +/// State-signature algorithm supported by `.kspwallet` V2. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WalletStateSignatureAlgorithmV2 { + /// Ed25519 detached signature. + Ed25519, +} + +impl WalletStateSignatureAlgorithmV2 { + /// Returns the normative one-byte V2 wire identifier. + #[must_use] + pub const fn wire_id(self) -> u8 { + return STATE_SIGNATURE_ALGORITHM_ED25519; + } +} + +/// Role of one V2 password-protected key slot. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WalletKeySlotRoleV2 { + /// OWNER slot giving access to the OWNER/root capability. + Owner, + /// VIEW slot giving access only to protected metadata and self-service VIEW password rotation. + View, +} + +impl WalletKeySlotRoleV2 { + /// Returns the normative one-byte V2 wire identifier. + #[must_use] + pub const fn wire_id(self) -> u8 { + return match self { + Self::Owner => KEY_SLOT_ROLE_OWNER, + Self::View => KEY_SLOT_ROLE_VIEW, + }; + } +} + +/// Protected V2 payload compartment kind. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WalletCompartmentKindV2 { + /// OWNER control material, including the format-administration private material. + OwnerControl, + /// VIEW/OWNER-readable Solana Pubkey, alias and notes. + Metadata, + /// OWNER-only Solana keypair material. + Secret, +} + +impl WalletCompartmentKindV2 { + /// Returns the normative one-byte V2 wire identifier. + #[must_use] + pub const fn wire_id(self) -> u8 { + return match self { + Self::OwnerControl => COMPARTMENT_KIND_OWNER_CONTROL, + Self::Metadata => COMPARTMENT_KIND_METADATA, + Self::Secret => COMPARTMENT_KIND_SECRET, + }; + } +} + +/// Parsed Argon2id parameters embedded in one V2 key slot. +#[derive(Clone, Eq, PartialEq)] +pub struct WalletKdfParametersV2 { + algorithm: WalletKdfAlgorithmV2, + version: u32, + memory_kib: u32, + iterations: u32, + parallelism: u32, + salt: std::vec::Vec, +} + +impl WalletKdfParametersV2 { + /// Creates one crate-owned V2 KDF value after the caller has selected the serialized parameters. + pub(crate) fn new_internal(version: u32, memory_kib: u32, iterations: u32, parallelism: u32, salt: std::vec::Vec) -> Self { + return Self { algorithm: WalletKdfAlgorithmV2::Argon2id, version, memory_kib, iterations, parallelism, salt }; + } + + /// Returns the KDF algorithm. + #[must_use] + pub const fn algorithm(&self) -> WalletKdfAlgorithmV2 { + return self.algorithm; + } + + /// Returns the serialized Argon2 version. + #[must_use] + pub const fn version(&self) -> u32 { + return self.version; + } + + /// Returns the Argon2 memory cost in KiB. + #[must_use] + pub const fn memory_kib(&self) -> u32 { + return self.memory_kib; + } + + /// Returns the Argon2 iteration/time cost. + #[must_use] + pub const fn iterations(&self) -> u32 { + return self.iterations; + } + + /// Returns the Argon2 parallelism cost. + #[must_use] + pub const fn parallelism(&self) -> u32 { + return self.parallelism; + } + + /// Returns the public KDF salt bytes. + #[must_use] + pub fn salt(&self) -> &[u8] { + return self.salt.as_slice(); + } +} + +impl std::fmt::Debug for WalletKdfParametersV2 { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("WalletKdfParametersV2") + .field("algorithm", &self.algorithm) + .field("version", &self.version) + .field("memory_kib", &self.memory_kib) + .field("iterations", &self.iterations) + .field("parallelism", &self.parallelism) + .field("salt_bytes", &self.salt.len()) + .finish(); + } +} + +/// Parsed AEAD wrapping payload embedded in one V2 key slot. +#[derive(Clone, Eq, PartialEq)] +pub struct WalletKeyWrapV2 { + algorithm: WalletAeadAlgorithmV2, + nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], + ciphertext: std::vec::Vec, +} + +impl WalletKeyWrapV2 { + /// Creates one crate-owned V2 wrapping value. + pub(crate) fn new_internal(nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec) -> Self { + return Self { algorithm: WalletAeadAlgorithmV2::XChaCha20Poly1305, nonce, ciphertext }; + } + + /// Returns the wrapping AEAD algorithm. + #[must_use] + pub const fn algorithm(&self) -> WalletAeadAlgorithmV2 { + return self.algorithm; + } + + /// Returns the public AEAD nonce. + #[must_use] + pub const fn nonce(&self) -> &[u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES] { + return &self.nonce; + } + + /// Returns the wrapped-key ciphertext bytes. + #[must_use] + pub fn ciphertext(&self) -> &[u8] { + return self.ciphertext.as_slice(); + } +} + +impl std::fmt::Debug for WalletKeyWrapV2 { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("WalletKeyWrapV2") + .field("algorithm", &self.algorithm) + .field("nonce_bytes", &self.nonce.len()) + .field("ciphertext_bytes", &self.ciphertext.len()) + .finish(); + } +} + +/// One validated OWNER or VIEW key slot from a V2 envelope. +#[derive(Clone, Eq, PartialEq)] +pub struct WalletKeySlotV2 { + slot_id: [u8; crate::KSPWALLET_V2_SLOT_ID_BYTES], + role: WalletKeySlotRoleV2, + kdf: WalletKdfParametersV2, + wrap: WalletKeyWrapV2, +} + +impl WalletKeySlotV2 { + /// Creates one crate-owned V2 key slot. + pub(crate) fn new_internal( + slot_id: [u8; crate::KSPWALLET_V2_SLOT_ID_BYTES], + role: WalletKeySlotRoleV2, + kdf: WalletKdfParametersV2, + wrap: WalletKeyWrapV2, + ) -> 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_V2_SLOT_ID_BYTES] { + return &self.slot_id; + } + + /// Returns the key-slot role. + #[must_use] + pub const fn role(&self) -> WalletKeySlotRoleV2 { + return self.role; + } + + /// Returns the serialized password-KDF parameters. + #[must_use] + pub const fn kdf(&self) -> &WalletKdfParametersV2 { + return &self.kdf; + } + + /// Returns the wrapped capability payload. + #[must_use] + pub const fn wrap(&self) -> &WalletKeyWrapV2 { + return &self.wrap; + } +} + +impl std::fmt::Debug for WalletKeySlotV2 { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("WalletKeySlotV2") + .field("slot_id_bytes", &self.slot_id.len()) + .field("role", &self.role) + .field("kdf", &self.kdf) + .field("wrap", &self.wrap) + .finish(); + } +} + +/// OWNER-signed descriptor binding the optional self-service VIEW slot in V2. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WalletViewDescriptorV2 { + enabled: bool, + slot_id: std::option::Option<[u8; crate::KSPWALLET_V2_SLOT_ID_BYTES]>, +} + +impl WalletViewDescriptorV2 { + /// Creates a disabled V2 VIEW descriptor. + pub(crate) const fn disabled() -> Self { + return Self { enabled: false, slot_id: std::option::Option::None }; + } + + /// Creates an enabled V2 VIEW descriptor for one slot identifier. + pub(crate) const fn enabled_for_slot(slot_id: [u8; crate::KSPWALLET_V2_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 { + return self.enabled; + } + + /// Returns the signed VIEW slot identifier when VIEW is enabled. + #[must_use] + pub const fn slot_id(&self) -> std::option::Option<&[u8; crate::KSPWALLET_V2_SLOT_ID_BYTES]> { + return self.slot_id.as_ref(); + } +} + +/// One validated encrypted compartment from a V2 envelope. +#[derive(Clone, Eq, PartialEq)] +pub struct WalletEncryptedCompartmentV2 { + kind: WalletCompartmentKindV2, + payload_version: u32, + algorithm: WalletAeadAlgorithmV2, + nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], + ciphertext: std::vec::Vec, +} + +impl WalletEncryptedCompartmentV2 { + /// Creates one crate-owned V2 encrypted compartment. + pub(crate) fn new_internal( + kind: WalletCompartmentKindV2, + payload_version: u32, + nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], + ciphertext: std::vec::Vec, + ) -> Self { + return Self { kind, payload_version, algorithm: WalletAeadAlgorithmV2::XChaCha20Poly1305, nonce, ciphertext }; + } + + /// Returns the compartment kind. + #[must_use] + pub const fn kind(&self) -> WalletCompartmentKindV2 { + return self.kind; + } + + /// Returns the protected payload version for this compartment. + #[must_use] + pub const fn payload_version(&self) -> u32 { + return self.payload_version; + } + + /// Returns the compartment AEAD algorithm. + #[must_use] + pub const fn algorithm(&self) -> WalletAeadAlgorithmV2 { + return self.algorithm; + } + + /// Returns the public AEAD nonce. + #[must_use] + pub const fn nonce(&self) -> &[u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES] { + return &self.nonce; + } + + /// Returns the encrypted payload bytes. + #[must_use] + pub fn ciphertext(&self) -> &[u8] { + return self.ciphertext.as_slice(); + } +} + +impl std::fmt::Debug for WalletEncryptedCompartmentV2 { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("WalletEncryptedCompartmentV2") + .field("kind", &self.kind) + .field("payload_version", &self.payload_version) + .field("algorithm", &self.algorithm) + .field("nonce_bytes", &self.nonce.len()) + .field("ciphertext_bytes", &self.ciphertext.len()) + .finish(); + } +} + +/// Parsed detached OWNER state signature from a V2 envelope. +#[derive(Clone, Eq, PartialEq)] +pub struct WalletStateSignatureV2 { + algorithm: WalletStateSignatureAlgorithmV2, + signature: [u8; crate::KSPWALLET_V2_ED25519_SIGNATURE_BYTES], +} + +impl WalletStateSignatureV2 { + /// Creates one crate-owned V2 state signature. + pub(crate) const fn new_internal(signature: [u8; crate::KSPWALLET_V2_ED25519_SIGNATURE_BYTES]) -> Self { + return Self { algorithm: WalletStateSignatureAlgorithmV2::Ed25519, signature }; + } + + /// Returns the state-signature algorithm. + #[must_use] + pub const fn algorithm(&self) -> WalletStateSignatureAlgorithmV2 { + return self.algorithm; + } + + /// Returns the detached signature bytes. + #[must_use] + pub const fn signature(&self) -> &[u8; crate::KSPWALLET_V2_ED25519_SIGNATURE_BYTES] { + return &self.signature; + } +} + +impl std::fmt::Debug for WalletStateSignatureV2 { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("WalletStateSignatureV2") + .field("algorithm", &self.algorithm) + .field("signature_bytes", &self.signature.len()) + .finish(); + } +} + +/// Strict semantic representation of one parsed native `.kspwallet` V2 binary envelope. +#[derive(Clone, Eq, PartialEq)] +pub struct KspWalletEnvelopeV2 { + owner_auth_public_key: [u8; crate::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES], + view_descriptor: WalletViewDescriptorV2, + owner_slot: WalletKeySlotV2, + view_slot: std::option::Option, + owner_control: WalletEncryptedCompartmentV2, + metadata: WalletEncryptedCompartmentV2, + secret: WalletEncryptedCompartmentV2, + state_signature: WalletStateSignatureV2, +} + +impl KspWalletEnvelopeV2 { + /// Creates one crate-owned V2 envelope from already staged semantic fields. + pub(crate) fn new_internal( + owner_auth_public_key: [u8; crate::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES], + view_descriptor: WalletViewDescriptorV2, + owner_slot: WalletKeySlotV2, + view_slot: std::option::Option, + owner_control: WalletEncryptedCompartmentV2, + metadata: WalletEncryptedCompartmentV2, + secret: WalletEncryptedCompartmentV2, + state_signature: WalletStateSignatureV2, + ) -> Self { + return Self { + owner_auth_public_key, + view_descriptor, + owner_slot, + view_slot, + owner_control, + metadata, + secret, + state_signature, + }; + } + + /// Parses and structurally validates one complete canonical `.kspwallet` V2 binary document. + /// + /// This codec validates V2 framing, big-endian integer encoding, algorithm/role/kind identifiers, structural bounds, VIEW invariants and exact + /// consumption of the declared document length. Cryptographic authentication/decryption is deliberately owned by later Wallet orchestration. + pub fn parse_binary(source: &[u8]) -> ksp_core_lib::Result { + if source.len() > crate::KSPWALLET_MAX_FILE_BYTES { + return std::result::Result::Err(format_error("Wallet document exceeds the V2 maximum size", "document")); + } + let mut reader = BinaryReader::new(source); + let magic = match reader.read_fixed::<{ crate::KSPWALLET_MAGIC.len() }>("magic") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if magic.as_slice() != crate::KSPWALLET_MAGIC.as_bytes() { + return std::result::Result::Err(format_error("Wallet magic is invalid", "magic")); + } + let format_version = match reader.read_u16("format_version") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if format_version != FORMAT_VERSION_WIRE_V2 { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED, "Wallet format version is not supported") + .with_context("format_version", format_version.to_string()), + ); + } + let declared_length = match reader.read_u32("document_length") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let declared_length = match usize::try_from(declared_length) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(format_error("Wallet declared length cannot be represented", "document_length")), + }; + if declared_length != source.len() { + return std::result::Result::Err(format_error("Wallet declared length does not match the input length", "document_length")); + } + let flags = match reader.read_u16("flags") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if flags & !HEADER_FLAG_KNOWN_MASK != 0 { + return std::result::Result::Err(format_error("Wallet V2 header contains unsupported flag bits", "flags")); + } + let owner_auth_public_key = match reader.read_fixed::<{ crate::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES }>("owner_auth_public_key") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let view_enabled = flags & HEADER_FLAG_VIEW_ENABLED != 0; + let view_descriptor = if view_enabled { + let slot_id = match reader.read_fixed::<{ crate::KSPWALLET_V2_SLOT_ID_BYTES }>("view_descriptor.slot_id") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + WalletViewDescriptorV2::enabled_for_slot(slot_id) + } else { + WalletViewDescriptorV2::disabled() + }; + let owner_slot = match parse_key_slot(&mut reader, WalletKeySlotRoleV2::Owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let view_slot = if view_enabled { + match parse_key_slot(&mut reader, WalletKeySlotRoleV2::View) { + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } else { + std::option::Option::None + }; + if let (std::option::Option::Some(descriptor_slot_id), std::option::Option::Some(parsed_view_slot)) = (view_descriptor.slot_id(), view_slot.as_ref()) { + if descriptor_slot_id != parsed_view_slot.slot_id() { + return std::result::Result::Err(format_error("VIEW descriptor slot_id does not match the VIEW key slot", "view_descriptor.slot_id")); + } + } + let owner_control = match parse_compartment(&mut reader, WalletCompartmentKindV2::OwnerControl) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let metadata = match parse_compartment(&mut reader, WalletCompartmentKindV2::Metadata) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let secret = match parse_compartment(&mut reader, WalletCompartmentKindV2::Secret) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let state_signature = match parse_state_signature(&mut reader) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if !reader.is_finished() { + return std::result::Result::Err(format_error("Wallet V2 document contains trailing bytes", "document")); + } + let envelope = Self::new_internal(owner_auth_public_key, view_descriptor, owner_slot, view_slot, owner_control, metadata, secret, state_signature); + ksp_logging_lib::trace!( + target: crate::TRACING_TARGET, + operation = "wallet_wire_parse", + format_version = envelope.format_version(), + view_enabled = envelope.view_descriptor().enabled(), + "native wallet V2 binary envelope parsed" + ); + return std::result::Result::Ok(envelope); + } + + /// Serializes this semantic V2 envelope to the canonical KSP binary wire. + pub fn to_binary_bytes(&self) -> ksp_core_lib::Result> { + let validation = validate_envelope(self); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + let mut output = std::vec::Vec::with_capacity(1024); + output.extend_from_slice(crate::KSPWALLET_MAGIC.as_bytes()); + output.extend_from_slice(FORMAT_VERSION_WIRE_V2.to_be_bytes().as_slice()); + let length_offset = output.len(); + output.extend_from_slice(0_u32.to_be_bytes().as_slice()); + let flags = if self.view_descriptor.enabled() { HEADER_FLAG_VIEW_ENABLED } else { 0 }; + output.extend_from_slice(flags.to_be_bytes().as_slice()); + output.extend_from_slice(self.owner_auth_public_key()); + if let std::option::Option::Some(slot_id) = self.view_descriptor.slot_id() { + output.extend_from_slice(slot_id); + } + encode_key_slot(&mut output, self.owner_slot()); + if let std::option::Option::Some(view_slot) = self.view_slot() { + encode_key_slot(&mut output, view_slot); + } + encode_compartment(&mut output, self.owner_control()); + encode_compartment(&mut output, self.metadata()); + encode_compartment(&mut output, self.secret()); + output.push(self.state_signature.algorithm().wire_id()); + output.extend_from_slice(self.state_signature.signature()); + if output.len() > crate::KSPWALLET_MAX_FILE_BYTES { + return std::result::Result::Err(format_error("Wallet document exceeds the V2 maximum size", "document")); + } + let total_length = match u32::try_from(output.len()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(format_error("Wallet V2 document length exceeds the wire range", "document")), + }; + let length_end = length_offset + std::mem::size_of::(); + output[length_offset..length_end].copy_from_slice(total_length.to_be_bytes().as_slice()); + return std::result::Result::Ok(output); + } + + /// Returns the fixed native format version. + #[must_use] + pub const fn format_version(&self) -> u32 { + return crate::KSPWALLET_FORMAT_VERSION_V2; + } + + /// Returns the embedded Ed25519 public key that verifies OWNER-controlled state. + #[must_use] + pub const fn owner_auth_public_key(&self) -> &[u8; crate::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES] { + return &self.owner_auth_public_key; + } + + /// Returns the signed VIEW descriptor. + #[must_use] + pub const fn view_descriptor(&self) -> &WalletViewDescriptorV2 { + return &self.view_descriptor; + } + + /// Returns the mandatory OWNER key slot. + #[must_use] + pub const fn owner_slot(&self) -> &WalletKeySlotV2 { + return &self.owner_slot; + } + + /// Returns the optional VIEW key slot. + #[must_use] + pub const fn view_slot(&self) -> std::option::Option<&WalletKeySlotV2> { + return self.view_slot.as_ref(); + } + + /// Returns the OWNER-control encrypted compartment. + #[must_use] + pub const fn owner_control(&self) -> &WalletEncryptedCompartmentV2 { + return &self.owner_control; + } + + /// Returns the protected metadata encrypted compartment. + #[must_use] + pub const fn metadata(&self) -> &WalletEncryptedCompartmentV2 { + return &self.metadata; + } + + /// Returns the OWNER-only secret encrypted compartment. + #[must_use] + pub const fn secret(&self) -> &WalletEncryptedCompartmentV2 { + return &self.secret; + } + + /// Returns the detached OWNER state signature. + #[must_use] + pub const fn state_signature(&self) -> &WalletStateSignatureV2 { + return &self.state_signature; + } + + /// Builds the deterministic binary transcript covered by the V2 OWNER state signature. + #[must_use] + pub fn state_transcript(&self) -> std::vec::Vec { + return crate::state_transcript_v2(self); + } + + /// Builds the deterministic AEAD AAD for the V2 OWNER key slot. + #[must_use] + pub fn owner_slot_aad(&self) -> std::vec::Vec { + return crate::slot_aad_v2(self, self.owner_slot()); + } + + /// Builds the deterministic AEAD AAD for the optional V2 VIEW key slot. + #[must_use] + pub fn view_slot_aad(&self) -> std::option::Option> { + return match self.view_slot() { + std::option::Option::Some(slot) => std::option::Option::Some(crate::slot_aad_v2(self, slot)), + std::option::Option::None => std::option::Option::None, + }; + } + + /// Builds deterministic AEAD AAD for one V2 encrypted compartment. + #[must_use] + pub fn compartment_aad(&self, kind: WalletCompartmentKindV2) -> std::vec::Vec { + let compartment = match kind { + WalletCompartmentKindV2::OwnerControl => self.owner_control(), + WalletCompartmentKindV2::Metadata => self.metadata(), + WalletCompartmentKindV2::Secret => self.secret(), + }; + return crate::compartment_aad_v2(self, compartment); + } +} + +impl std::fmt::Debug for KspWalletEnvelopeV2 { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("KspWalletEnvelopeV2") + .field("format_version", &crate::KSPWALLET_FORMAT_VERSION_V2) + .field("owner_auth_public_key_bytes", &self.owner_auth_public_key.len()) + .field("view_descriptor", &self.view_descriptor) + .field("owner_slot", &self.owner_slot) + .field("view_slot", &self.view_slot) + .field("owner_control", &self.owner_control) + .field("metadata", &self.metadata) + .field("secret", &self.secret) + .field("state_signature", &self.state_signature) + .finish(); + } +} + +fn parse_key_slot(reader: &mut BinaryReader<'_>, expected_role: WalletKeySlotRoleV2) -> ksp_core_lib::Result { + let role_id = match reader.read_u8("key_slot.role") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let role = match role_id { + KEY_SLOT_ROLE_OWNER => WalletKeySlotRoleV2::Owner, + KEY_SLOT_ROLE_VIEW => WalletKeySlotRoleV2::View, + _ => return std::result::Result::Err(format_error("Wallet V2 key slot role is unsupported", "key_slot.role")), + }; + if role != expected_role { + return std::result::Result::Err(format_error("Wallet V2 key slots are not in canonical OWNER/VIEW order", "key_slot.role")); + } + let slot_id = match reader.read_fixed::<{ crate::KSPWALLET_V2_SLOT_ID_BYTES }>("key_slot.slot_id") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let kdf_algorithm = match reader.read_u8("key_slot.kdf.algorithm") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if kdf_algorithm != KDF_ALGORITHM_ARGON2ID { + return std::result::Result::Err(crypto_parameter_error("Wallet V2 key slots require Argon2id", "key_slot.kdf.algorithm")); + } + let version = match reader.read_u32("key_slot.kdf.version") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let memory_kib = match reader.read_u32("key_slot.kdf.memory_kib") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let iterations = match reader.read_u32("key_slot.kdf.iterations") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let parallelism = match reader.read_u32("key_slot.kdf.parallelism") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let salt_length = match reader.read_u8("key_slot.kdf.salt_length") { + std::result::Result::Ok(value) => usize::from(value), + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let salt = match reader.read_vec(salt_length, "key_slot.kdf.salt") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let kdf = WalletKdfParametersV2::new_internal(version, memory_kib, iterations, parallelism, salt); + let kdf_validation = validate_kdf(&kdf); + if let std::result::Result::Err(error) = kdf_validation { + return std::result::Result::Err(error); + } + let wrap_algorithm = match reader.read_u8("key_slot.wrap.algorithm") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if wrap_algorithm != AEAD_ALGORITHM_XCHACHA20_POLY1305 { + return std::result::Result::Err(crypto_parameter_error("Wallet V2 key wrapping requires XChaCha20-Poly1305", "key_slot.wrap.algorithm")); + } + let nonce = match reader.read_fixed::<{ crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES }>("key_slot.wrap.nonce") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let ciphertext_length = match reader.read_u16("key_slot.wrap.ciphertext_length") { + std::result::Result::Ok(value) => usize::from(value), + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let ciphertext = match reader.read_vec(ciphertext_length, "key_slot.wrap.ciphertext") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let wrap = WalletKeyWrapV2::new_internal(nonce, ciphertext); + let wrap_validation = validate_wrap(&wrap); + if let std::result::Result::Err(error) = wrap_validation { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(WalletKeySlotV2::new_internal(slot_id, role, kdf, wrap)); +} + +fn parse_compartment(reader: &mut BinaryReader<'_>, expected_kind: WalletCompartmentKindV2) -> ksp_core_lib::Result { + let kind_id = match reader.read_u8("compartment.kind") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let kind = match kind_id { + COMPARTMENT_KIND_OWNER_CONTROL => WalletCompartmentKindV2::OwnerControl, + COMPARTMENT_KIND_METADATA => WalletCompartmentKindV2::Metadata, + COMPARTMENT_KIND_SECRET => WalletCompartmentKindV2::Secret, + _ => return std::result::Result::Err(format_error("Wallet V2 compartment kind is unsupported", "compartment.kind")), + }; + if kind != expected_kind { + return std::result::Result::Err(format_error("Wallet V2 compartments are not in canonical order", "compartment.kind")); + } + let payload_version = match reader.read_u32("compartment.payload_version") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let algorithm = match reader.read_u8("compartment.algorithm") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if algorithm != AEAD_ALGORITHM_XCHACHA20_POLY1305 { + return std::result::Result::Err(crypto_parameter_error("Wallet V2 compartments require XChaCha20-Poly1305", "compartment.algorithm")); + } + let nonce = match reader.read_fixed::<{ crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES }>("compartment.nonce") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let ciphertext_length = match reader.read_u32("compartment.ciphertext_length") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let ciphertext_length = match usize::try_from(ciphertext_length) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(format_error("Wallet compartment length cannot be represented", "compartment.ciphertext_length")); + }, + }; + let ciphertext = match reader.read_vec(ciphertext_length, "compartment.ciphertext") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let compartment = WalletEncryptedCompartmentV2::new_internal(kind, payload_version, nonce, ciphertext); + let validation = validate_compartment(&compartment); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(compartment); +} + +fn parse_state_signature(reader: &mut BinaryReader<'_>) -> ksp_core_lib::Result { + let algorithm = match reader.read_u8("state_signature.algorithm") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if algorithm != STATE_SIGNATURE_ALGORITHM_ED25519 { + return std::result::Result::Err(crypto_parameter_error("Wallet V2 state authentication requires Ed25519", "state_signature.algorithm")); + } + let signature = match reader.read_fixed::<{ crate::KSPWALLET_V2_ED25519_SIGNATURE_BYTES }>("state_signature.signature") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(WalletStateSignatureV2::new_internal(signature)); +} + +fn validate_envelope(envelope: &KspWalletEnvelopeV2) -> ksp_core_lib::Result<()> { + if envelope.owner_slot.role() != WalletKeySlotRoleV2::Owner { + return std::result::Result::Err(format_error("Wallet V2 requires one canonical OWNER key slot", "owner_slot.role")); + } + let owner_validation = validate_slot(envelope.owner_slot()); + if let std::result::Result::Err(error) = owner_validation { + return std::result::Result::Err(error); + } + match (envelope.view_descriptor.enabled(), envelope.view_descriptor.slot_id(), envelope.view_slot()) { + (false, std::option::Option::None, std::option::Option::None) => {}, + (true, std::option::Option::Some(descriptor_slot_id), std::option::Option::Some(view_slot)) => { + if view_slot.role() != WalletKeySlotRoleV2::View || descriptor_slot_id != view_slot.slot_id() { + return std::result::Result::Err(format_error("Wallet V2 VIEW descriptor and VIEW key slot do not match", "view_descriptor")); + } + let view_validation = validate_slot(view_slot); + if let std::result::Result::Err(error) = view_validation { + return std::result::Result::Err(error); + } + }, + _ => return std::result::Result::Err(format_error("Wallet V2 VIEW descriptor shape is invalid", "view_descriptor")), + } + for compartment in [envelope.owner_control(), envelope.metadata(), envelope.secret()] { + let compartment_validation = validate_compartment(compartment); + if let std::result::Result::Err(error) = compartment_validation { + return std::result::Result::Err(error); + } + } + return std::result::Result::Ok(()); +} + +fn validate_slot(slot: &WalletKeySlotV2) -> ksp_core_lib::Result<()> { + let kdf_validation = validate_kdf(slot.kdf()); + if let std::result::Result::Err(error) = kdf_validation { + return std::result::Result::Err(error); + } + return validate_wrap(slot.wrap()); +} + +fn validate_kdf(kdf: &WalletKdfParametersV2) -> ksp_core_lib::Result<()> { + if kdf.algorithm() != WalletKdfAlgorithmV2::Argon2id || kdf.version() != crate::KSPWALLET_V2_ARGON2_VERSION { + return std::result::Result::Err(crypto_parameter_error("Wallet V2 requires Argon2id version 19", "key_slot.kdf")); + } + if kdf.memory_kib() == 0 || kdf.memory_kib() > crate::KSPWALLET_V2_MAX_ARGON2_MEMORY_KIB { + return std::result::Result::Err(crypto_parameter_error("Wallet Argon2 memory_kib is outside the V2 structural range", "key_slot.kdf.memory_kib")); + } + if kdf.iterations() == 0 || kdf.iterations() > crate::KSPWALLET_V2_MAX_ARGON2_ITERATIONS { + return std::result::Result::Err(crypto_parameter_error("Wallet Argon2 iterations is outside the V2 structural range", "key_slot.kdf.iterations")); + } + if kdf.parallelism() == 0 || kdf.parallelism() > crate::KSPWALLET_V2_MAX_ARGON2_PARALLELISM { + return std::result::Result::Err(crypto_parameter_error("Wallet Argon2 parallelism is outside the V2 structural range", "key_slot.kdf.parallelism")); + } + if kdf.memory_kib() < kdf.parallelism() * 8 { + return std::result::Result::Err(crypto_parameter_error( + "Wallet Argon2 memory_kib must provide at least eight 1-KiB blocks per lane", + "key_slot.kdf.memory_kib", + )); + } + if kdf.salt().len() < crate::KSPWALLET_V2_MIN_KDF_SALT_BYTES || kdf.salt().len() > crate::KSPWALLET_V2_MAX_KDF_SALT_BYTES { + return std::result::Result::Err(crypto_parameter_error("Wallet KDF salt length is outside the V2 range", "key_slot.kdf.salt")); + } + return std::result::Result::Ok(()); +} + +fn validate_wrap(wrap: &WalletKeyWrapV2) -> ksp_core_lib::Result<()> { + if wrap.algorithm() != WalletAeadAlgorithmV2::XChaCha20Poly1305 { + return std::result::Result::Err(crypto_parameter_error("Wallet V2 key wrapping requires XChaCha20-Poly1305", "key_slot.wrap.algorithm")); + } + if wrap.ciphertext().len() < crate::KSPWALLET_V2_AEAD_TAG_BYTES || wrap.ciphertext().len() > crate::KSPWALLET_V2_MAX_KEY_WRAP_CIPHERTEXT_BYTES { + return std::result::Result::Err(format_error("Wallet V2 key-wrap ciphertext length is invalid", "key_slot.wrap.ciphertext")); + } + return std::result::Result::Ok(()); +} + +fn validate_compartment(compartment: &WalletEncryptedCompartmentV2) -> ksp_core_lib::Result<()> { + if compartment.payload_version() != crate::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION { + return std::result::Result::Err(format_error("Wallet protected payload version is unsupported by V2", "compartment.payload_version")); + } + if compartment.algorithm() != WalletAeadAlgorithmV2::XChaCha20Poly1305 { + return std::result::Result::Err(crypto_parameter_error("Wallet V2 compartments require XChaCha20-Poly1305", "compartment.algorithm")); + } + let maximum = match compartment.kind() { + WalletCompartmentKindV2::OwnerControl => crate::KSPWALLET_V2_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES, + WalletCompartmentKindV2::Metadata => crate::KSPWALLET_V2_MAX_METADATA_CIPHERTEXT_BYTES, + WalletCompartmentKindV2::Secret => crate::KSPWALLET_V2_MAX_SECRET_CIPHERTEXT_BYTES, + }; + if compartment.ciphertext().len() < crate::KSPWALLET_V2_AEAD_TAG_BYTES || compartment.ciphertext().len() > maximum { + return std::result::Result::Err(format_error("Wallet V2 compartment ciphertext length is invalid", "compartment.ciphertext")); + } + return std::result::Result::Ok(()); +} + +fn encode_key_slot(output: &mut std::vec::Vec, slot: &WalletKeySlotV2) { + output.push(slot.role().wire_id()); + output.extend_from_slice(slot.slot_id()); + output.push(slot.kdf().algorithm().wire_id()); + output.extend_from_slice(slot.kdf().version().to_be_bytes().as_slice()); + output.extend_from_slice(slot.kdf().memory_kib().to_be_bytes().as_slice()); + output.extend_from_slice(slot.kdf().iterations().to_be_bytes().as_slice()); + output.extend_from_slice(slot.kdf().parallelism().to_be_bytes().as_slice()); + output.push(slot.kdf().salt().len() as u8); + output.extend_from_slice(slot.kdf().salt()); + output.push(slot.wrap().algorithm().wire_id()); + output.extend_from_slice(slot.wrap().nonce()); + output.extend_from_slice((slot.wrap().ciphertext().len() as u16).to_be_bytes().as_slice()); + output.extend_from_slice(slot.wrap().ciphertext()); + return; +} + +fn encode_compartment(output: &mut std::vec::Vec, compartment: &WalletEncryptedCompartmentV2) { + output.push(compartment.kind().wire_id()); + output.extend_from_slice(compartment.payload_version().to_be_bytes().as_slice()); + output.push(compartment.algorithm().wire_id()); + output.extend_from_slice(compartment.nonce()); + output.extend_from_slice((compartment.ciphertext().len() as u32).to_be_bytes().as_slice()); + output.extend_from_slice(compartment.ciphertext()); + return; +} + +struct BinaryReader<'a> { + source: &'a [u8], + offset: usize, +} + +impl<'a> BinaryReader<'a> { + const fn new(source: &'a [u8]) -> Self { + return Self { source, offset: 0 }; + } + + fn read_u8(&mut self, field: &'static str) -> ksp_core_lib::Result { + let bytes = match self.take(1, field) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(bytes[0]); + } + + fn read_u16(&mut self, field: &'static str) -> ksp_core_lib::Result { + let bytes = match self.read_fixed::<2>(field) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(u16::from_be_bytes(bytes)); + } + + fn read_u32(&mut self, field: &'static str) -> ksp_core_lib::Result { + let bytes = match self.read_fixed::<4>(field) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(u32::from_be_bytes(bytes)); + } + + fn read_fixed(&mut self, field: &'static str) -> ksp_core_lib::Result<[u8; LENGTH]> { + let bytes = match self.take(LENGTH, field) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let converted = <[u8; LENGTH]>::try_from(bytes); + return match converted { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(_) => std::result::Result::Err(format_error("Wallet V2 fixed-length field cannot be converted", field)), + }; + } + + fn read_vec(&mut self, length: usize, field: &'static str) -> ksp_core_lib::Result> { + let bytes = match self.take(length, field) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(bytes.to_vec()); + } + + fn take(&mut self, length: usize, field: &'static str) -> ksp_core_lib::Result<&'a [u8]> { + let end = match self.offset.checked_add(length) { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(format_error("Wallet V2 field length overflows the parser range", field)), + }; + if end > self.source.len() { + return std::result::Result::Err(format_error("Wallet V2 document is truncated", field)); + } + let bytes = &self.source[self.offset..end]; + self.offset = end; + return std::result::Result::Ok(bytes); + } + + const fn is_finished(&self) -> bool { + return self.offset == self.source.len(); + } +} + +fn format_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); +} + +fn crypto_parameter_error(message: &'static str, field: &'static str) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID, message).with_context("field", field); +} + +#[cfg(test)] +#[path = "../unit_tests/wire_v2.rs"] +mod tests; diff --git a/crates/ksp-wallet-lib/tests/fixtures/kspwallet_v2_wire_only.bin b/crates/ksp-wallet-lib/tests/fixtures/kspwallet_v2_wire_only.bin new file mode 100644 index 0000000000000000000000000000000000000000..aff75b55fe048360abb152b29e07864f260cf922 GIT binary patch literal 628 zcmeYc4hVPj@o^1dU}9ikDq&z`U}R!uVP#|I;N;@w;pO8O5EK#?5fu}ckd%^^k(HBI zP*hS@QB_md(A3h_(bdyuL|4Gbz`!63GzSEjftXRiz|hFp#MI2(!qUpx#@5cBF(5D~ zI3zSIJR&kGIwm$QJ|QtFIVCkMok5|Yv8lPGwXMCQv#YzOx37P~#7UE`o> z=ggfqp9#Zypbf$d3=Kevfdz;e1soimoLyYq+&w(KynTHA{24Pcv$At?^YRM{i;7E1 z%gQS%tEy{i>*^U47A#z}c*)Xb%U7&iwR+9kb?Y~5+_ZVi)@|E&?A*0`&)$9eL7re_ zM0W#B7f{mRz`;X@j~qRA{KUyqr_Y={cmBe~OP8-)y>|V^&0Dwc+`V`I!NW(7pFDl` z{Kd;xuiw0V_Z}F?KofD<2Qm>oIMF?f9+==DX2j(VposzK0fH;^afK6loG{|@+=q{! WK7aZ8?fZ|PzkdJu`|m%lqyzx{&iNSt literal 0 HcmV?d00001 diff --git a/crates/ksp-wallet-lib/tests/public_api.rs b/crates/ksp-wallet-lib/tests/public_api.rs index 868c4ba..cfa3878 100644 --- a/crates/ksp-wallet-lib/tests/public_api.rs +++ b/crates/ksp-wallet-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-wallet-lib/tests/public_api.rs -// version: 9 +// version: 10 //! Public API canaries for the Wallet foundation. @@ -81,6 +81,25 @@ fn strict_v1_envelope_and_transcript_are_available_from_crate_root() -> ksp_core return std::result::Result::Ok(()); } +#[test] +fn public_pre_015_v2_binary_wire_codec_is_available_from_crate_root() -> ksp_core_lib::Result<()> { + let source = include_bytes!("fixtures/kspwallet_v2_wire_only.bin"); + let envelope = match ksp_wallet_lib::KspWalletEnvelopeV2::parse_binary(source) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + assert_eq!(envelope.format_version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V2); + assert_eq!(envelope.owner_slot().role(), ksp_wallet_lib::WalletKeySlotRoleV2::Owner); + assert!(envelope.view_descriptor().enabled()); + assert_eq!(envelope.metadata().kind(), ksp_wallet_lib::WalletCompartmentKindV2::Metadata); + let encoded = match envelope.to_binary_bytes() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + assert_eq!(encoded.as_slice(), source); + 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; diff --git a/crates/ksp-wallet-lib/unit_tests/transcript_v2.rs b/crates/ksp-wallet-lib/unit_tests/transcript_v2.rs new file mode 100644 index 0000000..4c01b29 --- /dev/null +++ b/crates/ksp-wallet-lib/unit_tests/transcript_v2.rs @@ -0,0 +1,55 @@ +// file: crates/ksp-wallet-lib/unit_tests/transcript_v2.rs +// version: 1 + +const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v2_wire_only.bin"); + +#[test] +fn v2_state_transcript_is_domain_separated_and_deterministic() -> ksp_core_lib::Result<()> { + let envelope = match crate::KspWalletEnvelopeV2::parse_binary(FIXTURE) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let first = envelope.state_transcript(); + let second = envelope.state_transcript(); + assert_eq!(first, second); + assert!(first.starts_with(crate::KSPWALLET_V2_STATE_TRANSCRIPT_DOMAIN)); + assert!(!first.starts_with(crate::KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN)); + return std::result::Result::Ok(()); +} + +#[test] +fn v2_owner_and_view_slot_aad_are_role_domain_separated() -> ksp_core_lib::Result<()> { + let envelope = match crate::KspWalletEnvelopeV2::parse_binary(FIXTURE) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let owner = envelope.owner_slot_aad(); + let view = match envelope.view_slot_aad() { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "fixture VIEW slot is missing")); + }, + }; + assert!(owner.starts_with(crate::KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN)); + assert!(view.starts_with(crate::KSPWALLET_V2_VIEW_SLOT_AAD_DOMAIN)); + assert_ne!(owner, view); + return std::result::Result::Ok(()); +} + +#[test] +fn v2_compartment_aad_is_domain_separated_by_kind() -> ksp_core_lib::Result<()> { + let envelope = match crate::KspWalletEnvelopeV2::parse_binary(FIXTURE) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let owner_control = envelope.compartment_aad(crate::WalletCompartmentKindV2::OwnerControl); + let metadata = envelope.compartment_aad(crate::WalletCompartmentKindV2::Metadata); + let secret = envelope.compartment_aad(crate::WalletCompartmentKindV2::Secret); + assert!(owner_control.starts_with(crate::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN)); + assert!(metadata.starts_with(crate::KSPWALLET_V2_METADATA_AAD_DOMAIN)); + assert!(secret.starts_with(crate::KSPWALLET_V2_SECRET_AAD_DOMAIN)); + assert_ne!(owner_control, metadata); + assert_ne!(metadata, secret); + assert_ne!(owner_control, secret); + return std::result::Result::Ok(()); +} diff --git a/crates/ksp-wallet-lib/unit_tests/wire_v2.rs b/crates/ksp-wallet-lib/unit_tests/wire_v2.rs new file mode 100644 index 0000000..39b1987 --- /dev/null +++ b/crates/ksp-wallet-lib/unit_tests/wire_v2.rs @@ -0,0 +1,136 @@ +// file: crates/ksp-wallet-lib/unit_tests/wire_v2.rs +// version: 1 + +const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v2_wire_only.bin"); +const V1_JSON_FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json"); + +#[test] +fn strict_v2_fixture_parses_and_round_trips_byte_exact() -> ksp_core_lib::Result<()> { + let parsed = match crate::KspWalletEnvelopeV2::parse_binary(FIXTURE) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + assert_eq!(parsed.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2); + assert!(parsed.view_descriptor().enabled()); + assert_eq!(parsed.owner_slot().role(), crate::WalletKeySlotRoleV2::Owner); + assert_eq!(parsed.view_slot().map(crate::WalletKeySlotV2::role), std::option::Option::Some(crate::WalletKeySlotRoleV2::View)); + assert_eq!(parsed.owner_control().kind(), crate::WalletCompartmentKindV2::OwnerControl); + assert_eq!(parsed.metadata().kind(), crate::WalletCompartmentKindV2::Metadata); + assert_eq!(parsed.secret().kind(), crate::WalletCompartmentKindV2::Secret); + let encoded = match parsed.to_binary_bytes() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + assert_eq!(encoded.as_slice(), FIXTURE); + return std::result::Result::Ok(()); +} + +#[test] +fn v2_wire_fixture_is_materially_smaller_than_equivalent_v1_json_fixture() { + assert!(FIXTURE.len() < V1_JSON_FIXTURE.len()); + assert_eq!(FIXTURE.len(), 628); + assert_eq!(V1_JSON_FIXTURE.len(), 2037); +} + +#[test] +fn unknown_v2_format_version_is_rejected_before_body_parsing() { + let mut source = FIXTURE.to_vec(); + source[9..11].copy_from_slice(3_u16.to_be_bytes().as_slice()); + let result = crate::KspWalletEnvelopeV2::parse_binary(source.as_slice()); + assert!(result.is_err()); + let error = match result { + std::result::Result::Err(error) => error, + std::result::Result::Ok(_) => return, + }; + assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED); +} + +#[test] +fn declared_length_mismatch_and_trailing_bytes_are_rejected() { + let mut mismatched = FIXTURE.to_vec(); + mismatched[11..15].copy_from_slice(627_u32.to_be_bytes().as_slice()); + let mismatch_result = crate::KspWalletEnvelopeV2::parse_binary(mismatched.as_slice()); + assert!(mismatch_result.is_err()); + let mut trailing = FIXTURE.to_vec(); + trailing.push(0); + trailing[11..15].copy_from_slice(629_u32.to_be_bytes().as_slice()); + let trailing_result = crate::KspWalletEnvelopeV2::parse_binary(trailing.as_slice()); + assert!(trailing_result.is_err()); +} + +#[test] +fn unknown_header_flags_are_rejected() { + let mut source = FIXTURE.to_vec(); + source[15..17].copy_from_slice(2_u16.to_be_bytes().as_slice()); + let result = crate::KspWalletEnvelopeV2::parse_binary(source.as_slice()); + assert!(result.is_err()); + let error = match result { + std::result::Result::Err(error) => error, + std::result::Result::Ok(_) => return, + }; + assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID); +} + +#[test] +fn enabled_view_descriptor_must_match_the_v2_view_slot() { + let mut source = FIXTURE.to_vec(); + source[49] ^= 0x01; + let result = crate::KspWalletEnvelopeV2::parse_binary(source.as_slice()); + assert!(result.is_err()); + let error = match result { + std::result::Result::Err(error) => error, + std::result::Result::Ok(_) => return, + }; + assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID); +} + +#[test] +fn v2_key_slots_require_canonical_owner_then_view_order() { + let mut source = FIXTURE.to_vec(); + source[65] = 2; + let result = crate::KspWalletEnvelopeV2::parse_binary(source.as_slice()); + assert!(result.is_err()); + let error = match result { + std::result::Result::Err(error) => error, + std::result::Result::Ok(_) => return, + }; + assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID); +} + +#[test] +fn v2_pathological_kdf_parameters_are_rejected_before_crypto() { + let mut source = FIXTURE.to_vec(); + source[87..91].copy_from_slice(0_u32.to_be_bytes().as_slice()); + let result = crate::KspWalletEnvelopeV2::parse_binary(source.as_slice()); + assert!(result.is_err()); + let error = match result { + std::result::Result::Err(error) => error, + std::result::Result::Ok(_) => return, + }; + assert_eq!(error.code(), crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID); +} + +#[test] +fn v2_oversized_document_is_rejected_before_body_allocation() { + let oversized = std::vec![0_u8; crate::KSPWALLET_MAX_FILE_BYTES + 1]; + let result = crate::KspWalletEnvelopeV2::parse_binary(oversized.as_slice()); + assert!(result.is_err()); + let error = match result { + std::result::Result::Err(error) => error, + std::result::Result::Ok(_) => return, + }; + assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID); +} + +#[test] +fn v2_envelope_debug_does_not_render_ciphertext_contents() -> ksp_core_lib::Result<()> { + let parsed = match crate::KspWalletEnvelopeV2::parse_binary(FIXTURE) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let rendered = format!("{parsed:?}"); + assert!(rendered.contains("ciphertext_bytes")); + assert!(!rendered.contains("128, 129, 130, 131")); + assert!(!rendered.contains("224, 225, 226, 227")); + return std::result::Result::Ok(()); +} diff --git a/deltas/0.2.6/pre.015.md b/deltas/0.2.6/pre.015.md new file mode 100644 index 0000000..0364e3f --- /dev/null +++ b/deltas/0.2.6/pre.015.md @@ -0,0 +1,435 @@ + + + +# Delta `0.2.6-pre.015` — wire binaire `.kspwallet` V2 et codec canonique + +## Base requise + +```text +0.2.6-pre.014-fix.001 appliquée +workspace.package.version = 0.2.6-pre.14.fix.1 +``` + +Le checkpoint opérateur de la base est intégralement vert : + +```text +cargo fmt --all OK +python3 scripts/audit_rust_workspace_rules.py clean +cargo check --workspace OK +cargo clippy --workspace --all-targets OK +cargo test -p ksp-app-config-desk OK +cargo test -p ksp-app-wallet-desk OK +cargo test --workspace OK +Wallet Desk release_compliance 4/4 OK +smokes live toujours opt-in/ignored +Tauri Config Desk crate-local OK visuel +Tauri Wallet Desk crate-local OK visuel + import/balance Devnet manuel +``` + +Le correctif `pre.014-fix.001` est donc accepté comme base de `pre.015`. + +## Type de livraison + +Cette tranche est fournie sous forme de **delta uniquement**, à appliquer sur la base précédente : + +```text +ksp-general-0.2.6-pre.015-delta.zip +``` + +Le signal technique devient : + +```text +workspace.package.version = 0.2.6-pre.15 +commit = v0.2.6-pre.015 +``` + +Aucun tag prerelease. + +Les `package.json` et `tauri.conf.json` des applications ne changent pas : `pre.015` ne modifie ni frontend, ni Tauri, ni contrat de build des Desks. + +## Objectif de la tranche + +`pre.015` fige le **format `.kspwallet` V2 binaire** et son codec structurel strict dans `ksp-wallet-lib`. + +Cette tranche réalise : + +```text +spécification normative V2 +framing binaire KSP propriétaire +codec parse/serialize byte-exact +identifiants numériques stables +bornes structurelles avant crypto +transcripts/AAD V2 séparés de V1 +fixture binaire canonique wire-only +tests déterministes du codec et des invariants +API publique du codec V2 depuis la racine de crate +documentation de la politique default != latest +réalignement du plan 0.2.6 et des releases 0.2.7+ +``` + +Cette tranche **ne réalise pas encore** : + +```text +create_wallet_file_v2 +open_wallet_*_file_v2 +façades génériques multi-version +basculement de Wallet Desk vers les façades génériques +migration V1 -> V2 +réécriture automatique de wallets V1 +second facteur / V3 +build Tauri de production +``` + +Ces responsabilités restent réparties entre `pre.016`, `pre.017` et `pre.018`. + +## Décision V1 / V2 + +V1 reste un format stable historique : + +```text +format_version = 1 +JSON UTF-8 strict +bytes -> Base64URL sans padding +APIs _v1 conservées +lecture durable obligatoire +``` + +V2 est un nouveau format explicite : + +```text +format_version = 2 +wire binaire KSP +aucune Base64 +aucune compression +entiers multi-octets big-endian +ordre et cardinalité canoniques +aucun trailing byte +``` + +Le caractère binaire n'est pas présenté comme une mesure cryptographique. Les propriétés de confidentialité/authenticité restent possédées par Argon2id, XChaCha20-Poly1305, Ed25519 et les règles VIEW/OWNER. + +## Wire V2 + +Le document commence par : + +```text +magic 9 bytes = KSPWALLET +format_version u16 BE = 2 +document_length u32 BE = taille totale exacte +flags u16 BE +owner_auth_public_key 32 bytes +view_slot_id 16 bytes si VIEW_ENABLED +``` + +Flags : + +```text +bit 0 = VIEW_ENABLED +autres bits = réservés et obligatoirement zéro +``` + +Le body est strictement : + +```text +OWNER key slot +VIEW key slot si VIEW_ENABLED +OWNER-CONTROL compartment +METADATA compartment +SECRET compartment +state signature +EOF immédiat +``` + +Les slots et compartiments transportent leurs IDs numériques de rôle/kind/algorithme, leurs longueurs explicites et leurs bytes directement. + +Bornes et détails complets : + +```text +docs/formats/KSPWALLET_V2.md +``` + +## Codec strict + +Nouveau module : + +```text +crates/ksp-wallet-lib/src/wire_v2.rs +``` + +Surface publique principale : + +```rust +KspWalletEnvelopeV2::parse_binary(...) +KspWalletEnvelopeV2::to_binary_bytes(...) +``` + +Le parser rejette avant toute opération cryptographique : + +```text +> 1 MiB +magic/version incorrects +document_length incohérent +flags réservés +troncature / longueur hors input +role/kind/algorithme inconnu +ordre OWNER/VIEW ou compartiments non canonique +VIEW descriptor incohérent +Argon2 structurellement invalide +salt/ciphertext hors bornes +payload_version non supporté +trailing bytes +``` + +Le codec n'authentifie ni ne déchiffre encore un wallet V2 ; cette composition avec les primitives Wallet existantes est réservée à `pre.016`. + +## Transcripts et AAD V2 + +Nouveau module : + +```text +crates/ksp-wallet-lib/src/transcript_v2.rs +``` + +V2 possède des domain separators distincts : + +```text +KSPWALLET-V2-STATE +KSPWALLET-V2-AAD-OWNER-SLOT +KSPWALLET-V2-AAD-VIEW-SLOT +KSPWALLET-V2-AAD-OWNER-CONTROL +KSPWALLET-V2-AAD-METADATA +KSPWALLET-V2-AAD-SECRET +``` + +La discipline TLV déterministe de V1 est conservée mais les rôles, algorithmes et kinds V2 sont transcriptés sous leurs IDs numériques. Les domains V1/V2 restent séparés pour éviter toute interchangeabilité cryptographique entre versions. + +Le VIEW wrap mutable reste hors du state transcript, comme en V1 ; le descriptor signé lie son identité stable et permet la self-rotation VIEW sans OWNER. + +## Fixture canonique + +Nouvelle fixture structurelle : + +```text +crates/ksp-wallet-lib/tests/fixtures/kspwallet_v2_wire_only.bin +``` + +Mesure : + +```text +V1 JSON wire-only = 2037 octets +V2 binary wire-only = 628 octets +réduction = 1409 octets, environ 69,2 % +``` + +Hash du fixture V2 : + +```text +sha256 = cbacb37326c76e6dde8d614601cd765a8c0fd9744f891d640b8a3cd71cf60a93 +``` + +Cette fixture valide framing/canonicalité ; elle n'est pas revendiquée comme un wallet V2 cryptographiquement authentifié tant que création/open V2 n'est pas matérialisé en `pre.016`. + +## Politique API durable : default distinct de latest + +La documentation fige dès `pre.015` la politique qui sera matérialisée en code dans `pre.016`. + +Deux notions doivent rester distinctes : + +```text +DEFAULT_WALLET_FORMAT +LATEST_SUPPORTED_WALLET_FORMAT +``` + +Après intégration runtime V2 : + +```text +DEFAULT_WALLET_FORMAT = V2 +LATEST_SUPPORTED_WALLET_FORMAT = V2 +``` + +Si V3 existe plus tard : + +```text +DEFAULT_WALLET_FORMAT = V2 # peut rester V2 +LATEST_SUPPORTED_WALLET_FORMAT = V3 +``` + +Une nouvelle version supportée ne change jamais automatiquement le default. + +### Création + +API générique : + +```text +create_wallet_file(...) -> DEFAULT_WALLET_FORMAT, donc V2 +``` + +APIs forcées : + +```text +create_wallet_file_v1(...) +create_wallet_file_v2(...) +create_wallet_file_v3(...) si V3 existe un jour +``` + +Les fonctions `_vN` restent une possibilité explicite pour les applications qui veulent imposer un wire précis. + +### Lecture / inspection + +API générique : + +```text +open_wallet_*_file(...) +inspect_locked_wallet_file(...) +``` + +doit détecter et dispatcher les versions supportées. + +API versionnée : + +```text +open_wallet_*_file_v1(...) +open_wallet_*_file_v2(...) +inspect_locked_wallet_file_v1(...) +inspect_locked_wallet_file_v2(...) +``` + +doit exiger exactement la version indiquée. + +La même règle est appliquée aux autres opérations pour lesquelles la version native du fichier est pertinente. + +## Future V3 / second facteur + +V2 est volontairement **sans second facteur**. + +Si un futur V3 introduit password + 2FA : + +```text +ksp-wallet-lib possède la politique d'autorisation +ksp-wallet-lib décide si un second facteur est requis +ksp-wallet-lib possède challenge et validation cryptographique +WalletOwner est impossible tant que les facteurs exigés ne sont pas satisfaits +Wallet Desk ne décide jamais qu'une preuve 2FA est valide +``` + +Wallet Desk ou un autre client peut néanmoins évoluer pour l'UX interactive : OTP, push/attente, enrollment, recovery, hardware/WebAuthn ou consentement externe. + +`ksp-wallet-lib` doit conserver sa frontière autonome vis-à-vis de Config/Transport/Tauri. Si un fournisseur réseau est nécessaire, une abstraction/provider KSP séparée peut obtenir la preuve tandis que Wallet reste propriétaire de la règle d'autorisation. + +## Nouveau découpage de `0.2.6` + +La documentation/candidate anciennement prévue en `pre.015` est repoussée afin de ne pas mélanger wire, runtime/migration et packaging : + +```text +pre.015 wire binaire V2 + codec canonique +pre.016 APIs génériques/versionnées + création/open V2 + Wallet Desk sur API générique +pre.017 migration explicite V1 -> V2 + persistence/canaris + régression Wallet Desk +pre.018 documentation finale + candidate + stratégie release/bundle/CWD + build Tauri final +rel.001 publication stable v0.2.6 +``` + +Le build Tauri reste interdit avant `pre.018` et devra être l'absolue dernière opération de validation de la candidate. + +## Réalignement de la série + +`pre.014` avait temporairement intercalé le chantier binaire en `0.2.7`. Cette décision est superseded puisque V2 est désormais traité dans `0.2.6`. + +La suite redevient : + +```text +0.2.7 WebSocket Solana standard +0.2.8 Helius LaserStream WebSocket +0.2.9 Yellowstone gRPC standard/provider-neutral +0.2.10 off-chain price transport +0.2.11 price visualization desk + intégration Wallet Desk +0.2.12 interface/wire foundation +0.2.13 program-api foundation +``` + +Le prompt `prompts/012-V0_2_7_START_PROMPT.md` est réaligné sur WebSocket Solana standard. + +## Fichiers ajoutés + +```text +crates/ksp-wallet-lib/src/transcript_v2.rs +crates/ksp-wallet-lib/src/wire_v2.rs +crates/ksp-wallet-lib/tests/fixtures/kspwallet_v2_wire_only.bin +crates/ksp-wallet-lib/unit_tests/transcript_v2.rs +crates/ksp-wallet-lib/unit_tests/wire_v2.rs +docs/formats/KSPWALLET_V2.md +deltas/0.2.6/pre.015.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +ROADMAP.md +crates/ksp-wallet-lib/README.md +crates/ksp-wallet-lib/USAGE.md +crates/ksp-wallet-lib/src/constants.rs +crates/ksp-wallet-lib/src/lib.rs +crates/ksp-wallet-lib/tests/public_api.rs +docs/000-README.md +docs/IDEAS.md +docs/architecture/004-COMPONENT_INVENTORY.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/013-V0_2_6_WALLET_DESK_PLAN.md +docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md +prompts/000-README.md +prompts/011-V0_2_6_START_PROMPT.md +prompts/012-V0_2_7_START_PROMPT.md +``` + +## Fichiers volontairement non modifiés + +```text +CHANGELOG.md +crates/ksp-app-config-desk/* +crates/ksp-app-wallet-desk/* +``` + +Le CHANGELOG reste réservé à la clôture. Les Desks ne changent pas tant que la façade générique/runtime V2 n'est pas prête en `pre.016`. + +## Contrôles assistant + +L'environnement de génération ne fournit pas `cargo`, `rustc` ou `rustfmt`. Les contrôles exécutables disponibles sont donc structurels : + +```text +python3 scripts/audit_rust_workspace_rules.py requis avant livraison +fixture V2 header/length/layout vérification indépendante +fixture V2 sha256 vérification indépendante +comparaison V1/V2 2037 / 628 octets +delta vérifié contre pre.014-fix.001 +``` + +Compilation, Clippy et tests restent autoritatifs côté opérateur. + +## Validation opérateur requise + +Après application du delta : + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-wallet-lib +cargo test -p ksp-app-wallet-desk +cargo test --workspace +``` + +Points attendus : + +```text +V1 reste intégralement vert +nouveaux tests wire_v2/transcript_v2 verts +public API V2 codec visible depuis crate root +aucune régression Wallet Desk +smokes réseau restent ignored sauf opt-in explicite +``` + +**Ne pas exécuter `cargo tauri build` en `pre.015`.** Le build final reste réservé à `pre.018` et doit demeurer l'ultime opération de validation de `0.2.6`. diff --git a/docs/000-README.md b/docs/000-README.md index 9033647..d623e9b 100644 --- a/docs/000-README.md +++ b/docs/000-README.md @@ -1,5 +1,5 @@ - + # Documentation KSP @@ -74,7 +74,7 @@ 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 l’implé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 historique clôturé [`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`, les payloads/create/open en `pre.005`, la persistence en `pre.006`, l'administration/signature en `pre.007` et les adapters transfer en `pre.008`. `pre.009` ferme l'audit adversarial/interoperability/compliance dans [`validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) avant la documentation finale `pre.010` ; `pre.010` finalise [`../crates/ksp-wallet-lib/README.md`](../crates/ksp-wallet-lib/README.md), [`../crates/ksp-wallet-lib/USAGE.md`](../crates/ksp-wallet-lib/USAGE.md), la spec, les graphes et la matrice ; `pre.010-fix.001`–`fix.003` ferment ensuite la mise à niveau Dalek et la normalisation Rust/audit structurel. `0.2.5-rel.001` publie la release stable et [`../prompts/011-V0_2_6_START_PROMPT.md`](../prompts/011-V0_2_6_START_PROMPT.md) ouvre `0.2.6 — Wallet Desk`. Le gate `0.2.6-pre.001` est matérialisé dans [`plans/013-V0_2_6_WALLET_DESK_PLAN.md`](plans/013-V0_2_6_WALLET_DESK_PLAN.md) : il réaudite Config Desk et les APIs finales, retient le gabarit Bootstrap/Font Awesome/DataTables/Select/SimpleBar/resize-observer-polyfill, fixe `std.wallet` avec root global + sous-répertoire de profil, création automatique des répertoires, secrets `KSP_SECRET_WALLET_PASS_*` via Config, inventory/path safety, lifecycle VIEW/OWNER, screen/command/DTO maps et balance HTTP. Le forecast souple est détaillé jusqu’à `pre.015` : `pre.014` est réservé au polish Bootstrap/splashscreen, puis `pre.015` finalise README/USAGE/docs/validation, le prompt `0.2.7` et le build Tauri final. `0.2.6-pre.002` matérialise la crate `ksp-app-wallet-desk` et son shell Tauri splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, `resize-observer-polyfill`, TS-RS et bridge frontend Logging ; `0.2.6-pre.003` ajoute `cfg.std.wallet`/`schema.std.wallet`, le composite concret Wallet Desk, les adapters Config capables de préserver une sélection composite, les profils Wallet `default`/`temporary`/`tests` et la préparation automatique du répertoire global/effectif par l’application. L’inventory `.kspwallet` reste réservé à `pre.004`. +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 l’implé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 historique clôturé [`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`, les payloads/create/open en `pre.005`, la persistence en `pre.006`, l'administration/signature en `pre.007` et les adapters transfer en `pre.008`. `pre.009` ferme l'audit adversarial/interoperability/compliance dans [`validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) avant la documentation finale `pre.010` ; `pre.010` finalise [`../crates/ksp-wallet-lib/README.md`](../crates/ksp-wallet-lib/README.md), [`../crates/ksp-wallet-lib/USAGE.md`](../crates/ksp-wallet-lib/USAGE.md), la spec, les graphes et la matrice ; `pre.010-fix.001`–`fix.003` ferment ensuite la mise à niveau Dalek et la normalisation Rust/audit structurel. `0.2.5-rel.001` publie la release stable et [`../prompts/011-V0_2_6_START_PROMPT.md`](../prompts/011-V0_2_6_START_PROMPT.md) ouvre `0.2.6 — Wallet Desk`. Le gate `0.2.6-pre.001` est matérialisé dans [`plans/013-V0_2_6_WALLET_DESK_PLAN.md`](plans/013-V0_2_6_WALLET_DESK_PLAN.md) : il réaudite Config Desk et les APIs finales, retient le gabarit Bootstrap/Font Awesome/DataTables/Select/SimpleBar/resize-observer-polyfill, fixe `std.wallet` avec root global + sous-répertoire de profil, création automatique des répertoires, secrets `KSP_SECRET_WALLET_PASS_*` via Config, inventory/path safety, lifecycle VIEW/OWNER, screen/command/DTO maps et balance HTTP. Le forecast a été étendu après `pre.014` : `pre.015` fige le wire binaire `.kspwallet` V2, `pre.016` ajoute les APIs génériques/versionnées et la création/ouverture V2, `pre.017` ferme migration/canaris/régression Wallet Desk, puis `pre.018` finalise documentation, prompt `0.2.7` WebSocket et build Tauri. `0.2.6-pre.002` matérialise la crate `ksp-app-wallet-desk` et son shell Tauri splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, `resize-observer-polyfill`, TS-RS et bridge frontend Logging ; `0.2.6-pre.003` ajoute `cfg.std.wallet`/`schema.std.wallet`, le composite concret Wallet Desk, les adapters Config capables de préserver une sélection composite, les profils Wallet `default`/`temporary`/`tests` et la préparation automatique du répertoire global/effectif par l’application. L’inventory `.kspwallet` reste réservé à `pre.004`. ## Spécifications de formats diff --git a/docs/IDEAS.md b/docs/IDEAS.md index 3a1c88d..9fcbb4f 100644 --- a/docs/IDEAS.md +++ b/docs/IDEAS.md @@ -1,5 +1,5 @@ - + # Idées à explorer @@ -202,13 +202,13 @@ Formats/cibles à inventorier et prioriser selon usage réel : Chaque format doit être étudié côté sécurité, round-trip, secret/public, dépendances et compatibilité avant engagement. -### Conteneur binaire `.kspwallet` et formats logiques futurs +### `.kspwallet` V2 binaire et formats futurs -**Status :** Retenu pour audit en `0.2.7` / facteurs futurs à explorer +**Status :** V2 retenu et matérialisé en `0.2.6-pre.015` / facteurs futurs à explorer -Le JSON V1 actuel est un format d’interop lisible. Base64 seul n’apporte aucune sécurité et resterait un texte trivialement décodable avec environ un tiers de surcharge. `0.2.7` doit donc étudier un **conteneur de persistence binaire versionné** séparé du `format_version` logique/cryptographique : magic/framing explicite, lecture rétrocompatible du JSON V1 historique, écriture binaire par défaut après validation, migration explicite et test vectors. Le choix du codec/framing exact est audité avant engagement ; il ne doit pas casser les transcripts/AAD, VIEW/OWNER, keypair ou import/export. +Le JSON V1 actuel reste le format historique stable et lisible. Base64 seul n’apporte aucune sécurité et resterait un texte trivialement décodable avec environ un tiers de surcharge. La décision initialement envisagée pour `0.2.7` a été ramenée dans `0.2.6` : `pre.015` définit un **wire binaire V2 KSP** avec magic/framing explicite, entiers big-endian, identifiants numériques stables, longueurs bornées et lecture/écriture canonique stricte. V1 reste supporté sans réinterprétation ; la façade de lecture multi-version, la création V2 et la politique `DEFAULT_WALLET_FORMAT = V2` arrivent en `pre.016`, puis la migration explicite V1 -> V2 en `pre.017`. Le V2 ne modifie pas à lui seul les garanties cryptographiques de VIEW/OWNER, keypair ou import/export. -Un futur `format_version >= 2` pourra introduire d’autres modèles d’autorisation, notamment password + facteur supplémentaire. `ksp-wallet-lib` restera propriétaire du format, des challenges et de la vérification, mais toute interaction réelle (OTP, enrollment/recovery, hardware/WebAuthn, validation distante) exigera une évolution de Wallet Desk ou du client concerné. Un seed TOTP stocké uniquement dans le même fichier que le wallet ne doit pas être présenté automatiquement comme un second facteur indépendant contre un attaquant possédant ce fichier. +V2 est désormais réservé au wire binaire KSP sans second facteur. Un futur V3 pourra introduire d’autres modèles d’autorisation, notamment password + facteur supplémentaire. `ksp-wallet-lib` restera propriétaire du format, des challenges et de la vérification, mais toute interaction réelle (OTP, enrollment/recovery, hardware/WebAuthn, validation distante) exigera une évolution de Wallet Desk ou du client concerné. Un seed TOTP stocké uniquement dans le même fichier que le wallet ne doit pas être présenté automatiquement comme un second facteur indépendant contre un attaquant possédant ce fichier. ## Pipelines diff --git a/docs/architecture/004-COMPONENT_INVENTORY.md b/docs/architecture/004-COMPONENT_INVENTORY.md index ab7b095..f96cd97 100644 --- a/docs/architecture/004-COMPONENT_INVENTORY.md +++ b/docs/architecture/004-COMPONENT_INVENTORY.md @@ -1,5 +1,5 @@ - + # Inventaire initial des composants KSP @@ -26,14 +26,14 @@ Ce document maintient l'inventaire synthétique des composants retenus ou presse | On-chain HTTP | `ksp-onchain-transport-lib` | lib | Stable | `0.2.1`–`0.2.4` | HTTP standard complet : 52/52 current + 14/14 historical | | Wallet | `ksp-wallet-lib` | lib | Stable | `0.2.5` | `.kspwallet`, VIEW/OWNER, secrets, signature, import/export | | Wallet Desk | `ksp-app-wallet-desk` | app | Retenu | `0.2.6` | Wallet + Config composite + HTTP/balance | -| Wallet persistence | `ksp-wallet-lib` | lib | Retenu | `0.2.7` | conteneur binaire rétrocompatible autour du payload V1 | -| Standard WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.8` | WebSocket Solana complet, sessions/subscriptions | -| Helius WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.9` | LaserStream WebSocket comme extension du moteur standard | -| Yellowstone | `ksp-onchain-transport-lib` | lib | Pressenti | `0.2.10` | client gRPC standard/provider-neutral | -| Off-chain price | `ksp-offchain-transport-lib` | lib | Retenu | `0.2.11` | première abstraction/provider de prix SOL/USD, SOL/EUR | -| Price Desk | nom à fixer | app | Retenu | `0.2.12` | visualisation/validation des prix + intégration Wallet Desk | -| Wire | `ksp-interface-lib` | lib | Retenu | `0.2.13` | façade wire officielle + API publique wire | -| Program API | `ksp-program-api` | API | Retenu | `0.2.14` | contrats extensibles Program | +| Wallet V2 | `ksp-wallet-lib` | lib | En cours | `0.2.6` | wire binaire V2 + API default/versionnée + migration | +| Standard WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.7` | WebSocket Solana complet, sessions/subscriptions | +| Helius WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.8` | LaserStream WebSocket comme extension du moteur standard | +| Yellowstone | `ksp-onchain-transport-lib` | lib | Pressenti | `0.2.9` | client gRPC standard/provider-neutral | +| Off-chain price | `ksp-offchain-transport-lib` | lib | Retenu | `0.2.10` | première abstraction/provider de prix SOL/USD, SOL/EUR | +| Price Desk | nom à fixer | app | Retenu | `0.2.11` | visualisation/validation des prix + intégration Wallet Desk | +| Wire | `ksp-interface-lib` | lib | Retenu | `0.2.12` | façade wire officielle + API publique wire | +| Program API | `ksp-program-api` | API | Retenu | `0.2.13` | contrats extensibles Program | | Program impl. | `ksp-program-lib` | lib | Retenu | vertical slices ultérieurs | implementations Program officielles | | Program extension | `ksp-program--lib` | lib externe | À la demande | dès besoin | implementation externe de `ksp-program-api` | | Store API | `ksp-store-api` | API | Retenu | `0.3.1` | contrats persistence backend-agnostic, RAW d'abord | diff --git a/docs/formats/000-README.md b/docs/formats/000-README.md index ecc7f82..fb577b0 100644 --- a/docs/formats/000-README.md +++ b/docs/formats/000-README.md @@ -1,5 +1,5 @@ - + # Formats KSP @@ -10,3 +10,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 l'enveloppe/wire et les transcripts/AAD, `pre.004` ajoute Argon2id/XChaCha20-Poly1305/CSPRNG OS, `pre.005` fixe les payloads plaintext, le profil de création KSP calibré, l'autorité Ed25519 OWNER et le vecteur complet, `pre.006`–`pre.008` matérialisent persistence/administration/transfert, `pre.009` ferme l'audit adversarial/interoperabilité/compliance et `pre.010` synchronise la documentation finale sans modifier le wire V1. +- [`KSPWALLET_V2.md`](KSPWALLET_V2.md) — wire binaire natif V2 introduit par `0.2.6-pre.015` : framing canonique KSP, IDs numériques, longueurs big-endian, aucun Base64/compression, domains/transcripts V2 distincts et politique `default != latest`. diff --git a/docs/formats/KSPWALLET_V1.md b/docs/formats/KSPWALLET_V1.md index 763e98a..8ed3598 100644 --- a/docs/formats/KSPWALLET_V1.md +++ b/docs/formats/KSPWALLET_V1.md @@ -1,5 +1,5 @@ - + # `.kspwallet` V1 — spécification du format natif Wallet KSP @@ -22,6 +22,8 @@ règles unknown-field / unknown-version `0.2.5-pre.004` ajoute les primitives KDF/AEAD normatives et un premier vecteur cryptographique public. `0.2.5-pre.005` fixe 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. `0.2.5-pre.006` matérialise la persistence filesystem bornée et la création no-clobber. `0.2.5-pre.007` matérialise la signature Solana OWNER, l'administration des metadata, les rotations OWNER/VIEW, la révocation forte VIEW et leur remplacement filesystem capability-bound. `0.2.5-pre.008` matérialise les adapters Solana CLI JSON et Base58 complet, leur inspection sûre, l'import no-clobber vers un nouveau `.kspwallet` et l'export secret OWNER explicite. `pre.009` ferme l'audit adversarial/interoperability/compliance et `pre.010` synchronise la documentation de clôture sans modifier le wire ni les primitives. Après publication stable de V1, toute évolution qui modifie un élément déclaré **figé** par cette spécification doit être explicitement tracée ; une incompatibilité de wire exige un nouveau `format_version`. +Depuis `0.2.6-pre.015`, V1 reste explicitement supporté comme format historique stable tandis que V2 définit le nouveau wire binaire. Les APIs versionnées V1 sont conservées et une lecture générique future doit auto-détecter V1/V2 sans migration implicite. + 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`. ## 2. Modèle de confiance V1 diff --git a/docs/formats/KSPWALLET_V2.md b/docs/formats/KSPWALLET_V2.md new file mode 100644 index 0000000..8e9dcef --- /dev/null +++ b/docs/formats/KSPWALLET_V2.md @@ -0,0 +1,441 @@ + + + +# `.kspwallet` V2 — spécification du wire binaire natif KSP + +## 1. Statut et objectif + +Ce document est l'autorité normative du **wire `.kspwallet` `format_version = 2`** introduit par `0.2.6-pre.015`. + +V2 remplace l'enveloppe JSON/Base64url de V1 par un framing binaire KSP canonique. L'objectif est : + +```text +réduire fortement la taille persistée +supprimer l'encodage Base64 des champs déjà binaires +éviter qu'un .kspwallet nouvellement créé soit un document JSON lisible comme tel +conserver un format documenté et implémentable hors Rust +préserver les frontières cryptographiques Wallet +``` + +Le caractère binaire **n'ajoute aucune propriété cryptographique**. La confidentialité et l'authenticité continuent de dépendre d'Argon2id, XChaCha20-Poly1305 et Ed25519, jamais de l'absence de JSON. + +`0.2.6-pre.015` fige la grammaire binaire, les identifiants numériques, les bornes structurelles et les transcripts/AAD V2. La création/ouverture/persistence V2, l'auto-détection V1/V2 et le basculement de l'API générique vers V2 sont matérialisés dans les tranches suivantes de `0.2.6`. + +V1 reste un format stable supporté. V2 ne réinterprète jamais un document V1 avec sa propre grammaire. + +## 2. Politique de version et API + +Les notions suivantes sont distinctes : + +```text +DEFAULT_WALLET_FORMAT +LATEST_SUPPORTED_WALLET_FORMAT +``` + +À partir de l'intégration V2 : + +```text +DEFAULT_WALLET_FORMAT = V2 +LATEST_SUPPORTED_WALLET_FORMAT = V2 +``` + +Si un futur V3 apparaît, par exemple pour un modèle d'autorisation avec second facteur, `LATEST_SUPPORTED_WALLET_FORMAT` pourra devenir V3 tandis que `DEFAULT_WALLET_FORMAT` pourra **rester V2**. Le default ne suit jamais automatiquement la dernière version. + +La politique d'API retenue est : + +```text +create_wallet_file(...) -> format default explicitement choisi, V2 +create_wallet_file_v1(...) -> force V1 +create_wallet_file_v2(...) -> force V2 +create_wallet_file_v3(...) -> force V3 si V3 existe un jour + +open_wallet_*_file(...) -> détecte puis dispatch les versions supportées +open_wallet_*_file_v1(...) -> exige V1 +open_wallet_*_file_v2(...) -> exige V2 +open_wallet_*_file_v3(...) -> exige V3 si V3 existe un jour +``` + +Le même principe s'applique aux opérations dont la version de fichier est pertinente, notamment inspection et import natif. + +## 3. Encodage général + +Un document V2 est : + +```text +binaire +maximum 1 048 576 octets +entiers multi-octets = unsigned big-endian +ordre des champs = normatif +aucun padding implicite +aucune Base64 +aucune compression +aucun trailing byte +``` + +Les longueurs sont exprimées en **octets**. + +Une implémentation conforme doit vérifier les bornes **avant** toute allocation dépendant d'une longueur reçue. Le document entier est déjà borné par `1 048 576` octets avant parsing. + +## 4. Header V2 + +Le début de fichier est strictement : + +| Ordre | Champ | Taille | Valeur / règle | +|------:|---------------------------|-----------------:|------------------------------------------------| +| 1 | `magic` | 9 | ASCII exact `KSPWALLET` | +| 2 | `format_version` | 2 | `0x0002` | +| 3 | `document_length` | 4 | longueur totale exacte du fichier | +| 4 | `flags` | 2 | bit 0 = VIEW activé ; tous les autres bits = 0 | +| 5 | `owner_auth_public_key` | 32 | clé publique Ed25519 OWNER | +| 6 | `view_descriptor.slot_id` | 16 conditionnels | présent uniquement si `flags & 0x0001 != 0` | + +Offsets fixes avant le descripteur conditionnel : + +```text +0x0000..0x0008 magic +0x0009..0x000A format_version +0x000B..0x000E document_length +0x000F..0x0010 flags +0x0011..0x0030 owner_auth_public_key +``` + +Pour un wallet VIEW-enabled, `view_descriptor.slot_id` occupe ensuite `0x0031..0x0040`. + +### 4.1 Flags + +```text +0x0001 VIEW_ENABLED +0xFFFE réservé, doit être zéro +``` + +Tout bit réservé non nul rend le document invalide. V2 n'emploie pas les bits réservés comme mécanisme d'extension silencieuse ; une modification incompatible exige un nouveau format. + +## 5. Ordre canonique du body + +Après le header : + +```text +OWNER key slot obligatoire +VIEW key slot présent seulement si VIEW_ENABLED +OWNER-CONTROL encrypted compartment obligatoire +METADATA encrypted compartment obligatoire +SECRET encrypted compartment obligatoire +state signature obligatoire +EOF immédiat +``` + +Aucun compteur de slots ou de compartiments n'est nécessaire : leur cardinalité et leur ordre sont déterminés par V2. + +## 6. Key slot V2 + +Chaque key slot est encodé ainsi : + +| Champ | Taille | Valeur / règle | +|--------------------------|---------:|-----------------------------------------| +| `role` | 1 | `0x01` OWNER, `0x02` VIEW | +| `slot_id` | 16 | identifiant binaire exact | +| `kdf_algorithm` | 1 | `0x01` Argon2id | +| `kdf_version` | 4 | `19` | +| `memory_kib` | 4 | `1..1 048 576`, et `>= parallelism * 8` | +| `iterations` | 4 | `1..64` | +| `parallelism` | 4 | `1..64` | +| `salt_length` | 1 | `16..64` | +| `salt` | variable | exactement `salt_length` octets | +| `wrap_algorithm` | 1 | `0x01` XChaCha20-Poly1305 | +| `wrap_nonce` | 24 | nonce exact | +| `wrap_ciphertext_length` | 2 | `16..4096` | +| `wrap_ciphertext` | variable | exactement la longueur déclarée | + +Ordre obligatoire : + +```text +premier slot = OWNER +second slot = VIEW seulement si VIEW_ENABLED +``` + +Si VIEW est activé : + +```text +header.view_descriptor.slot_id == VIEW key slot.slot_id +``` + +Toute divergence est invalide avant KDF/déchiffrement. + +## 7. Compartiments V2 + +Chaque compartiment est encodé : + +| Champ | Taille | Valeur / règle | +|---------------------|---------:|------------------------------------------------------| +| `kind` | 1 | `0x01` OWNER-CONTROL, `0x02` METADATA, `0x03` SECRET | +| `payload_version` | 4 | `1` pour le profil initial V2 | +| `algorithm` | 1 | `0x01` XChaCha20-Poly1305 | +| `nonce` | 24 | nonce exact | +| `ciphertext_length` | 4 | longueur exacte | +| `ciphertext` | variable | ciphertext + tag Poly1305 | + +Bornes initiales : + +```text +OWNER-CONTROL : 16..4096 octets +METADATA : 16..65552 octets +SECRET : 16..4096 octets +``` + +L'ordre exact est OWNER-CONTROL, METADATA, SECRET. Le `kind` encodé doit correspondre à la position attendue ; il n'autorise pas un réordonnancement. + +Les payloads plaintext V2 conservent le modèle fonctionnel établi en V1 pour cette évolution : owner-control, metadata et secret Solana restent des compartiments distincts. Une future modification incompatible de leur sémantique exige une version de format explicite. + +## 8. Signature d'état + +La fin du document est : + +| Champ | Taille | Valeur / règle | +|-----------------------------|-------:|---------------------------| +| `state_signature.algorithm` | 1 | `0x01` Ed25519 | +| `state_signature.signature` | 64 | signature detached exacte | + +Aucun octet ne peut suivre ces 65 octets. + +## 9. Identifiants numériques figés + +### 9.1 Rôles + +```text +0x01 OWNER +0x02 VIEW +``` + +### 9.2 Algorithmes + +```text +KDF +0x01 Argon2id + +AEAD +0x01 XChaCha20-Poly1305 + +state signature +0x01 Ed25519 +``` + +### 9.3 Compartiments + +```text +0x01 OWNER-CONTROL +0x02 METADATA +0x03 SECRET +``` + +`0x00` est invalide dans ces espaces. Les autres valeurs sont réservées et rejetées par V2 tant qu'elles ne sont pas explicitement normalisées par une évolution compatible documentée ; aucune valeur inconnue n'est devinée. + +## 10. Canonicalité et rejet + +Le parser V2 doit rejeter avant crypto : + +```text +fichier > 1 MiB +magic incorrect +format_version != 2 +document_length différent de la taille reçue +flag réservé non nul +champ tronqué +longueur qui dépasse le reste du document +role/algorithm/kind inconnu +ordre OWNER/VIEW invalide +VIEW descriptor et VIEW slot incohérents +Argon2 hors bornes +salt hors bornes +ciphertext hors bornes +payload_version non supporté +trailing bytes +``` + +Il n'existe qu'un encodage canonique d'un même état sémantique V2 : mêmes champs, même ordre, mêmes largeurs, même endianness et aucune donnée ignorée. + +## 11. Transcripts et AAD V2 + +V2 possède ses propres domaines ; V1 et V2 ne partagent jamais un domain separator : + +```text +KSPWALLET-V2-STATE +KSPWALLET-V2-AAD-OWNER-SLOT +KSPWALLET-V2-AAD-VIEW-SLOT +KSPWALLET-V2-AAD-OWNER-CONTROL +KSPWALLET-V2-AAD-METADATA +KSPWALLET-V2-AAD-SECRET +``` + +Le transcript/AAD V2 conserve la discipline TLV déterministe de V1 : + +```text +domain || 0x00 +puis pour chaque champ : + tag:u16 big-endian + length:u64 big-endian + value:length bytes +``` + +Les tags restent alignés avec les familles V1 : + +```text +0x0001 magic +0x0002 format_version +0x0003 owner_auth_public_key +0x0010 view_enabled +0x0011 view_role +0x0012 view_slot_id +0x0100 slot_id +0x0101 slot_role +0x0102 kdf_algorithm +0x0103 kdf_version +0x0104 kdf_memory_kib +0x0105 kdf_iterations +0x0106 kdf_parallelism +0x0107 kdf_salt +0x0108 wrap_algorithm +0x0109 wrap_nonce +0x010A wrap_ciphertext +0x0200 compartment_kind +0x0201 compartment_version +0x0202 compartment_algorithm +0x0203 compartment_nonce +0x0204 compartment_ciphertext +0x0500 state_signature_algorithm +``` + +Différence normative V2 : les rôles/algorithmes/kinds sont transcriptés sous leur **ID numérique d'un octet**, et `format_version = 2` est transcripté en `u32` big-endian. Les domaines distincts empêchent qu'un transcript V1 et un transcript V2 soient interchangeables. + +### 11.1 State transcript + +Le state transcript contient : + +```text +common +view descriptor +OWNER slot avec wrap nonce+ciphertext +OWNER-CONTROL avec nonce+ciphertext +METADATA avec nonce+ciphertext +SECRET avec nonce+ciphertext +state signature algorithm +``` + +Le VIEW wrap mutable n'est pas ajouté au state transcript, selon le modèle d'autorisation VIEW déjà retenu : son identité stable reste liée par le descriptor signé tandis que son credential peut être self-rotaté sans OWNER. + +### 11.2 Slot AAD + +Le slot AAD contient : + +```text +common +slot_id +role +KDF algorithm/version/parameters/salt +wrap algorithm +``` + +Le wrap nonce/ciphertext n'est pas inclus dans son propre AAD. + +### 11.3 Compartment AAD + +Le compartment AAD contient : + +```text +common +kind +payload_version +algorithm +``` + +Le nonce/ciphertext n'est pas inclus dans son propre AAD. + +## 12. V1 et V2 + +V1 et V2 sont deux formats explicites : + +| Propriété | V1 | V2 | +|--------------------------|------------------------------------|---------------| +| enveloppe | JSON UTF-8 | binaire KSP | +| champs binaires | Base64url no-pad | bytes directs | +| version | `1` | `2` | +| canonicalité | sémantique JSON + Base64 canonique | byte-exact | +| taille fixture wire-only | 2037 octets | 628 octets | +| Argon2id | oui | oui | +| XChaCha20-Poly1305 | oui | oui | +| Ed25519 OWNER state | oui | oui | +| VIEW/OWNER | oui | oui | + +La réduction mesurée sur le fixture structurel de référence est : + +```text +2037 -> 628 octets +-1409 octets +~69,2 % +``` + +Cette mesure n'est pas une promesse de ratio constant : la taille dépend notamment des metadata chiffrées. + +## 13. Fixture structurelle + +Fixture canonique de `pre.015` : + +```text +crates/ksp-wallet-lib/tests/fixtures/kspwallet_v2_wire_only.bin +size = 628 +sha256 = cbacb37326c76e6dde8d614601cd765a8c0fd9744f891d640b8a3cd71cf60a93 +``` + +Préfixe hexadécimal : + +```text +4b535057414c4c4554 0002 00000274 0001 +000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f +202122232425262728292a2b2c2d2e2f +``` + +Lecture : + +```text +4b535057414c4c4554 = "KSPWALLET" +0002 = format_version V2 +00000274 = 628 octets +0001 = VIEW_ENABLED +``` + +La fixture `wire_only` vérifie framing/canonicalité ; elle n'est pas présentée comme une signature cryptographique V2 valide tant que la création/open V2 n'est pas intégrée par la tranche suivante. + +## 14. Compatibilité et migration + +V2 n'autorise aucune migration implicite à l'ouverture. + +Politique prévue : + +```text +lecture générique V1/V2 oui +création générique V2 +création V1 forcée oui +création V2 forcée oui +réécriture V1 -> V2 cachée non +migration V1 -> V2 opération explicite +``` + +Une migration doit authentifier le wallet source avec la capability requise, produire un nouveau document V2 valide et respecter la persistence no-clobber/atomique. Une simple transcodification non authentifiée des bytes V1 n'est pas suffisante puisque V2 possède ses propres domains/transcripts. + +## 15. Future V3 / second facteur + +V2 ne réalise aucun second facteur. + +Si un futur V3 ajoute une autorisation dépendant d'un facteur externe : + +```text +ksp-wallet-lib possède la règle qui exige le facteur +ksp-wallet-lib possède le challenge et la validation cryptographique +aucun WalletOwner ne peut être obtenu si le facteur requis n'est pas satisfait +Wallet Desk ne décide jamais que le second facteur est valide +``` + +Une UI comme Wallet Desk peut néanmoins devoir évoluer pour orchestrer l'expérience : attente de confirmation, saisie OTP, enrollment, recovery, hardware/WebAuthn ou consentement externe. + +Le réseau ou le fournisseur externe ne doit pas forcer `ksp-wallet-lib` à dépendre directement de Config/Transport/Tauri. Une abstraction/provider KSP séparée peut fournir la preuve à Wallet tandis que Wallet reste propriétaire de la politique d'autorisation. + +L'arrivée d'un V3 ne change pas automatiquement le default : V2 peut rester `DEFAULT_WALLET_FORMAT` aussi longtemps que KSP le décide explicitement. diff --git a/docs/plans/000-README.md b/docs/plans/000-README.md index bfc7c05..df576ca 100644 --- a/docs/plans/000-README.md +++ b/docs/plans/000-README.md @@ -1,5 +1,5 @@ - + # Plans KSP @@ -21,7 +21,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou - [`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 historique clôturé de la release stable `0.2.5 — Wallet foundation`, ouvert par `pre.001`, livré jusqu’à `pre.010`, renforcé par `pre.010-fix.001`–`fix.003` pour Dalek 3 et la normalisation Rust/audit structurel, puis publié par `rel.001`; il couvre `.kspwallet` V1, VIEW/OWNER, crypto, persistence, administration, transfer et compliance. -- [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan actif de `0.2.6 — Wallet Desk`, ouvert par `pre.001`; `pre.002` matérialise le shell Tauri et `pre.003` la composition `std.wallet`/composite ainsi que la préparation des répertoires. Le plan fixe aussi secrets `KSP_SECRET_WALLET_PASS_*`, inventory DataTables/path safety, lifecycle VIEW/OWNER, screen/command/DTO maps, balance HTTP, administration retenue et forecast souple détaillé jusqu’à `pre.015`, avec `pre.014` réservé au polish Bootstrap/splashscreen puis `pre.015` à la documentation finale, au prompt `0.2.7` et au build Tauri final. +- [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan actif de `0.2.6 — Wallet Desk`, ouvert par `pre.001`; `pre.002` matérialise le shell Tauri et `pre.003` la composition `std.wallet`/composite ainsi que la préparation des répertoires. Le plan fixe aussi secrets `KSP_SECRET_WALLET_PASS_*`, inventory DataTables/path safety, lifecycle VIEW/OWNER, screen/command/DTO maps, balance HTTP, administration retenue et forecast étendu jusqu’à `pre.018` : `pre.014` polish Bootstrap/splashscreen, `pre.015` wire binaire `.kspwallet` V2, `pre.016` APIs génériques/versionnées + V2, `pre.017` migration/canaris, puis `pre.018` documentation candidate et build Tauri final. Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre. diff --git a/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md b/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md index dd1a7df..31a4220 100644 --- a/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md +++ b/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md @@ -1,5 +1,5 @@ - + # Séquence des releases fonctionnelles KSP @@ -361,14 +361,13 @@ Par défaut : 0.2.4 HTTP Blocks + Economics + compliance complète 0.2.5 wallet foundation (.kspwallet) 0.2.6 Wallet Desk -0.2.7 .kspwallet binary persistence container -0.2.8 standard Solana WebSocket -0.2.9 Helius LaserStream WebSocket -0.2.10 Yellowstone gRPC standard foundation -0.2.11 off-chain price transport -0.2.12 price visualization desk + intégration prix dans Wallet Desk -0.2.13 interface/wire foundation -0.2.14 program-api foundation +0.2.7 standard Solana WebSocket +0.2.8 Helius LaserStream WebSocket +0.2.9 Yellowstone gRPC standard foundation +0.2.10 off-chain price transport +0.2.11 price visualization desk + intégration prix dans Wallet Desk +0.2.12 interface/wire foundation +0.2.13 program-api foundation ``` `0.2.1-pre.001` a appliqué le gate de sizing et refusé le scope HTTP monolithique initial : l'inventaire du 2026-08-17 contient 52 méthodes courantes et 14 méthodes Deprecated historiques. Ce premier delta avait réparti la couverture typée sur `0.2.1`–`0.2.6`. `0.2.1-pre.001-fix.001` recalibre ensuite les 48 méthodes restantes sur trois releases complémentaires `0.2.2`–`0.2.4`, soit trois sessions nominales au maximum si chaque release utilise sa session complète. Si une release se clôt plus vite que prévu, la même session peut enchaîner la suivante après clôture complète de la précédente et nouveau gate de sizing positif. Un Wallet Desk utile doit pouvoir lire le solde du wallet : `getBalance` fait donc partie des quatre canaris de la foundation `0.2.1`, avant Wallet. Les transports live arrivent ensuite ; Interface/Program restent préparés avant les couches de données décodées. @@ -443,29 +442,26 @@ pre.011 disable/recreate VIEW fort pre.012 export OWNER fichier CLI JSON/Base58 pre.013 intégration, compliance, sécurité et smoke pre.014 polish du gabarit Bootstrap et du splashscreen -pre.015 documentation finale, validations, prompt 0.2.7 et cargo tauri build en dernière opération +pre.015 .kspwallet V2 wire binaire + codec canonique +pre.016 APIs génériques/versionnées + création/ouverture V2 +pre.017 migration V1 -> V2 + persistence/canaris + régression Wallet Desk +pre.018 documentation finale, validations, prompt 0.2.7 et cargo tauri build en dernière opération rel.001 publication stable 0.2.6 ``` La tranche `pre.014` est réservée aux défauts visuels/templating observés en usage réel, notamment les scrollbars occasionnelles du splashscreen ; son contenu précis sera borné à partir du retour opérateur avant implémentation. `ROADMAP.md` reste synthétique et `CHANGELOG.md` n'est synchronisé qu'à la phase documentaire finale. -## `0.2.7` — `.kspwallet` binary persistence container - -Mission : faire évoluer uniquement la couche de persistence de `ksp-wallet-lib` afin qu’un `.kspwallet` nouvellement écrit ne soit plus un document JSON directement lisible dans un éditeur texte ordinaire, sans prétendre ajouter de sécurité cryptographique par simple obfuscation. - -Le gate `pre.001` doit comparer framing binaire custom, codecs binaires stables et éventuelle compression, puis fixer un conteneur explicitement versionné. Base64 seul est exclu comme solution : il reste textuel, trivialement réversible et augmente la taille. Le conteneur et le `format_version` logique/cryptographique sont séparés afin de conserver V1 pour les semantics VIEW/OWNER actuelles et de réserver les futurs formats logiques à de vraies évolutions d’autorisation. La lecture rétrocompatible des JSON V1 historiques et les migrations/test vectors sont obligatoires. Wallet Desk ne doit pas être modifié fonctionnellement ; ses tests/smokes sont rejoués comme preuve consommateur. - -## `0.2.8` — WebSocket Solana standard +## `0.2.7` — WebSocket Solana standard Mission : couvrir la surface WebSocket standard officielle ciblée. Une URL peut avoir plusieurs sessions physiques ; une session peut avoir plusieurs subscriptions. Un pool automatique de sessions est reporté jusqu'à besoin concret. -## `0.2.9` — Helius LaserStream WebSocket +## `0.2.8` — Helius LaserStream WebSocket Mission : étendre le moteur WebSocket standard avec les opérations/filtres/capabilities Helius ciblés sans copier le client. -## `0.2.10` — Yellowstone gRPC standard +## `0.2.9` — Yellowstone gRPC standard Mission : introduire un backend Yellowstone standard/provider-neutral. @@ -473,21 +469,21 @@ Le `pre.001` est un gate de sizing : inventorier toute la surface normative cibl Les profiles/adapters Helius/Triton/ERPC/Chainstack/Shyft sont reportés après les priorités fondatrices. -## `0.2.11` / `0.2.12` — Off-chain price + app +## `0.2.10` / `0.2.11` — Off-chain price + app -`0.2.11` introduit `ksp-offchain-transport-lib` avec au minimum SOL/USD et SOL/EUR via une abstraction indépendante du premier provider. +`0.2.10` introduit `ksp-offchain-transport-lib` avec au minimum SOL/USD et SOL/EUR via une abstraction indépendante du premier provider. -`0.2.12` ajoute une petite application desk de visualisation/validation. Après stabilisation de cette application spécialisée, la même release doit intégrer la capacité de prix offchain dans `ksp-app-wallet-desk` sans dupliquer la récupération/normalisation appartenant au composant spécialisé. +`0.2.11` ajoute une petite application desk de visualisation/validation. Après stabilisation de cette application spécialisée, la même release doit intégrer la capacité de prix offchain dans `ksp-app-wallet-desk` sans dupliquer la récupération/normalisation appartenant au composant spécialisé. Metadata HTTP/IPFS/Arweave viendra au premier besoin Metadata réel. -## `0.2.13` — Interface foundation +## `0.2.12` — Interface foundation `ksp-interface-lib` devient la façade wire officielle et expose une API publique wire utilisable par les implementations officielles et externes. Aucune `ksp-interface-api` séparée n'est retenue pour l'instant. -## `0.2.14` — Program API foundation +## `0.2.13` — Program API foundation Introduire `ksp-program-api`, sans suffixe `-lib`, comme contrat d'extension Program. diff --git a/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md b/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md index 1cefcd5..6e9abac 100644 --- a/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md +++ b/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.2.6` — Wallet Desk @@ -771,32 +771,32 @@ outil de logging contrôlé ## 12. Command map -| Command | Entrée frontend | Sortie sûre | Délégation | -|----------------------------------------------|--------------------|-------------------------------------|---------------------------------| -| `get_runtime_status` | — | `WalletRuntimeStatusDto` | Config/app state | -| `list_wallets` | — | `Vec` | app filesystem + Wallet inspect | -| `refresh_wallets` | — | inventory DTO | app filesystem + Wallet inspect | -| `select_wallet` | wallet id | `LockedWalletDto` | app resolver + Wallet inspect | -| `deselect_wallet` | — | session DTO | app state | -| `create_wallet` | create request | `WalletAuthorizedDto` OWNER | Wallet create file | -| `unlock_wallet_view` | password | authorized VIEW | Wallet open VIEW | -| `unlock_wallet_owner` | password | authorized OWNER | Wallet open OWNER | -| `unlock_wallet_view_with_configured_secret` | — | authorized VIEW / operation result | Config env + Wallet open VIEW | -| `unlock_wallet_owner_with_configured_secret` | — | authorized OWNER / operation result | Config env + Wallet open OWNER | -| `lock_wallet` | — | `LockedWalletDto` | drop handle + inspect | -| `refresh_wallet_balance` | — | `WalletBalanceDto` | Transport getBalance | -| `inspect_import_source` | format only | transfer inspection DTO / cancel | Rust native picker + Wallet inspect | -| `import_wallet` | import request | authorized OWNER | staged bytes + Wallet import | -| `update_wallet_alias` | alias mutation | authorized DTO | WalletOwner | -| `add_wallet_note` | note create | authorized DTO | WalletOwner | -| `update_wallet_note` | note update | authorized DTO | WalletOwner | -| `delete_wallet_note` | note id | authorized DTO | WalletOwner | -| `rotate_owner_password` | rotation request | operation result | WalletOwner | -| `rotate_view_password` | rotation request | operation result | WalletOwner | -| `disable_view` | confirmation | authorized DTO | WalletOwner | -| `recreate_view` | new VIEW password | authorized DTO | WalletOwner | -| `export_wallet_owner` | format + save path | operation result | Wallet export file | -| `emit_frontend_log` | redacted payload | `()` | Logging facade | +| Command | Entrée frontend | Sortie sûre | Délégation | +|----------------------------------------------|--------------------|-------------------------------------|-------------------------------------| +| `get_runtime_status` | — | `WalletRuntimeStatusDto` | Config/app state | +| `list_wallets` | — | `Vec` | app filesystem + Wallet inspect | +| `refresh_wallets` | — | inventory DTO | app filesystem + Wallet inspect | +| `select_wallet` | wallet id | `LockedWalletDto` | app resolver + Wallet inspect | +| `deselect_wallet` | — | session DTO | app state | +| `create_wallet` | create request | `WalletAuthorizedDto` OWNER | Wallet create file | +| `unlock_wallet_view` | password | authorized VIEW | Wallet open VIEW | +| `unlock_wallet_owner` | password | authorized OWNER | Wallet open OWNER | +| `unlock_wallet_view_with_configured_secret` | — | authorized VIEW / operation result | Config env + Wallet open VIEW | +| `unlock_wallet_owner_with_configured_secret` | — | authorized OWNER / operation result | Config env + Wallet open OWNER | +| `lock_wallet` | — | `LockedWalletDto` | drop handle + inspect | +| `refresh_wallet_balance` | — | `WalletBalanceDto` | Transport getBalance | +| `inspect_import_source` | format only | transfer inspection DTO / cancel | Rust native picker + Wallet inspect | +| `import_wallet` | import request | authorized OWNER | staged bytes + Wallet import | +| `update_wallet_alias` | alias mutation | authorized DTO | WalletOwner | +| `add_wallet_note` | note create | authorized DTO | WalletOwner | +| `update_wallet_note` | note update | authorized DTO | WalletOwner | +| `delete_wallet_note` | note id | authorized DTO | WalletOwner | +| `rotate_owner_password` | rotation request | operation result | WalletOwner | +| `rotate_view_password` | rotation request | operation result | WalletOwner | +| `disable_view` | confirmation | authorized DTO | WalletOwner | +| `recreate_view` | new VIEW password | authorized DTO | WalletOwner | +| `export_wallet_owner` | format + save path | operation result | Wallet export file | +| `emit_frontend_log` | redacted payload | `()` | Logging facade | Aucune commande de signature arbitraire n'est ajoutée dans cette release. @@ -1677,7 +1677,7 @@ workspace checkpoint complet Le smoke live est volontairement `ignored` dans `cargo test --workspace`. Il doit être déclenché explicitement avec un `KSP_WALLETS_DIRECTORY` dédié ; le test charge ce paramètre uniquement via `ConfigEnvironment`, crée un `.kspwallet` réel dans le répertoire effectif du profil composite, ouvre VIEW, exécute `getBalance` via le Transport composite puis supprime le wallet canari. Ainsi le smoke cross-crates vit dans la surface d'orchestration Wallet Desk et non dans Config ou Transport. -Cette tranche doit produire la preuve que les surfaces ajoutées fonctionnent ensemble avant le polish visuel `pre.014` et la documentation finale `pre.015`. +Cette tranche doit produire la preuve que les surfaces ajoutées fonctionnent ensemble avant le polish visuel `pre.014` et les tranches V2 binaires intercalées avant la candidate finale. ### `pre.014` — polish gabarit Bootstrap et splashscreen @@ -1693,25 +1693,65 @@ réutilisation du gabarit commun sans déplacer de logique métier Cette tranche n'est pas un prétexte pour refondre l'UX métier. Les détails précis des défauts et critères d'acceptation seront fournis par l'opérateur lorsqu'elle sera ouverte. -### `pre.015` — documentation finale, candidate build et prompt suivant +### `pre.015` — `.kspwallet` V2 : wire binaire + codec canonique + +Cette tranche intercalée remplace l'ancien départ immédiat en documentation finale. Elle fixe le wire binaire V2 et son codec structurel dans `ksp-wallet-lib` sans modifier encore Wallet Desk ni les APIs de persistence V1 utilisées en production. + +```text +KSPWALLET magic binaire +format_version = 2 +longueur totale explicite +flags réservés stricts +OWNER puis VIEW optionnel +Argon2id / XChaCha20-Poly1305 / Ed25519 par IDs numériques +compartiments OWNER-CONTROL / METADATA / SECRET +entiers big-endian +aucune Base64 +aucune compression +aucun trailing byte +domains/transcripts/AAD V2 distincts de V1 +fixture wire-only canonique + comparaison de taille +``` + +La politique API est figée dès cette tranche : `DEFAULT_WALLET_FORMAT` et `LATEST_SUPPORTED_WALLET_FORMAT` sont deux notions distinctes. À l'intégration V2, le default devient V2. Une future V3 n'entraîne jamais automatiquement le déplacement du default. + +### `pre.016` — APIs génériques/versionnées + création/ouverture V2 + +Matérialiser : + +```text +create_wallet_file(...) -> V2 par default explicite +create_wallet_file_v1(...) conservé +create_wallet_file_v2(...) ajouté +open/inspect génériques -> détection V1/V2 +open/inspect _v1/_v2 -> format forcé strict +create/open in-memory V2 + crypto/transcripts V2 +Wallet Desk migre vers les APIs génériques, jamais vers un numéro de format +``` + +Une future API `_v3` pourra être ajoutée sans transformer automatiquement le default en V3. + +### `pre.017` — migration V1 -> V2 + persistence/canaris + +Ajouter la migration explicite et authentifiée V1 vers V2, les paths de remplacement/no-clobber nécessaires, les canaris adversariaux/interop et la régression complète Wallet Desk. Aucune migration silencieuse lors d'une simple ouverture. + +### `pre.018` — documentation finale, candidate build et prompt suivant Dernière tranche prévue avant `rel.001` : ```text -crates/ksp-app-wallet-desk/README.md -crates/ksp-app-wallet-desk/USAGE.md -docs/architecture / inventory / dependency graph synchronisés si impactés +README/USAGE Wallet Desk docs/validation 0.2.6 finalisée ROADMAP / functional sequence / CHANGELOG synchronisés +stratégie CWD/resources release packagée fermée dépendances et versions finales réauditées -commandes smoke documentées -prompt de démarrage 0.2.7 préparé validation Rust workspace finale parcours fonctionnel cargo tauri dev final +prompt de démarrage 0.2.7 WebSocket finalisé cargo tauri build en toute dernière opération ``` -Si le build final ou la documentation révèle une correction technique, utiliser `pre.015-fix.NNN` ou insérer une tranche supplémentaire. `pre.015` n'est pas une obligation de clôture artificielle. +Si une correction technique apparaît, utiliser `pre.018-fix.NNN` ou insérer une tranche supplémentaire. ### `rel.001` — publication stable @@ -1871,7 +1911,7 @@ KSP_WALLETS_DIRECTORY=var/wallet-desk-pre014 \ bash -lc 'cd crates/ksp-app-wallet-desk && cargo tauri dev' ``` -Le build Tauri de production reste reporté à `pre.015` et doit demeurer l’absolue dernière opération. +À ce stade de `pre.014`, le build Tauri de production était encore reporté à `pre.015`. Cette décision historique est **superseded par l’extension décidée en `pre.015`** : le build final est désormais réservé à `pre.018` et doit demeurer l’absolue dernière opération. ### Correctif `0.2.6-pre.014-fix.001` — layout desktop et canari durable @@ -1887,6 +1927,6 @@ splash : feed #debug-info pleine largeur dans les deux Desks pre.013 gate : le canari ne fige plus une shell_phase destinée à évoluer à chaque tranche ``` -Le recalage du CWD n'existe actuellement qu'en `debug_assertions` dans les deux `main.rs`. La candidate `pre.015` doit donc vérifier explicitement la stratégie de résolution production/bundle pour `config`, `.env`, `logs` et `wallets` avant le build final ; le comportement release ne doit pas être supposé identique au parcours `cargo tauri dev`. +Le recalage du CWD n'existe actuellement qu'en `debug_assertions` dans les deux `main.rs`. La candidate finale `pre.018` doit donc vérifier explicitement la stratégie de résolution production/bundle pour `config`, `.env`, `logs` et `wallets` avant le build final ; le comportement release ne doit pas être supposé identique au parcours `cargo tauri dev`. Ce fix touche le frontend/build contract et porte donc le signal technique `workspace.package.version = 0.2.6-pre.14.fix.1`. `ROADMAP.md`, `CHANGELOG.md` et `docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md` restent inchangés. diff --git a/docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md b/docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md index 5f4d6c0..12ed041 100644 --- a/docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md +++ b/docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md @@ -1,11 +1,11 @@ - + # Validation `0.2.6` — Wallet Desk integration / compliance / security ## 1. Objet -Cette matrice est ouverte par `0.2.6-pre.013` comme preuve durable de composition et de conformité de `ksp-app-wallet-desk`. Elle sera complétée en `pre.015` avec les résultats finaux, le polish visuel de `pre.014`, la documentation de clôture et le dernier build Tauri. +Cette matrice est ouverte par `0.2.6-pre.013` comme preuve durable de composition et de conformité de `ksp-app-wallet-desk`. Le calendrier a été étendu après `pre.014` : `pre.015` introduit le wire V2, `pre.016` les APIs génériques/versionnées, `pre.017` migration/régression, puis `pre.018` porte la clôture documentaire et le dernier build Tauri. Elle ne remplace ni le plan [`../plans/013-V0_2_6_WALLET_DESK_PLAN.md`](../plans/013-V0_2_6_WALLET_DESK_PLAN.md), ni la matrice Wallet V1 [`008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md), ni les deltas `0.2.6`. @@ -51,19 +51,19 @@ Le checkpoint opérateur `pre.012` du 21 août 2026 est vert : `cargo fmt`, audi ## 3. Matrice de frontières -| Frontière | Contrat `0.2.6` | Preuve durable | -|---|---|---| -| Frontend -> Config | aucun accès direct env/.env | `release_compliance::backend_keeps_config_wallet_transport_and_logging_ownership_boundaries` + ownership Config | -| Frontend -> Wallet secret | passwords uniquement request-only ; jamais de keypair en IPC | `desktop_security` + `release_compliance::response_dtos_keep_secret_key_material_out_of_ipc` | -| Frontend -> filesystem | aucun path arbitraire ; picker natif Rust | tests import/export + capability audit | -| Frontend -> réseau Solana | aucun `fetch`/WebSocket/RPC direct | `release_compliance::frontend_has_no_direct_filesystem_network_or_secret_persistence_surface` | -| Wallet Desk -> Solana crates | aucune dépendance protocole directe | `release_compliance::wallet_desk_dependency_firewall_and_tauri_capabilities_are_minimal` | -| Wallet Desk -> HTTP | `ksp-onchain-transport-lib` uniquement | manifest + smoke Devnet | -| Wallet Desk -> Wallet crypto | `ksp-wallet-lib` uniquement | manifest + feature canaries | -| Wallet Desk -> logging | façade `ksp-logging-lib` + adapter `tauri-plugin-tracing` | source audit + workspace logging canary | -| locked -> frontend | format/version/VIEW enabled seulement | `desktop_security` | -| VIEW/OWNER handles | Rust-only `AppState` | session/security tests | -| export | OWNER-only, file direct, no-clobber | `desktop_security` + runtime `pre.012` | +| Frontière | Contrat `0.2.6` | Preuve durable | +|------------------------------|--------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| Frontend -> Config | aucun accès direct env/.env | `release_compliance::backend_keeps_config_wallet_transport_and_logging_ownership_boundaries` + ownership Config | +| Frontend -> Wallet secret | passwords uniquement request-only ; jamais de keypair en IPC | `desktop_security` + `release_compliance::response_dtos_keep_secret_key_material_out_of_ipc` | +| Frontend -> filesystem | aucun path arbitraire ; picker natif Rust | tests import/export + capability audit | +| Frontend -> réseau Solana | aucun `fetch`/WebSocket/RPC direct | `release_compliance::frontend_has_no_direct_filesystem_network_or_secret_persistence_surface` | +| Wallet Desk -> Solana crates | aucune dépendance protocole directe | `release_compliance::wallet_desk_dependency_firewall_and_tauri_capabilities_are_minimal` | +| Wallet Desk -> HTTP | `ksp-onchain-transport-lib` uniquement | manifest + smoke Devnet | +| Wallet Desk -> Wallet crypto | `ksp-wallet-lib` uniquement | manifest + feature canaries | +| Wallet Desk -> logging | façade `ksp-logging-lib` + adapter `tauri-plugin-tracing` | source audit + workspace logging canary | +| locked -> frontend | format/version/VIEW enabled seulement | `desktop_security` | +| VIEW/OWNER handles | Rust-only `AppState` | session/security tests | +| export | OWNER-only, file direct, no-clobber | `desktop_security` + runtime `pre.012` | ## 4. Inventory / path adversarial @@ -238,4 +238,9 @@ cargo test --workspace : BLOCKED par un faux positif du canari Logging global Le blocage workspace est limité à `ksp-logging-lib/tests/ownership.rs::workspace_crates_do_not_bypass_ksp_logging_facade` : le scanner voit le littéral `tracing::` dans `release_compliance.rs`, alors que ce littéral est la chaîne recherchée par le canari et non un usage de `tracing`. `pre.013-fix.002` supprime ce littéral statique en construisant le motif à l'exécution, sans modifier le scanner global ni relâcher la politique Logging. -La matrice reste ouverte jusqu'à `pre.015`, où seront ajoutés le résultat du polish `pre.014`, le parcours fonctionnel final, les docs README/USAGE et le build Tauri exécuté en toute dernière opération. +La matrice reste ouverte jusqu'à `pre.018`, après les tranches V2 `pre.015`–`pre.017`; `pre.018` ajoute le parcours fonctionnel final, README/USAGE et le build Tauri exécuté en toute dernière opération. + + +## 11. Intercalation V2 à partir de `pre.015` + +`pre.015` ne modifie pas encore Wallet Desk : il ajoute dans `ksp-wallet-lib` un codec binaire V2 strict et une fixture canonique. Les gates Desk `pre.013`/`pre.014` restent donc applicables sans changement. `pre.016` devra faire migrer le Desk vers les APIs génériques afin qu’il ne connaisse plus `_v1`/`_v2`. diff --git a/prompts/000-README.md b/prompts/000-README.md index 4d7433b..272dbb8 100644 --- a/prompts/000-README.md +++ b/prompts/000-README.md @@ -1,5 +1,5 @@ - + # Prompts KSP @@ -32,5 +32,4 @@ Le prompt générique `0.1.x` a été affiné pendant `0.0.3` puis remplacé par - [`009-V0_2_4_START_PROMPT.md`](009-V0_2_4_START_PROMPT.md) — prompt préparé par `0.2.3-pre.009`, destiné à ouvrir `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale` après publication stable de `0.2.3`; il cible les 15 wrappers restants et impose `KSP-TRANSPORT-007` ainsi qu'un nouvel audit/sizing à `pre.001`. - [`010-V0_2_5_START_PROMPT.md`](010-V0_2_5_START_PROMPT.md) — prompt préparé par `0.2.4-pre.009` puis finalisé en version 2 par `pre.009-fix.001`, destiné à ouvrir `0.2.5 — Wallet foundation` sur la base stable `v0.2.4`; il impose audit/threat-model/sizing avant choix cryptographiques et cadre `.kspwallet` interopérable, capacités indépendantes VIEW/OWNER, metadata protégées, key slots/rotations, signature, persistence atomique et import/export extensible sans `WalletPolicy`. - [`011-V0_2_6_START_PROMPT.md`](011-V0_2_6_START_PROMPT.md) — prompt préparé par `0.2.5-pre.010` puis renforcé pendant `pre.010-fix.001`–`fix.003`, destiné à ouvrir `0.2.6 — Wallet Desk` après le tag stable `v0.2.5`; il impose une première tranche audit/sizing, rappelle les règles Rust/audit structurel, cadre Config composite + Wallet + HTTP `getBalance`, lifecycle VIEW/OWNER, sécurité password/export, validation frontend/Tauri et conserve le TODO `0.2.12` d’intégration des prix offchain dans Wallet Desk après validation de la Price Desk spécialisée. - -- [`012-V0_2_7_START_PROMPT.md`](012-V0_2_7_START_PROMPT.md) — prompt préparé pendant `0.2.6-pre.014`, destiné à ouvrir `0.2.7 — conteneur binaire .kspwallet` après publication stable de `0.2.6`; il sépare version de conteneur et `format_version` logique V1, exclut Base64 comme pseudo-sécurité, impose compatibilité JSON V1/migration/interop et utilise Wallet Desk comme canari consommateur sans modification fonctionnelle attendue. +- [`012-V0_2_7_START_PROMPT.md`](012-V0_2_7_START_PROMPT.md) — prompt réaligné par `0.2.6-pre.015` pour ouvrir `0.2.7 — WebSocket Solana standard`; le chantier `.kspwallet` V2 ayant été ramené dans `0.2.6`, il rétablit la séquence Transport et impose audit officiel/sizing avant implémentation. diff --git a/prompts/011-V0_2_6_START_PROMPT.md b/prompts/011-V0_2_6_START_PROMPT.md index f491198..0a5d999 100644 --- a/prompts/011-V0_2_6_START_PROMPT.md +++ b/prompts/011-V0_2_6_START_PROMPT.md @@ -1,5 +1,5 @@ - + # Prompt de démarrage `0.2.6` — Wallet Desk @@ -190,7 +190,7 @@ pre.006 wallet details + balance HTTP + refresh/diagnostics pre.007 metadata administration + rotations VIEW/OWNER pre.008 strong VIEW disable/recreate + export OWNER + security UX pre.009 integration tests + Devnet smoke + compliance/security review -pre.010 README/USAGE/build final + prompt 0.2.7 si nécessaire +pre.010 README/USAGE/build final + prompt suivant si nécessaire rel.001 publication stable ``` @@ -652,7 +652,7 @@ Le gate n'est considéré franchi que si ces livrables sont écrits/consignés d ## 24. Hors périmètre ```text -modification du format logique/cryptographique .kspwallet V1 sans défaut démontré (le conteneur de persistence est traité séparément en 0.2.7) +modification destructive du wire V1 stable ; V2 binaire est ajouté explicitement en `pre.015+` sans réécrire V1 WalletPolicy / execution policy construction/simulation/envoi de transaction WebSocket/gRPC @@ -666,11 +666,11 @@ trading Le Wallet Desk valide et administre Wallet ; il ne devient pas l'application globale KSP. -### TODO futur `0.2.12` — prix offchain dans Wallet Desk +### TODO futur `0.2.11` — prix offchain dans Wallet Desk La visualisation de prix offchain n'appartient pas au scope de `0.2.6`. Une application spécialisée de visualisation de prix offchain doit d'abord être réalisée et valider durablement ses sources, contrats, rafraîchissement, cache et UX. -Pour `0.2.12`, prévoir explicitement un chantier d'intégration de cette capacité dans `ksp-app-wallet-desk` afin qu'un wallet puisse afficher les informations de prix offchain pertinentes sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé. Le futur travail devra réauditer la frontière entre application, bibliothèque/service partagé et Transport avant implémentation ; le TODO n'autorise pas l'ajout anticipé de logique de prix dans Wallet Desk pendant `0.2.6`. +Pour `0.2.11`, prévoir explicitement un chantier d'intégration de cette capacité dans `ksp-app-wallet-desk` afin qu'un wallet puisse afficher les informations de prix offchain pertinentes sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé. Le futur travail devra réauditer la frontière entre application, bibliothèque/service partagé et Transport avant implémentation ; le TODO n'autorise pas l'ajout anticipé de logique de prix dans Wallet Desk pendant `0.2.6`. ## 25. Résultat attendu de `0.2.6` @@ -718,4 +718,9 @@ canari pre.013 : ne plus figer la phase shell historique version technique : 0.2.6-pre.14.fix.1 ``` -Le workflow Tauri reste crate-local. En debug, les deux binaires recalent ensuite leur current working directory sur la racine du workspace via `CARGO_MANIFEST_DIR/../..`; la stratégie release/bundle reste un point explicite à fermer en `pre.015`. +Le workflow Tauri reste crate-local. En debug, les deux binaires recalent ensuite leur current working directory sur la racine du workspace via `CARGO_MANIFEST_DIR/../..`; la stratégie release/bundle reste un point explicite à fermer dans la candidate finale `pre.018`. + + +## Addendum `pre.015` — V2 binaire intercalé + +Après validation du polish `pre.014-fix.001`, la release est volontairement étendue : `pre.015` wire/codec V2, `pre.016` APIs génériques/versionnées et V2 runtime, `pre.017` migration/régression, `pre.018` candidate finale. Le prompt `0.2.7` redevient WebSocket Solana standard. diff --git a/prompts/012-V0_2_7_START_PROMPT.md b/prompts/012-V0_2_7_START_PROMPT.md index 0508691..a47dd3a 100644 --- a/prompts/012-V0_2_7_START_PROMPT.md +++ b/prompts/012-V0_2_7_START_PROMPT.md @@ -1,268 +1,82 @@ - + -# Prompt de démarrage `0.2.7` — conteneur binaire `.kspwallet` +# Prompt de démarrage `0.2.7` — WebSocket Solana standard ## 1. Contexte de reprise -La base attendue est la release stable : +Base attendue : tag stable `v0.2.6`. -```text -v0.2.6 -``` - -`0.2.5` a stabilisé `ksp-wallet-lib` et le format logique/cryptographique `.kspwallet` V1 : capacités VIEW/OWNER indépendantes, Argon2id, XChaCha20-Poly1305, autorité OWNER Ed25519, persistence atomique/no-clobber, rotations, révocation VIEW forte, signature et transferts Solana CLI JSON/Base58. - -`0.2.6` a validé cette surface au travers de `ksp-app-wallet-desk` sans déplacer la propriété du format ou des secrets hors de Wallet. +`0.2.6` a validé Wallet Desk puis intégré le wire binaire `.kspwallet` V2, les APIs Wallet génériques/versionnées, la compatibilité V1/V2 et les migrations explicites. `0.2.7` revient donc à la séquence Transport prévue avant l'intercalation temporaire du chantier binaire. La release à ouvrir est : ```text -0.2.7 — conteneur binaire de persistence .kspwallet +0.2.7 — WebSocket Solana standard ``` -La première tranche est `0.2.7-pre.001` et commence obligatoirement par **audit + threat-model + options de framing/codec + compatibilité/migration + sizing**, avant toute modification du wire de persistence. +`0.2.7-pre.001` commence obligatoirement par audit officiel actuel + inventory exhaustif des subscriptions/unsubscriptions/notifications + threat-model/reconnexion + sizing avant implémentation lourde. -## 2. Motivation +## 2. Mission -Le document `.kspwallet` V1 actuellement persisté est un JSON strict. Ses valeurs sensibles restent cryptographiquement protégées, mais le fichier peut être ouvert et lu comme texte par un éditeur ordinaire. +Étendre `ksp-onchain-transport-lib` à la surface WebSocket Solana standard retenue, sans déplacer Config dans Transport et sans introduire encore Helius LaserStream. -L'objectif de `0.2.7` est de rendre la **persistence physique** binaire et explicitement framed, sans prétendre que cette transformation ajoute une protection cryptographique supplémentaire. - -### Non-solution explicite : Base64 seul - -Base64 seul n'est pas une solution acceptable : - -- il reste textuel/ASCII ; -- il est trivialement réversible ; -- il ne fournit aucune confidentialité ni intégrité supplémentaire ; -- il augmente typiquement la taille d'environ un tiers. - -Le changement recherché est un vrai **conteneur binaire de persistence**, pas une obfuscation textuelle. - -## 3. Séparation normative : conteneur vs format logique - -Ne pas consommer artificiellement `format_version = 2` uniquement parce que la persistence devient binaire. - -Distinguer explicitement : +Contrat de sessions : ```text -container/framing version - != -wallet logical/cryptographic format_version +une même URL peut porter plusieurs sessions physiques +une session peut porter plusieurs subscriptions +pas de pool/scheduler automatique complexe sans besoin démontré +reconnexion bornée et états de subscription explicites +notifications typées sans fuite d'URL/secrets dans Debug/logs ``` -Le payload logique V1 doit pouvoir conserver ses invariants actuels : +## 3. Frontières + +Conserver : ```text -VIEW / OWNER -KDF / salts / slots -AEAD / nonces / AAD -OWNER signature/transcript -metadata/secret compartments -Solana keypair identity -rotation / strong VIEW administration +Transport -X-> Config +Transport -X-> Wallet +Transport -X-> Store +Transport -X-> tracing direct +Config -> Transport autorisé +ksp-logging-lib seul propriétaire des émissions applicatives ``` -Un futur `format_version >= 2` reste disponible pour de vraies évolutions d'autorisation/crypto, notamment second facteur. +## 4. Audit pre.001 -## 4. Audit obligatoire `pre.001` +Auditer au minimum : -Comparer au minimum : +- documentation Solana/Agave actuelle ; +- méthodes subscribe/unsubscribe courantes, deprecated/unstable et notifications associées ; +- formes de paramètres/options et réponses ; +- reconnexion, resubscribe, backpressure, cancellation et shutdown ; +- plusieurs sessions pour une même URL ; +- limites provider utiles sans les figer comme règles Solana universelles ; +- dépendances WebSocket Rust actuelles et leur graphe ; +- sizing « une release = une session ». -1. framing binaire custom KSP avec grammar explicite ; -2. codecs binaires Serde adaptés et stables ; -3. formats self-describing éventuels ; -4. éventuelle compression du payload si elle apporte une vraie valeur ; -5. impact interop externe Rust/Python/Go/C ; -6. complexité de parsing borné et rejet des inputs adversariaux ; -7. dépendances nouvelles et leur maturité/maintenance ; -8. capacité à préserver les test vectors cryptographiques V1 ; -9. migration et détection sûre du JSON V1 legacy ; -10. règles d'unknown container version / taille / trailing bytes / canonicalité. - -Ne choisir aucun codec uniquement parce qu'il est pratique dans Rust : le format doit rester spécifiable indépendamment de l'implémentation. - -## 5. Compatibilité obligatoire - -`ksp-wallet-lib` doit rester capable de lire les `.kspwallet` JSON V1 déjà créés. - -Le candidat doit définir explicitement : +## 5. Hors périmètre ```text -legacy JSON V1 read -new binary container read -new binary container write default -migration explicite legacy -> binary -no-clobber / atomic replace -rollback/interruption behavior +Helius LaserStream WebSocket -> 0.2.8 +Yellowstone gRPC +Store/materialization +Wallet/2FA +pool automatique sophistiqué de sessions WebSocket +frontend réseau direct ``` -Une simple ouverture d'un legacy wallet ne doit pas le réécrire silencieusement sans contrat explicite. - -## 6. Sécurité - -Le conteneur binaire n'est **pas** présenté comme une nouvelle couche de chiffrement. - -Le threat model V1 reste notamment : - -- attaquant possédant le fichier ; -- connaissance complète de la spec ; -- essais offline illimités sur les passwords ; -- connaissance éventuelle d'un seul credential VIEW ou OWNER ; -- capacité à modifier/remplacer/rollback le fichier. - -Les gains de `0.2.7` sont : - -```text -framing déterministe -non-textual persistence -parsing/versioning physique explicite -meilleure séparation container/payload -préparation des futures migrations -``` - -Pas : - -```text -obscurity = security -Base64 = encryption -binaire = protection contre attaque offline -``` - -## 7. Frontière `ksp-wallet-lib` - -Le changement doit rester possédé par `ksp-wallet-lib`. - -`ksp-app-wallet-desk` ne doit pas connaître : - -- le codec du conteneur ; -- sa magic/version physique ; -- son layout ; -- ses offsets ; -- la distinction legacy/new lors des opérations ordinaires. - -Les APIs de haut niveau Wallet doivent absorber cette évolution autant que possible. - -## 8. Wallet Desk comme canari consommateur - -Aucune modification fonctionnelle de `ksp-app-wallet-desk` n'est attendue uniquement pour ce changement de persistence. - -Après modification de Wallet, rejouer au minimum : - -```bash -cargo test -p ksp-wallet-lib -cargo test -p ksp-app-wallet-desk -cargo test --workspace -``` - -Puis un parcours fonctionnel Wallet Desk doit prouver au minimum : - -```text -inventory -create -locked inspect -VIEW unlock -OWNER unlock -metadata -rotation -strong VIEW disable/recreate -export/import -balance HTTP -``` - -Si le Desk doit être modifié pour comprendre le conteneur physique, considérer cela comme un signal d'abstraction insuffisante et réauditer la frontière Wallet avant d'accepter le changement. - -## 9. Interop et test vectors - -Mettre à jour la spec `.kspwallet` avec une grammar byte-level du conteneur : - -- magic ; -- version du conteneur ; -- longueur(s) ; -- codec/payload kind ; -- endianess ; -- bornes ; -- trailing bytes ; -- canonicalité ; -- unknown-version policy. - -Conserver les test vectors cryptographiques V1 lorsque les transcripts/AAD ne changent pas, et ajouter des fixtures déterministes container + payload. - -Prévoir au moins un parseur externe de canari afin d'éviter un format binaire « Rust-only » accidentel. - -## 10. Futur `format_version >= 2` / second facteur - -Ce chantier **ne réalise pas** password + OTP/2FA. - -Pour une future version logique : - -- `ksp-wallet-lib` possède le format, les facteurs, challenges et vérifications ; -- un client interactif tel que Wallet Desk devra évoluer lorsqu'un OTP, enrollment/recovery, hardware/WebAuthn ou consentement externe doit être saisi/présenté ; -- un seed TOTP enfermé uniquement dans le même fichier ne doit pas être assimilé automatiquement à un second facteur indépendant contre un attaquant qui possède ce fichier. - -Le conteneur `0.2.7` doit simplement laisser cette évolution possible sans la pré-concevoir excessivement. - -## 11. Dépendances - -Toute nouvelle dépendance externe commune est déclarée dans `[workspace.dependencies]` puis consommée avec `.workspace = true`. - -Avant ajout : - -- vérifier la version actuelle ; -- auditer maintenance/licence/graphe ; -- éviter les générations obsolètes inutiles ; -- vérifier qu'une petite grammar custom n'est pas plus durable qu'un codec lourd, ou inversement. - -## 12. Discipline Rust / tests - -Conserver toutes les règles KSP actuelles : Rust 2024, async-first, pas de `unsafe`, `unwrap`, `expect`, `panic` runtime, audits structurels, dépendance Logging via `ksp-logging-lib`, Config hors Wallet. - -Validation générale : +## 6. Validation ```bash cargo fmt --all python3 scripts/audit_rust_workspace_rules.py cargo check --workspace cargo clippy --workspace --all-targets -cargo test -p ksp-wallet-lib -cargo test -p ksp-app-wallet-desk +cargo test -p ksp-onchain-transport-lib cargo test --workspace ``` -## 13. Forecast souple à recalibrer en `pre.001` - -Point de départ recommandé : - -```text -pre.001 audit framing/codec/threat-model/interop/sizing -pre.002 spec container + parser bounded + fixtures -pre.003 writer binary + persistence integration -pre.004 legacy JSON read + migration explicite -pre.005 adversarial/interop/test vectors -pre.006 Wallet Desk regression + live functional smoke -pre.007 compliance/docs/prompt suivant -rel.001 -``` - -Ce découpage est indicatif ; le gate `pre.001` doit le recalibrer selon le choix de format réel. - -## 14. Résultat attendu - -À la clôture stable : - -```text -les nouveaux .kspwallet sont persistés dans un conteneur binaire spécifié -les anciens JSON V1 restent lisibles -aucune sécurité fictive n'est attribuée au binaire/Base64 -les semantics cryptographiques V1 restent intactes sauf défaut démontré -les migrations sont explicites et testées -Wallet Desk fonctionne sans connaissance du format physique -interop externe et parsing adversarial sont prouvés -``` - -## 15. Instruction d'ouverture - -Commencer par relire la spec `.kspwallet` V1, les modules `wire`, `transcript`, `persistence`, `wallet`, `owner`, `transfer` et tous les test vectors actuels. - -Ensuite produire l'audit `0.2.7-pre.001` avant de coder le nouveau conteneur. +Prévoir des fixtures déterministes et un smoke réseau opt-in seulement lorsqu'une composition réelle le justifie.