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)viasha3_256de@noble/hashes/sha3 - Retourne le hash hex lowercase 64 chars, encapsule en
Sha3_256Hashbranded 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
imageBytesen AES-256-GCM avec le DEK et le nonce - Reutilise
gcmde@noble/ciphers/aes(pattern existantsrc/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
0x00viaTypedArray.fill(0x00) - Appelle
zeroize()desrc/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_256generateDEK()→ DEK 256 bitsgenerateNonce()→ nonce 96 bitsencryptFileLevel(imageBytes, dek, nonce)→ ciphertext + tagkekProvider.wrapDEK(dek)→dek_wrapped_b64+kek_idzeroizeDEK(dek)→ buffer ecrase dans lefinally- Retourne
CryptoCapturePipelineResult - Zeroization garantie dans
finallymeme 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 danstransformIgnorePatternsexclusion - Mock :
expo-crypto.getRandomBytesmocke pour rendre les tests deterministes - Mock :
KekProvidermocke 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_idlowercase (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¶
-
hashLocalretourne un hash SHA3-256 conforme^[a-f0-9]{64}$ -
generateDEKretourne 32 bytes via CSPRNG -
generateNonceretourne 12 bytes via CSPRNG -
encryptFileLevelproduit un ciphertext AES-256-GCM + tag 16 bytes -
CryptoCapturePipeline.execute()compose hash → dek → nonce → encrypt → wrap → zeroize - Le
finallygarantitzeroizeDEK(dek)meme en cas d'erreur - Le DEK n'apparait pas en clair dans le resultat du pipeline
-
KekProviderest 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