← Retour au blog

Comment RedaktPDF chiffre vos PDF : plongée dans notre architecture E2EE

Jury D'Ambros··8 min de lecture

La plupart des éditeurs PDF en ligne traitent vos fichiers sur leurs serveurs. Ils reçoivent votre document en clair, le manipulent et vous le renvoient. À l'instant où votre fichier quitte votre navigateur, il est lisible par l'infrastructure de cette entreprise — ses ingénieurs, ses journaux et toute personne obtenant un accès non autorisé à leurs systèmes.

RedaktPDF fonctionne autrement. Chaque PDF que vous téléversez en tant qu'utilisateur enregistré est chiffré dans votre navigateur avant qu'un seul octet n'atteigne nos serveurs. Le serveur stocke un texte chiffré qu'il ne peut pas lire. Cet article explique précisément comment cela fonctionne — la hiérarchie des clés, les primitives cryptographiques et les décisions de conception derrière chaque choix.

Pour un aperçu non technique, voir Pourquoi le chiffrement de bout en bout est important pour l'édition PDF.

Le modèle zero-knowledge

Le terme «zero-knowledge» est surchargé en cryptographie, mais dans ce contexte il signifie quelque chose de précis : le serveur détient des données cryptographiquement inutiles sans informations que seul l'utilisateur possède.

Voici ce que le serveur de RedaktPDF connaît de vos documents chiffrés :

  • La KEK chiffrée — inutile sans votre clé maîtresse
  • Les clés de fichier chiffrées — inutiles sans la KEK
  • Les blobs chiffrés dans S3 — illisibles sans les clés de fichier
  • Le sel PBKDF2 — non secret ; requis pour la dérivation de clé
  • Votre adresse e-mail et les métadonnées de compte

Voici ce que le serveur ne voit jamais :

  • Votre mot de passe (seul un hash bcrypt est stocké pour l'authentification)
  • La clé maîtresse — dérivée dans votre navigateur, jamais transmise
  • La KEK en clair — déchiffrée uniquement dans votre navigateur
  • Les clés de fichier en clair — déchiffrées uniquement dans votre navigateur
  • Contenu des PDF, images de page et texte extrait — chiffrés avant le téléversement

Pourquoi cela compte-t-il en pratique ? Imaginez un pire scénario : un attaquant exfiltre l'intégralité de la base de données et chaque objet S3. Il dispose des clés chiffrées et des blobs chiffrés. Sans votre mot de passe, il ne peut pas dériver la clé maîtresse. Sans la clé maîtresse, la KEK chiffrée ne peut pas être déchiffrée. Sans la KEK, les clés par fichier restent scellées. La fuite ne donne rien d'exploitable.

Ce n'est pas un engagement contractuel — c'est une propriété cryptographique de l'architecture.

La hiérarchie des clés

RedaktPDF utilise une hiérarchie de clés à trois niveaux. Chaque niveau remplit un rôle distinct.

User Password
    |
    v PBKDF2(password, salt, 600,000 iterations, SHA-256)
Master Key (MK) ---- stored nowhere (derived on-demand)
    |
    v AES-GCM wrapKey
Key Encryption Key (KEK) ---- wrapped copy stored in DB
    |
    v AES-GCM wrapKey (per document)
File Key (FK) ---- wrapped copy stored in DB per document
    |
    v AES-256-GCM encrypt
Encrypted PDF, page images, text metadata ---- stored in S3

Clé maîtresse (MK). Dérivée de votre mot de passe via PBKDF2. Elle n'est stockée nulle part — ni sur le serveur, ni dans le navigateur au-delà de la session courante. Chaque fois que vous vous connectez à une session chiffrée, la MK est re-dérivée depuis votre mot de passe et n'existe qu'en mémoire. Si vous fermez l'onglet, elle disparaît.

Clé de chiffrement de clé (KEK). Une clé AES-GCM 256 bits générée aléatoirement. Elle est générée une seule fois quand vous activez le chiffrement, puis immédiatement wrappée (chiffrée) avec la MK. La forme chiffrée est stockée en base. La forme en clair n'existe en mémoire du navigateur que durant une session active.

La KEK existe pour une raison opérationnelle : les changements de mot de passe. Quand vous changez votre mot de passe, une nouvelle MK est dérivée. Sans couche KEK, la clé de chaque document devrait être re-chiffrée avec la nouvelle MK. Avec la couche KEK, seul le wrapper de la KEK doit être mis à jour — une seule écriture en base, quel que soit le nombre de documents. Toutes les clés de fichier existantes restent valides.

Clé de fichier (FK). Une clé AES-GCM 256 bits générée aléatoirement, unique par document. Elle chiffre les octets du PDF, les images de page, les miniatures et le texte extrait. La forme chiffrée est stockée en base à côté des métadonnées du document. Isoler les clés par document signifie qu'une compromission hypothétique d'une clé n'affecte qu'un seul document, pas l'ensemble de votre bibliothèque de fichiers.

Dérivation de clé : du mot de passe à la clé maîtresse

La dérivation de la clé maîtresse utilise l'implémentation PBKDF2 de la Web Crypto API. Voici le code de production tiré de packages/crypto/src/index.ts :

const PBKDF2_ITERATIONS = 600_000;

export async function deriveMasterKey(
  password: string,
  salt: Uint8Array,
): Promise<CryptoKey> {
  const keyMaterial = await globalThis.crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(password),
    'PBKDF2',
    false,
    ['deriveKey'],
  );

  return globalThis.crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: salt as unknown as ArrayBuffer,
      iterations: PBKDF2_ITERATIONS,
      hash: 'SHA-256',
    },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false, // not extractable
    ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'],
  );
}

