Aller au contenu

PD-103 — Plan d'implémentation — Capture probatoire d'écran et scellement automatique

1. Découpage en composants

1.1 Projet ProbatioVault-app (React Native / Expo SDK 54)

# Composant Responsabilité Fichiers
M1 capture-state-machine FSM monotone 8 états, gardes de transition, terminal enforcement src/capture/state-machine.ts
M2 capture-crypto SHA3-256 hash, AES-256-GCM file-level, RSA-OAEP-SHA256 wrapping DEK, zeroization buffer src/capture/crypto-pipeline.ts, src/capture/kek-provider.ts
M3 capture-ocr OCR local Apple Vision, non-bloquant, désactivable src/capture/ocr-service.ts
M4 capture-upload Upload single + multipart, retry borné, intégrité S3, annulation, reprise différée src/capture/upload-service.ts, src/capture/multipart-upload.ts
M5 capture-store Zustand store persistant, état capture + différé + TTL watchdog src/store/useCaptureStore.ts
M6 capture-orchestrator Coordination séquentielle M1-M5 : purgeStale → capture → hash → encrypt → upload → purge locale src/capture/orchestrator.ts
M7 capture-purge purgeStale() au démarrage, suppression locale post-UPLOADED, watchdog TTL différé src/capture/purge-manager.ts
M8 capture-ui Écran capture, prévisualisation, validation/annulation, toggle OCR, indicateur progression src/screens/CaptureScreen.tsx, src/components/capture/

1.2 Projet ProbatioVault-backend (NestJS / TypeORM / PostgreSQL)

# Composant Responsabilité Fichiers
M9 capture-ingest POST /documents/capture — validation §5.1, normalisation capture_id, skew timestamp, unwrap DEK via keyring, idempotence fingerprint, persistance capture_events src/modules/capture/capture.controller.ts, src/modules/capture/services/capture-ingest.service.ts, src/modules/capture/dto/, src/modules/capture/entities/capture-event.entity.ts
M10 capture-idempotence Calcul payload_canonical_sha256, comparaison fingerprint, réponse 200/409 src/modules/capture/services/capture-idempotence.service.ts
M11 capture-kek-keyring Keyring KEK courante + historiques, unwrap DEK, codes erreur 422/503 src/modules/capture/services/kek-keyring.service.ts
M12 capture-reconciliation Cron réconciliation : scan non-terminaux, SEAL_DELAYED trigger/clearing, GC orphelins S3, re-enqueue src/modules/capture/services/capture-reconciliation.service.ts
M13 capture-migration Migration DDL : table capture_events + table capture_audit_log (append-only journal probatoire), index, colonnes payload_canonical_sha256, seal_delayed, seal_delayed_conforming_cycles src/database/migrations/XXXXXX-PD103-CreateCaptureEvents.ts, src/database/migrations/XXXXXX-PD103-CreateCaptureAuditLog.ts
M14 capture-tests-backend Tests unitaires + intégration backend src/modules/capture/__tests__/

1.3 Projet ProbatioVault-infra (Terraform / Ansible)

# Composant Responsabilité Fichiers
M15 capture-s3-config Bucket S3 captures, politique presigned URLs, lifecycle rules orphelins terraform/modules/s3-captures/

1.4 Composants existants réutilisés (pas de modification)

Composant existant Réutilisation
src/seal/state-machine.ts Pattern FSM monotone — copié et adapté pour M1
src/crypto/aes-gcm.ts AES-256-GCM — réutilisé tel quel par M2
@noble/hashes SHA-256 — réutilisé pour merkle leaf par M2
src/store/useUploadStore.ts Pattern Zustand persist — inspiration pour M5
src/modules/urgent-seal/ Pattern DEK wrapping backend — inspiration pour M11
src/modules/documents/services/deposit.service.ts Pattern pg_advisory_xact_lock pour idempotence — réutilisé par M10
Rate-limit config existant Extension pour route /documents/capture

2. Flux techniques

2.1 Flux nominal A — Capture et préparation locale (M6 → M7 → M1 → M2 → M3)

  1. M6.orchestrator appelle M7.purgeStale() — suppression artefacts résiduels (INV-103-07).
  2. Capture écran via expo-screen-captureimage_original_bytes (PNG brut).
  3. M1 transition [*] → CAPTURED.
  4. M2.hashLocal(image_original_bytes)hash_sha3_256 via @noble/hashes/sha3 (INV-103-03).
  5. Si OCR activé : M3.extractOCR(image_original_bytes) — non-bloquant, échec absorbé (INV-103-04).
  6. M2.generateDEK() → 256 bits via react-native-get-random-values (CSPRNG).
  7. M2.generateNonce() → 96 bits via CSPRNG (INV-103-39).
  8. M2.encryptFileLevel(image_bytes, DEK, nonce)ciphertext + tag (INV-103-06).
  9. M2.wrapDEK(DEK, KEK_pub)dek_wrapped_b64 + kek_id (INV-103-30).
  10. M2.zeroizeDEK(buffer)TypedArray.fill(0x00) + déréférence (INV-103-32).
  11. M1 transition CAPTURED → UPLOADING — garde : hash + chiffrement validés (INV-103-20).

