Comment un serveur peut-il stocker et synchroniser nos secrets sans posséder les clés pour les lire ? Nous allons suivre la façon dont Confidex crée ses clés, les protège dans un verrou, puis les utilise pour chiffrer les données et vérifier les modifications.
Verrou
Seul l'Élu peut ouvrir la porte
Matrix Reloaded
Imaginons que nous construisions un coffre-fort virtuel, un vault, pour conserver des données secrètes. Il doit garder ces données sur un ordinateur portable après la fermeture de l’application et permettre de les ouvrir sur un autre appareil. Le coffre-fort aura besoin de ses propres clés. Nous devons les stocker de sorte que copier les fichiers ne permette à un tiers ni de lire leur contenu ni de le modifier.
Un verrou, un lock, protège les clés : c’est un petit fichier chiffré. Pour l’ouvrir, il faut un mot de passe maître et un code secret, secret_key ; aucun des deux ne figure dans le fichier. Nous pourrions imprimer le verrou entier dans le journal du matin. Les lecteurs en auraient une copie, mais cette publication ne révélerait pas les clés privées.
Supposons qu’un tiers obtienne ce fichier et apprenne même le mot de passe maître. Il peut désormais répéter tous les calculs qui ne nécessitent que le mot de passe, mais ils ne produiront pas la clé d’ouverture du verrou : secret_key intervient aussi dans le calcul. Le fichier ne contient aucune vérification séparée du seul mot de passe. L’ouverture exige toujours le code secret inconnu.
Ce code provient de données cryptographiquement aléatoires ; personne ne l’invente. Tant que secret_key est inconnu, même un mot de passe maître correctement deviné n’ouvre pas le verrou : il manque encore un secret aléatoire à l’attaquant. Un mot de passe faible ne suffit donc pas à rendre une copie du serveur vulnérable à une attaque par dictionnaire réalisable en pratique.
Si l’attaquant obtient aussi secret_key, il peut tester ses hypothèses sur le mot de passe maître hors ligne. C’est coûteux, mais la difficulté dépend alors de la robustesse du mot de passe.
Clés
Messieurs, à quoi servent les clés ? Clés… débloquer des choses ?
Pirates des Caraïbes – Le Coffre du Mort
L’application obtient 32 octets aléatoires du générateur cryptographique du système d’exploitation et les prépare pour servir de clé. Ces octets préparés constituent la clé maîtresse privée, master_key.
L’application dérive toujours les mêmes clés de master_key : répéter les mêmes calculs sur les mêmes octets donne le même résultat. Un ordinateur portable et un téléphone peuvent donc retrouver des clés identiques. Pour obtenir master_key sur un nouvel appareil, l’application doit d’abord ouvrir le verrou avec le mot de passe maître et secret_key.
La première paire : le chiffrement
Je suis le Keymaster.
Chasseurs de fantômes
Pour stocker des données chiffrées, l’application calcule une clé publique à partir de master_key : secret_key. Elle permet de chiffrer des données pour le coffre-fort, tandis que le déchiffrement exige master_key.
« Publique » décrit le rôle de cette clé dans l’algorithme, pas une obligation de la distribuer à tout le monde. Dans Confidex, elle reste chez l’utilisateur. C’est aussi le deuxième secret nécessaire pour ouvrir le verrou : elle reste donc en local et n’est pas envoyée sur le réseau. À elle seule, secret_key permet de chiffrer des données pour le coffre-fort, mais pas de les déchiffrer ni de signer des modifications en son nom.
X25519 est l’algorithme qui prend master_key et calcule secret_key. Retrouver master_key à partir de secret_key est pratiquement irréalisable. Une attaque classique sur un supercalculateur imaginaire effectuant un milliard de milliards d’opérations sur la courbe par seconde prendrait en moyenne environ 2 300 milliards d’années (estimation de SafeCurves).
La deuxième paire : les signatures
La seule chose originale est la signature
Attrape-moi si tu peux
La première paire permet de masquer le contenu. Il nous faut maintenant un moyen de confirmer que le propriétaire du coffre-fort a autorisé une modification. Le serveur doit lui aussi pouvoir vérifier les signatures : nous devons donc lui envoyer une clé publique de vérification. La partie publique de la première paire est secret_key, que le serveur ne doit pas recevoir. Nous créons donc une deuxième paire de clés permanente pour les signatures.
Plutôt que de stocker un autre secret indépendant, nous dérivons une clé privée de signature de master_key et l’appelons signing_key. L’application l’utilise pour signer les données et le verrou. Les autres appareils et le serveur utilisent sa partie publique pour vérifier les signatures sans lire le contenu.
Nous pouvons retrouver cette paire à partir du même master_key. L’application en dérive une valeur de départ : 32 octets qui déterminent entièrement la clé de signature.
Pour dériver des clés différentes selon les usages, nous incluons une étiquette fixe dans le calcul : ici, confidex/vault/ed25519/v1. Le même master_key et la même étiquette produisent toujours la même clé.
Le mot de passe maître n’intervient pas encore dans ces calculs. Les deux paires dépendent des octets aléatoires de master_key, pas d’une chaîne inventée par une personne. Cette distinction sera utile plus tard : le mot de passe protégera l’accès à des clés qui existent déjà. Nous pourrons donc le changer sans changer les clés elles-mêmes.
La dérivation utilise une fonction fondée sur le hachage : HKDF, HMAC-based Extract-and-Expand Key Derivation Function. Confidex l’utilise avec SHA-256, d’où le nom HKDF-SHA256. Les 32 octets obtenus forment la graine de la paire Ed25519. La partie publique de signing_key ne révèle pas secret_key ; nous pouvons donc l’envoyer au serveur pour vérifier les signatures.
Qu’est-ce qu’un fichier verrou et à quoi sert-il ?
Ouvrez les portes de la baie de pods, HAL.
Une odyssée de l'espace
Je suis désolé, Dave. J'ai bien peur de ne pas pouvoir faire ça.
Le verrou stocke master_key sous forme chiffrée. Son ouverture exige le mot de passe maître et secret_key. L’en-tête public contient la version du format, l’identifiant du coffre-fort, la partie publique de signing_key, un sel et les paramètres de traitement du mot de passe. La version du format interne est chiffrée avec master_key.
Lorsqu’elle crée le verrou, l’application possède déjà master_key et calcule donc secret_key avant le chiffrement. À l’ouverture, l’utilisateur fournit secret_key séparément : il ne figure pas dans la copie publique du verrou.
Après le déchiffrement, l’application calcule la partie publique de master_key avec X25519 et exige qu’elle corresponde à secret_key. Elle retrouve ensuite signing_key et compare sa partie publique à la clé de l’en-tête. Ainsi, le verrou se vérifie lui-même.
Pour ralentir la recherche du mot de passe maître, chaque tentative doit coûter du temps et de la mémoire. Confidex utilise Argon2id avec 64 Mio de mémoire et trois passes. Le traitement du mot de passe utilise aussi un sel aléatoire. Ce sel n’est pas secret : il fait produire des résultats différents au même mot de passe dans des verrous différents.
Si secret_key est déjà connu, la longueur du mot de passe change radicalement l’ampleur d’une attaque. Il existe environ 12 millions de combinaisons de 5 lettres latines minuscules aléatoires, environ 8 milliards pour 7 lettres et environ 95 millions de milliards pour 12. Passer de 7 à 12 lettres multiplie l’espace de recherche par près de 12 millions. Si la recherche de 7 lettres prenait en moyenne une journée sur une machine donnée, 5 lettres prendraient environ deux minutes, et 12 environ 33 000 ans.
Pour obtenir une seule clé à partir du résultat d’Argon2id et de secret_key, nous utilisons la fonction HKDF-SHA256 présentée plus haut, avec l’étiquette confidex/lock/wrap/v1. Le résultat est wrapping_key, la clé qui chiffre le verrou.
Nous devons cacher master_key et le protéger contre les modifications. XChaCha20-Poly1305 le chiffre avec wrapping_key et ajoute une étiquette d’authentification. Celle-ci lie le texte chiffré à l’en-tête public : un mauvais mot de passe ou une modification des octets protégés fait échouer l’ouverture. L’étiquette et une valeur aléatoire à usage unique, le nonce, sont stockées avec le texte chiffré.
À chaque création ou nouveau chiffrement du verrou, l’application obtient un nouveau sel et un nouveau nonce. Le contenu chiffré comprend un octet de version et les 32 octets de master_key ; un nonce de 24 octets et une étiquette de 16 octets sont stockés à côté. La documentation de l’algorithme décrit cette construction.
Une divergence entre les clés retrouvées peut provenir d’un verrou volontairement mal formé ou d’un bogue du programme qui l’a créé. Une corruption aléatoire des octets est généralement détectée plus tôt, lors de la vérification de l’étiquette.
Le serveur ne possède pas les secrets nécessaires pour vérifier cette étiquette. Pour transférer le verrou, l’application ajoute donc une signature créée avec signing_key. Elle couvre l’ensemble du fichier terminé, y compris l’en-tête public et le contenu chiffré. Le serveur vérifie la signature puis, au premier envoi, épingle la partie publique complète de signing_key ainsi que l’identifiant interne du coffre-fort. Il vérifie les modifications suivantes avec cette clé. Le verrou et les données chiffrées sont signés avec le même signing_key, retrouvé à partir de master_key.
Le coffre-fort
Nous pouvons maintenant publier le verrou. Seule une personne qui connaît secret_key et le mot de passe maître master password peut lire master_key. En revanche, n’importe qui peut prendre la clé publique de signature dans le fichier verrou et vérifier les données signées sans voir le contenu du verrou ni celui des données.
Prenons un mot de passe, SuperPa55worD. Pour l’instant, nous voulons seulement l’enregistrer : après la fermeture de l’application, nous devons pouvoir envoyer le fichier verrou et le mot de passe chiffré n’importe où, sans que personne ne voie SuperPa55worD, tout en permettant de vérifier que nous en sommes l’auteur. Le chiffrement doit être réversible avec secret_key et le mot de passe maître master password. Un algorithme symétrique peut chiffrer et déchiffrer avec la même clé. Puisque nous ne stockons pas cette clé, nous devons pouvoir la retrouver à l’aide du verrou. Nous l’appellerons message_key.
Nous obtiendrons un message_key distinct pour chaque nouveau chiffrement. Il reste à organiser sa récupération : laisser la clé en clair à côté du texte chiffré permettrait à toute personne disposant d’une copie de tout lire.
À partir de nouveaux octets aléatoires, l’application crée une paire X25519 temporaire, uniquement pour ce chiffrement : record_key. Elle prend ensuite la partie privée de record_key et la clé publique permanente du coffre-fort, secret_key, pour calculer un secret partagé. Une fonction distincte dérive message_key de ce secret ; cette clé chiffre notre mot de passe.
À la lecture, l’application emprunte un autre chemin. Elle utilise master_key et la partie publique de record_key. Ces deux clés produisent le même secret partagé. Mathématiquement, les deux chemins multiplient le même point initial de la courbe par deux nombres privés, dans un ordre différent. Cet ordre ne change pas le résultat. Nous pouvons donc retrouver la clé de déchiffrement sans conserver la partie privée de record_key.
Obtenir ce secret à partir des deux clés publiques est pratiquement impossible : chacun des deux chemins exige la clé privée correspondante. Les points publics ne permettent pas de retrouver efficacement les nombres privés qui les ont produits. Cette astuce mathématique fonde la sécurité du coffre-fort. Mais elle n’empêche pas quelqu’un de nous remettre un autre coffre-fort auquel notre verrou aurait été fixé.
Nous pouvons maintenant stocker le texte chiffré avec la partie publique de record_key. La partie privée de record_key et le message_key utilisé ne sont plus nécessaires pour les lectures futures. L’application retrouvera ensuite la clé de déchiffrement à partir de master_key et de la partie publique conservée de record_key. Un seul secret permanent du coffre-fort permet ainsi de déchiffrer de nombreuses valeurs chiffrées séparément, chacune avec son propre message_key, retrouvé au moment de la lecture.
Cacher le texte ne suffit pas : nous devons aussi détecter sa modification à la lecture. Le chiffrement ajoute donc une étiquette d’authentification, une courte valeur vérifiée avec les données chiffrées. Si la vérification échoue, l’application rejette le résultat. Elle ne doit pas présenter un texte aléatoire ou modifié comme un mot de passe correctement déchiffré.
Nous avons décrit une construction dans laquelle une paire asymétrique aide à établir un secret partagé, tandis que message_key chiffre le contenu. Elle s’appelle Hybrid Public Key Encryption, ou HPKE, et est spécifiée dans la RFC 9180. Confidex utilise X25519 pour convenir du secret, HKDF-SHA256 pour dériver les clés et ChaCha20-Poly1305 pour le chiffrement avec contrôle d’intégrité. Ici, HKDF reçoit le secret partagé établi avec record_key et la paire de master_key ; c’est une tâche distincte de la dérivation de la graine Ed25519. La RFC 5869 décrit HKDF.
Pour le chiffrement suivant, l’application crée un nouveau record_key et un nouveau contexte HPKE. Réutiliser la même combinaison de message_key et de nonce compromet la protection du chiffrement. Dans Confidex, la clé et le nonce sont dérivés au sein de HPKE, utilisés pour un seul message, puis le contexte est abandonné.
ChaCha20 utilise lui-même un message_key de 256 bits. Pour mesurer l’ampleur d’une attaque directe, imaginons un supercalculateur fictif testant un milliard de milliards de candidats par seconde. La recherche parmi les 2²⁵⁶ clés possibles prendrait environ 1,8 × 10⁵¹ ans. La RFC 8439 décrit l’algorithme et son analyse.
Un enregistrement
Nous sommes heureux ?
Pulp-Fiction
Nous avons enregistré le mot de passe, mais la connexion à un site demande généralement aussi un nom d’utilisateur et une adresse. Il faut parfois y joindre une note. Si nous stockons ces valeurs séparément, l’application doit se souvenir qu’elles vont ensemble. Mettons ces « papiers » dans une enveloppe. Nous réunissons les champs dans un ensemble que nous appelons un enregistrement, un record.
Un enregistrement peut contenir n’importe quelle combinaison de champs. L’application les rassemble dans un paquet, le compresse et chiffre le tout selon la construction que nous venons de décrire. Les valeurs, noms, types et étiquettes des champs sont protégés. Modifier l’adresse du site, même en conservant le même mot de passe, crée une nouvelle version de l’enregistrement entier, chiffrée avec un nouveau record_key.
Chaîne de hachage
VOTRE PROJET DEVRAIT-IL UTILISER UNE BLOCKCHAIN ? → NON
XKCD2267
Une personne qui connaît secret_key peut préparer un enregistrement chiffré pour notre coffre-fort. Pour éviter de l’accepter comme le nôtre, l’application signe chaque version avec signing_key. Sans cette clé, un tiers ne peut pas créer une version dont la signature serait acceptée par le serveur et les autres appareils.
La version chiffrée doit être accompagnée de certaines informations : à quel coffre-fort et à quel enregistrement elle appartient, quand elle a changé et quelles versions la précèdent. Elles forment l’en-tête public. Il ne contient pas le mot de passe, mais exige lui aussi une protection : sinon, quelqu’un pourrait modifier l’appartenance et les relations des objets sans toucher au texte chiffré. L’application signe les octets exacts de l’en-tête avec le texte chiffré. Modifier les données signées invalide la signature.
Le serveur ressemble à un entrepôt de colis scellés. Il voit l’en-tête, le contenu chiffré et la signature, mais pas le mot de passe à l’intérieur, et sait que le destinataire existe dans la ville. Il vérifie la signature avec la partie publique épinglée de signing_key et décide s’il peut accepter la nouvelle version. Il n’a besoin ni du mot de passe maître, ni de secret_key, ni d’aucune clé privée. À la lecture, le client vérifie la même signature, puis contrôle à nouveau le lien entre le contenu et l’en-tête pendant le déchiffrement, à l’aide de l’étiquette d’authentification HPKE.
La signature protège aussi les liens entre versions. Un hachage SHA-256 des octets exacts de l’en-tête et du texte chiffré devient l’identifiant de la version. L’en-tête de la version suivante contient l’identifiant de la précédente. Lorsque nous changeons le mot de passe d’un site, l’application crée et signe un nouvel objet avec ce lien. L’ancien objet et sa signature restent inchangés. Remplacer le prédécesseur tout en conservant la signature de la nouvelle version échouera donc. Si deux appareils apportent des modifications indépendantes, les deux branches sont conservées ; une version fusionnée peut faire référence aux deux.
Vous vous souvenez de la blockchain ? La voilà :-).
L’en-tête public permet de vérifier les liens sans déchiffrer, mais certaines informations restent visibles. Le serveur connaît les identifiants, les liens entre versions, l’heure de modification fournie par le client, le nombre de champs et la taille encodée de chaque valeur. Le chiffrement masque le contenu de l’enregistrement, pas ces informations structurelles.
Synchronisation
« Pas une fuite de données, une sauvegarde distribuée gratuite. »
Mème Internet.
Pour travailler sur plusieurs appareils, nous devons transférer le verrou et l’historique des enregistrements entre eux. Le format des objets ne change pas : nous transférons les mêmes octets chiffrés et les mêmes signatures que ceux stockés en local.
Pour un transfert manuel, l’application crée une archive ZIP contenant le verrou, toutes les versions chiffrées des enregistrements stockées localement et une liste de fichiers signée. Ni le mot de passe ni secret_key n’y figurent. Sur un autre appareil, l’application ouvre d’abord le verrou, puis vérifie les signatures, déchiffre les enregistrements et restaure une copie locale du coffre-fort. L’importation est destinée à un appareil où ce coffre-fort n’existe pas encore ; elle ne fusionne pas l’historique avec une copie existante.
Un serveur de synchronisation échange les modifications entre les copies en usage. Au premier envoi, il reçoit le verrou signé, vérifie la signature et calcule l’adresse publique du coffre-fort à partir de la partie publique de signing_key. Il mémorise ensuite cette clé et l’identifiant du coffre-fort présent dans l’en-tête : le coffre-fort est ainsi enregistré sur le serveur. Les modifications suivantes doivent correspondre à cet identifiant et porter une signature de la même clé.
Un nouvel appareil a besoin de l’adresse du coffre-fort et de secret_key. L’application les réunit donc dans un code de transfert avec une somme de contrôle. L’utilisateur transfère ce code sur l’appareil et saisit séparément le mot de passe maître ; seule l’adresse publique est envoyée au serveur.
Après avoir ouvert le verrou téléchargé, l’application compare les parties publiques des clés retrouvées à secret_key et à l’en-tête, puis vérifie l’adresse calculée par rapport au code de transfert. Elle conserve ensuite la partie publique vérifiée de signing_key pour contrôler les enregistrements.
Pendant la synchronisation, l’application récupère les versions manquantes et envoie ses modifications. Le serveur vérifie les signatures et les liens avec les versions précédentes, stocke les objets acceptés et les transmet aux autres appareils. La vérification des données reçues, leur déchiffrement et la résolution des conflits s’effectuent sur les appareils de l’utilisateur.
L’enregistrement ne demande ni nom, ni adresse e-mail, ni mot de passe séparé. Le serveur voit l’adresse publique, la clé de vérification des signatures, les en-têtes publics, la taille des objets et les liens entre versions. Il voit aussi les adresses IP et l’heure des requêtes. Le mot de passe maître, secret_key, les clés privées et les contenus en clair restent sur les appareils de l’utilisateur. Toute personne qui connaît l’adresse publique peut télécharger les objets chiffrés, mais leur lecture exige les secrets de l’utilisateur.
Ces vérifications détectent les objets falsifiés, mais ne prouvent pas que le serveur a montré tout l’historique et son état le plus récent. Il peut masquer une branche que le client n’a jamais vue ou renvoyer un ensemble de données ancien mais cohérent. Une signature confirme l’authenticité d’un objet, pas l’exhaustivité de la réponse du serveur. L’utilisateur peut remarquer l’absence de modifications sur un autre appareil, mais un historique ancien cohérent passera la vérification de la chaîne de hachage.
Changer le mot de passe maître
Alors, Votre Sainteté voudrait-elle changer son mot de passe ?
Pirates informatiques (1995)
Changer le mot de passe maître ne chiffre à nouveau que le verrou ; master_key à l’intérieur reste identique. Les enregistrements, leurs signatures, secret_key et l’adresse du coffre-fort restent donc inchangés. Le nouveau verrou est signé avec le même signing_key.
Une ancienne copie du verrou s’ouvre toujours avec l’ancien mot de passe et le même secret_key ; changer le mot de passe ne révoque pas les clés déjà exposées.
La vue d’ensemble
Le verrou protège master_key, message_key chiffre les enregistrements et signing_key crée les signatures. Les archives ZIP et le serveur reçoivent des objets chiffrés accompagnés de signatures.