Décortiquons le code :

  1. importKey('raw', ...) — importe le mot de passe encodé en UTF-8 comme matériau brut de clé. Ce n'est pas encore une clé utilisable ; ce ne sont que des octets fournis à la fonction de dérivation.

  2. deriveKey(...) — applique PBKDF2 avec SHA-256 et 600 000 itérations pour produire la clé maîtresse.

  3. { name: 'AES-GCM', length: 256 } — spécifie que la sortie est une clé AES-GCM 256 bits.

  4. false (extractable) — c'est critique. La clé maîtresse est non extractible. Même du code JavaScript s'exécutant dans le même contexte navigateur ne peut pas lire les octets bruts de la clé. Elle vit dans le key store interne de la Web Crypto API et ne peut être utilisée que pour des opérations cryptographiques, jamais exportée. Cela empêche un script malveillant de lire la clé en mémoire.

  5. ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'] — les usages de la clé. La clé maîtresse peut wrapper et unwrapper la KEK, et chiffrer/déchiffrer directement si nécessaire.

Pourquoi 600 000 itérations ? PBKDF2 est intentionnellement lent — chaque itération supplémentaire rend les attaques par force brute proportionnellement plus coûteuses. La recommandation de l'OWASP en 2023 est de 600 000 itérations pour PBKDF2-SHA256. Cela rend chaque essai de mot de passe coûteux en temps CPU mesurable. À 600 000 itérations, un attaquant tentant une attaque par dictionnaire contre un sel volé doit payer ce coût CPU pour chaque mot de passe candidat. Même avec un cluster GPU, casser un mot de passe fort est irréalisable en pratique.

Le sel aléatoire de 16 octets garantit que des mots de passe identiques produisent des clés maîtresses différentes, déjouant les attaques par rainbow table.

Chiffrer un PDF

Une fois qu'une clé de fichier est disponible, tout le chiffrement de données utilise la même fonction. Voici la fonction de production encryptBlob du package crypto :

export async function encryptBlob(
  data: Uint8Array,
  key: CryptoKey,
): Promise<Uint8Array> {
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
  const ciphertext = await globalThis.crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    data as unknown as ArrayBuffer,
  );
  return concat(iv, new Uint8Array(ciphertext));
}

Trois points à noter :

IV aléatoire par chiffrement. IV_BYTES vaut 12 (octets), longueur d'IV recommandée par le NIST pour AES-GCM. Un nouvel IV de 12 octets est généré à chaque appel d'encryptBlob. Cela signifie que chiffrer deux fois le même PDF produit deux textes chiffrés différents. Si des IV étaient réutilisés avec la même clé, la sécurité d'AES-GCM s'effondrerait — deux textes chiffrés sous la même clé et le même IV fuitent le XOR des textes en clair et permettent à un attaquant de forger des tags d'authentification. Des IV aléatoires éliminent ce risque.

Chiffrement authentifié. AES-GCM est un chiffre AEAD (Authenticated Encryption with Associated Data). Il fournit à la fois la confidentialité (le contenu ne peut être lu) et l'intégrité (le contenu ne peut être modifié silencieusement). Le tag d'authentification GCM ajouté au texte chiffré détecte toute altération. Si un blob chiffré stocké est modifié dans S3, le déchiffrement lèvera une erreur plutôt que de renvoyer un texte en clair corrompu.

Format auto-descripteur. L'IV est préfixé au texte chiffré (concat(iv, new Uint8Array(ciphertext))). La sortie est un seul tableau d'octets : [12 bytes IV][remaining bytes: ciphertext + 16-byte auth tag]. Le déchiffrement lit les 12 premiers octets comme IV, le reste comme texte chiffré authentifié. Aucun stockage séparé d'IV n'est nécessaire.

Cette même fonction chiffre les octets du PDF, chaque image de page rendue, les miniatures et le JSON du texte extrait — tout ce qui pourrait exposer le contenu du document.

Quand vous utilisez l'éditeur PDF sécurisé pour appliquer des modifications, ces opérations sont assemblées et exportées entièrement dans votre navigateur. Le document édité n'est jamais re-téléversé sous forme déchiffrée.

