123 lines
4.3 KiB
TypeScript
123 lines
4.3 KiB
TypeScript
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<AuthSecp256k1OptionalDeps>;
|
|
|
|
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<AuthSecp256k1> {
|
|
const deps = {
|
|
secp256k1: await instantiateSecp256k1(),
|
|
...inputDeps,
|
|
};
|
|
|
|
return new AuthSecp256k1(deps, options);
|
|
}
|
|
|
|
/**
|
|
* TODO: Consider adding a Record<string, Mutex> where each key is the signature to guarantee signatures arent being processed concurrently.
|
|
*/
|
|
readonly #deps: Required<AuthSecp256k1Deps>;
|
|
readonly #options: AuthSecp256k1Options;
|
|
|
|
/**
|
|
* @param deps - The dependencies to use
|
|
* @param options - The options to use
|
|
*/
|
|
private constructor(deps: Required<AuthSecp256k1Deps>, 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<boolean> {
|
|
// 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<boolean> {
|
|
// 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');
|
|
}
|
|
}
|
|
}
|