diff --git a/src/utils/auth.bench.ts b/src/utils/auth.bench.ts new file mode 100644 index 00000000..fbffc857 --- /dev/null +++ b/src/utils/auth.bench.ts @@ -0,0 +1,11 @@ +import { generateToken, getTokenHash } from '@/utils/auth.ts'; + +Deno.bench('generateToken', async () => { + await generateToken(); +}); + +Deno.bench('getTokenHash', async (b) => { + const { token } = await generateToken(); + b.start(); + await getTokenHash(token); +}); diff --git a/src/utils/auth.test.ts b/src/utils/auth.test.ts new file mode 100644 index 00000000..a0256b5d --- /dev/null +++ b/src/utils/auth.test.ts @@ -0,0 +1,18 @@ +import { assertEquals } from '@std/assert'; +import { decodeHex } from '@std/encoding/hex'; + +import { generateToken, getTokenHash } from '@/utils/auth.ts'; + +Deno.test('generateToken', async () => { + const sk = decodeHex('a0968751df8fd42f362213f08751911672f2a037113b392403bbb7dd31b71c95'); + + const { token, hash } = await generateToken(sk); + + assertEquals(token, 'token15ztgw5wl3l2z7d3zz0cgw5v3zee09gphzyanjfqrhwma6vdhrj2sauwknd'); + assertEquals(hash, decodeHex('ab4c4ead4d1c72a38fffd45b999937b7e3f25f867b19aaf252df858e77b66a8a')); +}); + +Deno.test('getTokenHash', async () => { + const hash = await getTokenHash('token15ztgw5wl3l2z7d3zz0cgw5v3zee09gphzyanjfqrhwma6vdhrj2sauwknd'); + assertEquals(hash, decodeHex('ab4c4ead4d1c72a38fffd45b999937b7e3f25f867b19aaf252df858e77b66a8a')); +}); diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 00000000..8d71ed6f --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,30 @@ +import { bech32 } from '@scure/base'; +import { generateSecretKey } from 'nostr-tools'; + +/** + * Generate an auth token for the API. + * + * Returns a bech32 encoded API token and the SHA-256 hash of the bytes. + * The token should be presented to the user, but only the hash should be stored in the database. + */ +export async function generateToken(sk = generateSecretKey()): Promise<{ token: `token1${string}`; hash: Uint8Array }> { + const words = bech32.toWords(sk); + const token = bech32.encode('token', words); + + const buffer = await crypto.subtle.digest('SHA-256', sk); + const hash = new Uint8Array(buffer); + + return { token, hash }; +} + +/** + * Get the SHA-256 hash of an API token. + * First decodes from bech32 then hashes the bytes. + * Used to identify the user in the database by the hash of their token. + */ +export async function getTokenHash(token: `token1${string}`): Promise { + const { bytes: sk } = bech32.decodeToBytes(token); + const buffer = await crypto.subtle.digest('SHA-256', sk); + + return new Uint8Array(buffer); +}