Aller au contenu

PD-262 — Plan d'implementation

1. Decoupage en composants

C1 — Module natif iOS : TamperingDetectorModule (Swift + ObjC bridge)

Responsabilite : Execution des controles de compromission en natif iOS, decision lockout fail-closed, persistance Keychain du flag lockout.

Fichiers : - ios/ProbatioVault/TamperingDetectorModule.swift (create) - ios/ProbatioVault/TamperingDetectorModule.m (create)

Sous-composants internes (dans le meme fichier Swift ou fichiers Swift prives) : - JailbreakDetector : paths /Applications/Cydia.app, /private/var/lib/apt, sandbox escape (fork), writable /private, dylib loaded images, symlink checks. - FridaDetector : scan ports loopback 27042/27043, detection process name frida-server, verification DYLD_INSERT_LIBRARIES. - CodesignValidator : coherence bundle signature / provisioning profile, detection re-signing. - StateMachine : gestion MONITORED -> TAMPERED_SESSION -> LOCKED_PERSISTENT, transitions et interdictions. - LockoutPersistence : lecture/ecriture lockout_persistent_flag en Keychain avec kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, gestion first_launch_clean.

Exports RN (via ObjC bridge) : - performCheck() -> Promise<{tampered: Bool, reasonCode: String?}> - getLockoutState() -> Promise<String> (MONITORED | TAMPERED_SESSION | LOCKED_PERSISTENT) - isFirstLaunchClean() -> Promise<Bool> - getDeviceIdPseudo() -> Promise<String> — retourne SHA-256(identifierForVendor.uuidString UTF-8) hex lowercase 64 chars, calcule cote natif Swift. Retourne chaine vide si identifierForVendor indisponible.

Bootstrapping first_launch_clean : - Au premier appel de initialize(), si lockout_persistent_flag est absent ET first_launch_clean est absent en Keychain, le module natif ecrit first_launch_clean=true en Keychain. - Ce marqueur persiste entre les lancements et les mises a jour de l'app. - Il n'est supprime que lors d'une reinstallation propre (le Keychain peut persister — le module verifie la coherence avec un marqueur UserDefaults volatil de session).

C2 — Bridge TypeScript : tamperingDetector.ts

Responsabilite : Wrapper TypeScript autour du module natif, typage strict, error mapping domaine, exposition de l'API au runtime RN.

Fichiers : - src/services/tamperingDetector.ts (create) - src/services/tamperingDetector.types.ts (create)

Exports : - performCheck(): Promise<TamperingResult> - getLockoutState(): Promise<TamperingState> - isFirstLaunchClean(): Promise<boolean> - getDeviceIdPseudo(): Promise<string> — wrapper du natif, retourne hash hex 64 chars ou chaine vide - Types : TamperingResult, TamperingState, TamperingReasonCode, TamperingError

C3 — Service : antiTamperingService.ts

Responsabilite : Orchestration des controles (cold start, foreground, periodique), decision de purge, emission audit best effort, gating environnement.

Fichiers : - src/services/antiTampering.ts (create) - src/services/antiTampering.types.ts (create)

Logique : - initialize() : verifie environnement (DEBUG/SIMULATOR/QA/RELEASE), lance controle cold start. - onForeground() : re-check avant reprise operations sensibles. - startPeriodicCheck(intervalSec: number) : timer periodique avec clamp [30..60]. - handleDetection(reason: TamperingReasonCode) : orchestration purge + lockout + audit. - Gating : inactif en DEBUG + SIMULATOR ; actif en RELEASE/TESTFLIGHT/PRODUCTION ; QA via build flag compile ANTI_TAMPERING_QA_ENABLED.

C4 — Service : tamperingPurgeService.ts

Responsabilite : Purge immediate des secrets memoire, tokens, caches crypto, temporaires dechiffres, previews. Retry borne (max 3, intervalle 1s).

Fichiers : - src/services/tamperingPurge.ts (create)

Logique : - purgeAll() : orchestre la purge de toutes les cibles. - Cibles de purge : - Cles en memoire via zeroize() (module crypto existant src/crypto/zeroize.ts) - Tokens session/refresh via useAuthStore.getState().clearTokens() - Caches crypto temporaires (DEK, fragments, buffers de dechiffrement) - Previews et fichiers temporaires dechiffres - K_master via keychainStorage.deleteMasterKey() (service existant PD-98) - Secret biometrique via biometricKeychain.deleteBiometricSecret() (service existant PD-107) - Ne purge PAS les blobs persistants deja chiffres (base locale chiffree, fichiers chiffres metier) — ils restent inexploitables sans secrets. - Retry : max 3 tentatives, intervalle 1s, abandon apres 3 echecs avec log purge incomplete.

C5 — Service : tamperingAuditEmitter.ts

Responsabilite : Construction et emission best effort du payload audit ANTI_TAMPERING_LOCKOUT vers le backend.

Fichiers : - src/services/tamperingAudit.ts (create)

