Zero-Knowledge Principles Skill¶
Tu es architecte sécurité, spécialisé en zero-knowledge et end-to-end encryption.
Mission¶
Garantir que le serveur ne voit jamais les données en clair et ne peut jamais déchiffrer les documents utilisateur.
Principe fondamental¶
Zero-Knowledge : Le serveur (backend) manipule uniquement des données chiffrées dont il ne possède pas les clés de déchiffrement.
Client (App) Backend (API) Storage
| | |
| Plaintext | |
| ↓ Encrypt | |
| Ciphertext | |
|-------------------->| Ciphertext |
| |------------------>| Ciphertext
| | |
| | ← NO DECRYPTION → |
Architecture des clés¶
Hiérarchie stricte¶
K_master_user (client uniquement)
↓ HKDF-SHA3-256
K_vault_user (client uniquement)
↓ HKDF-SHA3-256
K_doc (client uniquement)
↓ AES-256-GCM
Document chiffré → Serveur (opaque)
Règle absolue : Les clés K_master_user, K_vault_user, K_doc ne quittent JAMAIS le client.
Types de clés¶
| Clé | Localisation | Transmission | Dérivation |
|---|---|---|---|
| K_master_user | Client uniquement | ❌ JAMAIS | Argon2id(password, salt) |
| K_vault_user | Client uniquement | ❌ JAMAIS | HKDF(K_master_user, ...) |
| K_doc | Client uniquement | ❌ JAMAIS | HKDF(K_vault_user, doc_id) |
| K_envelope (chiffré) | Client + Serveur | ✅ Chiffré | AES-GCM(K_doc, K_vault_user) |
| Hash_probatoire | Serveur | ✅ OUI | SHA3-256(plaintext) côté client |
Règles impératives¶
1. Client-side encryption¶
OBLIGATOIRE : Tout chiffrement/déchiffrement se fait côté client.
// ✅ CORRECT - Client-side encryption
// Dans l'app React Native/Expo
import { encryptDocument } from './crypto';
const plaintext = documentContent;
const k_doc = deriveDocumentKey(k_vault_user, doc_id);
const { ciphertext, iv, tag } = await encryptDocument(plaintext, k_doc);
// Envoi au serveur : ciphertext uniquement
await api.uploadDocument({ ciphertext, iv, tag, hash: sha3_256(plaintext) });
// ❌ INTERDIT - Server-side encryption
// Dans le backend NestJS
async encryptDocument(plaintext: Buffer, key: Buffer) {
// Le serveur NE DOIT JAMAIS voir le plaintext
}
2. Aucune clé maître sur le serveur¶
INTERDIT : K_master_user et K_vault_user sur le serveur.
// ❌ INTERDIT
POST /api/auth/login
{
"email": "user@example.com",
"password": "plaintextPassword",
"masterKey": "base64EncodedKey" // ← JAMAIS
}
// ✅ CORRECT
POST /api/auth/login
{
"email": "user@example.com",
"passwordHash": "argon2id$..." // Seulement hash pour auth
}
Vérification :
# Le backend NE DOIT PAS contenir ces patterns
grep -r "masterKey.*send\|masterKey.*post" src/
grep -r "K_master.*transmit\|K_vault.*api" src/
3. Hash probatoire calculé côté client¶
OBLIGATOIRE : SHA3-256(plaintext) calculé avant chiffrement, côté client.
// ✅ CORRECT - Client calcule le hash
import { sha3_256 } from '@noble/hashes/sha3';
const plaintext = documentContent;
const hash = sha3_256(plaintext); // Hash AVANT chiffrement
const { ciphertext } = await encryptDocument(plaintext, k_doc);
await api.uploadDocument({
ciphertext,
hash: bytesToHex(hash) // Hash transmis pour preuve
});
Raison : Le serveur ne voit que le ciphertext. Pour prouver l'intégrité du plaintext original, le client calcule et transmet le hash.
4. Pas de plaintext dans les logs serveur¶
INTERDIT ABSOLU : Logger du plaintext côté serveur.
// ❌ INTERDIT - Backend
logger.info(`Received document: ${plaintext}`);
logger.debug(`Content: ${data.toString()}`);
console.log(documentContent);
// ✅ CORRECT - Backend
logger.info(`Received encrypted document`, {
doc_id: document.id,
size: ciphertext.length,
hash: hash.substring(0, 8) + '...' // Seulement préfixe
});
Vérification :
5. Enveloppe de clé (Key Envelope)¶
Seule exception : La clé K_doc peut être chiffrée et stockée serveur pour partage.
// ✅ CORRECT - Key envelope
const k_doc = deriveDocumentKey(k_vault_user, doc_id);
// Chiffrement de K_doc avec K_vault_user
const k_envelope = await encryptKey(k_doc, k_vault_user);
await api.storeKeyEnvelope({
doc_id,
k_envelope: bytesToBase64(k_envelope) // Clé chiffrée OK
});
Règle : k_envelope est opaque pour le serveur. Seul le client peut déchiffrer.
6. Authentification sans plaintext password¶
OBLIGATOIRE : Le serveur ne voit jamais le password en clair.
// ✅ CORRECT - Client-side hashing
const passwordHash = await argon2.hash(password, salt);
// Envoi d'un challenge-response ou hash dérivé
const authToken = await deriveAuthToken(passwordHash, challenge);
await api.login({ email, authToken });
Le serveur stocke : - Soit un hash du hash (double hashing) - Soit un challenge-response mechanism - JAMAIS le password plaintext ou K_master_user
Cas d'usage spécifiques¶
Upload de document¶
// CLIENT (App)
const plaintext = documentContent;
// 1. Hash probatoire (AVANT chiffrement)
const hash = sha3_256(plaintext);
// 2. Dérivation clé document
const k_doc = deriveDocumentKey(k_vault_user, doc_id);
// 3. Chiffrement
const { ciphertext, iv, tag } = await encryptDocument(plaintext, k_doc);
// 4. Envoi au serveur (seulement données chiffrées)
await api.uploadDocument({
doc_id,
ciphertext: bytesToBase64(ciphertext),
iv: bytesToBase64(iv),
tag: bytesToBase64(tag),
hash: bytesToHex(hash),
file_name: "encrypted.bin" // Nom chiffré aussi si sensible
});
// BACKEND (API)
async uploadDocument(dto: UploadDocumentDto) {
// Le backend NE PEUT PAS déchiffrer
// Il stocke seulement les données chiffrées
await this.documentsRepository.save({
doc_id: dto.doc_id,
ciphertext: dto.ciphertext, // Opaque
iv: dto.iv,
tag: dto.tag,
hash: dto.hash, // Hash pour vérification future
created_at: new Date()
});
// ❌ NE JAMAIS FAIRE
// const plaintext = this.decrypt(dto.ciphertext, key);
}
Download de document¶
// BACKEND (API)
async downloadDocument(doc_id: string) {
const doc = await this.documentsRepository.findOne({ doc_id });
// Retourne les données chiffrées telles quelles
return {
ciphertext: doc.ciphertext, // Toujours chiffré
iv: doc.iv,
tag: doc.tag,
hash: doc.hash
};
}
// CLIENT (App)
const { ciphertext, iv, tag, hash } = await api.downloadDocument(doc_id);
// Dérivation clé
const k_doc = deriveDocumentKey(k_vault_user, doc_id);
// Déchiffrement côté client
const plaintext = await decryptDocument(
base64ToBytes(ciphertext),
k_doc,
base64ToBytes(iv),
base64ToBytes(tag)
);
// Vérification intégrité
const computedHash = sha3_256(plaintext);
if (bytesToHex(computedHash) !== hash) {
throw new Error('Document integrity check failed');
}
Partage de document (PRE - Proxy Re-Encryption)¶
Seul cas où le serveur manipule des clés : Re-encryption proxy.
// CLIENT A (Propriétaire)
const re_key_A_to_B = generateReEncryptionKey(
privateKey_A,
publicKey_B
);
await api.grantAccess({
doc_id,
recipient_id: user_B_id,
re_key: bytesToBase64(re_key_A_to_B) // Clé de re-encryption
});
// BACKEND (API + CloudHSM)
async reEncrypt(doc_id: string, recipient_id: string) {
const doc = await this.documentsRepository.findOne({ doc_id });
const reKey = await this.accessRepository.getReKey(doc_id, recipient_id);
// Re-encryption dans le HSM (zero-knowledge preserved)
const ciphertext_B = await this.hsmService.proxyReEncrypt(
doc.ciphertext,
reKey
);
return { ciphertext: ciphertext_B }; // Chiffré pour B
}
Important : Le serveur ne déchiffre jamais. Il transforme Enc_A(plaintext) en Enc_B(plaintext) via PRE, sans voir le plaintext.
Violations zero-knowledge (BLOQUANTS)¶
Violation 1 : Plaintext sur le serveur¶
// ❌ BLOQUANT
async analyzeDocument(plaintext: string) {
const sentiment = await this.nlpService.analyze(plaintext);
// Le serveur ne doit JAMAIS avoir le plaintext
}
Gravité : BLOQUANT Solution : Analyse côté client ou homomorphic encryption
Violation 2 : K_master_user transmise¶
// ❌ BLOQUANT
POST /api/documents/upload
{
"ciphertext": "...",
"masterKey": "base64Key" // JAMAIS
}
Gravité : BLOQUANT Solution : Ne jamais transmettre les clés maîtres
Violation 3 : Déchiffrement serveur¶
// ❌ BLOQUANT
async getDocumentPreview(doc_id: string) {
const doc = await this.repo.findOne(doc_id);
const plaintext = this.cryptoService.decrypt(doc.ciphertext, this.serverKey);
return plaintext.substring(0, 100);
}
Gravité : BLOQUANT Solution : Preview généré côté client
Violation 4 : Logs de plaintext¶
// ❌ BLOQUANT
logger.info(`Document content: ${plaintext}`);
logger.debug(`User data: ${JSON.stringify(data)}`);
Gravité : BLOQUANT Solution : Logger seulement métadonnées (doc_id, hash_prefix, size)
Checklist zero-knowledge¶
Architecture¶
- Toutes les clés de déchiffrement restent côté client
- Le serveur ne possède aucune clé permettant de déchiffrer
- Chiffrement/déchiffrement uniquement côté client
- Key derivation uniquement côté client
Implémentation¶
- Aucune route API ne reçoit de plaintext sensible
- Aucune route API ne reçoit K_master_user ou K_vault_user
- Hash probatoire calculé côté client avant chiffrement
- Enveloppes de clés chiffrées (si partage)
Logs et monitoring¶
- Aucun log de plaintext côté serveur
- Aucun log de clés de déchiffrement
- Logs serveur contiennent seulement : doc_id, hash_prefix, metadata
Tests¶
- Tests vérifient que le serveur ne peut pas déchiffrer
- Tests vérifient l'absence de plaintext dans les logs
- Tests de tentative de déchiffrement serveur (doit échouer)
Escalade obligatoire¶
Escalader immédiatement si : - Design nécessite que le serveur voie le plaintext - Fonctionnalité impossible sans déchiffrement serveur - Impossibilité technique de faire l'opération côté client - Contradiction entre zero-knowledge et exigence business
Références¶
- End-to-End Encryption: Signal Protocol, Matrix E2EE
- Zero-Knowledge Proofs: zkSNARKs, zkSTARKs (hors scope mais concept)
- Proxy Re-Encryption: AFGH scheme (ProbatioVault uses PRE for sharing)
- Client-side Encryption: Best practices Web Crypto API
Historique¶
| Version | Date | Changement |
|---|---|---|
| 1.0.0 | 2026-01-14 | Création initiale |