1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
import { startRegistration } from "@simplewebauthn/browser";
import { post } from "$lib/utils";
const PRF_SALT = new TextEncoder().encode("coinos-wallet-key");
function bufferToBase64url(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = "";
for (const b of bytes) binary += String.fromCharCode(b);
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function base64urlToBuffer(base64url: string): ArrayBuffer {
const padded =
base64url.replace(/-/g, "+").replace(/_/g, "/") +
"==".slice(0, (4 - (base64url.length % 4)) % 4);
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer as ArrayBuffer;
}
async function deriveKeyLegacy(credentialId: string): Promise<ArrayBuffer> {
const data = new TextEncoder().encode("coinos-wallet-key:" + credentialId);
return crypto.subtle.digest("SHA-256", data);
}
async function evaluatePrf(credentialId: string): Promise<ArrayBuffer | null> {
try {
const assertion = (await navigator.credentials.get({
publicKey: {
challenge: crypto.getRandomValues(new Uint8Array(32)),
allowCredentials: [
{ id: base64urlToBuffer(credentialId), type: "public-key" as const },
],
extensions: { prf: { eval: { first: PRF_SALT } } } as any,
userVerification: "required",
},
})) as PublicKeyCredential;
const results = (assertion.getClientExtensionResults() as any).prf?.results;
return results?.first || null;
} catch {
return null;
}
}
function authHeader(token: string) {
return { authorization: `Bearer ${token}` };
}
export async function registerPasskey(
token?: string,
): Promise<{ prfKey: ArrayBuffer; credentialId: string; prfSupported: boolean }> {
const headers = token ? authHeader(token) : undefined;
const origin = window.location.origin;
const options = await post("/passkey/register/options", { origin }, headers);
const registration = await startRegistration({ optionsJSON: options });
await post("/passkey/register/verify", { ...registration, origin }, headers);
// Try PRF evaluation with an immediate get() after registration
const prfResult = await evaluatePrf(registration.id);
if (prfResult) {
return { prfKey: prfResult, credentialId: registration.id, prfSupported: true };
}
// Fallback to legacy SHA-256(credentialId)
const legacyKey = await deriveKeyLegacy(registration.id);
return { prfKey: legacyKey, credentialId: registration.id, prfSupported: false };
}
export async function loginWithPasskey(): Promise<{
credential: any;
challengeId: string;
prfKey: ArrayBuffer;
legacyKey: ArrayBuffer;
}> {
const origin = window.location.origin;
const { challengeId, ...options } = await post("/passkey/login/options", { origin });
// Convert options from JSON to WebAuthn API format
const publicKeyOptions: PublicKeyCredentialRequestOptions = {
challenge: base64urlToBuffer(options.challenge),
rpId: options.rpId,
timeout: options.timeout,
userVerification: (options.userVerification as UserVerificationRequirement) || "required",
allowCredentials: options.allowCredentials?.map((c: any) => ({
id: base64urlToBuffer(c.id),
type: c.type as PublicKeyCredentialType,
transports: c.transports,
})),
extensions: {
prf: { eval: { first: PRF_SALT } },
} as any,
};
const assertion = (await navigator.credentials.get({
publicKey: publicKeyOptions,
})) as PublicKeyCredential;
const response = assertion.response as AuthenticatorAssertionResponse;
// Serialize response in the format the server expects (same as simplewebauthn)
const credential = {
id: assertion.id,
rawId: bufferToBase64url(assertion.rawId),
response: {
authenticatorData: bufferToBase64url(response.authenticatorData),
clientDataJSON: bufferToBase64url(response.clientDataJSON),
signature: bufferToBase64url(response.signature),
userHandle: response.userHandle ? bufferToBase64url(response.userHandle) : null,
},
type: assertion.type,
clientExtensionResults: {},
authenticatorAttachment: (assertion as any).authenticatorAttachment || null,
};
// Extract PRF result
const prfResults = (assertion.getClientExtensionResults() as any).prf?.results;
const prfOutput: ArrayBuffer | null = prfResults?.first || null;
// Compute legacy key for migration
const legacyKey = await deriveKeyLegacy(assertion.id);
// Use PRF output if available, otherwise fall back to legacy
const prfKey = prfOutput || legacyKey;
return { credential, challengeId, prfKey, legacyKey };
}
|