Aller au contenu

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

Conformité

  • 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

  1. 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.

  2. 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é.

  3. 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.

  4. 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.