BlockLucky Documentation officielle GitHub

Cryptographie et secrets

Toutes les primitives cryptographiques du backend sont regroupées dans backend/src/utils/crypto.util.ts et reposent sur le module crypto de Node.js. Les secrets viennent exclusivement de l'environnement et sont validés au démarrage par backend/src/config/env.config.ts. Cette page décrit ce qui est chiffré, ce qui est haché, comment les secrets sont contrôlés et comment les faire tourner.

Inventaire

Donnée Traitement Fonction Réversible
Clé privée du portefeuille citoyen AES-256-GCM encryptText Oui, avec ENCRYPTION_KEY
Secret TOTP AES-256-GCM encryptText Oui, avec ENCRYPTION_KEY
Recto et verso d'une pièce d'identité AES-256-GCM, format binaire encryptBuffer Oui, avec ENCRYPTION_KEY, jusqu'à leur effacement à la décision de la Mairie
Mot de passe bcrypt, coût 12 hashValue Non
Code de connexion, d'inscription ou de réinitialisation HMAC-SHA-256 avec clé dérivée hmacSha256 Non
Email d'une tentative sur un compte inconnu (sujet de blocage) HMAC-SHA-256 avec clé dérivée hmacSha256 Non
Ticket de connexion avant le code de l'application (nom de la clé Redis auth:login-ticket:<HMAC>) HMAC-SHA-256 avec clé dérivée hmacSha256 Non
Code de secours 2FA SHA-256 de backup:<code> sha256 Non
Jeton de suppression RGPD SHA-256 sha256 dans backend/src/api/user/user.repo.ts Non
Signature des webhooks HMAC-SHA-256 avec WEBHOOK_SECRET hmacSha256 Sans objet
Session JWT HS256 avec JWT_SECRET backend/src/services/session.service.ts Signé, non chiffré
Email du citoyen Aucun, en clair en base

Chiffrement AES-256-GCM

Clé

ENCRYPTION_KEY contient 64 caractères hexadécimaux, soit 32 octets. Le schéma d'environnement refuse toute autre forme. La clé est chargée une fois au démarrage par Buffer.from(env.ENCRYPTION_KEY, 'hex').

Format v2:

Texte
v2:<iv en base64url>:<étiquette en base64url>:<texte chiffré en base64url>
Élément Taille Origine
v2 Préfixe Version du format (pas de la clé)
IV 12 octets crypto.randomBytes(12) à chaque chiffrement
Étiquette 16 octets cipher.getAuthTag()
Texte chiffré Variable aes-256-gcm sur le texte en UTF-8

L'IV de 12 octets est la taille recommandée pour GCM. Il est tiré au hasard pour chaque valeur ; avec quelques milliers de valeurs chiffrées sous une même clé, le risque de collision est négligeable. L'étiquette rend le chiffrement authentifié : une valeur modifiée en base ou déchiffrée avec une autre clé fait échouer decipher.final() au lieu de produire une clé privée erronée.

Format binaire des pièces d'identité

Les images des pièces d'identité sont stockées dans des colonnes BYTEA de IdentityDocument sous une forme binaire, sans préfixe de version :

Texte
<iv : 12 octets><étiquette : 16 octets><image chiffrée>

encryptBuffer tire un IV par image, et decryptBuffer refuse une valeur trop courte puis vérifie l'étiquette. Les deux faces sont chiffrées séparément. Le déchiffrement n'a lieu que dans GET /api/admin/identity-verifications/:id, tant que la demande est en attente ; l'image déchiffrée part au navigateur de l'agent en data: URL avec Cache-Control: no-store. La décision remet les deux colonnes à NULL.

Ancien format CBC

decryptText accepte encore, en lecture seule, l'ancien format iv:données en hexadécimal chiffré en AES-256-CBC. Ce format n'est plus jamais écrit. Il est non authentifié ; le garder en lecture permet de relire d'anciennes valeurs, mais aucune donnée du schéma actuel ne devrait plus l'utiliser. readTotpSecret de backend/src/api/auth/auth.services.ts accepte aussi un secret TOTP en clair s'il ne porte pas le préfixe v2:, pour la même raison de compatibilité.