2.2 Flux nominal B — Upload et ingestion (M4 → M9 → M10 → M11)

  1. M4 demande URL pré-signée S3 au backend.
  2. Si size_bytes ≤ 10MB : PUT single avec Content-MD5 (INV-103-06).
  3. Si size_bytes > 10MB : upload multipart M4.multipart (§5.5bis).
  4. M4 POST /documents/capture avec payload complet incluant dek_wrapped_b64, kek_id.
  5. M9 valide le payload (§5.1), normalise capture_id lowercase (INV-103-31), vérifie skew timestamp (±300s).
  6. M10 calcule payload_canonical_sha256, vérifie idempotence (INV-103-37) → 200/409 si déjà vu.
  7. M11 tente unwrap DEK via keyring (INV-103-34) → 422 ou 503 si échec.
  8. M9 persiste capture_events avec signature_status='PENDING_SIGNATURE' dans transaction ACID (INV-103-10).
  9. Sur ACK 202 : M1 transition UPLOADING → UPLOADED, M7 purge locale immédiate (INV-103-38).
  10. Sur échec réseau : M1 transition UPLOADING → UPLOAD_DEFERRED, stockage local chiffré.

2.3 Flux nominal C — Scellement et notification (pipeline backend existant + M12)

  1. Post-commit async : worker Merkle existant agrège leaf SHA-256(payload_canonique_json).
  2. Pipeline HSM → TSA → blockchain existant (PD-56/PD-55/PD-41).
  3. Garde PENDING_SEAL → SEALED : signature_status='SIGNED' AND hsm_signature_ref IS NOT NULL (INV-103-08).
  4. Push iOS à SEALED : "Votre capture a été scellée avec succès. Preuve vérifiable disponible." (INV-103-12).
  5. SEALED → ANCHOR_CONFIRMED après confirmation blockchain (INV-103-27).
  6. M12 cron réconciliation (10 min) : scan non-terminaux, trigger SEAL_DELAYED si âge > sealSla, clearing après 3 cycles conformes (INV-103-35/36), GC orphelins S3 > orphanTtl (INV-103-33).

2bis. Diagramme de dépendances agents (step 6b)

graph LR
    subgraph "Wave 1 — Fondations (parallélisables)"
        M1[M1: capture-state-machine]
        M2[M2: capture-crypto]
        M3[M3: capture-ocr]
        M7[M7: capture-purge]
        M13[M13: capture-migration]
    end

    subgraph "Wave 2 — Services dépendants"
        M5[M5: capture-store]
        M4[M4: capture-upload]
        M11[M11: capture-kek-keyring]
        M10[M10: capture-idempotence]
    end

    subgraph "Wave 3 — Orchestration"
        M6[M6: capture-orchestrator]
        M9[M9: capture-ingest]
        M12[M12: capture-reconciliation]
    end

    subgraph "Wave 4 — UI + Tests"
        M8[M8: capture-ui]
        M14[M14: capture-tests-backend]
        M15[M15: capture-s3-config]
    end

    M1 --> M5
    M1 --> M6
    M2 --> M4
    M2 --> M6
    M3 --> M6
    M7 --> M6
    M5 --> M6
    M4 --> M6
    M13 --> M9
    M11 --> M9
    M10 --> M9
    M9 --> M12
    M6 --> M8
    M9 --> M14
    M12 --> M14

