v0.2.5-pre.010
This commit is contained in:
192
crates/ksp-wallet-lib/README.md
Normal file
192
crates/ksp-wallet-lib/README.md
Normal 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`.
|
||||
279
crates/ksp-wallet-lib/USAGE.md
Normal file
279
crates/ksp-wallet-lib/USAGE.md
Normal 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`.
|
||||
Reference in New Issue
Block a user