Coffre-fort Obleevo
Aussi offert en English · Español · Deutsch · Português · Română · 日本語 · Italiano
Le format de fichier .obleevo, version 1
Comment un fichier de coffre-fort Obleevo est construit, octet par octet, afin que n’importe qui puisse vérifier ce qu’Obleevo fait de sa phrase de passe, ou écrire son propre programme pour ouvrir un coffre-fort sans Obleevo.
1. Vue d’ensemble
Un coffre-fort Obleevo (en anglais : Vault) est un fichier unique, habituellement nommé quelque-chose.obleevo, qui contient des fichiers, des notes et des mots de passe chiffrés dans le navigateur sous une phrase de passe. La personne garde le fichier; aucun serveur ne le conserve. Ce document décrit exactement la version 1, afin que n’importe qui puisse écrire un lecteur indépendant ou vérifier le nôtre. L’implémentation de référence est public/js/vault-core.js; ses tests sont test/vault.test.mjs. En cas d’écart entre cette traduction et la version anglaise, c’est la version anglaise, tirée directement de docs/vault-format.md, qui fait foi.
Tous les entiers sont non signés et petit-boutistes (little-endian). Les décalages sont exprimés en octets à partir du début du fichier. || désigne la concaténation. SHA-256, HKDF-SHA256 (RFC 5869) et AES-256-GCM (NIST SP 800-38D, nonce de 96 bits, étiquette de 128 bits) sont les algorithmes standard. UTF-8(s) est l’encodage UTF-8 d’une chaîne.
décalage taille champ
0 8 magic "OBLEEVO\0" = 4F 42 4C 45 45 56 4F 00
8 2 version du format 1
10 1 identifiant de KDF 1 = Argon2id, version 0x13 (RFC 9106)
11 1 drapeaux 0 (aucun drapeau défini en version 1)
12 4 mémoire Argon2 m, Kio
16 4 itérations Argon2 t
20 4 parallélisme Argon2 p
24 16 salt aléatoire, nouveau à chaque changement de phrase de passe
40 16 vault id aléatoire, nouveau à la création et à chaque renouvellement des clés
---------- fin de l’EN-TÊTE (56 octets)
56 12 wrap nonce aléatoire
68 48 wrapped data key AES-256-GCM(KEK, DEK de 32 octets), 32 + 16 d’étiquette
---------- fin de l’EMPLACEMENT DE CLÉ (décalage 116)
116 16 checksum 16 premiers octets de SHA-256(octets 0..115)
132 12 manifest nonce aléatoire, nouveau à chaque enregistrement
144 4 L longueur du chiffré du manifeste, étiquette comprise
---------- fin du PRÉFIXE (148 octets)
148 L chiffré du manifeste AES-256-GCM
148+L ... corps des fichiers les blocs de chaque fichier, dans l’ordre du manifeste
Le fichier se termine exactement là où se termine le dernier bloc du dernier fichier. Un fichier plus long ou plus court est refusé.
2. Clés
2.1 Normalisation de la phrase de passe
La phrase de passe est une chaîne Unicode. Avant usage, elle est normalisée :
- Appliquer la normalisation Unicode NFC.
- Soit T la chaîne NFC débarrassée de tout caractère WS de début et de fin, WS désignant exactement ces quatre points de code et aucun autre : U+0009 (tabulation), U+000A (saut de ligne), U+000D (retour chariot), U+0020 (espace). (Ni U+00A0, ni U+3000, ni U+FEFF, ni aucune autre espace.)
- Si T est formée de six groupes d’exactement quatre caractères ASCII pris parmi
0-9,A-Z,a-z, les groupes étant séparés par un ou plusieurs caractères de {-(U+002D)} ∪ WS, et de rien d’autre - en expression régulière,^[0-9A-Za-z]{4}(?:[-\t\n\r ]+[0-9A-Za-z]{4}){5}$, sans option d’insensibilité à la casse ni option Unicode -, c’est la forme d’une phrase de passe générée. Utiliser alors T avec les lettres ASCIIa-zmises en majusculesA-Z, chaque suite de caractères séparateurs remplacée par un seul-, chaqueIetLremplacé par1, et chaqueOremplacé par0. - Sinon, utiliser telle quelle la chaîne NFC de l’étape 1, blancs compris.
Le résultat, en UTF-8, est le mot de passe Argon2id P. Une phrase de passe vide n’est jamais valide.
2.2 Clé de chiffrement de clé (KEK)
KEK = Argon2id(P, salt, m = memory KiB, t = iterations, p = parallelism,
version 0x13, tag length 32, no secret K, no associated data X)
La KEK sert de clé AES-256-GCM.
Un lecteur DOIT refuser, avant d’exécuter Argon2id, tout fichier dont les paramètres sortent de ces limites (erreur params) :
| paramètre | minimum | maximum |
|---|---|---|
| mémoire m | 19456 Kio (19 Mio) | 262144 Kio (256 Mio) |
| itérations t | 2 | 10 |
| parallélisme p | 1 | 4, et m >= 8p |
Le minimum empêche un fichier rétrogradé de dériver une clé bon marché; le maximum empêche un fichier forgé de faire dépenser à un appareil plus de quelques secondes et 256 Mio avant même de pouvoir dire « mauvaise phrase de passe ». Les nouveaux coffres sont écrits avec m = 65536 (64 Mio), t = 3, p = 1. Les paramètres résident dans l’en-tête afin qu’un enregistrement ultérieur puisse les relever; comme l’en-tête est authentifié (2.3, 3), les modifier dans le fichier rend celui-ci impossible à ouvrir.
L’implémentation de référence, en outre : signale à l’appelant, avant la dérivation, qu’un fichier demande plus que les valeurs par défaut sur l’une ou l’autre dimension (inspectVault), afin que la personne puisse être avertie; exécute Argon2id dans un worker qu’on peut annuler; et n’exécute jamais un coût supérieur aux valeurs par défaut sur le fil principal d’une page.
2.3 Clé de données (DEK)
La DEK est faite de 32 octets aléatoires, générés à la création du coffre-fort, puis de nouveau à chaque renouvellement des clés (7.1). Un changement rapide de phrase de passe la conserve.
wrapped data key = AES-256-GCM-Encrypt(key = KEK, nonce = wrap nonce,
plaintext = DEK, AAD = HEADER)
HEADER désigne les octets 0..55. Une mauvaise phrase de passe, comme toute modification de l’en-tête, fait échouer ce déchiffrement.
2.4 Sous-clés
manifest key = HKDF-SHA256(IKM = DEK, salt = vault id (16 bytes),
info = UTF-8("obleevo vault manifest v1"), L = 32)
file key = HKDF-SHA256(IKM = DEK, salt = kid (16 bytes),
info = UTF-8("obleevo vault file v1"), L = 32)
Chaque kid est fait de 16 octets aléatoires choisis au moment où le corps du fichier correspondant est chiffré (voir 5). Il est inscrit dans le manifeste sous la forme de 32 caractères hexadécimaux minuscules.
3. Somme de contrôle
Les octets 116..131 sont les 16 premiers octets du SHA-256 des octets 0..115.
Ce n’est pas un mécanisme de sécurité - n’importe qui peut la recalculer. Elle existe pour qu’un lecteur puisse distinguer un fichier endommagé (erreur tampered) d’une mauvaise phrase de passe (erreur passphrase) : sans elle, un sel ou une clé enveloppée modifiés ressembleraient exactement à une mauvaise phrase de passe. La sécurité vient des étiquettes AES-GCM.
4. Manifeste
manifest plaintext = AES-256-GCM-Decrypt(key = manifest key, nonce = manifest
nonce, ciphertext = bytes 148 .. 148+L-1, AAD = bytes 0..147)
L’AAD est le préfixe entier : en-tête, emplacement de clé, somme de contrôle, nonce du manifeste et L. Toute modification de l’un d’eux fait échouer le déchiffrement du manifeste.
L doit valoir au moins 4096 + 16 et au plus 64 Mio + 16, et 148 + L ne doit pas dépasser la taille du fichier.
Le clair est bourré :
plaintext = len (4 bytes, LE) || JSON (len bytes, UTF-8) || zero bytes
Sa longueur totale est un palier : le plus petit de 4096, 8192, 16384, ... jusqu’à 1 Mio qui contient 4 + len, ou, au-delà de 1 Mio, le multiple entier suivant de 1 Mio. Les octets de bourrage doivent être nuls. Le bourrage masque le nombre et la longueur exacts des notes et des mots de passe.
Le JSON est un objet comportant exactement deux membres :
{ "v": 1, "items": [ ... ] }
Chaque élément est un objet comportant exactement ces membres, et aucun autre :
| kind | membres |
|---|---|
file | id, kind, name, type, size, chunks, kid, created, modified |
note | id, kind, name, text, created, modified |
password | id, kind, name, username, password, url, notes, created, modified |
totp | id, kind, name, issuer, account, secret, algorithm, digits, period, created, modified |
id: 32 caractères hexadécimaux minuscules (16 octets aléatoires), unique dans le coffre-fort.name: le nom du fichier, ou le titre de la note ou du mot de passe; chaîne d’au plus 4096 unités de code UTF-16.type: le type de média du fichier, chaîne d’au plus 255 unités de code (peut être vide).size: la longueur du fichier en octets, entier non négatif inférieur à 2^53.chunks:max(1, ceil(size / 1048576)), et inférieur à 2^32.kid: 32 caractères hexadécimaux minuscules, unique parmi les fichiers du coffre-fort.text(note),notes(mot de passe) : chaînes d’au plus 8 Mi unités de code.username,password,url: chaînes d’au plus 65536 unités de code.issuer,account(totp) : chaînes d’au plus 4096 unités de code.secret(totp) : le secret partagé de double authentification (2FA) en base32 RFC 4648, sous son unique graphie canonique : uniquementA-Zmajuscules et2-7, sans espaces ni bourrage=; sa longueur modulo 8 ne vaut ni 1, ni 3, ni 6, et il se décode (en ignorant les bits qui suivent le dernier octet entier) en 10 à 128 octets.algorithm(totp) : exactement"SHA1","SHA256"ou"SHA512".digits(totp) : le nombre 6 ou 8.period(totp) : le nombre 30 ou 60.created,modified: millisecondes depuis le 1970-01-01 UTC, finies, >= 0.
Un lecteur DOIT rejeter un manifeste qui enfreint l’une de ces règles (erreur format), même s’il s’est déchiffré. Le contenu des notes et des mots de passe ne réside que dans le manifeste, tout comme un secret 2FA : il n’est jamais écrit ailleurs dans le fichier.
Un élément totp est un générateur de mots de passe à usage unique fondés sur le temps (RFC 6238) : le code à l’instant Unix T secondes est le HOTP (RFC 4226, avec HMAC-algorithm et digits chiffres) du compteur floor(T / period) exprimé sur 8 octets gros-boutistes, sous le secret décodé.
Compatibilité. Le type totp a été ajouté après la sortie des premiers lecteurs (Obleevo 1.24 et son outil d’ouverture hors ligne). Ces lecteurs refusent un manifeste qui en contient un, avec l’erreur format, puisque leur table de types est fermée. La version du format reste 1 : tout coffre-fort sans élément totp est lu par tous les lecteurs, et un rédacteur qui en ajoute un ne peut pas changer la version sans rechiffrer chaque fichier (la version fait partie de VH, 5).
5. Corps des fichiers
Le clair de chaque fichier est découpé en blocs de 1 Mio (1048576 octets); le dernier bloc contient le reste. Un fichier de 0 octet a exactement un bloc, de 0 octet. Le bloc i (en comptant à partir de 0) d’un fichier de n blocs est :
nonce = LE64(i) || 00 00 00 00 (12 bytes)
VH = SHA-256(bytes 0..9 || vault id) (32 bytes)
AAD = VH || kid (16 raw bytes) || LE64(i) || last || LE64(n) (65 bytes)
where last = 01 if i = n - 1, else 00
chunk = AES-256-GCM-Encrypt(key = file key(kid), nonce, plaintext chunk, AAD)
Chaque bloc a la longueur de son clair plus 16 octets. Les fichiers se suivent dans l’ordre du manifeste, sans rien entre eux; le corps du k-ième fichier commence donc à 148 + L + somme, sur les fichiers précédents, de (size + 16 * chunks).
Ce que l’AAD apporte : un bloc déplacé à une autre position (index), retiré ou répété (index et nombre), tronqué à une frontière de bloc (nombre et drapeau de fin), déplacé vers un autre fichier (kid, qui change aussi la clé) ou vers un autre coffre-fort (vault id, et une autre DEK) ne s’authentifie plus.
VH ne couvre que les parties de l’en-tête qu’un changement rapide de phrase de passe laisse intactes - magic, version et vault id - et non le sel ni les paramètres Argon2, de sorte qu’un changement rapide n’oblige pas à rechiffrer chaque fichier. Ces champs restent authentifiés par l’enveloppe de clé et par le manifeste. Un renouvellement des clés (7.1) change le vault id, et donc VH, en même temps que toutes les clés.
Le kid d’un fichier est choisi à nouveau chaque fois que son clair est chiffré, et n’est jamais réutilisé pour un autre chiffrement : une clé de fichier chiffre exactement une suite de blocs, si bien qu’aucune paire clé et nonce ne peut se présenter deux fois. Un rédacteur qui recopie tels quels les blocs d’un fichier depuis un enregistrement antérieur conserve son kid.
6. Lecture, dans l’ordre
- Moins de 10 octets, ou octets 0..7 différents du magic :
format. - Version supérieure à 1 :
version. Version 0 :format. - Moins de 148 octets :
tampered. - Somme de contrôle discordante :
tampered. - Identifiant de KDF autre que 1, ou drapeaux non nuls :
version. - Paramètres hors des limites de 2.2 :
params. (Argon2id n’a pas été exécuté.) Lhors limites, ou au-delà de la fin du fichier :tampered.- Dériver la KEK; désenvelopper la DEK. Échec :
passphrase. - Déchiffrer le manifeste. Échec :
tampered. Contenu invalide :format. - La taille du fichier n’est pas exactement 148 + L + tous les corps :
tampered. - Un bloc qui ne se déchiffre pas, au moment où il est lu :
tampered.
Un lecteur ne rend aucun clair issu d’un bloc qui ne s’est pas authentifié. Lorsqu’il lit un fichier en continu, il ne libère le dernier bloc qu’après avoir vérifié que les n blocs ont été lus et que le total vaut exactement size.
7. Écriture
- Générer la DEK, le sel, le vault id ainsi que chaque nonce et chaque kid avec un générateur aléatoire cryptographiquement sûr (
crypto.getRandomValues). - Chaque enregistrement écrit un nouveau nonce de manifeste.
- Un changement rapide de phrase de passe écrit un nouveau sel, un nouveau nonce d’enveloppe et (facultativement) de nouveaux paramètres Argon2, et réenveloppe la même DEK.
- Un renouvellement des clés (7.1) écrit tout cela avec une nouvelle DEK et un nouveau vault id, et chaque corps de fichier rechiffré.
- Recopier les blocs d’un fichier depuis un enregistrement antérieur sans les rechiffrer est permis et attendu; l’implémentation de référence vérifie d’abord l’étiquette de chaque bloc recopié, de sorte qu’une source endommagée ne puisse pas produire un nouveau fichier impossible à ouvrir.
7.1 Renouvellement des clés
Un renouvellement des clés fait d’une copie ce qui est, sur le plan cryptographique, un nouveau coffre-fort contenant les mêmes éléments : un nouveau sel, une nouvelle DEK aléatoire, un nouveau vault id aléatoire, une nouvelle clé de manifeste (tirée de la nouvelle DEK et du nouvel id), et chaque fichier rechiffré sous un nouveau kid aléatoire avec la nouvelle DEK, lié au nouveau VH. Rien du chiffré de l’ancienne copie ni aucune ancienne clé n’est réutilisé. Un rédacteur lit chaque bloc source, l’authentifie sous les anciennes clés exactement comme en 6, et seulement ensuite chiffre son clair dans la nouvelle copie; un bloc qui échoue arrête toute l’écriture, qui doit alors laisser l’ancien fichier en place.
L’implémentation de référence procède en deux étapes, chacune tout ou rien. changePassphrase(p) (dont le renouvellement des clés est le comportement par défaut) dérive les nouvelles clés et fait passer le coffre-fort ouvert à celles-ci en une seule étape synchrone, tout à la fin; une annulation ou un échec avant ce moment laisse le coffre-fort exactement tel qu’il était. Les corps de fichiers restent dans la copie d’où ils viennent, toujours lisibles là sous les anciennes clés, jusqu’au prochain enregistrement, qui les rechiffre un à un, bloc par bloc, à mesure qu’il écrit la nouvelle copie. Un enregistrement qui échoue ou qui est annulé laisse l’ancien fichier intact (le flux d’écriture est abandonné) et peut être relancé. Une fois le coffre rattaché à la nouvelle copie, les enregistrements recopient de nouveau les blocs.
Ce que protège chaque mode. Supposons qu’un attaquant détienne une ancienne copie et son ancienne phrase de passe - donc, en l’ouvrant, l’ancienne DEK - et obtienne plus tard une copie plus récente.
changement rapide (rekey: false) | renouvellement des clés (par défaut) | |
|---|---|---|
| l’ancienne phrase de passe ouvre la copie plus récente | non | non |
| l’ancienne DEK lit l’index de la copie plus récente (notes, mots de passe, secrets 2FA, noms de fichiers) | oui | non |
| l’ancienne DEK lit les corps de fichiers de la copie plus récente | oui | non |
| la copie plus récente peut être reliée à l’ancienne par son vault id | oui (même id) | non (nouvel id; la taille totale reste un indice) |
| les anciennes copies elles-mêmes | s’ouvrent toujours avec l’ancienne phrase de passe | s’ouvrent toujours avec l’ancienne phrase de passe |
| coût | Argon2id seulement | Argon2id, puis le prochain enregistrement lit et rechiffre chaque fichier |
Aucun des deux modes ne change quoi que ce soit aux copies qui existent déjà : une ancienne copie reste exactement aussi lisible qu’avant, par quiconque la détient avec sa phrase de passe. Le renouvellement des clés ne protège que ce qui est écrit après lui.
8. Ce que ce format ne protège pas
- Le retour en arrière. Une copie plus ancienne du même coffre-fort s’ouvre avec la même phrase de passe; rien dans le fichier n’indique qu’il n’est pas le plus récent. Gardez le fichier dans un endroit dont l’historique vous inspire confiance.
- Une ancienne phrase de passe divulguée, après un changement rapide. Un changement rapide réenveloppe la clé de données, mais la conserve : quiconque a ouvert une ancienne copie avec l’ancienne phrase de passe détient la DEK, et peut lire l’index et les corps de fichiers des copies ultérieures s’il les obtient. Un renouvellement des clés (7.1, le comportement par défaut de l’implémentation de référence) ferme cette porte pour chaque copie écrite après lui, et ne change rien aux copies écrites avant.
- Les tailles. La longueur totale du fichier révèle à peu près la taille totale des fichiers qu’il contient (chaque fichier ajoute 16 octets par Mio). Le nombre et la taille des notes et des mots de passe sont masqués jusqu’au palier du manifeste.
- L’appareil. Le chiffrement ne peut pas protéger un coffre-fort ouvert dans un navigateur compromis ou sur une machine compromise.
- Les phrases de passe faibles. Argon2id rend chaque essai coûteux, non impossible. L’implémentation de référence refuse de créer un coffre-fort, ou d’en changer la phrase de passe, avec une phrase de passe que son estimateur ne classe pas à 70 bits et 12 caractères ou plus (erreur
weak). L’estimateur est une heuristique volontairement pessimiste, non une mesure, et le format de fichier lui-même ne peut rien imposer : un autre rédacteur pourrait utiliser n’importe quelle phrase de passe.
9. Vecteur de test
test/vault.test.mjs vérifie Argon2id au moyen du vecteur de la section 5.3 du RFC 9106, et chacune des règles ci-dessus au moyen d’attaques sur de vrais fichiers. Un coffre-fort d’exemple peut être produit ainsi :
import { createVault, generatePassphrase } from './public/js/vault-core.js'
const pass = generatePassphrase() // weak passphrases are refused ('weak')
const v = await createVault(pass)
v.addNote('hello', 'world')
const blob = await v.save()
Dernière révision:
À propos · Faire affaire avec nous · Tutoriels · Pourquoi · FAQ · Conditions · Confidentialité · Accessibilité · Témoins · Sécurité · Modèle de menace · Audits de sécurité · Nous joindre · Nouveautés
Obleevo est développé par The House of Brendrof. Logiciel propriétaire, d’utilisation gratuite.