Logique : - emitLockoutEvent(reason, secondaryReasons?) : construit le payload conforme a §3.3, valide les regex, tente l'envoi. - Payload : event_type, reason_code, app_version, build_number, device_id_pseudo (obtenu via tamperingDetector.getDeviceIdPseudo() — calcul natif Swift), timestamp, environment. - Validation stricte : tout champ non conforme aux regex §3.3 entraine rejet local (pas d'envoi), lockout conserve. - Aucune donnee sensible dans le payload (allowlist stricte). - Echec reseau : log local, lockout conserve, pas de retry bloquant UX.

C6 — Ecran lockout : TamperingLockoutScreen.tsx

Responsabilite : UI non dismissable affichee lors d'un lockout. Aucune interaction possible.

Fichiers : - src/screens/security/TamperingLockoutScreen.tsx (create) - src/components/security/LockoutOverlay.tsx (create)

Logique : - Ecran plein ecran sans bouton de navigation. - Message explicatif generique (sans detail technique). - Pas de bouton dismiss, pas de gesture de fermeture. - i18n : cles tampering.lockout.title, tampering.lockout.message.

C7 — Hook : useAntiTampering.ts

Responsabilite : Hook React integrant le service anti-tampering dans le lifecycle de l'app (AppState listener pour foreground).

Fichiers : - src/hooks/useAntiTampering.ts (create)

Logique : - Souscription AppState change (background -> active -> onForeground()). - Appel initialize() au mount. - Exposition de isLocked: boolean pour le navigation guard.

C8 — Integration navigation + store

Responsabilite : Integration du lockout dans la navigation (redirect vers TamperingLockoutScreen) et extension du store securite.

Fichiers : - src/store/useSecurityStore.ts (update — ajout tamperingLockout: boolean) - src/navigation/ (update — guard conditionnel) - src/i18n/ (update — cles tampering)

C9 — Tests unitaires et integration

Responsabilite : Couverture de tous les TC-* references dans la specification de tests.

Fichiers : - src/__tests__/services/antiTampering.test.ts (create) - src/__tests__/services/tamperingPurge.test.ts (create) - src/__tests__/services/tamperingAudit.test.ts (create) - src/__tests__/services/tamperingDetector.test.ts (create) - src/__tests__/hooks/useAntiTampering.test.ts (create) - src/__mocks__/TamperingDetectorModule.ts (create)

C10 — Configuration Expo plugin

Responsabilite : Enregistrement du module natif dans la config Expo.

Fichiers : - app.config.js (update — plugin config si necessaire pour le module natif custom)


2. Flux techniques

FT1 — Cold start (F1)

App launch
  -> AppDelegate didFinishLaunching
  -> RN bridge init
  -> useAntiTampering.initialize()
    -> antiTamperingService.initialize()
      -> Verifie environnement (INV-262-07)
        -> Si DEBUG/SIMULATOR: return (module inactif)
        -> Si QA sans ANTI_TAMPERING_QA_ENABLED: return
      -> tamperingDetector.getLockoutState()
        -> Si LOCKED_PERSISTENT: lockout immediat (F5)
      -> Verifie first_launch_clean (INV-262-09, F6)
        -> Si absent et app initialisee: fail-closed LOCKED_PERSISTENT
      -> tamperingDetector.performCheck()
        -> Controles natifs (jailbreak, Frida, codesign)
        -> Si tampered=true: handleDetection(reasonCode) (F3)
        -> Si tampered=false: MONITORED, deblocage
      -> startPeriodicCheck(45)

FT2 — Retour foreground (F2)

AppState change: background -> active
  -> useAntiTampering detecte transition
  -> antiTamperingService.onForeground()
    -> tamperingDetector.performCheck()
    -> Si tampered=true: handleDetection()
    -> Si tampered=false: session continue

FT3 — Detection compromission (F3)

tamperingDetector.performCheck() retourne tampered=true
  -> antiTamperingService.handleDetection(reasonCode)
    -> State: MONITORED -> TAMPERED_SESSION
    -> tamperingPurgeService.purgeAll()
      -> zeroize() cles memoire
      -> clearTokens() session/refresh
      -> deleteMasterKey() Keychain
      -> deleteBiometricSecret() Keychain
      -> purge caches crypto/temp/previews
      -> Si echec partiel: retry (max 3, 1s intervalle)
    -> State: TAMPERED_SESSION -> LOCKED_PERSISTENT
      -> Ecriture lockout_persistent_flag=true en Keychain
    -> tamperingAuditEmitter.emitLockoutEvent(reasonCode)
      -> Payload construit et valide
      -> Envoi best effort (non bloquant)
    -> Navigation -> TamperingLockoutScreen
    -> Delai max decision <= 1s P95

FT4 — Reporting audit best effort (F4)

tamperingAuditEmitter.emitLockoutEvent(reason)
  -> Construit payload §3.3
    -> event_type: "ANTI_TAMPERING_LOCKOUT"
    -> reason_code: ReasonCode (enum valide)
    -> app_version: SemVer
    -> build_number: string numerique
    -> device_id_pseudo: SHA-256(identifierForVendor) hex lowercase 64 chars
    -> timestamp: ISO-8601 UTC
    -> environment: enum
  -> Validation regex stricte chaque champ
    -> Si invalide: rejet, log local, lockout conserve
  -> POST backend /audit/events (ou endpoint existant auditService)
    -> Si succes: log ok
    -> Si echec reseau: log local, lockout conserve

