06 — Chiffrement bout en bout et secrets avancés#

Le chiffrement bout en bout (E2EE) garantit que seuls les participants légitimes d’une communication peuvent accéder au contenu en clair. Ce chapitre explore les mécanismes sous-jacents — du protocole Double Ratchet à l’envelope encryption — ainsi que les outils de gestion de secrets avancés (Vault, SOPS, Sealed Secrets) qui industrialisent ces principes.

Prérequis

Ce chapitre approfondit cicd/19_secrets_management.md. Maîtrise de Vault, Kubernetes et des primitives cryptographiques (ECDH, AES-GCM, RSA-OAEP) requise.


Principes du chiffrement bout en bout#

Définition et propriétés#

Le E2EE satisfait trois propriétés fondamentales :

  • Confidentialité totale : ni le serveur relayeur, ni l’opérateur de plateforme, ni un observateur réseau ne peut déchiffrer les messages.

  • Forward Secrecy (PFS) : la compromission d’une clé à long terme ne compromet pas les sessions passées (les clés de session sont éphémères et détruites après usage).

  • Break-in Recovery : si une clé de session est compromise, les sessions suivantes restent confidentielles grâce à l’injection régulière d’aléatoire via Diffie-Hellman.

Modèle de menace#

Acteur

E2EE sans PFS

E2EE avec Double Ratchet

Fournisseur de service

Chiffré

Chiffré

Observateur réseau passif

Chiffré

Chiffré

Compromission clé à long terme

Déchiffre tout

Ne déchiffre que le futur immédiat

Compromission clé de session

Déchiffre la session

Isolé à quelques messages


Double Ratchet — le protocole Signal#

Le protocole Signal combine deux mécanismes de renouvellement de clés imbriqués.

KDF Chain — le ratchet symétrique#

Une KDF Chain est une chaîne de dérivation de clés :

Chain Key (CK_n)
    │
    ├── HMAC(CK_n, 0x01) → Message Key MK_n  (chiffrement du message n)
    └── HMAC(CK_n, 0x02) → Chain Key CK_{n+1}  (étape suivante)

Chaque message consomme une Message Key unique, dérivée de la Chain Key courante. La Chain Key évolue à chaque message — compromission d’une MK n’expose pas les autres.

Ratchet Diffie-Hellman#

Régulièrement (à chaque changement de sens de la conversation), un nouveau ECDH est exécuté entre les clés éphémères des deux participants. Le secret ECDH résultant est injecté comme graine d’une nouvelle KDF Chain :

Alice (DH_ratchet_A_new)  ←→  Bob (DH_ratchet_B)
        │
        └── ECDH(DH_A_new, DH_B) → Root Key' → nouvelles Chain Keys

Ce mécanisme fournit la propriété de Break-in Recovery : même si un attaquant capture l’état complet des clés symétriques, il lui faudra résoudre un problème ECDH pour suivre les futures sessions.

Protocole X3DH — établissement de session initial#

Avant le Double Ratchet, X3DH (Extended Triple Diffie-Hellman) établit les clés initiales sans que les deux parties soient simultanément en ligne :

Alice calcule :
  DH1 = ECDH(IK_A, SPK_B)          # Identity key A + Signed PreKey B
  DH2 = ECDH(EK_A, IK_B)           # Ephemeral key A + Identity key B
  DH3 = ECDH(EK_A, SPK_B)          # Ephemeral key A + Signed PreKey B
  DH4 = ECDH(EK_A, OPK_B)          # Ephemeral key A + One-Time PreKey B
  SK  = KDF(DH1 || DH2 || DH3 || DH4)   # Clé de session initiale

Implémentation dans la pratique

Signal, WhatsApp, Facebook Messenger (mode secret) et Google Messages utilisent tous le protocole Double Ratchet / X3DH. La bibliothèque de référence est libsignal-protocol.


Vault avancé#

Transit Engine — chiffrement-as-a-service#

Transit Engine expose le chiffrement/déchiffrement comme une API, sans jamais exposer la clé :

# Activer le moteur Transit
vault secrets enable transit

# Créer une clé de chiffrement nommée (type AES-256-GCM96 par défaut)
vault write -f transit/keys/my-app-key

# Chiffrer une donnée
vault write transit/encrypt/my-app-key \
  plaintext=$(echo -n "données sensibles" | base64)
