From 8d1b1b8abcdc119fcee41a4f0c1ddda3be4f8865 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Oct 2024 19:36:44 -0500 Subject: [PATCH] Add encrypted captcha answer, move AES utils --- src/controllers/api/captcha.ts | 14 ++++++++++++++ src/controllers/api/oauth.ts | 5 +++-- src/db/migrations/037_auth_tokens.ts | 5 +++-- src/middleware/signerMiddleware.ts | 5 +++-- src/utils/aes.bench.ts | 18 ++++++++++++++++++ src/utils/aes.test.ts | 15 +++++++++++++++ src/utils/aes.ts | 17 +++++++++++++++++ src/utils/auth.bench.ts | 19 +------------------ src/utils/auth.test.ts | 13 +------------ src/utils/auth.ts | 24 ------------------------ 10 files changed, 75 insertions(+), 60 deletions(-) create mode 100644 src/utils/aes.bench.ts create mode 100644 src/utils/aes.test.ts create mode 100644 src/utils/aes.ts diff --git a/src/controllers/api/captcha.ts b/src/controllers/api/captcha.ts index 046dda9f..914f3aad 100644 --- a/src/controllers/api/captcha.ts +++ b/src/controllers/api/captcha.ts @@ -1,6 +1,9 @@ import { createCanvas, loadImage } from '@gfx/canvas-wasm'; +import { encodeBase64 } from '@std/encoding/base64'; import { AppController } from '@/app.ts'; +import { DittoWallet } from '@/DittoWallet.ts'; +import { aesEncrypt } from '@/utils/aes.ts'; export const captchaController: AppController = async (c) => { const { puzzle, piece, solution } = await generateCaptcha( @@ -14,9 +17,20 @@ export const captchaController: AppController = async (c) => { }, ); + const answerData = { + solution, + created_at: new Date().toISOString(), + }; + + const encoded = new TextEncoder().encode(JSON.stringify(answerData)); + const encrypted = await aesEncrypt(DittoWallet.captchaKey, encoded); + return c.json({ + type: 'puzzle', + token: crypto.randomUUID(), puzzle: puzzle.toDataURL(), piece: piece.toDataURL(), + answer_data: encodeBase64(encrypted), }); }; diff --git a/src/controllers/api/oauth.ts b/src/controllers/api/oauth.ts index c4c3eca4..61f8e1cd 100644 --- a/src/controllers/api/oauth.ts +++ b/src/controllers/api/oauth.ts @@ -8,7 +8,8 @@ import { Conf } from '@/config.ts'; import { Storages } from '@/storages.ts'; import { nostrNow } from '@/utils.ts'; import { parseBody } from '@/utils/api.ts'; -import { encryptSecretKey, generateToken } from '@/utils/auth.ts'; +import { aesEncrypt } from '@/utils/aes.ts'; +import { generateToken } from '@/utils/auth.ts'; const passwordGrantSchema = z.object({ grant_type: z.literal('password'), @@ -98,7 +99,7 @@ async function getToken( await kysely.insertInto('auth_tokens').values({ token_hash: hash, pubkey, - nip46_sk_enc: await encryptSecretKey(Conf.seckey, nip46Seckey), + nip46_sk_enc: await aesEncrypt(Conf.seckey, nip46Seckey), nip46_relays: relays, created_at: new Date(), }).execute(); diff --git a/src/db/migrations/037_auth_tokens.ts b/src/db/migrations/037_auth_tokens.ts index 71e971d3..2f6d1890 100644 --- a/src/db/migrations/037_auth_tokens.ts +++ b/src/db/migrations/037_auth_tokens.ts @@ -1,7 +1,8 @@ import { Kysely, sql } from 'kysely'; -import { encryptSecretKey, getTokenHash } from '@/utils/auth.ts'; import { Conf } from '@/config.ts'; +import { aesEncrypt } from '@/utils/aes.ts'; +import { getTokenHash } from '@/utils/auth.ts'; interface DB { nip46_tokens: { @@ -38,7 +39,7 @@ export async function up(db: Kysely): Promise { await db.insertInto('auth_tokens').values({ token_hash: await getTokenHash(token.api_token), pubkey: token.user_pubkey, - nip46_sk_enc: await encryptSecretKey(Conf.seckey, token.server_seckey), + nip46_sk_enc: await aesEncrypt(Conf.seckey, token.server_seckey), nip46_relays: JSON.parse(token.relays), created_at: token.connected_at, }).execute(); diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts index 8200ae1d..b4cab1ec 100644 --- a/src/middleware/signerMiddleware.ts +++ b/src/middleware/signerMiddleware.ts @@ -7,7 +7,8 @@ import { Conf } from '@/config.ts'; import { ConnectSigner } from '@/signers/ConnectSigner.ts'; import { ReadOnlySigner } from '@/signers/ReadOnlySigner.ts'; import { Storages } from '@/storages.ts'; -import { decryptSecretKey, getTokenHash } from '@/utils/auth.ts'; +import { aesDecrypt } from '@/utils/aes.ts'; +import { getTokenHash } from '@/utils/auth.ts'; /** We only accept "Bearer" type. */ const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); @@ -31,7 +32,7 @@ export const signerMiddleware: AppMiddleware = async (c, next) => { .where('token_hash', '=', tokenHash) .executeTakeFirstOrThrow(); - const nep46Seckey = await decryptSecretKey(Conf.seckey, nip46_sk_enc); + const nep46Seckey = await aesDecrypt(Conf.seckey, nip46_sk_enc); c.set('signer', new ConnectSigner(pubkey, new NSecSigner(nep46Seckey), nip46_relays)); } catch { diff --git a/src/utils/aes.bench.ts b/src/utils/aes.bench.ts new file mode 100644 index 00000000..9d8dd3b9 --- /dev/null +++ b/src/utils/aes.bench.ts @@ -0,0 +1,18 @@ +import { generateSecretKey } from 'nostr-tools'; + +import { aesDecrypt, aesEncrypt } from '@/utils/aes.ts'; + +Deno.bench('aesEncrypt', async (b) => { + const sk = generateSecretKey(); + const decrypted = generateSecretKey(); + b.start(); + await aesEncrypt(sk, decrypted); +}); + +Deno.bench('aesDecrypt', async (b) => { + const sk = generateSecretKey(); + const decrypted = generateSecretKey(); + const encrypted = await aesEncrypt(sk, decrypted); + b.start(); + await aesDecrypt(sk, encrypted); +}); diff --git a/src/utils/aes.test.ts b/src/utils/aes.test.ts new file mode 100644 index 00000000..583b96a2 --- /dev/null +++ b/src/utils/aes.test.ts @@ -0,0 +1,15 @@ +import { assertEquals } from '@std/assert'; +import { encodeHex } from '@std/encoding/hex'; +import { generateSecretKey } from 'nostr-tools'; + +import { aesDecrypt, aesEncrypt } from '@/utils/aes.ts'; + +Deno.test('aesDecrypt & aesEncrypt', async () => { + const sk = generateSecretKey(); + const data = generateSecretKey(); + + const encrypted = await aesEncrypt(sk, data); + const decrypted = await aesDecrypt(sk, encrypted); + + assertEquals(encodeHex(decrypted), encodeHex(data)); +}); diff --git a/src/utils/aes.ts b/src/utils/aes.ts new file mode 100644 index 00000000..983fc39c --- /dev/null +++ b/src/utils/aes.ts @@ -0,0 +1,17 @@ +/** Encrypt data with AES-GCM and a secret key. */ +export async function aesEncrypt(sk: Uint8Array, plaintext: Uint8Array): Promise { + const secretKey = await crypto.subtle.importKey('raw', sk, { name: 'AES-GCM' }, false, ['encrypt']); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const buffer = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, secretKey, plaintext); + + return new Uint8Array([...iv, ...new Uint8Array(buffer)]); +} + +/** Decrypt data with AES-GCM and a secret key. */ +export async function aesDecrypt(sk: Uint8Array, ciphertext: Uint8Array): Promise { + const secretKey = await crypto.subtle.importKey('raw', sk, { name: 'AES-GCM' }, false, ['decrypt']); + const iv = ciphertext.slice(0, 12); + const buffer = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, secretKey, ciphertext.slice(12)); + + return new Uint8Array(buffer); +} diff --git a/src/utils/auth.bench.ts b/src/utils/auth.bench.ts index 8c3da7cf..fbffc857 100644 --- a/src/utils/auth.bench.ts +++ b/src/utils/auth.bench.ts @@ -1,6 +1,4 @@ -import { generateSecretKey } from 'nostr-tools'; - -import { decryptSecretKey, encryptSecretKey, generateToken, getTokenHash } from '@/utils/auth.ts'; +import { generateToken, getTokenHash } from '@/utils/auth.ts'; Deno.bench('generateToken', async () => { await generateToken(); @@ -11,18 +9,3 @@ Deno.bench('getTokenHash', async (b) => { b.start(); await getTokenHash(token); }); - -Deno.bench('encryptSecretKey', async (b) => { - const sk = generateSecretKey(); - const decrypted = generateSecretKey(); - b.start(); - await encryptSecretKey(sk, decrypted); -}); - -Deno.bench('decryptSecretKey', async (b) => { - const sk = generateSecretKey(); - const decrypted = generateSecretKey(); - const encrypted = await encryptSecretKey(sk, decrypted); - b.start(); - await decryptSecretKey(sk, encrypted); -}); diff --git a/src/utils/auth.test.ts b/src/utils/auth.test.ts index e9e610c1..cf462d5d 100644 --- a/src/utils/auth.test.ts +++ b/src/utils/auth.test.ts @@ -1,8 +1,7 @@ import { assertEquals } from '@std/assert'; import { decodeHex, encodeHex } from '@std/encoding/hex'; -import { generateSecretKey } from 'nostr-tools'; -import { decryptSecretKey, encryptSecretKey, generateToken, getTokenHash } from '@/utils/auth.ts'; +import { generateToken, getTokenHash } from '@/utils/auth.ts'; Deno.test('generateToken', async () => { const sk = decodeHex('a0968751df8fd42f362213f08751911672f2a037113b392403bbb7dd31b71c95'); @@ -17,13 +16,3 @@ Deno.test('getTokenHash', async () => { const hash = await getTokenHash('token15ztgw5wl3l2z7d3zz0cgw5v3zee09gphzyanjfqrhwma6vdhrj2sauwknd'); assertEquals(encodeHex(hash), 'ab4c4ead4d1c72a38fffd45b999937b7e3f25f867b19aaf252df858e77b66a8a'); }); - -Deno.test('encryptSecretKey & decryptSecretKey', async () => { - const sk = generateSecretKey(); - const data = generateSecretKey(); - - const encrypted = await encryptSecretKey(sk, data); - const decrypted = await decryptSecretKey(sk, encrypted); - - assertEquals(encodeHex(decrypted), encodeHex(data)); -}); diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 05e838a9..8d71ed6f 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -28,27 +28,3 @@ export async function getTokenHash(token: `token1${string}`): Promise { - const secretKey = await crypto.subtle.importKey('raw', sk, { name: 'AES-GCM' }, false, ['encrypt']); - const iv = crypto.getRandomValues(new Uint8Array(12)); - const buffer = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, secretKey, decrypted); - - return new Uint8Array([...iv, ...new Uint8Array(buffer)]); -} - -/** - * Decrypt a secret key with AES-GCM. - * This function is used to retrieve the secret key from the database. - */ -export async function decryptSecretKey(sk: Uint8Array, encrypted: Uint8Array): Promise { - const secretKey = await crypto.subtle.importKey('raw', sk, { name: 'AES-GCM' }, false, ['decrypt']); - const iv = encrypted.slice(0, 12); - const buffer = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, secretKey, encrypted.slice(12)); - - return new Uint8Array(buffer); -}