480 lines
17 KiB
Markdown
480 lines
17 KiB
Markdown
<!-- file: docs/formats/KSPWALLET_V2.md -->
|
|
<!-- version: 4 -->
|
|
|
|
# `.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` et publié stable avec KSP `0.2.6`.
|
|
|
|
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.
|