FT5 — Redemarrage apres lockout (F5)

NOTE : FT5 et FT6 decrivent une reconstruction d'etat au boot, PAS une transition de la machine a etats. L'etat est initialise directement depuis la persistance Keychain sans passer par la sequence de transitions MONITORED->TAMPERED_SESSION->LOCKED_PERSISTENT. C'est conforme a la spec §5 (machine a etats) car les transitions s'appliquent uniquement en session active.

App launch
  -> antiTamperingService.initialize()
    -> tamperingDetector.getLockoutState()
    -> Lecture Keychain lockout_persistent_flag
    -> Si true: Etat INITIALISE a LOCKED_PERSISTENT (reconstruction, pas transition)
    -> Navigation -> TamperingLockoutScreen (immediat)
    -> Aucune transition sortante (etat terminal)

FT6 — Reconstruction etat au boot — cas degrade (F6)

NOTE : Reconstruction d'etat, pas transition de state machine.

App launch
  -> tamperingDetector.getLockoutState()
  -> Lecture Keychain lockout_persistent_flag
    -> Si donnee corrompue: Etat INITIALISE a LOCKED_PERSISTENT (fail-closed)
    -> Si absente ET first_launch_clean absent ET app initialisee:
       Etat INITIALISE a LOCKED_PERSISTENT (fail-closed)
    -> Si absente ET first_launch_clean present: Etat INITIALISE a MONITORED (premier lancement)

2b. Diagrammes Mermaid

Graphe de dependances inter-composants

graph TD
    C7["C7 — useAntiTampering<br/>(Hook React)"]
    C8["C8 — Navigation + Store<br/>(Integration)"]
    C3["C3 — antiTamperingService<br/>(Orchestration)"]
    C2["C2 — tamperingDetector.ts<br/>(Bridge TS)"]
    C1["C1 — TamperingDetectorModule<br/>(Swift natif)"]
    C4["C4 — tamperingPurgeService<br/>(Purge secrets)"]
    C5["C5 — tamperingAuditEmitter<br/>(Audit best effort)"]
    C6["C6 — TamperingLockoutScreen<br/>(UI lockout)"]
    C9["C9 — Tests"]
    C10["C10 — Expo plugin config"]

    %% Modules existants
    KS["keychainStorage<br/>(PD-98)"]
    BK["biometricKeychain<br/>(PD-107)"]
    AS["useAuthStore"]
    ZR["crypto/zeroize.ts"]
    SS["useSecurityStore"]

    C7 -->|"AppState listener<br/>initialize / onForeground"| C3
    C7 -->|"isLocked"| C8
    C8 -->|"guard navigation"| C6
    C8 -->|"tamperingLockout"| SS
    C3 -->|"performCheck /<br/>getLockoutState"| C2
    C2 -->|"RN bridge<br/>ObjC"| C1
    C3 -->|"purgeAll()"| C4
    C3 -->|"emitLockoutEvent()"| C5
    C5 -->|"getDeviceIdPseudo()"| C2
    C4 -->|"deleteMasterKey()"| KS
    C4 -->|"deleteBiometricSecret()"| BK
    C4 -->|"clearTokens()"| AS
    C4 -->|"zeroize()"| ZR
    C9 -.->|"teste"| C1
    C9 -.->|"teste"| C2
    C9 -.->|"teste"| C3
    C9 -.->|"teste"| C4
    C9 -.->|"teste"| C5
    C9 -.->|"teste"| C7
    C10 -.->|"config plugin"| C1

    style C1 fill:#e74c3c,color:#fff
    style C3 fill:#2980b9,color:#fff
    style C4 fill:#e67e22,color:#fff
    style C5 fill:#8e44ad,color:#fff
    style C6 fill:#27ae60,color:#fff
    style KS fill:#95a5a6,color:#fff
    style BK fill:#95a5a6,color:#fff
    style AS fill:#95a5a6,color:#fff
    style ZR fill:#95a5a6,color:#fff
    style SS fill:#95a5a6,color:#fff

Sequence — Cold start avec detection (FT1 + FT3)

