Aller au contenu

PD-103 — Agent Developer — Module M2 : capture-crypto

1. Identite agent

  • Agent : agent-developer (Agent B — Claude)
  • Story : PD-103
  • Module : M2 — capture-crypto
  • Wave : 1 (fondation, sans dependance inter-agents)
  • Date : 2026-04-03

2. Resume

Module M2 implemente le pipeline cryptographique local pour la capture probatoire d'ecran : hash SHA3-256, chiffrement AES-256-GCM file-level, wrapping DEK via RSA-OAEP-SHA256, et zeroization du DEK apres wrapping.

Fichiers a creer : - src/capture/crypto-pipeline.ts — Pipeline crypto local (hash, encrypt, wrap, zeroize) - src/capture/kek-provider.ts — Fournisseur de cle publique backend KEK - src/__tests__/capture/crypto-pipeline.test.ts — Tests contractuels + qualite

Fichiers existants reutilises : - src/capture/types.ts — Branded types, constantes, interfaces (T1 capture-types) - src/crypto/aes-gcm.ts — AES-256-GCM encrypt/decrypt via @noble/ciphers - src/crypto/zeroize.ts — Fonctions zeroize, withZeroize, SecureBuffer - src/crypto/utils.ts — bytesToHex, bytesToBase64, hexToBytes, base64ToBytes - src/crypto/constants.ts — AES_KEY_LENGTH, AES_NONCE_LENGTH, AES_TAG_LENGTH

3. Artefacts livres

Fichier Role Lignes estimees
src/capture/crypto-pipeline.ts Hash SHA3-256 + AES-256-GCM encrypt + RSA-OAEP wrap + zeroize DEK ~250
src/capture/kek-provider.ts Interface + implementation KEK provider (cle publique backend) ~80
src/__tests__/capture/crypto-pipeline.test.ts Tests contractuels TC-INV-02, TC-INV-03, TC-INV-12, TC-ERR-05, TC-ERR-07 + qualite ~400

4. Architecture

4.1 Decisions architecturales

architectural_decisions:
  - decision: "Composition fonctionnelle avec fonctions pures exportees + classe CryptoCapturePipeline"
    rationale: "Chaque etape (hash, encrypt, wrap, zeroize) est testable isolement ; la classe compose le flux complet avec try/finally pour zeroization."
    alternatives_considered:
      - "Pipeline purement fonctionnel sans classe"
      - "Heritage depuis un BaseCryptoPipeline"
    trade_offs: "Classe legere (pas d'etat mutable) mais offre un point d'orchestration unique pour le finally/zeroize."

  - decision: "Utilisation de @noble/hashes/sha3 pour SHA3-256 (pur JS) avec fallback documente"
    rationale: "react-native-quick-crypto n'est pas garanti disponible en SHA3-256 dans tous les builds. @noble/hashes est pure JS, auditee, compatible Hermes. Le plan prevoit un fallback @noble/hashes si le natif n'est pas disponible (HT-103-01)."
    alternatives_considered:
      - "react-native-quick-crypto exclusif"
      - "expo-crypto digest (pas de SHA3-256)"
    trade_offs: "Performance pure JS plus lente que natif sur images > 100 MB. Acceptable pour P95 < 3s sur device reference (images screenshot < 10 MB typiquement)."

4.2 Pattern utilise

Pattern composition fonctionnelle avec zeroization obligatoire (CLAUDE.md : regles crypto PD-242) :

let dek: Uint8Array | null = null;
try {
  dek = generateDEK();
  // ... utilisation du DEK ...
} finally {
  if (dek) {
    zeroize(dek);
  }
}

Le module expose : 1. Fonctions pures : hashLocal, encryptFileLevel, generateDEK, generateNonce, wrapDEK, zeroizeDEK 2. Classe orchestratrice : CryptoCapturePipeline qui compose les fonctions avec try/finally 3. Interface injectable : KekProvider pour fournir la cle publique backend

4.3 Dependances

