v0.2.5-pre.010

This commit is contained in:
2026-08-20 09:58:14 +02:00
parent e91bf36e9e
commit 5bf9651038
15 changed files with 1363 additions and 43 deletions

View File

@@ -0,0 +1,192 @@
<!-- file: crates/ksp-wallet-lib/README.md -->
<!-- version: 1 -->
# `ksp-wallet-lib`
`ksp-wallet-lib` est la bibliothèque KSP propriétaire du Wallet Solana natif. Elle possède le format autonome `.kspwallet` V1, les capacités indépendantes VIEW/OWNER, la protection du secret Solana, la signature, l'administration des metadata, les rotations de credentials, la persistence native et les adapters d'import/export explicitement supportés.
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.
## Responsabilités
La crate possède :
- le format natif `.kspwallet` V1 et son parser JSON strict ;
- les key slots OWNER/VIEW indépendants ;
- Argon2id pour les KDF de passwords ;
- XChaCha20-Poly1305 pour le wrapping et les compartiments ;
- une autorité Ed25519 d'administration distincte de la keypair Solana ;
- les compartiments `owner_control`, `metadata` et `secret` ;
- la création et l'ouverture in-memory ;
- la création et l'ouverture fichier async-first ;
- la projection verrouillée minimale ;
- la signature Solana via OWNER sans getter secret ;
- l'alias et les notes protégés ;
- les rotations OWNER/VIEW ;
- la révocation forte VIEW par OWNER ;
- la persistence create/import no-clobber ;
- le remplacement administratif capability-bound avec détection de handle stale ;
- l'inspection/import/export Solana CLI JSON et Base58 de keypair complète ;
- les diagnostics et erreurs Wallet sans exposition de secrets.
La crate ne possède pas :
```text
Config
Transport HTTP/WebSocket/gRPC
balance/réseau
WalletPolicy / execution policy
construction ou envoi de transaction
Store
Tauri/UI
hardware wallet / remote signer
anti-rollback externe
```
## Frontières de dépendances
La direction de production est :
```text
ksp-wallet-lib
-> ksp-core-lib
-> ksp-logging-lib
-> argon2 / chacha20poly1305 / getrandom
-> ed25519-dalek / solana-keypair
-> serde / serde_json
-> tempfile / tokio
-> zeroize
```
Les dépendances suivantes sont interdites :
```text
ksp-wallet-lib -X-> ksp-config-lib
ksp-wallet-lib -X-> ksp-onchain-transport-lib
ksp-wallet-lib -X-> execution policy
ksp-wallet-lib -X-> Store
ksp-wallet-lib -X-> Tauri
ksp-wallet-lib -X-> tracing direct
ksp-wallet-lib -X-> std::env
ksp-wallet-lib -X-> solana-pubkey direct
ksp-wallet-lib -X-> solana-signer direct
ksp-wallet-lib -X-> solana-signature direct
```
La Pubkey publique est toujours `ksp_core_lib::Pubkey`. `solana-keypair` reste un détail secret/signing interne à Wallet et n'est pas réexportée.
## Modèle de capacités
### Verrouillé
Sans password, `inspect_locked_wallet_v1` et `inspect_locked_wallet_file_v1` exposent uniquement :
```text
format_version
view_enabled
```
La Pubkey, l'alias et les notes restent chiffrés.
### VIEW
`WalletView` expose :
```text
Pubkey
alias
notes
rotation de son propre password VIEW
```
VIEW ne possède aucune API de signature, d'export secret, de mutation metadata, de rotation OWNER ou de désactivation/recréation VIEW.
### OWNER
`WalletOwner` expose toutes les metadata autorisées et ajoute :
```text
signature Solana
export du secret via adapters explicites
alias/notes administration
rotation OWNER
rotation VIEW sans ancien password VIEW
disable VIEW avec rekey metadata fort
recreate VIEW avec nouveau slot et nouveau K_metadata
```
OWNER ne dépend jamais du password VIEW.
## Format `.kspwallet` V1
Le format est publiquement spécifié dans [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md). Il est autonome : un fichier valide et le password de la capacité concernée suffisent à l'ouverture ; aucun pepper KSP, keychain, OTP, service distant, réseau ou secret externe n'est requis.
Les principales primitives sont :
```text
KDF Argon2id v19
profil création 65 536 KiB / 3 iterations / 1 lane
AEAD XChaCha20-Poly1305
CSPRNG OS via getrandom
state signature Ed25519
wire binary Base64url sans padding
format JSON UTF-8 strict
```
Les paramètres KDF sont sérialisés dans chaque slot afin que de futurs defaults puissent évoluer sans rendre les wallets existants illisibles.
## Persistence
Toute création/import reçoit un chemin explicite du caller et applique le no-clobber. Wallet ne découvre ni ne crée un répertoire configuré par lui-même.
Les mutations OWNER/VIEW persistées utilisent un remplacement capability-bound : le fichier courant doit encore correspondre à l'enveloppe authentifiée attendue par le handle. Un handle stale ou une mauvaise cible retourne `wallet.state_conflict`.
Cette protection ne constitue pas un CAS filesystem linéarisable et ne fournit pas d'anti-rollback externe. Les garanties d'atomicité/crash-durability dépendent également de l'OS et du filesystem.
## Import/export
Les formats built-in V1 sont :
```text
WalletTransferFormat::SolanaCliJson
WalletTransferFormat::SolanaKeypairBase58
```
L'inspection d'un transfert retourne seulement sa Pubkey et son format. L'import crée toujours un nouveau `.kspwallet` no-clobber autour de la keypair validée. L'export secret appartient uniquement à `WalletOwner`.
`WalletTransferFormat` est `#[non_exhaustive]` afin de permettre des formats built-in supplémentaires sans prétendre fournir un plugin public arbitraire de codec secret.
## Sécurité et limites
Le threat model V1 considère notamment un attaquant possédant une copie complète du fichier et capable d'essais de password offline sans limite serveur. Argon2id augmente le coût de chaque essai ; il ne compense pas un password faible.
Limites explicitement assumées :
- remplacement total par un autre wallet valide non détectable depuis le nouveau fichier seul ;
- rollback vers une ancienne copie valide non détectable sans état/ancre externe ;
- absence de second facteur, keychain, hardware wallet ou remote signer en V1 ;
- `zeroize` réduit les copies possédées mais ne prouve pas l'effacement physique de toute copie potentielle ;
- les permissions filesystem sont une hygiène externe, pas une garantie cryptographique.
La matrice durable [`../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) enregistre les canaris adversariaux, l'interop indépendante et l'audit Cargo.
## Tests et vecteurs
Les fixtures publiques test-only sont sous `tests/fixtures/` :
```text
kspwallet_v1_wire_only.json
kspwallet_v1_crypto_vectors.json
kspwallet_v1_full_vector.json
kspwallet_v1_full_vector_meta.json
```
Elles couvrent le wire, Argon2id/XChaCha20-Poly1305, l'ouverture VIEW/OWNER, la signature d'état et l'interop transfer. Les tests adversariaux couvrent tampering, wrong passwords, frontières VIEW/OWNER, persistence, concurrence et diagnostics secrets.
## Documentation
- [`USAGE.md`](USAGE.md) — exemples des principales surfaces publiques ;
- [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md) — spécification normative indépendante de Rust ;
- [`../../docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](../../docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan historique et threat model de `0.2.5` ;
- [`../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) — matrice de sécurité/interoperabilité/compliance ;
- [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_START_PROMPT.md) — reprise vers Wallet Desk après publication stable de `0.2.5`.

View File

@@ -0,0 +1,279 @@
<!-- file: crates/ksp-wallet-lib/USAGE.md -->
<!-- version: 2 -->
# 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).
Les exemples utilisent des chemins explicites : Wallet ne lit ni Config ni environnement pour découvrir un répertoire.
## 1. Créer un nouveau `.kspwallet`
```rust
async fn create_example() -> ksp_core_lib::Result<()> {
let metadata = ksp_wallet_lib::WalletCreateMetadataV1::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(
"wallets/devnet-main.kspwallet",
owner_password,
Some(view_password),
metadata,
)
.await;
let owner = match created {
Ok(value) => value,
Err(error) => return Err(error),
};
println!("{}", owner.pubkey());
return Ok(());
}
```
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.
## 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 locked = match inspected {
Ok(value) => value,
Err(error) => return Err(error),
};
println!("format={}", locked.format_version());
println!("view_enabled={}", locked.view_enabled());
return Ok(());
}
```
Cette projection ne contient volontairement ni Pubkey, ni alias, ni notes.
## 3. Ouvrir avec VIEW
```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 mut view = match opened {
Ok(value) => value,
Err(error) => return Err(error),
};
println!("pubkey={}", view.pubkey());
println!("alias={:?}", view.alias());
for note in view.notes() {
println!("note={} text={}", note.id(), note.text());
}
let rotated = view
.rotate_view_password(
"wallets/devnet-main.kspwallet",
ksp_wallet_lib::ViewPassword::new(std::string::String::from("NEW-VIEW-PASSWORD")),
)
.await;
if let Err(error) = rotated {
return Err(error);
}
return Ok(());
}
```
VIEW ne possède aucune API `sign`, `export_transfer`, `update_alias`, `add_note`, `rotate_owner_password`, `disable_view` ou `recreate_view`.
## 4. Ouvrir avec OWNER et signer
```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 owner = match opened {
Ok(value) => value,
Err(error) => return Err(error),
};
return owner.sign(message);
}
```
La signature retournée contient 64 octets Ed25519. Aucun getter public ne retourne la keypair Solana.
## 5. Administrer alias et notes
```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 mut owner = match opened {
Ok(value) => value,
Err(error) => return Err(error),
};
let alias_result = owner
.update_alias("wallets/devnet-main.kspwallet", Some(std::string::String::from("primary-devnet")))
.await;
if let Err(error) = alias_result {
return Err(error);
}
let added = owner
.add_note("wallets/devnet-main.kspwallet", std::string::String::from("rotation trimestrielle"))
.await;
let note_id = match added {
Ok(value) => value,
Err(error) => return Err(error),
};
let updated = owner
.update_note(
"wallets/devnet-main.kspwallet",
note_id.as_str(),
std::string::String::from("rotation contrôlée"),
)
.await;
if let Err(error) = updated {
return Err(error);
}
return Ok(());
}
```
Chaque mutation vérifie que le fichier courant correspond encore à l'état authentifié attendu par le handle. Un handle stale reçoit `wallet.state_conflict`.
## 6. Rotations de credentials
OWNER peut changer son password :
```rust
let result = owner
.rotate_owner_password(
"wallets/devnet-main.kspwallet",
ksp_wallet_lib::OwnerPassword::new(std::string::String::from("NEW-OWNER-PASSWORD")),
)
.await;
```
OWNER peut aussi changer le password VIEW sans connaître l'ancien :
```rust
let result = owner
.rotate_view_password(
"wallets/devnet-main.kspwallet",
ksp_wallet_lib::ViewPassword::new(std::string::String::from("NEW-VIEW-PASSWORD")),
)
.await;
```
Ces rotations de credentials ne changent pas la keypair Solana.
## 7. Révocation forte VIEW
La simple rotation VIEW remplace le credential courant mais ne peut pas retirer des metadata déjà connues d'un ancien détenteur VIEW. OWNER peut effectuer une révocation forte pour les metadata futures :
```rust
let disabled = owner.disable_view("wallets/devnet-main.kspwallet").await;
if let Err(error) = disabled {
return Err(error);
}
let recreated = owner
.recreate_view(
"wallets/devnet-main.kspwallet",
ksp_wallet_lib::ViewPassword::new(std::string::String::from("FRESH-VIEW-PASSWORD")),
)
.await;
```
`disable_view` rekey les metadata. `recreate_view` crée ensuite un nouveau slot VIEW autour du nouveau `K_metadata`. La keypair Solana reste inchangée.
## 8. Inspecter et importer une keypair externe
Le format doit être choisi explicitement ; Wallet ne fait pas d'auto-détection heuristique.
```rust
async fn inspect_transfer_example(source: &[u8]) -> ksp_core_lib::Result<()> {
let inspected = ksp_wallet_lib::inspect_wallet_transfer(
source,
ksp_wallet_lib::WalletTransferFormat::SolanaCliJson,
);
let info = match inspected {
Ok(value) => value,
Err(error) => return Err(error),
};
println!("{}", info.pubkey());
return Ok(());
}
```
Import fichier vers un nouveau `.kspwallet` :
```rust
let imported = ksp_wallet_lib::import_wallet_transfer_file_v1(
"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![]),
)
.await;
```
L'import valide la cohérence secret/public, conserve exactement la keypair Solana et génère un nouvel environnement cryptographique KSP. Une destination native existante n'est jamais écrasée.
## 9. Exporter avec OWNER
En mémoire :
```rust
let exported = owner.export_transfer(ksp_wallet_lib::WalletTransferFormat::SolanaKeypairBase58);
```
Vers un fichier no-clobber :
```rust
let exported = owner
.export_transfer_file(
"exports/devnet-main.keypair",
ksp_wallet_lib::WalletTransferFormat::SolanaCliJson,
)
.await;
```
Les octets retournés par `export_transfer` contiennent volontairement le secret. Le caller doit limiter leur durée de vie et les zeroize lorsqu'approprié. Sur Unix, l'export fichier tente `0600`, qui reste une hygiène filesystem et non une garantie cryptographique.
## 10. Surfaces in-memory
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
```
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.
## 11. Erreurs et diagnostics
Les erreurs Wallet sont des `ksp_core_lib::Error` avec codes `wallet.*`. Les erreurs de password restent génériques (`owner_unlock_failed`, `view_unlock_failed`) et ne doivent jamais être transformées en oracle détaillant KDF/wrap/ciphertext.
Les `Debug` des passwords et metadata protégées sont redacted. Les logs Wallet ne doivent contenir ni password, ni keypair, ni payload exporté.
## 12. Intégration avec Config et Transport
Wallet lui-même ne dépend ni de Config ni de Transport. Une application compose explicitement les couches :
```text
ksp-config-lib
-> fournit chemins/profils applicatifs
ksp-wallet-lib
-> ouvre le wallet et fournit la Pubkey autorisée
ksp-onchain-transport-lib
-> utilise cette Pubkey pour getBalance et autres lectures réseau
```
Cette composition est le rôle de `0.2.6 — ksp-app-wallet-desk`, pas de `ksp-wallet-lib`.