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_okpourFAILED → 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 deexportIden réponse) ; le 403 URL survient lors du GET sursignedUrl(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.jsonen 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
.pvproofexclusivement (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) aveccreateSHA3_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_okavant retry (garde deFAILED → PREPARING) - Audit : aucun artefact clair résiduel hors
.pvprooffinal (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-notificationspour 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échiffrementn/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 danssrc/crypto/keys.tsassure la dérivation complètekmaster → K_share → K_doc. Aucune opération de déchiffrement ne doit accepter kmaster en argument direct. - Zeroization (INV-283-13) : Utiliser
zeroize()desrc/crypto/zeroize.tssur les Uint8Array de clés dès que possible. Les strings hex sont immutables en JS — limitation connue et acceptée (cf. commentaire existant danskeys.ts:167).
7.2 Anti zip-slip (INV-283-10)¶
- Attaque : Un nom de fichier comme
../../manifest.jsonpourrait écraser des fichiers critiques lors de l'extraction. - Mitigation :
FilenameSanitizersupprime..,/,\, 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
TempFileManageret 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.jsexistant) - 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 bundlerzod: ESM + CJS dual, pas de problèmeexpo-notifications,expo-file-system: CJS standard Expo
Variables CI¶
CI=truepour 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]) deexportId- Si moins de 8 alphanumériques disponibles : compléter par des
0à droite - Implémenté dans
ExportOrchestrator.deriveExportId8(exportId: string): string