Dependance Usage Justification
@noble/hashes/sha3 SHA3-256 hash local Pure JS, auditee, compatible Hermes (INV-103-03)
@noble/ciphers/aes AES-256-GCM file-level encrypt Deja utilise dans src/crypto/aes-gcm.ts (INV-103-06)
expo-crypto CSPRNG pour DEK (256 bits) et nonce (96 bits) Deja utilise dans le codebase (INV-103-39)
src/crypto/zeroize.ts Zeroization buffer DEK Pattern existant PD-97 (INV-103-32)
src/crypto/utils.ts bytesToHex, bytesToBase64 Conversion existante

RSA-OAEP-SHA256 wrapping : Le wrapping DEK avec RSA-OAEP-SHA256 necessite react-native-quick-crypto ou un equivalent natif. En l'absence de ce module dans le build, le KekProvider doit etre substitue par un mock en test. L'implementation reelle du wrapping RSA est derriere l'interface KekProvider.wrapDEK() pour decoupler le pipeline crypto du module natif RSA.

4.4 Exports publics

// --- crypto-pipeline.ts ---

// Fonctions pures
export function hashLocal(imageBytes: Uint8Array): Sha3_256Hash;
export function generateDEK(): Uint8Array;
export function generateNonce(): Uint8Array;
export function encryptFileLevel(
  imageBytes: Uint8Array,
  dek: Uint8Array,
  nonce: Uint8Array,
): { ciphertext: Uint8Array; tag: Uint8Array };
export function zeroizeDEK(buffer: Uint8Array): void;

// Classe orchestratrice
export class CryptoCapturePipeline {
  constructor(kekProvider: KekProvider);
  execute(imageBytes: Uint8Array): Promise<CryptoCapturePipelineResult>;
}

// Resultat du pipeline
export interface CryptoCapturePipelineResult {
  readonly hashSha3_256: Sha3_256Hash;
  readonly ciphertext: Uint8Array;
  readonly aesGcmNonceB64: AesGcmNonceB64;
  readonly aesGcmTagB64: AesGcmTagB64;
  readonly dekWrappedB64: DekWrappedB64;
  readonly kekId: KekId;
}

// --- kek-provider.ts ---

export interface KekProvider {
  /** Wrappe le DEK avec la KEK publique backend active (RSA-OAEP-SHA256) */
  wrapDEK(dek: Uint8Array): Promise<{ dekWrappedB64: DekWrappedB64; kekId: KekId }>;
  /** Retourne le kek_id de la cle active */
  getActiveKekId(): KekId;
}

export class StaticKekProvider implements KekProvider { ... }

4.5 Diagramme pipeline

sequenceDiagram
    participant O as Orchestrateur (M6)
    participant P as CryptoCapturePipeline
    participant H as @noble/hashes/sha3
    participant C as @noble/ciphers/aes
    participant R as KekProvider (RSA-OAEP)
    participant Z as zeroize

    O->>P: execute(imageBytes)
    P->>H: sha3_256(imageBytes)
    H-->>P: hash_sha3_256 (hex lowercase)
    P->>P: generateDEK() via expo-crypto (256 bits CSPRNG)
    P->>P: generateNonce() via expo-crypto (96 bits CSPRNG)
    P->>C: gcm(dek, nonce).encrypt(imageBytes)
    C-->>P: ciphertext + tag
    P->>R: wrapDEK(dek)
    R-->>P: dek_wrapped_b64 + kek_id
    P->>Z: zeroizeDEK(dek) — fill(0x00)
    P-->>O: CryptoCapturePipelineResult

5. Implementation detaillee

5.1 src/capture/crypto-pipeline.ts

hashLocal(imageBytes)

  • Calcule SHA3-256(imageBytes) via sha3_256 de @noble/hashes/sha3
  • Retourne le hash hex lowercase 64 chars, encapsule en Sha3_256Hash branded type
  • Validation : le resultat respecte CAPTURE_VALIDATION_PATTERNS.HASH_SHA3_256
  • Invariants couverts : INV-103-03 (hash local avant tout upload)
import { sha3_256 } from "@noble/hashes/sha3";
import { bytesToHex } from "../crypto/utils";
import { asSha3_256Hash, type Sha3_256Hash } from "./types";

