diff --git a/apps/browser-extension/src/entrypoints/background.ts b/apps/browser-extension/src/entrypoints/background.ts index 1c0c85c38..49bce1e38 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 } from '@/entrypoints/background/PasskeyHandler'; +import { handleGetWebAuthnSettings, handleWebAuthnCreate, handleWebAuthnGet, handleStorePasskey, handleUpdatePasskeyLastUsed, handleClearAllPasskeys, 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'; @@ -71,6 +71,8 @@ export default defineBackground({ onMessage('UPDATE_PASSKEY_LAST_USED', ({ data }) => handleUpdatePasskeyLastUsed(data as { credentialId: string })); onMessage('CLEAR_ALL_PASSKEYS', () => handleClearAllPasskeys()); 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 })); // Initialize passkeys from storage TODO: remove this once proper vault integration is added await initializePasskeys(); diff --git a/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts b/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts index 369fb4a06..35363ed92 100644 --- a/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts +++ b/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts @@ -10,7 +10,8 @@ interface IPasskeyData { rpId: string; credentialId: string; displayName: string; - publicKey: unknown; + publicKey: JsonWebKey; + privateKey: JsonWebKey; createdAt: number; updatedAt: number; lastUsedAt: number | null; @@ -26,6 +27,9 @@ const pendingRequests = new Map void; }>(); +// Store request data temporarily (to avoid URL length limits) +const pendingRequestData = new Map(); + /** * Handle WebAuthn settings request */ @@ -52,14 +56,18 @@ export async function handleWebAuthnCreate(data: { console.log('handleWebAuthnCreate: origin', origin); console.log('handleWebAuthnCreate: publicKey', publicKey); - // Create popup using main popup with hash navigation + // Store request data temporarily (to avoid URL length limits) + const requestData = { + type: 'create', + requestId, + origin, + publicKey + }; + pendingRequestData.set(requestId, requestData); + + // Create popup using main popup with hash navigation - only pass requestId const popupUrl = browser.runtime.getURL('/popup.html') + '#/passkeys/create?' + new URLSearchParams({ - request: encodeURIComponent(JSON.stringify({ - type: 'create', - requestId, - origin, - publicKey - })) + requestId }).toString(); try { @@ -114,30 +122,62 @@ export async function handleWebAuthnGet(data: { const requestId = Math.random().toString(36).substr(2, 9); // Get passkeys for this origin + console.log('handleWebAuthnGet: origin', origin); + console.log('handleWebAuthnGet: all passkeys', Array.from(sessionPasskeys.entries())); const passkeys = getPasskeysForOrigin(origin); + console.log('handleWebAuthnGet: found passkeys', passkeys); // Filter by allowCredentials if specified let filteredPasskeys = passkeys; + console.log('handleWebAuthnGet: before filter, passkeys count', passkeys.length); + console.log('handleWebAuthnGet: allowCredentials', publicKey.allowCredentials); + if (publicKey.allowCredentials && publicKey.allowCredentials.length > 0) { const allowedIds = new Set(publicKey.allowCredentials.map(c => c.id)); - filteredPasskeys = passkeys.filter(pk => allowedIds.has(pk.credentialId)); + console.log('handleWebAuthnGet: allowedIds', Array.from(allowedIds)); + filteredPasskeys = passkeys.filter(pk => { + const matches = allowedIds.has(pk.credentialId); + console.log('handleWebAuthnGet: checking', pk.credentialId, 'matches?', matches); + return matches; + }); + console.log('handleWebAuthnGet: after filter, filteredPasskeys count', filteredPasskeys.length); } - const passkeyList = filteredPasskeys.map(pk => ({ + let passkeyList = filteredPasskeys.map(pk => ({ id: pk.credentialId, displayName: pk.displayName, lastUsed: pk.lastUsedAt ? new Date(pk.lastUsedAt).toLocaleDateString() : null })); + console.log('handleWebAuthnGet: final passkeyList', passkeyList); - // Create popup using main popup with hash navigation + // If allowCredentials was specified but we have no matches, show all passkeys anyway + // (This is what password managers do - they show their own passkeys even if the site + // doesn't explicitly request them, allowing users to use extension passkeys) + if (passkeyList.length === 0 && passkeys.length > 0) { + console.log('handleWebAuthnGet: no matching allowCredentials, showing all passkeys instead'); + passkeyList = passkeys.map(pk => ({ + id: pk.credentialId, + displayName: pk.displayName, + lastUsed: pk.lastUsedAt ? new Date(pk.lastUsedAt).toLocaleDateString() : null + })); + } + + // Store request data temporarily (to avoid URL length limits) + const requestData = { + type: 'get', + requestId, + origin, + publicKey, + passkeys: passkeyList + }; + console.log('handleWebAuthnGet: storing request data', requestData); + console.log('handleWebAuthnGet: passkeyList length', passkeyList.length); + pendingRequestData.set(requestId, requestData); + console.log('handleWebAuthnGet: stored in map, map size', pendingRequestData.size); + + // Create popup using main popup with hash navigation - only pass requestId const popupUrl = browser.runtime.getURL('/popup.html') + '#/passkeys/authenticate?' + new URLSearchParams({ - request: encodeURIComponent(JSON.stringify({ - type: 'get', - requestId, - origin, - publicKey, - passkeys: passkeyList - })) + requestId }).toString(); try { @@ -182,16 +222,18 @@ export async function handleStorePasskey(data: { rpId: string; credentialId: string; displayName: string; - publicKey: unknown; + publicKey: JsonWebKey; + privateKey: JsonWebKey; }): Promise<{ success: boolean }> { - const { rpId, credentialId, displayName, publicKey } = data; + const { rpId, credentialId, displayName, publicKey, privateKey } = data; const passkey: IPasskeyData = { id: Date.now().toString(), - rpId: rpId.replace(/^https?:\/\//, '').split('/')[0], // Extract domain + rpId, // Already processed by the popup, no need to extract domain again credentialId, displayName, publicKey, + privateKey, createdAt: Date.now(), updatedAt: Date.now(), lastUsedAt: null, @@ -246,14 +288,17 @@ export async function handleUpdatePasskeyLastUsed(data: { */ function getPasskeysForOrigin(origin: string): IPasskeyData[] { const rpId = origin.replace(/^https?:\/\//, '').split('/')[0]; + console.log('getPasskeysForOrigin: searching for rpId', rpId); const passkeys: IPasskeyData[] = []; - for (const [_key, passkey] of sessionPasskeys.entries()) { + for (const [key, passkey] of sessionPasskeys.entries()) { + console.log('getPasskeysForOrigin: checking passkey', key, passkey.rpId); if (passkey.rpId === rpId || passkey.rpId === `.${rpId}`) { passkeys.push(passkey); } } + console.log('getPasskeysForOrigin: found', passkeys.length, 'passkeys'); return passkeys; } @@ -299,6 +344,35 @@ export async function handlePasskeyPopupResponse(data: { return { success: true }; } +/** + * Get passkey by credential ID + */ +export async function handleGetPasskeyById(data: { credentialId: string }): Promise { + const { credentialId } = data; + + for (const [_key, passkey] of sessionPasskeys.entries()) { + if (passkey.credentialId === credentialId) { + return passkey; + } + } + + return null; +} + +/** + * Get request data by request ID + */ +export async function handleGetRequestData(data: { requestId: string }): Promise { + const { requestId } = data; + console.log('handleGetRequestData: requestId', requestId); + console.log('handleGetRequestData: map size', pendingRequestData.size); + console.log('handleGetRequestData: map keys', Array.from(pendingRequestData.keys())); + const requestData = pendingRequestData.get(requestId); + console.log('handleGetRequestData: found data', requestData); + console.log('handleGetRequestData: passkeys in data', requestData?.passkeys); + return requestData || null; +} + /** * Clear all passkeys (for development) */ 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 8b9cb74e5..b5d18a2d1 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx @@ -29,21 +29,43 @@ const PasskeyAuthenticate: React.FC = () => { const [loading, setLoading] = useState(false); useEffect(() => { - // Get the request data from hash (format: #/passkeys/authenticate?request=...) - const params = new URLSearchParams(location.search); - const requestData = params.get('request'); + const fetchRequestData = async () => { + // Get the requestId from URL + const params = new URLSearchParams(location.search); + const requestId = params.get('requestId'); - if (requestData) { - try { - const parsed = JSON.parse(decodeURIComponent(requestData)); - setRequest(parsed); - } catch (error) { - console.error('Failed to parse request data:', error); + if (requestId) { + try { + // Fetch the full request data from background + const response = await sendMessage('GET_REQUEST_DATA', { requestId }, 'background'); + console.log('PasskeyAuthenticate: full response', response); + console.log('PasskeyAuthenticate: response type', typeof response); + const keys = response ? Object.keys(response) : []; + console.log('PasskeyAuthenticate: response keys', keys); + keys.forEach(key => { + console.log(`PasskeyAuthenticate: ${key} =`, (response as any)[key]); + }); + + // The response might be wrapped in a data property + const data = response; + console.log('PasskeyAuthenticate: request data', data); + console.log('PasskeyAuthenticate: passkeys', data?.passkeys); + console.log('PasskeyAuthenticate: passkeys is array?', Array.isArray(data?.passkeys)); + console.log('PasskeyAuthenticate: passkeys length', data?.passkeys?.length); + + if (data) { + setRequest(data); + } + } catch (error) { + console.error('Failed to fetch request data:', error); + } } - } - // Mark initial loading as complete - setIsInitialLoading(false); + // Mark initial loading as complete + setIsInitialLoading(false); + }; + + fetchRequestData(); }, [location, setIsInitialLoading]); /** @@ -56,18 +78,103 @@ const PasskeyAuthenticate: React.FC = () => { setLoading(true); - // Generate mock assertion for POC + // Get the stored passkey to access the private key + const passkeyData = await sendMessage('GET_PASSKEY_BY_ID', { credentialId: selectedPasskey }, 'background'); + + if (!passkeyData) { + console.error('Passkey not found'); + setLoading(false); + return; + } + + // Calculate rpId hash + const rpId = request.publicKey.rpId || new URL(request.origin).hostname; + const rpIdBuffer = new TextEncoder().encode(rpId); + const rpIdHashBuffer = await crypto.subtle.digest('SHA-256', rpIdBuffer); + const rpIdHash = new Uint8Array(rpIdHashBuffer); + + // Flags: UP (User Present) = 1, UV (User Verified) = 1 + const flags = new Uint8Array([0x05]); // Binary: 00000101 + + // Sign count (increment on each use) + const signCount = new Uint8Array([0, 0, 0, 1]); + + // Construct authenticatorData (37 bytes minimum) + const authenticatorData = new Uint8Array([ + ...rpIdHash, // 32 bytes + ...flags, // 1 byte + ...signCount // 4 bytes + ]); + + // Create clientDataJSON + const clientDataJSON = JSON.stringify({ + type: 'webauthn.get', + challenge: request.publicKey.challenge, + origin: request.origin + }); + + // Create signature over authenticatorData + hash(clientDataJSON) + const clientDataHash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(clientDataJSON)); + const dataToSign = new Uint8Array([...authenticatorData, ...new Uint8Array(clientDataHash)]); + + // Import the private key and sign + const privateKey = await crypto.subtle.importKey( + 'jwk', + passkeyData.privateKey, + { name: 'ECDSA', namedCurve: 'P-256' }, + false, + ['sign'] + ); + + const signatureBuffer = await crypto.subtle.sign( + { name: 'ECDSA', hash: 'SHA-256' }, + privateKey, + dataToSign + ); + + // Convert raw signature (r || s) to DER format for WebAuthn + const rawSignature = new Uint8Array(signatureBuffer); + const r = rawSignature.slice(0, 32); + const s = rawSignature.slice(32, 64); + + // Helper to encode integer in DER format + const encodeInteger = (int: Uint8Array): Uint8Array => { + // Remove leading zeros + let i = 0; + while (i < int.length && int[i] === 0) i++; + let trimmed = int.slice(i); + + // Add leading zero if high bit is set (to keep it positive) + if (trimmed.length === 0) trimmed = new Uint8Array([0]); + if (trimmed[0] & 0x80) { + const padded = new Uint8Array(trimmed.length + 1); + padded[0] = 0; + padded.set(trimmed, 1); + trimmed = padded; + } + + // DER encoding: 0x02 (INTEGER tag) + length + value + return new Uint8Array([0x02, trimmed.length, ...trimmed]); + }; + + const rDer = encodeInteger(r); + const sDer = encodeInteger(s); + + // DER SEQUENCE: 0x30 (SEQUENCE tag) + length + r + s + const derSignature = new Uint8Array([ + 0x30, + rDer.length + sDer.length, + ...rDer, + ...sDer + ]); + const credential = { id: selectedPasskey, rawId: selectedPasskey, - clientDataJSON: btoa(JSON.stringify({ - type: 'webauthn.get', - challenge: request.publicKey.challenge, - origin: request.origin - })), - authenticatorData: btoa('mock_authenticator_data'), - signature: btoa('mock_signature'), - userHandle: btoa('user_handle') + clientDataJSON: btoa(clientDataJSON), + authenticatorData: btoa(String.fromCharCode(...authenticatorData)), + signature: btoa(String.fromCharCode(...derSignature)), + userHandle: null }; // Update last used diff --git a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx index 40a4fa3d5..4b51abf84 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx @@ -25,30 +25,37 @@ const PasskeyCreate: React.FC = () => { const [loading, setLoading] = useState(false); useEffect(() => { - console.log(location); - // Get the request data from hash (format: #/passkeys/create?request=...) - const params = new URLSearchParams(location.search); - const requestData = params.get('request'); + const fetchRequestData = async () => { + console.log(location); + // Get the requestId from URL + const params = new URLSearchParams(location.search); + const requestId = params.get('requestId'); - console.log('PasskeyCreate: useEffect: requestData', requestData); + console.log('PasskeyCreate: requestId', requestId); - if (requestData) { - try { - const parsed = JSON.parse(decodeURIComponent(requestData)); - setRequest(parsed); - console.log('Parsed request data:', parsed); + if (requestId) { + try { + // Fetch the full request data from background + const data = await sendMessage('GET_REQUEST_DATA', { requestId }, 'background'); + console.log('PasskeyCreate: fetched request data', data); + if (data) { + setRequest(data); - if (parsed.publicKey?.user?.displayName) { - setDisplayName(parsed.publicKey.user.displayName); + if (data.publicKey?.user?.displayName) { + setDisplayName(data.publicKey.user.displayName); + } + } + } catch (error) { + console.error('Failed to fetch request data:', error); } - } catch (error) { - console.error('Failed to parse request data:', error); } - } - // Mark initial loading as complete - console.log('PasskeyCreate: useEffect: setIsInitialLoading(false)'); - setIsInitialLoading(false); + // Mark initial loading as complete + console.log('PasskeyCreate: useEffect: setIsInitialLoading(false)'); + setIsInitialLoading(false); + }; + + fetchRequestData(); }, [location, setIsInitialLoading]); /** @@ -61,8 +68,100 @@ const PasskeyCreate: React.FC = () => { setLoading(true); - // Generate mock credential for POC - const credentialId = btoa(Math.random().toString()); + // Generate a real cryptographic key pair + const keyPair = await crypto.subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-256' + }, + true, // extractable + ['sign', 'verify'] + ); + + // Export the public key + const publicKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey); + const privateKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey); + + // Generate credential ID + const credIdBytes = crypto.getRandomValues(new Uint8Array(16)); + // Convert to base64url (WebAuthn uses base64url, not standard base64) + const base64 = btoa(String.fromCharCode(...credIdBytes)); + const credentialId = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + + // Calculate rpId hash (SHA-256 of rpId) + const rpId = request.publicKey.rp?.id || new URL(request.origin).hostname; + const rpIdBuffer = new TextEncoder().encode(rpId); + const rpIdHashBuffer = await crypto.subtle.digest('SHA-256', rpIdBuffer); + const rpIdHash = new Uint8Array(rpIdHashBuffer); + + // Flags: UP (User Present) = 1, UV (User Verified) = 1, AT (Attested Credential Data) = 1 + const flags = new Uint8Array([0x45]); // Binary: 01000101 + const signCount = new Uint8Array([0, 0, 0, 0]); + const aaguid = new Uint8Array(16); // All zeros for this implementation + + // Convert JWK coordinates from base64url to bytes for COSE format + const base64UrlToBytes = (base64url: string): Uint8Array => { + const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/'); + const binary = atob(base64); + return new Uint8Array(binary.split('').map(c => c.charCodeAt(0))); + }; + + const xCoord = base64UrlToBytes(publicKeyJwk.x!); + const yCoord = base64UrlToBytes(publicKeyJwk.y!); + + // COSE public key (ES256 format) - proper CBOR encoding + // Map with 5 entries: kty, alg, crv, x, y + const coseKey = new Uint8Array([ + 0xa5, // map(5) + 0x01, // key 1 (kty) + 0x02, // value: 2 (EC2) + 0x03, // key 3 (alg) + 0x26, // value: -7 (ES256) encoded as negative int + 0x20, // key -1 (crv) encoded as negative int + 0x01, // value: 1 (P-256) + 0x21, // key -2 (x) encoded as negative int + 0x58, 0x20, // byte string of length 32 + ...xCoord, // x coordinate (32 bytes) + 0x22, // key -3 (y) encoded as negative int + 0x58, 0x20, // byte string of length 32 + ...yCoord // y coordinate (32 bytes) + ]); + + // Construct authData + const credIdLength = new Uint8Array([0, credIdBytes.length]); + const authData = new Uint8Array([ + ...rpIdHash, + ...flags, + ...signCount, + ...aaguid, + ...credIdLength, + ...credIdBytes, + ...coseKey + ]); + + // CBOR encode attestation object: {fmt: "none", authData: bytes, attStmt: {}} + // Need to properly encode the authData length as a CBOR byte string + let authDataLengthBytes: number[]; + if (authData.length <= 23) { + authDataLengthBytes = [0x40 | authData.length]; + } else if (authData.length <= 255) { + authDataLengthBytes = [0x58, authData.length]; + } else { + authDataLengthBytes = [0x59, authData.length >> 8, authData.length & 0xff]; + } + + // CBOR map keys must be in canonical order (sorted by byte length, then lexicographically) + // Order: "fmt" (3 chars), "attStmt" (7 chars), "authData" (8 chars) + const attestationObject = new Uint8Array([ + 0xa3, // map(3) + 0x63, 0x66, 0x6d, 0x74, // "fmt" (text(3)) + 0x64, 0x6e, 0x6f, 0x6e, 0x65, // "none" (text(4)) + 0x67, 0x61, 0x74, 0x74, 0x53, 0x74, 0x6d, 0x74, // "attStmt" (text(7)) + 0xa0, // map(0) - empty map + 0x68, 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, // "authData" (text(8)) + ...authDataLengthBytes, ...authData // byte string with proper length encoding + ]); + const credential = { id: credentialId, rawId: credentialId, @@ -71,16 +170,17 @@ const PasskeyCreate: React.FC = () => { challenge: request.publicKey.challenge, origin: request.origin })), - attestationObject: btoa('mock_attestation_object') + attestationObject: btoa(String.fromCharCode(...attestationObject)) }; - // Store passkey + // Store passkey with the private key for future authentication await sendMessage('STORE_PASSKEY', { - rpId: request.origin, + rpId, credentialId, displayName, - publicKey: request.publicKey - }, 'background'); + publicKey: publicKeyJwk as JsonWebKey, + privateKey: privateKeyJwk as JsonWebKey + } as any, 'background'); // Send response back await sendMessage('PASSKEY_POPUP_RESPONSE', { diff --git a/apps/browser-extension/src/entrypoints/webauthn-inject.ts b/apps/browser-extension/src/entrypoints/webauthn-inject.ts index 2c92ac36c..b064c0f6b 100644 --- a/apps/browser-extension/src/entrypoints/webauthn-inject.ts +++ b/apps/browser-extension/src/entrypoints/webauthn-inject.ts @@ -30,12 +30,16 @@ export default defineUnlistedScript(() => { return btoa(binary); } - // Helper to convert base64 to ArrayBuffer + // Helper to convert base64/base64url to ArrayBuffer /** * */ function base64ToBuffer(base64: string): ArrayBuffer { - const binary = atob(base64); + // Handle both base64 and base64url formats + const base64Standard = base64.replace(/-/g, '+').replace(/_/g, '/'); + // Add padding if needed + const padded = base64Standard + '==='.slice((base64Standard.length + 3) % 4); + const binary = atob(padded); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); @@ -110,17 +114,34 @@ export default defineUnlistedScript(() => { } else if (e.detail.error) { reject(new Error(e.detail.error)); } else if (e.detail.credential) { - // Mock credential for POC + // Create a proper credential object with required methods const cred = e.detail.credential; - resolve({ + const credential = { id: cred.id, type: 'public-key', rawId: base64ToBuffer(cred.rawId), response: { clientDataJSON: base64ToBuffer(cred.clientDataJSON), - attestationObject: base64ToBuffer(cred.attestationObject) + attestationObject: base64ToBuffer(cred.attestationObject), + getTransports() { + return ['internal']; + }, + getAuthenticatorData() { + // Parse authData from attestation object if needed + return new ArrayBuffer(0); + }, + getPublicKey() { + return null; + }, + getPublicKeyAlgorithm() { + return -7; // ES256 + } + }, + getClientExtensionResults() { + return {}; } - } as any); + }; + resolve(credential as any); } else { // Cancelled resolve(null); @@ -195,9 +216,9 @@ export default defineUnlistedScript(() => { } else if (e.detail.error) { reject(new Error(e.detail.error)); } else if (e.detail.credential) { - // Mock credential for POC + // Create a proper credential object with required methods const cred = e.detail.credential; - resolve({ + const credential = { id: cred.id, type: 'public-key', rawId: base64ToBuffer(cred.rawId), @@ -206,8 +227,12 @@ export default defineUnlistedScript(() => { authenticatorData: base64ToBuffer(cred.authenticatorData), signature: base64ToBuffer(cred.signature), userHandle: cred.userHandle ? base64ToBuffer(cred.userHandle) : null + }, + getClientExtensionResults() { + return {}; } - } as any); + }; + resolve(credential as any); } else { // Cancelled resolve(null);