diff --git a/apps/browser-extension/src/entrypoints/background.ts b/apps/browser-extension/src/entrypoints/background.ts index f8ff5e333..8b779af36 100644 --- a/apps/browser-extension/src/entrypoints/background.ts +++ b/apps/browser-extension/src/entrypoints/background.ts @@ -3,7 +3,7 @@ import { onMessage, sendMessage } from "webext-bridge/background"; import { handleResetAutoLockTimer, handlePopupHeartbeat, handleSetAutoLockTimeout } from '@/entrypoints/background/AutolockTimeoutHandler'; import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardClearTimeout, handleSetClipboardClearTimeout, handleGetClipboardCountdownState } from '@/entrypoints/background/ClipboardClearHandler'; import { setupContextMenus } from '@/entrypoints/background/ContextMenu'; -import { handleGetWebAuthnSettings, handleWebAuthnCreate, handleWebAuthnGet, handleStorePasskey, handleUpdatePasskeyLastUsed, handleClearAllPasskeys, handlePasskeyPopupResponse, initializePasskeys, handleGetRequestData, handleGetPasskeyById } from '@/entrypoints/background/PasskeyHandler'; +import { handleGetWebAuthnSettings, handleWebAuthnCreate, handleWebAuthnGet, handleStorePasskey, handleUpdatePasskeyLastUsed, handleClearAllPasskeys, handleDeletePasskey, handlePasskeyPopupResponse, initializePasskeys, handleGetRequestData, handleGetPasskeyById } from '@/entrypoints/background/PasskeyHandler'; import { handleOpenPopup, handlePopupWithCredential, handleOpenPopupCreateCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler'; import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetEncryptionKey, handleGetEncryptionKeyDerivationParams, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreEncryptionKey, handleStoreEncryptionKeyDerivationParams, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler'; @@ -70,6 +70,7 @@ export default defineBackground({ onMessage('STORE_PASSKEY', ({ data }) => handleStorePasskey(data as { rpId: string; credentialId: string; displayName: string; publicKey: unknown })); onMessage('UPDATE_PASSKEY_LAST_USED', ({ data }) => handleUpdatePasskeyLastUsed(data as { credentialId: string; newSignCount?: number })); onMessage('CLEAR_ALL_PASSKEYS', () => handleClearAllPasskeys()); + onMessage('DELETE_PASSKEY', ({ data }) => handleDeletePasskey(data as { credentialId: string })); onMessage('PASSKEY_POPUP_RESPONSE', ({ data }) => handlePasskeyPopupResponse(data as { requestId: string; credential?: any; fallback?: boolean; cancelled?: boolean })); onMessage('GET_REQUEST_DATA', ({ data }) => handleGetRequestData(data as { requestId: string })); onMessage('GET_PASSKEY_BY_ID', ({ data }) => handleGetPasskeyById(data as { credentialId: string })); diff --git a/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts b/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts index 2156dbd2f..6b65a762b 100644 --- a/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts +++ b/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts @@ -250,7 +250,29 @@ export async function handleStorePasskey(data: { * In production, this would be stored in the vault database * For now, also store in local storage for persistence across reloads */ - const storedPasskeys: Record = await storage.getItem('local:passkeys') || {}; + let storedPasskeys: Record = {}; + const rawData = await storage.getItem('local:passkeys'); + + // Handle migration from old array format or corrupted string data + if (typeof rawData === 'string') { + // Data was stored as stringified JSON (old format), clear it + console.warn('PasskeyHandler: Found old/corrupted passkey storage format, migrating...'); + try { + const parsed = JSON.parse(rawData); + if (Array.isArray(parsed)) { + // Convert array to record format + for (const pk of parsed) { + const pkKey = `${pk.rpId}:${pk.credentialId}`; + storedPasskeys[pkKey] = pk; + } + } + } catch (e) { + console.error('PasskeyHandler: Failed to migrate old passkey data', e); + } + } else if (rawData && typeof rawData === 'object') { + storedPasskeys = rawData as Record; + } + storedPasskeys[key] = passkey; await storage.setItem('local:passkeys', storedPasskeys); @@ -275,7 +297,13 @@ export async function handleUpdatePasskeyLastUsed(data: { sessionPasskeys.set(key, passkey); // Update in storage too - const storedPasskeys: Record = await storage.getItem('local:passkeys') || {}; + const rawData = await storage.getItem('local:passkeys'); + let storedPasskeys: Record = {}; + + if (typeof rawData === 'object' && rawData !== null) { + storedPasskeys = rawData as Record; + } + if (storedPasskeys[key]) { storedPasskeys[key] = passkey; await storage.setItem('local:passkeys', storedPasskeys); @@ -310,7 +338,29 @@ function getPasskeysForOrigin(origin: string): IPasskeyData[] { * Initialize passkeys from storage on startup */ export async function initializePasskeys(): Promise { - const storedPasskeys: Record = await storage.getItem('local:passkeys') || {}; + const rawData = await storage.getItem('local:passkeys'); + let storedPasskeys: Record = {}; + + // Handle migration from old array format or corrupted string data + if (typeof rawData === 'string') { + console.warn('PasskeyHandler: Found old/corrupted passkey storage format during init, migrating...'); + try { + const parsed = JSON.parse(rawData); + if (Array.isArray(parsed)) { + // Convert array to record format + for (const pk of parsed) { + const pkKey = `${pk.rpId}:${pk.credentialId}`; + storedPasskeys[pkKey] = pk; + } + // Save migrated data + await storage.setItem('local:passkeys', storedPasskeys); + } + } catch (e) { + console.error('PasskeyHandler: Failed to migrate old passkey data during init', e); + } + } else if (rawData && typeof rawData === 'object') { + storedPasskeys = rawData as Record; + } for (const [key, passkey] of Object.entries(storedPasskeys)) { sessionPasskeys.set(key, passkey as IPasskeyData); @@ -385,3 +435,29 @@ export async function handleClearAllPasskeys(): Promise<{ success: boolean }> { await storage.removeItem('local:passkeys'); return { success: true }; } + +/** + * Delete a specific passkey + */ +export async function handleDeletePasskey(data: { credentialId: string }): Promise<{ success: boolean }> { + const { credentialId } = data; + + // Find and remove from session storage + let deletedKey: string | null = null; + for (const [key, passkey] of sessionPasskeys.entries()) { + if (passkey.credentialId === credentialId) { + sessionPasskeys.delete(key); + deletedKey = key; + break; + } + } + + if (deletedKey) { + // Remove from storage (storage API expects Record format, not stringified) + const storedPasskeys: Record = await storage.getItem('local:passkeys') || {}; + delete storedPasskeys[deletedKey]; + await storage.setItem('local:passkeys', storedPasskeys); + } + + return { success: !!deletedKey }; +} diff --git a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx index cdb134df7..09a187185 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx @@ -74,10 +74,10 @@ const PasskeyAuthenticate: React.FC = () => { }, [location, setIsInitialLoading]); /** - * + * Handle passkey authentication */ - const handleUsePasskey = async () => { - if (!request || !selectedPasskey) { + const handleUsePasskey = async (credentialId: string) => { + if (!request) { return; } @@ -85,7 +85,7 @@ const PasskeyAuthenticate: React.FC = () => { try { console.log('PasskeyAuthenticate: Starting authentication'); - console.log('PasskeyAuthenticate: selectedPasskey', selectedPasskey); + console.log('PasskeyAuthenticate: credentialId', credentialId); console.log('PasskeyAuthenticate: request', request); // Create the provider with storage callbacks @@ -112,16 +112,16 @@ const PasskeyAuthenticate: React.FC = () => { }; // Get the assertion using the provider - const credential = await provider.getAssertion(getRequest, selectedPasskey, { - uvPerformed: false, // Set to true if you implement actual user verification + const credential = await provider.getAssertion(getRequest, credentialId, { + uvPerformed: true, // Set to true if you implement actual user verification includeBEBS: true // Include backup-eligible and backup-state flags }); - console.info('PasskeyAuthenticate: Created credential successfully', credential); + console.info('PasskeyAuthenticate: Received assertion successfully', credential); // Update last used timestamp await sendMessage('UPDATE_PASSKEY_LAST_USED', { - credentialId: selectedPasskey + credentialId }, 'background'); // Send response back @@ -138,6 +138,35 @@ const PasskeyAuthenticate: React.FC = () => { } }; + /** + * Handle passkey deletion + */ + const handleDeletePasskey = async (credentialId: string, event: React.MouseEvent) => { + event.stopPropagation(); // Prevent triggering authentication + + if (!confirm('Are you sure you want to delete this passkey?')) { + return; + } + + try { + await sendMessage('DELETE_PASSKEY', { credentialId }, 'background'); + + // Update the request to remove the deleted passkey + if (request?.passkeys) { + const updatedPasskeys = request.passkeys.filter(pk => pk.id !== credentialId); + setRequest({ ...request, passkeys: updatedPasskeys }); + + // Clear selection if the deleted passkey was selected + if (selectedPasskey === credentialId) { + setSelectedPasskey(null); + } + } + } catch (error) { + console.error('Failed to delete passkey:', error); + alert(`Failed to delete passkey: ${error instanceof Error ? error.message : String(error)}`); + } + }; + /** * */ @@ -195,24 +224,34 @@ const PasskeyAuthenticate: React.FC = () => { {request.passkeys && request.passkeys.length > 0 ? (
{request.passkeys.map((pk) => (
setSelectedPasskey(pk.id)} + className="relative group p-3 rounded-lg border cursor-pointer transition-colors bg-white border-gray-200 hover:bg-blue-50 hover:border-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:hover:bg-blue-900 dark:hover:border-blue-700" + onClick={() => !loading && handleUsePasskey(pk.id)} > -
- {pk.displayName} -
-
- Last used: {pk.lastUsed || 'Never'} +
+
+
+ {pk.displayName} +
+
+ Last used: {pk.lastUsed || 'Never'} +
+
+
))} @@ -228,17 +267,6 @@ const PasskeyAuthenticate: React.FC = () => {
- {request.passkeys && request.passkeys.length > 0 && ( - - )} -