export function hashLocal(imageBytes: Uint8Array): Sha3_256Hash {
  if (imageBytes.length === 0) {
    throw new CaptureHashError("Image bytes cannot be empty");
  }
  const hashBytes = sha3_256(imageBytes);
  const hashHex = bytesToHex(hashBytes);
  return asSha3_256Hash(hashHex);
}

generateDEK()

  • Genere 256 bits (32 bytes) aleatoires via expo-crypto.getRandomBytes(32) (CSPRNG)
  • Retourne Uint8Array(32)
  • Invariant couvert : INV-103-30 (DEK aleatoire 256 bits par capture)
import * as Crypto from "expo-crypto";
import { AES_KEY_LENGTH } from "../crypto/constants";

export function generateDEK(): Uint8Array {
  return Crypto.getRandomBytes(AES_KEY_LENGTH);
}

generateNonce()

  • Genere 96 bits (12 bytes) aleatoires via expo-crypto.getRandomBytes(12) (CSPRNG)
  • Retourne Uint8Array(12)
  • Invariant couvert : INV-103-39 (nonce CSPRNG unique par capture)
import { AES_NONCE_LENGTH } from "../crypto/constants";

export function generateNonce(): Uint8Array {
  return Crypto.getRandomBytes(AES_NONCE_LENGTH);
}

encryptFileLevel(imageBytes, dek, nonce)

  • Chiffre imageBytes en AES-256-GCM avec le DEK et le nonce
  • Reutilise gcm de @noble/ciphers/aes (pattern existant src/crypto/aes-gcm.ts)
  • Retourne { ciphertext, tag } (tag = 16 derniers bytes du ciphertext+tag)
  • Invariants couverts : INV-103-06 (chiffrement file-level obligatoire)
import { gcm } from "@noble/ciphers/aes.js";
import { AES_KEY_LENGTH, AES_TAG_LENGTH } from "../crypto/constants";

export function encryptFileLevel(
  imageBytes: Uint8Array,
  dek: Uint8Array,
  nonce: Uint8Array,
): { ciphertext: Uint8Array; tag: Uint8Array } {
  if (dek.length !== AES_KEY_LENGTH) {
    throw new CaptureEncryptionError("DEK must be exactly 32 bytes (256 bits)");
  }
  if (nonce.length !== AES_NONCE_LENGTH) {
    throw new CaptureEncryptionError("Nonce must be exactly 12 bytes (96 bits)");
  }

  const cipher = gcm(dek, nonce);
  const ciphertextWithTag = cipher.encrypt(imageBytes);

  const ciphertext = ciphertextWithTag.slice(0, -AES_TAG_LENGTH);
  const tag = ciphertextWithTag.slice(-AES_TAG_LENGTH);

  return { ciphertext, tag };
}

zeroizeDEK(buffer)

  • Ecrase le buffer DEK avec 0x00 via TypedArray.fill(0x00)
  • Appelle zeroize() de src/crypto/zeroize.ts (double passe : fill + boucle explicite)
  • Invariant couvert : INV-103-32 (buffer DEK ecrase apres wrapping)
  • Limitation documentee : effacement physique non garanti en JS (GC non deterministe)
import { zeroize } from "../crypto/zeroize";

export function zeroizeDEK(buffer: Uint8Array): void {
  zeroize(buffer);
}

CryptoCapturePipeline (classe orchestratrice)

  • Compose les fonctions pures dans l'ordre contractuel (§5.4) :
  • hashLocal(imageBytes)hash_sha3_256
  • generateDEK() → DEK 256 bits
  • generateNonce() → nonce 96 bits
  • encryptFileLevel(imageBytes, dek, nonce) → ciphertext + tag
  • kekProvider.wrapDEK(dek)dek_wrapped_b64 + kek_id
  • zeroizeDEK(dek) → buffer ecrase dans le finally
  • Retourne CryptoCapturePipelineResult
  • Zeroization garantie dans finally meme en cas d'erreur (CLAUDE.md : regles crypto)
export class CryptoCapturePipeline {
  constructor(private readonly kekProvider: KekProvider) {}

