Files
khadhroony-solana-project/docs/formats/KSPWALLET_V1.md
2026-08-19 13:09:16 +02:00

833 lines
27 KiB
Markdown

<!-- file: docs/formats/KSPWALLET_V1.md -->
<!-- version: 3 -->
# `.kspwallet` V1 — spécification du format natif Wallet KSP
## 1. Statut et objectif
Ce document est l'autorité normative du **wire `.kspwallet` `format_version = 1`**.
`0.2.5-pre.003` fige :
```text
enveloppe JSON UTF-8 stricte
encodage Base64url sans padding
key slots OWNER / VIEW
limites structurelles V1
transcript binaire OWNER
AAD de wrapping OWNER / VIEW
AAD des compartiments owner-control / metadata / secret
règles unknown-field / unknown-version
```
`0.2.5-pre.004` ajoute les primitives KDF/AEAD normatives et un premier vecteur cryptographique public. `0.2.5-pre.005` fixe maintenant les payloads plaintext V1, l'autorité Ed25519 OWNER, les procédures de création et d'ouverture VIEW/OWNER, le profil de création KSP issu du benchmark opérateur et un vecteur `.kspwallet` complet généré indépendamment du code Rust. La persistence filesystem, les mutations administratives, la signature Solana publique et les adapters import/export restent dans les tranches suivantes. Toute évolution qui modifie un élément déjà déclaré **figé** par cette spécification exige une évolution explicitement tracée avant la release stable ; après publication de V1, une incompatibilité de wire exige un nouveau `format_version`.
Le but final est qu'une implémentation indépendante en Rust, Python, Go, C/C++, Java ou autre puisse créer, parser, vérifier et ouvrir un `.kspwallet` sans lire le code source de `ksp-wallet-lib`.
## 2. Modèle de confiance V1
V1 est **autonome**. Ouvrir un wallet ne requiert aucun :
```text
salt externe
pepper KSP
OTP
secret compilé dans KSP
service distant
réseau
keychain OS
fichier secret annexe
ancre de confiance externe
```
Tous les salts, nonces, paramètres KDF, wrapped keys, ciphertexts et éléments nécessaires à l'interprétation cryptographique sont dans le fichier.
Sans password OWNER, un détenteur du fichier ne doit pas obtenir la keypair Solana, signer, exporter le secret ni produire une modification OWNER-authentifiée des metadata. VIEW donne uniquement accès à Pubkey/alias/notes et à la rotation de **son propre password VIEW**.
Les ACL et permissions du système de fichiers sont hors du contrat cryptographique de `.kspwallet`. Remplacer entièrement un fichier par un autre wallet valide ne révèle pas l'ancien secret et revient à substituer une autre identité Wallet.
## 3. Encodage général
Le document est :
```text
JSON
UTF-8
objet top-level unique
maximum 1 048 576 octets
trailing whitespace JSON autorisé
trailing data non-whitespace interdit
```
L'ordre des propriétés JSON n'est **pas sémantique**. Une implémentation peut réordonner ou réindenter le JSON sans changer le transcript cryptographique tant que l'état sémantique parsé est identique.
Tous les champs binaires utilisent **Base64url RFC 4648 alphabet URL-safe, sans padding** :
```text
alphabet : A-Z a-z 0-9 - _
padding = interdit
trailing bits non canoniques = interdits
```
Le parsing normatif effectue conceptuellement :
```text
decode Base64url sans padding
-> re-encode Base64url sans padding
-> la chaîne obtenue doit être exactement égale à l'entrée
```
## 4. Magic et version
Valeurs V1 :
```text
magic = "KSPWALLET"
format_version = 1
```
Un `magic` différent est invalide.
Un `format_version` différent doit être signalé comme **version non supportée**, et non interprété avec la grammaire V1.
`format_version` ne change pas lors d'une rotation de password, d'une modification d'alias/note ou d'une réécriture atomique.
## 5. Enveloppe JSON exacte
La forme V1 est :
```json
{
"magic": "KSPWALLET",
"format_version": 1,
"owner_auth_public_key": "<32 octets Base64url>",
"view_descriptor": {
"enabled": true,
"slot_id": "<16 octets Base64url ou null>"
},
"key_slots": [
{
"slot_id": "<16 octets Base64url>",
"role": "owner",
"kdf": {
"algorithm": "argon2id",
"version": 19,
"memory_kib": 65536,
"iterations": 3,
"parallelism": 1,
"salt": "<16..64 octets Base64url>"
},
"wrap": {
"algorithm": "xchacha20-poly1305",
"nonce": "<24 octets Base64url>",
"ciphertext": "<Base64url>"
}
},
{
"slot_id": "<16 octets Base64url>",
"role": "view",
"kdf": {
"algorithm": "argon2id",
"version": 19,
"memory_kib": 32768,
"iterations": 4,
"parallelism": 1,
"salt": "<16..64 octets Base64url>"
},
"wrap": {
"algorithm": "xchacha20-poly1305",
"nonce": "<24 octets Base64url>",
"ciphertext": "<Base64url>"
}
}
],
"owner_control": {
"control_version": 1,
"algorithm": "xchacha20-poly1305",
"nonce": "<24 octets Base64url>",
"ciphertext": "<Base64url>"
},
"metadata": {
"metadata_version": 1,
"algorithm": "xchacha20-poly1305",
"nonce": "<24 octets Base64url>",
"ciphertext": "<Base64url>"
},
"secret": {
"secret_version": 1,
"algorithm": "xchacha20-poly1305",
"nonce": "<24 octets Base64url>",
"ciphertext": "<Base64url>"
},
"state_signature": {
"algorithm": "ed25519",
"signature": "<64 octets Base64url>"
}
}
```
Les valeurs Argon2 montrées dans l'exemple OWNER correspondent au profil de création KSP retenu en `pre.005`. L'exemple VIEW reste seulement illustratif : chaque key slot sérialise ses propres paramètres et toute combinaison respectant les bornes V1 reste lisible. Le profil par défaut KSP ne constitue donc pas une contrainte imposée aux implémentations externes conformes.
## 6. `owner_auth_public_key`
`owner_auth_public_key` contient exactement **32 octets** : la clé publique Ed25519 de l'autorité d'administration du format Wallet.
Elle est distincte de la keypair Solana. La Pubkey Solana reste dans le compartiment metadata chiffré et ne doit pas apparaître dans l'enveloppe verrouillée.
La clé privée correspondant à `owner_auth_public_key` appartient au matériau OWNER protégé ; elle n'est jamais un champ public du fichier.
## 7. Descripteur VIEW
`view_descriptor` est OWNER-authentifié.
Règles :
```text
enabled = true => slot_id contient exactement 16 octets et un unique slot role="view" possède le même slot_id
enabled = false => slot_id est null et aucun slot role="view" n'existe
```
Le `slot_id` VIEW est stable pendant une rotation self-service du password VIEW.
VIEW peut modifier uniquement les paramètres de protection de **son slot VIEW courant** dans les limites V1 et rewrapper la même capability metadata. VIEW ne peut pas modifier le descripteur signé, désactiver/recréer VIEW ou transformer son slot en OWNER.
## 8. Key slots
V1 accepte :
```text
exactement 1 slot role="owner"
0 ou 1 slot role="view"
maximum total = 2
slot_id unique entre les slots
```
L'ordre des entrées dans le tableau JSON `key_slots` n'est pas sémantique. Pour le transcript, OWNER est traité séparément et VIEW est représenté par son descripteur stable.
### 8.1 KDF
Identifiant V1 :
```text
algorithm = "argon2id"
version = 19
```
Bornes structurelles de parsing :
```text
memory_kib : 1 .. 1 048 576
iterations : 1 .. 64
parallelism : 1 .. 64
memory_kib >= 8 * parallelism
salt : 16 .. 64 octets
```
Ces plafonds sont des bornes de format/rejet hostile ; ils ne définissent pas à eux seuls les paramètres de création.
Le profil de création KSP V1 retenu en `0.2.5-pre.005` est :
```text
memory_kib = 65 536
iterations = 3
parallelism = 1
salt = 32 octets CSPRNG neufs par slot
```
Le benchmark opérateur du 2026-08-19, exécuté avec le test calibrateur livré par `pre.004`, a mesuré environ 1742 ms pour `64 MiB / 3 / 1`, 3459 ms pour `128 MiB / 3 / 1` et 6925 ms pour `256 MiB / 3 / 1`. Le premier candidat est retenu comme équilibre initial ; ces mesures caractérisent la machine/profil testés et ne sont pas une promesse de latence portable. Les paramètres étant sérialisés dans chaque slot, KSP pourra durcir les defaults futurs sans rendre les wallets existants illisibles.
Le password KDF est la séquence exacte des octets UTF-8 fournis, sans normalisation Unicode implicite, avec une longueur maximale de 1024 octets et un password vide refusé.
### 8.2 Wrapping
Identifiant V1 :
```text
algorithm = "xchacha20-poly1305"
nonce = 24 octets
ciphertext >= 16 octets
ciphertext <= 4096 octets
```
Le ciphertext contient le tag Poly1305 de 16 octets produit par l'AEAD.
Le plaintext wrappé est exactement **32 octets** :
```text
OWNER slot -> K_owner_root (32 octets)
VIEW slot -> K_metadata (32 octets)
```
OWNER et VIEW ont des KDF/salts/nonces indépendants. OWNER n'a jamais besoin du slot VIEW pour accéder à `K_metadata`, puisque cette clé est également contenue dans `owner_control` sous `K_owner_root`.
## 9. Compartiments chiffrés
Trois compartiments indépendants existent :
```text
owner_control
metadata
secret
```
Leurs versions initiales sont indépendantes :
```text
control_version = 1
metadata_version = 1
secret_version = 1
```
Les trois utilisent :
```text
algorithm = "xchacha20-poly1305"
nonce = 24 octets
```
Bornes ciphertext V1 :
```text
owner_control : 16 .. 4096 octets
metadata : 16 .. 65 552 octets
secret : 16 .. 4096 octets
```
La metadata plaintext V1 reste bornée à 65 536 octets. `pre.005` fixe les trois plaintexts :
### 9.1 `owner_control`
Plaintext binaire de **96 octets exacts**, dans cet ordre :
```text
offset 0..32 : admin_signing_secret Ed25519 (32 octets)
offset 32..64 : K_metadata (32 octets)
offset 64..96 : K_secret (32 octets)
```
`admin_signing_secret` est la seed privée Ed25519 correspondant à `owner_auth_public_key`. Cette autorité est distincte de la keypair Solana.
### 9.2 `metadata`
Plaintext JSON UTF-8, objet strict sans champs inconnus :
```json
{
"pubkey": "<Pubkey Solana Base58 canonique>",
"alias": "<string ou null>",
"notes": [
{"id": "<16 octets Base64url sans padding>", "text": "<UTF-8>"}
]
}
```
Contraintes :
```text
pubkey = Base58 canonique d'une Pubkey Solana 32 octets
alias = null ou <= 256 octets UTF-8
notes = maximum 64
note.id = exactement 16 octets, Base64url canonique sans padding, unique dans le payload
note.text = <= 8192 octets UTF-8
payload total <= 65 536 octets
```
L'ordre des propriétés JSON du plaintext metadata n'est pas cryptographiquement canonicalisé : l'AEAD protège les octets plaintext réellement choisis par le créateur, et la signature OWNER protège ensuite le ciphertext. Le serializer KSP émet un JSON compact dans l'ordre `pubkey`, `alias`, `notes`, puis `id`, `text` pour chaque note.
### 9.3 `secret`
Plaintext binaire de **64 octets exacts**, compatible avec la représentation Solana/Ed25519 standard :
```text
offset 0..32 : secret Ed25519 Solana
offset 32..64 : public Ed25519 Solana
```
À l'ouverture OWNER, l'implémentation doit valider que la moitié publique correspond au secret et que la Pubkey dérivée est exactement celle du payload metadata. Un mismatch est du key material invalide, jamais une nouvelle identité acceptée silencieusement.
## 10. Signature d'état OWNER
V1 :
```text
algorithm = "ed25519"
signature = 64 octets
```
La signature porte sur le **transcript sémantique OWNER-controlled**, jamais sur les octets JSON bruts.
`state_signature.signature` elle-même est exclue du transcript. L'identifiant `algorithm = "ed25519"` est inclus.
Les paramètres/salt/nonce/ciphertext du slot VIEW self-service sont exclus de la signature OWNER ; le descripteur stable VIEW est inclus.
La vérification Ed25519 de l'état OWNER-controlled doit précéder toute dérivation de password. `owner_auth_public_key` est parsée comme clé de vérification Ed25519 et la signature est vérifiée en mode strict sur le transcript exact de la section 12. Une mutation de metadata/secret/owner-control/slot OWNER sans clé privée d'administration est donc rejetée avant Argon2. Lors d'une ouverture OWNER, la seed d'administration déchiffrée depuis `owner_control` doit en plus redériver exactement `owner_auth_public_key`.
## 11. Codec binaire transcript/AAD
### 11.1 Préfixe de domaine
Chaque transcript/AAD commence par :
```text
ASCII(domain_separator) || 0x00
```
### 11.2 Champ TLV
Chaque champ suivant utilise exactement :
```text
tag : u16 big-endian
length : u64 big-endian
value : `length` octets
```
Un entier `u32` est encodé dans `value` comme **4 octets big-endian**.
Un booléen est encodé comme un octet :
```text
false = 0x00
true = 0x01
```
Les champs binaires sont les **octets décodés** du Base64url, jamais le texte Base64url.
### 11.3 Tags V1
| Tag hex | Champ |
|--------:|----------------------------------------------------|
| `0001` | magic |
| `0002` | format_version |
| `0003` | owner_auth_public_key |
| `0010` | view enabled |
| `0011` | rôle VIEW littéral `view` |
| `0012` | view slot_id ; longueur zéro lorsque VIEW disabled |
| `0100` | slot_id |
| `0101` | slot role |
| `0102` | KDF algorithm |
| `0103` | KDF version |
| `0104` | KDF memory_kib |
| `0105` | KDF iterations |
| `0106` | KDF parallelism |
| `0107` | KDF salt |
| `0108` | wrap algorithm |
| `0109` | wrap nonce |
| `010A` | wrap ciphertext |
| `0200` | compartment kind |
| `0201` | compartment payload version |
| `0202` | compartment algorithm |
| `0203` | compartment nonce |
| `0204` | compartment ciphertext |
| `0500` | state-signature algorithm |
Les tags ne remplacent pas l'ordre normatif ; **l'ordre ci-dessous est obligatoire**.
## 12. Transcript OWNER state signature
Domain separator :
```text
KSPWALLET-V1-STATE
```
Ordre exact :
```text
0001 magic = ASCII "KSPWALLET"
0002 format_version = u32 BE 1
0003 owner_auth_public_key = 32 octets
0010 view enabled = 00/01
0011 ASCII "view"
0012 view slot_id = 16 octets si enabled, longueur 0 sinon
OWNER slot uniquement :
0100 owner slot_id
0101 ASCII "owner"
0102 ASCII "argon2id"
0103 Argon2 version u32 BE
0104 memory_kib u32 BE
0105 iterations u32 BE
0106 parallelism u32 BE
0107 owner salt
0108 ASCII "xchacha20-poly1305"
0109 owner wrap nonce
010A owner wrap ciphertext
owner_control :
0200 ASCII "owner-control"
0201 control_version u32 BE
0202 ASCII "xchacha20-poly1305"
0203 nonce
0204 ciphertext
metadata :
0200 ASCII "metadata"
0201 metadata_version u32 BE
0202 ASCII "xchacha20-poly1305"
0203 nonce
0204 ciphertext
secret :
0200 ASCII "secret"
0201 secret_version u32 BE
0202 ASCII "xchacha20-poly1305"
0203 nonce
0204 ciphertext
0500 ASCII "ed25519"
```
Sont explicitement **exclus** du transcript OWNER :
```text
VIEW KDF parameters
VIEW salt
VIEW wrap nonce
VIEW wrap ciphertext
state_signature.signature
```
Cette exclusion autorise VIEW à changer son propre password en rewrappant sa capability sans posséder l'autorité OWNER. Elle ne lui permet pas de modifier metadata/secret/OWNER state, qui restent signés.
## 13. AAD des key slots
### 13.1 OWNER
Domain separator :
```text
KSPWALLET-V1-AAD-OWNER-SLOT
```
Ordre exact :
```text
0001 magic
0002 format_version
0003 owner_auth_public_key
0100 slot_id
0101 "owner"
0102 "argon2id"
0103 KDF version
0104 memory_kib
0105 iterations
0106 parallelism
0107 salt
0108 "xchacha20-poly1305"
```
Le nonce est passé séparément à l'AEAD et n'est pas répété dans l'AAD. Le ciphertext/tag est le résultat AEAD et n'appartient pas à son propre AAD.
### 13.2 VIEW
Domain separator :
```text
KSPWALLET-V1-AAD-VIEW-SLOT
```
La suite TLV est identique à OWNER sauf :
```text
0101 = ASCII "view"
0100 = slot_id VIEW signé par view_descriptor
```
Les paramètres KDF/salt sont volontairement dans l'AAD calculé pour **l'état courant** du slot VIEW. VIEW peut les remplacer lors d'une rotation de son password, puis produire un nouveau wrapping valide de la même capability metadata.
## 14. AAD des compartiments
Les trois AAD commencent par les champs communs :
```text
0001 magic
0002 format_version
0003 owner_auth_public_key
```
Puis :
```text
0200 compartment kind
0201 compartment payload version
0202 "xchacha20-poly1305"
```
Domain separators :
```text
owner_control : KSPWALLET-V1-AAD-OWNER-CONTROL
metadata : KSPWALLET-V1-AAD-METADATA
secret : KSPWALLET-V1-AAD-SECRET
```
Valeurs de `compartment kind` :
```text
owner-control
metadata
secret
```
Le nonce est fourni séparément à l'AEAD et le ciphertext/tag est le résultat de l'opération ; ni l'un ni l'autre n'est dupliqué dans cet AAD.
## 15. Parsing et rejet stricts
V1 rejette :
```text
magic inconnu
champ top-level inconnu
champ nested inconnu
champ requis absent
champ dupliqué JSON
role slot autre que owner/view
algorithme autre que les identifiants V1
Argon2 version autre que 19
KDF hors bornes structurelles
Base64url invalide, paddé ou non canonique
owner_auth_public_key != 32 octets
slot_id != 16 octets
XChaCha nonce != 24 octets
state signature != 64 octets
OWNER slot absent ou dupliqué
VIEW slot dupliqué
slot_id OWNER == slot_id VIEW
VIEW descriptor incohérent avec VIEW slot
payload version autre que 1
ciphertext sous 16 octets ou au-dessus de sa borne V1
document > 1 MiB
trailing data non-whitespace
```
La vérification de taille du document précède le parsing JSON et toute opération KDF coûteuse.
## 16. JSON produit par KSP
Le serializer KSP V1 émet actuellement :
```text
JSON pretty-print
ordre stable des champs du modèle KSP
OWNER slot avant VIEW slot
newline final
```
Cet ordre et ce pretty-print sont un **profil de sortie KSP**, pas une canonicalisation cryptographique. Une implémentation externe conforme peut produire un autre ordre/espacement JSON si le parsing V1 aboutit au même état sémantique.
## 17. Checksum
Aucun checksum supplémentaire V1.
La détection cryptographique repose sur :
```text
AEAD des wrapped keys/compartiments
state_signature OWNER pour l'état OWNER-controlled
AAD domain-separated
```
Un checksum non authentifié n'ajouterait pas de garantie de sécurité utile.
## 18. Fixture structurelle `pre.003`
Le dépôt contient :
```text
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_wire_only.json
```
Cette fixture est **wire-only et test-only** :
```text
ses ciphertexts sont des octets artificiels
sa signature est artificielle
elle ne constitue pas un wallet cryptographiquement valide
elle ne fixe pas les defaults Argon2 de production
elle ne contient aucune clé réelle
```
Elle fige néanmoins :
```text
JSON V1
Base64url
longueurs
slots/descripteur
transcript OWNER exact
AAD exacts
round-trip du codec
```
Le premier vecteur cryptographique public KDF+wrapping est ajouté par `pre.004`. `pre.005` ajoute en plus un `.kspwallet` complet cryptographiquement valide et ses métadonnées de contrôle test-only, décrits en section 22.
## 19. Invariants encore à compléter sans modifier le wire figé
Après `pre.005`, les tranches restantes portent sur les opérations autour du format déjà défini :
```text
pre.006 : persistence async/atomique/no-clobber
pre.007 : signature Solana, metadata admin, rotations OWNER/VIEW et révocation forte VIEW
pre.008 : import/export
pre.009+ : audit adversarial, compliance et documentation de clôture
```
Toute découverte imposant de modifier la grammaire, les payloads plaintext, les tags, l'ordre transcript ou les domain separators définis dans ce document doit être traitée explicitement avant la publication stable, jamais masquée par une tolérance du parseur.
## 20. Primitives cryptographiques effectives depuis `pre.004`
### 20.1 Argon2id
La dérivation d'une key-encryption key (KEK) V1 utilise exactement :
```text
algorithm = Argon2id
version = 19 / 0x13
output = 32 octets
password = octets UTF-8 exacts fournis par le caller
salt = octets sérialisés dans le key slot
m_cost = memory_kib sérialisé
t_cost = iterations sérialisé
p_cost = parallelism sérialisé
pepper = aucun
secret Argon2 externe = aucun
```
Un password vide est rejeté. V1 limite l'entrée password à 1024 octets UTF-8. Le KDF est une opération CPU/mémoire coûteuse ; les futures API async create/open l'exécuteront hors du thread executor conformément au plan Wallet.
Le default de création n'est **pas** déterminé par les defaults de la crate RustCrypto. Le benchmark opérateur a comparé :
```text
64 MiB / 3 passes / 1 lane -> 1742 ms
128 MiB / 3 passes / 1 lane -> 3459 ms
256 MiB / 3 passes / 1 lane -> 6925 ms
```
KSP retient donc initialement `64 MiB / 3 / 1` avec un salt CSPRNG de 32 octets par slot. Un wallet conserve toujours ses propres paramètres sérialisés, indépendamment des defaults futurs.
### 20.2 XChaCha20-Poly1305
V1 utilise une clé de **32 octets** et un nonce de **24 octets**. Le résultat `ciphertext` stocké est la sortie AEAD avec tag Poly1305 postfixé de 16 octets. Chaque chiffrement/wrapping de production reçoit un nonce neuf fourni par le CSPRNG OS. Les AAD sont les TLV domain-separated définis dans cette spécification ; ils ne sont ni secrets ni chiffrés.
Une erreur de déchiffrement/tag est exposée comme une erreur générique d'authentification Wallet et ne distingue pas ciphertext, key, nonce ou AAD incorrect.
### 20.3 CSPRNG
KSP utilise la source cryptographique du système d'exploitation via `getrandom`. V1 ne définit aucun PRNG, seed ou état aléatoire propriétaire KSP. La génération de content keys, salts et nonces de production doit échouer si le CSPRNG OS échoue.
### 20.4 Content keys et wrapping
Les content keys V1 manipulées par les primitives de `pre.004` sont des clés symétriques de 32 octets :
```text
OWNER password -> Argon2id -> KEK_OWNER -> XChaCha wrap -> K_owner_root
VIEW password -> Argon2id -> KEK_VIEW -> XChaCha wrap -> K_metadata
```
Les clés possédées en mémoire sont non-`Copy`, non-`Clone` par défaut, redacted en `Debug` et zeroized au `Drop`. Lors d'un unwrap, le plaintext intermédiaire est zeroized après copie dans le type secret possédé.
### 20.5 Vecteur interopérable `pre.004`
Le fichier public :
```text
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_crypto_vectors.json
```
contient un password, un salt, une content key et un nonce **test-only publics**. Il fixe :
```text
Argon2id -> derived_key attendu
XChaCha20-Poly1305(derived_key, nonce, AAD, content_key) -> wrapped_key attendu
```
Le vecteur a été recalculé indépendamment de l'implémentation Rust avec Argon2id puis la construction XChaCha20-Poly1305 `HChaCha20 + ChaCha20-Poly1305 IETF`. Ces valeurs ne constituent jamais des secrets de production.
## 21. Procédures V1 matérialisées par `pre.005`
### 21.1 Création
Une création KSP V1 en mémoire suit conceptuellement :
```text
1. générer K_owner_root, K_metadata et K_secret indépendants (32 octets chacun) ;
2. générer une seed Ed25519 d'administration de format indépendante de la keypair Solana ;
3. générer une nouvelle keypair Solana Ed25519 et dériver la Pubkey metadata ;
4. encoder owner_control, metadata et secret selon la section 9 ;
5. générer slot_id/salt/nonce OWNER et, si demandé, slot_id/salt/nonce VIEW indépendants ;
6. Argon2id(password OWNER) -> KEK_OWNER -> wrap K_owner_root ;
7. si VIEW existe : Argon2id(password VIEW) -> KEK_VIEW -> wrap K_metadata ;
8. chiffrer owner_control avec K_owner_root, metadata avec K_metadata, secret avec K_secret ;
9. construire le transcript OWNER et le signer avec l'autorité Ed25519 de format ;
10. vérifier immédiatement la signature produite avant de retourner le handle OWNER.
```
Les opérations Argon2id sont exécutées hors du thread executor async par une frontière blocking dédiée. La création `pre.005` est **in-memory** : la persistence arrive en `pre.006`.
### 21.2 Ouverture VIEW
```text
1. parser/rejeter strictement l'enveloppe ;
2. vérifier la state signature OWNER avant tout KDF ;
3. exiger un slot VIEW cohérent avec view_descriptor ;
4. Argon2id(password VIEW, paramètres du slot VIEW) ;
5. unwrap K_metadata avec l'AAD VIEW courant ;
6. déchiffrer uniquement metadata ;
7. valider le payload metadata ;
8. retourner Pubkey/alias/notes + capability VIEW.
```
Une ouverture VIEW ne déchiffre **jamais** `owner_control` ni `secret` et ne matérialise ni `K_owner_root`, ni `K_secret`, ni la keypair Solana, ni la seed Ed25519 d'administration.
### 21.3 Ouverture OWNER
```text
1. parser/rejeter strictement l'enveloppe ;
2. vérifier la state signature OWNER avant tout KDF ;
3. Argon2id(password OWNER, paramètres slot OWNER) ;
4. unwrap K_owner_root ;
5. déchiffrer owner_control et récupérer admin_signing_secret, K_metadata, K_secret ;
6. vérifier que admin_signing_secret redérive owner_auth_public_key ;
7. déchiffrer/valider metadata ;
8. déchiffrer secret, reconstruire strictement la keypair Solana 64 octets ;
9. vérifier cohérence secret/public et égalité avec la Pubkey metadata ;
10. retourner capability OWNER en conservant les secrets seulement dans l'état OWNER opaque.
```
Aucun getter public de secret n'est introduit par cette procédure.
## 22. Vecteur complet interopérable `pre.005`
Le dépôt publie :
```text
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector.json
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector_meta.json
```
Ces fichiers sont **TEST ONLY**. Ils publient volontairement passwords, secrets, clés et résultats attendus et ne doivent jamais servir de wallet réel ni d'exemple de paramètres sécurisés. Pour garder les tests rapides, leurs key slots utilisent `32 KiB / 2 / 1`, très en dessous du profil de création KSP.
Le vector fixe notamment :
```text
password OWNER = pre005-owner-password
password VIEW = pre005-view-password
Pubkey attendue = 8zH45w576QJUEGtpXqZvEi6UPddmMfKopLatocZGDw6
alias attendu = pre005-vector-wallet
2 notes test-only
owner/view derived keys
autorité Ed25519
K_owner_root / K_metadata / K_secret
owner-control plaintext
metadata plaintext
keypair Solana 64 octets
state transcript exact
state signature exacte
wallet JSON complet
```
Le vecteur a été généré et revérifié indépendamment du code Rust avec Argon2id, XChaCha20-Poly1305 construit via HChaCha20 + ChaCha20-Poly1305 IETF et Ed25519. Une implémentation externe conforme doit pouvoir reproduire les mêmes dérivations, déchiffrements et vérifications à partir des deux fichiers sans dépendre d'un type Rust KSP.