Obleevo

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 :

  1. Appliquer la normalisation Unicode NFC.
  2. 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.)
  3. 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 ASCII a-z mises en majuscules A-Z, chaque suite de caractères séparateurs remplacée par un seul -, chaque I et L remplacé par 1, et chaque O remplacé par 0.
  4. 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ètreminimummaximum
mémoire m19456 Kio (19 Mio)262144 Kio (256 Mio)
itérations t210
parallélisme p14, 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 :

kindmembres
fileid, kind, name, type, size, chunks, kid, created, modified
noteid, kind, name, text, created, modified
passwordid, kind, name, username, password, url, notes, created, modified
totpid, kind, name, issuer, account, secret, algorithm, digits, period, created, modified

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

  1. Moins de 10 octets, ou octets 0..7 différents du magic : format.
  2. Version supérieure à 1 : version. Version 0 : format.
  3. Moins de 148 octets : tampered.
  4. Somme de contrôle discordante : tampered.
  5. Identifiant de KDF autre que 1, ou drapeaux non nuls : version.
  6. Paramètres hors des limites de 2.2 : params. (Argon2id n’a pas été exécuté.)
  7. L hors limites, ou au-delà de la fin du fichier : tampered.
  8. Dériver la KEK; désenvelopper la DEK. Échec : passphrase.
  9. Déchiffrer le manifeste. Échec : tampered. Contenu invalide : format.
  10. La taille du fichier n’est pas exactement 148 + L + tous les corps : tampered.
  11. 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

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écentenonnon
l’ancienne DEK lit l’index de la copie plus récente (notes, mots de passe, secrets 2FA, noms de fichiers)ouinon
l’ancienne DEK lit les corps de fichiers de la copie plus récenteouinon
la copie plus récente peut être reliée à l’ancienne par son vault idoui (même id)non (nouvel id; la taille totale reste un indice)
les anciennes copies elles-mêmess’ouvrent toujours avec l’ancienne phrase de passes’ouvrent toujours avec l’ancienne phrase de passe
coûtArgon2id seulementArgon2id, 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

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.

Retour à Obleevo