sequenceDiagram
    participant App as App Launch
    participant Hook as C7 useAntiTampering
    participant Svc as C3 antiTamperingService
    participant Bridge as C2 tamperingDetector.ts
    participant Native as C1 TamperingDetectorModule
    participant Purge as C4 tamperingPurgeService
    participant Audit as C5 tamperingAuditEmitter
    participant KS as keychainStorage (PD-98)
    participant BK as biometricKeychain (PD-107)
    participant Nav as C8 Navigation
    participant Screen as C6 LockoutScreen

    App->>Hook: mount
    Hook->>Svc: initialize()
    Svc->>Svc: check env (INV-262-07)
    alt DEBUG / SIMULATOR
        Svc-->>Hook: return (inactif)
    end
    Svc->>Bridge: getLockoutState()
    Bridge->>Native: getLockoutState()
    Native->>Native: lecture Keychain lockout_persistent_flag
    Native-->>Bridge: MONITORED
    Bridge-->>Svc: MONITORED

    Svc->>Bridge: isFirstLaunchClean()
    Bridge->>Native: isFirstLaunchClean()
    Native-->>Bridge: true
    Bridge-->>Svc: true

    Svc->>Bridge: performCheck()
    Bridge->>Native: performCheck()
    Native->>Native: JailbreakDetector + FridaDetector + CodesignValidator
    Native-->>Bridge: {tampered: true, reasonCode: "JAILBREAK_DETECTED"}
    Bridge-->>Svc: TamperingResult(tampered=true)

    Note over Svc: handleDetection() — budget <= 1s P95

    Svc->>Svc: state MONITORED -> TAMPERED_SESSION
    Svc->>Purge: purgeAll()
    Purge->>KS: deleteMasterKey()
    Purge->>BK: deleteBiometricSecret()
    Purge->>Purge: clearTokens() + zeroize() + caches
    Purge-->>Svc: purge OK

    Svc->>Native: state TAMPERED_SESSION -> LOCKED_PERSISTENT
    Native->>Native: ecriture lockout_persistent_flag=true (Keychain AVANT resolve)

    Svc->>Audit: emitLockoutEvent("JAILBREAK_DETECTED")
    Audit->>Bridge: getDeviceIdPseudo()
    Bridge->>Native: getDeviceIdPseudo()
    Native-->>Bridge: SHA-256(identifierForVendor) hex 64 chars
    Audit->>Audit: construction payload §3.3 + validation regex
    Audit->>Audit: POST /audit/events (best effort, fire-and-forget)

    Svc-->>Hook: isLocked=true
    Hook->>Nav: tamperingLockout=true
    Nav->>Screen: redirect TamperingLockoutScreen

Sequence — Redemarrage apres lockout (FT5)

sequenceDiagram
    participant App as App Launch
    participant Hook as C7 useAntiTampering
    participant Svc as C3 antiTamperingService
    participant Bridge as C2 tamperingDetector.ts
    participant Native as C1 TamperingDetectorModule
    participant Nav as C8 Navigation
    participant Screen as C6 LockoutScreen

    App->>Hook: mount
    Hook->>Svc: initialize()
    Svc->>Bridge: getLockoutState()
    Bridge->>Native: getLockoutState()
    Native->>Native: lecture Keychain lockout_persistent_flag = true
    Native-->>Bridge: LOCKED_PERSISTENT
    Bridge-->>Svc: LOCKED_PERSISTENT

    Note over Svc: Etat terminal — reconstruction, pas transition (FT5/FT6)

    Svc-->>Hook: isLocked=true
    Hook->>Nav: tamperingLockout=true
    Nav->>Screen: redirect immediat TamperingLockoutScreen
    Note over Screen: Aucune interaction possible, etat terminal

3. Mapping invariants -> mecanismes

Invariant ID Exigence Mecanisme Composant Observable Risque
INV-262-01-fail-closed Detection positive OU erreur detecteur force tampered=true Tout retour != PASS du module natif => tampered=true. Catch global avec DETECTOR_ERROR C1 (natif), C3 (service) Logs locaux reason_code + etat machine Faible — pattern simple, testable
INV-262-02-native-authority Decision lockout/purge par couche native uniquement, JS ne peut inhiber Decision prise dans TamperingDetectorModule.swift ; JS ne peut que lire le resultat. Lockout Keychain ecrit cote natif avant retour JS C1 (natif) Test adversarial TC-INV-02 : hook JS sans effet Moyen — RN bridge asynchrone, s'assurer que le natif ecrit avant resolve
INV-262-03-trigger-coverage Controles au cold start, foreground, periodique initialize() (cold), onForeground() (foreground), startPeriodicCheck() (timer) C3 (service), C7 (hook) Traces trigger dans logs locaux Faible — 3 points d'entree explicites
INV-262-04-single-lockout-state Un seul etat tampered=true, causes en audit uniquement Booleen unique tampered dans state machine. reason_code uniquement dans payload audit C1 (natif), C5 (audit) Etat expose = booleen, log = reason_code Faible
INV-262-05-purge-on-detect Purge immediate secrets, tokens, caches crypto, temp, previews tamperingPurgeService.purgeAll() appele dans handleDetection() avant lockout final C4 (purge) Verification post-purge : cles nulles, tokens vides, caches supprimes Moyen — exhaustivite depend de la liste de cibles (Q-05)
INV-262-06-local-first Lockout/purge independants du reseau Purge et lockout Keychain executees avant tentative audit. Audit non bloquant (fire-and-forget) C3 (service), C4 (purge), C5 (audit) Test offline TC-NOM-05 : meme comportement Faible
INV-262-07-release-gating Inactif DEBUG/SIMULATOR, actif RELEASE/TF/PROD, QA par build flag compile Check #if DEBUG, TARGET_OS_SIMULATOR, ANTI_TAMPERING_QA_ENABLED dans le natif Swift. Pas de runtime config C1 (natif), C3 (service) Matrice environnement TC-NOM-06 Faible — compile-time, non mutable
INV-262-08-transition-model MONITORED->TAMPERED autorise, TAMPERED->LOCKED autorise, retour interdit State machine avec enum + guard dans transition(). Seules transitions explicitement autorisees passent C1 (natif) TC-NOM-11, TC-INV-08 : rejet transitions interdites Faible — exhaustif par enumeration
INV-262-09-terminal-state LOCKED_PERSISTENT -> * interdit Aucune methode de transition sortante depuis LOCKED_PERSISTENT. Check au boot : si LOCKED -> lockout immediat C1 (natif) TC-NOM-08, TC-NR-02 : redemarrages successifs restent locked Faible
INV-262-10-envelope-encryption Aucun secret temporaire en clair au repos Utilisation exclusive de Keychain (kSecAttr) pour persistance. Cles memoire via zeroize() a la purge. DEK/fragments chiffres AES-256-GCM C1 (natif), C4 (purge), crypto existant TC-NOM-10, TC-INV-10 : audit stockage local Moyen — depend du respect par les autres modules
INV-262-11-no-sensitive-telemetry Aucun dump memoire ni secret dans telemetrie Allowlist stricte des champs payload (§3.3). Validation regex avant envoi. Rejet si champ supplementaire C5 (audit) TC-NOM-07, TC-INV-11 : inspection payload Faible — allowlist stricte
INV-262-12-state-persistence Lockout persiste localement et re-applique au redemarrage lockout_persistent_flag en Keychain avec kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly. Lecture au boot dans initialize() C1 (natif) TC-NOM-08, TC-ERR-08 : persistance multi-reboot Faible — Keychain iOS fiable