  async execute(imageBytes: Uint8Array): Promise<CryptoCapturePipelineResult> {
    if (imageBytes.length === 0) {
      throw new CaptureHashError("Image bytes cannot be empty");
    }

    // Etape 1 : hash local (INV-103-03)
    const hashSha3_256 = hashLocal(imageBytes);

    // Etape 2-6 : encrypt + wrap avec zeroization garantie
    let dek: Uint8Array | null = null;
    try {
      dek = generateDEK();
      const nonce = generateNonce();

      // Etape 4 : chiffrement file-level (INV-103-06)
      const { ciphertext, tag } = encryptFileLevel(imageBytes, dek, nonce);

      // Etape 5 : wrapping DEK (INV-103-30)
      const { dekWrappedB64, kekId } = await this.kekProvider.wrapDEK(dek);

      // Conversion base64 pour le DTO
      const aesGcmNonceB64 = asAesGcmNonceB64(bytesToBase64(nonce));
      const aesGcmTagB64 = asAesGcmTagB64(bytesToBase64(tag));

      return {
        hashSha3_256,
        ciphertext,
        aesGcmNonceB64,
        aesGcmTagB64,
        dekWrappedB64,
        kekId,
      };
    } finally {
      // Etape 6 : zeroization (INV-103-32)
      if (dek) {
        zeroizeDEK(dek);
      }
    }
  }
}

Classes d'erreur

export class CaptureHashError extends Error {
  readonly code = CAPTURE_ERROR_CODES.HASH_MISMATCH;
  constructor(message: string) {
    super(message);
    this.name = "CaptureHashError";
  }
}

export class CaptureEncryptionError extends Error {
  readonly code = CAPTURE_ERROR_CODES.ENCRYPTION_FAILED;
  constructor(message: string) {
    super(message);
    this.name = "CaptureEncryptionError";
  }
}

5.2 src/capture/kek-provider.ts

Interface abstraite pour le fournisseur de cle publique backend :

import type { DekWrappedB64, KekId } from "./types";

export interface KekProvider {
  wrapDEK(dek: Uint8Array): Promise<{ dekWrappedB64: DekWrappedB64; kekId: KekId }>;
  getActiveKekId(): KekId;
}

Implementation concrete StaticKekProvider pour les tests et le bootstrap initial :

import { bytesToBase64 } from "../crypto/utils";
import { asDekWrappedB64, asKekId } from "./types";
import type { DekWrappedB64, KekId } from "./types";

export class StaticKekProvider implements KekProvider {
  constructor(
    private readonly publicKey: CryptoKey | Uint8Array,
    private readonly kekId: KekId,
  ) {}

  async wrapDEK(dek: Uint8Array): Promise<{ dekWrappedB64: DekWrappedB64; kekId: KekId }> {
    // RSA-OAEP-SHA256 wrapping via react-native-quick-crypto ou SubtleCrypto
    const wrappedBytes = await this.rsaOaepWrap(dek);
    const dekWrappedB64 = asDekWrappedB64(bytesToBase64(wrappedBytes));
    return { dekWrappedB64, kekId: this.kekId };
  }

  getActiveKekId(): KekId {
    return this.kekId;
  }

  private async rsaOaepWrap(dek: Uint8Array): Promise<Uint8Array> {
    // Implementation depend du runtime :
    // - react-native-quick-crypto : crypto.publicEncrypt()
    // - SubtleCrypto (si disponible) : crypto.subtle.encrypt()
    // Decouplage via cette interface permet de swapper sans refactoring
    throw new Error("RSA-OAEP wrapping not implemented — requires native crypto module");
  }
}

Note implementation : Le rsaOaepWrap est volontairement non implemente dans cette classe de base. L'implementation reelle sera fournie par un NativeKekProvider utilisant react-native-quick-crypto (dependance §10.1 de la spec). Le StaticKekProvider sert de support pour les tests avec un mock du wrapping.

5.3 Configuration Jest

