Aller au contenu

PD-283 — Plan d'implémentation

1. Découpage en composants

C1 — ExportStateMachine (machine à états export)

Responsabilité : Gestion contractuelle des transitions d'état export (IDLE → … → READY / FAILED / DELETED).

  • Store Zustand dédié (useExportStore)
  • Transitions explicites avec garde (canTransition(from, to))
  • Toute transition non listée dans la table spec §5.5 est rejetée avec erreur
  • Garde purge_ok pour FAILED → PREPARING
  • Journalisation horodatée de chaque transition

Fichiers : src/export/state-machine.ts, src/store/useExportStore.ts

C2 — ExportApiClient (appel backend export)

Responsabilité : Appel POST /exports/complaint-file, validation structurelle de la réponse, gestion 403/422/erreurs.

  • Validation Zod du payload réponse (exportId, manifest, chronology, signedUrls, guideUrl, readmeVerification, rejectedProofs)
  • Validation regex contractuelle de exportId (^[A-Za-z0-9_-]{8,128}$)
  • Validation HTTPS stricte sur chaque signedUrl
  • Refresh d'URLs (nouvel appel API) en cas d'expiration

Fichiers : src/export/api-client.ts, src/export/schemas.ts

C3 — FileDownloader (téléchargement avec retry)

Responsabilité : Téléchargement séquentiel des artefacts chiffrés via URLs signées, avec retry borné.

  • Retry 3 tentatives max, backoff strict 1s/2s/4s
  • Détection expiration URL (403 en contexte download, distingué du 403 FREE qui n'arrive qu'à l'appel API initial) → délégation refresh à C2
  • Disambiguation 403 : le 403 FREE ne survient qu'au POST /exports/complaint-file (pas de exportId en réponse) ; le 403 URL survient lors du GET sur signedUrl (contexte download)
  • Écriture streaming sur disque (pas de buffer RAM complet)
  • Détection espace disque insuffisant avant téléchargement

Fichiers : src/export/file-downloader.ts

C4 — CryptoDecryptPipeline (déchiffrement Zero-Knowledge)

Responsabilité : Dérivation K_doc par preuve et déchiffrement AES-256-GCM local.

  • Dérivation : kmaster → K_share → K_doc(proofId) via modules existants (src/crypto/keys.ts)
  • Déchiffrement fichier chiffré → fichier clair transitoire sur disque
  • Zeroization immédiate de K_doc après usage
  • Protection K_doc en transit via Keychain/Data Protection (INV-283-13)
  • kmaster JAMAIS utilisée directement pour déchiffrer un payload (INV-283-11)

Fichiers : src/export/crypto-pipeline.ts

C5 — PvproofAssembler (assemblage ZIP streaming)

Responsabilité : Construction du conteneur .pvproof conforme RFC PV-PACK-001 en streaming disque.

  • pvproof.json en première entrée, mode STORED (sans compression)
  • Arborescence : preuves/, enveloppes/, fichiers racine
  • Insertion séquentielle : un seul fichier clair en mémoire à la fois (INV-283-02)
  • Passthrough strict pour manifest, chronology, enveloppes, README (INV-283-07)
  • Extension finale .pvproof exclusivement (INV-283-09)
  • Nommage : PV-Dossier-Plainte_{YYYY-MM-DD}_{exportId8}.pvproof

Fichiers : src/export/pvproof-assembler.ts

C6 — FilenameSanitizer (sanitization anti zip-slip)

Responsabilité : Neutralisation des noms de fichiers dangereux avant insertion dans l'archive.

  • Interdit .., /, \, caractères contrôle ASCII
  • Normalisation NFC (Unicode)
  • Gestion collision post-sanitization : suffixe incrémental _2, _3
  • Rejet si nom vide après sanitization → fichier dans rejectedFiles[]
  • Limite 120 chars

Fichiers : src/export/filename-sanitizer.ts

C7 — StreamingHasher (hash SHA3-256 streaming)

Responsabilité : Calcul du hash SHA3-256 final sur le conteneur .pvproof complet en streaming.

  • Lecture streaming du fichier final (pas de chargement RAM complet)
  • Sortie : 64 hex lowercase
  • Utilise @noble/hashes/sha3 (sha3_256) avec createSHA3_256() pour hashing incrémental

Fichiers : src/export/streaming-hasher.ts

C8 — TempFileManager (gestion fichiers temporaires)

Responsabilité : Création, suivi et suppression sécurisée de tous les fichiers temporaires.

  • Registre de tous les fichiers temporaires créés
  • Suppression immédiate après usage (INV-283-05)
  • Purge forcée en cas d'échec (ERR-08)
  • Vérification purge_ok avant retry (garde de FAILED → PREPARING)
  • Audit : aucun artefact clair résiduel hors .pvproof final (INV-283-01)

Fichiers : src/export/temp-file-manager.ts

C9 — NotificationScheduler (rappel 24h)

Responsabilité : Planification notification locale à T0+24h (±5min) après génération.

  • Utilise expo-notifications pour planification locale
  • Si permission refusée : flag pour bandeau in-app persistant
  • Valeur fixe 24h (V1, pas configurable)

Fichiers : src/export/notification-scheduler.ts

C10 — ExportOrchestrator (orchestrateur principal)

Responsabilité : Coordination séquentielle de tous les composants (C1-C9), émission progression.

  • Séquence : API call → validation → download → decrypt → assemble → hash → ready
  • Délégation refresh URL à C2 si expiration détectée
  • Gestion annulation utilisateur (transition vers IDLE + purge)
  • Émission événements de progression pour C11
  • Gestion rejet partiel (confirmation utilisateur via callback)
  • Contrôle taille export (warning 500-1024 MB, refus >1024 MB)

Fichiers : src/export/orchestrator.ts

C11 — ExportScreen (UI export)

Responsabilité : Écran de progression multi-étapes, actions post-export, gestion erreurs visuelles.

  • Progression : Téléchargement n/N, Déchiffrement n/N, Assemblage, Finalisation (spinner seul interdit)
  • Écran rejets partiels : liste + motifs + boutons Continuer/Annuler
  • Écran erreur : message cause + CTA retry/retour
  • Écran READY : hash copiable + boutons Partager/Sauvegarder/Supprimer
  • Bandeau in-app persistant si permission notification refusée
  • ERR-01 : message bloquant + CTA Premium
  • Bouton annulation disponible pendant DOWNLOADING/DECRYPTING/ASSEMBLING
  • Warning taille 500-1024 MB avec confirmation

Fichiers : src/screens/ExportScreen.tsx, src/components/export/ExportProgress.tsx, src/components/export/ExportActions.tsx, src/components/export/RejectedProofsList.tsx

2. Flux techniques

2.1 Flux nominal (F-01)

┌─────────────────┐
│  ExportScreen    │ 1. Utilisateur sélectionne proofIds[] et lance export
│  (C11)           │
└────────┬────────┘
         │ dispatch startExport(proofIds)
┌─────────────────┐
│ ExportApiClient │ 2. POST /exports/complaint-file { proofIds }
│  (C2)           │    ⚠️ Appel API AVANT toute transition d'état
│                 │    Si 403 → ERR-01 (message bloquant, reste IDLE, aucune transition)
│                 │    Si 422 → ERR-02 (rejet total, reste IDLE)
│                 │    Validation Zod réponse (exportId, manifest, signedUrls…)
│                 │    Si rejectedProofs[] non vide → callback UI (F-02)
│                 │    Distinction 403 FREE vs 403 URL expirée : header/body API
│                 │    (403 FREE = pas de champ exportId ; 403 URL = contexte download)
└────────┬────────┘
         │ réponse 200 validée
┌─────────────────┐
│ ExportOrchest.  │ 3. IDLE → PREPARING (C1) — uniquement après réponse 200 validée
│  (C10)          │
└────────┬────────┘
┌─────────────────┐
│ PREPARING →     │ 4. Transition DOWNLOADING (C1)
│ DOWNLOADING     │
└────────┬────────┘
         ▼  Pour chaque preuve valide (séquentiel) :
┌─────────────────┐
│ FileDownloader  │ 5a. Téléchargement fichier chiffré via signedUrl
│  (C3)           │     → fichier temporaire sur disque
│                 │     Retry 3x (1s/2s/4s) si échec réseau
│                 │     Si URL expirée → refresh via C2 (F-03)
└────────┬────────┘
         │ fichier chiffré téléchargé
┌─────────────────┐
│ DOWNLOADING →   │ 5b. Transition DECRYPTING (C1)
│ DECRYPTING      │
└────────┬────────┘
┌─────────────────┐
│ CryptoDecrypt   │ 5c. Dérivation K_doc = HKDF(K_share, proofId)
│ Pipeline (C4)   │     Déchiffrement AES-256-GCM → fichier clair transitoire
│                 │     Zeroize K_doc immédiatement
└────────┬────────┘
         │ fichier clair prêt
┌─────────────────┐
│ DECRYPTING →    │ 5d. Transition ASSEMBLING (C1)
│ ASSEMBLING      │
└────────┬────────┘
┌─────────────────┐
│ FilenameSanit.  │ 5e. Sanitization nom + gestion collision
│  (C6)           │
└────────┬────────┘
┌─────────────────┐
│ PvproofAssembl. │ 5f. Ajout dans preuves/{nom-sanitizé}
│  (C5)           │     Ajout enveloppe dans enveloppes/
└────────┬────────┘
┌─────────────────┐
│ TempFileMgr     │ 5g. Suppression immédiate fichier chiffré + clair transitoire
│  (C8)           │
└────────┬────────┘
         │ ← retour à 5a pour preuve suivante (ASSEMBLING → DOWNLOADING)
         ▼  Après toutes les preuves :
┌─────────────────┐
│ FileDownloader  │ 6a. Téléchargement guide_plainte_france.pdf via guideUrl
│  (C3)           │     (même mécanisme retry 3x que les preuves)
│                 │     → fichier temporaire sur disque
└────────┬────────┘
┌─────────────────┐
│ PvproofAssembl. │ 6b. Ajout artefacts racine :
│  (C5)           │    - pvproof.json (1ère entrée, STORED) — déjà écrit en premier (étape 3bis)
│                 │    - manifest.json (passthrough depuis réponse API)
│                 │    - chronology.json (passthrough depuis réponse API)
│                 │    - README_VERIFICATION.txt (passthrough depuis réponse API readmeVerification)
│                 │    - guide_plainte_france.pdf (passthrough depuis guideUrl téléchargé en 6a)
│                 │    Finalisation archive → .pvproof
└────────┬────────┘
┌─────────────────┐
│ ASSEMBLING →    │ 7. Transition HASHING (C1)
│ HASHING         │
└────────┬────────┘
┌─────────────────┐
│ StreamingHasher │ 8. SHA3-256 streaming sur .pvproof complet
│  (C7)           │    → hash 64 hex lowercase
└────────┬────────┘
┌─────────────────┐
│ HASHING → READY │ 9. Transition READY (C1)
│ NotifScheduler  │    Planification notification 24h (C9)
│  (C9)           │    Si permission refusée → flag bandeau in-app
└────────┬────────┘
┌─────────────────┐
│  ExportScreen   │ 10. Affichage hash copiable + Partager / Sauvegarder / Supprimer
│  (C11)          │
└─────────────────┘

Note sur l'ordre d'insertion dans l'archive : pvproof.json doit être la première entrée (INV-283-08). L'assembleur commence par écrire pvproof.json en STORED, puis insère les preuves et enveloppes au fil du flux, puis les artefacts racine restants. L'astuce : pvproof.json est généré avec des métadonnées connues dès l'appel API (exportId, proofIds, date), donc écrit en premier avant le téléchargement.

Correction séquence : pvproof.json est écrit en premier (avant la boucle de téléchargement), les preuves et enveloppes sont ajoutées en streaming, puis les artefacts racine (manifest, chronology, README, guide) sont ajoutés en dernier.

2.2 Flux rejet partiel (F-02)

ExportApiClient (C2) reçoit rejectedProofs[] non vide
ExportOrchestrator (C10) invoque callback UI
ExportScreen (C11) affiche liste rejets + motifs
   ┌─────┴──────┐
   │             │
   ▼             ▼
Continuer      Annuler
(sous-ensemble)  (→ IDLE)
Reprise F-01 avec proofIds filtrés (valides uniquement)

2.3 Flux refresh URL (F-03)

FileDownloader (C3) détecte 403/expiration
ExportOrchestrator (C10) demande refresh à C2
ExportApiClient (C2) nouvel appel POST /exports/complaint-file
Mise à jour signedUrls[] dans le contexte d'export
Reprise téléchargement au prochain artefact non finalisé
(aucune duplication d'entrée archive déjà validée)

2.4 Diagrammes Mermaid

Graphe de dépendances inter-composants

graph TD
    C11[C11 — ExportScreen<br/>UI export] -->|dispatch startExport| C10
    C10[C10 — ExportOrchestrator<br/>orchestrateur principal] -->|appel API| C2[C2 — ExportApiClient<br/>appel backend]
    C10 -->|transitions état| C1[C1 — ExportStateMachine<br/>machine à états]
    C10 -->|téléchargement| C3[C3 — FileDownloader<br/>download + retry]
    C10 -->|déchiffrement| C4[C4 — CryptoDecryptPipeline<br/>Zero-Knowledge]
    C10 -->|assemblage ZIP| C5[C5 — PvproofAssembler<br/>conteneur .pvproof]
    C10 -->|hash final| C7[C7 — StreamingHasher<br/>SHA3-256]
    C10 -->|notification 24h| C9[C9 — NotificationScheduler<br/>rappel local]
    C10 -->|gestion temporaires| C8[C8 — TempFileManager<br/>purge sécurisée]
    C3 -->|refresh URL expirée| C2
    C5 -->|sanitization noms| C6[C6 — FilenameSanitizer<br/>anti zip-slip]
    C4 -->|dérivation K_doc| CRYPTO[src/crypto/keys.ts<br/>module existant]
    C2 -->|HTTP client| API[src/services/api.ts<br/>module existant]
    C10 -->|lecture proofIds| VAULT[useVaultStore<br/>store existant]

    classDef new fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
    classDef existing fill:#f3e5f5,stroke:#7b1fa2,stroke-width:1px,stroke-dasharray:5
    class C1,C2,C3,C4,C5,C6,C7,C8,C9,C10,C11 new
    class CRYPTO,API,VAULT existing

Diagramme de séquence — Flux nominal (F-01)

sequenceDiagram
    actor User
    participant Screen as C11 — ExportScreen
    participant Orch as C10 — ExportOrchestrator
    participant SM as C1 — ExportStateMachine
    participant ApiClient as C2 — ExportApiClient
    participant Backend as Backend API (PD-85)
    participant Downloader as C3 — FileDownloader
    participant Crypto as C4 — CryptoDecryptPipeline
    participant Sanitizer as C6 — FilenameSanitizer
    participant Assembler as C5 — PvproofAssembler
    participant TempMgr as C8 — TempFileManager
    participant Hasher as C7 — StreamingHasher
    participant Notif as C9 — NotificationScheduler

    User->>Screen: Sélectionne proofIds[], lance export
    Screen->>Orch: startExport(proofIds)
    Orch->>ApiClient: POST /exports/complaint-file
    ApiClient->>Backend: HTTP POST { proofIds }
    Backend-->>ApiClient: 200 { exportId, manifest, signedUrls, ... }
    ApiClient-->>Orch: réponse validée (Zod)

    Orch->>SM: transition(IDLE → PREPARING)
    SM-->>Orch: OK
    Orch->>SM: transition(PREPARING → DOWNLOADING)

    loop Pour chaque preuve (séquentiel)
        Orch->>Downloader: download(signedUrl)
        Downloader-->>TempMgr: enregistre fichier chiffré
        Downloader-->>Orch: fichier chiffré prêt

        Orch->>SM: transition(DOWNLOADING → DECRYPTING)
        Orch->>Crypto: deriveKDoc + decrypt(fichier chiffré)
        Crypto-->>TempMgr: enregistre fichier clair transitoire
        Crypto-->>Orch: fichier clair prêt

        Orch->>SM: transition(DECRYPTING → ASSEMBLING)
        Orch->>Sanitizer: sanitize(filename)
        Sanitizer-->>Orch: nom sûr
        Orch->>Assembler: addEntry(nom sûr, fichier clair)

        Orch->>TempMgr: release(chiffré + clair)
        Note over TempMgr: Suppression immédiate (INV-283-05)

        Orch->>SM: transition(ASSEMBLING → DOWNLOADING)
    end

    Orch->>Downloader: download(guideUrl)
    Orch->>Assembler: addRootArtifacts(manifest, chronology, README, guide)
    Orch->>Assembler: finalize() → fichier .pvproof

    Orch->>SM: transition(ASSEMBLING → HASHING)
    Orch->>Hasher: sha3_256_streaming(.pvproof)
    Hasher-->>Orch: hash 64 hex lowercase

    Orch->>SM: transition(HASHING → READY)
    Orch->>Notif: scheduleNotification(T0 + 24h)
    Orch-->>Screen: { hash, pvproofPath }
    Screen-->>User: Hash copiable + Partager / Sauvegarder / Supprimer

Diagramme de séquence — Flux refresh URL expirée (F-03)

sequenceDiagram
    participant Downloader as C3 — FileDownloader
    participant Orch as C10 — ExportOrchestrator
    participant ApiClient as C2 — ExportApiClient
    participant Backend as Backend API (PD-85)

    Downloader->>Downloader: GET signedUrl → 403 (contexte download)
    Downloader-->>Orch: URL_EXPIRED

    Orch->>ApiClient: refresh URLs
    ApiClient->>Backend: POST /exports/complaint-file (nouvel appel)
    Backend-->>ApiClient: 200 { nouvelles signedUrls }
    ApiClient-->>Orch: URLs mises à jour

    Orch->>Downloader: reprise au prochain artefact non finalisé
    Note over Downloader: Aucune duplication d'entrée déjà validée

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-283-01 Aucun clair hors .pvproof final TempFileManager registre + suppression immédiate + audit post-export C8, C10 Scan sandbox : 0 fichier clair résiduel Échec suppression OS → ERR-08 bloquant
INV-283-02 Streaming disque, 1 clair en mémoire max PvproofAssembler ajoute entrée par entrée ; CryptoDecryptPipeline écrit en fichier disque puis lecture streaming C5, C4 Profil mémoire pic < 50MB Lib ZIP qui bufferise tout en RAM
INV-283-03 Hash SHA3-256 streaming sur conteneur final StreamingHasher avec sha3_256.create() en lecture par chunks (64KB) C7 Hash 64 hex lowercase recalculable indépendamment Chunk trop gros → pic RAM
INV-283-04 Notification locale 24h ±5min, bandeau si refusé NotificationScheduler via expo-notifications ; flag notificationDenied dans store C9, C11 Notification à T0+24h ±5min OU bandeau visible Expo Notifications API change
INV-283-05 Suppression immédiate temporaires TempFileManager.release(fileId) appelé dès que fichier n'est plus nécessaire C8 Audit horodaté : delta création/suppression < 1s Race condition si crash mid-export
INV-283-06 Retry 3 max, backoff ½/4s FileDownloader compteur + délais constants [1000, 2000, 4000] C3 Logs horodatés montrent exactement 3 tentatives Tentative #4 accidentelle
INV-283-07 Passthrough strict artefacts backend PvproofAssembler copie binaire sans transformation (bytes identiques) C5 Comparaison SHA3-256 source vs entrée archive Encodage/BOM ajouté involontairement
INV-283-08 pvproof.json 1ère entrée STORED PvproofAssembler écrit pvproof.json en premier avec méthode STORE (compression=0) C5 Inspection ZIP central directory : offset 0, method=STORED Lib ZIP qui trie les entrées
INV-283-09 Extension .pvproof exclusivement Nommage via regex contractuelle dans ExportOrchestrator C10 Nom final vérifié par regex ^PV-Dossier-Plainte_…\.pvproof$ Renommage par OS/framework
INV-283-10 Sanitization anti zip-slip FilenameSanitizer : interdit .., /, \, contrôles ASCII ; NFC ; collision _N C6 Noms archivés vérifiés post-insertion Caractère unicode invisible non détecté
INV-283-11 K_doc uniquement, jamais kmaster direct CryptoDecryptPipeline dérive K_share → K_doc via deriveKDocFromMaster() ; assertion : aucune opération crypto avec kmaster sur payload C4 Traces : deriveKDoc appelé, pas decrypt(kmaster, …) Raccourci développeur
INV-283-12 Transitions non listées interdites ExportStateMachine.transition(to) : lookup table exhaustive, rejet avec erreur si transition hors table C1 Journal transitions : aucune transition invalide Oubli d'une transition dans la table
INV-283-13 Secrets temporaires protégés au repos K_doc stockée en mémoire volatile uniquement, zeroize après usage ; fichiers temp dans directory protégé iOS Data Protection C4, C8 Audit : aucun secret en clair persistant en storage durable Expo FileSystem hors sandbox protégée

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

Critère ID Mécanisme(s) Composant Observable Risque
CA-01 Appel API POST /exports/complaint-file avec validation Zod du payload + réponse C2 Requête sortante avec proofIds[], réponse parsée sans erreur Schéma API PD-85 change
CA-02 Tableau backoff [1000, 2000, 4000], compteur tentatives, arrêt strict à 3 C3 Logs horodatés : 3 tentatives max, délais conformes
CA-03 Détection 403/expiration → refresh API → reprise index courant (pas de duplication) C2, C3, C10 Nouvelle requête API visible + reprise sans crash Double insertion si index mal géré
CA-04 deriveKDocFromMaster(kmaster, proofId)decrypt(kDoc, ciphertext) ; assertion kmaster pas en argument decrypt C4 Traces crypto : deriveKDoc présent, aucun decrypt(kmaster…)
CA-05 ZIP streaming (ajout entrée par entrée) + déchiffrement fichier par fichier sur disque C5, C4 Profil mémoire : pic < 50MB sur benchmark 50 preuves/200MB
CA-06 pvproof.json ajouté en premier avec { compress: false } (STORED) C5 Inspection ZIP : 1ère entrée = pvproof.json, method=0
CA-07 Insertion preuves/, enveloppes/, fichiers racine conformément au RFC C5 Listing archive : structure conforme
CA-08 StreamingHasher → SHA3-256 hex 64 chars lowercase C7, C11 Hash affiché = 64 hex [a-f0-9]{64}, bouton copie fonctionnel
CA-09 Regex nommage dans orchestrateur : PV-Dossier-Plainte_{date}_{exportId8}.pvproof C10 Nom fichier final vérifié
CA-10 Émission d'événements { step, current, total } par l'orchestrateur → UI multi-étapes C10, C11 UI montre étapes + compteurs n/N ; spinner seul absent
CA-11 Callback onRejectedProofs(list) → UI liste + motifs + boutons Continuer/Annuler C10, C11 Liste rejets visible, choix utilisateur respecté
CA-12 expo-notifications.scheduleNotificationAsync à T0+24h ; si permission refusée → notificationDenied=true → bandeau C9, C11 Notification à T0+24h ±5min OU bandeau persistant visible
CA-13 TempFileManager purge totale + audit final C8 Scan sandbox post-export : 0 artefact clair résiduel
CA-14 FilenameSanitizer : interdit ../, /, \, contrôles ; collision _N C6 Noms dangereux neutralisés dans archive
CA-15 Architecture streaming + séquentiel optimisé C3, C4, C5, C10 Benchmark 12 preuves/50MB : P95 < 30s (iPhone 12, Wi-Fi) Réseau réel variable
CA-16 Idem + vérification absence goulot d'étranglement sur volume C3, C4, C5, C10 Benchmark 50 preuves/200MB : P95 < 60s (iPhone 12, Wi-Fi)

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 F-01, CA-01, CA-07, CA-08, CA-09 C2 (API), C5 (assembler), C7 (hash), C10 (nommage) Requête sortante + archive structure + hash + nom final Integration
TC-NOM-02 F-02, CA-11 C2 (rejectedProofs parsing), C10 (callback), C11 (UI) Liste rejets affichée + archive 8 preuves Integration
TC-NOM-03 F-02, ERR-02 C2 (422 handling), C1 (→ IDLE) Aucun assemblage, message explicite, état IDLE Unit
TC-NOM-04 F-03, CA-03 C3 (détection expiration), C2 (refresh), C10 (reprise) Nouvelle requête API + pas de duplication archive Integration
TC-NOM-05 F-04, CA-10 C10 (événements progression), C11 (affichage) Étapes visibles avec compteurs n/N Unit (C10) + UI (C11)
TC-NOM-06 F-01 étape 8, READY/DELETED C1 (transitions), C11 (actions) Partager/Sauvegarder/Supprimer fonctionnels, DELETED → IDLE Integration
TC-NOM-07 §5.5, annulation C1 (→ IDLE), C8 (purge) Transition IDLE + 0 temporaire résiduel Integration
TC-NOM-08 F-01 étape 5, RFC PV-PACK-001 C5 (passthrough), C10 (artefacts racine) guide + README présents, binaire identique Unit
TC-INV-01 INV-283-01 C8 (purge), C10 (audit) Scan sandbox : 0 clair hors .pvproof Integration
TC-INV-02 INV-283-02 C5 (streaming), C4 (file-based decrypt) Profil mémoire pic < 50MB Perf
TC-INV-03 INV-283-03 C7 (streaming hash) Hash recalculable = hash affiché Unit
TC-INV-04 INV-283-04 C9 (notification), C11 (bandeau) Notif planifiée OU bandeau visible Integration
TC-INV-05 INV-283-05 C8 (registre + suppression immédiate) Delta création/suppression < 1s Unit
TC-INV-06 INV-283-06 C3 (retry borné) Logs : 3 tentatives, backoff ½/4s Unit
TC-INV-07 INV-283-07 C5 (passthrough copie binaire) SHA3-256(source) === SHA3-256(entrée archive) Unit
TC-INV-08 INV-283-08 C5 (pvproof.json en premier, STORED) Inspection ZIP central directory Unit
TC-INV-09 INV-283-09 C10 (nommage regex) Nom conforme regex contractuelle Unit
TC-INV-10 INV-283-10 C6 (sanitizer) Noms dangereux neutralisés + collision Unit
TC-INV-11 INV-283-11 C4 (K_doc seule) Traces : aucun decrypt(kmaster, payload) Unit
TC-INV-12 INV-283-12 C1 (transitions gardées) Transition interdite → erreur Unit
TC-INV-13 INV-283-13 C4 (zeroize), C8 (dir protégé) Aucun secret persistant en clair Integration
TC-ERR-01 ERR-01 C2 (403 handling), C11 (message bloquant) Aucun cycle PREPARING, message CTA Premium Unit
TC-ERR-02 ERR-03 C3 (expiration), C2 (refresh) Refresh API déclenché, pas de corruption Integration
TC-ERR-03 ERR-04 C3 (404/410 handling), C1 (→ FAILED) Abort + message cause + preuve concernée Unit
TC-ERR-04 ERR-05, CA-02 C3 (retry strict) 3 tentatives, backoff, prompt "Réessayer ?" Unit
TC-ERR-05 ERR-06 C2 (validation Zod), C10 (abort) Abort + motif artefact corrompu Unit
TC-ERR-06 ERR-07 C3/C10 (check espace disque) Abort + message capacité Integration
TC-ERR-07 ERR-08 C8 (échec suppression) FAILED sécurité, retry bloqué Unit
TC-ERR-08 §3.3 exportId C2 (validation regex exportId) Abort + "identifiant export invalide" Unit
TC-ERR-09 §3.3 signedUrl C2 (validation HTTPS stricte) URL rejetée, refresh obligatoire Unit
TC-ERR-10 §3.4 taille C10 (contrôle taille, warning/refus) Warning 500-1024MB, refus >1024MB Unit
TC-NEG-01 proofId non UUID C2 (validation pré-appel) Aucun trafic sortant, message utilisateur Unit
TC-NEG-02 ../../manifest.json C6 (sanitizer) Nom neutralisé, pas d'écrasement Unit
TC-NEG-03 Caractères contrôle ASCII C6 (sanitizer) Remplacement ou rejet Unit
TC-NEG-04 Collision post-sanitization C6 (suffixe _2, _3) Deux entrées distinctes dans archive Unit
TC-NEG-05 integrityHash uppercase/≠64 C2 (validation regex) Export invalidé + abort Unit
TC-NEG-06 .pvproof > 1024MB C10 (contrôle taille) Refus export Unit
TC-NEG-07 READY → DOWNLOADING C1 (transition gardée) Transition refusée Unit
TC-NEG-08 Secret en clair persistant C8 (audit), C4 (zeroize) FAILED sécurité + purge Sec
TC-NR-01 Format nom final C10 (regex) Nom conforme regex Unit
TC-NR-02 Structure archive RFC C5 (assembleur) Arborescence et ordre entrées Unit
TC-NR-03 Politique retry C3 (retry borné) ½/4s et max 3 stables Unit
TC-NR-04 Machine à états C1 (transitions) Aucune transition interdite Unit
TC-NR-05 Perf 12 preuves/50MB C3+C4+C5+C10 P95 < 30s Perf
TC-NR-06 Perf 50 preuves/200MB C3+C4+C5+C10 P95 < 60s Perf
TC-NR-07 Pic mémoire C5 (streaming) Pic < 50MB Perf
TC-NR-08 Purge temporaires C8 (audit) 0 résiduel post-run Sec

6. Gestion des erreurs

Erreur Condition Composant détecteur Traitement Observable
ERR-01 API retourne 403 (plan FREE) C2 Transition → IDLE (pas de PREPARING), message bloquant + CTA Premium Aucun trafic réseau post-403, UI bloquante
ERR-02 API retourne 422 (100% rejetées) C2 Transition → IDLE, message explicite, aucun assemblage 0 fichier créé
ERR-03 URL signée expirée (403/401 spécifique) C3 Délégation refresh → C2 → nouvel appel API → mise à jour URLs → reprise Nouvelle requête API visible
ERR-04 404/410 fichier introuvable C3 Transition → FAILED, message cause + preuve concernée Pas de hash final, pas de .pvproof
ERR-05 Timeout/réseau transitoire C3 Retry 1s/2s/4s puis prompt "Réessayer ?" via C11 3 tentatives horodatées dans logs
ERR-06 Hash/format invalide artefact C2 Abort export → FAILED, message artefact corrompu Validation Zod échoue sur le champ
ERR-07 Espace disque insuffisant C10 Vérification avant assemblage (FileSystem.getFreeDiskStorageAsync()), abort si insuffisant Message capacité requise
ERR-08 Échec suppression temporaire C8 Export → FAILED sécurité, purge forcée obligatoire avant retry, garde purge_ok canTransition(FAILED, PREPARING) bloqué tant que purge_ok === false
ERR-09 Nom non sanitizable (vide après) C6 Fichier rejeté → rejectedFiles[], export continue avec les restants Motif dans rejectedFiles

Codes d'erreur internes :

Code Signification Mapping
EXPORT_FORBIDDEN 403 plan FREE ERR-01
EXPORT_ALL_REJECTED 422 toutes preuves rejetées ERR-02
URL_EXPIRED URL signée expirée ERR-03
ARTIFACT_NOT_FOUND 404/410 artefact backend ERR-04
NETWORK_ERROR Timeout/réseau après 3 retries ERR-05
ARTIFACT_CORRUPT Hash/format invalide ERR-06
DISK_FULL Espace insuffisant ERR-07
TEMP_CLEANUP_FAILED Suppression temporaire échouée ERR-08
FILENAME_REJECTED Nom non sanitizable ERR-09
INVALID_EXPORT_ID exportId hors regex TC-ERR-08
INVALID_URL_SCHEME signedUrl non HTTPS TC-ERR-09
EXPORT_TOO_LARGE >1024 MB TC-ERR-10
INVALID_TRANSITION Transition d'état interdite INV-283-12

7. Impacts sécurité

7.1 Zero-Knowledge

  • K_doc uniquement (INV-283-11) : La fonction deriveKDocFromMaster() existante dans src/crypto/keys.ts assure la dérivation complète kmaster → K_share → K_doc. Aucune opération de déchiffrement ne doit accepter kmaster en argument direct.
  • Zeroization (INV-283-13) : Utiliser zeroize() de src/crypto/zeroize.ts sur les Uint8Array de clés dès que possible. Les strings hex sont immutables en JS — limitation connue et acceptée (cf. commentaire existant dans keys.ts:167).

7.2 Anti zip-slip (INV-283-10)

  • Attaque : Un nom de fichier comme ../../manifest.json pourrait écraser des fichiers critiques lors de l'extraction.
  • Mitigation : FilenameSanitizer supprime .., /, \, caractères contrôle ASCII, normalise NFC, tronque à 120 chars.
  • Observable : Test TC-NEG-02, TC-NEG-03, TC-NEG-04.

7.3 Protection fichiers temporaires

  • Répertoire : Utiliser FileSystem.cacheDirectory (protégé par iOS Data Protection quand l'appareil est verrouillé).
  • Durée de vie : Chaque fichier temporaire est tracké dans TempFileManager et supprimé immédiatement après consommation.
  • Crash recovery : Au démarrage de l'app, TempFileManager.purgeStale() supprime tout résidu de la session précédente.

7.4 Validation d'entrées

  • proofIds : Validation UUID v4 côté client avant appel API (TC-NEG-01).
  • exportId : Validation regex ^[A-Za-z0-9_-]{8,128}$.
  • signedUrls : Validation schéma HTTPS stricte (TC-ERR-09).
  • integrityHash : Regex ^[a-f0-9]{64}$ (lowercase strict, TC-NEG-05).

7.5 Réseau

  • HTTPS uniquement : Toute URL non-HTTPS est rejetée (pas de fallback HTTP).
  • Retry borné : 3 tentatives max, pas de boucle infinie.
  • Pas de persistance de credentials en dehors du secure store existant.

8. Hypothèses techniques

ID Hypothèse Impact si faux Mitigation
HT-01 expo-file-system supporte la lecture/écriture streaming par chunks sur iOS Impossible de respecter INV-283-02 (pic mémoire < 50MB) Fallback : utiliser react-native-blob-util ou module natif pour I/O streaming
HT-02 Une librairie ZIP compatible React Native supporte l'ajout séquentiel d'entrées en streaming sans tout bufferiser en RAM INV-283-02 compromise Évaluer react-native-zip-archive ou archiver (via polyfill Node streams) ; sinon module natif
HT-03 @noble/hashes/sha3 fonctionne avec createSHA3_256() pour hashing incrémental en React Native (Hermes) Hash streaming impossible ; fallback hash sur buffer complet > RAM Tester en early spike ; fallback expo-crypto
HT-04 L'API backend PD-85 retourne le payload exact tel que documenté dans la spec (exportId, manifest, chronology, signedUrls, guideUrl, readmeVerification, rejectedProofs) Parsing/validation échoue, export non fonctionnel Ajouter mode strict Zod avec .passthrough() pour les champs inconnus
HT-05 expo-notifications supporte scheduleNotificationAsync avec trigger { seconds: 86400 } sur iOS en foreground Notification à 24h non planifiable Fallback : react-native-push-notification ou calcul timer + rappel à prochaine ouverture
HT-06 Les fichiers chiffrés du backend sont au format AES-256-GCM avec structure { ciphertext, nonce, tag } compatible avec src/crypto/aes-gcm.ts:decrypt() Impossible de déchiffrer → export bloqué Vérifier format exact avec PD-85, adapter le parser si nécessaire
HT-07 FileSystem.getFreeDiskStorageAsync() retourne une estimation fiable sur iOS Faux positifs/négatifs sur contrôle espace disque (ERR-07) Marge de sécurité (taille estimée × 1.5)

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

9.1 Risque critique — Librairie ZIP streaming

Problème : Aucune librairie ZIP mature et compatible React Native ne garantit l'insertion séquentielle d'entrées en streaming disque sans buffer RAM complet. Les solutions connues (react-native-zip-archive, jszip) chargent tout en mémoire.

Mitigation : 1. Spike obligatoire (priorité maximale) : Évaluer archiver + polyfill Node streams (Hermes compatible ?) ou fflate (streaming mode). 2. Si aucune solution JS pure ne fonctionne : module natif iOS (Foundation.Archive / ZIPFoundation) exposé via Expo Module. 3. Le choix doit être validé AVANT l'implémentation des autres composants.

9.2 Risque moyen — Performance réseau

Les benchmarks P95 (CA-15, CA-16) dépendent du réseau réel. En Wi-Fi stable c'est atteignable, mais le TTL de 30 min des URLs signées combiné avec un réseau lent pourrait forcer des refreshs fréquents.

9.3 Risque moyen — Crash mid-export

Un crash de l'app pendant l'assemblage laisse des fichiers temporaires. Le TempFileManager.purgeStale() au démarrage couvre ce cas, mais il faut s'assurer que le registre des temporaires est persisté (AsyncStorage) pour survivre au crash.

9.4 Piège — pvproof.json en première entrée

Le format ZIP exige que l'entrée soit ajoutée séquentiellement. Si la librairie trie les entrées par nom ou chemin, pvproof.json ne sera pas en position 0. Vérifier explicitement l'ordre dans le central directory après assemblage.

9.5 Piège — Strings immutables en JavaScript

Les clés hex (K_doc) en string ne peuvent pas être zéroïsées efficacement en JS. La mitigation existante (commentaire keys.ts:167) est connue et acceptée. Minimiser le temps de vie des strings clé : dériver, utiliser, oublier la référence.

9.6 Dette — Validation structurelle manifest/chronology

La spec PD-283 se limite à valider JSON + champs de premier niveau. Les schémas détaillés PD-85 ne sont pas fournis. Accepté comme dette (cf. règle non testable §9 spec).

9.7 Piège — Sandbox Expo FileSystem

FileSystem.cacheDirectory est purgé par l'OS sous pression mémoire. Les fichiers temporaires de l'export peuvent disparaître en cours de route. Utiliser FileSystem.documentDirectory pour le .pvproof final uniquement, et cacheDirectory pour les temporaires (acceptable car ils sont éphémères).

10. Hors périmètre

  • Android : Exclu V1 (iOS uniquement).
  • Web/PWA : Exclu.
  • Export en background : Exclu (foreground uniquement).
  • Chiffrement du conteneur .pvproof : Le conteneur final n'est pas chiffré.
  • Outil CLI pv verify : Hors scope.
  • Enregistrement MIME OS : Pas de type MIME dédié enregistré.
  • Génération PDF timeline : Pas de PDF riche généré côté app.
  • Modification artefacts backend : Interdit, passthrough strict.
  • Schémas JSON détaillés PD-85 : Validation limitée (JSON valide + champs 1er niveau).

Périmètre de test

Niveau de test In scope Hors scope (justification)
Unitaire Tous les composants C1-C10 : state machine, sanitizer, hasher, retry, validation Zod, nommage, API client (mocké)
Intégration Interactions C2↔C3 (download+retry+refresh), C4↔C5 (decrypt→assemble), C8↔C10 (purge), C1↔C10 (transitions orchestrées) Interaction avec backend réel (PD-85 non déployé en test local) — mockée
E2E Flux complet depuis sélection preuves jusqu'à .pvproof généré, avec API mockée E2E sur device physique avec backend réel : nécessite env de staging PD-85 déployé — hors périmètre PD-283
Perf Benchmarks TC-NR-05, TC-NR-06, TC-NR-07 avec fichiers synthétiques Benchmarks réseau réel (variable)
Sécurité TC-NEG-01 à TC-NEG-08, TC-INV-01, TC-INV-05, TC-INV-11, TC-INV-13 Pentest complet app mobile

Justification hors scope E2E device réel : Le backend PD-85 (POST /exports/complaint-file) n'est pas garanti déployé en environnement de test au moment de l'implémentation PD-283. L'intégration E2E sera validée dans un ticket dédié post-déploiement PD-85 en staging.

Tous les niveaux de test intra-story sont couverts. L'intégration avec le backend réel est la seule exclusion justifiée.

Mécanismes cross-module

Aucune modification d'autres modules. PD-283 est un module autonome côté app (src/export/) qui consomme : - Le module crypto existant (src/crypto/) en lecture seule (appels deriveKDocFromMaster, decrypt) - Le service API existant (src/services/api.ts) pour l'appel HTTP - Le store vault existant (src/store/useVaultStore.ts) en lecture seule (accès aux proofIds)

Aucun guard, middleware ou intercepteur n'est ajouté sur des routes d'autres modules.

Contraintes techniques

Runner de tests

  • Framework : Jest (déjà configuré dans ProbatioVault-app, jest.config.js existant)
  • Compatibilité : ESM via @jest/globals + transformateur Babel (Hermes)

Dépendances inter-PD

Dépendance Statut Impact
PD-85 (backend export API) EN COURS API mockée pour PD-283, intégration E2E post-staging PD-85
PD-34 (key derivation kmaster) DONE Module src/crypto/keys.ts existant, K_doc dérivable
PD-242 (recovery envelope) DONE Enveloppes fournies par backend, passthrough côté app
PD-282 (ProofEnvelope seal eIDAS) DONE Enveloppes fournies par backend, passthrough côté app

Compatibilité ESM/CJS

  • @noble/hashes : ESM natif, compatible Hermes via Metro bundler
  • zod : ESM + CJS dual, pas de problème
  • expo-notifications, expo-file-system : CJS standard Expo

Variables CI

  • CI=true pour désactiver les interactions utilisateur Jest
  • Pas de DATABASE_URL (app mobile, pas de DB)
  • Tests perf sur simulateur iOS (pas de device physique en CI)

Règle exportId8 padding

  • exportId8 = 8 premiers caractères alphanumériques (filtrés [A-Za-z0-9]) de exportId
  • Si moins de 8 alphanumériques disponibles : compléter par des 0 à droite
  • Implémenté dans ExportOrchestrator.deriveExportId8(exportId: string): string