From dafbde3a8eb46b6361feb52152a4eb2064bc20d7 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Fri, 12 Sep 2025 23:17:58 +0800 Subject: [PATCH] refactor(credential): improve error handling and documentation --- .../src/storage/prf/source.ts | 36 ++++++++-- .../src/storage/prf/storage.ts | 9 +++ .../src/storage/prf/web-authn.ts | 72 +++++++++++++------ 3 files changed, 90 insertions(+), 27 deletions(-) diff --git a/libraries/adb-credential-web/src/storage/prf/source.ts b/libraries/adb-credential-web/src/storage/prf/source.ts index dabcd81f..54c4a0b0 100644 --- a/libraries/adb-credential-web/src/storage/prf/source.ts +++ b/libraries/adb-credential-web/src/storage/prf/source.ts @@ -1,11 +1,35 @@ -export interface TangoPrfSource { - create(input: Uint8Array): Promise<{ - output: BufferSource; - id: Uint8Array; - }>; +import type { MaybePromiseLike } from "@yume-chan/async"; +interface TangoPrfCreationResult { + /** + * The generated PRF output + */ + output: BufferSource; + + /** + * ID of the created secret key + */ + id: Uint8Array; +} + +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, + ): MaybePromiseLike; + + /** + * 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, - ): Promise; + ): MaybePromiseLike; } diff --git a/libraries/adb-credential-web/src/storage/prf/storage.ts b/libraries/adb-credential-web/src/storage/prf/storage.ts index 58d84a58..1c4fadb2 100644 --- a/libraries/adb-credential-web/src/storage/prf/storage.ts +++ b/libraries/adb-credential-web/src/storage/prf/storage.ts @@ -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 | 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; diff --git a/libraries/adb-credential-web/src/storage/prf/web-authn.ts b/libraries/adb-credential-web/src/storage/prf/web-authn.ts index fdf60845..6901a08f 100644 --- a/libraries/adb-credential-web/src/storage/prf/web-authn.ts +++ b/libraries/adb-credential-web/src/storage/prf/web-authn.ts @@ -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 { 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): Promise<{ output: BufferSource; id: Uint8Array; @@ -73,22 +90,27 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource { const challenge = new Uint8Array(32); crypto.getRandomValues(challenge); - const attestation = await navigator.credentials.create({ - publicKey: { - challenge, - extensions: { prf: { eval: { first: input } } }, - pubKeyCredParams: [ - { type: "public-key", alg: -7 }, - { type: "public-key", alg: -257 }, - ], - rp: { name: this.#appName }, - user: { - id: challenge, - name: this.#userName, - displayName: this.#userName, + let attestation; + try { + attestation = await navigator.credentials.create({ + publicKey: { + challenge, + extensions: { prf: { eval: { first: input } } }, + pubKeyCredParams: [ + { type: "public-key", alg: -7 }, + { type: "public-key", alg: -257 }, + ], + rp: { name: this.#appName }, + user: { + id: challenge, + name: this.#userName, + displayName: this.#userName, + }, }, - }, - }); + }); + } 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, @@ -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; }