PD-101 — Plan d'implémentation
1. Découpage en composants
| # | Composant | Responsabilité | Fichiers | Dépendances |
| C1 | upload-types | Types partagés, branded types (DocId, Sha3Hash), enums d'états, DTOs | src/types/upload.ts | — |
| C2 | upload-purge | purgeStale() au démarrage du flux, suppression artefacts sensibles sur disque, nullification références mémoire | src/services/upload/uploadPurge.ts | C1 |
| C3 | upload-crypto | Pipeline crypto : SHA3-256 hash, AES-256-GCM FILE-LEVEL encryption, vérification intégrité (recalcul), dérivation clé document via HKDF | src/services/upload/uploadCrypto.ts | C1, crypto.ts existant, hkdf.ts existant |
| C4 | upload-network | Couche réseau : initialisation backend, URL pré-signées, upload simple, upload multipart (chunks du ciphertext), AbortMultipartUpload, finalisation | src/services/upload/uploadNetwork.ts | C1, api.ts existant |
| C5 | upload-retry | Stratégie retry : backoff exponentiel (½/4s) + jitter, retry par chunk (multipart) ou global (simple), compteur max 3 | src/services/upload/uploadRetry.ts | C1 |
| C6 | upload-progress | Calcul progression : pourcentage, ETA, vitesse instantanée, agrégation multi-chunk | src/services/upload/uploadProgress.ts | C1 |
| C7 | upload-cancel | Orchestration annulation : stop client, abort S3 (best-effort), transition backend CANCELLED, purge artefacts | src/services/upload/uploadCancel.ts | C1, C2, C4 |
| C8 | upload-store | Store Zustand : état UI (UPLOADING/FAILED/COMPLETED/CANCELLED), machine à états avec transitions strictes, persistance progression | src/store/useUploadStore.ts | C1, zustand |
| C9 | upload-orchestrator | Flux principal : sélection → validation taille → disclaimer EXIF → hash → chiffrement → vérification intégrité → init backend → upload → finalisation. Orchestre C2-C8. | src/services/upload/uploadOrchestrator.ts | C1-C8 |
| C10 | upload-background | Gestion background iOS : expo-task-manager / URLSession, notification locale unique au retour foreground | src/services/upload/uploadBackground.ts | C1, C8, C9 |
| C11 | upload-audit | Émission événements audit : consentement optimisé, confirmation EXIF, transitions d'état, purgeStale, annulation, erreurs | src/services/upload/uploadAudit.ts | C1, auditService.ts existant |
| C12 | upload-ui | Écrans et composants : sélection fichier, disclaimer EXIF, avertissement transcodage, consentement optimisé, progression, annulation | src/screens/vault/UploadDocumentScreen.tsx (update), src/components/upload/ (create) | C1, C8, C9 |
| C13 | upload-tests | Tests unitaires et intégration pour tous les composants | src/__tests__/services/upload/, src/__tests__/store/useUploadStore.test.ts | C1-C12 |
Arborescence cible
src/
types/
upload.ts (C1 — types partagés)
services/
upload/
index.ts (barrel export)
uploadPurge.ts (C2)
uploadCrypto.ts (C3)
uploadNetwork.ts (C4)
uploadRetry.ts (C5)
uploadProgress.ts (C6)
uploadCancel.ts (C7)
uploadOrchestrator.ts (C9)
uploadBackground.ts (C10)
uploadAudit.ts (C11)
store/
useUploadStore.ts (C8)
screens/vault/
UploadDocumentScreen.tsx (C12 — update existant)
components/upload/
ExifDisclaimerSheet.tsx (C12)
TranscodingWarningSheet.tsx (C12)
OptimizedConsentSheet.tsx (C12)
UploadProgressBar.tsx (C12)
UploadStatusBadge.tsx (C12)
__tests__/
services/upload/
uploadPurge.test.ts (C13)
uploadCrypto.test.ts (C13)
uploadNetwork.test.ts (C13)
uploadRetry.test.ts (C13)
uploadProgress.test.ts (C13)
uploadCancel.test.ts (C13)
uploadOrchestrator.test.ts (C13)
uploadBackground.test.ts (C13)
uploadAudit.test.ts (C13)
store/
useUploadStore.test.ts (C13)
2. Flux techniques
2.1 Flux nominal A — Dépôt fidèle (mode par défaut)
┌─────────────┐
│ Utilisateur │
│ sélectionne │
│ fichier │
└──────┬──────┘
│
▼
┌──────────────────┐
│ C12: File Picker │──── Photos picker ou File picker
│ + métadonnées │ → (filename, mime_type, file_size_bytes, uri)
└──────┬───────────┘
│
▼
┌──────────────────┐ file_size > 500MB ?
│ C9: Validation │────── OUI ──→ ERR-01: refus immédiat UX
│ taille │
└──────┬───────────┘
│ NON
▼
┌──────────────────┐ Source Photos + transcodage détecté ?
│ C12: Transcodage │────── OUI ──→ Avertissement + choix utilisateur
│ détection │ → ios_transcoded=true si continue
└──────┬───────────┘
│
▼
┌──────────────────┐
│ C12: Disclaimer │──── Confirmation explicite requise
│ EXIF │ → Bloquant : pas d'upload sans confirmation
└──────┬───────────┘
│
▼
┌──────────────────┐
│ C2: purgeStale() │──── Supprime artefacts sensibles résiduels
│ │ → Tracé dans audit
└──────┬───────────┘
│
▼
┌──────────────────┐
│ C9: Génère │──── crypto.randomUUID()
│ doc_id (UUID) │
└──────┬───────────┘
│
▼
┌──────────────────────────────────────┐
│ C3: Hash SHA3-256 (fichier soumis) │
│ → hash_initial │
│ │
│ C3: Chiffrement AES-256-GCM │
│ → nonce unique 12 bytes (1 par doc) │
│ → ciphertext FILE-LEVEL │
│ → auth_tag 16 bytes │
│ │
│ C3: Vérification intégrité │
│ → recalcul hash sur plaintext mémoire│
│ → compare hash_initial │
│ → si divergence: abort ERR-02 │
└──────┬───────────────────────────────┘
│
▼
┌──────────────────┐
│ C8: Store → │
│ UPLOADING │
└──────┬───────────┘
│
▼
┌──────────────────────────────────────┐
│ C4: Init backend │
│ → POST /documents/init │
│ { doc_id, sha3_256, file_size, │
│ mime_type, nonce(b64), │
│ auth_tag(b64), optimized, │
│ ios_transcoded } │
│ → Réponse : presigned URL(s), │
│ upload_id (si multipart), │
│ backend status=PENDING │
└──────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ C4+C5+C6: Upload │
│ │
│ SI < 10MB (10 000 000 bytes): │
│ → Upload simple (PUT presigned) │
│ → Retry global (max 3, 1/2/4s+j) │
│ │
│ SI >= 10MB: │
│ → Découpe ciphertext en chunks │
│ (5MB par défaut, clamp [5,50]) │
│ → Upload chaque chunk (PUT) │
│ → Retry par chunk (max 3) │
│ → C6 agrège progression │
│ │
│ C7: Annulation possible à tout moment│
└──────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ C4: Finalisation backend │
│ → POST /documents/:doc_id/finalize │
│ → Backend PENDING → READY │
│ → Worker async READY → PENDING_SEAL │
└──────┬───────────────────────────────┘
│
▼
┌──────────────────┐
│ C8: Store → │
│ COMPLETED │
│ C11: Audit event │
└──────────────────┘
2.2 Flux annulation
Utilisateur appuie "Annuler"
│
▼
┌──────────────────────────────────────┐
│ C7: uploadCancel │
│ 1. Signal abort → tâche réseau stop │
│ 2. Si multipart en cours: │
│ → AbortMultipartUpload (best-eff.)│
│ 3. POST /documents/:doc_id/cancel │
│ → Backend: CANCELLED │
│ (garde: UPDATE WHERE status= │
│ 'PENDING' OR status='READY') │
│ 4. C2: purge artefacts locaux │
│ 5. C8: Store → CANCELLED (terminal) │
│ 6. C11: audit event annulation │
└──────────────────────────────────────┘
2.3 Flux retry après FAILED
Utilisateur appuie "Réessayer"
│
▼
Nouveau flux COMPLET:
→ Nouveau doc_id (UUID v4)
→ Nouveau nonce
→ Nouveau chiffrement
→ Nouvel upload
→ Ancien enregistrement backend inchangé
2.4 Flux background (C10)
App → background pendant UPLOADING
│
▼
┌──────────────────────────────────────┐
│ C10: Background task continue │
│ → Upload réseau via URLSession bg │
│ → Progression sauvegardée localement │
│ │
│ À terminaison: │
│ → Notification locale unique │
│ (succès ou échec) │
│ → Flag de résultat persisté │
│ │
│ Au retour foreground: │
│ → Synchronisation état réel │
│ → Pas de doublon notification │
│ → C8 reflète l'état final │
└──────────────────────────────────────┘
2.5 Diagrammes Mermaid
Graphe de dépendances des composants
graph TD
C1[C1: upload-types<br/>Types partagés, branded types]
C2[C2: upload-purge<br/>purgeStale, suppression artefacts]
C3[C3: upload-crypto<br/>SHA3-256, AES-256-GCM, HKDF]
C4[C4: upload-network<br/>Init backend, presigned URLs, multipart]
C5[C5: upload-retry<br/>Backoff exponentiel + jitter]
C6[C6: upload-progress<br/>%, ETA, vitesse, agrégation]
C7[C7: upload-cancel<br/>Abort client, S3, backend CANCELLED]
C8[C8: upload-store<br/>Zustand, machine à états]
C9[C9: upload-orchestrator<br/>Flux principal, orchestre C2-C8]
C10[C10: upload-background<br/>expo-task-manager, URLSession]
C11[C11: upload-audit<br/>Événements audit, sanitizer]
C12[C12: upload-ui<br/>Écrans, modals, progression]
C13[C13: upload-tests<br/>Tests unitaires + intégration]
C2 --> C1
C3 --> C1
C4 --> C1
C5 --> C1
C6 --> C1
C7 --> C1
C7 --> C2
C7 --> C4
C8 --> C1
C9 --> C1
C9 --> C2
C9 --> C3
C9 --> C4
C9 --> C5
C9 --> C6
C9 --> C7
C9 --> C8
C10 --> C1
C10 --> C8
C10 --> C9
C11 --> C1
C12 --> C1
C12 --> C8
C12 --> C9
C13 -.-> C1
C13 -.-> C2
C13 -.-> C3
C13 -.-> C4
C13 -.-> C5
C13 -.-> C6
C13 -.-> C7
C13 -.-> C8
C13 -.-> C9
C13 -.-> C10
C13 -.-> C11
C13 -.-> C12
C3 -.->|crypto.ts existant| EXT1[crypto.ts]
C3 -.->|hkdf.ts existant| EXT2[hkdf.ts]
C4 -.->|api.ts existant| EXT3[api.ts]
C11 -.->|auditService.ts existant| EXT4[auditService.ts]
style C9 fill:#f9f,stroke:#333,stroke-width:2px
style C1 fill:#bbf,stroke:#333,stroke-width:2px
style EXT1 fill:#ddd,stroke:#999,stroke-dasharray:5 5
style EXT2 fill:#ddd,stroke:#999,stroke-dasharray:5 5
style EXT3 fill:#ddd,stroke:#999,stroke-dasharray:5 5
style EXT4 fill:#ddd,stroke:#999,stroke-dasharray:5 5
Diagramme de séquence — Flux nominal (dépôt fidèle)
sequenceDiagram
participant U as Utilisateur
participant C12 as C12: upload-ui
participant C9 as C9: orchestrator
participant C2 as C2: purge
participant C3 as C3: crypto
participant C8 as C8: store
participant C4 as C4: network
participant C5 as C5: retry
participant C6 as C6: progress
participant C11 as C11: audit
participant BE as Backend API
participant S3 as S3 presigned
U->>C12: Sélection fichier
C12->>C9: startUpload(file)
C9->>C2: purgeStale()
C2-->>C11: audit(purge_event)
C9->>C9: doc_id = crypto.randomUUID()
C9->>C3: hashSHA3(plaintext)
C3-->>C9: hash_initial
C9->>C3: encryptAES256GCM(plaintext, DEK)
C3-->>C9: {ciphertext, nonce, auth_tag}
C9->>C3: verifyIntegrity(plaintext, hash_initial)
alt Hash diverge
C3-->>C9: ERR-02
C9->>C2: purge artefacts
C9->>C8: transition → FAILED
end
C9->>C8: transition → UPLOADING
C8-->>C12: UI update (progression)
C9->>C4: initBackend(doc_id, sha3, nonce, auth_tag, ...)
C4->>BE: POST /documents/init
BE-->>C4: {presigned_urls, upload_id?}
alt file_size < 10MB (simple)
C9->>C4: uploadSimple(presigned_url, ciphertext)
C4->>C5: avec retry global (max 3)
C5->>S3: PUT ciphertext
S3-->>C5: 200 OK
C5-->>C4: success
C4-->>C6: progress(100%)
else file_size >= 10MB (multipart)
loop Chaque chunk du ciphertext
C9->>C4: uploadChunk(part_url, chunk_n)
C4->>C5: avec retry par chunk (max 3)
C5->>S3: PUT chunk_n
S3-->>C5: 200 OK + ETag
C5-->>C4: success
C4-->>C6: progress(chunk_n / total)
C6-->>C8: update(%, ETA, vitesse)
C8-->>C12: UI update
end
end
C9->>C4: finalize(doc_id)
C4->>BE: POST /documents/:doc_id/finalize
BE-->>C4: 200 (PENDING → READY)
C9->>C8: transition → COMPLETED
C9-->>C11: audit(upload_completed)
C8-->>C12: UI update (succès)
Note over BE: Worker async<br/>READY → PENDING_SEAL<br/>(PD-55)
Diagramme de séquence — Annulation
sequenceDiagram
participant U as Utilisateur
participant C12 as C12: upload-ui
participant C7 as C7: cancel
participant C4 as C4: network
participant C2 as C2: purge
participant C8 as C8: store
participant C11 as C11: audit
participant BE as Backend API
participant S3 as S3
U->>C12: Appuie "Annuler"
C12->>C7: cancelUpload(doc_id)
C7->>C4: signal abort (tâche réseau)
C4-->>C7: réseau stoppé
opt Multipart en cours
C7->>C4: abortMultipartUpload(upload_id)
C4->>S3: AbortMultipartUpload (best-effort)
S3-->>C4: ack
end
C7->>C4: cancelBackend(doc_id)
C4->>BE: POST /documents/:doc_id/cancel
Note over BE: UPDATE WHERE<br/>status IN ('PENDING','READY')
BE-->>C4: 200 CANCELLED
C7->>C2: purge artefacts locaux
C7->>C8: transition → CANCELLED (terminal)
C7-->>C11: audit(upload_cancelled)
C8-->>C12: UI update (annulé)
3. Mapping invariants → mécanismes
| Invariant ID | Exigence | Mécanisme | Composant | Observable | Risque |
| INV-01-zero-knowledge | Aucun octet clair envoyé au backend/S3 | Chiffrement FILE-LEVEL complet avant toute requête réseau. Le ciphertext est le seul payload transmis. | C3, C4 | Inspection proxy : aucun plaintext dans les payloads HTTP | Faible si C3 est correct |
| INV-02-pre-encryption | Chiffrement terminé avant première requête upload | Séquence stricte dans C9 : encrypt() → initBackend() → upload(). Aucun appel réseau avant retour de encrypt(). | C3, C9 | Timestamps : fin chiffrement < début premier appel réseau | Faible (séquence explicite) |
| INV-03-probatory-fidelity-default | Fichier probatoire = fichier soumis bit-à-bit en mode par défaut | Hash SHA3-256 calculé sur le fichier reçu par l'app (pas de transformation). Aucune compression/conversion en mode optimized=false. | C3, C9 | Hash probatoire stocké = hash du fichier soumis | Moyen (transcodage iOS) |
| INV-04-explicit-optimized-consent | Consentement explicite traçable avant transformation | Modal OptimizedConsentSheet avec confirmation explicite. Preuve de consentement (§3.4) persistée en metadata backend + audit. optimized flag immutable après confirmation. | C12, C11 | Journal audit contient preuve §3.4 + metadata backend optimized=true | Faible |
| INV-05-hash-functional-validation | Hash validé fonctionnellement (recalcul post-chiffrement) | Après chiffrement, recalcul SHA3-256 sur le plaintext conservé en mémoire. Comparaison avec hash initial. Divergence → abort ERR-02. | C3 | Test de corruption détecté : flux avorté, aucun upload émis | Faible |
| INV-06-nonce-uniqueness | Nonce unique par doc_id, un seul nonce FILE-LEVEL | Nonce généré par crypto.getRandomValues(12) (CSPRNG). Un seul appel par flux. Aucun chiffrement chunk-level. | C3 | 100 dépôts : aucune collision nonce. 1 nonce par doc_id. | Très faible (12 bytes CSPRNG) |
| INV-07-sensitive-purge | Aucun artefact sensible persistant sur disque + purgeStale() au démarrage | purgeStale() appelé en premier dans startUpload(). Purge fichiers dans documentDirectory/upload-temp/. Nullification références mémoire (best-effort Hermes). | C2 | Traces purgeStale() dans audit. Absence de fichiers résiduels après flux. | Moyen (crash pré-finally) |
| INV-08-envelope-encryption | Artefacts crypto temporaires chiffrés au repos | DEK dérivée via HKDF (K_master + doc_id). Nonce et auth_tag transmis en base64 via API, jamais stockés en clair en DB. Backend utilise envelope encryption HSM. | C3, C4 | Inspection DB : aucun secret crypto en clair | Faible (backend PD-35 DONE) |
| INV-09-state-transitions | Transitions d'état explicitement autorisées/interdites | Machine à états dans C8 avec transition(from, to) qui valide chaque transition contre la table §5.7. Transitions interdites → throw. | C8 | Tests exhaustifs de chaque transition autorisée/interdite | Faible |
| INV-10-no-sensitive-logs | Pas de plaintext/clé/nonce/hash corrélable dans les logs | Logger wrapper dans C11 qui sanitize les payloads. Pattern deny-list : hex 64 chars, base64 nonces, clés. doc_id et correlation_id autorisés. | C11 | Scan automatisé des journaux : aucun pattern sensible | Moyen (discipline développeur) |
4. Mapping critères d'acceptation → mécanismes
| Critère ID | Mécanisme(s) | Composant | Observable | Risque |
| CA-01 | Hash SHA3-256 calculé sur fichier reçu (pas de transformation en mode par défaut). Aucune étape de compression/conversion dans le pipeline optimized=false. | C3, C9 | Hash fichier soumis = hash probatoire stocké | Faible |
| CA-02 | Modal consentement + preuve §3.4 (JSON avec action, timestamp, doc_id, user_confirmed). Persistance en metadata backend + journal audit. Flag optimized=true immutable. | C12, C11, C4 | Audit contient preuve §3.4 + metadata optimized=true | Faible |
| CA-03 | Chiffrement FILE-LEVEL avant réseau. Seul le ciphertext transit via presigned URL. | C3, C4 | Proxy : aucun plaintext dans les requêtes | Faible |
| CA-04 | Nonce 12 bytes via CSPRNG (crypto.getRandomValues). Un seul nonce par doc_id. | C3 | 100 dépôts sans collision | Très faible |
| CA-05 | Vérification file_size_bytes > 500_000_000 en premier dans le flux. Pas de chiffrement ni d'appel backend si rejet. | C9, C12 | UI refus + absence d'appel réseau | Faible |
| CA-06 | Condition file_size_bytes < 10_000_000 → simple, sinon multipart. Seuil fixe, non configurable. | C4 | 9 999 999 bytes → simple, 10 000 000 → multipart | Faible |
| CA-07 | C5 : retry global pour upload simple, retry par chunk pour multipart. Backoff [1,2,4]s + jitter. | C5 | Traces d'exécution retry conformes | Faible |
| CA-08 | C7 : signal abort client → AbortMultipartUpload S3 → PATCH backend CANCELLED. Purge artefacts locaux. | C7, C4 | État final CANCELLED + absence parts S3 actives | Moyen (best-effort S3) |
| CA-09 | C10 : upload continue en background. Notification unique. État synchronisé au retour foreground. | C10, C8 | Pas de perte progression ni doublon notification | Moyen (contrainte iOS) |
| CA-10 | purgeStale() en première étape de startUpload(). Suppression fichiers dans upload-temp/. | C2 | Trace startup + suppression artefacts | Faible |
| CA-11 | Recalcul SHA3-256 sur plaintext en mémoire post-chiffrement. Comparaison avec hash initial. Divergence → ERR-02. | C3 | Test corruption : flux avorté, pas d'upload | Faible |
| CA-12 | Upload 100MB en ≤30s P95 WiFi sur iPhone 12/A14. Chunk size 5MB, parallélisme séquentiel. | C4, C6 | Campagne perf P95 | Moyen (dépend réseau) |
| CA-13 | DEK dérivée par HKDF, transmise chiffrée. Backend envelope encryption (PD-35). | C3, C4 | DB : aucun secret en clair | Faible |
| CA-14 | Logger sanitizer dans C11 avec deny-list de patterns sensibles. | C11 | Scan journaux sans patterns sensibles | Moyen |
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-03, CA-01, Flux 5.1 | C3 hash + C4 multipart + C8 transitions PENDING→READY→PENDING_SEAL | Hash probatoire = hash fichier soumis, état final COMPLETED | Integration |
| TC-NOM-02 | CA-06, §5.5 | C4 seuil 10 000 000 bytes | Stratégie simple vs multipart selon taille | Unit |
| TC-NOM-03 | INV-04, CA-02, §3.4 | C12 modal consentement + C11 preuve §3.4 + C4 metadata optimized=true | Audit + metadata + flag immutable | Integration |
| TC-NOM-04 | Flux 5.1 étape 4 | C12 détection transcodage + C4 metadata ios_transcoded=true | Avertissement affiché, metadata enregistrées | Unit |
| TC-NOM-05 | CA-09, Flux 5.3 | C10 background task + notification unique + C8 sync retour foreground | 1 notification, état cohérent au retour | Integration |
| TC-NOM-06 | INV-07, CA-10 | C2 purgeStale() avant chiffrement | Traces audit + absence artefacts résiduels | Unit |
| TC-NOM-07 | Flux 5.1 étape 5 | C12 disclaimer EXIF bloquant | Upload impossible sans confirmation explicite | Unit |
| TC-NOM-08 | Flux 5.4 | C9 nouveau flux complet (nouveau doc_id, nonce, chiffrement) | Ancien enregistrement inchangé, nouveau doc_id | Integration |
| TC-ERR-01 | ERR-01, CA-05 | C9 validation taille > 500MB | Rejet UX, aucun chiffrement/upload | Unit |
| TC-ERR-02 | ERR-02, INV-02, INV-07 | C3 échec chiffrement → abort + C2 purge | État FAILED, artefacts purgés, pas d'upload | Unit |
| TC-ERR-03 | ERR-03/04/05, CA-07 | C5 retry simple global vs multipart par chunk + backoff ½/4 + jitter | Nombre tentatives, délais observés | Unit |
| TC-ERR-04 | ERR-06, ERR-09 | C4 rejet 400 sur metadata invalide | Pas de transition READY, motif tracé | Unit |
| TC-ERR-05 | ERR-08, CA-09 | C10 déduplication notification | Au plus 1 notification par session | Integration |
| TC-ERR-06 | ERR-06 | C4+C5 : pas de retry sur 4xx | État FAILED immédiat, message mappé | Unit |
| TC-ERR-07 | ERR-07, CA-08, INV-09 | C7 annulation orchestrée + C4 AbortMultipart + C8 CANCELLED terminal | Pas de retour état non terminal | Integration |
| TC-ERR-08 | ERR-10, §5.7 race condition | C4 UPDATE WHERE status='READY' atomique | Un seul gagne, pas d'état incohérent | Integration (mock) |
| TC-INV-01 | INV-01, CA-03 | C3 chiffrement avant réseau + C4 payloads ciphertext | Proxy : aucun plaintext | Sec |
| TC-INV-02 | INV-02, CA-03 | C9 séquence stricte encrypt→upload | Timestamps fin crypto < début réseau | Unit |
| TC-INV-03 | INV-03, CA-01 | C3 hash sur fichier soumis, pas de transformation | Hash identique multi-formats | Unit |
| TC-INV-04 | INV-04, CA-02 | C12 modal + C9 blocage sans consentement | Dépôt bloqué, optimized=false | Unit |
| TC-INV-05 | INV-05, CA-11 | C3 recalcul hash post-chiffrement | Corruption détectée → abort ERR-02 | Unit |
| TC-INV-06 | INV-06, CA-04 | C3 nonce CSPRNG 12 bytes, unicité 100 dépôts | Aucune collision, 1 nonce/doc | Unit |
| TC-INV-07 | INV-07, CA-10 | C2 purgeStale() + suppression fichiers | Traces audit, pas de fichiers résiduels | Unit |
| TC-INV-08 | INV-08, CA-13 | C3 DEK via HKDF + C4 transport base64 | DB : aucun secret en clair | Integration |
| TC-INV-09 | INV-09, §5.7 | C8 machine à états stricte | Toutes transitions autorisées OK, interdites rejetées | Unit |
| TC-INV-10 | INV-10, CA-14 | C11 sanitizer deny-list | Scan journaux sans patterns sensibles | Unit |
| TC-NR-01 | Seuil multipart fixe | C4 seuil 10 000 000 bytes | 9 999 999 simple, 10 000 000 multipart | Unit |
| TC-NR-02 | Transitions terminales | C8 COMPLETED/CANCELLED terminaux | Aucune sortie possible | Unit |
| TC-NR-03 | Perf 100MB P95 ≤30s | C4+C6 upload + progression | Rapport campagne P95 | Perf |
| TC-NR-04 | Retry + jitter | C5 backoff observé + jitter actif | Délais conformes | Unit |
| TC-NR-05 | purgeStale au démarrage | C2 trace systématique | Exécution systématique | Unit |
| TC-NR-06 | Chiffrement FILE-LEVEL | C3 un seul nonce par fichier | Pas de chiffrement chunk-level | Unit |
| TC-NEG-01 | doc_id non UUID v4 | C4 validation format | Rejet 400 | Unit |
| TC-NEG-02 | sha3_256 uppercase/longueur | C4 validation regex ^[a-f0-9]{64}$ | Rejet 400 | Unit |
| TC-NEG-03 | nonce longueur ≠ 12 | C3 validation stricte | Échec chiffrement | Unit |
| TC-NEG-04 | auth_tag longueur ≠ 16 | C4 validation format | Rejet 400 | Unit |
| TC-NEG-05 | optimized non booléen | C4 validation type strict | Rejet 400 | Unit |
| TC-NEG-06 | mime_type invalide RFC | C9 validation MIME pattern | Rejet 400 | Unit |
| TC-NEG-07 | PENDING_SEAL → CANCELLED | C8 transition interdite | Refusée | Unit |
| TC-NEG-08 | Jitter désactivé | C5 vérification config | Non-conformité | Unit |
| TC-NEG-09 | Upload en clair injecté | C3+C9 chiffrement obligatoire | Échec conformité | Sec |
| TC-NEG-10 | Logs avec nonce/clé/hash | C11 sanitizer | Violation détectée | Unit |
| TC-NEG-11 | Chiffrement chunk-level | C3 un seul nonce par doc | Non-conformité | Unit |
6. Gestion des erreurs
| ID erreur | Composant | Traitement | Code HTTP / UI | Observable |
| ERR-01 | C9 | file_size_bytes > 500_000_000 → refus immédiat. Aucun chiffrement, aucun appel réseau. | UI : message "Fichier trop volumineux (max 500 MB)" | Absence d'appel backend |
| ERR-02 | C3 | Échec chiffrement OU divergence hash vérification intégrité → abort. C2 purge artefacts. C8 → FAILED. C11 audit. | UI : "Erreur de chiffrement, veuillez réessayer" | État FAILED, artefacts purgés |
| ERR-03/04/05 | C5 | Erreurs retryable (timeout, perte connectivité, 5xx). Backoff ½/4s + jitter. Max 3 tentatives par chunk (multipart) ou global (simple). Si épuisé → FAILED. | UI : progression stagne puis "Échec réseau" | Traces retry dans logs |
| ERR-06 | C4, C5 | HTTP 4xx (400, 403, 404, 409, 413, 422) → non retryable. FAILED immédiat. Message mappé au code HTTP. | UI : message spécifique par code | Pas de retry, état FAILED |
| ERR-07 | C7 | Annulation utilisateur. Stop client. AbortMultipartUpload S3 (best-effort). Backend CANCELLED (garde atomique si READY). Purge async. | UI : "Upload annulé" | État CANCELLED terminal |
| ERR-08 | C10 | Déduplication notification. Flag notificationSent par session d'upload. | UI : au plus 1 notification | Absence doublon |
| ERR-09 | C4 | Validation format metadata (doc_id, sha3_256, etc.) côté client avant envoi + côté backend. Rejet 400. | API : 400 + motif | Pas de transition READY |
| ERR-10 | C4, C7 | Race condition READY→CANCELLED vs READY→PENDING_SEAL. UPDATE WHERE status='READY' atomique. Si worker gagne → annulation échoue, message client. | UI : "Scellement en cours, annulation impossible" | Un seul gagne |
Mapping codes HTTP → messages UI
| Code HTTP | Clé i18n | Message (fr) |
| 400 | upload.error.badRequest | "Données invalides, vérifiez le fichier" |
| 403 | upload.error.forbidden | "Accès refusé" |
| 404 | upload.error.notFound | "Ressource introuvable" |
| 409 | upload.error.conflict | "Conflit de version, réessayez" |
| 413 | upload.error.tooLarge | "Fichier trop volumineux" |
| 422 | upload.error.unprocessable | "Format non supporté" |
| 5xx | upload.error.server | "Erreur serveur, réessayez plus tard" |
7. Impacts sécurité
| Risque | Mitigation | Composant | Journalisation |
| Fuite plaintext en transit | Chiffrement FILE-LEVEL obligatoire avant réseau (INV-01, INV-02). TLS supposé (H-07). | C3, C9 | Audit : événement chiffrement terminé |
| Réutilisation nonce GCM | Nonce CSPRNG 12 bytes unique par doc_id. Un seul nonce par fichier (pas chunk-level). Nouveau nonce à chaque retry complet. | C3 | Audit : nonce_id logué (pas le nonce brut) |
| Artefacts sensibles résiduels | purgeStale() au démarrage. finally block dans l'orchestrateur. Purge fichiers upload-temp/. | C2, C9 | Audit : traces purge |
| Secrets en logs | Sanitizer deny-list dans C11. Patterns : hex 64 chars, base64 nonces/tags, clés. | C11 | Scan automatisé |
| Race condition annulation | UPDATE WHERE status='READY' atomique côté backend. Client reçoit erreur explicite si worker gagne. | C4, C7 | Audit : résultat race condition |
| Corruption hash silencieuse | Validation fonctionnelle hash (recalcul post-chiffrement). Pas seulement regex. | C3 | Audit : résultat vérification |
| Énumération doc_id | Messages d'erreur uniformes (404) pour doc inexistant ou non autorisé (anti-enumeration). | C4 | — |
- Zero-knowledge : respecté — backend ne voit jamais le plaintext.
- RGPD : fichiers temporaires sensibles purgés (INV-07). Pas de stockage de contenu en clair côté backend.
- eIDAS : hash probatoire SHA3-256 calculé sur le fichier soumis (INV-03/INV-05), scellé après upload (worker PD-55).
8. Hypothèses techniques
| ID | Hypothèse | Impact si faux | Mitigation |
| H-01 | React Native + Expo SDK 54 + TypeScript (pas Swift natif) | Architecture invalide | Vérifier package.json avant implémentation |
| H-02 | Backend expose endpoints presigned + finalisation (PD-63 DONE) | Blocage upload en production | STUB: PD-63 — endpoints mockés en dev |
| H-03 | Worker ancrage async (PD-55 DONE) traite READY/PENDING_SEAL | Perte continuité probatoire | Hors scope PD-101 — dépendance documentée |
| H-04 | Chunk multipart borné [5,50] MB | Perf/mémoire à redéfinir | Clamp dans C4 |
| H-05 | Transport nonce/auth_tag en base64 | Contrat API incompatible | Valider avec backend avant implémentation |
| H-06 | Purge RAM best-effort sur Hermes | Tests limités à nullification refs | Documenter dans tests |
| H-07 | TLS (HTTPS) pour tous les transports | Fuite en transit | Non testable côté mobile — infra (PD-19 DONE) |
| HT-01 | expo-file-system supporte la lecture streaming pour fichiers > 100MB | OOM si lecture complète en mémoire | Fallback : lecture par blocs avec readAsStringAsync et offset |
| HT-02 | crypto.getRandomValues disponible dans Hermes (React Native 0.81.5) | Fallback expo-crypto pour nonce | Triple fallback dans randomBytes() existant |
| HT-03 | @noble/ciphers AES-256-GCM supporte des fichiers > 50MB en mémoire sur iPhone 12 | OOM sur gros fichiers | Monitoring mémoire + alerte si > 100MB RAM utilisée |
| HT-04 | URLs presigned S3 supportent PUT multipart (chaque part = PUT distinct) | Architecture upload à revoir | Valider avec PD-63 |
9. Points de vigilance (risques, dette, pièges)
Risques
-
Mémoire — Fichiers volumineux : Le chiffrement FILE-LEVEL exige de charger le fichier entier en mémoire (plaintext + ciphertext). Pour un fichier de 500 MB, cela représente ~1 GB de RAM. Sur iPhone 12 (4 GB RAM), cela peut provoquer un OOM. Mitigation : monitoring mémoire dans C3, warning UX au-delà de 200 MB, test de perf sur device réel.
-
Transcodage iOS silencieux : Photos picker peut transcoder HEIC → JPEG sans avertissement explicite de l'OS. La détection dans C12 dépend de la comparaison UTType demandé vs reçu. Mitigation : test sur assets réels iOS, documentation du comportement observé.
-
Background upload iOS : Les contraintes iOS sur les tâches background sont strictes (30s max pour beginBackgroundTask, URLSession background pour le réseau). L'upload multipart en background nécessite URLSessionConfiguration.background. Mitigation : C10 utilise expo-task-manager + URLSession natif si nécessaire.
-
Race condition S3 lifecycle : Les parts orphelines après AbortMultipartUpload échoué sont nettoyées par lifecycle policy S3 (7 jours). Pendant cette fenêtre, elles consomment du stockage. Mitigation : job de réconciliation backend (§5.9 spec), non implémenté dans PD-101 (backend-side).
Dette technique identifiée
crypto.ts existant utilise Math.random() pour finalDocId (ligne 321) — à corriger avec crypto.randomUUID() (cf. learning crypto.randomUUID obligatoire). DocumentUploadRequest dans api.ts actuel est incompatible avec le nouveau contrat (pas de presigned URL, pas de multipart). À remplacer par les nouveaux types de C1. - Type
Document dans types/document.ts utilise hash?: string (SHA-256 implicite) — à étendre avec sha3Hash pour SHA3-256.
Pièges connus
- Ne pas chiffrer chunk par chunk (INV-06) : un nonce par chunk avec la même clé casse la sécurité GCM. Le ciphertext est découpé APRÈS chiffrement complet.
- Ne pas réutiliser doc_id/nonce après FAILED (Flux 5.4) : chaque retry est un flux entièrement nouveau.
- Ne pas confondre
file_size_bytes SI (base 10) : 10 MB = 10 000 000 bytes, pas 10 485 760.
10. Hors périmètre
| Élément | Raison | Story de destination |
| Proxy Re-Encryption (PRE) | Architecture crypto avancée, hors scope upload | PD-41 (DONE) |
| Ancrage blockchain immédiat côté mobile | Worker backend async (PD-55) | PD-55 (DONE) |
| Génération preuve composite | Post-scellement | Stories futures |
| Synchronisation multi-device | Architecture distincte | PD-171 |
| Suppression sélective EXIF | Hors périmètre spec | Story future |
| Job de réconciliation S3 (§5.9) | Backend-side | Story future backend |
| TTL URL pré-signées (Q-02) | Non spécifié dans le besoin | À clarifier PO |
| Liste blanche MIME (Q-03) | Non spécifié | À clarifier PO |
| Persistance états UI après redémarrage (Q-04) | Non défini | À clarifier PO |
11. Périmètre de test
| Niveau de test | In scope | Hors scope (justification) |
| Unitaire | Tous les composants C1-C12 : upload-types, upload-purge, upload-crypto, upload-network (mock HTTP), upload-retry, upload-progress, upload-cancel, upload-store (machine à états), upload-orchestrator, upload-audit | — |
| Intégration | Interactions C3↔C4 (crypto→network), C9↔C8 (orchestrator→store), C7↔C4 (cancel→network), C10↔C8 (background→store) | Intégration avec backend réel (PD-63) : endpoints mockés, car dépendance cross-repo |
| E2E | Flux complet sélection→upload→COMPLETED sur émulateur iOS avec backend mocké | E2E sur device physique avec backend réel : nécessite environnement staging, hors scope CI |
| Perf | TC-NR-03 : campagne 100 MB P95 sur device de référence (iPhone 12/A14) | Perf en conditions réseau dégradées : non contractualisé |
| Sécurité | TC-INV-01 (inspection proxy), TC-NEG-09 (injection clair), TC-INV-10 (scan logs) | Pentest complet : hors scope story, audit sécurité séparé |
Tous les niveaux de test listés in scope sont couverts. La couverture minimale de 80% s'applique au périmètre in scope.
12. Mécanismes cross-module
Aucune modification d'autres modules. PD-101 crée un nouveau module services/upload/ et met à jour UploadDocumentScreen.tsx. Les interactions avec le backend se font via l'API existante (api.ts) étendue avec de nouveaux endpoints. Aucun guard cross-module n'est requis.