From f532d3ace30d7f75880a0d30d5e7bda5ca7f90d9 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Mon, 6 Jul 2026 22:25:02 +0800 Subject: [PATCH] Preserve WebAuthn credential purpose in backups --- src/services/backup-archive.ts | 5 +++-- src/services/backup-import.ts | 11 +++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/services/backup-archive.ts b/src/services/backup-archive.ts index f28e568..1a8a0e2 100644 --- a/src/services/backup-archive.ts +++ b/src/services/backup-archive.ts @@ -442,9 +442,10 @@ export function validateBackupPayloadContents( for (const row of accountPasskeyRows) { const id = String(row.id || '').trim(); const userId = String(row.user_id || '').trim(); + const purpose = row.purpose == null ? 'login' : String(row.purpose || '').trim(); const credentialId = String(row.credential_id || '').trim(); const publicKey = String(row.public_key || '').trim(); - if (!id || !userIds.has(userId) || !credentialId || !publicKey) { + if (!id || !userIds.has(userId) || !credentialId || !publicKey || (purpose !== 'login' && purpose !== 'twoFactor')) { throw new Error('Backup archive contains an invalid account passkey row'); } if (accountPasskeyIds.has(id)) throw new Error(`Backup archive contains duplicate account passkey id: ${id}`); @@ -493,7 +494,7 @@ export async function buildBackupArchive( queryRows(env.DB, 'SELECT id, user_id, name, created_at, updated_at FROM folders ORDER BY created_at ASC'), queryRows(env.DB, 'SELECT id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, archived_at, deleted_at FROM ciphers ORDER BY created_at ASC'), queryRows(env.DB, 'SELECT id, cipher_id, file_name, size, size_name, key FROM attachments ORDER BY cipher_id ASC, id ASC'), - queryRows(env.DB, 'SELECT id, user_id, name, public_key, credential_id, counter, type, aa_guid, transports, encrypted_user_key, encrypted_public_key, encrypted_private_key, supports_prf, created_at, updated_at FROM webauthn_credentials ORDER BY created_at ASC'), + queryRows(env.DB, 'SELECT id, user_id, purpose, name, public_key, credential_id, counter, type, aa_guid, transports, encrypted_user_key, encrypted_public_key, encrypted_private_key, supports_prf, created_at, updated_at FROM webauthn_credentials ORDER BY created_at ASC'), queryRows(env.DB, 'SELECT token, user_id, device_identifier, expires_at FROM trusted_two_factor_device_tokens WHERE expires_at >= ? ORDER BY user_id ASC, device_identifier ASC, expires_at DESC', date.getTime()), ]); const exportedConfigRows = sanitizeConfigRowsForExport(configRows); diff --git a/src/services/backup-import.ts b/src/services/backup-import.ts index 03364a8..66d2be2 100644 --- a/src/services/backup-import.ts +++ b/src/services/backup-import.ts @@ -254,6 +254,10 @@ function cloneRows(rows: SqlRow[]): SqlRow[] { return rows.map((row) => ({ ...row })); } +function normalizeAccountPasskeyPurpose(value: unknown): 'login' | 'twoFactor' { + return value == null ? 'login' : String(value).trim() === 'twoFactor' ? 'twoFactor' : 'login'; +} + function upsertConfigRow(rows: SqlRow[], key: string, value: string): SqlRow[] { let replaced = false; const nextRows = rows.map((row) => { @@ -303,7 +307,10 @@ async function importPreparedBackupRows(db: D1Database, payload: BackupPayload[' domain_settings: cloneRows(payload.domain_settings || []), user_revisions: cloneRows(payload.user_revisions || []), trusted_two_factor_device_tokens: cloneRows(payload.trusted_two_factor_device_tokens || []), - webauthn_credentials: cloneRows(payload.webauthn_credentials || []), + webauthn_credentials: cloneRows(payload.webauthn_credentials || []).map((row) => ({ + ...row, + purpose: normalizeAccountPasskeyPurpose(row.purpose), + })), folders: cloneRows(payload.folders || []), ciphers: cloneRows(payload.ciphers || []).map((row) => ({ ...row, @@ -668,7 +675,7 @@ async function importBackupRows(db: D1Database, payload: BackupPayload['db'], us buildInsertStatements( db, tableName('webauthn_credentials'), - ['id', 'user_id', 'name', 'public_key', 'credential_id', 'counter', 'type', 'aa_guid', 'transports', 'encrypted_user_key', 'encrypted_public_key', 'encrypted_private_key', 'supports_prf', 'created_at', 'updated_at'], + ['id', 'user_id', 'purpose', 'name', 'public_key', 'credential_id', 'counter', 'type', 'aa_guid', 'transports', 'encrypted_user_key', 'encrypted_public_key', 'encrypted_private_key', 'supports_prf', 'created_at', 'updated_at'], payload.webauthn_credentials || [] ) );