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 { import type { MaybePromiseLike } from "@yume-chan/async";
create(input: Uint8Array<ArrayBuffer>): Promise<{
output: BufferSource;
id: Uint8Array<ArrayBuffer>;
}>;
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( get(
id: BufferSource, id: BufferSource,
input: Uint8Array<ArrayBuffer>, input: Uint8Array<ArrayBuffer>,
): Promise<BufferSource>; ): MaybePromiseLike<BufferSource>;
} }

View file

@ -64,11 +64,20 @@ const Bundle = struct(
{ littleEndian: true }, { littleEndian: true },
); );
/**
* A `TangoDataStorage` that encrypts and decrypts data using PRF
*/
export class TangoPrfStorage implements TangoKeyStorage { export class TangoPrfStorage implements TangoKeyStorage {
readonly #storage: TangoKeyStorage; readonly #storage: TangoKeyStorage;
readonly #source: TangoPrfSource; readonly #source: TangoPrfSource;
#prevId: Uint8Array<ArrayBuffer> | undefined; #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) { constructor(storage: TangoKeyStorage, source: TangoPrfSource) {
this.#storage = storage; this.#storage = storage;
this.#source = source; this.#source = source;

View file

@ -25,16 +25,24 @@ class NotSupportedError extends Error {
} }
} }
class AssertionFailedError extends Error { class OperationCancelledError extends Error {
constructor() { constructor() {
super("Assertion failed"); super("The operation is either cancelled by user or timed out");
} }
} }
export class TangoWebAuthnPrfSource implements TangoPrfSource { export class TangoWebAuthnPrfSource implements TangoPrfSource {
static NotSupportedError = NotSupportedError; 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> { static async isSupported(): Promise<boolean> {
if (typeof PublicKeyCredential === "undefined") { if (typeof PublicKeyCredential === "undefined") {
return false; return false;
@ -57,7 +65,8 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
readonly #userName: string; 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 appName Name of your website shows in Passkey manager
* @param userName Display name of the credential 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; 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<{ async create(input: Uint8Array<ArrayBuffer>): Promise<{
output: BufferSource; output: BufferSource;
id: Uint8Array<ArrayBuffer>; id: Uint8Array<ArrayBuffer>;
@ -73,7 +90,9 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
const challenge = new Uint8Array(32); const challenge = new Uint8Array(32);
crypto.getRandomValues(challenge); crypto.getRandomValues(challenge);
const attestation = await navigator.credentials.create({ let attestation;
try {
attestation = await navigator.credentials.create({
publicKey: { publicKey: {
challenge, challenge,
extensions: { prf: { eval: { first: input } } }, extensions: { prf: { eval: { first: input } } },
@ -89,6 +108,9 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
}, },
}, },
}); });
} catch {
throw new OperationCancelledError();
}
checkCredential(attestation); checkCredential(attestation);
@ -108,6 +130,14 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
return { output, id }; 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( async get(
id: BufferSource, id: BufferSource,
input: Uint8Array<ArrayBuffer>, input: Uint8Array<ArrayBuffer>,
@ -125,7 +155,7 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
}, },
}); });
} catch { } catch {
throw new AssertionFailedError(); throw new OperationCancelledError();
} }
checkCredential(assertion); checkCredential(assertion);
@ -141,5 +171,5 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
export namespace TangoWebAuthnPrfSource { export namespace TangoWebAuthnPrfSource {
export type NotSupportedError = typeof NotSupportedError; export type NotSupportedError = typeof NotSupportedError;
export type AssertionFailedError = typeof AssertionFailedError; export type OperationCancelledError = typeof OperationCancelledError;
} }