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)¶
M6.orchestratorappelleM7.purgeStale()— suppression artefacts résiduels (INV-103-07).- Capture écran via
expo-screen-capture→image_original_bytes(PNG brut). M1transition[*] → CAPTURED.M2.hashLocal(image_original_bytes)→hash_sha3_256via@noble/hashes/sha3(INV-103-03).- Si OCR activé :
M3.extractOCR(image_original_bytes)— non-bloquant, échec absorbé (INV-103-04). M2.generateDEK()→ 256 bits viareact-native-get-random-values(CSPRNG).M2.generateNonce()→ 96 bits via CSPRNG (INV-103-39).M2.encryptFileLevel(image_bytes, DEK, nonce)→ciphertext + tag(INV-103-06).M2.wrapDEK(DEK, KEK_pub)→dek_wrapped_b64 + kek_id(INV-103-30).M2.zeroizeDEK(buffer)→TypedArray.fill(0x00)+ déréférence (INV-103-32).M1transitionCAPTURED → UPLOADING— garde : hash + chiffrement validés (INV-103-20).
2.2 Flux nominal B — Upload et ingestion (M4 → M9 → M10 → M11)¶
M4demande URL pré-signée S3 au backend.- Si
size_bytes ≤ 10MB: PUT single avecContent-MD5(INV-103-06). - Si
size_bytes > 10MB: upload multipart M4.multipart (§5.5bis). M4POST/documents/captureavec payload complet incluantdek_wrapped_b64,kek_id.M9valide le payload (§5.1), normalisecapture_idlowercase (INV-103-31), vérifie skew timestamp (±300s).M10calculepayload_canonical_sha256, vérifie idempotence (INV-103-37) → 200/409 si déjà vu.M11tente unwrap DEK via keyring (INV-103-34) → 422 ou 503 si échec.M9persistecapture_eventsavecsignature_status='PENDING_SIGNATURE'dans transaction ACID (INV-103-10).- Sur ACK 202 :
M1transitionUPLOADING → UPLOADED,M7purge locale immédiate (INV-103-38). - Sur échec réseau :
M1transitionUPLOADING → UPLOAD_DEFERRED, stockage local chiffré.
2.3 Flux nominal C — Scellement et notification (pipeline backend existant + M12)¶
- Post-commit async : worker Merkle existant agrège leaf
SHA-256(payload_canonique_json). - Pipeline HSM → TSA → blockchain existant (PD-56/PD-55/PD-41).
- Garde
PENDING_SEAL → SEALED:signature_status='SIGNED' AND hsm_signature_ref IS NOT NULL(INV-103-08). - Push iOS à
SEALED: "Votre capture a été scellée avec succès. Preuve vérifiable disponible." (INV-103-12). SEALED → ANCHOR_CONFIRMEDaprès confirmation blockchain (INV-103-27).M12cron réconciliation (10 min) : scan non-terminaux, triggerSEAL_DELAYEDsi â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)¶
-
Transcodage implicite Expo —
expo-screen-capturepeut 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. -
SHA3-256 performance mobile —
@noble/hashes/sha3est pur JavaScript. Sur images > 100 MB, la latence risque de dépasser le SLA 3s. Mitigation : sireact-native-quick-cryptone supporte pas SHA3, envisager un module natif C++ via JSI. Mesurer en campagne perf TC-NOM-10. -
Zeroization DEK en JavaScript —
TypedArray.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. -
Upload multipart et abort —
AbortMultipartUploadS3 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). -
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_SEALindéfiniment. Documenter comme STUB avec stories de destination. -
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. -
Rate-limit granularité —
user_id + IPpeut être contourné par changement d'IP (mobile). Acceptable pour le quota initial (60 req/min). Surveiller en production. -
Sérialisation canonique JSON — La stabilité du fingerprint dépend de la sérialisation UTF-8 déterministe. Utiliser
JSON.stringifyavecreplacertriant les clés, PAS de bibliothèque tierce non contrôlée. Tester avec TC-INV-06 (4 variantes). -
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:transformIgnorePatternsexclutreact-native-quick-cryptomoduleNameMapperredirige 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).