# → ciphertext: vault:v1:abc123...

# Déchiffrer
vault write transit/decrypt/my-app-key \
  ciphertext="vault:v1:abc123..."
# → plaintext: (base64 de la donnée originale)

# Rotation de la clé (sans migrer les données)
vault write -f transit/keys/my-app-key/rotate
# Les nouvelles encryptions utilisent v2, les anciennes (v1) restent déchiffrables

# Migration optionnelle des anciens ciphertexts
vault write transit/rewrap/my-app-key \
  ciphertext="vault:v1:abc123..."
# → ciphertext: vault:v2:xyz456...

La politique Vault associée :

path "transit/encrypt/my-app-key" {
  capabilities = ["update"]
}
path "transit/decrypt/my-app-key" {
  capabilities = ["update"]
}
# NE PAS accorder transit/keys/my-app-key (export de la clé)

Dynamic Secrets — base de données#

# Activer le moteur Database
vault secrets enable database

# Configurer une connexion PostgreSQL
vault write database/config/myapp-db \
  plugin_name=postgresql-database-plugin \
  allowed_roles="readonly,readwrite" \
  connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/myapp" \
  username="vault_admin" password="vault_admin_pass"

# Définir un rôle (credentials temporaires, TTL 1h)
vault write database/roles/readonly \
  db_name=myapp-db \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
  default_ttl=1h max_ttl=24h

# Obtenir des credentials (valables 1h, révoqués automatiquement après)
vault read database/creds/readonly
# → username: v-app-readonly-xyz123
# → password: A1b2c3d4...
# → lease_id: database/creds/readonly/xyz...

# Révocation immédiate si compromise
vault lease revoke database/creds/readonly/xyz...

PKI Engine — TTL courts et intégration Kubernetes#

Voir le chapitre 05 pour la configuration du moteur PKI. L’intégration Kubernetes se fait via cert-manager ou via l’agent Vault Injector :

# Annotation Vault Agent sur le Pod
apiVersion: v1
kind: Pod
metadata:
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/readonly"
    vault.hashicorp.com/role: "myapp"
spec:
  containers:
    - name: app
      image: myapp:latest
      # Les credentials sont montés dans /vault/secrets/db-creds

Envelope Encryption#

Principe DEK / KEK#

L”envelope encryption (chiffrement en enveloppe) sépare deux niveaux de clés :

Données ──── chiffrées par ────► DEK (Data Encryption Key, AES-256)
DEK     ──── chiffrée par ─────► KEK (Key Encryption Key, RSA ou AES-256)
  • Le DEK est une clé symétrique générée aléatoirement pour chaque objet (ou session).

  • Le KEK est une clé maîtresse gérée par un KMS (AWS KMS, Cloud KMS, Vault).

  • Seul le DEK chiffré (ciphertext) est stocké avec les données. La KEK ne quitte jamais le KMS.

Avantages opérationnels#

Rotation de KEK sans redéchiffrement des données : on rechiffre uniquement le DEK (quelques octets) avec la nouvelle KEK, pas les données elles-mêmes. Idéal pour des téraoctets de données.

Isolation des accès : différents services peuvent avoir des DEK différents, tous protégés par la même KEK, ou par des KEK distinctes selon les politiques d’accès.

AWS KMS et Google Cloud KMS#

# AWS KMS : chiffrement direct (données < 4 Ko)
aws kms encrypt \
  --key-id arn:aws:kms:eu-west-1:123456789012:key/mrk-abc123 \
  --plaintext fileb://secret.txt \
  --output text --query CiphertextBlob | base64 -d > secret.enc

# Générer un DEK via AWS KMS (GenerateDataKey)
aws kms generate-data-key \
  --key-id arn:aws:kms:eu-west-1:123456789012:key/mrk-abc123 \
  --key-spec AES_256
# → plaintext DEK (utiliser pour chiffrer, puis détruire en mémoire)
# → ciphertext DEK (stocker avec les données)

SOPS avec age et AWS KMS#

SOPS (Secrets OPerationS, Mozilla) chiffre sélectivement les valeurs d’un fichier YAML/JSON/ENV tout en laissant les clés lisibles.

Configuration .sops.yaml multi-environnements#

