ADR-0013 — QR code sécurisé AES-256-GCM avec device key par appareil
Status : Accepted Date : 2026-03 (rétroactif) Deciders : équipe sécurité Nex Tickets : —
Contexte
L’app Nex permet le paiement par QR code (le payeur scanne, ou se fait scanner). Risques :
- Replay : un QR capté en photo ou screenshot rejoué pour vol.
- Forgery : un attaquant fabrique un QR pour piéger un payeur.
- MITM : interception d’un QR dans le canal d’affichage.
Le QR doit être à la fois court (lisible par caméra fiable) et vérifiable par le serveur.
Décision
- Chiffrement : AES-256-GCM (authenticated encryption — confidentialité + intégrité + auth).
- Une clé par appareil : à l’enrôlement, le serveur génère une
device_key(AES-256 hex 64 chars) stockée dansqr_device_keys(entity TypeORM côtéauth). - Payload chiffré =
userId,deviceId,nonce,expiresAt. - Nonce garantit l’unicité (anti-replay au niveau cryptographique, vérifié serveur via cache court).
- Tag d’authentification GCM rejeté à la résolution = QR invalide.
- Rotation : la device key peut être révoquée et remplacée à tout moment côté serveur.
- Stockage côté app : la clé n’est pas stockée côté mobile — seul le QR final est stocké (limite la portée d’une compromission appareil).
Conséquences
Positives
- Confidentialité + intégrité + authenticité dans une seule primitive éprouvée.
- Anti-replay natif via nonce + TTL court.
- Révocation par appareil sans toucher les autres.
- Surface mobile minimale (pas de gestion de clé côté app).
Négatives / risques
- Si la device key fuite côté serveur, tous les QR signés par elle sont compromis. Mitigation :
qr_device_keysn’est accessible que côtéauth, et la clé est stockée chiffrée at-rest (à confirmer cf. ADR-0008 et /security/encryption-standards). - Le serveur doit valider rapidement : ajouter du cache sans casser l’anti-replay.
- Pas de SSL pinning mobile aujourd’hui (/security/known-gaps) — un attaquant qui peut intercepter le trafic peut voler un QR avant qu’il ne soit consommé.
Alternatives écartées
- HMAC seul — pas de confidentialité, le payload est lisible (révèle
userId, etc.). - RSA signature — payload trop volumineux pour QR, signature seule sans chiffrement.
- Une clé globale partagée — compromission catastrophique.
- JWT signé symétrique — proche de la décision retenue mais sans confidentialité native.
Suivi
- Voir /security/qr-code pour le détail d’implémentation.
- Code :
services/auth/src/auth/qr-validation.service.ts, entityqr-device-key.entity.ts. - Rate limiting sur
POST /qr/resolveà appliquer/renforcer (cf. known-gaps). - Migration future vers URL HTTPS au lieu de payload AES embarqué — non priorisée.