Waves d'exécution : - Wave 1 (5 agents parallèles) : M1, M2, M3, M7, M13 — aucune dépendance inter-agents. - Wave 2 (4 agents parallèles) : M5 (dépend M1), M4 (dépend M2), M11, M10. - Wave 3 (3 agents) : M6 (dépend M1-M5, M7), M9 (dépend M10, M11, M13), M12 (dépend M9). - Wave 4 (3 agents parallèles) : M8 (dépend M6), M14 (dépend M9, M12), M15.

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-103-01-fidelity soumis = original bit-à-bit Aucune transformation entre capture et upload : PNG brut passé directement au pipeline crypto M6 hash(original) == hash(submitted) en trace Transcodage implicite par Expo/RN image pipeline
INV-103-02-no-transform Recompression/redimensionnement interdits Capture via expo-screen-capture buffer brut, pas de passage par Image Manipulator M6, M8 Métadonnées PNG invariantes pre/post Mise à jour SDK Expo introduisant transformation
INV-103-03-hash-local-first SHA3-256 local avant upload M2.hashLocal() appelé AVANT M4.upload() dans l'orchestrateur, trace horodatée M2, M6 Traces ordonnées hash_ts < upload_ts Inversion d'appel dans orchestrateur
INV-103-04-ocr-local-only OCR local, non-bloquant, sans IA externe M3 utilise Apple Vision uniquement, try/catch absorbant, aucun appel réseau M3 Absence d'appel réseau IA dans traces Dépendance transitive appelant un service distant
INV-103-05-ocr-non-probative OCR enrichit mais ne remplace pas l'image OCR stocké comme métadonnée optionnelle, jamais comme substitut de image_probatory_bytes M3, M6 Champs OCR optionnels dans payload POST N/A
INV-103-06-encryption-file-level AES-256-GCM file-level + intégrité S3 M2.encryptFileLevel() produit ciphertext complet ; M4 ajoute Content-MD5 ou x-amz-content-sha256 au PUT S3 M2, M4 Header intégrité dans requête S3 capturée Oubli header sur multipart parts
INV-103-07-purge-startup purgeStale() au démarrage M7.purgeStale() appelé en première étape de M6.orchestrate(), trace horodatée M7, M6 Trace purge avant capture dans logs Crash entre purge et capture
INV-103-08-sealed-guard PENDING_SEAL → SEALED si SIGNED + hsm_signature_ref Garde dans worker backend existant, colonne capture_events.signature_status vérifiée avant transition M9, pipeline existant Cas négatif bloqué, cas positif passe Worker bypass la garde
INV-103-09-envelope-encryption DEK wrappé RSA-OAEP, jamais clair en base M2 wrappe avant upload, M11 unwrappe en mémoire volatile, colonne dek_wrapped jamais NULL M2, M11 Audit base : aucune colonne DEK clair Persistance accidentelle du DEK
INV-103-10-atomicity-scope DB sync ACID, async post-commit M9 persiste dans transaction unique ; Merkle/TSA/HSM/blockchain via queue BullMQ post-commit M9, pipeline existant Rollback pré-commit ne laisse aucun artefact Race entre commit et enqueue
INV-103-11-distributed-protection Locks, idempotence, réconciliation, rate-limit, clearing Lock pg_advisory_xact_lock(capture_id) (M10), idempotence fingerprint (M10), cron réconciliation (M12), rate-limit Redis (config existant), clearing compteur persistant (M12) M10, M12 Traces lock/idempotence/réconciliation Drift horloge entre workers
INV-103-12-notify-sealed Push à SEALED Notification push iOS via expo-notifications déclenchée par SSE/polling backend à SEALED M6, pipeline existant Réception push + statut app Échec silencieux push
INV-103-20 CAPTURED → UPLOADING si hash + chiffrement OK Garde dans M1 : transition refusée si hash_sha3_256 ou ciphertext absent M1 Transition refusée en test sans hash Garde contournée
INV-103-21 CAPTURED → CANCELLED sur annulation M8 déclenche annulation, M1 autorise transition M1, M8 État CANCELLED, aucune persistance N/A
INV-103-22 UPLOADING → UPLOADED si S3 OK + ACK M4 confirme PUT S3, M9 renvoie 202, orchestrateur transite M4, M6 Double condition vérifiée en trace ACK partiel (S3 OK mais POST échoue)
INV-103-23 UPLOADING → UPLOAD_DEFERRED sur échec M4 épuise retries, orchestrateur transite vers différé M4, M6 État UPLOAD_DEFERRED + payload local Retry infini sans bascule
INV-103-24 UPLOAD_DEFERRED → UPLOADING si JWT + nouvelle URL M6 vérifie session JWT + redemande URL pré-signée avant reprise M6, M4 Trace refresh URL avant PUT Réutilisation URL expirée
INV-103-25 UPLOADED → PENDING_SEAL après persistance M9 persiste dans capture_events + journal append-only, transition automatique M9 Entrée DB + journal avec timestamp Race condition commit/transition
INV-103-26 PENDING_SEAL → SEALED respecte garde Délégué au pipeline existant (PD-55/PD-56) via INV-103-08 Pipeline existant Garde vérifiée N/A
INV-103-27 SEALED → ANCHOR_CONFIRMED après blockchain Délégué au pipeline blockchain existant (PD-41) Pipeline existant tx_hash non vide N/A
INV-103-28-terminal-states CANCELLED et ANCHOR_CONFIRMED terminaux M1 : transition sortante rejetée avec log "état terminal" M1 100% rejets en test N/A
INV-103-29 UPLOADING → CANCELLED + purge + abort S3 M8 déclenche annulation, M4 appelle AbortMultipartUpload ou annule PUT, M7 purge locale M4, M7, M8 Trace abort S3 + suppression locale Abort S3 échoue silencieusement
INV-103-30-key-exchange DEK 256 bits → wrap RSA-OAEP → dek_wrapped_b64 → unwrap backend M2 génère + wrappe, M11 unwrappe via keyring M2, M11 dek_wrapped_b64 + kek_id dans POST, trace unwrap backend KEK indisponible au moment du wrapping
INV-103-31 capture_id normalisé lowercase M6 normalise côté mobile avant émission, M9 normalise à réception M6, M9 Base + logs lowercase uniquement Oubli normalisation d'un côté
INV-103-32-dek-zeroization Buffer DEK écrasé 0x00 après wrapping M2.zeroizeDEK() : TypedArray.fill(0x00) + déréférence M2 Test buffer = 0x00 post-wrapping GC non déterministe JS (limitation documentée)
INV-103-33-s3-orphan-gc Orphelins S3 supprimés > orphanTtl M12 cron scan S3 vs capture_events, suppression si âge > 900s M12 Log audit suppression orphelin Orphelin non détecté (prefix mismatch)
INV-103-34-kek-keyring-rotation Keyring KEK courante + historiques M11 itère keyring en priorisant kek_id, rétention ≥ deferredUploadTtl + dedupWindow M11 Unwrap réussi avec ancien kek_id Keyring trop court → 422 sur captures différées
INV-103-35 SEAL_DELAYED après sealSla M12 cron détecte PENDING_SEAL > 10 min, positionne flag M12 Log flag positionné + timestamp Skew horloge cron
INV-103-36 Clearing après 3 cycles conformes M12 compteur persistant seal_delayed_conforming_cycles, reset si cycle non conforme M12 Compteur observable + flag levé après 3 Cycle comptabilisé à tort comme conforme
INV-103-37 Fingerprint canonique pour idempotence M10 sérialise JSON clés triées, exclut OCR, calcule SHA-256, compare M10 payload_canonical_sha256 stable et stocké Sérialisation non déterministe
INV-103-38 UPLOAD_DEFERRED → CANCELLED à TTL M7 watchdog vérifie TTL, M1 autorise transition, M7 purge M7, M1 Transition + purge après TTL Watchdog ne s'exécute pas (app killed)
INV-103-39-nonce-csprng Nonce 96 bits CSPRNG unique par capture M2.generateNonce() via react-native-get-random-values M2 Nonce unique par capture en trace N/A