Hachage

Usage Algorithme Justification
Mots de passe bcrypt, coût 12 (hashValue), comparaison par compareHash Hachage lent et salé, adapté à un secret choisi par l'utilisateur. Un compte sans mot de passe est comparé à une empreinte factice pour égaliser le temps de réponse.
Codes email HMAC-SHA-256(HMAC_KEY, "subjectId:code") Le code n'a que 10⁶ valeurs : un simple SHA-256 se retrouverait par force brute. La clé HMAC, dérivée de ENCRYPTION_KEY par HMAC(ENCRYPTION_KEY, "blocklucky:hmac"), l'empêche sans connaître la clé. Le code vit 10 minutes.
Codes de secours SHA-256("backup:" + code) 50 bits d'entropie par code. Pas de sel : un hachage lent serait plus robuste.
Jetons RGPD SHA-256(jeton) Jeton de 32 octets aléatoires : une recherche exhaustive est hors de portée.

bcrypt est fourni par bcryptjs, une implémentation en JavaScript pur : un hachage au coût 12 consomme le processeur du processus Node.js pendant plusieurs centaines de millisecondes, partagées avec toutes les autres requêtes. La limite de 30 requêtes en 10 minutes par adresse IP sur les routes d'authentification borne cette charge, mais une attaque distribuée sur /signin ralentirait toute l'API. Une implémentation native (bcrypt) ou un hachage dans un thread de travail l'éviterait.

Aléa et comparaisons

Fonction Source Usage
generateOtpCode crypto.randomInt Codes email à 6 chiffres
generateBackupCode crypto.randomInt sur un alphabet de 32 caractères Codes de secours
generateRandomToken crypto.randomBytes(32) en base64url Jetons de suppression RGPD
generatePromoCode crypto.randomBytes(6) en hexadécimal Codes promo partenaires (48 bits)
TotpService.generateSecret crypto.randomBytes(32) Secret TOTP de 160 bits
Wallet.createRandom d'ethers Générateur cryptographique d'ethers Clés de portefeuille
safeEqual crypto.timingSafeEqual après comparaison des longueurs Codes, signatures, jeton de métriques

Aucun appel à Math.random n'intervient dans une valeur de sécurité. Il sert seulement à étaler les durées de cache (withJitter de backend/src/config/redis.config.ts).

Secrets refusés en production

Quand NODE_ENV vaut production, la validation superRefine du schéma d'environnement arrête le processus si l'une de ces conditions est vraie :

Variable Condition de refus
JWT_SECRET Égale à la valeur de développement
ENCRYPTION_KEY Égale à la valeur de développement
RELAYER_PRIVATE_KEY Égale à la valeur de développement (compte n°0 de Hardhat, publiquement connu)
WEBHOOK_SECRET Égale à la valeur de développement
LOGIN_EMAIL_CODE_REQUIRED false
ADMIN_MFA_REQUIRED false
BLOCKCHAIN_MODE Différente de onchain
METRICS_TOKEN Vide alors que METRICS_ENABLED vaut true

Le processus écrit la liste des problèmes sur la sortie d'erreur puis s'arrête avec le code 1 :

Texte
Configuration invalide, démarrage interrompu :
  - JWT_SECRET : JWT_SECRET utilise la valeur de développement, interdite en production

Toutes les variables sont aussi vérifiées en forme dans tous les environnements : JWT_SECRET d'au moins 16 caractères, ENCRYPTION_KEY de 64 caractères hexadécimaux, RELAYER_PRIVATE_KEY au format 0x suivi de 64 caractères hexadécimaux, WEBHOOK_SECRET d'au moins 8 caractères.

