Back to sh0
sh0

Auth en Rust : Argon2id, JWT, TOTP et clés API

Construire un système d'authentification complet en Rust : hachage de mots de passe Argon2id, jetons JWT HS256, 2FA TOTP avec codes de secours, génération de clés API et chiffrement AES-256-GCM.

Thales & Claude | March 30, 2026 6 min sh0
EN/ FR/ ES
authrustargon2idjwttotpapi-keyssecurityencryption

La plupart des plateformes PaaS externalisent l'authentification vers un service tiers. Auth0, Clerk, Supabase Auth -- les options ne manquent pas. Mais sh0.dev est livré comme un binaire unique que vous faites tourner sur votre propre serveur avec zéro dépendance externe. Il n'y a pas de « faire appel à un fournisseur d'auth hébergé » quand vous êtes l'intégralité de la plateforme. Nous avons dû construire chaque couche nous-mêmes : hachage de mots de passe, émission de jetons, authentification à deux facteurs, gestion des clés API, et un système de chiffrement maître pour protéger les secrets au repos.

Cet article parcourt les cinq couches d'authentification que nous avons construites en Phase 9, les décisions de conception derrière chacune, et le code qui les lie ensemble.


La crate sh0-auth

L'authentification vit dans sa propre crate -- sh0-auth -- séparée des handlers API, de la couche base de données et du binaire principal. Cette séparation est délibérée. La logique d'auth change rarement. Elle a besoin de sa propre suite de tests. Et isoler le code cryptographique dans un module focalisé rend l'audit praticable.

La crate expose cinq modules : password, jwt, api_key, crypto et totp. Chacun possède une seule responsabilité.


Couche 1 : hachage de mots de passe Argon2id

Nous avons choisi Argon2id -- le gagnant du Password Hashing Competition de 2015 -- pour le stockage des mots de passe. Il combine la résistance d'Argon2i aux attaques par canal auxiliaire avec la résistance d'Argon2d au cracking GPU. L'implémentation utilise la crate argon2 avec un format de sortie PHC string, qui encode l'algorithme, la version, le coût mémoire, le coût temporel, le parallélisme, le sel et le hash dans une seule chaîne auto-descriptive.

rustpub fn hash_password(password: &str) -> Result<String, AuthError> {
    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default(); // Argon2id v19, 19 Mo de mémoire, 2 itérations
    let hash = argon2
        .hash_password(password.as_bytes(), &salt)
        .map_err(|_| AuthError::HashFailed)?;
    Ok(hash.to_string())
}

Le format PHC est important parce qu'il rend le hash auto-évolutif. Si nous augmentons plus tard le coût mémoire de 19 Mio à 64 Mio, les hashs existants se vérifient toujours correctement -- les paramètres sont embarqués dans la chaîne.

Un point subtil : pendant l'audit de sécurité, nous avons découvert une vulnérabilité d'énumération d'utilisateurs par timing. Quand une tentative de connexion ciblait un utilisateur inexistant, le serveur retournait immédiatement -- pas de calcul de hash. Le correctif : toujours exécuter une vérification Argon2id fictive même quand l'utilisateur n'existe pas.


Couche 2 : jetons JWT (HS256, expiration 7 jours)

Nous utilisons HS256 (HMAC-SHA256) via la crate jsonwebtoken. Les algorithmes asymétriques comme RS256 font sens quand plusieurs services doivent vérifier les jetons indépendamment. sh0 est un binaire unique -- HS256 est plus simple, plus rapide et parfaitement approprié.

rustpub fn create_token(user_id: &str, email: &str, role: &str, secret: &[u8]) -> Result<String, AuthError> {
    let now = chrono::Utc::now().timestamp() as usize;
    let claims = Claims {
        sub: user_id.to_string(),
        email: email.to_string(),
        role: role.to_string(),
        exp: now + 7 * 24 * 60 * 60, // 7 jours
        iat: now,
        jti: Uuid::new_v4().to_string(),
    };
    encode(&Header::default(), &claims, &EncodingKey::from_secret(secret))
        .map_err(|_| AuthError::TokenCreationFailed)
}

Chaque jeton reçoit un UUID v4 jti (JWT ID). Cela sert deux objectifs : rendre chaque jeton unique même s'il est émis dans la même seconde pour le même utilisateur, et fournir un identifiant pour une future révocation de jeton.


Couche 3 : clés API avec recherche par préfixe

Le format de clé : préfixe sh0_ suivi de 32 caractères alphanumériques aléatoires cryptographiquement. Nous ne stockons jamais la clé complète. Nous stockons un hash SHA-256 de la clé et un key_prefix séparé contenant les 8 premiers caractères après sh0_. La comparaison utilise subtle::ConstantTimeEq pour empêcher les attaques par timing.


Couche 4 : chiffrement maître AES-256-GCM

Un PaaS stocke des secrets : mots de passe de bases de données, clés API pour les services tiers, variables d'environnement marquées comme sensibles. sh0 utilise AES-256-GCM (Galois/Counter Mode) pour le chiffrement d'enveloppe, avec des clés dérivées d'une phrase de passe maître via PBKDF2 avec 100 000 itérations.

Le mode GCM fournit à la fois confidentialité et authenticité -- si un attaquant modifie le texte chiffré, le déchiffrement échoue plutôt que de produire du texte en clair corrompu.


Couche 5 : authentification à deux facteurs TOTP

La dernière couche est les mots de passe à usage unique basés sur le temps (RFC 6238). Nous avons implémenté cela avec la crate totp-rs, générant des codes à 6 chiffres avec une fenêtre de 30 secondes et une tolérance de +/-1 étape.

Le flux de configuration TOTP a trois étapes :

  1. Configuration : le serveur génère un secret base32 aléatoire et retourne une URI otpauth:// pour le scan du QR code
  2. Confirmation : l'utilisateur soumet un code TOTP valide depuis son application d'authentification, prouvant qu'il a bien enregistré le secret
  3. Connexion avec 2FA : après la vérification du mot de passe, le serveur exige un code TOTP dans une seconde requête

Les codes de secours sont à usage unique, hachés avec Argon2id avant stockage. Quand un code de secours est consommé lors de la connexion, il est définitivement supprimé de la base de données.


L'extracteur AuthUser

Les cinq couches convergent en un seul endroit : l'extracteur Axum AuthUser. N'importe quel handler qui a besoin d'authentification ajoute simplement auth: AuthUser à sa liste de paramètres. Axum appelle l'extracteur avant l'exécution du handler. Ce pattern signifie que la logique d'authentification est écrite une fois et appliquée partout par le système de types.


Le flux de première installation

sh0 n'a pas de compte admin par défaut. La première fois que vous démarrez le binaire, le système est en « mode configuration ». Le endpoint /api/auth/setup n'est disponible que quand zéro utilisateur existe. Cela élimine un problème de sécurité courant des outils auto-hébergés : les identifiants par défaut.


24 tests pour le code cryptographique

Nous avons écrit 24 tests unitaires dans sh0-auth couvrant le hachage de mots de passe, la vérification JWT, la génération de clés API, le chiffrement/déchiffrement AES-256-GCM, la vérification TOTP et les codes de secours. Total des tests après la Phase 9 : 162, tous passants.


Prochain dans la série : Nous avons audité notre propre plateforme et trouvé 88 problèmes de sécurité.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles