PD-99 — Plan d'implémentation
Navigation User Story
| Document | | | -------- | --- | | [Expression de Besoin](index.md) | | | **Plan d'implémentation** | *(ce document)* | | [Critères d'acceptation](PD-99-acceptability.md) | | | [Retour d'expérience](PD-99-rex.md) | | [Retour a mobile-ios](../PD-195-epic.md) - [Index User Story](index.md)
1. Découpage en composants
1.1 Composants UI (Frontend iOS)
| Composant | Responsabilité |
LoginScreen.tsx | Écran principal de connexion (email + mot de passe) |
BiometricLoginPrompt.tsx | Composant de connexion facilitée par biométrie |
LoginForm.tsx | Formulaire avec validation temps réel |
PasswordDisclaimer.tsx | Rappel explicite : mot de passe irrécupérable |
AuthErrorMessage.tsx | Affichage erreurs neutres et non-discriminantes |
1.2 Services (Frontend iOS)
| Service | Responsabilité |
src/services/srp.ts | Client SRP-6a (génération A, calcul M1, vérification M2) |
src/services/api.ts | Appels endpoints /auth/login/challenge et /auth/login/verify |
src/services/keyDerivation.ts | Dérivation K_auth via Argon2id |
src/services/storage.ts | Stockage sécurisé tokens (expo-secure-store) |
src/services/biometric.ts | Gestion Face ID / Touch ID |
1.3 State Management
| Store/Hook | Responsabilité |
useAuth.ts | Hook unifié : état authentification, méthodes login/logout |
useAuthStore.ts | Zustand store : Master Key en mémoire |
useBiometric.ts | Hook biométrie : disponibilité, enrollment, vérification |
1.4 Composants Backend (existants)
| Composant | Responsabilité |
auth.controller.ts | Endpoints /auth/login/challenge, /auth/login/verify |
srp.service.ts | Validation SRP serveur, calcul B, vérification M1 |
srp-session-store.service.ts | Sessions SRP éphémères (Redis, TTL 5 min) |
2. Flux techniques
2.1 Flux nominal : Connexion email + mot de passe
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client │ │ Backend │ │ Redis │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ 1. GET /auth/srp-params │ │
│─────────────────────────────────>│ │
│ {N, g, version} │ │
│<─────────────────────────────────│ │
│ │ │
│ 2. Génère a (256 bits) │ │
│ A = g^a mod N │ │
│ │ │
│ 3. POST /auth/login/challenge │ │
│ {email, A} │ │
│─────────────────────────────────>│ │
│ │ 4. Lookup user (email) │
│ │ 5. Génère b, calcule B │
│ │ 6. Store session ──────────────>│
│ {salt, B} │ │
│<─────────────────────────────────│ │
│ │ │
│ 7. K_auth = Argon2id(pwd, salt) │ │
│ x = SHA3-256(salt || K_auth) │ │
│ u = SHA3-256(PAD(A)||PAD(B)) │ │
│ S = (B - k*g^x)^(a+u*x) mod N│ │
│ K = SHA3-256(S) │ │
│ M1 = SHA3-256(A || B || K) │ │
│ │ │
│ 8. POST /auth/login/verify │ │
│ {email, M1} │ │
│─────────────────────────────────>│ 9. Get session <────────────────│
│ │ 10. Calcule S, K, M1_expected │
│ │ 11. Vérifie M1 === M1_expected │
│ │ 12. Génère JWT + M2 │
│ │ 13. Delete session ─────────────>│
│ {accessToken, M2} │ │
│<─────────────────────────────────│ │
│ │ │
│ 14. M2_expected = SHA3-256(...) │ │
│ 15. Vérifie M2 === M2_expected │ │
│ 16. Store JWT (SecureStore) │ │
│ 17. Unlock Master Key local │ │
│ │ │
2.2 Flux connexion facilitée (biométrie)
┌─────────────┐ ┌─────────────┐
│ Client │ │ Keychain │
└──────┬──────┘ └──────┬──────┘
│ │
│ 1. Prompt Face ID / Touch ID │
│─────────────────────────────────>│
│ 2. Validation biométrique │
│<─────────────────────────────────│
│ │
│ 3. Récupère credentials chiffrés│
│ (email + K_auth dérivée) │
│─────────────────────────────────>│
│ {email, encryptedCredentials} │
│<─────────────────────────────────│
│ │
│ 4. Déchiffre avec clé biométrie │
│ 5. Exécute flux SRP standard │
│ (challenge → verify) │
│ │
Cas d'erreur possibles :
- Email inconnu
- Mot de passe incorrect
- Compte non validé
- Compte suspendu
- Erreur serveur
┌──────────────────────────────┐
Tous les cas ──────>│ Délai aléatoire 50-150ms │
│ + Réponse HTTP 401 │
│ + Message neutre identique │
└──────────────────────────────┘
2bis. Diagrammes Mermaid
Graphe de dépendances des composants
graph TD
subgraph UI["Composants UI"]
LS[LoginScreen.tsx]
BLP[BiometricLoginPrompt.tsx]
LF[LoginForm.tsx]
PD[PasswordDisclaimer.tsx]
AEM[AuthErrorMessage.tsx]
end
subgraph Services["Services"]
SRP[srp.ts]
API[api.ts]
KD[keyDerivation.ts]
STO[storage.ts]
BIO[biometric.ts]
end
subgraph State["State Management"]
UA[useAuth.ts]
UAS[useAuthStore.ts]
UB[useBiometric.ts]
end
subgraph Backend["Backend existant"]
AC[auth.controller.ts]
SS[srp.service.ts]
SSS[srp-session-store.service.ts]
end
subgraph Infra["Infrastructure"]
RED[(Redis)]
end
LS --> LF
LS --> PD
LS --> AEM
LS --> BLP
LS --> UA
BLP --> UB
BLP --> BIO
LF --> UA
UA --> SRP
UA --> API
UA --> KD
UA --> STO
UA --> UAS
UB --> BIO
UB --> STO
API -->|HTTP| AC
AC --> SS
SS --> SSS
SSS --> RED
SRP -.->|Argon2id| KD
BIO -.->|SecureStore| STO
Diagramme de séquence : connexion SRP-6a nominale
sequenceDiagram
actor U as Utilisateur
participant LS as LoginScreen
participant UA as useAuth
participant KD as keyDerivation.ts
participant SRP as srp.ts
participant API as api.ts
participant AC as auth.controller.ts
participant SS as srp.service.ts
participant SSS as srp-session-store
participant RED as Redis
U->>LS: Saisit email + mot de passe
LS->>UA: login(email, password)
Note over UA,API: Phase 1 — Challenge
UA->>API: GET /auth/srp-params
API->>AC: getSrpParams()
AC-->>API: {N, g, version}
API-->>UA: {N, g, version}
UA->>SRP: generateEphemeral()
SRP-->>UA: {a, A}
UA->>API: POST /auth/login/challenge {email, A}
API->>AC: challenge(email, A)
AC->>SS: createChallenge(email, A)
SS->>SSS: storeSession(sessionId, {A, b, B, v})
SSS->>RED: SET srp:session:id TTL=300s
RED-->>SSS: OK
AC-->>API: {salt, B}
API-->>UA: {salt, B}
Note over UA,SRP: Phase 2 — Dérivation + Preuve
UA->>KD: deriveKauth(password, salt)
Note right of KD: Argon2id(pwd, salt)<br/>OWASP params
KD-->>UA: K_auth
UA->>SRP: computeProof(A, B, K_auth, salt)
Note right of SRP: x = SHA3-256(salt || K_auth)<br/>u = SHA3-256(PAD(A)||PAD(B))<br/>S = (B - k·g^x)^(a+u·x) mod N<br/>K = SHA3-256(S)<br/>M1 = SHA3-256(A || B || K)
SRP-->>UA: {M1, K}
Note over UA,API: Phase 3 — Verify
UA->>API: POST /auth/login/verify {email, M1}
API->>AC: verify(email, M1)
AC->>SSS: getSession(sessionId)
SSS->>RED: GET srp:session:id
RED-->>SSS: {A, b, B, v}
AC->>SS: verifyProof(session, M1)
Note right of SS: Calcule S, K, M1_expected<br/>Vérifie M1 === M1_expected
SS-->>AC: {M2, jwt}
AC->>SSS: deleteSession(sessionId)
SSS->>RED: DEL srp:session:id
AC-->>API: {accessToken, M2}
API-->>UA: {accessToken, M2}
Note over UA,SRP: Phase 4 — Authentification mutuelle
UA->>SRP: verifyServer(A, M1, K, M2)
Note right of SRP: M2_expected = SHA3-256(A||M1||K)<br/>Vérifie M2 === M2_expected
SRP-->>UA: OK
UA->>STO: storeToken(accessToken)
UA->>UAS: setMasterKey(K_master)
UA-->>LS: Authentifié
LS-->>U: Navigation écran principal
Diagramme de séquence : connexion facilitée (biométrie)
sequenceDiagram
actor U as Utilisateur
participant BLP as BiometricLoginPrompt
participant UB as useBiometric
participant BIO as biometric.ts
participant STO as storage.ts
participant UA as useAuth
U->>BLP: Appui bouton biométrie
BLP->>UB: authenticateWithBiometric()
UB->>BIO: promptBiometric()
BIO->>BIO: Face ID / Touch ID
alt Biométrie réussie
BIO-->>UB: success
UB->>STO: getSecureCredentials()
Note right of STO: Keychain protégé<br/>par biométrie
STO-->>UB: {email, encryptedKauth}
UB->>UB: déchiffre credentials
UB->>UA: login(email, K_auth)
Note over UA: Exécute flux SRP standard<br/>(cf. diagramme nominal)
UA-->>BLP: Authentifié
BLP-->>U: Navigation écran principal
else Biométrie annulée / échouée
BIO-->>UB: failure
UB-->>BLP: cancelled
BLP-->>U: Retour écran login classique
end
3. Mapping invariants vers mecanismes
| Invariant ID | Exigence | Mécanisme | Composant | Observable | Risque |
| INV-01 | Toute authentification DOIT utiliser SRP-6a avec observable contractuel | Protocole challenge/response RFC 5054, endpoints /auth/login/challenge + /auth/login/verify | srp.ts, srp.service.ts | Observable contractuel : 2 appels séquentiels (challenge → verify), payload sans champ password, échange A/B/M1/M2 | Moyen |
| INV-02 | Mot de passe JAMAIS transmis en clair | Dérivation locale K_auth, ForbidPasswordGuard | keyDerivation.ts, Backend guard | Analyse réseau : payload sans champ password | Critique |
| INV-04 | Terme « mot de passe » DOIT être utilisé | Label statique "Mot de passe" dans UI | LoginForm.tsx | Inspection UI : label exact | Faible |
| INV-05 | Connexion fondatrice et facilitée DOIVENT être visuellement différenciées | UI conditionnelle avec éléments distincts (libellé, titre, icône) selon mode | LoginScreen.tsx, BiometricLoginPrompt.tsx | Inspection UI : au moins 1 élément distinct parmi libellé/titre/message d'aide/icône entre les 2 modes | Faible |
| INV-06 | Différenciation visuelle NE DOIT PAS persister après connexion | Aucun state loginMode persisté, navigation sans paramètre de mode | Navigation React, useAuth | Navigation UI : écrans post-login strictement identiques quel que soit le mode utilisé, aucun indicateur visible | Faible |
| INV-07 | Biométrie DOIT être optionnelle | Toggle dans Settings, fallback login fondateur toujours disponible | useBiometric.ts, SettingsScreen.tsx | UI propose toujours login classique même si biométrie activée | Faible |
| INV-08 | Biométrie DOIT pouvoir être désactivée globalement et/ou par appareil | Toggle global + liste appareils avec flag device_id | biometric.ts, storage.ts | Paramètres : 2 niveaux de désactivation fonctionnels | Moyen |
| INV-09 | Messages d'erreur NE DOIVENT PAS permettre d'inférence exploitable | Message unique hardcodé, délai constant 50-150ms | AuthErrorMessage.tsx, Backend middleware | Tests négatifs : même texte exact + même timing pour tous les cas d'échec | Élevé |
| INV-11 | Rappel d'irrécupérabilité avec texte exact obligatoire | Composant PasswordDisclaimer avec texte statique de la spec | PasswordDisclaimer.tsx | Inspection UI : texte exact "Votre mot de passe est connu de vous seul. Il ne peut pas être récupéré ni communiqué, y compris à la demande." | Faible |
Note : INV-03 (aucun humain ne peut connaître le mot de passe) et INV-10 (login = acte engageant) sont des exigences de gouvernance non testables par les scénarios applicatifs (cf. spec).
4. Mapping criteres d'acceptation vers mecanismes
| Critère ID | Exigence spec | Mécanisme(s) | Composant | Observable | Risque |
| CA-01 | La connexion fondatrice utilise SRP-6a | Protocole challenge/response, échange A/B/M1/M2 | srp.ts, useAuth.loginBackend() | Traces de flux : 2 appels séquentiels /auth/login/challenge puis /auth/login/verify, payload sans champ password | Moyen |
| CA-02 | Le mot de passe n'est jamais transmis en clair | Dérivation locale K_auth + SRP, ForbidPasswordGuard | keyDerivation.ts, srp.ts, Backend guard | Analyse réseau : payload contient uniquement email, A, M1 | Critique |
| CA-03 | Les écrans fondatrice et facilitée sont distincts | UI conditionnelle selon hasBiometricCredentials | LoginScreen.tsx, BiometricLoginPrompt.tsx | Inspection UI : libellé/titre/icône distincts entre les 2 modes | Faible |
| CA-04 | Aucun indicateur de mode ne persiste après login | Pas de state persistant du mode de connexion | Navigation React, useAuth | Navigation UI : écrans post-login identiques quel que soit le mode utilisé | Faible |
| CA-05 | La biométrie est désactivable globalement | Toggle global dans Settings + suppression credentials | SettingsScreen.tsx, biometric.ts | Paramètres : switch "Biométrie" désactivable, effet immédiat | Faible |
| CA-06 | La biométrie est désactivable par appareil | Flag par device_id dans SecureStore | biometric.ts, storage.ts | Paramètres : liste appareils avec toggle individuel | Moyen |
| CA-07 | Les messages d'erreur sont identiques | Message unique hardcodé, aucune variation | AuthErrorMessage.tsx | Tests négatifs : même texte pour email inconnu, mdp incorrect, compte invalide | Élevé |
| CA-08 | Tout écran de login affiche le rappel d'irrécupérabilité avec texte exact | Composant PasswordDisclaimer avec texte statique | PasswordDisclaimer.tsx, LoginScreen.tsx | Inspection UI : texte exact "Votre mot de passe est connu de vous seul..." visible | Faible |
| CA-09 | Message d'erreur générique correspond exactement au texte requis | Constante ERROR_MESSAGE avec texte exact de la spec | constants.ts, AuthErrorMessage.tsx | Inspection UI : texte exact "La connexion a échoué. Vérifiez vos informations et réessayez." | Élevé |
5. Mapping tests vers mecanismes et observables
| Test ID | Référence spec | Mécanisme(s) | Point(s) d'observation | Niveau |
| TC-INV-01 | INV-01, CA-01 | Header X-Auth-Protocol | Observable contractuel : Header X-Auth-Protocol: SRP-6a-v1 présent dans réponses HTTP /auth/login/challenge et /auth/login/verify | Integration |
| TC-LOGIN-01 | CA-01, CA-02 | SRP challenge + verify | JWT retourné, user authentifié, header X-Auth-Protocol: SRP-6a-v1 vérifié | Integration |
| TC-LOGIN-02 | INV-09, CA-07 | Erreur email inconnu | Message exact "La connexion a échoué...", HTTP 401 | E2E |
| TC-LOGIN-03 | INV-09, CA-07 | Erreur mot de passe incorrect | Message strictement identique à TC-LOGIN-02 | E2E |
| TC-LOGIN-04 | INV-09 | Timing email inconnu vs incorrect | Delta < 50ms entre les cas | Sec |
| TC-LOGIN-05 | INV-02 | Requêtes réseau | Aucun champ password dans payload | E2E |
| TC-LOGIN-06 | INV-01 | Vecteurs SRP RFC 5054 | M1, M2 conformes aux vecteurs | Unit |
| TC-LOGIN-07 | INV-01 | M2 invalide | Exception levée, pas de JWT stocké | Unit |
| TC-BIO-01 | INV-07 | Désactivation biométrie | Login classique toujours disponible | E2E |
| TC-BIO-02 | INV-07 | Biométrie sans login préalable | Prompt biométrie non disponible | E2E |
| TC-BIO-03 | INV-08, CA-05 | Désactivation globale | Toggle effectif, credentials supprimés | Integration |
| TC-BIO-04 | INV-08, CA-06 | Désactivation par appareil | Toggle par device_id effectif | Integration |
| TC-UI-01 | INV-11, CA-08 | Disclaimer affiché | Texte exact spec visible dans DOM | Unit |
| TC-UI-02 | INV-05, CA-03 | Mode biométrie vs classique | Au moins 1 élément distinct (libellé/titre/icône) | Unit |
| TC-UI-03 | INV-06, CA-04 | Non-persistance après login | Écrans post-login identiques quel que soit le mode | E2E |
| TC-UI-04 | INV-04 | Label "Mot de passe" | Terme exact utilisé dans UI | Unit |
| TC-ERR-01 | CA-09 | Message erreur exact | Texte "La connexion a échoué. Vérifiez vos informations et réessayez." | Unit |
| TC-ARGON-01 | INV-02 | Dérivation K_auth | Argon2id paramètres OWASP | Unit |
6. Gestion des erreurs
Texte exact obligatoire pour tout echec d'authentification :
La connexion a échoué. Vérifiez vos informations et réessayez.
Ce message unique est affiché pour tous les cas suivants, sans distinction :
| Situation interne | Message utilisateur | Comportement |
| Email inconnu | Message générique unique | Identique |
| Mot de passe incorrect | Message générique unique | Identique |
| Compte non validé | Message générique unique | Identique |
| Compte suspendu | Message générique unique | Identique |
| Session SRP expirée | Message générique unique | Identique |
| Erreur serveur | Message générique unique | Identique |
| Erreur réseau | Message générique unique | Identique |
| M2 invalide | Message générique unique | Identique |
6.2 Cas particulier : biometrie (ERR-03)
| Situation | Comportement |
| Biométrie annulée | Retour silencieux à l'écran de login (pas de message) |
| Biométrie échouée | Retour à l'écran de login (pas de message d'erreur textuel) |
Aucun code d'erreur distinct n'est expose ni loggue de maniere a permettre une inference.
| Aspect | Exigence | Implementation |
| Code HTTP | Unique | 401 Unauthorized pour tout echec auth |
| Message utilisateur | Unique | Texte exact de la spec, sans variation |
| Timing reponse | Indiscernable | Delai aleatoire 50-150ms applique a tous les cas |
| Logs backend | Non discriminants | Log unique auth.login.attempt avec success: true/false uniquement |
| State frontend | Unique | Etat error sans distinction de cause |
6.4 Logging securise et non-discriminant
// AUTORISÉ - Log unique non discriminant
logger.info('auth.login.attempt', {
emailHash: sha256(email),
success: false, // Seule information : succès ou échec
timestamp: Date.now()
});
// INTERDIT - Logs discriminants
logger.info('auth.login.failure.invalid_email', ...); // JAMAIS
logger.info('auth.login.failure.wrong_password', ...); // JAMAIS
logger.info('auth.login.failure.account_suspended', ...); // JAMAIS
Conformite CA-07/CA-09/INV-09 : Aucun element observable (message, code, timing, log) ne permet de distinguer la cause de l'echec.
7. Impacts securite
7.1 Risques identifies
| Risque | Probabilité | Impact | Mitigation |
| Timing attack (énumération) | Moyenne | Élevé | Délai aléatoire 50-150ms |
| Brute force | Moyenne | Élevé | Rate limiting + lockout progressif |
| Replay attack | Faible | Moyen | Session TTL 5 min + nonce unique |
| Man-in-the-middle | Faible | Critique | TLS 1.3 + certificate pinning |
| Credentials en mémoire | Moyenne | Élevé | Zeroize après usage |
| Biométrie bypass | Faible | Moyen | Biométrie = confort, pas autonome |
7.2 Mitigations implementees
| Mitigation | Composant | Vérification |
ForbidPasswordGuard | Backend | Test E2E payload avec password rejeté |
| Délai aléatoire | Backend middleware | Mesure temps réponse |
| Rate limiting | RateLimitGuard | Test 10+ tentatives |
| Session Redis TTL | srp-session-store.service.ts | Test expiration |
| Certificate pinning | api.ts (Expo) | Test MITM |
| Zeroize buffers | zeroize.ts | Test mémoire |
7.3 Journalisation securite
| Événement | Niveau | Données loggées |
| Login success | INFO | sha256(email), sha256(userId), timestamp |
| Login failure | WARN | sha256(email), raison générique, IP hashé |
| Rate limit triggered | WARN | sha256(email), IP hashé, count |
| M2 mismatch | ALERT | sha256(email), session_id (alerte critique) |
| Biometric enrollment | INFO | sha256(userId), device_id hashé |
| Exigence | Statut | Commentaire |
| RGPD (pas de stockage mot de passe) | Conforme | SRP-6a : seul verifier stocké |
| OWASP Authentication | Conforme | Argon2id, rate limiting, anti-énumération |
| ANSSI (authentification forte) | Conforme | Zero-Knowledge + option MFA biométrie |
8. Hypotheses techniques
| ID | Hypothèse | Impact si faux |
| H-01 | Argon2id disponible en production (EAS build) | Fallback PBKDF2 moins sécurisé en Expo Go |
| H-02 | SecureStore disponible sur iOS | Pas de stockage sécurisé = pas de biométrie |
| H-03 | Face ID / Touch ID enrollment existant | Biométrie non proposée si non configuré |
| H-04 | Connexion réseau disponible | Mode offline non supporté pour login |
| H-05 | Backend SRP opérationnel | Login impossible, fallback mode démo |
| H-06 | Redis disponible pour sessions SRP | Sessions en mémoire (moins scalable) |
| H-07 | Horloge client synchronisée | Session TTL potentiellement incorrecte |
9. Points de vigilance (risques, dette, pieges)
9.1 Risques techniques
- Latence SRP : 2 requêtes séquentielles + Argon2id (~500ms). Prévoir UX loading approprié.
- Expo Go vs EAS : Argon2id non disponible en Expo Go. Tester en build EAS.
- Biométrie iOS : Comportement différent Face ID vs Touch ID. Tester les deux.
- Session Redis : Si Redis down, login impossible. Prévoir fallback ou circuit breaker.
9.2 Dette technique identifiee
- Mode démo avec auth locale : à supprimer avant production.
- Refresh token non implémenté : JWT unique avec expiration.
- Rotation credentials biométrie : non planifiée.
9.3 Pieges a eviter
- Ne jamais logger le mot de passe, même partiellement.
- Ne jamais différencier les messages d'erreur selon le cas.
- Ne jamais permettre la biométrie sans auth initiale préalable.
- Toujours vérifier M2 côté client (authentification mutuelle).
- Toujours zeroize les buffers contenant des secrets après usage.
9.4 Limites connues
- JavaScript : strings immutables, zeroize limité pour K_auth string.
- Expo SecureStore : 2KB max par clé, suffisant pour credentials.
- Biométrie : contournable si device jailbreaké.
10. Hors perimetre
| Élément exclu | Justification | User Story associée |
| Inscription utilisateur | Déjà implémenté | PD-23 |
| Récupération mot de passe | Hors scope PD-99 | Future US |
| Réinitialisation mot de passe | Hors scope PD-99 | Future US |
| Changement mot de passe | Hors scope PD-99 | Future US |
| MFA (TOTP, SMS) | Non prévu V1 | Future US |
| SSO (OAuth, SAML) | Non prévu | Future US |
| Gestion sessions multiples | Hors scope | Future US |
| Mode offline | Non supporté | Future US |
| Dérivation K_master | Déjà implémenté | PD-33 |
| Stockage K_master Keychain | Déjà implémenté | PD-98 |
| Chiffrement Zero-Knowledge | Déjà implémenté | PD-97 |
Document généré selon le template PD-XX-plan.md