Spécification Technique — PD-107 Authentification Biométrique iOS
Version: 1.0 Date: 2026-02-14 Auteur: ChatGPT (GPT-5.3-codex) Statut: Prêt pour Gate 3 (CONFORMITY_CHECK) Références: PD-24/25 (SRP-6a), PD-33/97 (crypto), PD-37 (audit HSM), PD-42 (Zero-Knowledge)
1) Architecture technique
1.1 Diagramme de composants
flowchart LR
UI[UI React Native\nSettings + Unlock Screen] --> UO[Unlock Orchestrator]
UO --> LS[LocalAuth Adapter\nexpo-local-authentication]
UO --> KC[Keychain Adapter\nnative iOS SecItem\nbiometryCurrentSet]
UO --> CS[Crypto Service\nAES-GCM / HKDF / Argon2id bridge]
UO --> PS[Policy Engine\n(timeout, boot, attempts)]
UO --> SS[Secure Storage\nmetadata non sensible]
UO --> AQ[Audit Queue locale\nappend-only hash chain]
AQ --> API[NestJS Audit API]
API --> HSM[HSM Signing Service]
SRP[SRP-6a Auth Module] --> UO
CS --> KM[K_encryption lifecycle\nmémoire volatile]
1.2 Flux biométrie -> déverrouillage (nominal)
sequenceDiagram
participant U as User
participant App as Unlock Orchestrator
participant LA as LocalAuthentication
participant KC as iOS Keychain
participant C as Crypto Service
participant A as Audit API
U->>App: Ouvre l'app
App->>App: Policy checks (boot/timeout/attempts/session)
App->>LA: Prompt Face ID / Touch ID
LA-->>App: Success
App->>KC: Lire secret intermédiaire (biometryCurrentSet)
KC-->>App: K_bio_intermediate
App->>C: Déchiffrer envelope local + dériver clé session
C-->>App: K_encryption en mémoire (volatile)
App->>A: BIOMETRIC_AUTH_SUCCESS (queued if offline)
App-->>U: Coffre-fort déverrouillé (<1s cible)
1.3 Intégration avec modules existants
PD-33/PD-97: réutilisation AES-GCM + KDF; aucun secret persistant en clair. PD-24/PD-25: SRP-6a reste obligatoire pour login initial et fallback mot de passe. PD-37: événements audités côté backend, horodatage serveur + signature HSM. PD-42: respect de la hiérarchie de clés; la biométrie ne remplace pas le secret cryptographique racine.
1.4 Décision d'implémentation Expo/iOS
- Cible recommandée: Expo prebuild (managed + config plugin natif).
- Motif: besoin explicite de
kSecAccessControlBiometryCurrentSet + kSecAttrAccessibleWhenUnlockedThisDeviceOnly. expo-local-authentication: prompt biométrique UX. - Keychain sécurisé: module natif iOS (bridge) pour garantir les attributs de sécurité requis.
2) Spécifications fonctionnelles détaillées
F-107-01 — Détection capacité biométrique
- Description: détecte disponibilité biométrie + type (
FACE_ID/TOUCH_ID) et état d'enrôlement. - Entrées: état device iOS, permissions biométrie.
- Sorties:
BiometricCapability { available, enrolled, type, reasonIfUnavailable }. - Pré-conditions: app lancée.
- Post-conditions: UI settings adaptée (toggle activable/inactivable).
- Erreurs: capteur indisponible ->
ERR-107-001.
F-107-02 — Activation biométrie
- Description: active biométrie après confirmation mot de passe.
- Entrées: mot de passe valide (SRP session active), consentement explicite utilisateur.
- Sorties: état
ENABLED, secret intermédiaire stocké Keychain, log BIOMETRIC_ENABLED. - Pré-conditions:
F-107-01.available=true, utilisateur authentifié. - Post-conditions: clé intermédiaire protégée biométrie; compteur échecs remis à 0.
- Erreurs: mot de passe invalide
ERR-107-010, Keychain write fail ERR-107-020.
F-107-03 — Désactivation biométrie
- Description: supprime les artefacts biométriques locaux.
- Entrées: action utilisateur toggle OFF.
- Sorties: état
DISABLED, suppression item Keychain + metadata, log BIOMETRIC_DISABLED. - Pré-conditions: biométrie active.
- Post-conditions: biométrie non utilisable sans réactivation.
- Erreurs: suppression Keychain partielle
ERR-107-021 (retry + fail-safe disabled).
F-107-04 — Déverrouillage biométrique nominal
- Description: déverrouille via Face ID/Touch ID.
- Entrées: session locale verrouillée, biométrie active.
- Sorties: accès coffre,
K_encryption en mémoire volatile, log BIOMETRIC_AUTH_SUCCESS. - Pré-conditions: policy autorise biométrie (pas boot lock, pas timeout >30 min, pas lockout).
- Post-conditions: durée
unlockDurationMs mesurée; compteur échecs reset. - Erreurs: annulation user
ERR-107-031, mismatch biométrique ERR-107-030.
F-107-05 — Fallback mot de passe
- Description: bascule vers authentification mot de passe SRP-6a.
- Entrées: choix utilisateur ou policy imposée.
- Sorties: authentification via flux SRP standard, log
PASSWORD_AUTH_FALLBACK. - Pré-conditions: écran unlock affiché.
- Post-conditions: si succès, accès normal + possibilité de réinitialiser état lockout.
- Erreurs: SRP fail
ERR-107-040.
F-107-06 — Gestion échecs biométriques
- Description: verrouille biométrie locale après 3 échecs consécutifs.
- Entrées: événements d'échec LocalAuthentication.
- Sorties: compteur incrémenté,
REQUIRE_PASSWORD si >=3, log BIOMETRIC_AUTH_FAILURE. - Pré-conditions: biométrie active.
- Post-conditions: mot de passe obligatoire jusqu'à succès SRP.
- Erreurs: désynchronisation compteur
ERR-107-050.
F-107-07 — Révocation sur changement biométrique système
- Description: invalide l'accès biométrique si set biométrique iOS modifié.
- Entrées: lecture Keychain invalide / domaine biométrique modifié.
- Sorties: état
REVOKED, purge artefacts, fallback mot de passe, log BIOMETRIC_REVOKED_SYSTEM_CHANGE. - Pré-conditions: biométrie active.
- Post-conditions: réactivation manuelle obligatoire.
- Erreurs: impossible de distinguer cause ->
ERR-107-060 (traité comme révocation).
F-107-08 — Politique timeout/inactivité
- Description: exige mot de passe après >30 minutes d'inactivité.
- Entrées: timestamps app lifecycle, dernier unlock fort.
- Sorties:
REQUIRE_PASSWORD si seuil dépassé. - Pré-conditions: app revient foreground.
- Post-conditions: pas de déverrouillage biométrique tant que SRP non refait.
- Erreurs: horloge incohérente
ERR-107-070 (fail-safe -> mot de passe).
F-107-09 — Révocation après événements forts
- Description: impose réactivation manuelle après réinstallation ou changement mot de passe.
- Entrées: app installId changé, event backend
PASSWORD_CHANGED. - Sorties: état
DISABLED, logs BIOMETRIC_DISABLED reason. - Pré-conditions: événement détecté.
- Post-conditions: utilisateur doit repasser F-107-02.
- Erreurs: event backend manquant
ERR-107-080 (pessimiste -> disable).
F-107-10 — Journalisation probatoire
- Description: journalise tous événements auth, append-only, horodatés serveur, signés HSM.
- Entrées: événements locaux, deviceId, userId, contexte.
- Sorties: payload audit envoyé backend + ACK signé.
- Pré-conditions: connectivité ou file locale persistante.
- Post-conditions: aucun événement perdu (at-least-once + idempotency key).
- Erreurs: API indisponible
ERR-107-090 (queue locale chiffrée + retry exponentiel).
3) Invariants techniques (INV-107-XX)
| ID | Invariant | Vérification technique |
| INV-107-01 | La biométrie ne remplace pas le mot de passe cryptographique | State machine impose SRP sur événements forts (boot, timeout, lockout, pwd change) + tests E2E |
| INV-107-02 | Biométrie ne dérive jamais K_encryption directement | K_encryption uniquement via envelope + secret intermédiaire, revue code crypto |
| INV-107-03 | Biométrie déverrouille un secret encapsulé seulement | Contrat de UnlockOrchestrator: jamais de deriveFromBiometryRaw |
| INV-107-04 | Aucun bypass SRP-6a | Guards backend/session; route unlock locale n'émet jamais token auth serveur |
| INV-107-05 | Aucune clé brute persistée en clair | Audit storage + tests snapshot storage vide de secrets |
| INV-107-06 | Données biométriques brutes jamais collectées | Utilisation exclusive API iOS LocalAuthentication (bool success/failure) |
| INV-107-07 | Changement biométrie système révoque automatiquement | Keychain access control biometryCurrentSet; test device réel |
| INV-107-08 | 3 échecs biométriques => mot de passe obligatoire | Compteur local signé + E2E lockout |
| INV-107-09 | Inactivité >30 min => mot de passe obligatoire | Policy engine + tests timer simulé |
| INV-107-10 | Tous événements auth tracés et signés | Contrat audit + intégration backend HSM + vérification signature |
4) Interfaces et contrats
4.1 Types TypeScript (services)
export type BiometryType = "FACE_ID" | "TOUCH_ID" | "NONE";
export interface BiometricCapability {
available: boolean;
enrolled: boolean;
biometryType: BiometryType;
reasonIfUnavailable?: string;
}
export interface UnlockPolicyContext {
appBootTs: string;
lastStrongAuthTs?: string; // password/SRP success
lastUnlockTs?: string;
failedBiometricAttempts: number;
maxAttempts: 3;
inactivityTimeoutMs: number; // 30 * 60 * 1000
deviceRebootDetected: boolean;
passwordChangedAt?: string;
}
export type UnlockDecision =
| { type: "PROMPT_BIOMETRIC" }
| { type: "REQUIRE_PASSWORD"; reason: string };
export interface KeychainSecretRecord {
version: 1;
userId: string;
deviceId: string;
wrappedUnlockBlob: string; // base64 AES-GCM ciphertext
keyId: string;
createdAt: string;
}
export interface BiometricService {
getCapability(): Promise<BiometricCapability>;
prompt(reason: string): Promise<{ ok: boolean; errorCode?: string }>;
}
export interface KeychainService {
putBiometricProtectedSecret(alias: string, valueBase64: string): Promise<void>;
getBiometricProtectedSecret(alias: string): Promise<string>;
deleteSecret(alias: string): Promise<void>;
}
export interface UnlockOrchestrator {
enableBiometricWithPassword(password: string): Promise<void>;
disableBiometric(reason: string): Promise<void>;
evaluateUnlockPolicy(ctx: UnlockPolicyContext): UnlockDecision;
unlockWithBiometric(): Promise<void>;
unlockWithPassword(password: string): Promise<void>;
}
4.2 Signatures des hooks React
export interface UseBiometricSettingsResult {
capability: BiometricCapability;
enabled: boolean;
loading: boolean;
enable: (password: string) => Promise<void>;
disable: (reason?: string) => Promise<void>;
refresh: () => Promise<void>;
error?: { code: string; message: string };
}
export declare function useBiometricSettings(): UseBiometricSettingsResult;
export interface UseAppUnlockResult {
state:
| "LOCKED"
| "PROMPTING_BIOMETRIC"
| "UNLOCKED"
| "REQUIRE_PASSWORD"
| "REVOKED";
failedAttempts: number;
unlockWithBiometric: () => Promise<void>;
unlockWithPassword: (password: string) => Promise<void>;
resetError: () => void;
error?: { code: string; message: string; recoverable: boolean };
}
export declare function useAppUnlock(): UseAppUnlockResult;
4.3 Schéma JSON des logs d'audit
{
"eventId": "uuid-v7",
"eventType": "BIOMETRIC_AUTH_SUCCESS",
"userId": "string",
"deviceId": "string",
"storyId": "PD-107",
"tsClient": "2026-02-14T12:34:56.123Z",
"tsServer": "2026-02-14T12:34:56.456Z",
"payload": {
"biometryType": "FACE_ID",
"attemptCount": 0,
"unlockDurationMs": 420,
"reason": "string optional"
},
"integrity": {
"hash": "sha256-hex",
"prevHash": "sha256-hex",
"signature": "hsm-signature-base64",
"signatureKeyId": "audit-hsm-key-2026-01"
}
}
5) États et transitions
5.1 Diagramme d'état biométrie (lifecycle)
stateDiagram-v2
[*] --> DISABLED
DISABLED --> ENABLING: toggle ON + password OK
ENABLING --> ENABLED: keychain write OK
ENABLING --> DISABLED: erreur activation
ENABLED --> DISABLED: toggle OFF
ENABLED --> REVOKED: biometry changed / reinstall / password changed
REVOKED --> DISABLED: purge complete
DISABLED --> [*]
5.2 Machine à états du déverrouillage
stateDiagram-v2
[*] --> STARTUP_CHECK
STARTUP_CHECK --> REQUIRE_PASSWORD: boot|timeout|lockout|policy_fail
STARTUP_CHECK --> PROMPT_BIOMETRIC: policy_ok
PROMPT_BIOMETRIC --> UNLOCKED: biometric_success + 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)
REQUIRE_PASSWORD --> REQUIRE_PASSWORD: password_fail
UNLOCKED --> LOCKED: app_background/lock_event
LOCKED --> STARTUP_CHECK: app_foreground
6) Gestion des erreurs
| Code | Condition | Message utilisateur | Récupération |
| ERR-107-001 | Biométrie indisponible | "Face ID/Touch ID non disponible sur cet appareil." | Proposer mot de passe; désactiver toggle |
| ERR-107-010 | Mot de passe invalide à l'activation | "Mot de passe incorrect." | Retry limité + anti-bruteforce existant SRP |
| ERR-107-020 | Échec écriture Keychain | "Impossible d'activer la biométrie." | Retry technique; si échec persistant -> support |
| ERR-107-021 | Échec suppression Keychain | "Désactivation incomplète, nouvelle tentative requise." | Retry + purge au prochain lancement |
| ERR-107-030 | Échec reconnaissance biométrique | "Authentification biométrique échouée." | Nouvelle tentative si <3 |
| ERR-107-031 | Prompt annulé utilisateur | "Authentification annulée." | Option mot de passe immédiate |
| ERR-107-040 | Échec auth mot de passe/SRP | "Impossible de vous authentifier." | Retry SRP standard |
| ERR-107-050 | Compteur échecs incohérent | "Vérification de sécurité requise." | Forcer mot de passe + reset compteur |
| ERR-107-060 | Changement biométrique détecté | "Vos données biométriques ont changé. Réactivez la biométrie." | Forcer mot de passe + réactivation manuelle |
| ERR-107-070 | Contexte temps invalide | "Session expirée. Mot de passe requis." | Forcer mot de passe |
| ERR-107-080 | Événement sécurité manquant | "Vérification de sécurité requise." | Désactiver biométrie par précaution |
| ERR-107-090 | Audit backend indisponible | "Connexion limitée. Action enregistrée localement." | Queue locale chiffrée + retry |
7) Considérations de sécurité
7.1 STRIDE simplifié
| Menace | Exemple | Contrôle |
| S (Spoofing) | Usurpation identité locale | Biometry iOS + fallback SRP-6a sur cas forts |
| T (Tampering) | Altération logs locaux | Queue append-only hash chain + signature backend HSM |
| R (Repudiation) | Contestation d'action | Horodatage serveur + signature HSM |
| I (Information Disclosure) | Extraction secret storage | Keychain ThisDeviceOnly + aucune clé en clair |
| D (Denial of Service) | Lock biométrie abusif | Fallback mot de passe toujours disponible |
| E (Elevation of Privilege) | Bypass biométrie/policy | Machine d'état stricte + guard clauses + tests E2E |
7.2 Surface d'attaque identifiée
- Écran unlock (brute force local, shoulder surfing).
- Keychain access path (mauvaise config access control).
- Mémoire runtime JS (persistence non voulue).
- Sync audit offline->online (replay/duplication).
7.3 Contrôles implémentés
kSecAccessControlBiometryCurrentSet + WhenUnlockedThisDeviceOnly. - Timeout 30 min + lockout 3 essais.
- Zeroization best-effort sur buffers natifs/bridge.
- Idempotency key + nonce côté API audit.
- Blur app en background + nettoyage écran sensible.
8) Compatibilité et contraintes
- iOS supporté: minimum
iOS 15.0, recommandé iOS 16+. - Expo/Hermes:
- Hermes sans WebAssembly natif -> crypto sensible via module natif/bridge, pas wasm.
- Expo managed pur insuffisant pour garanties Keychain fines; adopter prebuild + plugin natif.
- Dépendances externes:
expo-local-authentication (prompt biométrique). expo-secure-store (métadonnées non sensibles uniquement, optionnel). - Module natif Keychain iOS (SecItem) pour access control strict.
- Tests device réel obligatoires:
- changement empreinte/visage,
- redémarrage device,
- réinstallation app,
- performance unlock < 1s.
Annexes d'implémentation (contrats opérationnels)
- Les événements
BIOMETRIC_ENABLED, BIOMETRIC_DISABLED, BIOMETRIC_AUTH_SUCCESS, BIOMETRIC_AUTH_FAILURE, BIOMETRIC_REVOKED_SYSTEM_CHANGE, PASSWORD_AUTH_FALLBACK sont obligatoires. - Aucun flux ne doit produire un état
UNLOCKED sans passer par unlockWithBiometric() valide ou unlockWithPassword() SRP valide. - Toute erreur non classifiée doit tomber en
REQUIRE_PASSWORD (fail-safe sécurisé).