4. Mapping criteres d'acceptation -> mecanismes

Critere ID Mecanisme(s) Composant Observable Risque
CA-01 initialize() execute performCheck() avant deblocage. Guard navigation bloque tant que check non termine C3, C7, C8 Traces audit local : trigger=cold_start avant acces secrets Faible
CA-02 onForeground() declenche via AppState listener dans useAntiTampering C3, C7 Compteur checks incremente par transition foreground Faible
CA-03 startPeriodicCheck(intervalSec) avec clamp : Math.max(30, Math.min(60, interval)) C3 Mesure intervalle reel entre checks Faible
CA-04 handleDetection() execute en < 1s : decision native synchrone + lockout Keychain immediat C1, C3 Timestamp detection vs timestamp lockout Moyen — budget P95
CA-05 State machine refuse transition TAMPERED->MONITORED (guard explicite) C1 Test non-regression TC-NOM-11 Faible
CA-06 LOCKED_PERSISTENT sans transition sortante + relecture au boot C1 Redemarrages successifs conservent lockout Faible
CA-07 purgeAll() couvre : cles memoire, tokens, K_master, secret bio, caches crypto, temp, previews C4 Verification post-purge de chaque cible Moyen (Q-05)
CA-08 purgeAll() exclut explicitement blobs persistants chiffres C4 Presence blobs post-lockout + store inaccessible Faible
CA-09 Payload construit par allowlist stricte §3.3, validation regex C5 Inspection payload : seuls champs autorises Faible
CA-10 Audit fire-and-forget apres lockout Keychain. Echec reseau = log local C5, C3 Test offline : meme lockout/purge Faible
CA-11 Gating compile-time : #if DEBUG, TARGET_OS_SIMULATOR, build flag ANTI_TAMPERING_QA_ENABLED C1, C3, C10 Matrice 6 environnements Faible
CA-12 Validation regex par champ avant envoi. Rejet si non conforme C5 TC-ERR-03 : payload invalide rejete Faible
CA-13 Catch global dans performCheck() -> reason_code=DETECTOR_ERROR -> fail-closed C1, C3 TC-NOM-09 : injection faute Faible
CA-14 Aucun secret temporaire en clair. Delegation aux modules crypto existants (AES-256-GCM, Keychain) C1, crypto existant Audit stockage local Moyen
CA-15 purgeAll() avec listes explicites : purge caches crypto, conserve blobs chiffres C4 Verification deux classes de donnees Faible
CA-16 Keychain access : kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly. Relecture au boot C1 Inspection metadonnees Keychain + boot Faible
CA-17 Absence lockout + absence first_launch_clean + app initialisee = LOCKED_PERSISTENT C1 TC-ERR-09 : simulation absence Faible
CA-18 Ecriture lockout_persistent_flag AVANT confirmation JS. Crash entre etats = LOCKED au reboot C1 TC-ERR-10 : kill process Moyen
CA-19 Retry purge : max 3, intervalle 1s, abandon avec log C4 Compteur retries + logs + etat final Faible

5. Mapping tests (TC-*) -> mecanismes + observables

