Comprendre le fonctionnement du 2FA TOTP dans gitrust¶
Ce que vous allez comprendre¶
- Décrire le flux cryptographique TOTP (RFC 6238) de la génération du secret à la vérification du code.
- Analyser les choix de sécurité : chiffrement du secret, codes de secours, challenges temporaires.
- Évaluer pourquoi le 2FA ne protège pas contre le vol de session et comment gitrust atténue ce risque.
Le problème concret¶
Alice utilise un mot de passe fort. Mais si sa base de mots de passe est compromise, ou si un attaquant intercepte son mot de passe (phishing), il peut se connecter à son compte gitrust et pousser du code malveillant. Un second facteur garantit que la possession du mot de passe seul ne suffit pas.
L'analogie¶
TOTP ressemble à un générateur de codes de distributeur bancaire qui change toutes les 30 secondes. La banque et le distributeur partagent un secret commun (la clé de votre carte). À partir de ce secret et de l'heure actuelle, les deux parties calculent indépendamment le même code. Personne n'a besoin de transmettre le code sur le réseau.
La différence avec un SMS OTP : le secret n'est jamais transmis après la configuration initiale. Un attaquant qui intercepte votre réseau ne voit jamais le code à venir.
Le modèle¶
Génération du secret (RFC 6238)¶
sequenceDiagram
participant User as Utilisateur
participant Server as Serveur gitrust
participant App as App authenticator
User->>Server: POST /settings/security/2fa/setup
Server->>Server: Générer secret aléatoire 20 octets (CSPRNG)
Server->>Server: Chiffrer AES-256-GCM → stocker dans user_totp
Server-->>User: { secret_base32, otpauth_uri, qr_code_data_uri }
User->>App: Scanner le QR code
App->>App: Stocker le secret localement
User->>Server: POST /settings/security/2fa/verify { code: "123456" }
Server->>Server: Déchiffrer secret, calculer TOTP, vérifier fenêtre ±1
Server-->>User: { backup_codes: [...] } — activé
L'URI OTPAuth encode tout ce qu'une app authenticator a besoin :
otpauth://totp/gitrust:alice?secret=JBSWY3DPEHPK3PXP&issuer=gitrust&algorithm=SHA1&digits=6&period=30
Calcul du code TOTP¶
TOTP est HOTP (RFC 4226) avec un compteur basé sur le temps :
counter = floor(unix_timestamp / 30)
hmac = HMAC-SHA1(secret, counter_as_8_bytes_big_endian)
offset = hmac[19] & 0x0f
code = (hmac[offset..offset+4] & 0x7fffffff) % 1_000_000
Le code est valide pendant 30 secondes. Gitrust accepte une fenêtre de tolérance ±1 période (90 secondes totales) pour compenser les légères dérives d'horloge entre l'appareil de l'utilisateur et le serveur.
Flux de login avec 2FA activé¶
sequenceDiagram
participant C as Client
participant S as Serveur
C->>S: POST /api/v1/auth/login { username, password }
S->>S: verify_credentials() → OK
S->>S: is_totp_enabled(user_id) → true
S->>S: create_challenge(user_id) → challenge_token (5 min, max 5 tentatives)
S-->>C: { requires_2fa: true, challenge_token: "abc..." }
C->>S: POST /api/v1/auth/2fa/verify { challenge_token, code }
S->>S: validate_challenge() → non expiré, < 5 tentatives
S->>S: verify_code() ou verify_backup_code()
S->>S: consume_challenge()
S-->>C: { access_token, refresh_token, user }
Le challenge expire après 5 minutes. Maximum 5 tentatives par challenge. Un challenge échoué 5 fois force une nouvelle authentification complète.
Stockage sécurisé du secret¶
Le secret TOTP n'est jamais stocké en clair en base de données. Il est chiffré avec AES-256-GCM via CryptoService avant insertion dans user_totp.encrypted_secret.
// À l'activation
let encrypted = CryptoService::encrypt(&secret_base32)?;
// → stocké : "enc:base64(nonce||ciphertext)"
// À la vérification
let secret = CryptoService::decrypt(&user_totp.encrypted_secret)?;
La clé de chiffrement est ENCRYPTION_KEY (32 octets, variable d'environnement). Si elle est absente, le TOTP ne peut pas être activé.
Codes de secours¶
À l'activation du 2FA, 10 codes de secours de 8 caractères alphanumériques sont générés. Ils sont stockés hachés bcrypt dans user_totp.backup_codes_json (JSON chiffré).
Chaque code est à usage unique : une fois utilisé, il est marqué consommé. L'utilisateur peut régénérer un nouveau jeu de 10 codes depuis /settings/security.
Format : XXXXXXXX (ex : A3B7K2M9) — reconnaissable par sa longueur (8 chars vs 6 chiffres pour TOTP).
La détection est automatique dans verify_backup_code : si le code contient des lettres ou fait 8 caractères, c'est un code de secours ; sinon c'est un code TOTP.
Force 2FA par l'administrateur¶
Un administrateur peut forcer le 2FA sur toute l'instance via AppSettingsService::set_setting("force_2fa_enabled", "true"). Quand ce flag est actif :
- Les utilisateurs sans 2FA sont redirigés vers la page de configuration à chaque login.
- Les endpoints API retournent
403avec{"error":"totp_required"}tant que le 2FA n'est pas configuré.
Alternatives et compromis¶
Pourquoi TOTP et pas WebAuthn/passkeys ? WebAuthn offre une protection supérieure contre le phishing (le domaine est lié cryptographiquement). Gitrust prévoit de l'ajouter en phase future. TOTP a été choisi pour sa compatibilité universelle (toutes les apps authenticator, hors connexion, pas de dépendance matérielle).
Pourquoi pas SMS OTP ? Les SMS OTP sont vulnérables au SIM swapping et à l'interception SS7. Gitrust ne les implémente pas.
La fenêtre de ±1 période réduit-elle la sécurité ? Marginalement. Elle augmente la fenêtre d'attaque de 30 à 90 secondes, mais un code TOTP reste à 6 chiffres (1/1 000 000 de probabilité de deviner), et les 5 tentatives max par challenge rendent le brute-force infaisable.
Vérifier votre compréhension¶
-
Un utilisateur est dans un fuseau horaire décalé de 45 secondes par rapport au serveur. Sa tentative de connexion TOTP échoue systématiquement. Pourquoi, et comment diagnostiquer ?
-
L'
ENCRYPTION_KEYdu serveur change (rotation). Que se passe-t-il pour les utilisateurs ayant déjà activé le 2FA ? Quelle procédure de migration faudrait-il mettre en place ?