4. Mapping critères d'acceptation → mécanismes

Critère ID Mécanisme(s) Composant Observable Risque
CA-103-01 Pipeline optimisé capture → hash : opérations locales synchrones < 3s P95 M2, M6 Instrumentation trigger → CAPTURED Devices plus lents que référence
CA-103-02 M2.hashLocal() appelé avant M4.upload() avec trace horodatée M2, M6 hash_ts < upload_ts Inversion séquentielle
CA-103-03 PNG brut sans transformation, hash triplet identique M6 hash(original) == hash(submitted) == hash(probatory) Transcodage implicite
CA-103-04 Aucun passage par Image Manipulator, pas de resize/recompression M6, M8 Métadonnées image invariantes Upgrade SDK
CA-103-05 Toggle OCR dans UI, payload conditionnel M3, M8 Présence/absence champs OCR N/A
CA-103-06 try/catch absorbant sur M3, aucun appel réseau IA M3 Flux continue post-échec OCR N/A
CA-103-07 Orchestrateur enchaîne hash → encrypt → upload sans input utilisateur post-validation M6 Aucune interaction entre validation et UPLOADED N/A
CA-103-08 Pipeline complet < 5s P95 sur device référence M2, M4, M6 Instrumentation trigger → UPLOADED Réseau lent
CA-103-09 Garde backend INV-103-08 vérifiée avant transition Pipeline existant Cas négatif bloqué N/A
CA-103-10 Push iOS via expo-notifications à SEALED M6, pipeline existant Réception push Échec push silencieux
CA-103-11 Pipeline Merkle/TSA/blockchain existant produit refs Pipeline existant merkle_proof_ref, tsa_token_ref, tx_hash non vides N/A
CA-103-12 M7.purgeStale() en première étape orchestrateur M7, M6 Trace horodatée avant capture N/A
CA-103-13 Chiffrement file-level avant tout transit/stockage, DEK wrappé M2, M11 Audit : aucun clair au repos N/A
CA-103-14 M1 rejette toute transition sortante depuis terminaux M1 100% rejets en test N/A
CA-103-15 dek_wrapped_b64 + kek_id dans POST, trace unwrap backend M2, M11, M9 Champs présents + trace unwrap sans DEK clair N/A
CA-103-16 M6 vérifie JWT + redemande URL avant reprise M6, M4 Trace refresh URL URL expirée réutilisée
CA-103-17 M9 rejette si |timestamp_device - now()| > 300s M9 HTTP 400 TIMESTAMP_SKEW_EXCEEDED N/A
CA-103-18 M10 compare payload_canonical_sha256 → 200/409 M10 Codes HTTP conformes N/A
CA-103-19 M4.multipart conserve ETags, reprend parts manquantes M4 CompleteMultipartUpload réussi après reprise upload_id expiré
CA-103-20 M4.abort() + M7.purgeLocal() sur annulation M4, M7 Trace AbortMultipartUpload + suppression N/A
CA-103-21 M10 sérialise JSON clés triées, normalise lowercase M10 Recalcul stable N/A
CA-103-22 M6 lowercase avant émission, M9 lowercase à réception M6, M9 Base lowercase uniquement N/A
CA-103-23 M2.zeroizeDEK() : fill(0x00) M2 Buffer vérifié 0x00 en test Limitation GC
CA-103-24 M12 cron scan S3 vs DB, suppression > orphanTtl M12 Log suppression orphelin N/A
CA-103-25 M11 itère keyring avec ancien kek_id M11 Unwrap réussi N/A
CA-103-26 M12 trigger SEAL_DELAYED + clearing 3 cycles M12 Flag + compteur N/A
CA-103-27 M7 supprime local immédiatement post-UPLOADED M7 Absence d'artefact local N/A
CA-103-28 M11 renvoie 422 vs 503 distinct M11, M9 Codes HTTP distincts N/A

5. Mapping tests (TC-*) → mécanismes + observables

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau de test visé
TC-NOM-01 INV-01..03, 06, 22, 30, 31 M2 hash + encrypt + wrap, M4 upload, M6 orchestration Hash triplet, traces horodatées, dek_wrapped_b64 + kek_id dans POST Integration
TC-NOM-02 INV-04, 05 M3 désactivé, M6 skip OCR Absence champs OCR dans payload Unit
TC-NOM-03 INV-04, 05 M3 try/catch absorbant Flux continue post-échec OCR Unit
TC-NOM-04 INV-07 M7.purgeStale() Trace purge avant capture Unit
TC-NOM-05 INV-23 M4 retry épuisé, M1 transition État UPLOAD_DEFERRED + payload chiffré local Integration
TC-NOM-06 INV-24 M6 vérifie JWT + refresh URL Trace nouvelle URL avant PUT Integration
TC-NOM-07 INV-25 M9 persistance capture_events Entrée DB traçable Integration
TC-NOM-08 INV-08, 12, 26 Garde backend + push Transition SEALED + push reçu Integration
TC-NOM-09 INV-27 Pipeline blockchain tx_hash non vide Integration
TC-NOM-10 CA-01 M2 + M6 pipeline perf P95 ≤ 3000ms Perf
TC-NOM-11 CA-08 M2 + M4 + M6 pipeline perf P95 ≤ 5000ms Perf
TC-NOM-12 INV-28 M1 terminal enforcement Rejet 100% transitions sortantes Unit
TC-NOM-13 INV-22, §5.5bis M4.multipart reprise partielle ETags conservés + CompleteMultipart Integration
TC-NOM-14 INV-29 M4.abort + M7.purge Trace abort S3 + suppression locale Integration
TC-NOM-15 INV-31, 37 M10 fingerprint + normalisation 200 idempotent malgré casse différente Integration
TC-NOM-16 INV-34 M11 keyring ancien kek_id Unwrap réussi Integration
TC-NOM-17 INV-38 M7 purge post-UPLOADED Absence artefact local Unit
TC-ERR-01 INV-23 M4 retry UPLOAD_DEFERRED après épuisement Unit
TC-ERR-02 INV-21 M1 + M8 annulation CANCELLED, aucune persistance Unit
TC-ERR-05 INV-03 M2 hash mismatch Rejet flux + purge Unit
TC-ERR-06 §5.1 M9 validation DTO HTTP 400 par champ invalide Unit
TC-ERR-07 INV-06 M2 échec crypto CANCELLED + purge Unit
TC-ERR-08 INV-08 Garde backend Rejet transition SEALED Unit
TC-ERR-09 INV-35 M12 cron SLA Flag SEAL_DELAYED positionné Unit
TC-ERR-10 INV-11 Rate-limit Redis HTTP 429 Integration
TC-ERR-11 INV-38 M7 watchdog TTL CANCELLED + purge locale Unit
TC-ERR-12 §5.2 M9 skew validation HTTP 400 TIMESTAMP_SKEW_EXCEEDED Unit
TC-ERR-13 INV-37 M10 fingerprint divergent HTTP 409 Conflict Unit
TC-ERR-16 INV-34 M11 unwrap incompatible HTTP 422 UNWRAP_DEK_FAILED Unit
TC-ERR-17 ER-103-17 M11 HSM indisponible HTTP 503 KEY_SERVICE_UNAVAILABLE Unit
TC-INV-01 INV-01, 02 M6 pipeline complet Hash triplet identique Integration
TC-INV-02 INV-03, 20 M1 garde CAPTURED→UPLOADING Transition refusée sans hash/chiffrement Unit
TC-INV-03 INV-06, 09, 30, 39 M2 pipeline crypto complet DEK + nonce CSPRNG + wrap + ciphertext only Unit
TC-INV-04 INV-10 M9 crash injection Rollback pré-commit, rattrapage post-commit Integration
TC-INV-05 INV-11 lock M10 pg_advisory_xact_lock Un seul worker transitionne Integration
TC-INV-06 INV-31, 37 M10 normalisation + fingerprint 4 variantes (A/B/C/D) conformes Unit
TC-INV-07 INV-11 réconciliation M12 re-enqueue orphelins Convergence sans doublon Integration
TC-INV-08 INV-36 M12 compteur cycles Clearing après exactement 3 cycles Unit
TC-INV-09 INV-29 M4 + M7 annulation Abort S3 + purge Integration
TC-INV-10 INV-28 M1 terminaux Rejet toute transition Unit
TC-INV-11 INV-33 M12 GC S3 Orphelins supprimés > orphanTtl Integration
TC-INV-12 INV-32 M2 zeroize Buffer 0x00 Unit
TC-INV-13 INV-34 M11 keyring Unwrap ancienne KEK Unit
TC-INV-14 INV-35 M12 trigger SEAL_DELAYED Flag positionné + compteur reset Unit