Le module crypto-pipeline.ts utilise @noble/hashes/sha3 et @noble/ciphers/aes qui sont ESM-only. Configuration :

  • Jest : les modules @noble/* doivent etre dans transformIgnorePatterns exclusion
  • Mock : expo-crypto.getRandomBytes mocke pour rendre les tests deterministes
  • Mock : KekProvider mocke pour isoler le pipeline crypto du module RSA natif
// jest.config.js (existant — extension)
transformIgnorePatterns: [
  "node_modules/(?!(@noble/hashes|@noble/ciphers|expo-crypto)/)",
],

6. Mapping invariants

Invariant Mecanisme Fonction/Methode Observable
INV-103-03 SHA3-256 local avant upload hashLocal() Hash hex 64 chars lowercase
INV-103-06 AES-256-GCM file-level encryptFileLevel() Ciphertext + tag 16 bytes
INV-103-09 DEK wrappe RSA-OAEP, jamais clair CryptoCapturePipeline.execute() dek_wrapped_b64 dans resultat
INV-103-30 DEK 256 bits + wrap + kek_id generateDEK() + kekProvider.wrapDEK() DEK 32 bytes + dek_wrapped_b64 + kek_id
INV-103-32 Buffer DEK ecrase 0x00 apres wrapping zeroizeDEK() dans finally Buffer verifie 0x00 en test
INV-103-39 Nonce 96 bits CSPRNG unique par capture generateNonce() Nonce 12 bytes via CSPRNG

7. Mapping tests (TC-*)

Test ID Reference spec Fonction testee Niveau
TC-INV-02 INV-103-03, INV-103-20 hashLocal() — hash calcule avant upload Unit
TC-INV-03 INV-103-06, INV-103-09, INV-103-30, INV-103-39 CryptoCapturePipeline.execute() — pipeline crypto complet Unit
TC-INV-12 INV-103-32 zeroizeDEK() — buffer 0x00 apres wrapping Unit
TC-ERR-05 INV-103-03, ER-103-05 hashLocal() — rejet image vide Unit
TC-ERR-07 INV-103-06, ER-103-07 encryptFileLevel() — echec chiffrement Unit
TC-NOM-01 (partiel) INV-103-01..03, 06, 30, 31 Pipeline complet hash+encrypt+wrap Unit

7.1 Plan de tests detaille

TEST-ID: TC-INV-02 (partiel M2)
Reference: INV-103-03

GIVEN
  - Image PNG de reference (bytes connus)
WHEN
  - hashLocal(imageBytes) est appele
THEN
  - Le hash retourne est un hex lowercase de 64 caracteres
  - Le hash respecte le pattern ^[a-f0-9]{64}$
  - Le hash est deterministe (meme image → meme hash)
  - Le hash est different pour des images differentes
TEST-ID: TC-INV-03
Reference: INV-103-06, INV-103-09, INV-103-30, INV-103-39

GIVEN
  - Image PNG de reference
  - KekProvider mocke retournant un dek_wrapped_b64 et kek_id valides
WHEN
  - CryptoCapturePipeline.execute(imageBytes) est appele
THEN
  - hashSha3_256 est present et valide (hex lowercase 64 chars)
  - ciphertext est non vide et different de imageBytes
  - aesGcmNonceB64 respecte ^[A-Za-z0-9+/]{16}$
  - aesGcmTagB64 respecte ^[A-Za-z0-9+/]{22}==$
  - dekWrappedB64 est non vide (provient du mock KEK)
  - kekId est non vide
  - Le DEK n'apparait nulle part dans le resultat
TEST-ID: TC-INV-12
Reference: INV-103-32

GIVEN
  - Un buffer DEK de 32 bytes avec valeurs non nulles
WHEN
  - zeroizeDEK(buffer) est appele
THEN
  - Chaque octet du buffer vaut 0x00
  - Le buffer original est modifie in-place
TEST-ID: TC-ERR-05
Reference: INV-103-03, ER-103-05

GIVEN
  - Image bytes vide (Uint8Array(0))
WHEN
  - hashLocal(imageBytes) est appele
THEN
  - Une CaptureHashError est levee
  - Aucun hash n'est retourne
TEST-ID: TC-ERR-07
Reference: INV-103-06, ER-103-07

GIVEN
  - DEK de taille incorrecte (16 bytes au lieu de 32)
WHEN
  - encryptFileLevel(imageBytes, badDek, nonce) est appele
THEN
  - Une CaptureEncryptionError est levee
  - Aucun ciphertext n'est retourne

7.2 Tests additionnels (non contractuels)

Test Objet
NON-CONTRACTUAL-01 hashLocal est deterministe (meme input → meme output)
NON-CONTRACTUAL-02 generateDEK() retourne 32 bytes
NON-CONTRACTUAL-03 generateNonce() retourne 12 bytes
NON-CONTRACTUAL-04 encryptFileLevel produit un ciphertext dechiffrable avec le meme DEK et nonce
NON-CONTRACTUAL-05 CryptoCapturePipeline.execute zeroize le DEK meme si wrapDEK echoue
NON-CONTRACTUAL-06 encryptFileLevel avec nonce de taille incorrecte leve une erreur
NON-CONTRACTUAL-07 Deux appels generateNonce() produisent des nonces differents (probabiliste)
NON-CONTRACTUAL-08 Le ciphertext est different du plaintext (non-identite)
NON-CONTRACTUAL-09 Le tag fait exactement 16 bytes (128 bits)
NON-CONTRACTUAL-10 Le pipeline complet ne retourne pas le DEK en clair dans son resultat

8. Matrice de couverture Test-ID → fichiers

TC-INV-02  → src/__tests__/capture/crypto-pipeline.test.ts
TC-INV-03  → src/__tests__/capture/crypto-pipeline.test.ts
TC-INV-12  → src/__tests__/capture/crypto-pipeline.test.ts
TC-ERR-05  → src/__tests__/capture/crypto-pipeline.test.ts
TC-ERR-07  → src/__tests__/capture/crypto-pipeline.test.ts
TC-NOM-01  → src/__tests__/capture/crypto-pipeline.test.ts (partiel : pipeline crypto)

9. Impacts securite

Risque Mitigation Observable
Fuite DEK en memoire JS zeroizeDEK() dans finally + dereference ; limitation GC documentee (INV-103-32) Test buffer 0x00 post-wrapping
Nonce AES-GCM reutilise CSPRNG via expo-crypto.getRandomBytes(12) unique par capture (INV-103-39) Unicite probabiliste en test
Hash calcule apres upload Pipeline sequentiel : hash avant encrypt avant upload (INV-103-03) Ordre d'execution impose par CryptoCapturePipeline.execute()
DEK taille insuffisante Validation dek.length === 32 dans encryptFileLevel Exception si taille incorrecte
Clair en transit encryptFileLevel avant tout PUT S3 ; M4 responsable du transport Ciphertext-only dans CryptoCapturePipelineResult

10. Hypotheses

ID Hypothese Impact si faux
HT-M2-01 @noble/hashes/sha3 est disponible et performant via le transform Jest/Vitest existant Si non : configurer un transform ESM supplementaire ou basculer en Vitest pour ces tests
HT-M2-02 expo-crypto.getRandomBytes() est synchrone et disponible en env Jest (via mock) Si non : mocker expo-crypto manuellement dans le setup Jest
HT-M2-03 Le wrapping RSA-OAEP-SHA256 est derriere l'interface KekProvider et teste via mock Si non : l'implementation reelle depend de react-native-quick-crypto (hors scope M2, integration dans M6)
HT-M2-04 Les images screenshot iOS < 10 MB typiquement, donc SHA3-256 pur JS < 100 ms Si faux (images > 100 MB) : benchmark obligatoire et eventuel module natif C++ via JSI

11. Dependances inter-modules

Module Type de dependance Description
M1 (state-machine) Consomme M2 M1 attend hashComputed + encryptionDone comme gardes pour CAPTURED → UPLOADING
M4 (upload) Consomme M2 M4 recoit CryptoCapturePipelineResult (ciphertext, nonce_b64, tag_b64, dek_wrapped_b64, kek_id)
M6 (orchestrator) Consomme M2 M6 appelle CryptoCapturePipeline.execute() puis transmet le resultat a M4
M7 (purge) Independant Aucune dependance directe
M11 (kek-keyring backend) Reciproque Le kek_id produit par M2 est consomme par M11 pour l'unwrap cote backend

12. Hors perimetre M2

  • Implementation reelle du wrapping RSA-OAEP-SHA256 (depend de react-native-quick-crypto, livree avec M6/integration)
  • Upload S3 et headers d'integrite (M4)
  • Stockage local chiffre du payload differe (M7)
  • OCR (M3)
  • Purge artefacts (M7)
  • Normalisation capture_id lowercase (responsabilite M6 orchestrateur)

13. Structure des tests

// src/__tests__/capture/crypto-pipeline.test.ts

describe("capture/crypto-pipeline", () => {
  // --- TC-INV-02 : hashLocal ---
  describe("hashLocal (INV-103-03)", () => {
    it("TC-INV-02: retourne un hash SHA3-256 hex lowercase 64 chars", ...);
    it("TC-INV-02: hash est deterministe", ...);
    it("TC-INV-02: hash differe pour des images differentes", ...);
    it("TC-ERR-05: leve CaptureHashError si image vide", ...);
  });

  // --- Fonctions utilitaires ---
  describe("generateDEK", () => {
    it("retourne 32 bytes", ...);
  });

  describe("generateNonce (INV-103-39)", () => {
    it("retourne 12 bytes", ...);
    it("deux appels produisent des valeurs differentes", ...);
  });

  // --- TC-ERR-07 : encryptFileLevel ---
  describe("encryptFileLevel (INV-103-06)", () => {
    it("chiffre avec AES-256-GCM et retourne ciphertext + tag", ...);
    it("ciphertext est dechiffrable avec le meme DEK et nonce", ...);
    it("tag fait exactement 16 bytes", ...);
    it("ciphertext est different du plaintext", ...);
    it("TC-ERR-07: leve CaptureEncryptionError si DEK taille incorrecte", ...);
    it("TC-ERR-07: leve CaptureEncryptionError si nonce taille incorrecte", ...);
  });

  // --- TC-INV-12 : zeroizeDEK ---
  describe("zeroizeDEK (INV-103-32)", () => {
    it("TC-INV-12: ecrase chaque octet du buffer a 0x00", ...);
    it("TC-INV-12: modifie le buffer in-place", ...);
  });

  // --- TC-INV-03 : CryptoCapturePipeline ---
  describe("CryptoCapturePipeline (INV-103-03, 06, 09, 30, 39)", () => {
    it("TC-INV-03: execute le pipeline complet et retourne tous les artefacts", ...);
    it("TC-INV-03: nonce_b64 respecte le pattern contractuel", ...);
    it("TC-INV-03: tag_b64 respecte le pattern contractuel", ...);
    it("TC-INV-03: le DEK n'apparait pas dans le resultat", ...);
    it("zeroize le DEK meme si wrapDEK echoue", ...);
    it("leve CaptureHashError si image vide", ...);
  });
});

14. Non-regression

Test NR Objet Observable
TC-NR-02 (partiel) Ordonnancement hash/chiffrement Hash calcule avant chiffrement dans pipeline
TC-NR-07 (partiel) Key exchange envelope dek_wrapped_b64 et kek_id presents dans resultat

15. Checklist pre-livraison

  • hashLocal retourne un hash SHA3-256 conforme ^[a-f0-9]{64}$
  • generateDEK retourne 32 bytes via CSPRNG
  • generateNonce retourne 12 bytes via CSPRNG
  • encryptFileLevel produit un ciphertext AES-256-GCM + tag 16 bytes
  • CryptoCapturePipeline.execute() compose hash → dek → nonce → encrypt → wrap → zeroize
  • Le finally garantit zeroizeDEK(dek) meme en cas d'erreur
  • Le DEK n'apparait pas en clair dans le resultat du pipeline
  • KekProvider est une interface injectable (pas de couplage dur au module RSA natif)
  • Tous les tests TC-INV-02, TC-INV-03, TC-INV-12, TC-ERR-05, TC-ERR-07 passent
  • Coverage >= 80% lignes et branches
  • 0 erreur ESLint, 0 erreur TypeScript