v0.2.6-pre.016

This commit is contained in:
2026-08-22 08:18:38 +02:00
parent 946d88322b
commit 92a2c4fff3
43 changed files with 2813 additions and 280 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/README.md -->
<!-- version: 26 -->
<!-- version: 27 -->
# `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 — <vue active>` 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 lapplication 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.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/USAGE.md -->
<!-- version: 26 -->
<!-- version: 27 -->
# 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_*`.

View File

@@ -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::<std::vec::Vec<_>>();
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,

View File

@@ -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),

View File

@@ -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"));
}

View File

@@ -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"));

View File

@@ -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,

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-logging-lib/USAGE.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# 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 :

View File

@@ -1,11 +1,11 @@
<!-- file: crates/ksp-wallet-lib/README.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# `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`.

View File

@@ -1,9 +1,9 @@
<!-- file: crates/ksp-wallet-lib/USAGE.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# 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 lorsquun 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.

View File

@@ -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.

View File

@@ -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<crate::SecretKeyV1> {
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
}
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) fn wrap_key(
wrapping_key: &crate::SecretKeyV1,

View File

@@ -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<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
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<crate::WalletView> {
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<crate::WalletOwner> {
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<crate::LockedWalletInfo> {
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<WalletFormat> {
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;

View File

@@ -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;

View File

@@ -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<std::string::String>,
notes: std::vec::Vec<crate::WalletNote>,
) -> 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.

View File

@@ -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<std::vec::Vec<u8>> {
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<std::vec::Vec<u8>> {
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;

View File

@@ -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);
}
}

View File

@@ -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<std::path::Path>,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
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<std::path::Path>,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
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<std::path::Path>,
password: crate::ViewPassword,
) -> ksp_core_lib::Result<crate::WalletView> {
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<std::path::Path>,
password: crate::ViewPassword,
) -> ksp_core_lib::Result<crate::WalletView> {
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<std::path::Path>,
password: crate::OwnerPassword,
) -> ksp_core_lib::Result<crate::WalletOwner> {
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<std::path::Path>,
password: crate::OwnerPassword,
) -> ksp_core_lib::Result<crate::WalletOwner> {
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<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
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<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
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<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
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<u8>) -> 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<u8>,
) -> 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<std::vec::Vec<u8>> {
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(&current) {
return std::result::Result::Err(error);
}
if &current != expected_current {
return std::result::Result::Err(state_conflict_error());
}
return std::result::Result::Ok(());
}
fn replace_wallet_file_with_hook<F>(destination: &std::path::Path, content: &[u8], before_publish: F) -> ksp_core_lib::Result<()>
where
F: std::ops::FnOnce() -> ksp_core_lib::Result<()>,

View File

@@ -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<std::vec::Vec<u8>> {
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<StagedEnvelope> {
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<StagedEnvelope> {
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<std::vec::Vec<u8>> {
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<std::vec::Vec<u8>> {
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<StagedEnvelope> {
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<std::vec::Vec<u8>> {
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<std::vec::Vec<u8>> {
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");
}

View File

@@ -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<std::path::Path>,
source: &[u8],
format: WalletTransferFormat,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
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<std::path::Path>,
source: &[u8],
format: WalletTransferFormat,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
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<std::path::Path>,
source: impl std::convert::AsRef<std::path::Path>,
format: WalletTransferFormat,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
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<std::path::Path>,
source: impl std::convert::AsRef<std::path::Path>,
format: WalletTransferFormat,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
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<u8>,
format: WalletTransferFormat,

View File

@@ -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<std::vec::Vec<u8>> {
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<std::vec::Vec<u8>> {
return self.state.envelope().to_json_bytes();
return self.state.json_bytes_v1();
}
}

View File

@@ -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<crate::Lo
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(crate::LockedWalletInfo::new(envelope.view_descriptor().enabled()));
return std::result::Result::Ok(crate::LockedWalletInfo::new(crate::KSPWALLET_FORMAT_VERSION_V1, envelope.view_descriptor().enabled()));
}
/// Executes the crate-internal create wallet v1 from keypair operation for the owning module.
@@ -821,7 +821,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
let 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,
@@ -830,7 +830,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
view_enabled = state.envelope().view_descriptor().enabled(),
"native wallet created in memory"
);
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)));
}
/// Verifies state signature.

File diff suppressed because it is too large Load Diff

View File

@@ -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<u8>) -> 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 {

View File

@@ -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);
}

View File

@@ -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(());
}

View File

@@ -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");

View File

@@ -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);
}