Files
khadhroony-solana-project/docs/formats/KSPWALLET_V1.md
2026-08-19 10:52:21 +02:00

17 KiB

.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 :

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

Les prereleases suivantes complètent les paramètres de création Argon2id benchmarkés, les opérations cryptographiques, les payloads plaintext exacts et les vecteurs cryptographiques complets. 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 :

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 :

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 :

alphabet : A-Z a-z 0-9 - _
padding = interdit
trailing bits non canoniques = interdits

Le parsing normatif effectue conceptuellement :

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 :

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 :

{
  "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 chiffrées dans cet exemple sont des valeurs de fixture structurelle, pas les defaults de création V1. Les defaults ne deviennent normatifs qu'après benchmark de pre.004.

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 :

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 :

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 :

algorithm = "argon2id"
version   = 19

Bornes structurelles de parsing :

memory_kib  : 1 .. 1 048 576
iterations  : 1 .. 64
parallelism : 1 .. 64
salt        : 16 .. 64 octets

Ces plafonds sont des bornes de format/rejet hostile ; ils ne définissent pas les paramètres de création par défaut. Ceux-ci sont benchmarkés séparément.

Le password KDF futur 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é à la création.

8.2 Wrapping

Identifiant V1 :

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 contenu plaintext exact des wrapped capabilities est figé avec la couche crypto/payload suivante ; la grammaire envelope/key-slot et son AAD sont déjà figés ici.

9. Compartiments chiffrés

Trois compartiments indépendants existent :

owner_control
metadata
secret

Leurs versions initiales sont indépendantes :

control_version  = 1
metadata_version = 1
secret_version   = 1

Les trois utilisent :

algorithm = "xchacha20-poly1305"
nonce     = 24 octets

Bornes ciphertext V1 :

owner_control : 16 .. 4096 octets
metadata      : 16 .. 65 552 octets
secret        : 16 .. 4096 octets

La metadata plaintext V1 reste bornée à 65 536 octets. Les payloads plaintext exacts sont consolidés dans la tranche dédiée, mais les champs wire ci-dessus ne changent pas.

Le compartiment metadata contient à terme au minimum :

Pubkey Solana Base58 canonique
alias optionnel <= 256 octets UTF-8
maximum 64 notes
texte note <= 8192 octets UTF-8
id de note = 16 octets aléatoires

Le compartiment secret contient la keypair Solana exacte nécessaire à la signature OWNER.

10. Signature d'état OWNER

V1 :

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.

11. Codec binaire transcript/AAD

11.1 Préfixe de domaine

Chaque transcript/AAD commence par :

ASCII(domain_separator) || 0x00

11.2 Champ TLV

Chaque champ suivant utilise exactement :

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 :

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 :

KSPWALLET-V1-STATE

Ordre exact :

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 :

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 :

KSPWALLET-V1-AAD-OWNER-SLOT

Ordre exact :

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 :

KSPWALLET-V1-AAD-VIEW-SLOT

La suite TLV est identique à OWNER sauf :

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 :

0001 magic
0002 format_version
0003 owner_auth_public_key

Puis :

0200 compartment kind
0201 compartment payload version
0202 "xchacha20-poly1305"

Domain separators :

owner_control : KSPWALLET-V1-AAD-OWNER-CONTROL
metadata      : KSPWALLET-V1-AAD-METADATA
secret        : KSPWALLET-V1-AAD-SECRET

Valeurs de compartment kind :

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 :

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 :

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 :

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 :

crates/ksp-wallet-lib/tests/fixtures/kspwallet_v1_wire_only.json

Cette fixture est wire-only et test-only :

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 :

JSON V1
Base64url
longueurs
slots/descripteur
transcript OWNER exact
AAD exacts
round-trip du codec

Les vecteurs cryptographiques publics complets avec passwords et secret test-only connus sont ajoutés après implémentation KDF/AEAD/wrapping/signature.

19. Invariants encore à compléter sans modifier le wire figé

Les tranches suivantes doivent compléter :

pre.004 : defaults Argon2 benchmarkés + implémentation KDF/AEAD/wrapping + vecteurs crypto
pre.005 : payloads owner-control/metadata/secret + create/open VIEW/OWNER + state signature effective
pre.006+ : persistence/administration/signature/import-export selon le plan Wallet

Toute découverte imposant de modifier la grammaire, 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.