# .sops.yaml — à la racine du repo
creation_rules:
  # Environnement production : AWS KMS + clé age de backup
  - path_regex: secrets/production/.*\.yaml$
    kms: arn:aws:kms:eu-west-1:123456789012:key/mrk-prod-abc123
    age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

  # Environnement staging : age uniquement
  - path_regex: secrets/staging/.*\.yaml$
    age: age1abc123...

  # Développeurs locaux
  - path_regex: secrets/dev/.*\.yaml$
    age: >-
      age1dev1...,
      age1dev2...

Workflow complet#

# Générer une paire de clés age
age-keygen -o ~/.config/sops/age/keys.txt
# → public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

# Chiffrer un fichier de secrets
sops --encrypt secrets/production/database.yaml > secrets/production/database.enc.yaml

# Éditer en place (déchiffrement temporaire en mémoire)
sops secrets/production/database.enc.yaml

# Déchiffrer pour utilisation en CI/CD
sops --decrypt secrets/production/database.enc.yaml | kubectl apply -f -

# Rotation de KEK : rechiffrement avec une nouvelle clé
sops updatekeys secrets/production/database.enc.yaml

SOPS et GitOps

Avec SOPS, les secrets chiffrés peuvent être versionnés dans Git. L’historique révèle les clés (identifiant KMS, empreinte age) mais jamais les valeurs en clair. Combine bien avec ArgoCD et le pattern GitOps.


Sealed Secrets Kubernetes#

Sealed Secrets (Bitnami) permet de stocker des Secrets Kubernetes chiffrés dans Git, déchiffrés uniquement à l’intérieur du cluster.

Architecture#

Developer                    Cluster
    │                            │
    ├─ kubeseal (chiffre)        ├─ sealed-secrets-controller
    │   ├─ clé publique du       │   ├─ clé privée (dans le cluster)
    │   │  controller            │   └─ déchiffre les SealedSecrets
    │   └─ crée SealedSecret     │
    └─ commit dans Git       └─ crée Secret Kubernetes standard

Utilisation#

# Installer le controller
helm install sealed-secrets \
  oci://registry-1.docker.io/bitnamicharts/sealed-secrets \
  -n kube-system

# Récupérer la clé publique du cluster
kubeseal --fetch-cert \
  --controller-namespace kube-system \
  --controller-name sealed-secrets > cluster-public-key.pem

# Créer un SealedSecret depuis un Secret standard
kubectl create secret generic db-password \
  --from-literal=password=s3cr3t --dry-run=client -o yaml |
kubeseal --cert cluster-public-key.pem \
  --format yaml > sealed-db-password.yaml

# Committer sealed-db-password.yaml dans Git (sûr)
# Le controller le déchiffrera et créera le Secret dans le cluster
# Exemple de SealedSecret résultant
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-password
  namespace: production
spec:
  encryptedData:
    password: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq...
  template:
    metadata:
      name: db-password
      namespace: production

Rotation de la clé du controller

La clé privée du controller Sealed Secrets doit être sauvegardée (kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key). Sa perte rend tous les SealedSecrets indéchiffrables — il faudrait rechiffrer l’intégralité des secrets avec une nouvelle clé.


Cellules Python#

Simulation Double Ratchet simplifié#

Hide code cell source

import os
import hashlib
import hmac as hmac_module
import struct

from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import seaborn as sns
import pandas as pd
import numpy as np
# --- Simulation Double Ratchet simplifié ---
# Implémentation pédagogique de 3 échanges

def hkdf_derive(input_key: bytes, salt: bytes, info: bytes, length: int = 32) -> bytes:
    """Dérivation HKDF-SHA256."""
    hkdf = HKDF(algorithm=hashes.SHA256(), length=length, salt=salt, info=info)
    return hkdf.derive(input_key)


def kdf_chain(chain_key: bytes) -> tuple[bytes, bytes]:
    """KDF Chain : retourne (message_key, next_chain_key)."""
    mk = hmac_module.new(chain_key, b'\x01', hashlib.sha256).digest()
    ck = hmac_module.new(chain_key, b'\x02', hashlib.sha256).digest()
    return mk, ck


def ecdh(private_key: X25519PrivateKey, public_key) -> bytes:
    """ECDH X25519."""
    return private_key.exchange(public_key)


