Plan d'Implémentation — PD-107 Authentification Biométrique iOS¶
Version : 1.1 Date : 2026-02-14 Auteur : Claude (Orchestrateur) Statut : Corrigé post Gate 5 v1 (RESERVE)
1. Analyse de l'existant¶
1.1 Code existant (à refactorer ou étendre)¶
| Fichier | Story | État actuel | Changements requis PD-107 |
|---|---|---|---|
services/biometric.ts | PD-99 | Stocke MDP via SecureStore | Refactor complet : stocker clé intermédiaire, pas MDP |
services/keychainStorage.ts | PD-98 | K_master avec WHEN_UNLOCKED | Extension : ajouter biometryCurrentSet |
hooks/useBiometric.ts | PD-99 | Hook basique | Remplacer par useBiometricSettings + useAppUnlock |
hooks/useAutoLock.ts | PD-174 | Timer + background | Étendre : intégrer policy engine |
store/useSecurityStore.ts | PD-174 | Zustand autolock | Étendre : ajouter état biométrie |
1.2 Code à créer¶
| Fichier | Type | Description |
|---|---|---|
ios/BiometricKeychainModule.swift | Native | Module iOS Keychain avec biometryCurrentSet |
services/biometricKeychain.ts | Bridge | Bridge JS pour module natif |
services/unlockOrchestrator.ts | Service | Machine à états unlock + policy engine |
services/auditService.ts | Service | File d'audit append-only + signature |
hooks/useBiometricSettings.ts | Hook | Activation/désactivation biométrie |
hooks/useAppUnlock.ts | Hook | Flux de déverrouillage app |
screens/settings/BiometricSettingsScreen.tsx | Screen | Écran paramètres biométrie |
components/unlock/BiometricPrompt.tsx | Component | UI prompt biométrique |
components/unlock/PasswordFallback.tsx | Component | UI fallback mot de passe |
2. Architecture technique¶
2.1 Diagramme de dépendances¶
┌─────────────────────────────────────────────────────────────────────────┐
│ UI Layer │
├─────────────────────────────────────────────────────────────────────────┤
│ BiometricSettingsScreen │ LockScreen (BiometricPrompt + │
│ (activation/désactivation) │ PasswordFallback) │
└───────────────┬─────────────┴────────────────┬──────────────────────────┘
│ │
▼ ▼
┌───────────────────────────────┐ ┌───────────────────────────────────────┐
│ useBiometricSettings │ │ useAppUnlock │
│ - capability │ │ - state machine │
│ - enable/disable │ │ - policy evaluation │
│ - password confirmation │ │ - failed attempts │
└───────────────┬───────────────┘ └───────────────┬───────────────────────┘
│ │
└──────────────┬───────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ UnlockOrchestrator │
│ - evaluateUnlockPolicy() - unlockWithBiometric() │
│ - enableBiometricWithPassword() - unlockWithPassword() │
│ - disableBiometric() - recordAttempt() → AuditService │
└───────────────┬─────────────────────────────────┬───────────────────────┘
│ │
┌───────┴───────┐ ┌──────┴──────┐
▼ ▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌────────────┐ ┌────────────────┐
│BiometricService│ │KeychainService│ │PolicyEngine│ │ AuditService │
│(LocalAuth API) │ │(Native Module)│ │(timeout, │ │(queue + sign) │
└───────────────┘ └───────┬───────┘ │boot,lockout│ └───────┬────────┘
│ └────────────┘ │
▼ ▼
┌───────────────────────┐ ┌─────────────────┐
│BiometricKeychainModule│ │ Backend API │
│(Swift native iOS) │ │ /audit/events │
│• biometryCurrentSet │ └─────────────────┘
│• Secure Enclave │
└───────────────────────┘
2.2 Machine à états (UnlockOrchestrator)¶
stateDiagram-v2
[*] --> STARTUP_CHECK
STARTUP_CHECK --> REQUIRE_PASSWORD: boot_detected OR timeout_exceeded OR lockout OR policy_fail
STARTUP_CHECK --> PROMPT_BIOMETRIC: biometric_enabled AND policy_ok
PROMPT_BIOMETRIC --> UNLOCKED: biometric_success AND decrypt_ok
PROMPT_BIOMETRIC --> BIOMETRIC_FAILED: biometric_fail
BIOMETRIC_FAILED --> PROMPT_BIOMETRIC: attempts < 3
BIOMETRIC_FAILED --> REQUIRE_PASSWORD: attempts >= 3
REQUIRE_PASSWORD --> UNLOCKED: password_success (SRP-6a)
REQUIRE_PASSWORD --> REQUIRE_PASSWORD: password_fail
UNLOCKED --> LOCKED: app_background OR lock_event OR timeout
LOCKED --> STARTUP_CHECK: app_foreground
note right of STARTUP_CHECK
Policy checks:
- deviceRebootDetected
- lastStrongAuthTs > 30min
- failedAttempts >= 3
- passwordChangedAt recent
end note 3. Décomposition en tâches¶
Phase 1 : Infrastructure native (2 tâches)¶
| # | Tâche | Agent | Fichiers | Dépendances |
|---|---|---|---|---|
| T1 | Module natif iOS Keychain | agent-developer | ios/BiometricKeychainModule.swift, ios/BiometricKeychainModule.m | Aucune |
| T2 | Bridge TypeScript Keychain | agent-developer | services/biometricKeychain.ts, services/biometricKeychain.types.ts | T1 |
Phase 2 : Services core (3 tâches)¶
| # | Tâche | Agent | Fichiers | Dépendances |
|---|---|---|---|---|
| T3 | Refactor BiometricService | agent-developer | services/biometric.ts (refactor) | T2 |
| T4 | PolicyEngine (rules) | agent-developer | services/policyEngine.ts | Aucune |
| T5 | UnlockOrchestrator | agent-developer | services/unlockOrchestrator.ts | T3, T4 |
Phase 3 : Audit & Logging (2 tâches)¶
| # | Tâche | Agent | Fichiers | Dépendances |
|---|---|---|---|---|
| T6 | AuditService (file locale) | agent-developer | services/auditService.ts, services/auditQueue.ts | Aucune |
| T7 | Intégration audit → orchestrator | agent-developer | services/unlockOrchestrator.ts (update) | T5, T6 |
Phase 4 : Hooks React (2 tâches)¶
| # | Tâche | Agent | Fichiers | Dépendances |
|---|---|---|---|---|
| T8 | useBiometricSettings hook | agent-developer | hooks/useBiometricSettings.ts | T5 |
| T9 | useAppUnlock hook | agent-developer | hooks/useAppUnlock.ts | T5, T8 |
Phase 5 : UI Components (3 tâches)¶
| # | Tâche | Agent | Fichiers | Dépendances |
|---|---|---|---|---|
| T10 | BiometricPrompt component | agent-developer | components/unlock/BiometricPrompt.tsx | T9 |
| T11 | PasswordFallback component | agent-developer | components/unlock/PasswordFallback.tsx | T9 |
| T12 | BiometricSettingsScreen | agent-developer | screens/settings/BiometricSettingsScreen.tsx | T8, T10 |
Phase 6 : Intégration & Tests (3 tâches)¶
| # | Tâche | Agent | Fichiers | Dépendances |
|---|---|---|---|---|
| T13 | Extension SecurityStore | agent-developer | store/useSecurityStore.ts (extend), store/useBiometricStore.ts | T8, T9 |
| T14 | Navigation + i18n | agent-developer | navigation/AppNavigator.tsx, i18n/locales/fr/settings.json, i18n/locales/en/settings.json | T12 |
| T15 | Tests unitaires services | agent-qa-unit | __tests__/services/biometric*.test.ts, __tests__/services/unlockOrchestrator.test.ts | T1-T7 |
Phase 7 : Tests avancés (2 tâches)¶
| # | Tâche | Agent | Fichiers | Dépendances |
|---|---|---|---|---|
| T16 | Tests hooks + intégration | agent-qa-unit | __tests__/hooks/useBiometric*.test.ts, __tests__/hooks/useAppUnlock.test.ts | T8, T9, T15 |
| T17 | Tests E2E Detox | agent-qa-ihm | e2e/biometricActivation.e2e.ts, e2e/biometricUnlock.e2e.ts | T10-T14 |
Total : 17 tâches
4. Décisions techniques¶
4.1 Expo prebuild vs native module¶
Décision : Expo prebuild avec config plugin natif
Justification : - expo-local-authentication pour le prompt biométrique (UX) - Module natif Swift pour Keychain avec kSecAccessControlBiometryCurrentSet - expo-secure-store insuffisant (pas d'access control biométrique)
Configuration requise :
// app.json
{
"expo": {
"plugins": [
["./plugins/withBiometricKeychain.js"]
],
"ios": {
"infoPlist": {
"NSFaceIDUsageDescription": "Déverrouiller votre coffre-fort ProbatioVault"
}
}
}
}
4.2 Stockage secret intermédiaire¶
Problème : PD-99 stocke le mot de passe → violation INV-107-01/02
Solution PD-107 : 1. À l'activation, dériver une clé intermédiaire K_bio via HKDF 2. Stocker K_bio dans Keychain avec biometryCurrentSet 3. Stocker envelope = AES-GCM(K_bio, session_secret) 4. Au déverrouillage, K_bio déchiffre l'envelope → accès session
Activation:
password → SRP-6a → session_proof → HKDF("biometric-unlock", session_proof) → K_bio
K_bio → Keychain (biometryCurrentSet)
session_secret → AES-GCM(K_bio) → envelope → SecureStore
Déverrouillage:
biometric → K_bio (Keychain) → decrypt(envelope) → session_secret → unlock
4.3 Détection changement biométrique système¶
Mécanisme iOS : - kSecAccessControlBiometryCurrentSet invalide l'item Keychain si empreinte/visage ajouté - Lecture Keychain retourne erreur -25293 (errSecAuthFailed) - Le bridge détecte cette erreur → BiometricSetChangedError - UnlockOrchestrator → état REVOKED + purge artefacts + fallback MDP
4.4 Policy Engine (règles)¶
type UnlockPolicy = {
requirePassword: boolean;
reason?: UnlockRequireReason;
};
type UnlockRequireReason =
| "DEVICE_BOOT" // Redémarrage détecté
| "TIMEOUT_EXCEEDED" // > 30 min inactivité
| "LOCKOUT" // >= 3 échecs biométriques
| "PASSWORD_CHANGED" // MDP changé récemment
| "BIOMETRIC_REVOKED" // Biométrie système modifiée
| "FIRST_UNLOCK"; // Premier unlock post-install
function evaluatePolicy(context: UnlockPolicyContext): UnlockPolicy {
if (context.deviceRebootDetected) {
return { requirePassword: true, reason: "DEVICE_BOOT" };
}
if (context.passwordChangedAt && isRecent(context.passwordChangedAt)) {
return { requirePassword: true, reason: "PASSWORD_CHANGED" };
}
if (context.failedBiometricAttempts >= 3) {
return { requirePassword: true, reason: "LOCKOUT" };
}
if (inactivityExceeded(context, 30 * 60 * 1000)) {
return { requirePassword: true, reason: "TIMEOUT_EXCEEDED" };
}
return { requirePassword: false };
}
4.5 Audit logging¶
Événements (tous signés backend) :
| Event | Champs | Trigger |
|---|---|---|
BIOMETRIC_ENABLED | userId, deviceId, biometryType | Activation réussie |
BIOMETRIC_DISABLED | userId, deviceId, reason | Désactivation |
BIOMETRIC_AUTH_SUCCESS | userId, deviceId, unlockDurationMs | Déverrouillage OK |
BIOMETRIC_AUTH_FAILURE | userId, deviceId, attemptCount, errorCode | Échec biométrique |
BIOMETRIC_REVOKED_SYSTEM_CHANGE | userId, deviceId | Empreinte/visage modifié |
PASSWORD_AUTH_FALLBACK | userId, deviceId, reason | Bascule vers MDP |
File locale : - Append-only avec hash chain (prevHash) - Chiffré AES-GCM avec clé device - Sync vers backend avec retry exponentiel - Idempotency key pour déduplication
5. Gestion des erreurs¶
| Code | Condition | Récupération |
|---|---|---|
ERR-107-001 | Biométrie indisponible | Désactiver toggle, proposer MDP |
ERR-107-010 | MDP invalide à l'activation | Retry avec anti-bruteforce SRP |
ERR-107-020 | Échec écriture Keychain | Retry 3x, sinon erreur utilisateur |
ERR-107-030 | Échec reconnaissance biométrique | Incrémenter compteur, retry si < 3 |
ERR-107-031 | Prompt annulé | Proposer MDP immédiatement |
ERR-107-050 | Compteur incohérent | Forcer MDP + reset compteur |
ERR-107-060 | Biométrie système modifiée | État REVOKED + MDP + log audit |
ERR-107-070 | Horloge incohérente | Forcer MDP (fail-safe) |
ERR-107-090 | Audit backend indisponible | Queue locale + retry |
6. Écarts Gate 3 traités¶
| ID | Description | Traitement dans ce plan |
|---|---|---|
| ECT-01 | Blur screen background non détaillé | Utiliser AppState listener + composant BlurOverlay (section 4.6) |
| ECT-02 | Détection jailbreak non spécifiée | Hors-scope MVP (log optionnel si jailbreak-detect lib) |
| ECT-03 | iOS 15.0 minimum | Documenté dans app.json + build check |
4.6 Blur screen en background (ECT-01)¶
// Dans App.tsx ou composant racine
useEffect(() => {
const subscription = AppState.addEventListener("change", (state) => {
if (state === "background" || state === "inactive") {
// Activer blur overlay
setShowBlurOverlay(true);
} else if (state === "active") {
// Désactiver après délai (éviter flash)
setTimeout(() => setShowBlurOverlay(false), 100);
}
});
return () => subscription.remove();
}, []);
4.7 Stratégie de migration PD-99 → PD-107 (AMB-01)¶
Contexte : PD-99 stocke directement { email, password } dans SecureStore. PD-107 utilise un schéma cryptographiquement sécurisé avec K_bio + envelope.
Format legacy (PD-99) :
Format PD-107 :
{
"version": 2,
"userId": "uuid",
"deviceId": "uuid",
"wrappedUnlockBlob": "base64_aes_gcm_ciphertext",
"keyId": "K_bio_id",
"createdAt": "2026-02-14T12:00:00Z"
}
Algorithme de migration (exécuté dans T3) :
async function migrateFromPD99(): Promise<MigrationResult> {
// 1. Détecter format legacy
const legacyData = await SecureStore.getItemAsync("pv_biometric_credentials_*");
if (!legacyData) return { migrated: false, reason: "NO_LEGACY_DATA" };
const parsed = JSON.parse(legacyData);
// 2. Vérifier version
if (parsed.version === 2) return { migrated: false, reason: "ALREADY_MIGRATED" };
if (!parsed.password || !parsed.email) {
// Corruption détectée → purger et forcer réactivation
await clearLegacyData();
return { migrated: false, reason: "CORRUPTED_LEGACY" };
}
// 3. Demander réauthentification SRP-6a avec le password legacy
// Note: Le password est temporairement en mémoire le temps de la migration
const srpResult = await srpAuthenticate(parsed.email, parsed.password);
if (!srpResult.success) {
// Password legacy invalide → purger
await clearLegacyData();
return { migrated: false, reason: "LEGACY_PASSWORD_INVALID" };
}
// 4. Générer nouveau schéma PD-107
const kBio = await deriveKBio(srpResult.sessionProof);
const envelope = await encryptEnvelope(kBio, srpResult.sessionSecret);
// 5. Stocker dans nouveau format
await biometricKeychain.storeBiometricSecret("pv_k_bio", kBio);
await SecureStore.setItemAsync("pv_biometric_envelope", JSON.stringify({
version: 2,
...envelope
}));
// 6. Purger legacy data
await clearLegacyData();
// 7. Zeroize sensitive buffers
zeroize(kBio);
zeroize(parsed.password);
return { migrated: true, reason: "SUCCESS" };
}
Points critiques : - Migration silencieuse au premier unlock post-update - Si migration échoue → biométrie désactivée, utilisateur doit réactiver - Aucune donnée legacy conservée après migration - Test de non-régression : TC-107-058 (legacy migration success), TC-107-059 (corrupted legacy)
Rollback : Pas de rollback possible (nouveau format seulement). En cas d'échec, utilisateur doit réactiver manuellement.
4.8 Playbooks de mitigation avec seuils (AMB-05)¶
| Risque | Seuil de décision | Plan A | Plan B | Owner | Délai max |
|---|---|---|---|---|---|
| Module natif Swift bloquant | T1 prend > 2 jours | Template Expo modules | Utiliser react-native-keychain avec accessControl: BIOMETRY_CURRENT_SET | Lead Dev | 2 jours |
| Simulateur biométrie limitée | Tests E2E échouent sur simulateur | Detox device.matchFace() | Tests manuels device réel uniquement + logs assertions | QA Lead | 1 jour |
| Migration PD-99 corrompt données | > 5% utilisateurs impactés | Migration automatique T3 | Désactiver auto-migration, forcer réactivation manuelle | PO | 0.5 jour |
| Audit backend indisponible | Queue locale > 500 events | Retry exponentiel (max 6h) | Fail-open avec log local chiffré, sync au retour backend | SRE | 6 heures |
4.9 Limites queue audit locale (AMB-06)¶
Configuration :
const AUDIT_QUEUE_CONFIG = {
maxEvents: 1000,
maxAgeHours: 72,
retryBackoffMs: [1000, 5000, 30000, 60000, 300000], // 1s, 5s, 30s, 1min, 5min
maxRetries: 10,
};
Comportement si limite atteinte : 1. Si queue.length >= maxEvents : supprimer les événements les plus anciens (FIFO) 2. Si event.age > maxAgeHours : marquer comme "expired" et supprimer 3. Si retries > maxRetries : déplacer vers Dead Letter Queue (DLQ) locale 4. DLQ locale : max 100 événements, nettoyée après sync réussie
7. Risques et mitigations¶
| Risque | Probabilité | Impact | Mitigation |
|---|---|---|---|
| Module natif Swift complexe | Moyenne | Fort | Utiliser template Expo modules, tests device réel |
| Hermes sans WebAssembly | Confirmé | Moyen | Crypto via module natif (déjà résolu PD-98) |
| Simulateur limité pour biométrie | Confirmé | Moyen | Tests manuels device + Detox avec device.matchFace() |
| Migration PD-99 → PD-107 | Moyenne | Fort | Migration automatique au premier unlock (detect legacy format) |
8. Ordre d'exécution recommandé¶
Phase 1 (Native) │ T1 ──┬──> T2
│ │
Phase 2 (Services) │ └──> T3 ──┬──> T5
│ │
│ T4 ───────┘
│
Phase 3 (Audit) │ T6 ────────────────> T7
│ │
Phase 4 (Hooks) │ └──> T8 ──┬──> T9
│ │
Phase 5 (UI) │ └──> T10, T11 ──> T12
│
Phase 6 (Intégration)│ T13 (parallel T8-T9), T14 (after T12), T15 (after T1-T7)
│
Phase 7 (Tests) │ T16 (after T15), T17 (after T14)
Durée estimée : 3-4 jours d'implémentation multi-agents
9. Critères de succès (Gate 5)¶
- Tous les fichiers listés sont identifiés avec chemin exact
- Dépendances inter-tâches cohérentes
- Architecture respecte les invariants INV-107-01 à INV-107-10
- Code contracts couvrent les 17 tâches
- Risques identifiés avec mitigations
- Écarts Gate 3 adressés
Annexe : Mapping vers Code Contracts¶
Voir PD-107-code-contracts.yaml pour les contrats détaillés par tâche.