refactor(credential): improve error handling and documentation

This commit is contained in:
Simon Chan 2025-09-12 23:17:58 +08:00
parent b45b0ba979
commit dafbde3a8e
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
3 changed files with 90 additions and 27 deletions

View file

@ -1,11 +1,35 @@
export interface TangoPrfSource {
create(input: Uint8Array<ArrayBuffer>): Promise<{
output: BufferSource;
id: Uint8Array<ArrayBuffer>;
}>;
import type { MaybePromiseLike } from "@yume-chan/async";
interface TangoPrfCreationResult {
/**
* The generated PRF output
*/
output: BufferSource;
/**
* ID of the created secret key
*/
id: Uint8Array<ArrayBuffer>;
}
export interface TangoPrfSource {
/**
* Creates a new secret key and generate PRF output using the key and input data.
*
* @param input The input data
*/
create(
input: Uint8Array<ArrayBuffer>,
): MaybePromiseLike<TangoPrfCreationResult>;
/**
* Generates PRF output using the secret key and input data.
*
* @param id ID of the secret key
* @param input The input data
*/
get(
id: BufferSource,
input: Uint8Array<ArrayBuffer>,
): Promise<BufferSource>;
): MaybePromiseLike<BufferSource>;
}

View file

@ -64,11 +64,20 @@ const Bundle = struct(
{ littleEndian: true },
);
/**
* A `TangoDataStorage` that encrypts and decrypts data using PRF
*/
export class TangoPrfStorage implements TangoKeyStorage {
readonly #storage: TangoKeyStorage;
readonly #source: TangoPrfSource;
#prevId: Uint8Array<ArrayBuffer> | undefined;
/**
* Creates a new instance of `TangoPrfStorage`
*
* @param storage Another `TangoDataStorage` to store and retrieve the encrypted data
* @param source The `TangoPrfSource` to generate PRF output
*/
constructor(storage: TangoKeyStorage, source: TangoPrfSource) {
this.#storage = storage;
this.#source = source;

View file

@ -25,16 +25,24 @@ class NotSupportedError extends Error {
}
}
class AssertionFailedError extends Error {
class OperationCancelledError extends Error {
constructor() {
super("Assertion failed");
super("The operation is either cancelled by user or timed out");
}
}
export class TangoWebAuthnPrfSource implements TangoPrfSource {
static NotSupportedError = NotSupportedError;
static AssertionFailedError = AssertionFailedError;
static OperationCancelledError = OperationCancelledError;
/**
* Checks if the runtime supports WebAuthn PRF extension.
*
* Note that using the extension also requires a supported authenticator.
* Whether an authenticator supports the extension can only be checked
* during the `create` process.
* @returns `true` if the runtime supports WebAuthn PRF extension
*/
static async isSupported(): Promise<boolean> {
if (typeof PublicKeyCredential === "undefined") {
return false;
@ -57,7 +65,8 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
readonly #userName: string;
/**
* Create a new instance of TangoWebAuthnPrfSource
* Creates a new instance of `TangoWebAuthnPrfSource`
*
* @param appName Name of your website shows in Passkey manager
* @param userName Display name of the credential shows in Passkey manager
*/
@ -66,6 +75,14 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
this.#userName = userName;
}
/**
* Creates a new credential and generate PRF output using the credential and input data.
*
* @param input The input data
* @returns The credential ID and PRF output
* @throws `NotSupportedError` if the runtime or authenticator doesn't support PRF extension
* @throws `OperationCancelledError` if the attestation is either cancelled by user or timed out
*/
async create(input: Uint8Array<ArrayBuffer>): Promise<{
output: BufferSource;
id: Uint8Array<ArrayBuffer>;
@ -73,7 +90,9 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
const challenge = new Uint8Array(32);
crypto.getRandomValues(challenge);
const attestation = await navigator.credentials.create({
let attestation;
try {
attestation = await navigator.credentials.create({
publicKey: {
challenge,
extensions: { prf: { eval: { first: input } } },
@ -89,6 +108,9 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
},
},
});
} catch {
throw new OperationCancelledError();
}
checkCredential(attestation);
@ -108,6 +130,14 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
return { output, id };
}
/**
* Generates PRF output using a credential and input data.
*
* @param id ID of a previously created credential
* @param input The input data
* @returns PRF output
* @throws `OperationCancelledError` if the attestation is either cancelled by user or timed out
*/
async get(
id: BufferSource,
input: Uint8Array<ArrayBuffer>,
@ -125,7 +155,7 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
},
});
} catch {
throw new AssertionFailedError();
throw new OperationCancelledError();
}
checkCredential(assertion);
@ -141,5 +171,5 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
export namespace TangoWebAuthnPrfSource {
export type NotSupportedError = typeof NotSupportedError;
export type AssertionFailedError = typeof AssertionFailedError;
export type OperationCancelledError = typeof OperationCancelledError;
}