# --- Initialisation X3DH simulée ---
# Alice et Bob génèrent leurs clés Identity (long terme) et Ratchet (éphémères)
alice_ik = X25519PrivateKey.generate()
bob_ik   = X25519PrivateKey.generate()

alice_ratchet = X25519PrivateKey.generate()
bob_ratchet   = X25519PrivateKey.generate()

# Shared secret initial (X3DH simplifié : un seul DH pour la démo)
initial_dh = ecdh(alice_ik, bob_ik.public_key())
root_key   = hkdf_derive(initial_dh, b'\x00' * 32, b'RootKey', 32)

# Chaînes initiales
alice_send_ck = hkdf_derive(root_key, b'\x00' * 32, b'AliceSend', 32)
bob_send_ck   = hkdf_derive(root_key, b'\x00' * 32, b'BobSend', 32)

print("=== État initial ===")
print(f"  Root Key (hex) : {root_key.hex()[:32]}...")

# Historique des états pour visualisation
history = []

def record(step: str, actor: str, mk: bytes, ck: bytes, dh_ratchet: bool = False):
    history.append({
        "étape": step, "acteur": actor,
        "mk_hex": mk.hex()[:12], "ck_hex": ck.hex()[:12],
        "dh_ratchet": dh_ratchet,
    })

# --- Échange 1 : Alice → Bob (symmetric ratchet) ---
mk1, alice_send_ck = kdf_chain(alice_send_ck)
record("1: Alice→Bob", "Alice", mk1, alice_send_ck, dh_ratchet=False)

plaintext1 = b"Bonjour Bob, message secret 1"
nonce1 = os.urandom(12)
ct1 = AESGCM(mk1).encrypt(nonce1, plaintext1, None)
dec1 = AESGCM(mk1).decrypt(nonce1, ct1, None)
assert dec1 == plaintext1

print(f"\n--- Échange 1 (Alice→Bob, symmetric ratchet) ---")
print(f"  Message key   : {mk1.hex()[:24]}...")
print(f"  Chiffré (hex) : {ct1.hex()[:32]}...")
print(f"  Déchiffré     : {dec1.decode()}")

# --- Échange 2 : Bob → Alice (DH ratchet + symmetric) ---
# Bob fait tourner le DH ratchet : génère nouvelle clé éphémère
bob_ratchet_new = X25519PrivateKey.generate()
dh_out = ecdh(bob_ratchet_new, alice_ratchet.public_key())
new_root, bob_send_ck = (
    hkdf_derive(dh_out, root_key, b'NewRoot', 32),
    hkdf_derive(dh_out, root_key, b'BobNewSend', 32),
)
root_key = new_root

mk2, bob_send_ck = kdf_chain(bob_send_ck)
record("2: Bob→Alice", "Bob", mk2, bob_send_ck, dh_ratchet=True)

plaintext2 = b"Bonjour Alice, reponse chiffree"
nonce2 = os.urandom(12)
ct2 = AESGCM(mk2).encrypt(nonce2, plaintext2, None)
dec2 = AESGCM(mk2).decrypt(nonce2, ct2, None)

print(f"\n--- Échange 2 (Bob→Alice, DH ratchet + symmetric) ---")
print(f"  Message key   : {mk2.hex()[:24]}...")
print(f"  Déchiffré     : {dec2.decode()}")

# --- Échange 3 : Alice → Bob (DH ratchet) ---
alice_ratchet_new = X25519PrivateKey.generate()
dh_out3 = ecdh(alice_ratchet_new, bob_ratchet_new.public_key())
new_root3, alice_send_ck = (
    hkdf_derive(dh_out3, root_key, b'NewRoot3', 32),
    hkdf_derive(dh_out3, root_key, b'AliceNewSend3', 32),
)
root_key = new_root3

mk3, alice_send_ck = kdf_chain(alice_send_ck)
record("3: Alice→Bob", "Alice", mk3, alice_send_ck, dh_ratchet=True)

plaintext3 = b"Message 3, nouvelle cle DH"
nonce3 = os.urandom(12)
ct3 = AESGCM(mk3).encrypt(nonce3, plaintext3, None)
dec3 = AESGCM(mk3).decrypt(nonce3, ct3, None)