6. Gestion des erreurs

Code Situation Mécanisme Composant Observable
Échec capture (M8) Rejet flux, pas de transition M8, M6 Aucun état créé
Hash mismatch (ER-103-05) Rejet + purge artefacts M2, M6 Trace rejet + purge
Échec chiffrement (ER-103-07) CANCELLED + purge M2, M1, M7 Transition + purge
Échec OCR (ER-103-03) Absorbé, flux continue M3 Trace warning, pas de blocage
Réseau KO retryable (ER-103-01) Retry borné (configurable 0..5, défaut 3), backoff exponentiel M4 Trace retries
Réseau KO final (ER-103-04) UPLOAD_DEFERRED + stockage local chiffré M4, M1 État différé
Annulation utilisateur (ER-103-02) CANCELLED + abort S3 + purge M1, M4, M7, M8 Trace abort + purge
400 Payload invalide (ER-103-06) Validation DTO NestJS + class-validator M9 HTTP 400 + détail champ
400 Skew timestamp (ER-103-12) |device_ts - now()| > 300s M9 TIMESTAMP_SKEW_EXCEEDED
401/403 Auth invalide Guard JWT existant Auth existant HTTP 401/403
409 Conflit idempotence (ER-103-13) Fingerprint divergent M10 HTTP 409 Conflict
422 Unwrap DEK échoue (ER-103-16) Keyring incompatible / DEK corrompu M11 UNWRAP_DEK_FAILED
429 Rate-limit (ER-103-10) Redis compteur user_id + IP / 60s Config rate-limit HTTP 429
503 HSM/KMS indisponible (ER-103-17) Service de clé privée down M11 KEY_SERVICE_UNAVAILABLE
SLA scellement dépassé (ER-103-09) SEAL_DELAYED flag + notification M12 Flag + log
TTL différé expiré (ER-103-11) UPLOAD_DEFERRED → CANCELLED + purge M7 Transition + purge
Intégrité S3 invalide (ER-103-14) Rejet part/objet + retry M4 Trace checksum mismatch
JWT expiré en reprise (ER-103-15) Pas de reprise, attente réauth M6 État conservé UPLOAD_DEFERRED

7. Impacts sécurité

