Formato criptográfico v1
A especificação completa do que o seu navegador executa e os vetores de teste para a verificar. A especificação está redigida em francês.
Transferir os vetores de teste (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).