Test ID Reference spec Mecanisme(s) Point(s) d'observation Niveau de test vise
TC-NOM-01 INV-262-03, INV-262-07, CA-01 initialize() + performCheck() + gating env Traces trigger=cold_start, tampered=false, etat=MONITORED Unit (service mock natif) + Integration
TC-NOM-02 INV-262-03, CA-02 AppState listener + onForeground() Compteur checks incremente par foreground Unit (hook) + Integration
TC-NOM-03 INV-262-03, CA-03 startPeriodicCheck() avec clamp [30..60] Intervalles mesures sur N>=10 executions Unit (timer mock)
TC-NOM-04 INV-262-01, INV-262-05, INV-262-08, CA-04, CA-07 handleDetection() + purgeAll() + state transition tampered=true, lockout <=1s P95, purge executee Integration + Perf
TC-NOM-05 INV-262-06, CA-10 Audit fire-and-forget, echec reseau silencieux Lockout/purge sans delai en offline Unit (mock reseau)
TC-NOM-06 INV-262-07, CA-11 Gating compile-time #if DEBUG + build flag Matrice 6 environnements Unit (conditions compilees)
TC-NOM-07 INV-262-11, CA-09, CA-12 Allowlist payload + validation regex §3.3 Payload conforme, aucun champ hors allowlist Unit (audit emitter)
TC-NOM-08 INV-262-09, INV-262-12, CA-06, CA-16 Keychain lockout_persistent_flag + relecture boot Lockout immediat multi-redemarrages Integration (mock Keychain)
TC-NOM-09 INV-262-01, ERR-01, CA-13 Catch global -> DETECTOR_ERROR -> fail-closed reason_code=DETECTOR_ERROR, lockout Unit (injection erreur)
TC-NOM-10 INV-262-10, CA-14 Verification stockage local au repos Aucun secret temporaire en clair Sec (audit stockage)
TC-NOM-11 INV-262-08, CA-05 State machine guard : TAMPERED->MONITORED refuse Transition rejetee Unit (state machine)
TC-NOM-12 CA-08, CA-15 purgeAll() + exclusion blobs chiffres Caches purges, blobs conserves Unit (purge service)
TC-ERR-02 ERR-02, INV-262-01 Timeout budget cold start -> fail-closed Lockout + journalisation timeout Unit (timeout simule)
TC-ERR-03 ERR-03, CA-12 Validation regex payload -> rejet envoi Log local rejet, lockout conserve Unit (audit emitter)
TC-ERR-04 ERR-04, INV-262-06 Reseau indisponible, audit echoue silencieusement Lockout conserve, pas retry bloquant Unit (mock reseau)
TC-ERR-05 ERR-05, INV-262-05, CA-19 purgeAll() echec partiel -> retry borne Lockout maintenu, retry enclenche Unit (purge service)
TC-ERR-06 ERR-06, INV-262-07 Runtime mutation ignoree, flag compile-time seul Anti-tampering inactif en simulateur malgre tentative Unit
TC-ERR-07 ERR-07, INV-262-04 Detection concurrente -> 1 lockout unique Unicite etat + cause primaire Unit (concurrence simulee)
TC-ERR-08 ERR-08, INV-262-12 Corruption Keychain -> fail-closed LOCKED_PERSISTENT au boot Unit (mock corruption)
TC-ERR-09 ERR-09, CA-17, INV-262-09, INV-262-12 Absence lockout + absence first_launch_clean LOCKED_PERSISTENT force Unit (mock absence)
TC-ERR-10 ERR-10, CA-18, INV-262-09, INV-262-12 Ecriture Keychain AVANT resolve Promise LOCKED_PERSISTENT au reboot apres crash Integration
TC-ERR-11 ERR-05, CA-19, INV-262-05 Retry purge : max 3, intervalle 1s Compteur retries, abandon, log Unit (purge service)
TC-INV-02 INV-262-02 Decision native, JS ne peut inhiber Lockout natif malgre hook JS Sec (adversarial) — integration sur device reel uniquement ; en CI unitaire, un test de contrat verifie que le mock respecte l'interface native mais ne peut pas prouver la resistance au hook JS
TC-INV-04 INV-262-04 Causes multiples -> 1 seul etat tampered Etat unique, causes en audit local Unit
TC-INV-08 INV-262-08 Transition TAMPERED->MONITORED refusee Seule TAMPERED->LOCKED autorisee Unit (state machine)
TC-INV-10 INV-262-10 Audit stockage local : aucun secret clair Tous artefacts chiffres Sec
TC-INV-11 INV-262-11 Allowlist payload stricte Champs §3.3 uniquement Unit (audit emitter)
TC-NR-01 INV-262-08 Guard transition TAMPERED->MONITORED Transition rejetee Unit (non-regression)
TC-NR-02 INV-262-09, INV-262-12, CA-06 Keychain lockout + N>=5 redemarrages Lockout immediat chaque reboot Integration
TC-NR-03 CA-04 Budget lockout <=1s P95 P95 mesure Perf
TC-NR-04 CA-03 Clamp [30..60] scheduler Intervalles observes aux bornes Unit
TC-NR-05 CA-12 Regex §3.3 chaque champ Rejet invalides Unit
TC-NR-06 INV-262-06, CA-10 Meme lockout online/offline Independance reseau Unit
TC-NEG-01 INV-262-11 Champ supplementaire dans payload -> rejet Log local rejet schema Unit
TC-NEG-02 CA-12 reason_code hors enum -> rejet Validation regex KO Unit
TC-NEG-03 INV-262-02 Tentative JS neutraliser lockout Echec, lockout natif maintenu Sec (adversarial) — integration sur device reel uniquement
TC-NEG-04 CA-03 Ports Frida invalides ignores Decision non degradee Unit
TC-NEG-05 CA-03 detection_interval_sec non numerique Defaut/clamp applique Unit
TC-NEG-06 INV-262-12 Corruption volontaire stockage lockout Fail-closed LOCKED_PERSISTENT Unit
TC-NEG-07 INV-262-04 Concurrence multi-causes rapide 1 seul lockout, cause primaire Unit

