Cryptographic format v1
The full specification of what your browser runs, and the test vectors to check it. The specification is written in French.
Download the test vectors (JSON)
Ce document décrit exactement ce qu'implémente packages/crypto. Les vecteurs de test
déterministes sont publiés dans packages/crypto/test-vectors.json
et vérifiés à chaque exécution des tests.
Primitives
| Usage | Primitive | Implémentation |
| --- | --- | --- |
| Chiffrement authentifié | AES-256-GCM, IV de 96 bits, étiquette de 128 bits | WebCrypto |
| Dérivation | HKDF-SHA256 (RFC 5869) | WebCrypto |
| Vérificateur de PIN | HMAC-SHA256 | WebCrypto |
| Empreintes | SHA-256 | WebCrypto |
| Dérivation du PIN | Argon2id, m = 65 536 Kio, t = 3, p = 1, sortie 32 octets | hash-wasm (WebAssembly, MIT), chargé seulement si un PIN est utilisé |
| Aléa | crypto.getRandomValues (par blocs de 64 Kio) | WebCrypto |
Aucune primitive n'est écrite à la main.
Encodages
- Binaire → texte : base64url sans remplissage. Le décodage est strict : remplissage, caractères étrangers et encodages non canoniques sont refusés.
- Chaînes
infoHKDF et données associées (AAD) : UTF-8, préfixeeclair v1. - HKDF utilise un sel vide (équivalent, par la RFC 5869, à 32 octets nuls).
Création d'un secret
id: 16 octets aléatoires, encodés en base64url (22 caractères).K: 32 octets aléatoires, la clé du lien.- Sans PIN :
Kc = HKDF(K, "eclair v1 content", 32). - Avec PIN (ou phrase de passe) :
- le PIN est normalisé en NFKC ;
pinSalt = HKDF(K, "eclair v1 pin-salt", 16)(dérivé, jamais transmis) ;a = Argon2id(PIN, pinSalt);Kc = HKDF(K ‖ a, "eclair v1 content", 32);Kauth = HKDF(K, "eclair v1 pin-auth", 32);verifier = HMAC-SHA256(Kauth, a), envoyé au serveur, qui ne stocke queSHA-256(verifier).
- Contenu clair : JSON canonique (clés d'objets triées récursivement) d'un des trois types :
Après déchiffrement, le JSON est validé avant tout affichage.{ "v": 1, "type": "text", "text": "…" } { "v": 1, "type": "structured", "template": "ftp", "fields": [{ "label": "…", "value": "…", "secret": true }] } { "v": 1, "type": "files", "text": "…", "files": [{ "name": "…", "mime": "…", "size": 0, "objectKey": "…", "chunkSize": 65536, "noncePrefix": "…" }] } - Bourrage :
u32 big-endian (longueur) ‖ JSON ‖ octets nuls, jusqu'au palier supérieur : 1 Kio, 4 Kio, 16 Kio, 64 Kio, puis multiples de 64 Kio. Le chiffré ne révèle que le palier. - Chiffrement :
AES-256-GCM(Kc, iv aléatoire de 12 octets, AAD = "eclair v1 secret " + id). L'AAD lie le chiffré à son identifiant : un chiffré déplacé sous un autreidne se déchiffre pas. - Libellé optionnel :
Klabel = HKDF(K, "eclair v1 label", 32), bourré comme le contenu,AES-256-GCM(Klabel, iv distinct, AAD = "eclair v1 label " + id). - Jeton de gestion : 32 octets aléatoires ; le client envoie
SHA-256(jeton), le serveur ne connaît jamais le jeton lui-même.
Liens
https://eclair.kayzen-lyon.com/s/{id}#v1.{n|p}.{K}
https://eclair.kayzen-lyon.com/gerer#v1.{id}.{jeton}
n: sans PIN,p: avec PIN. Rien d'autre dans le fragment.- Le fragment (
#…) n'est jamais envoyé par le navigateur au serveur. Aucune query string n'est utilisée. - Tout fragment qui n'est pas exactement de cette forme est refusé.
Ce que reçoit le serveur
| Champ | Contenu |
| --- | --- |
| id | Identifiant aléatoire |
| ciphertext, iv | Contenu bourré et chiffré |
| labelCiphertext, labelIv | Libellé chiffré (optionnel) |
| hasPin | Booléen |
| pinVerifier | HMAC(Kauth, Argon2id(PIN)), stocké haché |
| manageTokenHash | SHA-256(jeton de gestion) |
Le serveur ne reçoit jamais K, Kc, le PIN, le jeton de gestion ni le contenu clair.
Propriétés et limites
- Sans le lien complet, personne ne déchiffre, y compris avec un accès total au serveur et à la base.
- Le serveur ne peut pas tester de PIN :
verifierdépend deK, qu'il n'a pas. - Limite assumée : un attaquant qui détient le lien et une copie de la base peut tester les PIN hors ligne. Argon2id ralentit chaque essai (64 Mio de mémoire), mais un PIN de 4 à 6 chiffres reste cassable. Une phrase de passe longue protège bien mieux ; l'interface le dit.
- La taille réelle du contenu est masquée par paliers ; le palier, lui, reste visible.
- L'effacement des tampons en mémoire (
wipe) est un effort raisonnable : JavaScript ne garantit pas l'absence de copies.
Tests
- Vecteurs officiels : RFC 5869 (HKDF), RFC 4231 (HMAC), FIPS 180-2 (SHA-256), spécification GCM (cas 13 et 14).
- Argon2id : déterminisme, dépendance au sel, normalisation NFKC, paramètres v1. Les vecteurs de la
RFC 9106 utilisent un secret et des données associées que
hash-wasmn'expose pas : la conformité d'Argon2id repose sur les tests dehash-wasmet sur nos vecteurs v1. - Vecteurs v1 : 3 cas (texte sans PIN, texte avec PIN et libellé, contenu structuré avec phrase de passe).
- Couverture de
packages/crypto: 100 % des lignes, branches et fonctions (seuil bloquant : 95 %).
Régénérer les vecteurs (uniquement lors d'un changement de format, qui imposerait un format v2) :
UPDATE_VECTORS=1 pnpm --filter @sesame-eclair/crypto test
Fichiers (STREAM)
- Clé par fichier :
Kf_i = HKDF(Kc, "eclair v1 file " + i), oùKcest la clé de contenu du secret (elle dépend donc aussi du PIN éventuel). - Blocs de 64 Kio chiffrés en AES-256-GCM, sans données associées, avec le nonce
préfixe aléatoire (7 octets) ‖ compteur (4 octets, gros-boutiste) ‖ drapeau dernier bloc (1 octet). - Un fichier vide produit un bloc unique (marqué dernier). Toute troncature, extension, permutation ou modification d'un bloc fait échouer le déchiffrement.
- Stockage : 64 blocs par objet (4 Mio de clair). Clés d'objet
{upload}/{fichier}/{partie}, aléatoires. - Nom, type MIME, taille réelle et préfixe de nonce sont dans le contenu chiffré du secret. Le stockage voit la taille des objets chiffrés (proche de la taille réelle) : limite documentée.
- Les URL présignées durent 5 minutes. Après la dernière lecture, les objets restent 15 minutes pour le téléchargement, puis sont supprimés ; ils le sont immédiatement en cas de destruction, d'expiration ou de blocage après trop d'essais de code.
Demandes de secret (X25519)
- Le demandeur génère une paire X25519
(sk, pk)et un jeton de gestion. Le serveur ne reçoit quepketSHA-256(jeton). - Lien de dépôt :
/r/{id}#v1.{pk}[.{libellé en base64url}]. Le libellé ne voyage que dans le fragment. - Lien privé du demandeur :
/demandes#v1.{id}.{jeton}.{sk}. Il contient la clé de déchiffrement. - Dépôt : paire éphémère
(esk, epk),Kd = HKDF(ikm = X25519(esk, pk), sel = epk ‖ pk, "eclair v1 request"),AES-256-GCM(Kd, iv aléatoire, AAD = "eclair v1 request " + id), contenu bourré comme un secret. - Les points de faible ordre sont refusés (secret partagé nul).