Aller au contenu

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) :

{
  "email": "user@example.com",
  "password": "cleartext_password",
  "storedAt": 1707900000000
}

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.