Risque Mitigation Composant Journalisation
Fuite DEK en mémoire JS Zeroization TypedArray.fill(0x00) + déréférence immédiate ; limitation GC documentée (INV-103-32) M2 Test buffer 0x00
DEK en clair côté backend Unwrap en mémoire volatile uniquement, jamais persisté ; dek_wrapped seul en base (INV-103-09) M11 Audit colonne base
Clair en transit Chiffrement file-level obligatoire avant tout PUT S3 ; HTTPS/TLS pour URL pré-signées (INV-103-06) M2, M4 Trace header intégrité S3
Clair au repos local Stockage local uniquement chiffré ; purge immédiate post-UPLOADED ; purgeStale au démarrage (INV-103-07, 38) M7 Trace purge + audit filesystem
Nonce AES-GCM réutilisé Nonce 96 bits CSPRNG unique par capture (INV-103-39) M2 Unicité en trace
Replay/forge capture_id Normalisation lowercase + fingerprint canonique + fenêtre dédup 24h (INV-103-31, 37) M10 200/409 traçable
Skew temporel (rejeu) Tolérance ±300s stricte côté backend (§5.2) M9 HTTP 400
Orphelins S3 (fuite données/coûts) GC cron > orphanTtl (INV-103-33) M12 Log suppression
Rotation KEK perd accès captures différées Keyring rétention ≥ deferredUploadTtl + dedupWindow (INV-103-34) M11 Config keyring profondeur
Rate-limit insuffisant/excessif Quota configurable 1-300 req/min, défaut 60 (§5.2) Config rate-limit HTTP 429 traçable
Annulation sans cleanup Abort S3 + purge locale obligatoires (INV-103-29) M4, M7 Trace abort + purge

8. Hypothèses techniques

ID Hypothèse Impact si faux
HT-103-01 react-native-quick-crypto supporte SHA3-256 et RSA-OAEP-SHA256 dans Expo Dev Client (H-103-11). Stratégie crypto obligatoire (§10.1) : react-native-quick-crypto comme implémentation primaire. Fallback @noble/hashes/sha3 uniquement si natif non supporté, documenté comme dérogation avec benchmark obligatoire (P95 < 3s sur device référence). Fallback sur @noble/hashes/sha3 pour hash (pur JS, plus lent) ; RSA-OAEP via expo-crypto ou native module custom. Risque P95 capture > 3s. Benchmark obligatoire avant validation.
HT-103-02 expo-screen-capture fournit un buffer PNG brut sans transcodage implicite Si transformation implicite : interposer lecture directe du framebuffer via native module.
HT-103-03 Le pipeline de scellement backend (PD-56/PD-55/PD-41) est opérationnel et accepte un leaf hash pour agrégation Merkle STUB: dépendances PD-56/PD-55/PD-41. Les transitions PENDING_SEAL → SEALED → ANCHOR_CONFIRMED resteront non testables E2E tant que ces stories ne sont pas livrées.
HT-103-04 Le module documents backend expose déjà un mécanisme de génération d'URL pré-signée S3 réutilisable Si absent : créer un service S3PresignedUrlService dans M9 (ajout ~2j).
HT-103-05 L'API expo-notifications supporte push local + remote dans le même build EAS Si contrainte : configurer APNs dans EAS build config.
HT-103-06 pg_advisory_xact_lock suffit pour le lock distribué au scope capture_id (pas besoin de Redis lock) Si multi-instance DB read-replica : migrer vers Redis lock pattern (déjà utilisé pour rate-limit).
HT-103-07 Le scan S3 orphelins (M12) peut lister les objets par prefix captures/ avec pagination Si bucket partagé sans prefix : ajouter un prefix dédié dans M15 config Terraform.