6. Gestion des erreurs

Code erreur Cas Traitement Observable
ERR-262-001 Detecteur retourne erreur interne reason_code=DETECTOR_ERROR, fail-closed, lockout Log local ERR-262-001
ERR-262-002 Timeout budget controle depasse (>1500ms P95 cold, >1s lockout) fail-closed, lockout, journalisation locale Log local ERR-262-002 + timestamp depassement
ERR-262-003 Payload audit invalide (format §3.3) Rejet envoi, lockout conserve, log local Log local ERR-262-003 + champ invalide
ERR-262-004 Reseau indisponible pendant reporting Lockout conserve, pas de retry bloquant UX Log local ERR-262-004
ERR-262-005 Purge partielle (sous-ensemble inaccessible) Lockout maintenu, retry borne (max 3, 1s), abandon + log Log local ERR-262-005 + compteur retries
ERR-262-006 Flag QA incoherent (simulateur) ou tentative runtime Module reste inactif, tentative ignoree Log local ERR-262-006
ERR-262-007 Concurrence multi-causes 1 lockout unique, cause primaire retenue, secondaires en log Log local multi-causes
ERR-262-008 Donnee lockout corrompue au boot fail-closed LOCKED_PERSISTENT Log local ERR-262-008
ERR-262-009 Lockout absent + first_launch_clean absent + app initialisee fail-closed LOCKED_PERSISTENT Log local ERR-262-009
ERR-262-010 Crash/kill entre TAMPERED_SESSION et LOCKED_PERSISTENT Etat reconstruit LOCKED_PERSISTENT au reboot Log local ERR-262-010

7. Impacts securite

7.1 Surface d'attaque

Risque Mitigation Composant
Hook JS neutralisant lockout natif Decision prise cote Swift avant retour Promise au JS. Lockout Keychain ecrit avant resolve() C1
Frida attachee apres cold start Controle periodique (45s) + re-check foreground C3
Contournement par suppression Keychain Absence lockout flag traitee en fail-closed si app deja initialisee (INV-262-09) C1
Exfiltration secrets post-detection Purge immediate avant lockout UI, delai max 1s P95 C4
Faux positif en production Flag DETECTOR_ERROR distingue des detections valides ; audit forensic disponible C5
Memory dump avant purge Fenetre de risque inherente ; zeroize() best-effort JavaScript (limitation connue, cf. §9) C4, crypto/zeroize.ts

7.2 Journalisation securite

  • Traces locales structurees (trigger, reason_code, decision, purge, transition, emission).
  • Horodatage ISO-8601 UTC obligatoire.
  • Aucune donnee sensible dans les logs (ni locaux, ni telemetrie).

7.3 Conformite

  • INV-262-10 : chiffrement au repos pour tout artefact temporaire (AES-256-GCM / Keychain).
  • INV-262-11 : allowlist stricte payload audit.
  • RGPD : device_id_pseudo = hash SHA-256 one-way (pseudo-anonymisation).

8. Hypotheses techniques

ID Hypothese Impact si faux
HT-01 Le module natif Swift peut etre integre dans un projet Expo managed via config plugin ou prebuild. Si Expo managed ne supporte pas le module custom, un passage en bare workflow (prebuild) est necessaire. Reconfiguration build iOS. Le projet utilise deja react-native-keychain via plugin config, le pattern est etabli.
HT-02 UIDevice.current.identifierForVendor est disponible a l'initialisation du module natif et retourne une valeur stable pour la duree de vie de l'installation. Si indisponible, le device_id_pseudo est vide et l'evenement audit rejete (lockout conserve).
HT-03 Les controles natifs (paths, fork, dylib, ports) s'executent en < 1500ms P95 sur iPhone 12+ iOS 16+. Si depassement, fail-closed applique (pas de degradation gracieuse).
HT-04 Le Keychain iOS avec kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly survit au reboot et n'est pas efface par l'utilisateur sans reinstallation. Si faux, le lockout persistant serait contournable par reboot. Risque faible — c'est le comportement documente Apple.
HT-05 Les blobs locaux deja chiffres sont effectivement inutilisables sans secrets purges (K_master). Si faux, risque de recuperation indirecte. Mitigation : architecture crypto PD-98 garantit que sans K_master, aucun dechiffrement possible.
HT-06 Le zeroize() JavaScript existant (src/crypto/zeroize.ts) est best-effort (JavaScript GC non deterministe). Les secrets natifs Swift peuvent etre zeroes de maniere deterministe via memset_s. Si zeroize JS echoue silencieusement, fenetre de risque memoire. Mitigation : les secrets critiques (K_master, K_bio) sont en Keychain natif, pas en JS.
HT-07 L'ecriture Keychain lockout_persistent_flag=true est atomique (pas d'etat partiel en cas de crash mid-write). Si non atomique, le fail-closed boot reconstruit l'etat en LOCKED_PERSISTENT quand meme (protection double).
HT-08 Le backend dispose d'un endpoint pour recevoir l'evenement audit ANTI_TAMPERING_LOCKOUT ou peut etre etendu (PD-283). Si non disponible au moment de l'implementation, l'emission est un no-op loggue localement. Pas de perte de securite locale ; uniquement perte de visibilite forensic distante. STUB: PD-283 — endpoint backend audit anti-tampering.

