diff --git a/Cargo.toml b/Cargo.toml index 9026d84..65296dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 188 +# version: 189 [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.15.fix.1" +version = "0.2.6-pre.16" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/ROADMAP.md b/ROADMAP.md index 711c08a..5e860e6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # Roadmap KSP @@ -50,7 +50,7 @@ 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` + é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.6` — `ksp-app-wallet-desk` + évolution `.kspwallet` V2 : surface Desk acquise ; `pre.014` polish et `pre.015` wire binaire V2 acquis ; `pre.016` matérialise APIs génériques/versionnées + runtime V2 ; prochain gate `pre.017` migration/canaris, puis `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. diff --git a/crates/ksp-app-config-desk/README.md b/crates/ksp-app-config-desk/README.md index dab2f38..eb6db06 100644 --- a/crates/ksp-app-config-desk/README.md +++ b/crates/ksp-app-config-desk/README.md @@ -1,5 +1,5 @@ - + # `ksp-app-config-desk` @@ -111,7 +111,6 @@ Les bibliothèques consommées par le bundle applicatif sont déclarées sous `d Les outils de compilation/développement et déclarations de types appartiennent à `devDependencies`, notamment `@tauri-apps/cli`, `@types/bootstrap`, `@types/node`, `sass-embedded`, `typescript` et `vite`. Les commandes npm directes sont réservées à l'installation ou à la mise à jour de ces dépendances. Le cycle normal de développement et de build est **crate-local** : l'opérateur peut rester positionné à la racine du workspace, mais la commande exécute d'abord `cd crates/ksp-app-config-desk` puis `cargo tauri dev`/`build`. `-c/--config` n'est pas utilisé comme sélecteur de crate. - ## Logging frontend KSP Le frontend utilise `frontend/ts/frontend_log.ts`. Les helpers `frontendTrace`, `frontendDebug`, `frontendInfo`, `frontendWarn` et `frontendError`, ainsi que le bridge `console.*`, invoquent la commande Tauri `emit_frontend_log`. @@ -154,7 +153,6 @@ Après la readiness frontend, Rust émet `fade_in`, attend la durée minimale, Le shell principal expose les cinq routes de référence `Vue d'ensemble`, `Documents`, `Profils`, `Environnement / .env` et `Logging`. Le logo porte déjà l'identité KSP ; le texte du header suit donc la forme `Config Desk — ` au lieu de répéter `KSP`. Les quelques commandes principales restent des pills/tabs à droite ; un dropdown sera préféré lorsqu'une application possède trop de commandes pour conserver ce format lisible. La route Logging expose maintenant un brouillon typé éditable, sa persistence atomique et l’application runtime immédiate par hot reload. - ## Panneau Profils La vue **Profils** inspecte les documents validés qui exposent le contrat standard `default_profile` / `profiles`. Elle ne revalide ni ne fusionne les valeurs dans le frontend : le backend utilise `ConfigDocumentEngine::load_resolved_profile()` puis `ResolvedConfigProfile::resolve_effective_environment_detailed()`. @@ -183,7 +181,6 @@ Les actions utilisateur significatives et changements d'état sont journalisés Le splash utilise `DOS_Amazigh.ttf` (`font-family: Dos Amazigh`) pour son titre, via le `@font-face` déclaré dans `frontend/sass/splash.scss`. Le répertoire `frontend/fonts/` documente la provenance et l'empreinte de l'asset de référence. Le lifecycle du splash est désormais actif et utilise cette police pendant la transition vers la fenêtre principale. - ## Panneau Environnement / `.env` La vue **Environnement / `.env`** expose la projection sûre de `ConfigManagement::environment_report()` puis une zone de management qui appelle exclusivement `ConfigManagement::set_dotenv_value()` et `ConfigManagement::remove_dotenv_value()`. Elle ne lit jamais directement le process environment ni le fichier `.env`. Pour chaque variable KSP/KSPB actuellement présente dans le process ou `.env`, le frontend reçoit uniquement : nom, namespace, sensibilité, valeur desired sûre, valeur effective sûre, source effective et indicateur de shadowing. diff --git a/crates/ksp-app-config-desk/USAGE.md b/crates/ksp-app-config-desk/USAGE.md index 6a53f14..41b074f 100644 --- a/crates/ksp-app-config-desk/USAGE.md +++ b/crates/ksp-app-config-desk/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de `ksp-app-config-desk` @@ -97,7 +97,6 @@ frontend/ts/bindings/ksp_app_config_desk/... Les bindings sont générés au premier DTO Tauri réel ; aucune structure factice n'est ajoutée uniquement pour créer le répertoire. - ## Bootstrap backend Les arguments `--cfgpath`, `--schemapath` et `--filemap=...` sont transmis tels quels à `ksp-config-lib`. En build debug, le launcher replace le current working directory Rust à la racine du workspace avant ce bootstrap afin que les defaults relatifs `config/`, `config/schemas/` et `.env` désignent les ressources racine même lorsque Tauri lance `cargo run` depuis la crate de l'application. Le launcher ne lit pas ces ressources lui-même. Le profil Logging initial est le `default_profile` de `std.logging.json`. @@ -156,12 +155,10 @@ La sélection charge le source brut via `ConfigManagement::read_source()`, y com Les clics, sélections, chargements, remplacements DOM et sauvegardes sont tracés via le bridge frontend KSP sans journaliser le contenu du source. - ### Ouvrir un éditeur spécialisé depuis Documents Le panneau Documents demeure générique. Lorsqu'un `file_id` possède un adapter déclaré dans `shell_registry.ts`, le détail affiche **Ouvrir l'éditeur spécialisé**. Pour `cfg.std.logging`, ce bouton active la vue Logging sans dupliquer la lecture, la validation ou la persistence Config. Un document sans adapter spécialisé reste entièrement inspectable/réparable dans le panneau Documents et n'affiche pas ce bouton. - ## Panneau Profils La vue **Profils** charge l'inventaire des documents Config validés qui possèdent `default_profile` et `profiles`. Pour le document sélectionné : @@ -175,7 +172,6 @@ La vue **Profils** charge l'inventaire des documents Config validés qui possèd Avec le `std.logging.json` actuel et `KSP_LOGS_DIRECTORY` défini dans `.env`, la provenance attendue pour `/logs_directory` est `KSP_LOGS_DIRECTORY`, source `dotenv`, sensibilité `Internal`. Le contenu des vues JSON n'est pas recopié dans les logs ; seuls les identifiants de document/profil et métadonnées de résolution sont tracés. - ## Panneau Environnement / `.env` La vue **Environnement / `.env`** charge `get_environment_report`, qui projette exclusivement `ConfigManagement::environment_report()`. La table affiche : @@ -219,8 +215,6 @@ La vue **Logging** appelle `get_logging_document`. Le backend charge le document Le sélecteur **Profil édité** travaille sur un brouillon local. `logs_directory`, `default_profile`, profils, console, file sinks, filtres et target overrides sont modifiables. **Créer**, **Cloner**, **Renommer** et **Supprimer** agissent d'abord sur le brouillon ; la suppression de profil est confirmée par modal Bootstrap. **Sauvegarder et appliquer** envoie un candidat typé à `save_logging_document`, qui reconstruit les contrats Config, persiste atomiquement après validation, recharge un `ConfigEnvironment` frais, résout le `default_profile` et hot-reload le `LoggingGuard`. Si le runtime ne peut pas être préparé/rechargé, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** resynchronise uniquement le brouillon depuis la source persistée et demande confirmation si des changements non sauvegardés existent. Les messages Cargo/Tauri/Vite affichés par `cargo tauri dev` sont externes au runtime Logging KSP et ne dépendent pas de `console.enabled`. - - ## Test Logging — routing backend et bridge frontend La section **Test Logging — routing contrôlé** utilise un message explicitement destiné aux logs. Ne jamais y copier une valeur `KSP_SECRET_*` / `KSPB_SECRET_*`. diff --git a/crates/ksp-app-wallet-desk/src/app_state.rs b/crates/ksp-app-wallet-desk/src/app_state.rs index 0ae012f..b3bf0d9 100644 --- a/crates/ksp-app-wallet-desk/src/app_state.rs +++ b/crates/ksp-app-wallet-desk/src/app_state.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/src/app_state.rs -// version: 18 +// version: 19 //! Shared backend state owned by the Wallet Desk Tauri application. @@ -202,7 +202,7 @@ impl AppState { let note_texts = note.into_iter().collect::>(); let metadata = ksp_wallet_lib::WalletCreateMetadataV1::new(alias, note_texts); ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = filename.as_str(), view_enabled, "Wallet creation requested under effective Config root"); - let created = ksp_wallet_lib::create_wallet_file_v1(destination.as_path(), owner_password, view_password, metadata).await; + let created = ksp_wallet_lib::create_wallet_file(destination.as_path(), owner_password, view_password, metadata).await; let owner = match created { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { @@ -297,7 +297,7 @@ impl AppState { let transfer_format = pending.wallet_format(); ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_IMPORT, wallet_id = filename.as_str(), transfer_format = transfer_format.code(), view_enabled, "Wallet transfer import requested under effective Config root"); let imported = - ksp_wallet_lib::import_wallet_transfer_v1(destination.as_path(), pending.source_bytes(), transfer_format, owner_password, view_password, metadata) + ksp_wallet_lib::import_wallet_transfer(destination.as_path(), pending.source_bytes(), transfer_format, owner_password, view_password, metadata) .await; let owner = match imported { std::result::Result::Ok(value) => value, @@ -326,7 +326,7 @@ impl AppState { std::result::Result::Err(error) => return std::result::Result::Err(error), }; let password = ksp_wallet_lib::ViewPassword::new(request.password); - let opened = ksp_wallet_lib::open_wallet_view_file_v1(context.path.as_path(), password).await; + let opened = ksp_wallet_lib::open_wallet_view_file(context.path.as_path(), password).await; let view = match opened { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { @@ -346,7 +346,7 @@ impl AppState { std::result::Result::Err(error) => return std::result::Result::Err(error), }; let password = ksp_wallet_lib::OwnerPassword::new(request.password); - let opened = ksp_wallet_lib::open_wallet_owner_file_v1(context.path.as_path(), password).await; + let opened = ksp_wallet_lib::open_wallet_owner_file(context.path.as_path(), password).await; let owner = match opened { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { @@ -390,7 +390,7 @@ impl AppState { return std::result::Result::Err(error); }, }; - let opened = ksp_wallet_lib::open_wallet_view_file_v1(context.path.as_path(), ksp_wallet_lib::ViewPassword::new(secret)).await; + let opened = ksp_wallet_lib::open_wallet_view_file(context.path.as_path(), ksp_wallet_lib::ViewPassword::new(secret)).await; match opened { std::result::Result::Ok(view) => { ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock succeeded"); @@ -439,7 +439,7 @@ impl AppState { return std::result::Result::Err(error); }, }; - let opened = ksp_wallet_lib::open_wallet_owner_file_v1(context.path.as_path(), ksp_wallet_lib::OwnerPassword::new(secret)).await; + let opened = ksp_wallet_lib::open_wallet_owner_file(context.path.as_path(), ksp_wallet_lib::OwnerPassword::new(secret)).await; match opened { std::result::Result::Ok(owner) => { ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock succeeded"); @@ -516,7 +516,7 @@ impl AppState { (wallet_id, path) }, }; - let locked = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await; + let locked = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await; let locked = match locked { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -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()); - return match previous { + 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"); - std::result::Result::Ok(dto) + return std::result::Result::Ok(dto); }, other => { *session = other; drop(view); - std::result::Result::Err(ksp_core_lib::Error::new( + return 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) { @@ -958,7 +958,7 @@ impl AppState { async fn recover_view_state_conflict(&self, context: ViewOperationContext) { let ViewOperationContext { operation, path, pubkey, view, wallet_id } = context; drop(view); - let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await; + let inspected = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await; let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, @@ -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()); - return match previous { + 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"); - std::result::Result::Ok(dto) + return std::result::Result::Ok(dto); }, other => { *session = other; drop(owner); - std::result::Result::Err(ksp_core_lib::Error::new( + return 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) { @@ -1089,7 +1089,7 @@ impl AppState { async fn recover_owner_state_conflict(&self, context: OwnerOperationContext) { let OwnerOperationContext { operation, owner, path, pubkey, result_view_enabled: _, view_enabled, wallet_id } = context; drop(owner); - let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await; + let inspected = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await; let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, diff --git a/crates/ksp-app-wallet-desk/src/wallet_inventory.rs b/crates/ksp-app-wallet-desk/src/wallet_inventory.rs index 5f3bbee..6158c13 100644 --- a/crates/ksp-app-wallet-desk/src/wallet_inventory.rs +++ b/crates/ksp-app-wallet-desk/src/wallet_inventory.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/src/wallet_inventory.rs -// version: 3 +// version: 4 //! Root-scoped Wallet inventory and locked-file selection for Wallet Desk. @@ -113,7 +113,7 @@ pub(crate) async fn list_wallet_inventory(root: &std::path::Path) -> ksp_core_li ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, filename = filename.as_str(), is_symlink = metadata.file_type().is_symlink(), "Wallet inventory skipped a non-regular candidate"); continue; } - let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await; + let inspected = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await; match inspected { std::result::Result::Ok(locked) => entries.push(valid_inventory_entry(filename, locked)), std::result::Result::Err(error) => { @@ -155,7 +155,7 @@ pub(crate) async fn resolve_locked_wallet( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let locked = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await; + let locked = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await; let locked = match locked { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), diff --git a/crates/ksp-app-wallet-desk/tests/desktop_contract.rs b/crates/ksp-app-wallet-desk/tests/desktop_contract.rs index f99abd1..145aadc 100644 --- a/crates/ksp-app-wallet-desk/tests/desktop_contract.rs +++ b/crates/ksp-app-wallet-desk/tests/desktop_contract.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs -// version: 16 +// version: 17 //! Desktop build, shell and Config-status contract audits for Wallet Desk. @@ -98,7 +98,7 @@ fn pre_004_inventory_uses_wallet_library_and_exposes_only_locked_safe_fields() { let inventory = read_text(root.join("src/wallet_inventory.rs").as_path()); let main = read_text(root.join("frontend/ts/main.ts").as_path()); assert!(manifest.contains("ksp-wallet-lib = { path = \"../ksp-wallet-lib\" }")); - assert!(inventory.contains("ksp_wallet_lib::inspect_locked_wallet_file_v1")); + assert!(inventory.contains("ksp_wallet_lib::inspect_locked_wallet_file")); assert!(inventory.contains("symlink_metadata")); assert!(inventory.contains("WALLET_FILE_SUFFIX")); assert!(tauri.contains("list_wallets")); @@ -132,7 +132,7 @@ fn pre_005_create_and_session_lifecycle_are_backend_owned() { let tauri = read_text(root.join("src/tauri.rs").as_path()); let html = read_text(root.join("frontend/main.html").as_path()); let main = read_text(root.join("frontend/ts/main.ts").as_path()); - assert!(state.contains("ksp_wallet_lib::create_wallet_file_v1")); + assert!(state.contains("ksp_wallet_lib::create_wallet_file")); assert!(state.contains("WalletSession::Owner")); assert!(state.contains("WalletSession::Locked")); assert!(state.contains("std::mem::replace")); @@ -157,8 +157,8 @@ fn pre_006_unlock_surface_is_explicit_backend_owned_and_supports_view_owner() { let tauri = read_text(root.join("src/tauri.rs").as_path()); let html = read_text(root.join("frontend/main.html").as_path()); let main = read_text(root.join("frontend/ts/main.ts").as_path()); - assert!(state.contains("open_wallet_view_file_v1")); - assert!(state.contains("open_wallet_owner_file_v1")); + assert!(state.contains("open_wallet_view_file")); + assert!(state.contains("open_wallet_owner_file")); assert!(state.contains("WalletSession::PrivilegedOperation")); assert!(state.contains("WalletSession::View")); assert!(secrets.contains("environment_report")); @@ -220,7 +220,7 @@ fn pre_008_import_uses_rust_native_picker_and_wallet_transfer_adapters() { assert!(tauri.contains("import_wallet")); assert!(import.contains("ksp_wallet_lib::inspect_wallet_transfer")); assert!(import.contains("zeroize::Zeroizing")); - assert!(state.contains("ksp_wallet_lib::import_wallet_transfer_v1")); + assert!(state.contains("ksp_wallet_lib::import_wallet_transfer")); assert!(html.contains("id=\"inspectImportSource\"")); assert!(html.contains("id=\"importWalletForm\"")); assert!(html.contains("id=\"importSourcePubkey\"")); @@ -384,3 +384,19 @@ fn pre_014_template_has_normalized_html_headers_light_header_and_kbot_style_spla assert!(splash_ts.contains("add_message")); assert!(!splash_ts.contains("../sass/splash.scss")); } + +#[test] +fn pre_016_wallet_desk_uses_version_neutral_wallet_dispatch() { + let state = read_source("src/app_state.rs"); + let inventory = read_source("src/wallet_inventory.rs"); + assert!(state.contains("ksp_wallet_lib::create_wallet_file")); + assert!(state.contains("ksp_wallet_lib::import_wallet_transfer")); + assert!(state.contains("ksp_wallet_lib::open_wallet_view_file")); + assert!(state.contains("ksp_wallet_lib::open_wallet_owner_file")); + assert!(inventory.contains("ksp_wallet_lib::inspect_locked_wallet_file")); + assert!(!state.contains("create_wallet_file_v1")); + assert!(!state.contains("import_wallet_transfer_v1")); + assert!(!state.contains("open_wallet_view_file_v1")); + assert!(!state.contains("open_wallet_owner_file_v1")); + assert!(!inventory.contains("inspect_locked_wallet_file_v1")); +} diff --git a/crates/ksp-app-wallet-desk/tests/desktop_security.rs b/crates/ksp-app-wallet-desk/tests/desktop_security.rs index c3608f4..0a6bb5f 100644 --- a/crates/ksp-app-wallet-desk/tests/desktop_security.rs +++ b/crates/ksp-app-wallet-desk/tests/desktop_security.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/tests/desktop_security.rs -// version: 13 +// version: 14 //! Static desktop security contracts for the Wallet Desk pre.002 shell. @@ -172,7 +172,7 @@ fn owner_metadata_values_are_request_only_and_state_conflict_forces_reauthorizat assert!(!metadata.contains("derive(Clone")); assert!(!metadata.contains("derive(Debug")); assert!(state.contains("recover_owner_state_conflict")); - assert!(state.contains("inspect_locked_wallet_file_v1")); + assert!(state.contains("inspect_locked_wallet_file")); assert!(state.contains("ERROR_CODE_STATE_CONFLICT")); assert!(main.contains("isWalletStateConflict")); assert!(main.contains("select_wallet")); diff --git a/crates/ksp-app-wallet-desk/tests/wallet_desk_devnet_smoke.rs b/crates/ksp-app-wallet-desk/tests/wallet_desk_devnet_smoke.rs index d35b05a..fb28bb0 100644 --- a/crates/ksp-app-wallet-desk/tests/wallet_desk_devnet_smoke.rs +++ b/crates/ksp-app-wallet-desk/tests/wallet_desk_devnet_smoke.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/tests/wallet_desk_devnet_smoke.rs -// version: 1 +// version: 2 //! Opt-in live composition smoke owned by the Wallet Desk orchestration surface. @@ -72,7 +72,7 @@ async fn committed_wallet_desk_composite_creates_wallet_and_reaches_devnet_balan std::result::Result::Err(_) => return, }; let wallet_path = wallet_root.join(std::format!("wallet-desk-pre013-smoke-{}-{stamp}.kspwallet", std::process::id())); - let owner = ksp_wallet_lib::create_wallet_file_v1( + let owner = ksp_wallet_lib::create_wallet_file( wallet_path.as_path(), ksp_wallet_lib::OwnerPassword::new("ksp-pre013-devnet-owner".to_owned()), std::option::Option::Some(ksp_wallet_lib::ViewPassword::new("ksp-pre013-devnet-view".to_owned())), @@ -84,13 +84,13 @@ async fn committed_wallet_desk_composite_creates_wallet_and_reaches_devnet_balan std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; - let locked = ksp_wallet_lib::inspect_locked_wallet_file_v1(wallet_path.as_path()).await; + let locked = ksp_wallet_lib::inspect_locked_wallet_file(wallet_path.as_path()).await; assert!(locked.is_ok(), "created smoke wallet should inspect while locked"); if let std::result::Result::Ok(locked) = locked { - assert_eq!(locked.format_version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V1); + assert_eq!(locked.format_version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V2); assert!(locked.view_enabled()); } - let view = ksp_wallet_lib::open_wallet_view_file_v1(wallet_path.as_path(), ksp_wallet_lib::ViewPassword::new("ksp-pre013-devnet-view".to_owned())).await; + let view = ksp_wallet_lib::open_wallet_view_file(wallet_path.as_path(), ksp_wallet_lib::ViewPassword::new("ksp-pre013-devnet-view".to_owned())).await; assert!(view.is_ok(), "created smoke wallet should open with VIEW"); let view = match view { std::result::Result::Ok(value) => value, diff --git a/crates/ksp-logging-lib/USAGE.md b/crates/ksp-logging-lib/USAGE.md index d46d1fd..bce54e2 100644 --- a/crates/ksp-logging-lib/USAGE.md +++ b/crates/ksp-logging-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ksp-logging-lib @@ -92,8 +92,6 @@ let mut logging_guard = match initialize_result { Une configuration sans output actif est valide et installe une infrastructure initialement silencieuse qui pourra être activée plus tard par hot reload. - - ## Identité de lancement et isolation des fichiers persistants Une application qui utilise des sorties fichiers persistantes peut attacher une identité stable au runtime : diff --git a/crates/ksp-wallet-lib/README.md b/crates/ksp-wallet-lib/README.md index 1f9df36..cf7c65d 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 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. +`ksp-wallet-lib` est la bibliothèque KSP propriétaire du Wallet Solana natif. Elle possède le format autonome `.kspwallet` V1 et le format 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. Depuis `0.2.6-pre.016`, les APIs non versionnées créent/importent en V2 par default explicite et lisent V1/V2 par détection bornée. 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,7 +14,8 @@ 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 ; +- le wire binaire `.kspwallet` V2, son codec borné/canonique, sa création/ouverture/persistence et ses domains/transcripts distincts ; +- la façade générique V1/V2 et les variantes `_v1`/`_v2` permettant soit le default, soit un wire forcé ; - les key slots OWNER/VIEW indépendants ; - Argon2id pour les KDF de passwords ; - XChaCha20-Poly1305 pour le wrapping et les compartiments ; @@ -194,6 +195,20 @@ Elles couvrent le wire, Argon2id/XChaCha20-Poly1305, l'ouverture VIEW/OWNER, la - [`../../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` +## V2 en `0.2.6-pre.015` / `pre.016` -`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. +`pre.015` a figé le wire structurel V2, son codec et ses transcripts/AAD. `pre.016` matérialise le runtime V2 complet et la façade multi-version : + +```text +DEFAULT_WALLET_FORMAT = V2 +LATEST_SUPPORTED_WALLET_FORMAT = V2 + +create_wallet_file(...) -> V2 +create_wallet_file_v1(...) -> V1 forcé +create_wallet_file_v2(...) -> V2 forcé + +open/inspect génériques -> détection V1/V2 +open/inspect _v1/_v2 -> format forcé strict +``` + +`WalletOwner` et `WalletView` conservent le format natif qu'ils ont ouvert : metadata, rotations OWNER/VIEW, disable/recreate VIEW, self-rotation VIEW, signature et export ne transcodent jamais implicitement le fichier. Le default est une décision explicite et ne suit pas automatiquement une future V3. La migration authentifiée V1 -> V2 reste une opération séparée de `pre.017`. diff --git a/crates/ksp-wallet-lib/USAGE.md b/crates/ksp-wallet-lib/USAGE.md index c1ba753..d980f09 100644 --- a/crates/ksp-wallet-lib/USAGE.md +++ b/crates/ksp-wallet-lib/USAGE.md @@ -1,9 +1,9 @@ - + # Utilisation de `ksp-wallet-lib` -Ce guide présente les principales surfaces publiques de Wallet V1. La spécification cryptographique du fichier reste [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md). +Ce guide présente les principales surfaces publiques multi-version de Wallet. V1 reste spécifié par [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md) et V2 par [`../../docs/formats/KSPWALLET_V2.md`](../../docs/formats/KSPWALLET_V2.md). Les exemples utilisent des chemins explicites : Wallet ne lit ni Config ni environnement pour découvrir un répertoire. @@ -11,13 +11,13 @@ Les exemples utilisent des chemins explicites : Wallet ne lit ni Config ni envir ```rust async fn create_example() -> ksp_core_lib::Result<()> { - let metadata = ksp_wallet_lib::WalletCreateMetadataV1::new( + let metadata = ksp_wallet_lib::WalletCreateMetadata::new( Some(std::string::String::from("devnet-main")), vec![std::string::String::from("wallet de test")], ); let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD")); let view_password = ksp_wallet_lib::ViewPassword::new(std::string::String::from("VIEW-PASSWORD")); - let created = ksp_wallet_lib::create_wallet_file_v1( + let created = ksp_wallet_lib::create_wallet_file( "wallets/devnet-main.kspwallet", owner_password, Some(view_password), @@ -35,13 +35,13 @@ async fn create_example() -> ksp_core_lib::Result<()> { La destination doit avoir un parent existant. Une destination existante n'est jamais remplacée par une création. -Pour créer uniquement en mémoire, utiliser `create_wallet_v1` puis `WalletOwner::to_json_bytes()` si le caller possède lui-même une autre boundary de stockage. +Pour créer uniquement en mémoire avec le default, utiliser `create_wallet`. `WalletOwner::to_native_bytes()` restitue ensuite le wire natif courant. Les APIs `_v1` et `_v2` restent disponibles lorsqu’un caller doit forcer une version précise. ## 2. Inspecter un wallet verrouillé ```rust async fn inspect_example() -> ksp_core_lib::Result<()> { - let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1("wallets/devnet-main.kspwallet").await; + let inspected = ksp_wallet_lib::inspect_locked_wallet_file("wallets/devnet-main.kspwallet").await; let locked = match inspected { Ok(value) => value, Err(error) => return Err(error), @@ -59,7 +59,7 @@ Cette projection ne contient volontairement ni Pubkey, ni alias, ni notes. ```rust async fn open_view_example() -> ksp_core_lib::Result<()> { let password = ksp_wallet_lib::ViewPassword::new(std::string::String::from("VIEW-PASSWORD")); - let opened = ksp_wallet_lib::open_wallet_view_file_v1("wallets/devnet-main.kspwallet", password).await; + let opened = ksp_wallet_lib::open_wallet_view_file("wallets/devnet-main.kspwallet", password).await; let mut view = match opened { Ok(value) => value, Err(error) => return Err(error), @@ -89,7 +89,7 @@ VIEW ne possède aucune API `sign`, `export_transfer`, `update_alias`, `add_note ```rust async fn sign_example(message: &[u8]) -> ksp_core_lib::Result<[u8; ksp_wallet_lib::KSPWALLET_SOLANA_SIGNATURE_BYTES]> { let password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD")); - let opened = ksp_wallet_lib::open_wallet_owner_file_v1("wallets/devnet-main.kspwallet", password).await; + let opened = ksp_wallet_lib::open_wallet_owner_file("wallets/devnet-main.kspwallet", password).await; let owner = match opened { Ok(value) => value, Err(error) => return Err(error), @@ -105,7 +105,7 @@ La signature retournée contient 64 octets Ed25519. Aucun getter public ne retou ```rust async fn metadata_example() -> ksp_core_lib::Result<()> { let password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD")); - let opened = ksp_wallet_lib::open_wallet_owner_file_v1("wallets/devnet-main.kspwallet", password).await; + let opened = ksp_wallet_lib::open_wallet_owner_file("wallets/devnet-main.kspwallet", password).await; let mut owner = match opened { Ok(value) => value, Err(error) => return Err(error), @@ -207,13 +207,13 @@ async fn inspect_transfer_example(source: &[u8]) -> ksp_core_lib::Result<()> { Import fichier vers un nouveau `.kspwallet` : ```rust -let imported = ksp_wallet_lib::import_wallet_transfer_file_v1( +let imported = ksp_wallet_lib::import_wallet_transfer_file( "wallets/imported.kspwallet", "wallets/legacy-id.json", ksp_wallet_lib::WalletTransferFormat::SolanaCliJson, ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD")), None, - ksp_wallet_lib::WalletCreateMetadataV1::new(Some(std::string::String::from("imported")), vec![]), + ksp_wallet_lib::WalletCreateMetadata::new(Some(std::string::String::from("imported")), vec![]), ) .await; ``` @@ -246,14 +246,16 @@ Les octets retournés par `export_transfer` contiennent volontairement le secret Les équivalents sans I/O filesystem sont : ```text -create_wallet_v1 -open_wallet_view_v1 -open_wallet_owner_v1 -inspect_locked_wallet_v1 -inspect_wallet_transfer +create_wallet -> default V2 +create_wallet_v1 / _v2 -> version forcée +open_wallet_view -> détection V1/V2 +open_wallet_owner -> détection V1/V2 +inspect_locked_wallet -> détection V1/V2 +open/inspect *_v1 / *_v2 -> version forcée +inspect_wallet_transfer -> format transfer explicite ``` -Les handles `WalletOwner` et `WalletView` peuvent être sérialisés vers le document natif courant avec `to_json_bytes()`. Ces bytes restent un `.kspwallet` chiffré, pas un export de la keypair. +Les handles `WalletOwner` et `WalletView` sérialisent leur document courant avec `to_native_bytes()`. `to_json_bytes()` est conservé comme compatibilité V1 et renvoie une erreur de format pour un handle V2 au lieu de transcoder implicitement. ## 11. Erreurs et diagnostics @@ -278,13 +280,24 @@ 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`) +## Wire/runtime V2 (`0.2.6-pre.015` / `pre.016`) -Le codec structurel V2 peut être utilisé pour analyser une fixture/document V2 déjà produit : +Le codec structurel V2 reste disponible directement pour les outils qui travaillent explicitement au niveau wire : ```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. +Une application normale doit préférer les façades Wallet : + +```text +create_wallet_file(...) default V2 +create_wallet_file_v1(...) V1 forcé +create_wallet_file_v2(...) V2 forcé +open_wallet_*_file(...) auto-détection V1/V2 +open_wallet_*_file_v1/_v2 version forcée +inspect_locked_wallet_file(...) auto-détection V1/V2 +``` + +`DEFAULT_WALLET_FORMAT` et `LATEST_SUPPORTED_WALLET_FORMAT` sont intentionnellement indépendants. L'arrivée d'un futur V3 ne changera pas automatiquement le default V2. diff --git a/crates/ksp-wallet-lib/src/constants.rs b/crates/ksp-wallet-lib/src/constants.rs index 4a71444..8c2fdde 100644 --- a/crates/ksp-wallet-lib/src/constants.rs +++ b/crates/ksp-wallet-lib/src/constants.rs @@ -1,5 +1,5 @@ // file: crates/ksp-wallet-lib/src/constants.rs -// version: 8 +// version: 9 //! Wallet-owned constants. @@ -91,6 +91,14 @@ pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24; 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; +/// Default Argon2id iteration count for newly created V2 key slots. +pub const KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS: u32 = KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS; +/// Default Argon2id memory cost for newly created V2 key slots. +pub const KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB: u32 = KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB; +/// Default Argon2id parallelism for newly created V2 key slots. +pub const KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM: u32 = KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM; +/// Default KDF salt size generated independently for every newly created V2 key slot. +pub const KSPWALLET_V2_DEFAULT_KDF_SALT_BYTES: usize = KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES; /// 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. @@ -123,6 +131,8 @@ pub const KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNE 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"; +/// Exact Solana keypair plaintext size retained by the initial V2 secret payload profile. +pub const KSPWALLET_V2_SECRET_PLAINTEXT_BYTES: usize = KSPWALLET_V1_SECRET_PLAINTEXT_BYTES; /// 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. diff --git a/crates/ksp-wallet-lib/src/crypto.rs b/crates/ksp-wallet-lib/src/crypto.rs index 03863f7..632e886 100644 --- a/crates/ksp-wallet-lib/src/crypto.rs +++ b/crates/ksp-wallet-lib/src/crypto.rs @@ -1,12 +1,12 @@ // file: crates/ksp-wallet-lib/src/crypto.rs -// version: 4 +// version: 5 -//! In-memory cryptographic primitives for native `.kspwallet` V1. +//! In-memory cryptographic primitives shared by native `.kspwallet` V1/V2 runtime. use chacha20poly1305::KeyInit; // rust-rules: trait-import use chacha20poly1305::aead::Aead; // rust-rules: trait-import -/// Exact V1 content-key and password-derived-key size in bytes. +/// Exact content-key and password-derived-key size in bytes for the current V1/V2 profiles. pub(crate) const SECRET_KEY_BYTES: usize = 32; /// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization. @@ -68,6 +68,11 @@ pub(crate) fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParamet return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism()); } +/// Derives one V2 password wrapping key from serialized Argon2id parameters. +pub(crate) fn derive_password_key_v2(password: &[u8], kdf: &crate::WalletKdfParametersV2) -> ksp_core_lib::Result { + return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism()); +} + /// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD. pub(crate) fn wrap_key( wrapping_key: &crate::SecretKeyV1, diff --git a/crates/ksp-wallet-lib/src/format.rs b/crates/ksp-wallet-lib/src/format.rs new file mode 100644 index 0000000..1e23706 --- /dev/null +++ b/crates/ksp-wallet-lib/src/format.rs @@ -0,0 +1,111 @@ +// file: crates/ksp-wallet-lib/src/format.rs +// version: 2 + +//! Native `.kspwallet` version selection and bounded dispatch detection. + +/// Explicit native `.kspwallet` wire format understood by this Wallet release. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WalletFormat { + /// Stable historical JSON/Base64url format. + V1, + /// Canonical binary KSP format. + V2, +} + +impl WalletFormat { + /// Returns the serialized native format version. + #[must_use] + pub const fn version(self) -> u32 { + return match self { + Self::V1 => crate::KSPWALLET_FORMAT_VERSION_V1, + Self::V2 => crate::KSPWALLET_FORMAT_VERSION_V2, + }; + } +} + +/// Default format selected by the non-versioned native Wallet creation/import APIs. +/// +/// This is intentionally independent from [`LATEST_SUPPORTED_WALLET_FORMAT`]. Adding a future format does not implicitly move this default. +pub const DEFAULT_WALLET_FORMAT: WalletFormat = WalletFormat::V2; +/// Highest native Wallet format understood by this release. +pub const LATEST_SUPPORTED_WALLET_FORMAT: WalletFormat = WalletFormat::V2; + +/// Creates a new in-memory native Wallet using [`DEFAULT_WALLET_FORMAT`]. +pub async fn create_wallet( + owner_password: crate::OwnerPassword, + view_password: std::option::Option, + metadata: crate::WalletCreateMetadata, +) -> ksp_core_lib::Result { + return match DEFAULT_WALLET_FORMAT { + WalletFormat::V1 => crate::create_wallet_v1(owner_password, view_password, metadata).await, + WalletFormat::V2 => crate::create_wallet_v2(owner_password, view_password, metadata).await, + }; +} + +/// Opens VIEW from any currently supported native Wallet wire format. +pub async fn open_wallet_view(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result { + return match detect_wallet_format(source) { + std::result::Result::Ok(WalletFormat::V1) => crate::open_wallet_view_v1(source, password).await, + std::result::Result::Ok(WalletFormat::V2) => crate::open_wallet_view_v2(source, password).await, + std::result::Result::Err(error) => std::result::Result::Err(error), + }; +} + +/// Opens OWNER from any currently supported native Wallet wire format. +pub async fn open_wallet_owner(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result { + return match detect_wallet_format(source) { + std::result::Result::Ok(WalletFormat::V1) => crate::open_wallet_owner_v1(source, password).await, + std::result::Result::Ok(WalletFormat::V2) => crate::open_wallet_owner_v2(source, password).await, + std::result::Result::Err(error) => std::result::Result::Err(error), + }; +} + +/// Inspects the authenticated locked projection of any currently supported native Wallet wire format. +pub fn inspect_locked_wallet(source: &[u8]) -> ksp_core_lib::Result { + return match detect_wallet_format(source) { + std::result::Result::Ok(WalletFormat::V1) => crate::inspect_locked_wallet_v1(source), + std::result::Result::Ok(WalletFormat::V2) => crate::inspect_locked_wallet_v2(source), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; +} + +/// Detects the native wire format without decrypting or authenticating the document. +/// +/// Detection only selects the strict parser. The selected V1/V2 parser remains authoritative for all structural and cryptographic validation. +pub fn detect_wallet_format(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 maximum size")); + } + if source.starts_with(crate::KSPWALLET_MAGIC.as_bytes()) { + let version_start = crate::KSPWALLET_MAGIC.len(); + let version_end = version_start + 2; + let version_bytes = match source.get(version_start..version_end) { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(format_error("Wallet binary header is truncated")), + }; + let version = u16::from_be_bytes([version_bytes[0], version_bytes[1]]); + return match u32::from(version) { + crate::KSPWALLET_FORMAT_VERSION_V2 => std::result::Result::Ok(WalletFormat::V2), + other => std::result::Result::Err(version_error(other)), + }; + } + let first_non_whitespace = source.iter().copied().find(|byte| return !byte.is_ascii_whitespace()); + if first_non_whitespace == std::option::Option::Some(b'{') { + return std::result::Result::Ok(WalletFormat::V1); + } + return std::result::Result::Err(format_error("Wallet document does not match a supported native framing")); +} + +fn format_error(message: &'static str) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message); +} + +fn version_error(version: u32) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED, "Wallet format version is not supported") + .with_context("format_version", version.to_string()); +} + +#[cfg(test)] +#[path = "../unit_tests/format.rs"] +mod tests; diff --git a/crates/ksp-wallet-lib/src/lib.rs b/crates/ksp-wallet-lib/src/lib.rs index 482d200..628ed37 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: 12 +// version: 13 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -16,7 +16,8 @@ //! 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. `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 +//! persistence default or Wallet Desk dispatch. `0.2.6-pre.016` adds complete V2 create/open/administration, stable version-neutral dispatch, explicit V1/V2 +//! APIs and moves the default native creation/import format to V2 without coupling that default to the latest supported version. 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`. @@ -24,16 +25,19 @@ mod capability; mod constants; mod crypto; mod error; +mod format; mod metadata; mod owner; mod password; mod payload; mod persistence; +mod runtime; mod transcript; mod transcript_v2; mod transfer; mod view; mod wallet; +mod wallet_v2; mod wire; mod wire_v2; @@ -131,6 +135,14 @@ pub use self::constants::KSPWALLET_V1_XCHACHA_NONCE_BYTES; 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; +/// Default Argon2id iteration count for newly created V2 slots. +pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS; +/// Default Argon2id memory cost for newly created V2 slots. +pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB; +/// Default Argon2id parallelism for newly created V2 slots. +pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM; +/// Default KDF salt size generated for newly created V2 slots. +pub use self::constants::KSPWALLET_V2_DEFAULT_KDF_SALT_BYTES; /// 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. @@ -163,6 +175,8 @@ pub use self::constants::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN; 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; +/// Exact Solana keypair plaintext size retained by the initial V2 secret payload profile. +pub use self::constants::KSPWALLET_V2_SECRET_PLAINTEXT_BYTES; /// 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. @@ -205,9 +219,27 @@ pub use self::error::ERROR_CODE_STATE_CONFLICT; pub use self::error::ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED; /// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle. pub use self::error::ERROR_CODE_VIEW_UNLOCK_FAILED; +/// Default native Wallet format selected by non-versioned create/import APIs. +pub use self::format::DEFAULT_WALLET_FORMAT; +/// Highest native Wallet format supported by this release. +pub use self::format::LATEST_SUPPORTED_WALLET_FORMAT; +/// Explicit native Wallet wire-format selector. +pub use self::format::WalletFormat; +/// Creates a new in-memory native Wallet using the explicit default format. +pub use self::format::create_wallet; +/// Detects V1 JSON versus V2 binary framing before strict parser dispatch. +pub use self::format::detect_wallet_format; +/// Inspects any supported in-memory native Wallet document. +pub use self::format::inspect_locked_wallet; +/// Opens OWNER from any supported in-memory native Wallet document. +pub use self::format::open_wallet_owner; +/// Opens VIEW from any supported in-memory native Wallet document. +pub use self::format::open_wallet_view; /// Minimal non-secret information available while a native Wallet remains locked. pub use self::metadata::LockedWalletInfo; -/// Protected initial metadata supplied to native Wallet creation. +/// Protected initial metadata supplied to version-neutral native Wallet creation. +pub type WalletCreateMetadata = self::metadata::WalletCreateMetadataV1; +/// Protected initial metadata payload profile retained for explicit V1 compatibility. pub use self::metadata::WalletCreateMetadataV1; /// Safe metadata projection produced after VIEW or OWNER authorization. pub use self::metadata::WalletInfo; @@ -219,22 +251,46 @@ pub use self::owner::WalletOwner; pub use self::password::OwnerPassword; /// Owned VIEW password material with redacted diagnostics and drop-time zeroization. pub use self::password::ViewPassword; +/// Creates and no-clobber persists a native Wallet using the explicit default format. +pub use self::persistence::create_wallet_file; /// Creates and no-clobber persists a new native `.kspwallet` V1 file. pub use self::persistence::create_wallet_file_v1; +/// Creates and no-clobber persists a new native `.kspwallet` V2 binary file. +pub use self::persistence::create_wallet_file_v2; +/// Reads and verifies a locked native Wallet using V1/V2 auto-detection. +pub use self::persistence::inspect_locked_wallet_file; /// Reads and verifies a locked native `.kspwallet` V1 file. pub use self::persistence::inspect_locked_wallet_file_v1; +/// Reads and verifies a locked native `.kspwallet` V2 binary file. +pub use self::persistence::inspect_locked_wallet_file_v2; +/// Opens a native Wallet with OWNER capability using V1/V2 auto-detection. +pub use self::persistence::open_wallet_owner_file; /// Opens a native `.kspwallet` V1 file with OWNER capability. pub use self::persistence::open_wallet_owner_file_v1; +/// Opens a native `.kspwallet` V2 binary file with OWNER capability. +pub use self::persistence::open_wallet_owner_file_v2; +/// Opens a native Wallet with VIEW capability using V1/V2 auto-detection. +pub use self::persistence::open_wallet_view_file; /// Opens a native `.kspwallet` V1 file with VIEW capability. pub use self::persistence::open_wallet_view_file_v1; +/// Opens a native `.kspwallet` V2 binary file with VIEW capability. +pub use self::persistence::open_wallet_view_file_v2; /// Explicit secret-transfer format supported by Wallet. pub use self::transfer::WalletTransferFormat; /// Safe public identity projection of one validated secret-transfer source. pub use self::transfer::WalletTransferInspection; +/// Imports one in-memory transfer payload into a new no-clobber native Wallet using the explicit default format. +pub use self::transfer::import_wallet_transfer; +/// Imports one bounded external transfer file into a new no-clobber native Wallet using the explicit default format. +pub use self::transfer::import_wallet_transfer_file; /// Imports one bounded external transfer file into a new no-clobber native Wallet V1. pub use self::transfer::import_wallet_transfer_file_v1; +/// Imports one bounded external transfer file into a new no-clobber native Wallet V2. +pub use self::transfer::import_wallet_transfer_file_v2; /// Imports one in-memory transfer payload into a new no-clobber native Wallet V1. pub use self::transfer::import_wallet_transfer_v1; +/// Imports one in-memory transfer payload into a new no-clobber native Wallet V2. +pub use self::transfer::import_wallet_transfer_v2; /// Validates one in-memory transfer payload and exposes only its derived public identity. pub use self::transfer::inspect_wallet_transfer; /// Validates one bounded external transfer file and exposes only its derived public identity. @@ -249,6 +305,14 @@ pub use self::wallet::inspect_locked_wallet_v1; pub use self::wallet::open_wallet_owner_v1; /// Opens the VIEW capability from a native Wallet V1 document. pub use self::wallet::open_wallet_view_v1; +/// Creates a new in-memory native Wallet V2. +pub use self::wallet_v2::create_wallet_v2; +/// Parses and verifies locked native Wallet V2 state without unlocking protected metadata. +pub use self::wallet_v2::inspect_locked_wallet_v2; +/// Opens the OWNER capability from a native Wallet V2 document. +pub use self::wallet_v2::open_wallet_owner_v2; +/// Opens the VIEW capability from a native Wallet V2 document. +pub use self::wallet_v2::open_wallet_view_v2; /// Strict semantic representation of one parsed native `.kspwallet` V1 envelope. pub use self::wire::KspWalletEnvelopeV1; /// Authenticated-encryption algorithm fixed by native Wallet V1. @@ -308,6 +372,8 @@ pub(crate) use self::crypto::SecretKeyV1; pub(crate) use self::crypto::decrypt_bytes; /// Derives one V1 password wrapping key from serialized Argon2id parameters. pub(crate) use self::crypto::derive_password_key; +/// Derives one V2 password wrapping key from serialized Argon2id parameters. +pub(crate) use self::crypto::derive_password_key_v2; /// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD. pub(crate) use self::crypto::encrypt_bytes; /// Generates a fresh fixed-size byte array from the operating-system CSPRNG. @@ -328,7 +394,9 @@ pub(crate) use self::payload::decode_owner_control_payload; pub(crate) use self::payload::encode_initial_metadata_payload; /// Encodes owner control payload. pub(crate) use self::payload::encode_owner_control_payload; -/// Internal no-clobber native persistence path shared by transfer adapters. +/// Internal no-clobber native persistence path shared by version-neutral transfer adapters. +pub(crate) use self::persistence::persist_new_wallet_content; +/// Internal V1 no-clobber native persistence path retained for explicit compatibility adapters. pub(crate) use self::persistence::persist_new_wallet_content_v1; /// Persists new wallet fault before publish. #[cfg(test)] @@ -341,9 +409,15 @@ pub(crate) use self::persistence::persist_new_wallet_for_test; pub(crate) use self::persistence::replace_wallet_fault_before_publish; /// Replaces wallet file v1. pub(crate) use self::persistence::replace_wallet_file_v1; +/// Replaces wallet file v2. +pub(crate) use self::persistence::replace_wallet_file_v2; /// Replaces wallet for test. #[cfg(test)] pub(crate) use self::persistence::replace_wallet_for_test; +/// Version-neutral OWNER runtime state shared by public Wallet handles. +pub(crate) use self::runtime::OwnerState; +/// Version-neutral VIEW runtime state shared by public Wallet handles. +pub(crate) use self::runtime::ViewState; /// Internal deterministic compartment-AAD codec shared by Wallet crypto layers. pub(crate) use self::transcript::compartment_aad; /// Internal deterministic key-slot-AAD codec shared by Wallet crypto layers. @@ -357,12 +431,20 @@ 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; +pub(crate) use self::transfer::write_wallet_transfer_file; /// Crate-internal `OwnerStateV1` state shared across the owning crate. pub(crate) use self::wallet::OwnerStateV1; /// Crate-internal `ViewStateV1` state shared across the owning crate. pub(crate) use self::wallet::ViewStateV1; -/// Internal imported-keypair creation path shared by transfer adapters. +/// Internal imported-keypair V1 creation path shared by transfer adapters. pub(crate) use self::wallet::create_wallet_v1_from_keypair; -/// Verifies state signature. +/// Verifies V1 state signature. pub(crate) use self::wallet::verify_state_signature; +/// Crate-internal `OwnerStateV2` state shared across the owning crate. +pub(crate) use self::wallet_v2::OwnerStateV2; +/// Crate-internal `ViewStateV2` state shared across the owning crate. +pub(crate) use self::wallet_v2::ViewStateV2; +/// Internal imported-keypair V2 creation path shared by transfer adapters. +pub(crate) use self::wallet_v2::create_wallet_v2_from_keypair; +/// Verifies V2 state signature. +pub(crate) use self::wallet_v2::verify_state_signature_v2; diff --git a/crates/ksp-wallet-lib/src/metadata.rs b/crates/ksp-wallet-lib/src/metadata.rs index 14a2a08..feea52e 100644 --- a/crates/ksp-wallet-lib/src/metadata.rs +++ b/crates/ksp-wallet-lib/src/metadata.rs @@ -1,5 +1,5 @@ // file: crates/ksp-wallet-lib/src/metadata.rs -// version: 4 +// version: 5 /// Protected metadata requested when creating a new native Wallet. /// @@ -78,12 +78,13 @@ pub struct WalletInfo { impl WalletInfo { /// Creates a new `WalletInfo` value. pub(crate) fn new( + format_version: u32, capability: crate::WalletCapability, pubkey: ksp_core_lib::Pubkey, alias: std::option::Option, notes: std::vec::Vec, ) -> Self { - return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, capability, pubkey, alias, notes }; + return Self { format_version, capability, pubkey, alias, notes }; } /// Returns the native Wallet format version parsed for this projection. @@ -139,8 +140,8 @@ pub struct LockedWalletInfo { impl LockedWalletInfo { /// Creates a new `LockedWalletInfo` value. - pub(crate) const fn new(view_enabled: bool) -> Self { - return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, view_enabled }; + pub(crate) const fn new(format_version: u32, view_enabled: bool) -> Self { + return Self { format_version, view_enabled }; } /// Returns the native Wallet format version. diff --git a/crates/ksp-wallet-lib/src/owner.rs b/crates/ksp-wallet-lib/src/owner.rs index 0a6e6fc..23b5754 100644 --- a/crates/ksp-wallet-lib/src/owner.rs +++ b/crates/ksp-wallet-lib/src/owner.rs @@ -1,21 +1,27 @@ // file: crates/ksp-wallet-lib/src/owner.rs -// version: 6 +// version: 7 /// Authorized OWNER capability handle. /// -/// OWNER exposes protected metadata, Solana message signing and the authenticated administration operations defined by native Wallet V1. Secret key +/// OWNER exposes protected metadata, Solana message signing and the authenticated administration operations defined by the authenticated native Wallet format. Secret key /// material remains encapsulated and is never exposed through a general-purpose getter. pub struct WalletOwner { info: crate::WalletInfo, - state: crate::OwnerStateV1, + state: crate::OwnerState, } impl WalletOwner { /// Builds `WalletOwner` from unlocked. - pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::OwnerStateV1) -> Self { + pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::OwnerState) -> Self { return Self { info, state }; } + /// Returns the native Wallet format version backing this authorized handle. + #[must_use] + pub const fn format_version(&self) -> u32 { + return self.info.format_version(); + } + /// Returns the authorization capability represented by this handle. #[must_use] pub const fn capability(&self) -> crate::WalletCapability { @@ -84,7 +90,7 @@ impl WalletOwner { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let result = crate::write_wallet_transfer_file_v1(destination.as_ref().to_path_buf(), encoded, format).await; + let result = crate::write_wallet_transfer_file(destination.as_ref().to_path_buf(), encoded, format).await; if result.is_ok() { ksp_logging_lib::debug!( target: crate::TRACING_TARGET, @@ -181,11 +187,13 @@ impl WalletOwner { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await; + let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await; if let std::result::Result::Err(error) = persist_result { return std::result::Result::Err(error); } - self.state.apply_envelope(envelope); + if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) { + return std::result::Result::Err(error); + } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, operation = "wallet_rotate_owner_password", @@ -207,11 +215,13 @@ impl WalletOwner { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await; + let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await; if let std::result::Result::Err(error) = persist_result { return std::result::Result::Err(error); } - self.state.apply_envelope(envelope); + if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) { + return std::result::Result::Err(error); + } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, operation = "wallet_rotate_view_password", @@ -229,11 +239,13 @@ impl WalletOwner { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await; + let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await; if let std::result::Result::Err(error) = persist_result { return std::result::Result::Err(error); } - self.state.apply_strong_view_state(envelope, metadata_key); + if let std::result::Result::Err(error) = self.state.apply_strong_view_state(envelope, metadata_key) { + return std::result::Result::Err(error); + } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, operation = "wallet_disable_view", @@ -253,11 +265,13 @@ impl WalletOwner { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await; + let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await; if let std::result::Result::Err(error) = persist_result { return std::result::Result::Err(error); } - self.state.apply_strong_view_state(envelope, metadata_key); + if let std::result::Result::Err(error) = self.state.apply_strong_view_state(envelope, metadata_key) { + return std::result::Result::Err(error); + } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, operation = "wallet_recreate_view", @@ -267,9 +281,16 @@ impl WalletOwner { return std::result::Result::Ok(()); } - /// Serializes the complete locked `.kspwallet` V1 document without exposing any unlocked secret material. + /// Serializes the complete locked native Wallet in its current V1 or V2 wire format. + pub fn to_native_bytes(&self) -> ksp_core_lib::Result> { + return self.state.native_bytes(); + } + + /// Serializes a V1 handle as its historical JSON document. + /// + /// V2 handles return a format error instead of being converted implicitly. pub fn to_json_bytes(&self) -> ksp_core_lib::Result> { - return self.state.envelope().to_json_bytes(); + return self.state.json_bytes_v1(); } async fn persist_metadata_payload( @@ -282,11 +303,13 @@ impl WalletOwner { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let persist_result = persist_staged(destination, self.state.envelope(), &envelope).await; + let persist_result = self.state.persist_staged(destination, &envelope).await; if let std::result::Result::Err(error) = persist_result { return std::result::Result::Err(error); } - self.state.apply_envelope(envelope); + if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) { + return std::result::Result::Err(error); + } self.info = info; ksp_logging_lib::debug!(target: crate::TRACING_TARGET, operation = operation, capability = "owner", "wallet protected metadata updated"); return std::result::Result::Ok(()); @@ -299,18 +322,6 @@ impl std::fmt::Debug for WalletOwner { } } -async fn persist_staged( - destination: std::path::PathBuf, - expected_current: &crate::KspWalletEnvelopeV1, - envelope: &crate::KspWalletEnvelopeV1, -) -> ksp_core_lib::Result<()> { - let serialized = match envelope.to_json_bytes() { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - return crate::replace_wallet_file_v1(destination, expected_current.clone(), serialized).await; -} - #[cfg(test)] #[path = "../unit_tests/administration.rs"] mod tests; diff --git a/crates/ksp-wallet-lib/src/payload.rs b/crates/ksp-wallet-lib/src/payload.rs index 2ed9c98..3284012 100644 --- a/crates/ksp-wallet-lib/src/payload.rs +++ b/crates/ksp-wallet-lib/src/payload.rs @@ -1,7 +1,7 @@ // file: crates/ksp-wallet-lib/src/payload.rs -// version: 5 +// version: 6 -//! Plaintext payload codecs protected inside native `.kspwallet` V1 compartments. +//! Plaintext payload codecs shared by the initial native `.kspwallet` V1/V2 protected compartments. use base64::Engine; // rust-rules: trait-import use std::str::FromStr; // rust-rules: trait-import @@ -119,8 +119,8 @@ impl crate::MetadataPayloadV1 { } /// Consumes this value and returns info. - pub(crate) fn into_info(self, capability: crate::WalletCapability) -> crate::WalletInfo { - return crate::WalletInfo::new(capability, self.pubkey, self.alias, self.notes); + pub(crate) fn into_info(self, format_version: u32, capability: crate::WalletCapability) -> crate::WalletInfo { + return crate::WalletInfo::new(format_version, capability, self.pubkey, self.alias, self.notes); } } diff --git a/crates/ksp-wallet-lib/src/persistence.rs b/crates/ksp-wallet-lib/src/persistence.rs index 1bd0b86..4516d12 100644 --- a/crates/ksp-wallet-lib/src/persistence.rs +++ b/crates/ksp-wallet-lib/src/persistence.rs @@ -1,11 +1,54 @@ // file: crates/ksp-wallet-lib/src/persistence.rs -// version: 7 +// version: 8 -//! Async-first native Wallet V1 filesystem persistence. +//! Async-first native Wallet V1/V2 filesystem persistence and version-neutral dispatch. use std::io::Read; // rust-rules: trait-import use std::io::Write; // rust-rules: trait-import +/// Creates a new native `.kspwallet` using [`crate::DEFAULT_WALLET_FORMAT`]. +/// +/// The default is explicitly V2 in this release and does not track future `LATEST_SUPPORTED_WALLET_FORMAT` values automatically. +pub async fn create_wallet_file( + destination: impl std::convert::AsRef, + owner_password: crate::OwnerPassword, + view_password: std::option::Option, + metadata: crate::WalletCreateMetadata, +) -> ksp_core_lib::Result { + return match crate::DEFAULT_WALLET_FORMAT { + crate::WalletFormat::V1 => create_wallet_file_v1(destination, owner_password, view_password, metadata).await, + crate::WalletFormat::V2 => create_wallet_file_v2(destination, owner_password, view_password, metadata).await, + }; +} + +/// Creates and no-clobber persists a new native `.kspwallet` V2 binary file. +pub async fn create_wallet_file_v2( + destination: impl std::convert::AsRef, + owner_password: crate::OwnerPassword, + view_password: std::option::Option, + metadata: crate::WalletCreateMetadata, +) -> ksp_core_lib::Result { + let destination = destination.as_ref().to_path_buf(); + let owner = match crate::create_wallet_v2(owner_password, view_password, metadata).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let serialized = match owner.to_native_bytes() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if let std::result::Result::Err(error) = persist_new_wallet_async(destination, serialized).await { + return std::result::Result::Err(error); + } + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + operation = "wallet_create_file", + format_version = crate::KSPWALLET_FORMAT_VERSION_V2, + "native wallet persisted with no-clobber semantics" + ); + return std::result::Result::Ok(owner); +} + /// Creates a new native `.kspwallet` V1 at `destination` without overwriting an existing path. /// /// The complete encrypted document is created in memory first, written and synchronized through a temporary file in the destination directory, then @@ -40,6 +83,30 @@ pub async fn create_wallet_file_v1( return std::result::Result::Ok(owner); } +/// Opens a supported native `.kspwallet` file with VIEW capability after bounded V1/V2 detection. +pub async fn open_wallet_view_file( + source: impl std::convert::AsRef, + password: crate::ViewPassword, +) -> ksp_core_lib::Result { + let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return crate::open_wallet_view(bytes.as_slice(), password).await; +} + +/// Opens a native `.kspwallet` V2 binary file with VIEW capability. +pub async fn open_wallet_view_file_v2( + source: impl std::convert::AsRef, + password: crate::ViewPassword, +) -> ksp_core_lib::Result { + let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return crate::open_wallet_view_v2(bytes.as_slice(), password).await; +} + /// Opens a native `.kspwallet` V1 from `source` with VIEW capability. /// /// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and VIEW KDF flow. @@ -56,6 +123,30 @@ pub async fn open_wallet_view_file_v1( return crate::open_wallet_view_v1(bytes.as_slice(), password).await; } +/// Opens a supported native `.kspwallet` file with OWNER capability after bounded V1/V2 detection. +pub async fn open_wallet_owner_file( + source: impl std::convert::AsRef, + password: crate::OwnerPassword, +) -> ksp_core_lib::Result { + let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return crate::open_wallet_owner(bytes.as_slice(), password).await; +} + +/// Opens a native `.kspwallet` V2 binary file with OWNER capability. +pub async fn open_wallet_owner_file_v2( + source: impl std::convert::AsRef, + password: crate::OwnerPassword, +) -> ksp_core_lib::Result { + let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return crate::open_wallet_owner_v2(bytes.as_slice(), password).await; +} + /// Opens a native `.kspwallet` V1 from `source` with OWNER capability. /// /// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and OWNER KDF flow. @@ -72,6 +163,24 @@ pub async fn open_wallet_owner_file_v1( return crate::open_wallet_owner_v1(bytes.as_slice(), password).await; } +/// Reads and verifies the locked projection of a supported native `.kspwallet` file after bounded V1/V2 detection. +pub async fn inspect_locked_wallet_file(source: impl std::convert::AsRef) -> ksp_core_lib::Result { + let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return crate::inspect_locked_wallet(bytes.as_slice()); +} + +/// Reads and verifies the locked projection of a native `.kspwallet` V2 binary file without running a password KDF. +pub async fn inspect_locked_wallet_file_v2(source: impl std::convert::AsRef) -> ksp_core_lib::Result { + let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return crate::inspect_locked_wallet_v2(bytes.as_slice()); +} + /// Reads and verifies the locked projection of a native `.kspwallet` V1 from `source` without running a password KDF. pub async fn inspect_locked_wallet_file_v1(source: impl std::convert::AsRef) -> ksp_core_lib::Result { let source = source.as_ref().to_path_buf(); @@ -88,6 +197,11 @@ pub(crate) async fn persist_new_wallet_content_v1(destination: std::path::PathBu return persist_new_wallet_async(destination, content).await; } +/// Persists one already serialized native Wallet document with no-clobber semantics. +pub(crate) async fn persist_new_wallet_content(destination: std::path::PathBuf, content: std::vec::Vec) -> ksp_core_lib::Result<()> { + return persist_new_wallet_async(destination, content).await; +} + /// Replaces wallet file v1. pub(crate) async fn replace_wallet_file_v1( destination: std::path::PathBuf, @@ -103,6 +217,21 @@ pub(crate) async fn replace_wallet_file_v1( }; } +/// Replaces one authenticated V2 Wallet file only if the current V2 state still matches the caller's expected state. +pub(crate) async fn replace_wallet_file_v2( + destination: std::path::PathBuf, + expected_current: crate::KspWalletEnvelopeV2, + content: std::vec::Vec, +) -> ksp_core_lib::Result<()> { + let task = tokio::task::spawn_blocking(move || { + return replace_wallet_file_v2_checked_blocking(destination.as_path(), &expected_current, content.as_slice()); + }); + return match task.await { + std::result::Result::Ok(result) => result, + std::result::Result::Err(error) => std::result::Result::Err(blocking_atomic_error("replace_task", error)), + }; +} + async fn read_wallet_file_async(source: std::path::PathBuf) -> ksp_core_lib::Result> { let task = tokio::task::spawn_blocking(move || return read_wallet_file_blocking(source.as_path())); return match task.await { @@ -162,6 +291,18 @@ fn replace_wallet_file_checked_blocking( return replace_wallet_file_with_hook(destination, content, || return verify_expected_wallet_state(destination, expected_current)); } +fn replace_wallet_file_v2_checked_blocking( + destination: &std::path::Path, + expected_current: &crate::KspWalletEnvelopeV2, + content: &[u8], +) -> ksp_core_lib::Result<()> { + let current_check = verify_expected_wallet_state_v2(destination, expected_current); + if let std::result::Result::Err(error) = current_check { + return std::result::Result::Err(error); + } + return replace_wallet_file_with_hook(destination, content, || return verify_expected_wallet_state_v2(destination, expected_current)); +} + #[cfg(test)] fn replace_wallet_file_blocking(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> { return replace_wallet_file_with_hook(destination, content, || return std::result::Result::Ok(())); @@ -186,6 +327,24 @@ fn verify_expected_wallet_state(destination: &std::path::Path, expected_current: return std::result::Result::Ok(()); } +fn verify_expected_wallet_state_v2(destination: &std::path::Path, expected_current: &crate::KspWalletEnvelopeV2) -> ksp_core_lib::Result<()> { + let current_bytes = match read_wallet_file_blocking(destination) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let current = match crate::KspWalletEnvelopeV2::parse_binary(current_bytes.as_slice()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if let std::result::Result::Err(error) = crate::verify_state_signature_v2(¤t) { + return std::result::Result::Err(error); + } + if ¤t != expected_current { + return std::result::Result::Err(state_conflict_error()); + } + return std::result::Result::Ok(()); +} + fn replace_wallet_file_with_hook(destination: &std::path::Path, content: &[u8], before_publish: F) -> ksp_core_lib::Result<()> where F: std::ops::FnOnce() -> ksp_core_lib::Result<()>, diff --git a/crates/ksp-wallet-lib/src/runtime.rs b/crates/ksp-wallet-lib/src/runtime.rs new file mode 100644 index 0000000..b0bd854 --- /dev/null +++ b/crates/ksp-wallet-lib/src/runtime.rs @@ -0,0 +1,233 @@ +// file: crates/ksp-wallet-lib/src/runtime.rs +// version: 1 + +//! Version-neutral unlocked Wallet state dispatch used by stable OWNER/VIEW handles. + +/// One staged native envelope produced by an authenticated administration operation. +pub(crate) enum StagedEnvelope { + V1(crate::KspWalletEnvelopeV1), + V2(crate::KspWalletEnvelopeV2), +} + +/// Version-neutral OWNER runtime state. +pub(crate) enum OwnerState { + V1(crate::OwnerStateV1), + V2(crate::OwnerStateV2), +} + +impl OwnerState { + /// Returns the native format version owned by this OWNER state. + pub(crate) const fn format_version(&self) -> u32 { + return match self { + Self::V1(_) => crate::KSPWALLET_FORMAT_VERSION_V1, + Self::V2(_) => crate::KSPWALLET_FORMAT_VERSION_V2, + }; + } + + /// Exports the Solana identity through the requested external transfer format. + pub(crate) fn export_transfer(&self, format: crate::WalletTransferFormat) -> ksp_core_lib::Result> { + return match self { + Self::V1(state) => state.export_transfer(format), + Self::V2(state) => state.export_transfer(format), + }; + } + + /// Signs one message with the OWNER-authorized Solana identity. + pub(crate) fn sign_message(&self, message: &[u8]) -> ksp_core_lib::Result<[u8; crate::KSPWALLET_SOLANA_SIGNATURE_BYTES]> { + return match self { + Self::V1(state) => state.sign_message(message), + Self::V2(state) => state.sign_message(message), + }; + } + + /// Stages an authenticated metadata replacement without publishing it. + pub(crate) fn stage_metadata_payload(&self, payload: crate::MetadataPayloadV1) -> ksp_core_lib::Result<(StagedEnvelope, crate::WalletInfo)> { + return match self { + Self::V1(state) => state.stage_metadata_payload(payload).map(|(envelope, info)| (StagedEnvelope::V1(envelope), info)), + Self::V2(state) => state.stage_metadata_payload(payload).map(|(envelope, info)| (StagedEnvelope::V2(envelope), info)), + }; + } + + /// Stages an OWNER credential rotation in the state native format. + pub(crate) async fn stage_owner_password_rotation(&self, password: crate::OwnerPassword) -> ksp_core_lib::Result { + return match self { + Self::V1(state) => state.stage_owner_password_rotation(password).await.map(StagedEnvelope::V1), + Self::V2(state) => state.stage_owner_password_rotation(password).await.map(StagedEnvelope::V2), + }; + } + + /// Stages a VIEW credential rotation in the state native format. + pub(crate) async fn stage_view_password_rotation(&self, password: crate::ViewPassword) -> ksp_core_lib::Result { + return match self { + Self::V1(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V1), + Self::V2(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V2), + }; + } + + /// Stages strong VIEW disable while retaining OWNER authority. + pub(crate) fn stage_disable_view(&self) -> ksp_core_lib::Result<(StagedEnvelope, crate::SecretKeyV1)> { + return match self { + Self::V1(state) => state.stage_disable_view().map(|(envelope, key)| (StagedEnvelope::V1(envelope), key)), + Self::V2(state) => state.stage_disable_view().map(|(envelope, key)| (StagedEnvelope::V2(envelope), key)), + }; + } + + /// Stages strong VIEW recreation under a new credential. + pub(crate) async fn stage_recreate_view(&self, password: crate::ViewPassword) -> ksp_core_lib::Result<(StagedEnvelope, crate::SecretKeyV1)> { + return match self { + Self::V1(state) => state.stage_recreate_view(password).await.map(|(envelope, key)| (StagedEnvelope::V1(envelope), key)), + Self::V2(state) => state.stage_recreate_view(password).await.map(|(envelope, key)| (StagedEnvelope::V2(envelope), key)), + }; + } + + /// Atomically publishes a staged envelope after format-specific state-conflict verification. + pub(crate) async fn persist_staged(&self, destination: std::path::PathBuf, envelope: &StagedEnvelope) -> ksp_core_lib::Result<()> { + return match (self, envelope) { + (Self::V1(state), StagedEnvelope::V1(staged)) => { + let serialized = match staged.to_json_bytes() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + crate::replace_wallet_file_v1(destination, state.envelope().clone(), serialized).await + }, + (Self::V2(state), StagedEnvelope::V2(staged)) => { + let serialized = match staged.to_binary_bytes() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + crate::replace_wallet_file_v2(destination, state.envelope().clone(), serialized).await + }, + _ => std::result::Result::Err(version_state_error()), + }; + } + + /// Replaces the in-memory authenticated envelope after successful persistence. + pub(crate) fn apply_envelope(&mut self, envelope: StagedEnvelope) -> ksp_core_lib::Result<()> { + return match (self, envelope) { + (Self::V1(state), StagedEnvelope::V1(value)) => { + state.apply_envelope(value); + std::result::Result::Ok(()) + }, + (Self::V2(state), StagedEnvelope::V2(value)) => { + state.apply_envelope(value); + std::result::Result::Ok(()) + }, + _ => std::result::Result::Err(version_state_error()), + }; + } + + /// Replaces the envelope and metadata key after a strong VIEW administration operation. + pub(crate) fn apply_strong_view_state(&mut self, envelope: StagedEnvelope, metadata_key: crate::SecretKeyV1) -> ksp_core_lib::Result<()> { + return match (self, envelope) { + (Self::V1(state), StagedEnvelope::V1(value)) => { + state.apply_strong_view_state(value, metadata_key); + std::result::Result::Ok(()) + }, + (Self::V2(state), StagedEnvelope::V2(value)) => { + state.apply_strong_view_state(value, metadata_key); + std::result::Result::Ok(()) + }, + _ => std::result::Result::Err(version_state_error()), + }; + } + + /// Serializes the authenticated state in its native V1 or V2 wire format. + pub(crate) fn native_bytes(&self) -> ksp_core_lib::Result> { + return match self { + Self::V1(state) => state.envelope().to_json_bytes(), + Self::V2(state) => state.envelope().to_binary_bytes(), + }; + } + + /// Serializes only V1 state as JSON and rejects V2 instead of converting formats. + pub(crate) fn json_bytes_v1(&self) -> ksp_core_lib::Result> { + return match self { + Self::V1(state) => state.envelope().to_json_bytes(), + Self::V2(_) => std::result::Result::Err(v1_serialization_error()), + }; + } +} + +/// Version-neutral VIEW runtime state. +pub(crate) enum ViewState { + V1(crate::ViewStateV1), + V2(crate::ViewStateV2), +} + +impl ViewState { + /// Returns the native format version owned by this VIEW state. + pub(crate) const fn format_version(&self) -> u32 { + return match self { + Self::V1(_) => crate::KSPWALLET_FORMAT_VERSION_V1, + Self::V2(_) => crate::KSPWALLET_FORMAT_VERSION_V2, + }; + } + + /// Stages a VIEW credential rotation in the state native format. + pub(crate) async fn stage_view_password_rotation(&self, password: crate::ViewPassword) -> ksp_core_lib::Result { + return match self { + Self::V1(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V1), + Self::V2(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V2), + }; + } + + /// Atomically publishes a staged envelope after format-specific state-conflict verification. + pub(crate) async fn persist_staged(&self, destination: std::path::PathBuf, envelope: &StagedEnvelope) -> ksp_core_lib::Result<()> { + return match (self, envelope) { + (Self::V1(state), StagedEnvelope::V1(staged)) => { + let serialized = match staged.to_json_bytes() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + crate::replace_wallet_file_v1(destination, state.envelope().clone(), serialized).await + }, + (Self::V2(state), StagedEnvelope::V2(staged)) => { + let serialized = match staged.to_binary_bytes() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + crate::replace_wallet_file_v2(destination, state.envelope().clone(), serialized).await + }, + _ => std::result::Result::Err(version_state_error()), + }; + } + + /// Replaces the in-memory authenticated envelope after successful persistence. + pub(crate) fn apply_envelope(&mut self, envelope: StagedEnvelope) -> ksp_core_lib::Result<()> { + return match (self, envelope) { + (Self::V1(state), StagedEnvelope::V1(value)) => { + state.apply_envelope(value); + std::result::Result::Ok(()) + }, + (Self::V2(state), StagedEnvelope::V2(value)) => { + state.apply_envelope(value); + std::result::Result::Ok(()) + }, + _ => std::result::Result::Err(version_state_error()), + }; + } + + /// Serializes the authenticated state in its native V1 or V2 wire format. + pub(crate) fn native_bytes(&self) -> ksp_core_lib::Result> { + return match self { + Self::V1(state) => state.envelope().to_json_bytes(), + Self::V2(state) => state.envelope().to_binary_bytes(), + }; + } + + /// Serializes only V1 state as JSON and rejects V2 instead of converting formats. + pub(crate) fn json_bytes_v1(&self) -> ksp_core_lib::Result> { + return match self { + Self::V1(state) => state.envelope().to_json_bytes(), + Self::V2(_) => std::result::Result::Err(v1_serialization_error()), + }; + } +} + +fn version_state_error() -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet runtime state and staged format are inconsistent"); +} + +fn v1_serialization_error() -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet is not a V1 JSON document"); +} diff --git a/crates/ksp-wallet-lib/src/transfer.rs b/crates/ksp-wallet-lib/src/transfer.rs index 2cd39d4..2dddd5b 100644 --- a/crates/ksp-wallet-lib/src/transfer.rs +++ b/crates/ksp-wallet-lib/src/transfer.rs @@ -1,5 +1,5 @@ // file: crates/ksp-wallet-lib/src/transfer.rs -// version: 3 +// version: 4 //! Explicit OWNER-only Solana keypair import/export adapters. @@ -9,7 +9,7 @@ use std::io::Write; // rust-rules: trait-import use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import use zeroize::Zeroize; // rust-rules: trait-import -/// Explicit secret-transfer formats supported by Wallet `0.2.5`. +/// Explicit secret-transfer formats supported by Wallet. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WalletTransferFormat { @@ -106,6 +106,55 @@ pub async fn inspect_wallet_transfer_file( return inspect_wallet_transfer(bytes.as_slice(), format); } +/// Imports one in-memory Solana keypair transfer payload into a new native Wallet using [`crate::DEFAULT_WALLET_FORMAT`]. +pub async fn import_wallet_transfer( + destination: impl std::convert::AsRef, + source: &[u8], + format: WalletTransferFormat, + owner_password: crate::OwnerPassword, + view_password: std::option::Option, + metadata: crate::WalletCreateMetadata, +) -> ksp_core_lib::Result { + return match crate::DEFAULT_WALLET_FORMAT { + crate::WalletFormat::V1 => import_wallet_transfer_v1(destination, source, format, owner_password, view_password, metadata).await, + crate::WalletFormat::V2 => import_wallet_transfer_v2(destination, source, format, owner_password, view_password, metadata).await, + }; +} + +/// Imports one in-memory Solana keypair transfer payload into a new native `.kspwallet` V2 binary file. +pub async fn import_wallet_transfer_v2( + destination: impl std::convert::AsRef, + source: &[u8], + format: WalletTransferFormat, + owner_password: crate::OwnerPassword, + view_password: std::option::Option, + metadata: crate::WalletCreateMetadata, +) -> ksp_core_lib::Result { + let keypair = match decode_transfer_keypair(source, format) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let owner = match crate::create_wallet_v2_from_keypair(owner_password, view_password, metadata, keypair).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let serialized = match owner.to_native_bytes() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if let std::result::Result::Err(error) = crate::persist_new_wallet_content(destination.as_ref().to_path_buf(), serialized).await { + return std::result::Result::Err(error); + } + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + operation = "wallet_import_transfer", + format_version = crate::KSPWALLET_FORMAT_VERSION_V2, + transfer_format = format.code(), + "external Solana keypair imported into a new native wallet" + ); + return std::result::Result::Ok(owner); +} + /// Imports one in-memory Solana keypair transfer payload into a new native `.kspwallet` V1 file. /// /// Import always creates a fresh native Wallet envelope around the validated immutable Solana keypair and publishes with the same no-clobber semantics as @@ -143,6 +192,38 @@ pub async fn import_wallet_transfer_v1( return std::result::Result::Ok(owner); } +/// Imports one bounded external transfer file into a new native Wallet using [`crate::DEFAULT_WALLET_FORMAT`]. +pub async fn import_wallet_transfer_file( + destination: impl std::convert::AsRef, + source: impl std::convert::AsRef, + format: WalletTransferFormat, + owner_password: crate::OwnerPassword, + view_password: std::option::Option, + metadata: crate::WalletCreateMetadata, +) -> ksp_core_lib::Result { + let bytes = match read_transfer_file_async(source.as_ref().to_path_buf(), format).await { + std::result::Result::Ok(value) => zeroize::Zeroizing::new(value), + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return import_wallet_transfer(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await; +} + +/// Imports one bounded external transfer file into a new native `.kspwallet` V2 binary file. +pub async fn import_wallet_transfer_file_v2( + destination: impl std::convert::AsRef, + source: impl std::convert::AsRef, + format: WalletTransferFormat, + owner_password: crate::OwnerPassword, + view_password: std::option::Option, + metadata: crate::WalletCreateMetadata, +) -> ksp_core_lib::Result { + let bytes = match read_transfer_file_async(source.as_ref().to_path_buf(), format).await { + std::result::Result::Ok(value) => zeroize::Zeroizing::new(value), + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return import_wallet_transfer_v2(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await; +} + /// Imports one bounded external transfer file into a new native `.kspwallet` V1 file. /// /// The source file is never modified and the destination remains no-clobber. @@ -161,8 +242,8 @@ pub async fn import_wallet_transfer_file_v1( return import_wallet_transfer_v1(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await; } -/// Executes the crate-internal write wallet transfer file v1 operation for the owning module. -pub(crate) async fn write_wallet_transfer_file_v1( +/// Writes one bounded OWNER export using no-clobber external transfer semantics. +pub(crate) async fn write_wallet_transfer_file( destination: std::path::PathBuf, content: std::vec::Vec, format: WalletTransferFormat, diff --git a/crates/ksp-wallet-lib/src/view.rs b/crates/ksp-wallet-lib/src/view.rs index cd6213b..5d51eb0 100644 --- a/crates/ksp-wallet-lib/src/view.rs +++ b/crates/ksp-wallet-lib/src/view.rs @@ -1,5 +1,5 @@ // file: crates/ksp-wallet-lib/src/view.rs -// version: 4 +// version: 5 /// Authorized VIEW capability handle. /// @@ -7,15 +7,21 @@ /// Solana secret, OWNER administration material or metadata-write authority. pub struct WalletView { info: crate::WalletInfo, - state: crate::ViewStateV1, + state: crate::ViewState, } impl WalletView { /// Builds `WalletView` from unlocked. - pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::ViewStateV1) -> Self { + pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::ViewState) -> Self { return Self { info, state }; } + /// Returns the native Wallet format version backing this authorized handle. + #[must_use] + pub const fn format_version(&self) -> u32 { + return self.info.format_version(); + } + /// Returns the authorization capability represented by this handle. #[must_use] pub const fn capability(&self) -> crate::WalletCapability { @@ -65,15 +71,13 @@ impl WalletView { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let serialized = match envelope.to_json_bytes() { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - let persist_result = crate::replace_wallet_file_v1(destination.as_ref().to_path_buf(), self.state.envelope().clone(), serialized).await; + let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await; if let std::result::Result::Err(error) = persist_result { return std::result::Result::Err(error); } - self.state.apply_envelope(envelope); + if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) { + return std::result::Result::Err(error); + } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, operation = "wallet_rotate_view_password", @@ -83,9 +87,16 @@ impl WalletView { return std::result::Result::Ok(()); } - /// Serializes the unchanged locked `.kspwallet` V1 document without exposing the metadata content key. + /// Serializes the unchanged locked native Wallet in its current V1 or V2 wire format. + pub fn to_native_bytes(&self) -> ksp_core_lib::Result> { + return self.state.native_bytes(); + } + + /// Serializes a V1 handle as its historical JSON document. + /// + /// V2 handles return a format error instead of being converted implicitly. pub fn to_json_bytes(&self) -> ksp_core_lib::Result> { - return self.state.envelope().to_json_bytes(); + return self.state.json_bytes_v1(); } } diff --git a/crates/ksp-wallet-lib/src/wallet.rs b/crates/ksp-wallet-lib/src/wallet.rs index 0c3fe0e..e4ba675 100644 --- a/crates/ksp-wallet-lib/src/wallet.rs +++ b/crates/ksp-wallet-lib/src/wallet.rs @@ -1,5 +1,5 @@ // file: crates/ksp-wallet-lib/src/wallet.rs -// version: 8 +// version: 9 //! In-memory native Wallet V1 create/open orchestration. @@ -143,7 +143,7 @@ impl OwnerStateV1 { if let std::result::Result::Err(error) = verify_result { return std::result::Result::Err(error); } - return std::result::Result::Ok((envelope, payload.into_info(crate::WalletCapability::Owner))); + return std::result::Result::Ok((envelope, payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner))); } /// Executes the crate-internal stage owner password rotation operation for `OwnerStateV1`. @@ -497,7 +497,7 @@ pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) - std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let info = metadata_payload.into_info(crate::WalletCapability::View); + let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::View); let state = ViewStateV1::new(envelope, metadata_key); ksp_logging_lib::debug!( target: crate::TRACING_TARGET, @@ -506,7 +506,7 @@ pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) - capability = "view", "native wallet VIEW capability opened" ); - return std::result::Result::Ok(crate::WalletView::from_unlocked(info, state)); + return std::result::Result::Ok(crate::WalletView::from_unlocked(info, crate::ViewState::V1(state))); } /// Opens the OWNER capability from a native `.kspwallet` V1 JSON document. @@ -602,7 +602,7 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword) if &secret_pubkey != metadata_payload.pubkey() { return std::result::Result::Err(key_material_error()); } - let info = metadata_payload.into_info(crate::WalletCapability::Owner); + let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner); let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair); ksp_logging_lib::debug!( target: crate::TRACING_TARGET, @@ -611,7 +611,7 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword) capability = "owner", "native wallet OWNER capability opened" ); - return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state)); + return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, crate::OwnerState::V1(state))); } /// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material. @@ -624,7 +624,7 @@ pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result, + metadata_key: std::option::Option, + secret_key: std::option::Option, + admin_signing_key: std::option::Option, + solana_keypair: std::option::Option, +} + +impl OwnerStateV2 { + /// Creates a new `OwnerStateV2` value. + pub(crate) fn new( + envelope: crate::KspWalletEnvelopeV2, + owner_root: crate::SecretKeyV1, + metadata_key: crate::SecretKeyV1, + secret_key: crate::SecretKeyV1, + admin_signing_key: ed25519_dalek::SigningKey, + solana_keypair: solana_keypair::Keypair, + ) -> Self { + return Self { + envelope, + owner_root: std::option::Option::Some(owner_root), + metadata_key: std::option::Option::Some(metadata_key), + secret_key: std::option::Option::Some(secret_key), + admin_signing_key: std::option::Option::Some(admin_signing_key), + solana_keypair: std::option::Option::Some(solana_keypair), + }; + } + + /// Returns the current envelope. + pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV2 { + return &self.envelope; + } + + /// Applies envelope. + pub(crate) fn apply_envelope(&mut self, envelope: crate::KspWalletEnvelopeV2) { + self.envelope = envelope; + return; + } + + /// Applies strong view state. + pub(crate) fn apply_strong_view_state(&mut self, envelope: crate::KspWalletEnvelopeV2, metadata_key: crate::SecretKeyV1) { + self.envelope = envelope; + let previous = self.metadata_key.replace(metadata_key); + drop(previous); + return; + } + + /// Executes the crate-internal export transfer operation for `OwnerStateV2`. + pub(crate) fn export_transfer(&self, format: crate::WalletTransferFormat) -> ksp_core_lib::Result> { + let keypair = match self.solana_keypair.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(key_material_error()), + }; + return match format { + crate::WalletTransferFormat::SolanaCliJson => { + let mut keypair_bytes = keypair.to_bytes(); + let encoded = serde_json::to_vec(keypair_bytes.as_slice()); + keypair_bytes.zeroize(); + match encoded { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(_) => std::result::Result::Err(key_material_error()), + } + }, + crate::WalletTransferFormat::SolanaKeypairBase58 => { + let mut encoded = keypair.to_base58_string(); + let output = encoded.as_bytes().to_vec(); + encoded.zeroize(); + std::result::Result::Ok(output) + }, + }; + } + + /// Executes the crate-internal sign message operation for `OwnerStateV2`. + pub(crate) fn sign_message(&self, message: &[u8]) -> ksp_core_lib::Result<[u8; crate::KSPWALLET_SOLANA_SIGNATURE_BYTES]> { + let keypair = match self.solana_keypair.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(signing_error()), + }; + let mut keypair_bytes = keypair.to_bytes(); + let mut seed = [0_u8; crate::SECRET_KEY_BYTES]; + seed.copy_from_slice(&keypair_bytes[0..crate::SECRET_KEY_BYTES]); + let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed); + let signature = signing_key.sign(message).to_bytes(); + seed.zeroize(); + keypair_bytes.zeroize(); + return std::result::Result::Ok(signature); + } + + /// Executes the crate-internal stage metadata payload operation for `OwnerStateV2`. + pub(crate) fn stage_metadata_payload(&self, payload: crate::MetadataPayloadV1) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV2, crate::WalletInfo)> { + let metadata_key = match self.metadata_key.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), + }; + let admin_signing_key = match self.admin_signing_key.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), + }; + let mut plaintext = match payload.encode() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let nonce = match crate::random_nonce() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + plaintext.zeroize(); + return std::result::Result::Err(error); + }, + }; + let encrypted = crate::encrypt_bytes( + metadata_key, + &nonce, + self.envelope.compartment_aad(crate::WalletCompartmentKindV2::Metadata).as_slice(), + plaintext.as_slice(), + ); + plaintext.zeroize(); + let ciphertext = match encrypted { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let metadata = crate::WalletEncryptedCompartmentV2::new_internal( + crate::WalletCompartmentKindV2::Metadata, + crate::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION, + nonce, + ciphertext, + ); + let envelope = resign_envelope( + &self.envelope, + *self.envelope.view_descriptor(), + self.envelope.owner_slot().clone(), + self.envelope.view_slot().cloned(), + self.envelope.owner_control().clone(), + metadata, + self.envelope.secret().clone(), + admin_signing_key, + ); + let verify_result = verify_state_signature_v2(&envelope); + if let std::result::Result::Err(error) = verify_result { + return std::result::Result::Err(error); + } + return std::result::Result::Ok((envelope, payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V2, crate::WalletCapability::Owner))); + } + + /// Executes the crate-internal stage owner password rotation operation for `OwnerStateV2`. + pub(crate) async fn stage_owner_password_rotation(&self, new_password: crate::OwnerPassword) -> ksp_core_lib::Result { + let owner_root = match self.owner_root.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), + }; + let admin_signing_key = match self.admin_signing_key.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), + }; + let owner_slot = match rewrap_slot( + &self.envelope, + *self.envelope.owner_slot().slot_id(), + crate::WalletKeySlotRoleV2::Owner, + owner_root, + PasswordRotationV2::Owner(new_password), + ) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let envelope = resign_envelope( + &self.envelope, + *self.envelope.view_descriptor(), + owner_slot, + self.envelope.view_slot().cloned(), + self.envelope.owner_control().clone(), + self.envelope.metadata().clone(), + self.envelope.secret().clone(), + admin_signing_key, + ); + return verify_and_return(envelope); + } + + /// Executes the crate-internal stage view password rotation operation for `OwnerStateV2`. + pub(crate) async fn stage_view_password_rotation(&self, new_password: crate::ViewPassword) -> ksp_core_lib::Result { + let metadata_key = match self.metadata_key.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), + }; + let view_slot = match self.envelope.view_slot() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")), + }; + let new_slot = + match rewrap_slot(&self.envelope, *view_slot.slot_id(), crate::WalletKeySlotRoleV2::View, metadata_key, PasswordRotationV2::View(new_password)) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let envelope = crate::KspWalletEnvelopeV2::new_internal( + *self.envelope.owner_auth_public_key(), + *self.envelope.view_descriptor(), + self.envelope.owner_slot().clone(), + std::option::Option::Some(new_slot), + self.envelope.owner_control().clone(), + self.envelope.metadata().clone(), + self.envelope.secret().clone(), + self.envelope.state_signature().clone(), + ); + return verify_and_return(envelope); + } + + /// Executes the crate-internal stage disable view operation for `OwnerStateV2`. + pub(crate) fn stage_disable_view(&self) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV2, crate::SecretKeyV1)> { + if !self.envelope.view_descriptor().enabled() { + return std::result::Result::Err(capability_error("Wallet VIEW capability is already disabled")); + } + return self.stage_strong_view_change(std::option::Option::None); + } + + /// Executes the crate-internal stage recreate view operation for `OwnerStateV2`. + pub(crate) async fn stage_recreate_view( + &self, + new_password: crate::ViewPassword, + ) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV2, crate::SecretKeyV1)> { + let slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V2_SLOT_ID_BYTES }>() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let new_metadata_key = match crate::SecretKeyV1::random() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let view_slot = + match rewrap_slot(&self.envelope, slot_id, crate::WalletKeySlotRoleV2::View, &new_metadata_key, PasswordRotationV2::View(new_password)).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return self.stage_strong_view_change_with_key(std::option::Option::Some(view_slot), new_metadata_key); + } + + fn stage_strong_view_change( + &self, + view_slot: std::option::Option, + ) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV2, crate::SecretKeyV1)> { + let new_metadata_key = match crate::SecretKeyV1::random() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return self.stage_strong_view_change_with_key(view_slot, new_metadata_key); + } + + fn stage_strong_view_change_with_key( + &self, + view_slot: std::option::Option, + new_metadata_key: crate::SecretKeyV1, + ) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV2, crate::SecretKeyV1)> { + let current_metadata_key = match self.metadata_key.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), + }; + let owner_root = match self.owner_root.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), + }; + let secret_key = match self.secret_key.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), + }; + let admin_signing_key = match self.admin_signing_key.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), + }; + let current_plaintext_result = crate::decrypt_bytes( + current_metadata_key, + self.envelope.metadata().nonce(), + self.envelope.compartment_aad(crate::WalletCompartmentKindV2::Metadata).as_slice(), + self.envelope.metadata().ciphertext(), + ); + let mut metadata_plaintext = match current_plaintext_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()), + }; + let metadata_nonce = match crate::random_nonce() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + metadata_plaintext.zeroize(); + return std::result::Result::Err(error); + }, + }; + let metadata_ciphertext_result = crate::encrypt_bytes( + &new_metadata_key, + &metadata_nonce, + self.envelope.compartment_aad(crate::WalletCompartmentKindV2::Metadata).as_slice(), + metadata_plaintext.as_slice(), + ); + metadata_plaintext.zeroize(); + let metadata_ciphertext = match metadata_ciphertext_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let metadata = crate::WalletEncryptedCompartmentV2::new_internal( + crate::WalletCompartmentKindV2::Metadata, + crate::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION, + metadata_nonce, + metadata_ciphertext, + ); + let mut admin_secret = admin_signing_key.to_bytes(); + let mut owner_control_plaintext = crate::encode_owner_control_payload(&admin_secret, &new_metadata_key, secret_key); + admin_secret.zeroize(); + let owner_control_nonce = match crate::random_nonce() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + owner_control_plaintext.zeroize(); + return std::result::Result::Err(error); + }, + }; + let owner_control_ciphertext_result = crate::encrypt_bytes( + owner_root, + &owner_control_nonce, + self.envelope.compartment_aad(crate::WalletCompartmentKindV2::OwnerControl).as_slice(), + owner_control_plaintext.as_slice(), + ); + owner_control_plaintext.zeroize(); + let owner_control_ciphertext = match owner_control_ciphertext_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let owner_control = crate::WalletEncryptedCompartmentV2::new_internal( + crate::WalletCompartmentKindV2::OwnerControl, + crate::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION, + owner_control_nonce, + owner_control_ciphertext, + ); + let view_descriptor = match view_slot.as_ref() { + std::option::Option::Some(slot) => crate::WalletViewDescriptorV2::enabled_for_slot(*slot.slot_id()), + std::option::Option::None => crate::WalletViewDescriptorV2::disabled(), + }; + let envelope = resign_envelope( + &self.envelope, + view_descriptor, + self.envelope.owner_slot().clone(), + view_slot, + owner_control, + metadata, + self.envelope.secret().clone(), + admin_signing_key, + ); + let envelope = match verify_and_return(envelope) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok((envelope, new_metadata_key)); + } +} + +impl std::ops::Drop for OwnerStateV2 { + fn drop(&mut self) { + let owner_root = self.owner_root.take(); + let metadata_key = self.metadata_key.take(); + let secret_key = self.secret_key.take(); + let admin_signing_key = self.admin_signing_key.take(); + let solana_keypair = self.solana_keypair.take(); + drop(owner_root); + drop(metadata_key); + drop(secret_key); + drop(admin_signing_key); + drop(solana_keypair); + } +} + +/// Crate-internal `ViewStateV2` state shared across the owning crate. +pub(crate) struct ViewStateV2 { + envelope: crate::KspWalletEnvelopeV2, + metadata_key: std::option::Option, +} + +impl ViewStateV2 { + /// Creates a new `ViewStateV2` value. + pub(crate) fn new(envelope: crate::KspWalletEnvelopeV2, metadata_key: crate::SecretKeyV1) -> Self { + return Self { envelope, metadata_key: std::option::Option::Some(metadata_key) }; + } + + /// Returns the current envelope. + pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV2 { + return &self.envelope; + } + + /// Applies envelope. + pub(crate) fn apply_envelope(&mut self, envelope: crate::KspWalletEnvelopeV2) { + self.envelope = envelope; + return; + } + + /// Executes the crate-internal stage view password rotation operation for `ViewStateV2`. + pub(crate) async fn stage_view_password_rotation(&self, new_password: crate::ViewPassword) -> ksp_core_lib::Result { + let metadata_key = match self.metadata_key.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(crypto_operation_error()), + }; + let view_slot = match self.envelope.view_slot() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")), + }; + let new_slot = + match rewrap_slot(&self.envelope, *view_slot.slot_id(), crate::WalletKeySlotRoleV2::View, metadata_key, PasswordRotationV2::View(new_password)) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let envelope = crate::KspWalletEnvelopeV2::new_internal( + *self.envelope.owner_auth_public_key(), + *self.envelope.view_descriptor(), + self.envelope.owner_slot().clone(), + std::option::Option::Some(new_slot), + self.envelope.owner_control().clone(), + self.envelope.metadata().clone(), + self.envelope.secret().clone(), + self.envelope.state_signature().clone(), + ); + return verify_and_return(envelope); + } +} + +impl std::ops::Drop for ViewStateV2 { + fn drop(&mut self) { + let metadata_key = self.metadata_key.take(); + drop(metadata_key); + } +} + +enum PasswordRotationV2 { + Owner(crate::OwnerPassword), + View(crate::ViewPassword), +} + +struct ViewCreationMaterialV2 { + slot_id: [u8; crate::KSPWALLET_V2_SLOT_ID_BYTES], + kdf: crate::WalletKdfParametersV2, + wrap_nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], + password: crate::ViewPassword, +} + +/// Creates a new in-memory native `.kspwallet` V2 with a fresh Solana keypair. +/// +/// The optional VIEW password creates an independent VIEW slot. This function performs no filesystem I/O; use [`crate::create_wallet_file_v2`] when +/// the caller also wants no-clobber native filesystem publication. +pub async fn create_wallet_v2( + owner_password: crate::OwnerPassword, + view_password: std::option::Option, + metadata: crate::WalletCreateMetadata, +) -> ksp_core_lib::Result { + let mut solana_secret = match crate::random_bytes::<32>() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let solana_keypair = solana_keypair::Keypair::new_from_array(solana_secret); + solana_secret.zeroize(); + return crate::create_wallet_v2_from_keypair(owner_password, view_password, metadata, solana_keypair).await; +} + +/// Opens the VIEW capability from a native `.kspwallet` V2 binary document. +pub async fn open_wallet_view_v2(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result { + let envelope = match crate::KspWalletEnvelopeV2::parse_binary(source) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let verify_result = verify_state_signature_v2(&envelope); + if let std::result::Result::Err(error) = verify_result { + return std::result::Result::Err(error); + } + let view_slot = match envelope.view_slot() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")), + }; + let derived_result = derive_view_password_key_async(password, view_slot.kdf().clone()).await; + let derived = match derived_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let aad = envelope.view_slot_aad(); + let view_aad = match aad { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")), + }; + let metadata_key_result = crate::unwrap_key(&derived, view_slot.wrap().nonce(), view_aad.as_slice(), view_slot.wrap().ciphertext()); + let metadata_key = match metadata_key_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()), + }; + let metadata_plaintext_result = crate::decrypt_bytes( + &metadata_key, + envelope.metadata().nonce(), + envelope.compartment_aad(crate::WalletCompartmentKindV2::Metadata).as_slice(), + envelope.metadata().ciphertext(), + ); + let mut metadata_plaintext = match metadata_plaintext_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()), + }; + let metadata_payload_result = crate::decode_metadata_payload(metadata_plaintext.as_slice()); + metadata_plaintext.zeroize(); + let metadata_payload = match metadata_payload_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V2, crate::WalletCapability::View); + let state = ViewStateV2::new(envelope, metadata_key); + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + operation = "wallet_open_view", + format_version = crate::KSPWALLET_FORMAT_VERSION_V2, + capability = "view", + "native wallet VIEW capability opened" + ); + return std::result::Result::Ok(crate::WalletView::from_unlocked(info, crate::ViewState::V2(state))); +} + +/// Opens the OWNER capability from a native `.kspwallet` V2 binary document. +pub async fn open_wallet_owner_v2(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result { + let envelope = match crate::KspWalletEnvelopeV2::parse_binary(source) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let verify_result = verify_state_signature_v2(&envelope); + if let std::result::Result::Err(error) = verify_result { + return std::result::Result::Err(error); + } + let owner_slot = envelope.owner_slot(); + let derived_result = derive_owner_password_key_async(password, owner_slot.kdf().clone()).await; + let derived = match derived_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let owner_root_result = crate::unwrap_key(&derived, owner_slot.wrap().nonce(), envelope.owner_slot_aad().as_slice(), owner_slot.wrap().ciphertext()); + let owner_root = match owner_root_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()), + }; + let owner_control_plaintext_result = crate::decrypt_bytes( + &owner_root, + envelope.owner_control().nonce(), + envelope.compartment_aad(crate::WalletCompartmentKindV2::OwnerControl).as_slice(), + envelope.owner_control().ciphertext(), + ); + let mut owner_control_plaintext = match owner_control_plaintext_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()), + }; + let control_result = crate::decode_owner_control_payload(owner_control_plaintext.as_slice()); + owner_control_plaintext.zeroize(); + let control = match control_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let (mut admin_secret, metadata_key, secret_key) = control.into_parts(); + let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret); + admin_secret.zeroize(); + if admin_signing_key.verifying_key().to_bytes() != *envelope.owner_auth_public_key() { + return std::result::Result::Err(authentication_error()); + } + let metadata_plaintext_result = crate::decrypt_bytes( + &metadata_key, + envelope.metadata().nonce(), + envelope.compartment_aad(crate::WalletCompartmentKindV2::Metadata).as_slice(), + envelope.metadata().ciphertext(), + ); + let mut metadata_plaintext = match metadata_plaintext_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()), + }; + let metadata_payload_result = crate::decode_metadata_payload(metadata_plaintext.as_slice()); + metadata_plaintext.zeroize(); + let metadata_payload = match metadata_payload_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let secret_plaintext_result = crate::decrypt_bytes( + &secret_key, + envelope.secret().nonce(), + envelope.compartment_aad(crate::WalletCompartmentKindV2::Secret).as_slice(), + envelope.secret().ciphertext(), + ); + let mut secret_plaintext = match secret_plaintext_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()), + }; + if secret_plaintext.len() != crate::KSPWALLET_V2_SECRET_PLAINTEXT_BYTES { + secret_plaintext.zeroize(); + return std::result::Result::Err(key_material_error()); + } + let keypair_result = solana_keypair::Keypair::try_from(secret_plaintext.as_slice()); + let solana_keypair = match keypair_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + secret_plaintext.zeroize(); + return std::result::Result::Err(key_material_error()); + }, + }; + let secret_pubkey_result = pubkey_from_keypair_bytes(secret_plaintext.as_slice()); + let secret_pubkey = match secret_pubkey_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + secret_plaintext.zeroize(); + return std::result::Result::Err(error); + }, + }; + secret_plaintext.zeroize(); + if &secret_pubkey != metadata_payload.pubkey() { + return std::result::Result::Err(key_material_error()); + } + let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V2, crate::WalletCapability::Owner); + let state = OwnerStateV2::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair); + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + operation = "wallet_open_owner", + format_version = crate::KSPWALLET_FORMAT_VERSION_V2, + capability = "owner", + "native wallet OWNER capability opened" + ); + return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, crate::OwnerState::V2(state))); +} + +/// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material. +pub fn inspect_locked_wallet_v2(source: &[u8]) -> ksp_core_lib::Result { + let envelope = match crate::KspWalletEnvelopeV2::parse_binary(source) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let verify_result = verify_state_signature_v2(&envelope); + if let std::result::Result::Err(error) = verify_result { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(crate::LockedWalletInfo::new(crate::KSPWALLET_FORMAT_VERSION_V2, envelope.view_descriptor().enabled())); +} + +/// Executes the crate-internal create wallet v2 from keypair operation for the owning module. +pub(crate) async fn create_wallet_v2_from_keypair( + owner_password: crate::OwnerPassword, + view_password: std::option::Option, + metadata: crate::WalletCreateMetadata, + solana_keypair: solana_keypair::Keypair, +) -> ksp_core_lib::Result { + let owner_root = match crate::SecretKeyV1::random() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let metadata_key = match crate::SecretKeyV1::random() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let secret_key = match crate::SecretKeyV1::random() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut admin_secret = match crate::random_bytes::<{ crate::SECRET_KEY_BYTES }>() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret); + let owner_auth_public_key = admin_signing_key.verifying_key().to_bytes(); + let mut solana_keypair_bytes = solana_keypair.to_bytes(); + let pubkey_result = pubkey_from_keypair_bytes(&solana_keypair_bytes); + let pubkey = match pubkey_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + admin_secret.zeroize(); + solana_keypair_bytes.zeroize(); + return std::result::Result::Err(error); + }, + }; + let metadata_encoded_result = crate::encode_initial_metadata_payload(pubkey, metadata); + let (mut metadata_plaintext, metadata_payload) = match metadata_encoded_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + admin_secret.zeroize(); + solana_keypair_bytes.zeroize(); + return std::result::Result::Err(error); + }, + }; + let mut owner_control_plaintext = crate::encode_owner_control_payload(&admin_secret, &metadata_key, &secret_key); + admin_secret.zeroize(); + let owner_slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V2_SLOT_ID_BYTES }>() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + let owner_salt = match crate::random_bytes::<{ crate::KSPWALLET_V2_DEFAULT_KDF_SALT_BYTES }>() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + let owner_kdf = crate::WalletKdfParametersV2::new_creation(owner_salt.to_vec()); + let owner_wrap_nonce = match crate::random_nonce() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + let view_material_result = prepare_view_creation(view_password); + let view_material = match view_material_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + let owner_control_nonce = match crate::random_nonce() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + let metadata_nonce = match crate::random_nonce() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + let secret_nonce = match crate::random_nonce() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + let provisional = provisional_envelope( + owner_auth_public_key, + owner_slot_id, + owner_kdf.clone(), + owner_wrap_nonce, + view_material.as_ref(), + owner_control_nonce, + metadata_nonce, + secret_nonce, + ); + let owner_derived_result = derive_owner_password_key_async(owner_password, owner_kdf.clone()).await; + let owner_derived = match owner_derived_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + let owner_wrapped_result = crate::wrap_key(&owner_derived, &owner_root, &owner_wrap_nonce, provisional.owner_slot_aad().as_slice()); + let owner_wrapped = match owner_wrapped_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + let view_slot_result = seal_view_slot(&provisional, view_material, &metadata_key).await; + let view_slot = match view_slot_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + let owner_control_ciphertext_result = crate::encrypt_bytes( + &owner_root, + &owner_control_nonce, + provisional.compartment_aad(crate::WalletCompartmentKindV2::OwnerControl).as_slice(), + owner_control_plaintext.as_slice(), + ); + let owner_control_ciphertext = match owner_control_ciphertext_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + owner_control_plaintext.zeroize(); + let metadata_ciphertext_result = crate::encrypt_bytes( + &metadata_key, + &metadata_nonce, + provisional.compartment_aad(crate::WalletCompartmentKindV2::Metadata).as_slice(), + metadata_plaintext.as_slice(), + ); + let metadata_ciphertext = match metadata_ciphertext_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + metadata_plaintext.zeroize(); + let secret_ciphertext_result = crate::encrypt_bytes( + &secret_key, + &secret_nonce, + provisional.compartment_aad(crate::WalletCompartmentKindV2::Secret).as_slice(), + solana_keypair_bytes.as_slice(), + ); + let secret_ciphertext = match secret_ciphertext_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes); + }, + }; + solana_keypair_bytes.zeroize(); + let owner_slot = crate::WalletKeySlotV2::new_internal( + owner_slot_id, + crate::WalletKeySlotRoleV2::Owner, + owner_kdf, + crate::WalletKeyWrapV2::new_internal(owner_wrap_nonce, owner_wrapped), + ); + let view_descriptor = match view_slot.as_ref() { + std::option::Option::Some(slot) => crate::WalletViewDescriptorV2::enabled_for_slot(*slot.slot_id()), + std::option::Option::None => crate::WalletViewDescriptorV2::disabled(), + }; + let owner_control = crate::WalletEncryptedCompartmentV2::new_internal( + crate::WalletCompartmentKindV2::OwnerControl, + crate::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION, + owner_control_nonce, + owner_control_ciphertext, + ); + let metadata_compartment = crate::WalletEncryptedCompartmentV2::new_internal( + crate::WalletCompartmentKindV2::Metadata, + crate::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION, + metadata_nonce, + metadata_ciphertext, + ); + let secret_compartment = crate::WalletEncryptedCompartmentV2::new_internal( + crate::WalletCompartmentKindV2::Secret, + crate::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION, + secret_nonce, + secret_ciphertext, + ); + let unsigned = crate::KspWalletEnvelopeV2::new_internal( + owner_auth_public_key, + view_descriptor, + owner_slot.clone(), + view_slot.clone(), + owner_control.clone(), + metadata_compartment.clone(), + secret_compartment.clone(), + crate::WalletStateSignatureV2::new_internal([0_u8; crate::KSPWALLET_V2_ED25519_SIGNATURE_BYTES]), + ); + let signature = admin_signing_key.sign(unsigned.state_transcript().as_slice()).to_bytes(); + let envelope = crate::KspWalletEnvelopeV2::new_internal( + owner_auth_public_key, + view_descriptor, + owner_slot, + view_slot, + owner_control, + metadata_compartment, + secret_compartment, + crate::WalletStateSignatureV2::new_internal(signature), + ); + let verify_result = verify_state_signature_v2(&envelope); + if let std::result::Result::Err(error) = verify_result { + return std::result::Result::Err(error); + } + let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V2, crate::WalletCapability::Owner); + let state = OwnerStateV2::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair); + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + operation = "wallet_create", + format_version = crate::KSPWALLET_FORMAT_VERSION_V2, + view_enabled = state.envelope().view_descriptor().enabled(), + "native wallet created in memory" + ); + return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, crate::OwnerState::V2(state))); +} + +/// Verifies state signature. +pub(crate) fn verify_state_signature_v2(envelope: &crate::KspWalletEnvelopeV2) -> ksp_core_lib::Result<()> { + let verifying_key_result = ed25519_dalek::VerifyingKey::from_bytes(envelope.owner_auth_public_key()); + let verifying_key = match verifying_key_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()), + }; + let signature = ed25519_dalek::Signature::from_bytes(envelope.state_signature().signature()); + let verify_result = verifying_key.verify_strict(envelope.state_transcript().as_slice(), &signature); + return match verify_result { + std::result::Result::Ok(()) => std::result::Result::Ok(()), + std::result::Result::Err(_) => std::result::Result::Err(authentication_error()), + }; +} + +async fn rewrap_slot( + envelope: &crate::KspWalletEnvelopeV2, + slot_id: [u8; crate::KSPWALLET_V2_SLOT_ID_BYTES], + role: crate::WalletKeySlotRoleV2, + capability_key: &crate::SecretKeyV1, + password: PasswordRotationV2, +) -> ksp_core_lib::Result { + let salt = match crate::random_bytes::<{ crate::KSPWALLET_V2_DEFAULT_KDF_SALT_BYTES }>() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let kdf = crate::WalletKdfParametersV2::new_creation(salt.to_vec()); + let nonce = match crate::random_nonce() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let derived_result = match password { + PasswordRotationV2::Owner(value) => derive_owner_password_key_async(value, kdf.clone()).await, + PasswordRotationV2::View(value) => derive_view_password_key_async(value, kdf.clone()).await, + }; + let derived = match derived_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let provisional = crate::WalletKeySlotV2::new_internal( + slot_id, + role, + kdf.clone(), + crate::WalletKeyWrapV2::new_internal(nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V2_AEAD_TAG_BYTES]), + ); + let aad = crate::slot_aad(envelope, &provisional); + let wrapped = match crate::wrap_key(&derived, capability_key, &nonce, aad.as_slice()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::WalletKeySlotV2::new_internal(slot_id, role, kdf, crate::WalletKeyWrapV2::new_internal(nonce, wrapped))); +} + +fn resign_envelope( + base: &crate::KspWalletEnvelopeV2, + view_descriptor: crate::WalletViewDescriptorV2, + owner_slot: crate::WalletKeySlotV2, + view_slot: std::option::Option, + owner_control: crate::WalletEncryptedCompartmentV2, + metadata: crate::WalletEncryptedCompartmentV2, + secret: crate::WalletEncryptedCompartmentV2, + admin_signing_key: &ed25519_dalek::SigningKey, +) -> crate::KspWalletEnvelopeV2 { + let unsigned = crate::KspWalletEnvelopeV2::new_internal( + *base.owner_auth_public_key(), + view_descriptor, + owner_slot.clone(), + view_slot.clone(), + owner_control.clone(), + metadata.clone(), + secret.clone(), + crate::WalletStateSignatureV2::new_internal([0_u8; crate::KSPWALLET_V2_ED25519_SIGNATURE_BYTES]), + ); + let signature = admin_signing_key.sign(unsigned.state_transcript().as_slice()).to_bytes(); + return crate::KspWalletEnvelopeV2::new_internal( + *base.owner_auth_public_key(), + view_descriptor, + owner_slot, + view_slot, + owner_control, + metadata, + secret, + crate::WalletStateSignatureV2::new_internal(signature), + ); +} + +fn verify_and_return(envelope: crate::KspWalletEnvelopeV2) -> ksp_core_lib::Result { + let verify_result = verify_state_signature_v2(&envelope); + if let std::result::Result::Err(error) = verify_result { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(envelope); +} + +fn prepare_view_creation(password: std::option::Option) -> ksp_core_lib::Result> { + let password = match password { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Ok(std::option::Option::None), + }; + let slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V2_SLOT_ID_BYTES }>() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let salt = match crate::random_bytes::<{ crate::KSPWALLET_V2_DEFAULT_KDF_SALT_BYTES }>() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let wrap_nonce = match crate::random_nonce() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(std::option::Option::Some(ViewCreationMaterialV2 { + slot_id, + kdf: crate::WalletKdfParametersV2::new_creation(salt.to_vec()), + wrap_nonce, + password, + })); +} + +async fn seal_view_slot( + envelope: &crate::KspWalletEnvelopeV2, + material: std::option::Option, + metadata_key: &crate::SecretKeyV1, +) -> ksp_core_lib::Result> { + let material = match material { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Ok(std::option::Option::None), + }; + let derived_result = derive_view_password_key_async(material.password, material.kdf.clone()).await; + let derived = match derived_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let provisional_slot = crate::WalletKeySlotV2::new_internal( + material.slot_id, + crate::WalletKeySlotRoleV2::View, + material.kdf.clone(), + crate::WalletKeyWrapV2::new_internal(material.wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V2_AEAD_TAG_BYTES]), + ); + let aad = crate::slot_aad(envelope, &provisional_slot); + let wrapped_result = crate::wrap_key(&derived, metadata_key, &material.wrap_nonce, aad.as_slice()); + let wrapped = match wrapped_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(std::option::Option::Some(crate::WalletKeySlotV2::new_internal( + material.slot_id, + crate::WalletKeySlotRoleV2::View, + material.kdf, + crate::WalletKeyWrapV2::new_internal(material.wrap_nonce, wrapped), + ))); +} + +fn provisional_envelope( + owner_auth_public_key: [u8; crate::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES], + owner_slot_id: [u8; crate::KSPWALLET_V2_SLOT_ID_BYTES], + owner_kdf: crate::WalletKdfParametersV2, + owner_wrap_nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], + view_material: std::option::Option<&ViewCreationMaterialV2>, + owner_control_nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], + metadata_nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], + secret_nonce: [u8; crate::KSPWALLET_V2_XCHACHA_NONCE_BYTES], +) -> crate::KspWalletEnvelopeV2 { + let owner_slot = crate::WalletKeySlotV2::new_internal( + owner_slot_id, + crate::WalletKeySlotRoleV2::Owner, + owner_kdf, + crate::WalletKeyWrapV2::new_internal(owner_wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V2_AEAD_TAG_BYTES]), + ); + let (view_descriptor, view_slot) = match view_material { + std::option::Option::Some(material) => ( + crate::WalletViewDescriptorV2::enabled_for_slot(material.slot_id), + std::option::Option::Some(crate::WalletKeySlotV2::new_internal( + material.slot_id, + crate::WalletKeySlotRoleV2::View, + material.kdf.clone(), + crate::WalletKeyWrapV2::new_internal(material.wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V2_AEAD_TAG_BYTES]), + )), + ), + std::option::Option::None => (crate::WalletViewDescriptorV2::disabled(), std::option::Option::None), + }; + return crate::KspWalletEnvelopeV2::new_internal( + owner_auth_public_key, + view_descriptor, + owner_slot, + view_slot, + crate::WalletEncryptedCompartmentV2::new_internal( + crate::WalletCompartmentKindV2::OwnerControl, + crate::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION, + owner_control_nonce, + std::vec![0_u8; crate::KSPWALLET_V2_AEAD_TAG_BYTES], + ), + crate::WalletEncryptedCompartmentV2::new_internal( + crate::WalletCompartmentKindV2::Metadata, + crate::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION, + metadata_nonce, + std::vec![0_u8; crate::KSPWALLET_V2_AEAD_TAG_BYTES], + ), + crate::WalletEncryptedCompartmentV2::new_internal( + crate::WalletCompartmentKindV2::Secret, + crate::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION, + secret_nonce, + std::vec![0_u8; crate::KSPWALLET_V2_AEAD_TAG_BYTES], + ), + crate::WalletStateSignatureV2::new_internal([0_u8; crate::KSPWALLET_V2_ED25519_SIGNATURE_BYTES]), + ); +} + +async fn derive_owner_password_key_async(password: crate::OwnerPassword, kdf: crate::WalletKdfParametersV2) -> ksp_core_lib::Result { + let task = tokio::task::spawn_blocking(move || return crate::derive_password_key_v2(password.as_bytes(), &kdf)); + return match task.await { + std::result::Result::Ok(result) => result, + std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()), + }; +} + +async fn derive_view_password_key_async(password: crate::ViewPassword, kdf: crate::WalletKdfParametersV2) -> ksp_core_lib::Result { + let task = tokio::task::spawn_blocking(move || return crate::derive_password_key_v2(password.as_bytes(), &kdf)); + return match task.await { + std::result::Result::Ok(result) => result, + std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()), + }; +} + +fn pubkey_from_keypair_bytes(bytes: &[u8]) -> ksp_core_lib::Result { + let public_slice = match bytes.get(32..64) { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(key_material_error()), + }; + let public_array = match <[u8; 32]>::try_from(public_slice) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(key_material_error()), + }; + return std::result::Result::Ok(ksp_core_lib::Pubkey::new_from_array(public_array)); +} + +fn cleanup_create_error( + error: ksp_core_lib::Error, + owner_control_plaintext: &mut [u8], + metadata_plaintext: &mut [u8], + solana_keypair_bytes: &mut [u8], +) -> ksp_core_lib::Result { + owner_control_plaintext.zeroize(); + metadata_plaintext.zeroize(); + solana_keypair_bytes.zeroize(); + return std::result::Result::Err(error); +} + +fn authentication_error() -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_AUTHENTICATION_FAILED, "Wallet OWNER-authenticated state verification failed"); +} + +fn crypto_operation_error() -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_CRYPTO_OPERATION_FAILED, "Wallet blocking cryptographic operation could not complete"); +} + +fn view_unlock_error() -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_VIEW_UNLOCK_FAILED, "Wallet VIEW capability could not be unlocked"); +} + +fn owner_unlock_error() -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_OWNER_UNLOCK_FAILED, "Wallet OWNER capability could not be unlocked"); +} + +fn capability_error(message: &'static str) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_CAPABILITY_INSUFFICIENT, message); +} + +fn signing_error() -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_SIGNATURE_FAILED, "Wallet Solana message signing failed"); +} + +fn key_material_error() -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_KEY_MATERIAL_INVALID, "Wallet Solana key material is invalid or inconsistent"); +} + +#[cfg(test)] +#[path = "../unit_tests/wallet_v2.rs"] +mod tests; diff --git a/crates/ksp-wallet-lib/src/wire_v2.rs b/crates/ksp-wallet-lib/src/wire_v2.rs index 26b3702..3a6358a 100644 --- a/crates/ksp-wallet-lib/src/wire_v2.rs +++ b/crates/ksp-wallet-lib/src/wire_v2.rs @@ -1,5 +1,5 @@ // file: crates/ksp-wallet-lib/src/wire_v2.rs -// version: 2 +// version: 3 //! Strict native `.kspwallet` V2 binary wire envelope. @@ -120,6 +120,17 @@ impl WalletKdfParametersV2 { return Self { algorithm: WalletKdfAlgorithmV2::Argon2id, version, memory_kib, iterations, parallelism, salt }; } + /// Creates one V2 KDF value using the release-calibrated default creation profile. + pub(crate) fn new_creation(salt: std::vec::Vec) -> Self { + return Self::new_internal( + crate::KSPWALLET_V2_ARGON2_VERSION, + crate::KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB, + crate::KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS, + crate::KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM, + salt, + ); + } + /// Returns the KDF algorithm. #[must_use] pub const fn algorithm(&self) -> WalletKdfAlgorithmV2 { diff --git a/crates/ksp-wallet-lib/tests/public_api.rs b/crates/ksp-wallet-lib/tests/public_api.rs index cfa3878..a9320ea 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: 10 +// version: 11 //! Public API canaries for the Wallet foundation. @@ -177,3 +177,31 @@ fn public_pre_008_transfer_adapters_are_available_from_crate_root() { ); drop(import_file_future); } + +#[test] +fn public_pre_016_version_neutral_and_explicit_v2_surfaces_are_available_from_crate_root() { + assert_eq!(ksp_wallet_lib::DEFAULT_WALLET_FORMAT, ksp_wallet_lib::WalletFormat::V2); + assert_eq!(ksp_wallet_lib::LATEST_SUPPORTED_WALLET_FORMAT, ksp_wallet_lib::WalletFormat::V2); + assert_eq!(ksp_wallet_lib::WalletFormat::V1.version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V1); + assert_eq!(ksp_wallet_lib::WalletFormat::V2.version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V2); + let _ = ksp_wallet_lib::create_wallet; + let _ = ksp_wallet_lib::create_wallet_v2; + let _ = ksp_wallet_lib::open_wallet_view; + let _ = ksp_wallet_lib::open_wallet_view_v2; + let _ = ksp_wallet_lib::open_wallet_owner; + let _ = ksp_wallet_lib::open_wallet_owner_v2; + let _ = ksp_wallet_lib::inspect_locked_wallet; + let _ = ksp_wallet_lib::inspect_locked_wallet_v2; + let path = std::path::Path::new("not-polled-pre016.kspwallet"); + let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("public-pre016-owner-password")); + let create_future = ksp_wallet_lib::create_wallet_file(path, owner_password, std::option::Option::None, ksp_wallet_lib::WalletCreateMetadata::default()); + drop(create_future); + let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("public-pre016-owner-password")); + let create_v2_future = + ksp_wallet_lib::create_wallet_file_v2(path, owner_password, std::option::Option::None, ksp_wallet_lib::WalletCreateMetadata::default()); + drop(create_v2_future); + let inspect_future = ksp_wallet_lib::inspect_locked_wallet_file(path); + drop(inspect_future); + let inspect_v2_future = ksp_wallet_lib::inspect_locked_wallet_file_v2(path); + drop(inspect_v2_future); +} diff --git a/crates/ksp-wallet-lib/unit_tests/format.rs b/crates/ksp-wallet-lib/unit_tests/format.rs new file mode 100644 index 0000000..fbcf8ba --- /dev/null +++ b/crates/ksp-wallet-lib/unit_tests/format.rs @@ -0,0 +1,18 @@ +// file: crates/ksp-wallet-lib/unit_tests/format.rs +// version: 1 + +#[test] +fn default_and_latest_are_explicit_and_currently_v2() { + assert_eq!(crate::DEFAULT_WALLET_FORMAT, crate::WalletFormat::V2); + assert_eq!(crate::LATEST_SUPPORTED_WALLET_FORMAT, crate::WalletFormat::V2); + assert_eq!(crate::DEFAULT_WALLET_FORMAT.version(), crate::KSPWALLET_FORMAT_VERSION_V2); +} + +#[test] +fn detector_distinguishes_v1_json_and_v2_binary_without_crypto() -> ksp_core_lib::Result<()> { + let v1 = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json"); + let v2 = include_bytes!("../tests/fixtures/kspwallet_v2_wire_only.bin"); + assert_eq!(crate::detect_wallet_format(v1)?, crate::WalletFormat::V1); + assert_eq!(crate::detect_wallet_format(v2)?, crate::WalletFormat::V2); + return std::result::Result::Ok(()); +} diff --git a/crates/ksp-wallet-lib/unit_tests/transfer.rs b/crates/ksp-wallet-lib/unit_tests/transfer.rs index 5e26033..e92430e 100644 --- a/crates/ksp-wallet-lib/unit_tests/transfer.rs +++ b/crates/ksp-wallet-lib/unit_tests/transfer.rs @@ -1,5 +1,5 @@ // file: crates/ksp-wallet-lib/unit_tests/transfer.rs -// version: 3 +// version: 4 #[cfg(unix)] use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import @@ -144,6 +144,41 @@ fn cli_json_import_creates_new_no_clobber_wallet_with_imported_identity_and_meta source.zeroize(); } +#[test] +fn default_transfer_import_creates_v2_without_removing_explicit_v1_import() { + let directory = tempfile::tempdir().expect("Wallet transfer test directory must be creatable"); + let default_destination = directory.path().join("default-v2.kspwallet"); + let explicit_v1_destination = directory.path().join("explicit-v1.kspwallet"); + let keypair = test_keypair(); + let mut source = cli_json(&keypair); + let runtime = runtime(); + let default_owner = runtime + .block_on(crate::import_wallet_transfer( + default_destination.as_path(), + source.as_slice(), + crate::WalletTransferFormat::SolanaCliJson, + crate::OwnerPassword::new(std::string::String::from("pre016-default-import-owner")), + std::option::Option::None, + crate::WalletCreateMetadata::default(), + )) + .expect("default transfer import must succeed"); + assert_eq!(default_owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2); + let default_bytes = std::fs::read(default_destination).expect("default imported Wallet must be readable"); + assert_eq!(crate::detect_wallet_format(default_bytes.as_slice()).expect("default imported Wallet must detect"), crate::WalletFormat::V2); + let explicit_owner = runtime + .block_on(crate::import_wallet_transfer_v1( + explicit_v1_destination.as_path(), + source.as_slice(), + crate::WalletTransferFormat::SolanaCliJson, + crate::OwnerPassword::new(std::string::String::from("pre016-explicit-v1-import-owner")), + std::option::Option::None, + crate::WalletCreateMetadataV1::default(), + )) + .expect("explicit V1 transfer import must remain available"); + assert_eq!(explicit_owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1); + source.zeroize(); +} + #[test] fn transfer_file_import_is_non_destructive_and_bounded() { let directory = tempfile::tempdir().expect("Wallet transfer test directory must be creatable"); diff --git a/crates/ksp-wallet-lib/unit_tests/wallet_v2.rs b/crates/ksp-wallet-lib/unit_tests/wallet_v2.rs new file mode 100644 index 0000000..8a5995d --- /dev/null +++ b/crates/ksp-wallet-lib/unit_tests/wallet_v2.rs @@ -0,0 +1,126 @@ +// file: crates/ksp-wallet-lib/unit_tests/wallet_v2.rs +// version: 1 + +const V1_FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json"); + +fn runtime() -> tokio::runtime::Runtime { + return tokio::runtime::Builder::new_current_thread().build().expect("Wallet V2 test runtime must build"); +} + +#[test] +fn v2_create_open_and_generic_dispatch_preserve_authorized_identity() { + let runtime = runtime(); + let owner_password_text = std::string::String::from("pre016-v2-owner-password"); + let view_password_text = std::string::String::from("pre016-v2-view-password"); + let owner = runtime + .block_on(crate::create_wallet_v2( + crate::OwnerPassword::new(owner_password_text.clone()), + std::option::Option::Some(crate::ViewPassword::new(view_password_text.clone())), + crate::WalletCreateMetadataV1::new(std::option::Option::Some(std::string::String::from("pre016-v2")), std::vec::Vec::new()), + )) + .expect("V2 wallet creation must succeed"); + assert_eq!(owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2); + let pubkey = *owner.pubkey(); + let bytes = owner.to_native_bytes().expect("V2 owner must serialize native bytes"); + assert_eq!(crate::detect_wallet_format(bytes.as_slice()).expect("V2 framing must detect"), crate::WalletFormat::V2); + assert!(owner.to_json_bytes().is_err()); + let view = runtime + .block_on(crate::open_wallet_view(bytes.as_slice(), crate::ViewPassword::new(view_password_text))) + .expect("generic VIEW open must dispatch V2"); + assert_eq!(view.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2); + assert_eq!(*view.pubkey(), pubkey); + assert_eq!(view.alias(), std::option::Option::Some("pre016-v2")); + let reopened = runtime + .block_on(crate::open_wallet_owner(bytes.as_slice(), crate::OwnerPassword::new(owner_password_text))) + .expect("generic OWNER open must dispatch V2"); + assert_eq!(reopened.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2); + assert_eq!(*reopened.pubkey(), pubkey); +} + +#[test] +fn generic_dispatch_keeps_v1_read_compatibility() { + let runtime = runtime(); + assert_eq!(crate::detect_wallet_format(V1_FULL_VECTOR).expect("V1 framing must detect"), crate::WalletFormat::V1); + let locked = crate::inspect_locked_wallet(V1_FULL_VECTOR).expect("generic inspect must keep V1 compatibility"); + assert_eq!(locked.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1); + let owner = runtime + .block_on(crate::open_wallet_owner(V1_FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from("pre005-owner-password")))) + .expect("generic OWNER open must dispatch V1"); + assert_eq!(owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1); +} + +#[test] +fn default_file_creation_is_v2_and_v2_administration_stays_v2() { + let directory = tempfile::Builder::new().prefix("ksp-pre016-v2-").tempdir().expect("V2 test directory must be creatable"); + let path = directory.path().join("default.kspwallet"); + let runtime = runtime(); + let mut owner = runtime + .block_on(crate::create_wallet_file( + path.as_path(), + crate::OwnerPassword::new(std::string::String::from("pre016-default-owner")), + std::option::Option::Some(crate::ViewPassword::new(std::string::String::from("pre016-default-view"))), + crate::WalletCreateMetadataV1::default(), + )) + .expect("default file creation must succeed"); + assert_eq!(owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2); + let locked = runtime.block_on(crate::inspect_locked_wallet_file(path.as_path())).expect("generic inspect must read default file"); + assert_eq!(locked.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2); + runtime + .block_on(owner.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("pre016-updated")))) + .expect("V2 metadata update must persist in V2"); + runtime + .block_on(owner.rotate_owner_password(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre016-owner-rotated")))) + .expect("V2 OWNER rotation must persist in V2"); + runtime + .block_on(owner.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-rotated")))) + .expect("V2 VIEW rotation by OWNER must persist in V2"); + let reopened_owner = runtime + .block_on(crate::open_wallet_owner_file(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre016-owner-rotated")))) + .expect("rotated V2 OWNER must reopen through generic dispatch"); + assert_eq!(reopened_owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2); + assert_eq!(reopened_owner.alias(), std::option::Option::Some("pre016-updated")); + let reopened_view = runtime + .block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-rotated")))) + .expect("rotated V2 VIEW must reopen through generic dispatch"); + assert_eq!(reopened_view.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2); + assert_eq!(reopened_view.alias(), std::option::Option::Some("pre016-updated")); + runtime.block_on(owner.disable_view(path.as_path())).expect("V2 strong VIEW disable must persist"); + let disabled = runtime.block_on(crate::inspect_locked_wallet_file(path.as_path())).expect("disabled V2 must remain inspectable"); + assert!(!disabled.view_enabled()); + let old_view = runtime.block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-rotated")))); + assert_eq!(old_view.expect_err("disabled V2 VIEW must not reopen").code(), crate::ERROR_CODE_CAPABILITY_INSUFFICIENT); + runtime + .block_on(owner.recreate_view(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-recreated")))) + .expect("V2 strong VIEW recreation must persist"); + let mut recreated_view = runtime + .block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-recreated")))) + .expect("recreated V2 VIEW must open"); + runtime + .block_on(recreated_view.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-self-rotated")))) + .expect("V2 VIEW self-rotation must persist"); + let final_view = runtime + .block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-self-rotated")))) + .expect("self-rotated V2 VIEW must reopen"); + assert_eq!(final_view.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2); + let bytes = std::fs::read(path).expect("V2 file must remain readable for framing canary"); + assert_eq!(crate::detect_wallet_format(bytes.as_slice()).expect("administered file must still be V2"), crate::WalletFormat::V2); +} + +#[test] +fn explicit_versioned_readers_reject_the_other_native_wire_format() { + let runtime = runtime(); + let v1_as_v2 = + runtime.block_on(crate::open_wallet_owner_v2(V1_FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from("irrelevant-before-v2-parse")))); + assert_eq!(v1_as_v2.expect_err("explicit V2 reader must reject V1 framing").code(), crate::ERROR_CODE_FORMAT_INVALID); + let owner = runtime + .block_on(crate::create_wallet_v2( + crate::OwnerPassword::new(std::string::String::from("pre016-strict-v2-owner")), + std::option::Option::None, + crate::WalletCreateMetadata::default(), + )) + .expect("strict-reader V2 fixture creation must succeed"); + let bytes = owner.to_native_bytes().expect("strict-reader V2 fixture must serialize"); + let v2_as_v1 = + runtime.block_on(crate::open_wallet_owner_v1(bytes.as_slice(), crate::OwnerPassword::new(std::string::String::from("irrelevant-before-v1-parse")))); + assert_eq!(v2_as_v1.expect_err("explicit V1 reader must reject V2 framing").code(), crate::ERROR_CODE_FORMAT_INVALID); +} diff --git a/deltas/0.2.6/pre.016.md b/deltas/0.2.6/pre.016.md new file mode 100644 index 0000000..8133be5 --- /dev/null +++ b/deltas/0.2.6/pre.016.md @@ -0,0 +1,391 @@ + + + +# Delta `0.2.6-pre.016` — runtime `.kspwallet` V2, APIs multi-version et Wallet Desk version-neutral + +## Base requise + +```text +0.2.6-pre.015-fix.001 appliquée +workspace.package.version = 0.2.6-pre.15.fix.1 +``` + +Le checkpoint opérateur de cette 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 --workspace OK +ksp-wallet-lib 71 passed / 1 ignored +Wallet Desk release_compliance 4/4 OK +smokes réseau / benchmark ignored comme prévu +``` + +Le correctif `pre.015-fix.001` est donc accepté comme base de `pre.016`. + +## Signal technique + +Cette tranche modifie les APIs/runtime Wallet et le backend Wallet Desk : + +```text +workspace.package.version = 0.2.6-pre.16 +commit = v0.2.6-pre.016 +``` + +Aucun tag prerelease. + +Les `package.json`/`tauri.conf.json` ne sont pas modifiés : aucune dépendance frontend, configuration Tauri, permission ou contrat de bundle n'est changé. Le signal technique Rust reste porté par `workspace.package.version`. + +## Objectif de la tranche + +`pre.016` matérialise le contrat décidé en `pre.015` : V2 devient le **format natif default explicite** des créations/imports non versionnés, sans rendre le default dépendant de la dernière version supportée. + +La tranche ajoute simultanément : + +```text +runtime cryptographique V2 create/open VIEW/OWNER +persistence V2 create no-clobber + remplacement atomique +state-conflict V2 authentifié +façade in-memory multi-version +façade filesystem multi-version +variantes explicites V1/V2 +import transfer default V2 + variantes V1/V2 +handles OWNER/VIEW version-neutral +administration V2 complète +Wallet Desk exclusivement sur APIs génériques +canaris default/latest, V1/V2 et strict version dispatch +``` + +La migration persistée V1 -> V2 reste volontairement hors scope et appartient à `pre.017`. + +## Politique durable : default distinct de latest + +La politique est désormais matérialisée dans `ksp-wallet-lib` : + +```rust +DEFAULT_WALLET_FORMAT = WalletFormat::V2 +LATEST_SUPPORTED_WALLET_FORMAT = WalletFormat::V2 +``` + +`WalletFormat` est public et `non_exhaustive` afin qu'un futur V3 puisse être ajouté sans inviter les consumers à supposer que V2 est la dernière version possible. + +Invariant durable : + +```text +DEFAULT_WALLET_FORMAT != conceptuellement LATEST_SUPPORTED_WALLET_FORMAT +``` + +Les deux valeurs sont V2 dans cette release, mais leur égalité est contingente. Si V3 apparaît plus tard : + +```text +DEFAULT_WALLET_FORMAT peut rester V2 +LATEST_SUPPORTED_WALLET_FORMAT peut devenir V3 +``` + +Une nouvelle version supportée ne change jamais automatiquement le default. + +## APIs de création + +### In-memory + +```text +create_wallet(...) -> DEFAULT_WALLET_FORMAT, donc V2 +create_wallet_v1(...) -> V1 forcé +create_wallet_v2(...) -> V2 forcé +``` + +### Filesystem + +```text +create_wallet_file(...) -> DEFAULT_WALLET_FORMAT, donc V2 +create_wallet_file_v1(...) -> V1 forcé +create_wallet_file_v2(...) -> V2 forcé +``` + +Le type version-neutral `WalletCreateMetadata` est exposé pour les nouvelles APIs ; `WalletCreateMetadataV1` reste conservé pour compatibilité explicite V1. Le profil plaintext initial de metadata V2 conserve la sémantique établie en V1, conformément à la spec V2. + +## APIs de lecture et inspection + +Les APIs génériques détectent uniquement le framing nécessaire à la sélection du parser : + +```text +open_wallet_view(...) +open_wallet_owner(...) +inspect_locked_wallet(...) + +open_wallet_view_file(...) +open_wallet_owner_file(...) +inspect_locked_wallet_file(...) +``` + +Dispatch : + +```text +JSON dont le premier octet significatif est '{' -> parser strict V1 +magic binaire KSPWALLET + version 2 -> parser strict V2 +autre framing/version -> erreur format/version +``` + +La détection n'est jamais une validation de substitution : le parser V1/V2 sélectionné reste autoritatif pour toutes les contraintes structurelles et cryptographiques. + +Les APIs explicitement versionnées restent strictes : + +```text +*_v1 refuse V2 +*_v2 refuse V1 +``` + +## Runtime V2 + +`wallet_v2.rs` compose le wire/transcript figé en `pre.015` avec les primitives Wallet existantes : + +```text +Argon2id password KDF +XChaCha20-Poly1305 key wrapping +XChaCha20-Poly1305 OWNER-CONTROL / METADATA / SECRET +Ed25519 OWNER state signature +VIEW / OWNER indépendants +Solana keypair secret compartment +``` + +V2 conserve le profil plaintext fonctionnel initial de V1 pour owner-control, metadata et secret, mais utilise exclusivement : + +```text +wire V2 +IDs V2 +AAD V2 +state transcript V2 +format_version = 2 +``` + +Les domains V1/V2 restent non interchangeables. + +## Handles OWNER / VIEW version-neutral + +`WalletOwner` et `WalletView` ne sont plus couplés à `OwnerStateV1` / `ViewStateV1`. + +Un dispatch runtime interne conserve le format authentifié : + +```text +OwnerState::V1 / OwnerState::V2 +ViewState::V1 / ViewState::V2 +``` + +Les opérations publiques restent stables et dispatchent dans le format du handle : + +```text +signature OWNER +metadata alias/notes +rotation OWNER +rotation VIEW par OWNER +VIEW self-rotation +disable VIEW fort +recreate VIEW fort +export transfer +``` + +Une mutation V1 reste V1. Une mutation V2 reste V2. + +Aucune de ces opérations ne constitue une migration. + +### Sérialisation des handles + +Nouvelle surface : + +```text +to_native_bytes() -> V1 JSON si handle V1, V2 binaire si handle V2 +``` + +Compatibilité historique : + +```text +to_json_bytes() -> fonctionne uniquement pour V1 + rejette V2 +``` + +Il n'existe aucune transcodification implicite V2 -> V1 ou V1 -> V2. + +## Persistence V2 et stale-handle protection + +La création V2 conserve les garanties V1 : + +```text +construction complète en mémoire +écriture temporaire +sync +publication no-clobber +aucun overwrite d'une destination existante +``` + +Les mutations V2 réutilisent la discipline atomique avec une vérification format-spécifique : + +```text +relire destination bornée +parse_binary V2 strict +vérifier state signature V2 +comparer l'enveloppe authentifiée attendue +publier seulement si état courant identique +``` + +Un handle V2 stale reçoit le même contrat `wallet.state_conflict` qu'un handle V1. + +## Import transfer + +Les formats de transfert externes restent indépendants du format natif `.kspwallet` : + +```text +Solana CLI JSON +Solana keypair Base58 complet +``` + +La sortie native suit désormais : + +```text +import_wallet_transfer(...) -> V2 default +import_wallet_transfer_v1(...) -> V1 forcé +import_wallet_transfer_v2(...) -> V2 forcé + +import_wallet_transfer_file(...) -> V2 default +import_wallet_transfer_file_v1(...) -> V1 forcé +import_wallet_transfer_file_v2(...) -> V2 forcé +``` + +La keypair Solana reste identique ; seul le nouvel environnement natif KSP est créé dans la version choisie. + +## Wallet Desk + +`ksp-app-wallet-desk` ne référence plus aucune API native versionnée en production. + +Ses callsites utilisent uniquement : + +```text +create_wallet_file +import_wallet_transfer +open_wallet_view_file +open_wallet_owner_file +inspect_locked_wallet_file +``` + +Conséquences : + +```text +nouveau Wallet créé depuis le Desk V2 +nouvel import depuis le Desk V2 +wallet V1 existant toujours inspectable/ouvrable +wallet V2 inspectable/ouvrable +mutations après unlock format natif conservé +frontend ne connaît pas V1/V2 pour dispatcher +``` + +La version reste visible dans les projections métier (`format_version`) mais n'est pas une responsabilité de routing du frontend/Tauri. + +## Canaris `pre.016` + +Les nouveaux tests couvrent notamment : + +```text +DEFAULT_WALLET_FORMAT = V2 +LATEST_SUPPORTED_WALLET_FORMAT = V2 +framing detector V1/V2 +création V2 + open générique VIEW/OWNER +lecture générique d'une fixture V1 stable +création fichier générique -> V2 +metadata V2 persistée +rotation OWNER V2 +rotation VIEW V2 par OWNER +disable/recreate VIEW V2 +VIEW self-rotation V2 +wire V2 conservé après administration +lecteur V1 forcé refuse V2 +lecteur V2 forcé refuse V1 +import transfer générique -> V2 +import transfer V1 explicite toujours disponible +Wallet Desk sans callsite *_v1/*_v2 +``` + +Les anciens canaris Desk `pre.006`/`pre.009` sont réalignés sur la façade générique sans supprimer leurs garanties de sécurité historiques. + +## Hors scope + +`pre.016` ne réalise pas : + +```text +migration V1 -> V2 +réécriture automatique à l'ouverture +suppression des APIs V1 +V3 / second facteur +changement de default futur automatique +frontend spécifique au format +nouvelle permission Tauri +build Tauri de production +``` + +## Documentation synchronisée + +Cette tranche met à jour : + +```text +ROADMAP.md +crates/ksp-wallet-lib/README.md +crates/ksp-wallet-lib/USAGE.md +docs/IDEAS.md +docs/architecture/004-COMPONENT_INVENTORY.md +docs/formats/KSPWALLET_V2.md +docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md +docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md +prompts/011-V0_2_6_START_PROMPT.md +``` + +`CHANGELOG.md` reste réservé à la clôture `pre.018`. + +## Prochaine tranche + +`0.2.6-pre.017` doit maintenant porter la migration **explicite et authentifiée** V1 -> V2 : + +```text +API de migration nommée explicitement +source V1 authentifiée +nouveau document V2 cryptographiquement valide +aucune migration lors d'un open ordinaire +stratégie destination/no-clobber ou remplacement explicitement choisie +préservation identité Solana + metadata + policy VIEW applicable +canaris stale/tampering/failure atomicity +régression complète Wallet Desk V1/V2 +``` + +La candidate/documentation finale reste `pre.018`. + +## 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 +``` + +Un contrôle fonctionnel Wallet Desk est également utile avant `pre.017` : + +```bash +(cd crates/ksp-app-wallet-desk && cargo tauri dev) +``` + +Vérifier au minimum : + +```text +création -> format_version 2 +import -> format_version 2 +inventory V1/V2 +unlock OWNER/VIEW +metadata + rotations sur V2 +balance depuis handle autorisé +``` + +**Ne pas exécuter `cargo tauri build` en `pre.016`.** Le build de production reste réservé à `pre.018` et doit demeurer l'ultime opération de validation de la candidate. diff --git a/docs/000-README.md b/docs/000-README.md index d623e9b..62b11b2 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 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`. +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 et `pre.016` matérialise les APIs génériques/versionnées, le runtime V2 et la bascule version-neutral de Wallet Desk ; `pre.017` ferme migration/canaris/régression, 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 9fcbb4f..967e343 100644 --- a/docs/IDEAS.md +++ b/docs/IDEAS.md @@ -1,5 +1,5 @@ - + # Idées à explorer @@ -206,7 +206,7 @@ Chaque format doit être étudié côté sécurité, round-trip, secret/public, **Status :** V2 retenu et matérialisé en `0.2.6-pre.015` / facteurs futurs à explorer -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. +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/persistence V2, les APIs `_v1/_v2` et la politique `DEFAULT_WALLET_FORMAT = V2` sont matérialisées en `pre.016`, puis la migration explicite V1 -> V2 reste planifiée en `pre.017`. Le V2 ne modifie pas à lui seul les garanties cryptographiques de VIEW/OWNER, keypair ou import/export. 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. @@ -258,7 +258,6 @@ Définir la politique lorsqu'un registry reçoit plusieurs implémentations capa Le principe `domain/program/capability` est retenu. Les noms exacts des dossiers courts (`dec`, `exec_prep`) seront validés avec la première vraie arborescence. - ## Execution — idées d'implémentation ### Composition de policies diff --git a/docs/architecture/004-COMPONENT_INVENTORY.md b/docs/architecture/004-COMPONENT_INVENTORY.md index f96cd97..937e7bd 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,10 +26,10 @@ 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 V2 | `ksp-wallet-lib` | lib | En cours | `0.2.6` | wire binaire V2 + API default/versionnée + migration | +| Wallet V2 | `ksp-wallet-lib` | lib | En cours | `0.2.6` | wire/runtime V2 + API default/versionnée ; migration pre.017 | | 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 | +| 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 | diff --git a/docs/formats/000-README.md b/docs/formats/000-README.md index fb577b0..0b7de99 100644 --- a/docs/formats/000-README.md +++ b/docs/formats/000-README.md @@ -1,5 +1,5 @@ - + # Formats KSP @@ -10,4 +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`. +- [`KSPWALLET_V2.md`](KSPWALLET_V2.md) — wire binaire natif V2 introduit par `0.2.6-pre.015` puis runtime multi-version matérialisé en `pre.016` : 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 8ed3598..15686cc 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 @@ -1043,7 +1043,6 @@ Limites explicitement conservées en V1 : - la zeroization réduit les copies possédées mais ne constitue pas une preuve d'effacement physique de toute copie potentielle produite par le compilateur, l'OS ou le matériel ; - aucune revendication de résistance side-channel supplémentaire au-delà des primitives et bibliothèques retenues. - ## 26. Statut de clôture V1 À la publication stable `0.2.5`, cette spécification constitue la version normative V1 du format `.kspwallet`. Les guides d'utilisation KSP sont [`../../crates/ksp-wallet-lib/README.md`](../../crates/ksp-wallet-lib/README.md) et [`../../crates/ksp-wallet-lib/USAGE.md`](../../crates/ksp-wallet-lib/USAGE.md) ; ils ne remplacent pas le présent document comme autorité normative du wire. diff --git a/docs/formats/KSPWALLET_V2.md b/docs/formats/KSPWALLET_V2.md index 8e9dcef..68d415f 100644 --- a/docs/formats/KSPWALLET_V2.md +++ b/docs/formats/KSPWALLET_V2.md @@ -1,5 +1,5 @@ - + # `.kspwallet` V2 — spécification du wire binaire natif KSP @@ -19,7 +19,7 @@ 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`. +`0.2.6-pre.015` fige la grammaire binaire, les identifiants numériques, les bornes structurelles et les transcripts/AAD V2. `0.2.6-pre.016` matérialise la création/ouverture/persistence V2, l'auto-détection V1/V2, les APIs génériques/versionnées et le basculement de Wallet Desk vers la façade non versionnée. V1 reste un format stable supporté. V2 ne réinterprète jamais un document V1 avec sa propre grammaire. @@ -57,6 +57,30 @@ 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. +### 2.1 Surface matérialisée en `pre.016` + +La politique ci-dessus est désormais du code, pas uniquement une cible documentaire : + +```text +create_wallet(...) -> V2 default +create_wallet_v1(...) -> V1 forcé +create_wallet_v2(...) -> V2 forcé + +create_wallet_file(...) -> V2 default +create_wallet_file_v1(...) -> V1 forcé +create_wallet_file_v2(...) -> V2 forcé + +open_wallet_view/owner(...) -> détecte V1/V2 +open_wallet_*_v1/_v2(...) -> exige exactement V1/V2 +inspect_locked_wallet(...) -> détecte V1/V2 +inspect_locked_wallet_v1/_v2 -> exige exactement V1/V2 + +import_wallet_transfer(...) -> crée le default V2 +import_wallet_transfer_v1/_v2 -> force le format natif produit +``` + +`WalletOwner` et `WalletView` mémorisent le format natif authentifié. Les opérations d'administration et de rotation persistent dans ce même format ; aucune mutation ordinaire ne réalise de conversion V1/V2. `to_native_bytes()` sérialise le format courant, tandis que `to_json_bytes()` reste une compatibilité V1 et rejette V2. + ## 3. Encodage général Un document V2 est : @@ -80,14 +104,14 @@ Une implémentation conforme doit vérifier les bornes **avant** toute allocatio 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` | +| 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 : @@ -130,21 +154,21 @@ Aucun compteur de slots ou de compartiments n'est nécessaire : leur cardinalit 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 | +| 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 : @@ -165,14 +189,14 @@ Toute divergence est invalide avant KDF/déchiffrement. 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 | +| 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 : @@ -190,10 +214,10 @@ Les payloads plaintext V2 conservent le modèle fonctionnel établi en V1 pour c La fin du document est : -| Champ | Taille | Valeur / règle | -|-----------------------------|-------:|---------------------------| -| `state_signature.algorithm` | 1 | `0x01` Ed25519 | -| `state_signature.signature` | 64 | signature detached exacte | +| 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. @@ -353,17 +377,17 @@ Le nonce/ciphertext n'est pas inclus dans son propre AAD. 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 | +| 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 : @@ -402,7 +426,7 @@ Lecture : 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. +La fixture `wire_only` vérifie uniquement framing/canonicalité et reste volontairement distincte des wallets V2 runtime produits depuis `pre.016`; elle n'est pas présentée comme une signature cryptographique valide. ## 14. Compatibilité et migration diff --git a/docs/plans/000-README.md b/docs/plans/000-README.md index df576ca..0a74f84 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 é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. +- [`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 et `pre.016` APIs génériques/versionnées + runtime V2 désormais matérialisés ; prochain gate `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/006-V0_1_4_CONFIG_DESKTOP_PLAN.md b/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md index 3ef6457..945c363 100644 --- a/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md +++ b/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.1.4` — `ksp-app-config-desk` @@ -1302,7 +1302,6 @@ La validation finale a suivi KSP-APP-034 : tous les contrôles Rust et frontend, `0.1.4` est clôturée et publiée comme release stable. `ksp-app-config-desk` valide les frontières Config/Logging/Tauri prévues par ce plan, la baseline Logging canonique est revenue à `info`/`warn`, les TODO non bloquants sont reportés explicitement, et le prochain point de reprise est `0.2.0-pre.001` via `prompts/005-V0_2_0_START_PROMPT.md`. - ## 19. Dépendances et ordre d'introduction Aucune dépendance n'est ajoutée par `pre.001`. @@ -1458,7 +1457,6 @@ Aucune question n'empêche d'ouvrir le développement après validation du prés Ces points doivent être résolus par code/tests dans les prereleases prévues, pas par contournement applicatif. - ### Harmonisation desktop `0.2.6-pre.014` Le polish partagé avec Wallet Desk déplace les pills de navigation Config dans une sidebar verticale du contenu principal, tout en conservant la palette claire historique de Config Desk. Les quatre fichiers HTML des deux Desks utilisent les en-têtes normalisés `file/version`. Le splash reprend les flux kbot3 (messages généraux en bas, diagnostics debug en haut) avec trois timings Config-owned distincts `KSP_DESK_SPLASH_FADE_IN_MS`, `KSP_DESK_SPLASH_MINIMUM_MS` et `KSP_DESK_SPLASH_FADE_OUT_MS`. 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 6e9abac..2563859 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 @@ -25,7 +25,6 @@ Les validations opérateur du `2026-08-20` ont ensuite confirmé la base `pre.00 `0.2.6-pre.003` matérialise la composition Config prévue : `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, les profils Wallet `default`/`temporary`/`tests`, `ResolvedWalletConfig` et les adapters qui peuvent consommer un profil déjà sélectionné par composite sans perdre la provenance `Composite`. Wallet Desk valide les composants `logging`/`transport`/`wallet`, crée la racine et le sous-répertoire effectif absents avec logs `debug`, refuse les objets filesystem invalides/symlinks de sous-répertoire avec logs `error`, et expose uniquement les chemins/profils non secrets dans le statut runtime. Aucun inventory `.kspwallet` n’est encore effectué. `pre.003-fix.001` corrige les deux régressions de tests révélées par la validation workspace et le warning Clippy, puis rend visibles les événements `trace` frontend. `pre.003-fix.002` corrige ensuite le canari d’inventaire `.env.example` qui confondait un fragment d’identifiant Rust avec une variable KSP, remplace le profil temporaire `wallet_desk_dev` par une famille Logging réutilisable (`console_*`, `file_info`, `superdev`, `supertrace`) et fait sélectionner temporairement `supertrace` par le composite Wallet Desk afin de conserver la visibilité des actions frontend sans profil spécifique à l’application. `pre.003-fix.003` corrige uniquement les canaris révélés par la validation de `fix.002` : conformité `clippy::implicit_return` et séparation explicite entre fragment lexical KSP incorporé dans un identifiant et nom d’environnement concret suffixé. `pre.003-fix.004` ferme le dernier défaut Clippy de cette série en ajoutant les `return` explicites aux cinq closures restantes du test de composition `supertrace`, sans changer les profils Logging ni le composite. `pre.004` matérialise ensuite l’inventory locked réel. Sa validation opérateur confirme le comportement fonctionnel et le workspace complet, mais révèle deux warnings `unused_imports` sur des réexports crate-root internes uniquement utiles aux unit tests ; `pre.004-fix.001` les limite à `#[cfg(test)]` : ils disparaissent du build runtime mais restent disponibles au crate-root pour les unit tests, conformément aux règles d’exports KSP, sans changement de comportement. `pre.005` matérialise ensuite la création native dans Wallet Desk et le premier lifecycle durable `WalletSession` : création no-clobber sous la racine Config via `create_wallet_file_v1`, session OWNER conservée uniquement en Rust après création, sélection locked durable, `Lock`/`Deselect`/Refresh/changement de wallet avec drop des handles et purge frontend des projections protégées/passwords. Les unlock VIEW/OWNER de wallets existants restent strictement `pre.006`. `pre.005-fix.001` corrige le gate de compilation/tests révélé par l'opérateur sans modifier le lifecycle : la variante `WalletSession::Owner` contient désormais `Box` afin de fermer `clippy::large_enum_variant`, et le canari de sécurité définit explicitement `request_start` avant de contrôler les champs password request-only. `pre.006` matérialise ensuite les unlock existants : VIEW/OWNER manuel, découverte Config des candidats `KSP_SECRET_WALLET_PASS_*`, ordre déterministe filename normalisé -> numérique -> nommé, actions configurées explicites et jamais automatiques, `PrivilegedOperation` pendant Argon2, puis sessions `View`/`Owner` Rust-only avec candidate count seul projeté vers l’UI. `pre.006-fix.001` corrige les deux canaris `desktop_security` qui inspectaient trop largement le texte de `WalletAuthorizedDto` : la documentation du compteur de candidats contient légitimement le mot `password`, mais aucun champ password n’est sérialisé ; le test filtre désormais uniquement les lignes de champs `pub(crate)` de la structure. `pre.007` ferme ensuite le MVP réseau : Config sait mapper un profil Transport déjà sélectionné par composite sans perdre la provenance `Composite`; Wallet Desk construit et conserve `HttpTransportPool`, publie uniquement profile/role/cluster/provider/compteurs sûrs, et `refresh_wallet_balance` accepte zéro argument frontend. Rust copie la Pubkey depuis `WalletView`/`WalletOwner`, libère le mutex avant l'I/O `getBalance`, utilise `confirmed`, puis projette lamports, SOL exact à neuf décimales, slot et `api_version`. Une réponse arrivée après lock/deselect/changement d'autorisation est refusée afin d'empêcher une balance stale de réapparaître. `pre.007-fix.001` corrige uniquement le canari d'intégration qui utilisait le constructeur test-only `ConfigEnvironment::from_maps`; le test passe désormais par `ConfigEnvironment::load`, API publique utilisée en production. `pre.007-fix.002` corrige le second défaut du même canari : son message d'assertion ne formate plus `environment` avec `{:?}`, ce qui aurait exigé `ConfigEnvironment: Debug`. ConfigEnvironment reste volontairement non-`Debug` et le runtime n'est pas modifié. `pre.008` ajoute ensuite l'import de keypairs Solana CLI JSON/Base58 via le plugin Dialog utilisé exclusivement côté Rust : le frontend choisit seulement le format, Rust ouvre le picker natif, convertit le résultat en chemin local, lit la source avec une borne stricte, valide la Pubkey via `ksp-wallet-lib`, conserve les octets secrets dans `Zeroizing>` et ne renvoie que basename/format/Pubkey. L'import consomme cette source staged en mémoire avec `import_wallet_transfer_v1`, publie no-clobber sous la racine Wallet Config et ouvre la session OWNER avec de nouveaux credentials KSP. Aucun chemin source, contenu secret ou permission guest Dialog/FS ne traverse l'IPC. - ### Correctif `0.2.6-pre.013-fix.001` Le premier passage opérateur de `pre.013` a confirmé `fmt`, audit, `check` et Clippy, mais le nouveau canari `response_dtos_keep_secret_key_material_out_of_ipc` a produit un faux positif sur `WalletAuthorizedDto` : le scanner analysait aussi les commentaires du bloc de structure et rencontrait la documentation légitime `Wallet password candidates`. `pre.013-fix.001` aligne ce gate sur la règle déjà éprouvée en `pre.006-fix.001` : seuls les champs `pub(crate)` réellement sérialisables sont inspectés pour `password`, key material, path/content/bytes. Aucun DTO ni contrat IPC runtime n'est assoupli. @@ -771,32 +770,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. @@ -1473,7 +1472,6 @@ La source n'est jamais parsée en TypeScript et son chemin n'est jamais sériali Le plugin Dialog n'est pas exposé au guest JavaScript : aucun package npm `@tauri-apps/plugin-dialog`, aucune permission `dialog:*` et aucun plugin filesystem ne sont ajoutés. Le frontend ne reçoit que `source_name` (basename), `format` et `pubkey`. Les nouveaux passwords OWNER/VIEW transitent uniquement HTML/TypeScript -> Rust pour l'opération demandée, sont déplacés dans les wrappers Wallet, puis purgés côté frontend ; ils ne reviennent jamais dans une réponse. - #### `pre.008-fix.001` — conformité Clippy du staging import Statut : **préparé après validation opérateur de `0.2.6-pre.008`**. @@ -1717,19 +1715,29 @@ La politique API est figée dès cette tranche : `DEFAULT_WALLET_FORMAT` et `LAT ### `pre.016` — APIs génériques/versionnées + création/ouverture V2 -Matérialiser : +**Statut : matérialisé par `0.2.6-pre.016`.** + +Cette tranche réalise : ```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 +DEFAULT_WALLET_FORMAT = V2 +LATEST_SUPPORTED_WALLET_FORMAT = V2, sans couplage automatique entre les deux +WalletFormat public non_exhaustive +create_wallet/create_wallet_file/import_wallet_transfer génériques -> V2 default +variantes explicites _v1 conservées et _v2 ajoutées +open/inspect génériques -> détection bornée V1 JSON / V2 binaire open/inspect _v1/_v2 -> format forcé strict create/open in-memory V2 + crypto/transcripts V2 +persistence V2 no-clobber et remplacement atomique avec state-conflict +WalletOwner/WalletView version-neutral derrière des états runtime V1/V2 +metadata, rotations OWNER/VIEW, disable/recreate VIEW et VIEW self-rotation restent dans le format ouvert +to_native_bytes() sérialise V1 ou V2 ; to_json_bytes() reste V1-only +import transfer générique -> V2, variantes _v1/_v2 forcées Wallet Desk migre vers les APIs génériques, jamais vers un numéro de format +canaris explicites default V2, compatibilité lecture V1, strict _v1/_v2 et administration V2 ``` -Une future API `_v3` pourra être ajoutée sans transformer automatiquement le default en V3. +Une future API `_v3` pourra être ajoutée sans transformer automatiquement le default en V3. `pre.016` ne migre aucun fichier V1 existant : la migration authentifiée reste réservée à `pre.017`. ### `pre.017` — migration V1 -> V2 + persistence/canaris @@ -1880,7 +1888,6 @@ v0.2.6-pre.001-fix.002 Après application/commit du fix documentaire, `pre.002` démarre la crate Tauri et le shell complet du gabarit, avec Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, splash/main et logging bridge. - ## `pre.014` — polish desktop partagé et lancement Tauri multi-app La tranche harmonise Config Desk et Wallet Desk sans modifier les contrats Wallet métier : @@ -1913,7 +1920,6 @@ bash -lc 'cd crates/ksp-app-wallet-desk && cargo tauri dev' À 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 Le retour opérateur de `pre.014` confirme le lancement Tauri crate-local mais révèle plusieurs écarts purement gabarit. Le correctif conserve `pre.014` comme tranche de polish et applique les décisions suivantes : diff --git a/docs/rules/RULES_KSP.md b/docs/rules/RULES_KSP.md index 606d41f..fdd9c43 100644 --- a/docs/rules/RULES_KSP.md +++ b/docs/rules/RULES_KSP.md @@ -26,7 +26,6 @@ - **KSP-API-006** — `ksp-store-lib` contient PostgreSQL comme implémentation officielle de référence derrière `ksp-store-api`. - **KSP-API-007** — Une crate `*-api` n'est créée que lorsqu'un vrai besoin d'extension, backend ou lifecycle le justifie ; la symétrie de nommage n'est jamais une justification suffisante. - ## Configuration et environnement - **KSP-CONFIG-001** — `ksp-config-lib` est l'unique propriétaire KSP de la lecture des documents Config, du `.env`, des variables applicatives `KSP_*` / `KSPB_*` et de leur résolution ; les autres crates ne lisent pas directement ces sources. @@ -112,7 +111,6 @@ - **KSP-MAT-003** — Une extension externe de materializer doit pouvoir produire du D3 générique sans migration PostgreSQL spécialisée. - **KSP-MAT-004** — Une nouvelle projection relationnelle D4 exige explicitement un contrat Store/migration/backend correspondant ; cette responsabilité n'est pas cachée dans `ksp-materializer-api`. - ## Backlog, claims et reprise - **KSP-PROC-001** — Le backlog est défini relativement à l'identité/version/capability du processor et non par simple absence d'une row de sortie. 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 12ed041..80e5d40 100644 --- a/docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md +++ b/docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md @@ -1,5 +1,5 @@ - + # Validation `0.2.6` — Wallet Desk integration / compliance / security @@ -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 @@ -240,7 +240,19 @@ Le blocage workspace est limité à `ksp-logging-lib/tests/ownership.rs::workspa 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`. +`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` matérialise ensuite la façade version-neutral et fait migrer Wallet Desk vers elle : + +```text +create/import Desk API générique -> default V2 +open VIEW/OWNER Desk API générique -> détection V1/V2 +inventory/selection Desk inspect générique -> détection V1/V2 +aucun appel Desk à *_v1 ou *_v2 gate durable +V1 existant reste ouvrable sans migration +V2 nouvellement créé/importé administration complète conservée en V2 +``` + +Les canaris `pre.016` couvrent également le refus croisé des lecteurs explicitement versionnés et le maintien du format V2 après metadata/rotations/disable/recreate/self-rotation VIEW. La migration V1 -> V2 reste explicitement hors de cette tranche et appartient à `pre.017`. diff --git a/prompts/011-V0_2_6_START_PROMPT.md b/prompts/011-V0_2_6_START_PROMPT.md index 0a5d999..0f76a62 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 @@ -704,7 +704,6 @@ Ensuite : Les règles de la session ne doivent pas être renégociées au fil des corrections : elles sont dans les documents KSP versionnés et doivent être appliquées dès chaque premier patch. Si un conflit ou une règle manquante est découvert, corriger la règle et son contrôle durable avant de propager une nouvelle convention. - ## `pre.014-fix.001` — retour opérateur après reprise de session Le correctif de polish après `pre.014` traite uniquement les régressions de gabarit et de canari : @@ -720,7 +719,12 @@ 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 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. + +## Addendum `pre.016` — runtime V2 et façade multi-version matérialisés + +`pre.016` matérialise la politique figée en `pre.015` : `DEFAULT_WALLET_FORMAT = V2`, APIs génériques de création/import en V2, lecture/inspection V1/V2 auto-détectée, variantes `_v1`/`_v2` strictes et Wallet Desk consommant uniquement la façade non versionnée. Les handles OWNER/VIEW persistent leurs mutations dans le format natif ouvert ; aucune conversion implicite V1 -> V2 n'est effectuée. + +La prochaine tranche est `pre.017` : migration explicite authentifiée V1 -> V2, canaris adversariaux/persistence et régression Wallet Desk. `pre.018` conserve la documentation/candidate finale et le build Tauri en dernière opération.