59 lines
4.3 KiB
TypeScript
59 lines
4.3 KiB
TypeScript
import { callWorker } from './workerClient';
|
|
import type { AccountStatus } from './account';
|
|
import type { PasskeyOptionsBody, PasskeyInfoBody } from './generated/api';
|
|
|
|
const apiBase = import.meta.env.VITE_WEBDROP_API_URL ?? '/api/v1';
|
|
const call = <T>(operation: string, payload: object = {}) => callWorker<T>(operation, { ...payload, apiBase });
|
|
export const passkeysAvailable = () => typeof PublicKeyCredential !== 'undefined' && window.isSecureContext;
|
|
const decode = (value: string): Uint8Array<ArrayBuffer> => Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(value.length / 4) * 4, '=')), c => c.charCodeAt(0));
|
|
interface PrfResult { enabled?: boolean; results?: { first?: ArrayBuffer } }
|
|
const prfResult = (credential: PublicKeyCredential) => (credential.getClientExtensionResults() as AuthenticationExtensionsClientOutputs & { prf?: PrfResult }).prf;
|
|
function credentialJson(credential: PublicKeyCredential): string {
|
|
// W3C PRF: omit client extension outputs when sending credentials to the server.
|
|
return JSON.stringify({ ...credential.toJSON(), clientExtensionResults: {} });
|
|
}
|
|
async function assertion(options: PasskeyOptionsBody, credentialId?: string): Promise<PublicKeyCredential> {
|
|
const json = JSON.parse(options.options) as PublicKeyCredentialRequestOptionsJSON;
|
|
const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(json);
|
|
if (credentialId) publicKey.allowCredentials = [{ type: 'public-key', id: decode(credentialId) }];
|
|
const evalByCredential = Object.fromEntries(options.credentials.filter(p => !credentialId || p.id === credentialId).map(p => [p.id, { first: decode(p.prf_salt) }]));
|
|
publicKey.extensions = { ...publicKey.extensions, prf: { evalByCredential } } as AuthenticationExtensionsClientInputs;
|
|
const credential = await navigator.credentials.get({ publicKey });
|
|
if (!(credential instanceof PublicKeyCredential)) throw new Error('Passkey验证未完成');
|
|
return credential;
|
|
}
|
|
export async function bindPasskey(password: string, name: string): Promise<void> {
|
|
if (!passkeysAvailable()) throw new Error('当前浏览器不支持Passkey,请使用HTTPS或localhost');
|
|
let prf: ArrayBuffer | undefined;
|
|
try {
|
|
const options = await call<PasskeyOptionsBody>('beginPasskeyRegistration', { password });
|
|
const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(JSON.parse(options.options));
|
|
publicKey.extensions = { ...publicKey.extensions, prf: { eval: { first: decode(options.prf_salt!) } } } as AuthenticationExtensionsClientInputs;
|
|
const credential = await navigator.credentials.create({ publicKey });
|
|
if (!(credential instanceof PublicKeyCredential)) throw new Error('Passkey绑定未完成');
|
|
const extension = prfResult(credential); prf = extension?.results?.first;
|
|
const registered = await call<PasskeyInfoBody>('finishPasskeyRegistration', { id: options.id, credentialId: credential.id, credential: credentialJson(credential), name, prf });
|
|
if (!registered.can_recover && extension?.enabled) {
|
|
const request = await call<PasskeyOptionsBody>('beginPasskeyRecovery');
|
|
const proof = await assertion(request, registered.id); prf = prfResult(proof)?.results?.first;
|
|
if (prf) await call<void>('finishPasskeyRecovery', { id: request.id, credentialId: proof.id, credential: credentialJson(proof), prf });
|
|
}
|
|
} finally {
|
|
if (prf) new Uint8Array(prf).fill(0);
|
|
await call('cancelPasskey').catch(() => {});
|
|
}
|
|
}
|
|
export async function loginWithPasskey(username: string, remember: boolean): Promise<AccountStatus> {
|
|
if (!passkeysAvailable()) throw new Error('当前浏览器不支持Passkey,请使用HTTPS或localhost');
|
|
const options = await call<PasskeyOptionsBody>('beginPasskeyLogin', { username });
|
|
const proof = await assertion(options); const prf = prfResult(proof)?.results?.first;
|
|
try { return await call<AccountStatus>('finishPasskeyLogin', { username, id: options.id, credential: credentialJson(proof), prf, remember }); }
|
|
finally { if (prf) new Uint8Array(prf).fill(0); }
|
|
}
|
|
export const passkeyApi = {
|
|
list: () => call<PasskeyInfoBody[]>('listPasskeys'),
|
|
remove: (id: string) => call<void>('removePasskey', { id }),
|
|
unlock: (password: string, remember: boolean) => call<AccountStatus>('unlockPasskeyLogin', { password, remember }),
|
|
cancel: () => call<AccountStatus>('cancelPasskey'),
|
|
};
|