ADR-0003

Modèle Zero-Knowledge et stockage

source : docs/adr/0003-zero-knowledge-model.md · versionné · MADR-lite

ADR-0003 — Modèle Zero-Knowledge et stockage

  • Statut : Accepted
  • Date : 2026-04-24
  • Décideurs : Core team OmbrysWeb
  • Issue GitHub : #3
  • Références cahier des charges : § 5.3, § 4.2, § 6.4, § 7
  • Dépend de : ADR-0001, ADR-0002

Contexte

Le projet promet aux membres du collectif qu'aucune donnée utilisateur sensible n'est accessible en clair par le serveur — même en cas de compromission complète de l'infrastructure ou de saisie judiciaire. Cette promesse a des implications opérationnelles fortes :

  • impossibilité de faire de la recherche plein-texte côté serveur sur le contenu privé,
  • impossibilité d'afficher un preview serveur des fichiers chiffrés,
  • impossibilité de « réinitialiser le mot de passe » (il n'y en a pas, § 6.1 ; et même la clé de chiffrement n'est pas récupérable côté serveur),
  • charge cryptographique reportée sur le client.

En contrepartie elle supprime la grande majorité des scénarios de fuite de données (insider, fournisseur d'infra compromis, saisie, configuration erronée). Elle est cohérente avec le positionnement du collectif et l'engagement du warrant canary (§ 7).

Le principe ZK ne s'applique qu'aux données privées. Le contenu public (articles du blog, transparency report, page d'accueil) reste en clair côté serveur — sinon la recherche sémantique (pgvector) n'est pas réalisable.

Définitions

  • Donnée privée : contenu appartenant à un membre (notes, fichiers, messages, journal d'audit personnel, opérations), ou communication chiffrée entre membres.
  • Donnée publique : article de blog publié, transparency report, page statique, signature publique d'un build.
  • Métadonnée neutre : identifiants opaques, timestamps, tailles de blob, compteurs — nécessaires au fonctionnement mais conçus pour ne rien révéler de sensible.
  • Métadonnée sensible : graphe de contacts, fréquence des messages, sujets discutés — à minimiser autant que possible.

Décisions

1. Compartimentage des données

| Type de donnée | Stockage | Chiffrement | Clé | |---|---|---|---| | Articles blog publiés | PostgreSQL (blog-svc) | Disque (LUKS) uniquement | N/A | | Embeddings pgvector | PostgreSQL | Disque (LUKS) uniquement | N/A | | Comptes utilisateurs (credential ID WebAuthn, public key, fingerprint) | PostgreSQL (auth-svc) | Disque (LUKS) uniquement | N/A | | Métadonnées blobs (blob ID, owner ID opaque, taille, timestamp) | PostgreSQL (storage-svc) | Disque (LUKS) uniquement | N/A | | Contenu notes, fichiers, opérations | S3-compatible (Garage ou MinIO), blobs opaques | AES-256-GCM client-side | Dérivée Passkey PRF (ADR-0002), jamais serveur | | Messages E2EE | messaging-svc (relais) | Double ratchet (ADR-0002), ciphertext opaque | Dérivée Passkey + ratchet, jamais serveur | | Secrets serveur (clés API, certs CA en ligne) | HashiCorp Vault (transit + kv-v2) | Scellement Vault | Gérée par Vault (auto-unseal HSM/KMS) | | Logs applicatifs | Loki, chiffrés at-rest | LUKS + scrubbing PII | N/A | | Logs d'audit signés | audit-svc, append-only + hash-chain | Disque chiffré + signature Ed25519 | SPIFFE SVID du service signataire | | Backups off-site | Object storage distant | age + Kyber hybrid (ADR-0002) | Clé de backup rotée 180 jours |

2. Règle fondamentale

> Aucune clé de déchiffrement de donnée privée ne transite ni ne séjourne en clair sur un composant serveur.

En conséquence :

  • Les services backend ne reçoivent que des blobs opaques (ciphertext + nonce + AAD) pour la donnée privée.
  • Aucune API côté serveur ne prend en paramètre un mot de passe, une passphrase ou une clé symétrique.
  • Aucun log ne contient de clé, de contenu déchiffré, d'IV seul ou de payload riche. Vérifié par :
  • filtres de scrubbing tracing côté Rust,
  • revue automatisée trufflehog + gitleaks (§ 5.5),
  • audit externe (M6).

3. Chiffrement par objet — format d'enveloppe

Chaque blob chiffré (note, fichier, item d'opération) est encapsulé dans une enveloppe versionnée :

envelope v1 :=
  magic      : "OMBRYS\x01"           (7 octets, constant)
  version    : u8                      (= 1)
  algo_id    : u8                      (1 = AES-256-GCM, 2 = ChaCha20-Poly1305)
  bucket_id  : u8                      (1=notes, 2=files, 3=ops, 4=audit)
  key_epoch  : u32                     (époque de K_bucket, pour rotation)
  nonce      : 12 octets               (dérivé HKDF sur objectId + counter, unique)
  aad_len    : u16
  aad        : aad_len octets          (metadata publique bindée : blobId, ownerOpaqueId, objectType)
  ciphertext : N octets
  tag        : 16 octets               (authentification AEAD)
  • Le serveur persiste l'enveloppe entière comme un tableau d'octets sans interpréter le contenu (sauf pour vérifier le magic et la taille).
  • aad inclut des identifiants que le serveur voit déjà (blob ID, owner opaque) : il ne peut pas substituer un blob sans invalider l'AEAD côté client.
  • key_epoch permet une rotation de clé par bucket : lors d'une rotation, le client re-chiffre les blobs au rythme de l'accès (lazy re-encrypt).

4. Minimisation des métadonnées sensibles

  • Identifiants utilisateur opaques : le owner_id stocké est un ULID aléatoire non corrélable à l'identité publique. La correspondance pseudo → owner_id réside dans auth-svc uniquement.
  • Pas de graphe de contacts côté serveur : la messagerie utilise des inbox IDs éphémères (type Signal), sans liste d'amis centralisée.
  • Timestamps arrondis à la minute sur les métadonnées exposées côté relais (pas à la milliseconde).
  • Taille des blobs : padding à un palier (ex : ceil(size / 4 KiB) * 4 KiB) pour réduire l'inférence de contenu par taille.
  • IPs hachées avec sel tournant (§ 6.4), jamais en clair.

5. Recherche côté client

Conséquence directe du ZK : la recherche sur données privées est exécutée côté client :

  • Le client télécharge les enveloppes, les déchiffre localement, et construit un index en mémoire (ou persisté chiffré).
  • Pour les volumes importants (> 500 items), un index inversé chiffré type blind index / SSE peut être envisagé en v2 — ADR dédiée nécessaire, risque de fuites d'information sur patterns d'accès à évaluer.
  • Pas de recherche sémantique sur le contenu privé (pgvector réservé au contenu public).

6. Droit à l'oubli cryptographique

Conformément au RGPD :

  • La suppression d'un compte utilisateur déclenche :
  1. destruction de l'entrée WebAuthn (credential ID + public key) dans auth-svc,
  2. destruction des métadonnées blob associées (storage-svc),
  3. purge des messages relayés (messaging-svc).
  • Les blobs chiffrés orphelins peuvent subsister le temps d'un cycle de GC (≤ 30 jours) ; étant indéchiffrables sans la clé détruite avec le compte, ils sont équivalents à des données inexistantes.
  • Garantie juridique : la destruction de la clé (détenue seule par l'authenticator) rend le blob cryptographiquement inaccessible → conforme à l'exigence RGPD de suppression « dans la mesure du techniquement possible ».

7. Recovery et perte d'authenticator

  • Pas de backdoor serveur. La perte d'un Passkey sans backup = perte d'accès aux données.
  • Deux mécanismes de backup côté utilisateur :
  1. Multi-authenticator : chaque membre enregistre au moins deux Passkeys (ex : YubiKey primaire + secondaire, ou YubiKey + biométrie device).
  2. Recovery multi-signature M-of-N entre membres (issue #34) : la clé maîtresse peut être reconstituée à partir de N parts Shamir détenues par M autres membres de confiance. Aucune part ne permet à elle seule de déchiffrer.
  • La cérémonie de recovery est documentée et laisse une trace dans audit-svc (signatures des membres ayant coopéré).

8. Exceptions documentées

Cas où le serveur voit temporairement du clair — uniquement avec justification et ADR de révision :

  • blog-svc côté auteur : un brouillon en cours de rédaction peut être chiffré serveur-side (clé dérivée pour le bucket blog-draft) plutôt que client-side, pour supporter l'édition collaborative en temps réel. Décision finale : non pour v1 (trop tôt, augmente la surface). Les brouillons sont chiffrés client-side comme les notes.
  • Antivirus / scan malware sur fichiers uploadés : non. Incompatible avec ZK. Alternative : avertissement utilisateur + content security policy empêchant l'exécution, et restriction de certains types (exécutables) côté client.

Conséquences

Positives

  • Supprime la majorité des scénarios de fuite côté serveur.
  • Rend la saisie / mandat sans effet sur les données privées (cohérent warrant canary, § 7).
  • Clarté du modèle : le serveur n'a pas à être trusted pour la confidentialité.
  • Compatibilité RGPD par construction (droit à l'oubli cryptographique).

Négatives

  • Expérience utilisateur dégradée sur la recherche (pas de recherche serveur sur contenu privé).
  • Perte de Passkey = perte d'accès (atténuée par multi-auth et recovery M-of-N, mais l'ergonomie doit être excellente en M2, issue #34).
  • Charge crypto et bande passante côté client plus importante.
  • Impossible de déléguer à des services tiers (pas de CDN de fichiers privés, pas de scan antivirus).

Neutres

  • Auditabilité : le modèle ZK est vérifiable de l'extérieur (trafic réseau ne contient que des blobs opaques), ce qui renforce la crédibilité des claims.

Suivi

  • Spec crypto (issue #5) codifie formats d'enveloppe, test vectors, identifiants de version.
  • Threat model (issue #4) explicite les menaces résiduelles (metadata leakage, timing attacks, client compromise).
  • Audit externe (M6) : validation du modèle ZK par un tiers.
  • ADR de révision éventuelle : si une fonctionnalité v2 (ex : recherche sémantique privée via SSE) nécessite un relâchement contrôlé, elle passe par une nouvelle ADR avec analyse de risque explicite.

Changelog

  • 2026-04-24 : rédaction initiale, statut Accepted conditionné à validation audit externe (M6).