diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index 69b75915..20c65538 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -579,8 +579,17 @@ app.post('/api/register/start', async (c) => { // Generate challenge const challenge = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url'); - // Generate user ID - const userId = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url'); + // Derive userId deterministically from username so repeated /start calls for + // the same username produce the same WebAuthn user.id. Per WebAuthn spec, a + // repeated create() ceremony with the same (rpId, user.id) overwrites the + // existing passkey instead of creating a new one — stops failed retries from + // leaving orphan passkeys behind in the user's password manager. + const userIdSalt = process.env.USER_ID_SALT || process.env.JWT_SECRET || 'encryptid-user-id'; + const userIdBuf = await crypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(`${userIdSalt}:${username.toLowerCase()}`), + ); + const userId = Buffer.from(userIdBuf).toString('base64url'); // Store challenge in database const challengeRecord: StoredChallenge = {