9. Points de vigilance (risques, dette, pieges)

9.1 Risques techniques

  1. Expo managed vs bare workflow : Le projet utilise Expo managed. L'ajout d'un module natif Swift custom peut necessiter expo prebuild. Verifier que le pipeline EAS supporte ce changement. Le projet a deja un dossier ios/ avec AppDelegate.swift, ce qui suggere un mode bare/prebuild deja en place.

  2. Race condition crash mid-transition : Si le process est tue entre TAMPERED_SESSION et l'ecriture LOCKED_PERSISTENT en Keychain, l'etat doit etre reconstruit en fail-closed. Le mecanisme de reconstruction (F6) couvre ce cas via first_launch_clean et absence de lockout flag.

  3. Fenetre de risque memoire : Entre la detection et la completion de zeroize(), les secrets sont potentiellement en memoire. Mitigation : les secrets critiques (K_master, K_bio) sont dans le Keychain natif iOS, pas directement en memoire JS.

  4. Faux positifs : Certains controles (paths, fork) peuvent avoir des faux positifs sur des configurations iOS non standard. Le DETECTOR_ERROR est distinct des detections valides, mais un faux positif sur un controle specifique (ex: path qui existe pour une raison legitime) declenchera quand meme un lockout (fail-closed by design).

9.2 Dette technique

  • Q-05 (spec) : La liste exhaustive/versionnee des emplacements de caches/temporaires a purger n'est pas contractualisee. Le purge service (C4) devra maintenir une liste interne mise a jour avec chaque nouveau module qui cree des caches.
  • Q-03 (spec) : La politique precise de retry backend (nombre max, backoff) n'est pas definie. L'implementation utilise un fire-and-forget simple (1 tentative, pas de retry backend).
  • Q-04 (spec) : Le niveau de detail des causes secondaires en audit local n'est pas contractualise. L'implementation stocke les causes secondaires en log local sans format garanti.

9.3 Stubs inter-PD

  • STUB: PD-283 (endpoint audit backend) : L'emission d'evenement ANTI_TAMPERING_LOCKOUT suppose un endpoint backend /audit/events. Si non disponible au moment de l'implementation, l'emission est un no-op avec log local. Story de destination : PD-283 (a creer dans le backlog backend epic BACKEND-CORE).

10. Hors perimetre

Conformement a la specification §2 :

  • Root detection Android.
  • Wipe complet automatique des donnees persistantes chiffrees (blobs conserves, cf. INV-262-05).
  • Remote unlock / deblocage distant.
  • Detection proxy SSL/MITM.
  • Obfuscation Swift.
  • Toute logique de contournement UX apres lockout.
  • Signature cryptographique de l'evenement audit (non specifiee).
  • Toute regle non testable.

11. Perimetre de test

Niveau de test In scope Hors scope (justification)
Unitaire Tous les composants C1-C8 (services, state machine, purge, audit, gating, hook, types) --
Integration Interactions C1<->C2 (natif<->bridge), C3<->C4 (service<->purge), C3<->C5 (service<->audit), C7<->C8 (hook<->store/navigation) --
E2E Flux complet cold start + detection + lockout + redemarrage (Detox sur device reel) E2E backend (endpoint audit) — endpoint backend non existant, STUB: PD futur
Performance Budget P95 : lockout <=1s, cold start controle <=1500ms (N>=100 executions sur device reel) --
Securite (adversarial) TC-INV-02, TC-NEG-03 : hook JS sans effet sur lockout natif Pentest externe complet (hors scope story)

Tous les niveaux de test sont couverts, a l'exception de l'integration E2E avec le backend audit (stub inter-PD). La couverture minimale attendue (80%) s'applique au perimetre in scope.


12. Mecanismes cross-module

Interactions avec modules existants (lecture/deletion, pas de modification de routes)

Module existant Interaction Type
keychainStorage.ts (PD-98) deleteMasterKey() appele par tamperingPurgeService Appel service existant
biometricKeychain.ts (PD-107) deleteBiometricSecret() appele par tamperingPurgeService Appel service existant
useAuthStore clearTokens() appele par tamperingPurgeService Appel store existant
crypto/zeroize.ts zeroize() appele par tamperingPurgeService Appel utilitaire existant
useSecurityStore Extension avec tamperingLockout: boolean Modification store existant
Navigation Ajout guard conditionnel vers TamperingLockoutScreen Modification navigation existante

Aucune modification des routes d'autres modules. Les interactions sont des appels de services existants pour la purge et des extensions mineures du store/navigation pour l'integration du lockout.