Files
khadhroony-solana-project/docs/formats/KSPWALLET_V2.md
2026-08-22 09:39:13 +02:00

480 lines
15 KiB
Markdown

<!-- file: docs/formats/KSPWALLET_V2.md -->
<!-- version: 2 -->
# `.kspwallet` V2 — spécification du wire binaire natif KSP
## 1. Statut et objectif
Ce document est l'autorité normative du **wire `.kspwallet` `format_version = 2`** introduit par `0.2.6-pre.015`.
V2 remplace l'enveloppe JSON/Base64url de V1 par un framing binaire KSP canonique. L'objectif est :
```text
réduire fortement la taille persistée
supprimer l'encodage Base64 des champs déjà binaires
éviter qu'un .kspwallet nouvellement créé soit un document JSON lisible comme tel
conserver un format documenté et implémentable hors Rust
préserver les frontières cryptographiques Wallet
```
Le caractère binaire **n'ajoute aucune propriété cryptographique**. La confidentialité et l'authenticité continuent de dépendre d'Argon2id, XChaCha20-Poly1305 et Ed25519, jamais de l'absence de JSON.
`0.2.6-pre.015` fige la grammaire binaire, les identifiants numériques, les bornes structurelles et les transcripts/AAD V2. `0.2.6-pre.016` matérialise la création/ouverture/persistence V2, l'auto-détection V1/V2, les APIs génériques/versionnées et le basculement de Wallet Desk vers la façade non versionnée. `0.2.6-pre.017` ajoute la migration V1 -> V2 explicite et OWNER-authentifiée, en copie no-clobber ou remplacement atomique in-place.
V1 reste un format stable supporté. V2 ne réinterprète jamais un document V1 avec sa propre grammaire.
## 2. Politique de version et API
Les notions suivantes sont distinctes :
```text
DEFAULT_WALLET_FORMAT
LATEST_SUPPORTED_WALLET_FORMAT
```
À partir de l'intégration V2 :
```text
DEFAULT_WALLET_FORMAT = V2
LATEST_SUPPORTED_WALLET_FORMAT = V2
```
Si un futur V3 apparaît, par exemple pour un modèle d'autorisation avec second facteur, `LATEST_SUPPORTED_WALLET_FORMAT` pourra devenir V3 tandis que `DEFAULT_WALLET_FORMAT` pourra **rester V2**. Le default ne suit jamais automatiquement la dernière version.
La politique d'API retenue est :
```text
create_wallet_file(...) -> format default explicitement choisi, V2
create_wallet_file_v1(...) -> force V1
create_wallet_file_v2(...) -> force V2
create_wallet_file_v3(...) -> force V3 si V3 existe un jour
open_wallet_*_file(...) -> détecte puis dispatch les versions supportées
open_wallet_*_file_v1(...) -> exige V1
open_wallet_*_file_v2(...) -> exige V2
open_wallet_*_file_v3(...) -> exige V3 si V3 existe un jour
```
Le même principe s'applique aux opérations dont la version de fichier est pertinente, notamment inspection et import natif.
### 2.1 Surface matérialisée en `pre.016`
La politique ci-dessus est désormais du code, pas uniquement une cible documentaire :
```text
create_wallet(...) -> V2 default
create_wallet_v1(...) -> V1 forcé
create_wallet_v2(...) -> V2 forcé
create_wallet_file(...) -> V2 default
create_wallet_file_v1(...) -> V1 forcé
create_wallet_file_v2(...) -> V2 forcé
open_wallet_view/owner(...) -> détecte V1/V2
open_wallet_*_v1/_v2(...) -> exige exactement V1/V2
inspect_locked_wallet(...) -> détecte V1/V2
inspect_locked_wallet_v1/_v2 -> exige exactement V1/V2
import_wallet_transfer(...) -> crée le default V2
import_wallet_transfer_v1/_v2 -> force le format natif produit
```
`WalletOwner` et `WalletView` mémorisent le format natif authentifié. Les opérations d'administration et de rotation persistent dans ce même format ; aucune mutation ordinaire ne réalise de conversion V1/V2. `to_native_bytes()` sérialise le format courant, tandis que `to_json_bytes()` reste une compatibilité V1 et rejette V2.
## 3. Encodage général
Un document V2 est :
```text
binaire
maximum 1 048 576 octets
entiers multi-octets = unsigned big-endian
ordre des champs = normatif
aucun padding implicite
aucune Base64
aucune compression
aucun trailing byte
```
Les longueurs sont exprimées en **octets**.
Une implémentation conforme doit vérifier les bornes **avant** toute allocation dépendant d'une longueur reçue. Le document entier est déjà borné par `1 048 576` octets avant parsing.
## 4. Header V2
Le début de fichier est strictement :
| Ordre | Champ | Taille | Valeur / règle |
|---:|---|---:|---|
| 1 | `magic` | 9 | ASCII exact `KSPWALLET` |
| 2 | `format_version` | 2 | `0x0002` |
| 3 | `document_length` | 4 | longueur totale exacte du fichier |
| 4 | `flags` | 2 | bit 0 = VIEW activé ; tous les autres bits = 0 |
| 5 | `owner_auth_public_key` | 32 | clé publique Ed25519 OWNER |
| 6 | `view_descriptor.slot_id` | 16 conditionnels | présent uniquement si `flags & 0x0001 != 0` |
Offsets fixes avant le descripteur conditionnel :
```text
0x0000..0x0008 magic
0x0009..0x000A format_version
0x000B..0x000E document_length
0x000F..0x0010 flags
0x0011..0x0030 owner_auth_public_key
```
Pour un wallet VIEW-enabled, `view_descriptor.slot_id` occupe ensuite `0x0031..0x0040`.
### 4.1 Flags
```text
0x0001 VIEW_ENABLED
0xFFFE réservé, doit être zéro
```
Tout bit réservé non nul rend le document invalide. V2 n'emploie pas les bits réservés comme mécanisme d'extension silencieuse ; une modification incompatible exige un nouveau format.
## 5. Ordre canonique du body
Après le header :
```text
OWNER key slot obligatoire
VIEW key slot présent seulement si VIEW_ENABLED
OWNER-CONTROL encrypted compartment obligatoire
METADATA encrypted compartment obligatoire
SECRET encrypted compartment obligatoire
state signature obligatoire
EOF immédiat
```
Aucun compteur de slots ou de compartiments n'est nécessaire : leur cardinalité et leur ordre sont déterminés par V2.
## 6. Key slot V2
Chaque key slot est encodé ainsi :
| Champ | Taille | Valeur / règle |
|---|---:|---|
| `role` | 1 | `0x01` OWNER, `0x02` VIEW |
| `slot_id` | 16 | identifiant binaire exact |
| `kdf_algorithm` | 1 | `0x01` Argon2id |
| `kdf_version` | 4 | `19` |
| `memory_kib` | 4 | `1..1 048 576`, et `>= parallelism * 8` |
| `iterations` | 4 | `1..64` |
| `parallelism` | 4 | `1..64` |
| `salt_length` | 1 | `16..64` |
| `salt` | variable | exactement `salt_length` octets |
| `wrap_algorithm` | 1 | `0x01` XChaCha20-Poly1305 |
| `wrap_nonce` | 24 | nonce exact |
| `wrap_ciphertext_length` | 2 | `16..4096` |
| `wrap_ciphertext` | variable | exactement la longueur déclarée |
Ordre obligatoire :
```text
premier slot = OWNER
second slot = VIEW seulement si VIEW_ENABLED
```
Si VIEW est activé :
```text
header.view_descriptor.slot_id == VIEW key slot.slot_id
```
Toute divergence est invalide avant KDF/déchiffrement.
## 7. Compartiments V2
Chaque compartiment est encodé :
| Champ | Taille | Valeur / règle |
|---|---:|---|
| `kind` | 1 | `0x01` OWNER-CONTROL, `0x02` METADATA, `0x03` SECRET |
| `payload_version` | 4 | `1` pour le profil initial V2 |
| `algorithm` | 1 | `0x01` XChaCha20-Poly1305 |
| `nonce` | 24 | nonce exact |
| `ciphertext_length` | 4 | longueur exacte |
| `ciphertext` | variable | ciphertext + tag Poly1305 |
Bornes initiales :
```text
OWNER-CONTROL : 16..4096 octets
METADATA : 16..65552 octets
SECRET : 16..4096 octets
```
L'ordre exact est OWNER-CONTROL, METADATA, SECRET. Le `kind` encodé doit correspondre à la position attendue ; il n'autorise pas un réordonnancement.
Les payloads plaintext V2 conservent le modèle fonctionnel établi en V1 pour cette évolution : owner-control, metadata et secret Solana restent des compartiments distincts. Une future modification incompatible de leur sémantique exige une version de format explicite.
## 8. Signature d'état
La fin du document est :
| Champ | Taille | Valeur / règle |
|---|---:|---|
| `state_signature.algorithm` | 1 | `0x01` Ed25519 |
| `state_signature.signature` | 64 | signature detached exacte |
Aucun octet ne peut suivre ces 65 octets.
## 9. Identifiants numériques figés
### 9.1 Rôles
```text
0x01 OWNER
0x02 VIEW
```
### 9.2 Algorithmes
```text
KDF
0x01 Argon2id
AEAD
0x01 XChaCha20-Poly1305
state signature
0x01 Ed25519
```
### 9.3 Compartiments
```text
0x01 OWNER-CONTROL
0x02 METADATA
0x03 SECRET
```
`0x00` est invalide dans ces espaces. Les autres valeurs sont réservées et rejetées par V2 tant qu'elles ne sont pas explicitement normalisées par une évolution compatible documentée ; aucune valeur inconnue n'est devinée.
## 10. Canonicalité et rejet
Le parser V2 doit rejeter avant crypto :
```text
fichier > 1 MiB
magic incorrect
format_version != 2
document_length différent de la taille reçue
flag réservé non nul
champ tronqué
longueur qui dépasse le reste du document
role/algorithm/kind inconnu
ordre OWNER/VIEW invalide
VIEW descriptor et VIEW slot incohérents
Argon2 hors bornes
salt hors bornes
ciphertext hors bornes
payload_version non supporté
trailing bytes
```
Il n'existe qu'un encodage canonique d'un même état sémantique V2 : mêmes champs, même ordre, mêmes largeurs, même endianness et aucune donnée ignorée.
## 11. Transcripts et AAD V2
V2 possède ses propres domaines ; V1 et V2 ne partagent jamais un domain separator :
```text
KSPWALLET-V2-STATE
KSPWALLET-V2-AAD-OWNER-SLOT
KSPWALLET-V2-AAD-VIEW-SLOT
KSPWALLET-V2-AAD-OWNER-CONTROL
KSPWALLET-V2-AAD-METADATA
KSPWALLET-V2-AAD-SECRET
```
Le transcript/AAD V2 conserve la discipline TLV déterministe de V1 :
```text
domain || 0x00
puis pour chaque champ :
tag:u16 big-endian
length:u64 big-endian
value:length bytes
```
Les tags restent alignés avec les familles V1 :
```text
0x0001 magic
0x0002 format_version
0x0003 owner_auth_public_key
0x0010 view_enabled
0x0011 view_role
0x0012 view_slot_id
0x0100 slot_id
0x0101 slot_role
0x0102 kdf_algorithm
0x0103 kdf_version
0x0104 kdf_memory_kib
0x0105 kdf_iterations
0x0106 kdf_parallelism
0x0107 kdf_salt
0x0108 wrap_algorithm
0x0109 wrap_nonce
0x010A wrap_ciphertext
0x0200 compartment_kind
0x0201 compartment_version
0x0202 compartment_algorithm
0x0203 compartment_nonce
0x0204 compartment_ciphertext
0x0500 state_signature_algorithm
```
Différence normative V2 : les rôles/algorithmes/kinds sont transcriptés sous leur **ID numérique d'un octet**, et `format_version = 2` est transcripté en `u32` big-endian. Les domaines distincts empêchent qu'un transcript V1 et un transcript V2 soient interchangeables.
### 11.1 State transcript
Le state transcript contient :
```text
common
view descriptor
OWNER slot avec wrap nonce+ciphertext
OWNER-CONTROL avec nonce+ciphertext
METADATA avec nonce+ciphertext
SECRET avec nonce+ciphertext
state signature algorithm
```
Le VIEW wrap mutable n'est pas ajouté au state transcript, selon le modèle d'autorisation VIEW déjà retenu : son identité stable reste liée par le descriptor signé tandis que son credential peut être self-rotaté sans OWNER.
### 11.2 Slot AAD
Le slot AAD contient :
```text
common
slot_id
role
KDF algorithm/version/parameters/salt
wrap algorithm
```
Le wrap nonce/ciphertext n'est pas inclus dans son propre AAD.
### 11.3 Compartment AAD
Le compartment AAD contient :
```text
common
kind
payload_version
algorithm
```
Le nonce/ciphertext n'est pas inclus dans son propre AAD.
## 12. V1 et V2
V1 et V2 sont deux formats explicites :
| Propriété | V1 | V2 |
|---|---|---|
| enveloppe | JSON UTF-8 | binaire KSP |
| champs binaires | Base64url no-pad | bytes directs |
| version | `1` | `2` |
| canonicalité | sémantique JSON + Base64 canonique | byte-exact |
| taille fixture wire-only | 2037 octets | 628 octets |
| Argon2id | oui | oui |
| XChaCha20-Poly1305 | oui | oui |
| Ed25519 OWNER state | oui | oui |
| VIEW/OWNER | oui | oui |
La réduction mesurée sur le fixture structurel de référence est :
```text
2037 -> 628 octets
-1409 octets
~69,2 %
```
Cette mesure n'est pas une promesse de ratio constant : la taille dépend notamment des metadata chiffrées.
## 13. Fixture structurelle
Fixture canonique de `pre.015` :
```text
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v2_wire_only.bin
size = 628
sha256 = cbacb37326c76e6dde8d614601cd765a8c0fd9744f891d640b8a3cd71cf60a93
```
Préfixe hexadécimal :
```text
4b535057414c4c4554 0002 00000274 0001
000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
202122232425262728292a2b2c2d2e2f
```
Lecture :
```text
4b535057414c4c4554 = "KSPWALLET"
0002 = format_version V2
00000274 = 628 octets
0001 = VIEW_ENABLED
```
La fixture `wire_only` vérifie uniquement framing/canonicalité et reste volontairement distincte des wallets V2 runtime produits depuis `pre.016`; elle n'est pas présentée comme une signature cryptographique valide.
## 14. Compatibilité et migration
V2 n'autorise aucune migration implicite à l'ouverture.
Politique prévue :
```text
lecture générique V1/V2 oui
création générique V2
création V1 forcée oui
création V2 forcée oui
réécriture V1 -> V2 cachée non
migration V1 -> V2 opération explicite
```
`pre.017` matérialise cette politique avec trois surfaces :
```text
migrate_wallet_v1_to_v2(...)
migrate_wallet_file_v1_to_v2(...)
migrate_wallet_file_v1_to_v2_in_place(...)
```
La migration authentifie toujours le wallet source avec OWNER. Elle ne transcode pas les slots/ciphertexts V1 : elle ouvre le V1, conserve l'identité Solana et le payload metadata exact — y compris les identifiants de notes — puis construit un nouvel envelope V2 avec de nouveaux matériaux cryptographiques et les domains/transcripts V2. Le mot de passe OWNER fourni reste le credential OWNER cible.
Lorsque VIEW est activé en V1, le caller doit fournir un mot de passe VIEW cible pour V2, car le key-wrap V1 ne peut pas être réutilisé sous l'AAD V2. OWNER peut fournir l'ancien credential ou en choisir un nouveau, conformément à son autorité existante de rotation VIEW. Une migration pure conserve en revanche la forme de capability : elle ne peut ni activer VIEW sur une source désactivée, ni supprimer VIEW d'une source activée.
La migration fichier vers une autre destination est no-clobber et laisse V1 intact. La variante in-place compare l'état V1 authentifié attendu avant publication et utilise le remplacement atomique existant ; un changement concurrent retourne `wallet.state_conflict`. `wallet.migration_invalid` couvre une demande de migration incohérente.
Une simple transcodification non authentifiée des bytes V1 n'est pas suffisante puisque V2 possède ses propres domains/transcripts.
## 15. Future V3 / second facteur
V2 ne réalise aucun second facteur.
Si un futur V3 ajoute une autorisation dépendant d'un facteur externe :
```text
ksp-wallet-lib possède la règle qui exige le facteur
ksp-wallet-lib possède le challenge et la validation cryptographique
aucun WalletOwner ne peut être obtenu si le facteur requis n'est pas satisfait
Wallet Desk ne décide jamais que le second facteur est valide
```
Une UI comme Wallet Desk peut néanmoins devoir évoluer pour orchestrer l'expérience : attente de confirmation, saisie OTP, enrollment, recovery, hardware/WebAuthn ou consentement externe.
Le réseau ou le fournisseur externe ne doit pas forcer `ksp-wallet-lib` à dépendre directement de Config/Transport/Tauri. Une abstraction/provider KSP séparée peut fournir la preuve à Wallet tandis que Wallet reste propriétaire de la politique d'autorisation.
L'arrivée d'un V3 ne change pas automatiquement le default : V2 peut rester `DEFAULT_WALLET_FORMAT` aussi longtemps que KSP le décide explicitement.