Le flux de téléversement

Voici la séquence pour téléverser un PDF chiffré :

  1. Votre navigateur lit le fichier PDF comme un Uint8Array.
  2. PDF.js rend chaque page localement à 150 DPI (pleine résolution) et 72 DPI (miniature). Aucun rendu côté serveur.
  3. PDF.js extrait localement les positions du texte pour la recherche et la sélection.
  4. Le navigateur génère une nouvelle clé de fichier (FK) aléatoire.
  5. La FK chiffre : les octets du PDF, chaque PNG de page rendu, chaque PNG de miniature et le JSON des positions de texte — le tout via encryptBlob.
  6. La FK est wrappée avec la KEK de l'utilisateur via AES-GCM wrapKey.
  7. Le navigateur téléverse le PDF chiffré directement vers S3 via une URL pré-signée — le serveur émet l'URL mais ne reçoit jamais le contenu du fichier.
  8. Le navigateur envoie les blobs de pages chiffrés et la FK wrappée à /api/documents/confirm-encrypted.
  9. Le serveur stocke les blobs chiffrés dans S3 et la FK wrappée en base. À aucun moment il ne voit le PDF en clair, les images de page ou la clé de fichier.

Même les opérations de caviardage suivent ce modèle : les caviardages sont appliqués côté client à la représentation PDF en mémoire navigateur avant que le document ne soit chiffré et téléversé.

Pourquoi AES-GCM pour le wrapping de clés

Une note sur l'implémentation du wrapping de clés qui diffère du tableau de documentation dans docs/SECURITY.md.

Le tableau de SECURITY.md indique «AES-KW (Key Wrap)» pour la primitive de wrapping — référence à l'AES Key Wrap RFC 3394. L'implémentation réelle utilise wrapKey avec AES-GCM. Voici la fonction wrapKek :

export async function wrapKek(
  kek: CryptoKey,
  masterKey: CryptoKey,
): Promise<string> {
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
  const wrapped = await globalThis.crypto.subtle.wrapKey(
    'raw',
    kek,
    masterKey,
    {
      name: 'AES-GCM',
      iv,
    },
  );
  return toBase64(concat(iv, new Uint8Array(wrapped)));
}

La distinction compte : AES-KW (RFC 3394) n'utilise pas d'IV. Il fournit l'intégrité via une valeur de contrôle intégrée, mais son mécanisme d'authentification est plus étroit que GCM. AES-GCM avec un IV aléatoire fournit un AEAD complet — confidentialité, intégrité et authentification avec un tag d'authentification de 128 bits. Toute modification d'une clé wrappée est détectable.

Utiliser AES-GCM sur toute la pile (à la fois pour le chiffrement des données et le wrapping de clés) a un autre bénéfice pratique : la cohérence. L'implémentation utilise une seule primitive aux propriétés bien comprises et au très bon support navigateur. Pas de charge cognitive à raisonner sur deux modes cryptographiques différents.

Le tableau de SECURITY.md est un trou documentaire ; l'implémentation réelle est plus robuste. Ce post documente ce que le code fait réellement.

Ce que cela signifie en pratique

Lions le tout par un scénario concret. Supposons que la base de RedaktPDF et tous les objets S3 soient exfiltrés demain. L'attaquant obtient :

  • Votre adresse e-mail
  • Un hash bcrypt de votre mot de passe (cost factor 12 — pas utilisable directement)
  • Le sel PBKDF2
  • La KEK wrappée
  • Les FK wrappées pour chacun de vos documents
  • Les blobs chiffrés de chaque document

Pour déchiffrer un seul document, l'attaquant doit :

  1. Casser votre mot de passe à partir du hash bcrypt (ou le deviner à partir du sel PBKDF2, ce qui n'accélère pas significativement la recherche du mot de passe)
  2. Utiliser le mot de passe obtenu + sel pour exécuter PBKDF2 à 600 000 itérations afin de dériver la clé maîtresse
  3. Utiliser la clé maîtresse pour unwrapper la KEK
  4. Utiliser la KEK pour unwrapper la FK du document cible
  5. Utiliser la FK pour déchiffrer les blobs chiffrés

Chaque étape nécessite l'achèvement de la précédente. Le coût PBKDF2 est conçu pour rendre l'étape 2 coûteuse en temps de calcul par essai, rendant les attaques par force brute contre des mots de passe forts impraticables.

Une brèche complète expose les métadonnées mais pas le contenu. C'est la garantie pratique de l'architecture zero-knowledge. Pour un panorama plus large des pratiques de confidentialité dans l'édition PDF, voir notre guide de la confidentialité PDF.

Pour aller plus loin

Les spécifications Web Crypto API, PBKDF2 et AES-GCM derrière cette implémentation :

Prêt à essayer RedaktPDF ?

Modifiez, caviardez et annotez vos PDF directement dans votre navigateur — gratuit et chiffré.

Commencer

Outils associés

Articles associés