import type { Database } from '../services/storage/database.ts'; import { hexToBin, instantiateSecp256k1, type Secp256k1, sha256 } from '@bitauth/libauth'; import { UnauthorizedError } from '../errors/unauthorized-error.ts'; export type AuthSecp256k1RequiredDeps = { database: Database; }; export type AuthSecp256k1OptionalDeps = { secp256k1: Secp256k1; }; export type AuthSecp256k1Deps = AuthSecp256k1RequiredDeps & Partial; export type AuthSecp256k1Options = { timestampWindowMs: number; }; export class AuthSecp256k1 { /** * Create a new instance of AuthSecp256k1 * @returns A new instance of AuthSecp256k1 */ static async create(inputDeps: AuthSecp256k1Deps, options: AuthSecp256k1Options): Promise { const deps = { secp256k1: await instantiateSecp256k1(), ...inputDeps, }; return new AuthSecp256k1(deps, options); } /** * TODO: Consider adding a Record where each key is the signature to guarantee signatures arent being processed concurrently. */ readonly #deps: Required; readonly #options: AuthSecp256k1Options; /** * @param deps - The dependencies to use * @param options - The options to use */ private constructor(deps: Required, options: AuthSecp256k1Options) { this.#deps = deps; this.#options = options; } /** * Verify a signature * @param publicKeyHex - The public key to verify the signature against * @param signatureHex - The signature to verify * @param payload - The payload to verify the signature against * @returns Whether the signature is valid */ async verifySignature(publicKeyHex: string, signatureHex: string, payload: string): Promise { // Convert the public key and signature to binary const publicKey = hexToBin(publicKeyHex); const signature = hexToBin(signatureHex); // Convert the payload to bytes and compute the sha256 hash const payloadBytes = new TextEncoder().encode(payload); const messageHash = sha256.hash(payloadBytes); // Verify the signature const verified = this.#deps.secp256k1.verifySignatureDERLowS(signature, publicKey, messageHash); // If the signature is not valid, throw an unauthorized error if (!verified) { throw new UnauthorizedError('Invalid signature'); } // Return the verified signature return verified; } /** * Verify a signature is unique and add it to the database if it is * * @param signature - The signature to verify * @returns Whether the signature is unique */ async verifyUniqueRequest(signature: string): Promise { // If the signature is not provided, throw an unauthorized error if (!signature) { throw new UnauthorizedError('Signature is required'); } // Check if the signature has been used before const request = await this.#deps.database.db.selectFrom('authed_requests').selectAll() .where('signature', '=', signature) .executeTakeFirst(); // If the signature has been used before, throw an unauthorized error if (request) { throw new UnauthorizedError('Request already used'); } // Add the signature to the requests table await this.#deps.database.db.insertInto('authed_requests').values({ signature }) .execute(); // Return true if the signature is valid return true; } /** * Assert that a timestamp is within a allowed window * @param timestamp - The timestamp to check * @param windowMs - The window in milliseconds * * @throws An {@link UnauthorizedError} if the timestamp is outside the allowed window */ assertTimestampFreshness(timestamp: number, windowMs = this.#options.timestampWindowMs): void { // Subtract the timestamp from the current time to get the age in milliseconds const age = Math.abs(Date.now() - timestamp); // If the timestamp is outside the allowed window, throw an unauthorized error if (age > windowMs) { throw new UnauthorizedError('Timestamp outside allowed window'); } } }