Le fichier docker-compose.yml lance le backend avec NODE_ENV: production et transmet ${JWT_SECRET:-} : sans valeur dans l'environnement de l'hôte, la variable est une chaîne vide, que la longueur minimale refuse. La stack en conteneurs ne démarre donc pas sans secrets explicites. La procédure est décrite dans Stack complète en conteneurs.

Attention

Certaines valeurs par défaut ne sont pas refusées en production : AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY (minioadmin), le mot de passe contenu dans la valeur par défaut de DATABASE_URL, et SMTP_USER / SMTP_PASS vides. Elles doivent être fournies explicitement par l'environnement de production. Ajouter AWS_SECRET_ACCESS_KEY à la liste DEV_DEFAULTS étendrait le contrôle.

La liste complète des variables est sur la page Variables d'environnement. Le fichier backend/.env.example est comparé au schéma par npm run env:check en CI.

Stockage des secrets

Règle Mise en œuvre
Aucun secret réel dans Git gitleaks analyse tout l'historique en CI (Chaîne logicielle)
Valeurs de développement publiques Seules deux valeurs sont tolérées par .gitleaks.toml : la clé de chiffrement de développement et la clé n°0 de Hardhat, toutes deux refusées en production
Secrets par environnement Fournis par l'environnement d'exécution ; aucun gestionnaire de secrets (Vault, KMS) n'est intégré
Secrets du frontend Aucun : les variables NEXT_PUBLIC_* et EXPO_PUBLIC_* ne contiennent que des URL. La CI recherche des motifs de secret dans le bundle web.

Rotation

Il n'existe pas d'outil de rotation automatisé. Le tableau donne l'effet de chaque rotation manuelle et le travail qu'elle demande.

Secret Effet immédiat Travail associé
JWT_SECRET Toutes les sessions deviennent invalides Aucun : les utilisateurs se reconnectent
WEBHOOK_SECRET Les webhooks signés avec l'ancien secret sont refusés Mettre à jour l'émetteur en même temps
METRICS_TOKEN Le collecteur de métriques reçoit 401 Mettre à jour le collecteur
RELAYER_PRIVATE_KEY Le backend signe avec un nouveau compte Accorder les rôles au nouveau compte, révoquer l'ancien, transférer les ETH restants (voir Clés et garde des portefeuilles)
ENCRYPTION_KEY Toutes les clés de portefeuille, tous les secrets TOTP, toutes les pièces d'identité en attente et tous les codes email en cours deviennent illisibles Re-chiffrer chaque valeur avec la nouvelle clé avant la bascule

Danger

Le dépôt ne fournit aucun script de re-chiffrement, et le format v2: ne porte pas d'identifiant de clé : il n'est pas possible de faire cohabiter deux clés. Remplacer ENCRYPTION_KEY sans re-chiffrement rend les portefeuilles citoyens définitivement inutilisables. Sur un réseau de test, les fonds sont fictifs et redistribuables ; avec des fonds réels, ce serait une perte. Avant toute exploitation, il faut écrire ce script (lire chaque walletEncryptedKey, twoFactorSecret, frontEncrypted et backEncrypted, déchiffrer avec l'ancienne clé, chiffrer avec la nouvelle, dans une transaction) et ajouter un identifiant de clé au format, par exemple v3:<kid>:....

La sauvegarde de ENCRYPTION_KEY est aussi critique que celle de la base : l'une sans l'autre ne permet pas de restaurer les portefeuilles. Voir Sauvegardes et reprise.

Limites connues

Limite Détail
Clé unique pour tout Une seule ENCRYPTION_KEY protège toutes les données chiffrées et sert aussi, par dérivation, de clé HMAC. Pas de clé par utilisateur ni de séparation par usage.
Pas de module matériel Les clés vivent en mémoire du processus Node.js.
Emails en clair Nécessaires pour envoyer les codes ; protégés seulement par le contrôle d'accès à la base.
Pièces d'identité et clés dans la même base Une copie de la base et de ENCRYPTION_KEY suffit à lire les pièces en attente. Une clé dédiée, ou un stockage séparé, limiterait l'effet d'une fuite.