Contexte
L'identifiant OPAQUE (userIdentifier) est actuellement l'email. Une enveloppe OPAQUE est liée cryptographiquement à l'identifiant fixé à l'inscription et ne peut pas être re-générée côté serveur (il faudrait le mot de passe en clair). Comme l'email est mutable, on a dû :
- stocker l'identifiant d'inscription dans
opaque_records.user_identifier (colonne nullable, migration 0020 + backfill 0021),
- le rejouer au login (
auth-login.ts : record?.opaqueIdentifier ?? userIdentifier),
- l'épingler au change-email (
auth-account.ts : COALESCE(existing, oldEmail)).
L'invariant actuel est cohérent (« user_identifier = l'email sous lequel l'enveloppe courante a été créée » ; tous les flux d'écriture mettent à jour enveloppe + identifiant ensemble, le change-email épingle parce qu'il bouge l'email sans re-créer d'enveloppe). Mais le design sent la rustine : l'identité crypto est une donnée mutable (l'email), d'où la colonne nullable + les fallbacks ?? email / COALESCE.
Le client ne passe jamais d'identifiant OPAQUE (vérifié : client.startRegistration/finishRegistration/startLogin/finishLogin ne reçoivent que le password) — l'identifiant est choisi uniquement côté serveur. On peut donc changer ce que le serveur inscrit sans toucher au client.
Proposition
Lier l'enveloppe à users.id (UUID immuable) au lieu de l'email. L'email n'est alors plus jamais l'identité crypto ; le change-email redevient un simple UPDATE (+ notif + révocation) qui ne touche plus opaque_records.
Les 4 flux d'écriture d'enveloppe doivent basculer ensemble (sinon risque de lockout) :
auth-register-v2.ts (createRegistrationResponse + insert opaque_records)
auth-change-password.ts (:116, :163)
auth-recovery.ts (:355, :442)
auth-reset.ts (:186, :270)
Le login lit déjà opaque_records.user_identifier → marche pour tout le monde.
Option A — forward-only (sûre, recommandée pour commencer)
- Inscription + les 3 re-registrations utilisent
users.id.
- Comptes existants inchangés (leur enveloppe + identifiant email restent ; ils ne peuvent pas être re-keyés sans le mot de passe → legacy permanent, le fallback reste pour eux).
- Zéro risque pour l'existant.
Option B — nettoyage complet (après A)
- Confirmer 0 ligne
user_identifier NULL en prod (la 0021 a backfillé, l'inscription remplit toujours).
- Migration
NOT NULL sur user_identifier.
- Supprimer le fallback
?? email (login) et le COALESCE (change-email).
- change-email ne touche plus
opaque_records du tout.
Incidence sécurité
Aucune incidence négative. Neutre sur le cœur OPAQUE (l'identifiant est un input public, pas un secret ; la seule exigence est la cohérence écriture↔login). Le mot de passe n'est toujours pas transmis, l'enveloppe toujours indéchiffrable côté serveur, la dérivation de la clé qui déwrappe le KEK est indépendante de l'identifiant.
Petits gains :
- Privacy : la crypto est liée à un UUID opaque, plus à une PII (l'email).
- Liaison d'identité : un UUID est unique pour toujours (un email est réattribuable → deux enveloppes historiquement liées au même identifiant).
- Robustesse / dispo : un identifiant immuable rend l'invariant de cohérence impossible à casser par dérive → réduit le risque de re-créer un lockout type 1.5.
Seul risque = disponibilité (lockout), et il échoue fermé (jamais d'accès non autorisé) : si l'identifiant à l'écriture diverge de celui rejoué au login (bug), la personne ne peut plus se connecter, point. D'où l'exigence : basculer les 4 flux de façon cohérente + tests.
Le design actuel est correct et sûr. Ce n'est pas une faille à réparer — c'est de la dette technique / propreté. Ne rien faire est acceptable.
Critères d'acceptation
Refs
- Finding 1.5 (
docs/Audit-2026-06.md) — corrigé et prouvé par tests, c'est ce qui a déclenché la discussion.
- Discussion : pourquoi Proton fige l'email (l'adresse y est le produit + l'identité PGP + le handle de routage fédéré) alors que pour Nodea l'email n'est qu'une clé de login → choix inverse justifié.
Contexte
L'identifiant OPAQUE (
userIdentifier) est actuellement l'email. Une enveloppe OPAQUE est liée cryptographiquement à l'identifiant fixé à l'inscription et ne peut pas être re-générée côté serveur (il faudrait le mot de passe en clair). Comme l'email est mutable, on a dû :opaque_records.user_identifier(colonne nullable, migration 0020 + backfill 0021),auth-login.ts:record?.opaqueIdentifier ?? userIdentifier),auth-account.ts:COALESCE(existing, oldEmail)).L'invariant actuel est cohérent («
user_identifier= l'email sous lequel l'enveloppe courante a été créée » ; tous les flux d'écriture mettent à jour enveloppe + identifiant ensemble, le change-email épingle parce qu'il bouge l'email sans re-créer d'enveloppe). Mais le design sent la rustine : l'identité crypto est une donnée mutable (l'email), d'où la colonne nullable + les fallbacks?? email/COALESCE.Le client ne passe jamais d'identifiant OPAQUE (vérifié :
client.startRegistration/finishRegistration/startLogin/finishLoginne reçoivent que le password) — l'identifiant est choisi uniquement côté serveur. On peut donc changer ce que le serveur inscrit sans toucher au client.Proposition
Lier l'enveloppe à
users.id(UUID immuable) au lieu de l'email. L'email n'est alors plus jamais l'identité crypto ; le change-email redevient un simpleUPDATE(+ notif + révocation) qui ne touche plusopaque_records.Les 4 flux d'écriture d'enveloppe doivent basculer ensemble (sinon risque de lockout) :
auth-register-v2.ts(createRegistrationResponse+ insertopaque_records)auth-change-password.ts(:116,:163)auth-recovery.ts(:355,:442)auth-reset.ts(:186,:270)Le login lit déjà
opaque_records.user_identifier→ marche pour tout le monde.Option A — forward-only (sûre, recommandée pour commencer)
users.id.Option B — nettoyage complet (après A)
user_identifierNULL en prod (la 0021 a backfillé, l'inscription remplit toujours).NOT NULLsuruser_identifier.?? email(login) et leCOALESCE(change-email).opaque_recordsdu tout.Incidence sécurité
Aucune incidence négative. Neutre sur le cœur OPAQUE (l'identifiant est un input public, pas un secret ; la seule exigence est la cohérence écriture↔login). Le mot de passe n'est toujours pas transmis, l'enveloppe toujours indéchiffrable côté serveur, la dérivation de la clé qui déwrappe le KEK est indépendante de l'identifiant.
Petits gains :
Seul risque = disponibilité (lockout), et il échoue fermé (jamais d'accès non autorisé) : si l'identifiant à l'écriture diverge de celui rejoué au login (bug), la personne ne peut plus se connecter, point. D'où l'exigence : basculer les 4 flux de façon cohérente + tests.
Critères d'acceptation
users.id.NOT NULL, suppression des fallbacks, change-email sans accèsopaque_records.Auth-Spec.mddocumente la règle : identifiant OPAQUE =users.id, jamais l'email (+ note legacy email pour les comptes pré-bascule).Refs
docs/Audit-2026-06.md) — corrigé et prouvé par tests, c'est ce qui a déclenché la discussion.