print(f"\n--- Échange 3 (Alice→Bob, DH ratchet) ---")
print(f"  Message key   : {mk3.hex()[:24]}...")
print(f"  Déchiffré     : {dec3.decode()}")

# --- Visualisation des états de clés ---
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(12, 5))

colors_actor = {"Alice": "#4878d0", "Bob": "#ee854a"}
y_pos = {"Alice": 2.5, "Bob": 1.5}

for i, h in enumerate(history):
    x = i * 3.5 + 1
    y = y_pos[h["acteur"]]
    color = colors_actor[h["acteur"]]

    rect = FancyBboxPatch(
        (x - 1.5, y - 0.4), 3.0, 0.8,
        boxstyle="round,pad=0.05",
        facecolor=color, edgecolor="white", linewidth=1.5, alpha=0.82,
    )
    ax.add_patch(rect)

    label = h["étape"].split(":")[0]
    ax.text(x, y + 0.15, label, ha="center", va="center",
            fontsize=9, fontweight="bold", color="white")
    ax.text(x, y - 0.12, f"MK: {h['mk_hex']}…", ha="center", va="center",
            fontsize=7, color="white", fontstyle="italic")

    if h["dh_ratchet"]:
        ax.text(x, y + 0.5 if h["acteur"] == "Alice" else y - 0.7,
                "DH ratchet ↻", ha="center", va="center",
                fontsize=8, color="crimson", fontweight="bold")

# Flèches entre échanges
for i in range(len(history) - 1):
    x_from = i * 3.5 + 1
    x_to   = (i + 1) * 3.5 + 1
    ax.annotate("", xy=(x_to - 1.5, (2.5 + 1.5) / 2),
                xytext=(x_from + 1.5, (2.5 + 1.5) / 2),
                arrowprops=dict(arrowstyle="->", color="gray", lw=1.5))

ax.set_xlim(-0.5, len(history) * 3.5 + 0.5)
ax.set_ylim(0.5, 3.5)
ax.set_yticks([1.5, 2.5])
ax.set_yticklabels(["Bob", "Alice"], fontsize=11)
ax.set_xticks([])
ax.set_title("Double Ratchet — évolution des clés sur 3 échanges\n"
             "Les DH ratchets (en rouge) renouvellent l'entropie à chaque changement de sens",
             fontsize=11, fontweight="bold")

legend_handles = [
    mpatches.Patch(facecolor="#4878d0", label="Alice"),
    mpatches.Patch(facecolor="#ee854a", label="Bob"),
]
ax.legend(handles=legend_handles, loc="upper right", fontsize=9)
plt.savefig("double_ratchet.png", dpi=120, bbox_inches="tight")
plt.show()
=== État initial ===
  Root Key (hex) : ae54558822d426c431dc2bfcc78a496a...

--- Échange 1 (Alice→Bob, symmetric ratchet) ---
  Message key   : 1668ec9f007df62b883c2d1b...
  Chiffré (hex) : a6c84c48d0761f524a8c21df35a57d49...
  Déchiffré     : Bonjour Bob, message secret 1

--- Échange 2 (Bob→Alice, DH ratchet + symmetric) ---
  Message key   : f0a235546289fe54f6261c22...
  Déchiffré     : Bonjour Alice, reponse chiffree

--- Échange 3 (Alice→Bob, DH ratchet) ---
  Message key   : d3638a45265c9acb9b997d4b...
  Déchiffré     : Message 3, nouvelle cle DH
_images/032aab26b8a6344de545c13ccb5aff7c98cea7c49eb98222038eeb47f1b8666d.png

Simulation envelope encryption#

# --- Envelope Encryption : DEK chiffré par KEK ---

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

# 1. KEK : paire RSA-4096 (simulant un KMS)
kek_private = rsa.generate_private_key(public_exponent=65537, key_size=2048)
kek_public  = kek_private.public_key()
print("=== Envelope Encryption ===")
print(f"KEK RSA-2048 généré (clé publique taille : {kek_public.key_size} bits)")

# 2. DEK : clé AES-256 aléatoire (générée pour cette session/objet)
dek = AESGCM.generate_key(bit_length=256)
print(f"DEK AES-256 généré : {dek.hex()[:24]}...")

