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¶
-
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 dossierios/avec AppDelegate.swift, ce qui suggere un mode bare/prebuild deja en place. -
Race condition crash mid-transition : Si le process est tue entre
TAMPERED_SESSIONet l'ecritureLOCKED_PERSISTENTen Keychain, l'etat doit etre reconstruit en fail-closed. Le mecanisme de reconstruction (F6) couvre ce cas viafirst_launch_cleanet absence de lockout flag. -
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.
-
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_LOCKOUTsuppose 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.