mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-15 10:55:31 -04:00
Update passkey add and retrieve data flow (#520)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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<string, {
|
||||
reject: (error: any) => void;
|
||||
}>();
|
||||
|
||||
// Store request data temporarily (to avoid URL length limits)
|
||||
const pendingRequestData = new Map<string, any>();
|
||||
|
||||
/**
|
||||
* 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<IPasskeyData | null> {
|
||||
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<any> {
|
||||
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)
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user