# 3. Chiffrement des données avec le DEK (AES-256-GCM)
plaintext_data = b"Donnees ultra-sensibles : numeros de carte, cles API, etc."
nonce_data = os.urandom(12)
ciphertext_data = AESGCM(dek).encrypt(nonce_data, plaintext_data, b"contexte:production")
print(f"\n--- Chiffrement des données (DEK) ---")
print(f"Données chiffrées ({len(ciphertext_data)} octets) : {ciphertext_data.hex()[:32]}...")

# 4. Chiffrement du DEK avec la KEK (RSA-OAEP)
encrypted_dek = kek_public.encrypt(
    dek,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None,
    ),
)
print(f"\n--- Chiffrement du DEK (KEK RSA-OAEP) ---")
print(f"DEK chiffré ({len(encrypted_dek)} octets) : {encrypted_dek.hex()[:32]}...")
print("→ Stocker (ciphertext_data, nonce_data, encrypted_dek) — jamais le DEK en clair")

# 5. Déchiffrement complet (simulation récupération)
# 5a. Déchiffrer le DEK avec la KEK
recovered_dek = kek_private.decrypt(
    encrypted_dek,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None,
    ),
)
assert recovered_dek == dek
print(f"\n--- Déchiffrement ---")
print(f"DEK récupéré : {recovered_dek.hex()[:24]}... ✓")

# 5b. Déchiffrer les données avec le DEK récupéré
recovered_data = AESGCM(recovered_dek).decrypt(nonce_data, ciphertext_data, b"contexte:production")
assert recovered_data == plaintext_data
print(f"Données déchiffrées : {recovered_data.decode()} ✓")

# 6. Simulation rotation KEK : seul le DEK est rechiffré
kek_private_v2 = rsa.generate_private_key(public_exponent=65537, key_size=2048)
kek_public_v2  = kek_private_v2.public_key()

encrypted_dek_v2 = kek_public_v2.encrypt(
    recovered_dek,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None,
    ),
)
print(f"\n--- Rotation KEK v1 → v2 ---")
print(f"Nouveau DEK chiffré ({len(encrypted_dek_v2)} octets) : {encrypted_dek_v2.hex()[:32]}...")
print("→ Les données chiffrées (ciphertext_data) sont INCHANGÉES — rotation instantanée")
=== Envelope Encryption ===
KEK RSA-2048 généré (clé publique taille : 2048 bits)
DEK AES-256 généré : c88dd674cc5d26f57d300624...

--- Chiffrement des données (DEK) ---
Données chiffrées (74 octets) : fca0dc5aad8bf1b74877658249715b7a...

--- Chiffrement du DEK (KEK RSA-OAEP) ---
DEK chiffré (256 octets) : 2d8208a6aa1844f1b3a7ea070dad4627...
→ Stocker (ciphertext_data, nonce_data, encrypted_dek) — jamais le DEK en clair

--- Déchiffrement ---
DEK récupéré : c88dd674cc5d26f57d300624... ✓
Données déchiffrées : Donnees ultra-sensibles : numeros de carte, cles API, etc. ✓

--- Rotation KEK v1 → v2 ---
Nouveau DEK chiffré (256 octets) : 7ab0590b3bbe9e331aa51b7404a284bb...
→ Les données chiffrées (ciphertext_data) sont INCHANGÉES — rotation instantanée

Visualisation hiérarchie DEK/KEK#

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(12, 7))
ax.set_xlim(0, 12)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Hiérarchie de chiffrement par enveloppe (Envelope Encryption)\n"
             "KMS → KEK → DEK → Données",
             fontsize=13, fontweight="bold", pad=16)

def draw_box(ax, x, y, w, h, label, sublabel, color, fontsize=10):
    rect = FancyBboxPatch(
        (x, y), w, h,
        boxstyle="round,pad=0.15",
        facecolor=color, edgecolor="white", linewidth=2, alpha=0.88,
    )
    ax.add_patch(rect)
    ax.text(x + w / 2, y + h / 2 + 0.15, label,
            ha="center", va="center", fontsize=fontsize,
            fontweight="bold", color="white")
    ax.text(x + w / 2, y + h / 2 - 0.22, sublabel,
            ha="center", va="center", fontsize=8,
            color="white", fontstyle="italic", alpha=0.9)

# Niveaux
palette = sns.color_palette("deep", 6)

