import type { Database } from '../services/storage/database'; import { hexToBin, instantiateSecp256k1, type Secp256k1, sha256 } from '@bitauth/libauth'; import { UnauthorizedError } from '../errors/unauthorized-error'; 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. */ /** * @param deps - The dependencies to use * @param options - The options to use */ private constructor(private readonly deps: Required, private readonly options: AuthSecp256k1Options) {} /** * 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 { return true; // 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 = await 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; } async verifyUniqueRequest(signature: string): Promise { if (!signature) { throw new UnauthorizedError('Signature is required'); } console.log('Verifying unique request', signature); // Check if the signature has been used before const request = await this.deps.database.db. selectFrom('authed_requests') .selectAll() .where('signature', '=', signature) .executeTakeFirst(); 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 { const age = Math.abs(Date.now() - timestamp); if (age > windowMs) { throw new UnauthorizedError('Timestamp outside allowed window'); } } }