9. Points de vigilance (risques, dette, pièges)

  1. Transcodage implicite Expoexpo-screen-capture peut subir des transformations PNG silencieuses lors de mises à jour SDK. Mitigation : test de non-régression TC-NR-01 (hash triplet sur corpus de référence) à exécuter après chaque upgrade Expo SDK.

  2. SHA3-256 performance mobile@noble/hashes/sha3 est pur JavaScript. Sur images > 100 MB, la latence risque de dépasser le SLA 3s. Mitigation : si react-native-quick-crypto ne supporte pas SHA3, envisager un module natif C++ via JSI. Mesurer en campagne perf TC-NOM-10.

  3. Zeroization DEK en JavaScriptTypedArray.fill(0x00) n'offre aucune garantie d'effacement physique (GC non déterministe, copies V8). Limitation acceptée et documentée dans la spec (INV-103-32). Ne PAS promettre un effacement cryptographique complet.

  4. Upload multipart et abortAbortMultipartUpload S3 ne supprime pas immédiatement les parts uploadées (délai S3). Les orphelins multipart sont gérés par lifecycle rules S3 (M15) + GC réconciliation (M12).

  5. Dépendances pipeline scellement — PD-56 (Merkle), PD-55 (TSA), PD-41 (blockchain) sont des prérequis. Si non livrés, les captures resteront en PENDING_SEAL indéfiniment. Documenter comme STUB avec stories de destination.

  6. Keyring KEK profondeur — Une profondeur de 3 anciennes clés couvre 3 × deferredUploadTtl (3 × 24h = 72h). Si la politique de rotation est plus fréquente, augmenter la profondeur ou allonger la rétention.

  7. Rate-limit granularitéuser_id + IP peut être contourné par changement d'IP (mobile). Acceptable pour le quota initial (60 req/min). Surveiller en production.

  8. Sérialisation canonique JSON — La stabilité du fingerprint dépend de la sérialisation UTF-8 déterministe. Utiliser JSON.stringify avec replacer triant les clés, PAS de bibliothèque tierce non contrôlée. Tester avec TC-INV-06 (4 variantes).

  9. TTL watchdog en background iOS — Le watchdog TTL différé (M7) ne s'exécutera pas si l'app est en background killed par iOS. Mitigation : vérifier TTL au prochain lancement (via purgeStale).

10. Hors périmètre

  • Capture vidéo.
  • Capture système hors sandbox applicative.
  • Annotation d'image avancée.
  • Analyse IA distante.
  • Import galerie (story séparée).
  • Protection contre photo externe.
  • Notification à ANCHOR_CONFIRMED (Q-103-04 non résolu).
  • Whitelist langues OCR (Q-103-05 non résolu).

11. Mécanismes cross-module

Aucune modification d'autres modules existants. Le module capture est auto-contenu.

Les interactions avec les modules existants sont en lecture/appel uniquement : - Pipeline scellement (Merkle/TSA/HSM/blockchain) : enqueue via BullMQ post-commit. - Rate-limit : extension config existante (ajout route /documents/capture). - Auth : guard JWT existant appliqué au controller. - API presigned URL : M9 expose un endpoint POST /documents/capture/presign retournant { url, object_key, expires_at }. Le mobile utilise object_key pour upload puis l'inclut dans le payload POST. Si un service S3PresignedUrlService existe déjà dans documents, il est réutilisé ; sinon, créé dans M9 (estimé ~0.5j).

11bis. Journal probatoire append-only [v2]

La table capture_audit_log (M13) est un event store INSERT-ONLY : - Colonnes : id, capture_id, event_type, payload_json, created_at - Aucun UPDATE/DELETE autorisé (contrainte applicative + trigger DB BEFORE UPDATE OR DELETE RAISE EXCEPTION) - Chaque transition d'état est loguée dans cette table (INV-103-25) - Séparée de capture_events qui est mutable (statut courant)

11ter. Configuration Jest + ESM + react-native-quick-crypto [v2]

  • jest.config.js : transformIgnorePatterns exclut react-native-quick-crypto
  • moduleNameMapper redirige vers un mock pour les tests unitaires (crypto natif non dispo en Jest)
  • Tests d'intégration crypto via build EAS (device/simulateur) uniquement

12. Périmètre de test

Niveau de test In scope Hors scope (justification)
Unitaire M1 (FSM), M2 (crypto pipeline), M3 (OCR mock), M4 (upload mock), M5 (store), M7 (purge), M9 (validation/DTO), M10 (idempotence), M11 (keyring), M12 (réconciliation)
Intégration M6 (orchestrateur complet), M9+M10+M11 (ingest pipeline), M4+S3 (upload réel), M12+DB (réconciliation)
E2E Flux CAPTURED → UPLOADED (mobile → backend mock). Testable dans le périmètre PD-103. UPLOADED → SEALED → ANCHOR_CONFIRMED hors scope : dépendances PD-56/PD-55/PD-41 non livrées. STUB: PD-55 (scellement), PD-56 (merkle), PD-41 (notification).
Perf TC-NOM-10 (P95 capture ≤ 3s), TC-NOM-11 (P95 upload ≤ 5s) Perf scellement (dépend pipeline backend PD-55)
Sécurité TC-INV-03 (crypto audit), TC-INV-12 (zeroize), TC-NEG-* (adversarial) Pentest complet (hors PD-103)

Tous les niveaux de test internes à PD-103 sont couverts. Seul le E2E complet CAPTURED → ANCHOR_CONFIRMED est exclu car il dépend de stories non livrées (PD-56, PD-55, PD-41).