# KMS (niveau 0)
draw_box(ax, 3.5, 6.2, 5.0, 1.2, "KMS / HSM", "Clé maîtresse — jamais exportée", palette[3], fontsize=11)

# KEK (niveau 1)
draw_box(ax, 1.0, 4.2, 4.0, 1.2, "KEK (Key Encryption Key)", "RSA-4096 ou AES-256", palette[0])
draw_box(ax, 7.0, 4.2, 4.0, 1.2, "KEK v2 (après rotation)", "Même DEK rechiffré", palette[2])

# DEK (niveau 2)
draw_box(ax, 1.0, 2.2, 4.0, 1.2, "DEK (Data Encryption Key)", "AES-256, par objet/session", palette[1])
draw_box(ax, 7.0, 2.2, 4.0, 1.2, "DEK chiffré (stocké)", "Blob opaque, même clé", palette[4])

# Données (niveau 3)
draw_box(ax, 3.5, 0.2, 5.0, 1.2, "Données chiffrées", "AES-256-GCM ciphertext", palette[5])

# Flèches
arrow_props = dict(arrowstyle="-|>", color="#555", lw=2)

# KMS → KEK
ax.annotate("", xy=(3.0, 5.4), xytext=(4.5, 6.2), arrowprops=arrow_props)
ax.text(3.2, 5.85, "génère/protège", fontsize=8, color="#555", rotation=-30)

# KMS → KEK v2
ax.annotate("", xy=(9.0, 5.4), xytext=(7.5, 6.2), arrowprops=arrow_props)
ax.text(7.5, 5.85, "rotation", fontsize=8, color="#555", rotation=30)

# KEK → DEK chiffrement
ax.annotate("", xy=(3.0, 3.4), xytext=(3.0, 4.2), arrowprops=arrow_props)
ax.text(2.0, 3.8, "chiffre DEK", fontsize=8, color="#555")

# KEK v2 → DEK rechiffrement
ax.annotate("", xy=(9.0, 3.4), xytext=(9.0, 4.2), arrowprops=arrow_props)
ax.text(9.1, 3.8, "rechiffre DEK", fontsize=8, color="#555")

# DEK → Données
ax.annotate("", xy=(6.0, 1.4), xytext=(3.0, 2.2), arrowprops=arrow_props)
ax.text(3.8, 1.75, "chiffre données", fontsize=8, color="#555")

# DEK chiffré → Données (stockage)
ax.annotate("", xy=(6.5, 1.4), xytext=(9.0, 2.2), arrowprops=arrow_props)
ax.text(7.5, 1.75, "stocké avec", fontsize=8, color="#555")

plt.savefig("envelope_encryption.png", dpi=120, bbox_inches="tight")
plt.show()
print("Hiérarchie DEK/KEK visualisée.")
_images/a0e275a6b7553ffb2b3ed42a9df9dc5c67fdd59f3e854a31523b462e75725639.png
Hiérarchie DEK/KEK visualisée.

Résumé#

  1. E2EE garantit que seuls les participants déchiffrent les messages, indépendamment du serveur relayeur. La propriété fondamentale est la confidentialité persistante (PFS) : les sessions passées restent protégées même après compromission d’une clé à long terme.

  2. Double Ratchet combine un ratchet symétrique (KDF chain, une clé par message) et un ratchet Diffie-Hellman (injection d’entropie ECDH à chaque changement de sens). C’est le protocole de Signal, WhatsApp et Google Messages.

  3. Vault Transit Engine expose le chiffrement comme service d’API, permettant la rotation de clés sans migration des données existantes. Les TTL courts et les dynamic secrets réduisent drastiquement la durée de vie des credentials.

  4. Envelope encryption (DEK/KEK) permet la rotation de clé maîtresse sans rechiffrer les données : seul le DEK (quelques octets) est rechiffré. Indispensable pour les systèmes gérant de grands volumes de données chiffrées.

  5. SOPS rend les secrets versionnable dans Git en ne chiffrant que les valeurs (pas les clés), avec support multi-KMS. Compatible GitOps avec ArgoCD.

  6. Sealed Secrets déplace la confiance vers le cluster Kubernetes : seul le controller interne peut déchiffrer les secrets, les développeurs n’ont jamais accès à la clé privée.