From 6583471f406391ca13d9f6699a7b6c8375f467a7 Mon Sep 17 00:00:00 2001 From: Katalina Okano Date: Sat, 29 Jan 2022 02:22:47 -0500 Subject: [PATCH] nevermind x2, tweetnacl is bad but SubtleCrypto has what we need apparently --- packages/api/src/index.ts | 8 +++---- .../src/routes/guilds/guild-roles-put.spec.ts | 5 ++++ .../api/src/routes/guilds/guild-roles-put.ts | 9 +++++++ .../src/routes/interactions/helpers.spec.ts | 24 ++++++++++++------- .../api/src/routes/interactions/helpers.ts | 22 +++++++++++------ .../src/routes/interactions/interactions.ts | 2 +- 6 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 packages/api/src/routes/guilds/guild-roles-put.spec.ts create mode 100644 packages/api/src/routes/guilds/guild-roles-put.ts diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 3edebf9..25eaab8 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -5,8 +5,10 @@ import { authCallback } from '@roleypoly/api/src/routes/auth/callback'; import { authSessionDelete } from '@roleypoly/api/src/routes/auth/delete-session'; import { authSession } from '@roleypoly/api/src/routes/auth/session'; import { guildsGuild } from '@roleypoly/api/src/routes/guilds/guild'; +import { guildsRolesPut } from '@roleypoly/api/src/routes/guilds/guild-roles-put'; import { guildsGuildPatch } from '@roleypoly/api/src/routes/guilds/guilds-patch'; import { guildsSlug } from '@roleypoly/api/src/routes/guilds/slug'; +import { handleInteraction } from '@roleypoly/api/src/routes/interactions/interactions'; import { requireSession, withAuthMode, @@ -33,14 +35,12 @@ const guildsCommon = [injectParams, withSession, requireSession, requireMember]; router.get('/guilds/:guildId', ...guildsCommon, guildsGuild); router.patch('/guilds/:guildId', ...guildsCommon, requireEditor, guildsGuildPatch); router.delete('/guilds/:guildId/cache', ...guildsCommon, requireEditor, notImplemented); -router.put('/guilds/:guildId/roles', ...guildsCommon, notImplemented); +router.put('/guilds/:guildId/roles', ...guildsCommon, guildsRolesPut); // Slug is unauthenticated... router.get('/guilds/slug/:guildId', injectParams, guildsSlug); -// TODO: move this to another worker. -// It inflates the output by way too much. -// router.post('/interactions', handleInteraction); +router.post('/interactions', handleInteraction); router.get( '/legacy/preflight/:guildId', diff --git a/packages/api/src/routes/guilds/guild-roles-put.spec.ts b/packages/api/src/routes/guilds/guild-roles-put.spec.ts new file mode 100644 index 0000000..38deb85 --- /dev/null +++ b/packages/api/src/routes/guilds/guild-roles-put.spec.ts @@ -0,0 +1,5 @@ +describe('PUT /guilds/:id/roles', () => { + it('returns Not Implemented when called', () => { + expect(true).toBe(true); + }); +}); diff --git a/packages/api/src/routes/guilds/guild-roles-put.ts b/packages/api/src/routes/guilds/guild-roles-put.ts new file mode 100644 index 0000000..fbabee6 --- /dev/null +++ b/packages/api/src/routes/guilds/guild-roles-put.ts @@ -0,0 +1,9 @@ +import { Context, RoleypolyHandler } from '@roleypoly/api/src/utils/context'; +import { notImplemented } from '@roleypoly/api/src/utils/response'; + +export const guildsRolesPut: RoleypolyHandler = async ( + request: Request, + context: Context +) => { + return notImplemented(); +}; diff --git a/packages/api/src/routes/interactions/helpers.spec.ts b/packages/api/src/routes/interactions/helpers.spec.ts index f6c21b2..795332d 100644 --- a/packages/api/src/routes/interactions/helpers.spec.ts +++ b/packages/api/src/routes/interactions/helpers.spec.ts @@ -3,8 +3,16 @@ import nacl from 'tweetnacl'; import { configContext } from '../../utils/testHelpers'; import { verifyRequest } from './helpers'; +// +// Q: Why tweetnacl when WebCrypto is available? +// A: Discord uses tweetnacl on their end, thus is also +// used in far more examples of Discord Interactions than WebCrypto. +// We don't actually use it in Workers, as SubtleCrypto using NODE-ED25519 +// is better in every way, and still gives us the same effect. +// + describe('verifyRequest', () => { - it('validates a successful Discord interactions request', () => { + it('validates a successful Discord interactions request', async () => { const [config, context] = configContext(); const timestamp = String(Date.now()); @@ -32,10 +40,10 @@ describe('verifyRequest', () => { }, }); - expect(verifyRequest(context.config, request, body)).toBe(true); + expect(await verifyRequest(context.config, request, body)).toBe(true); }); - it('fails to validate a headerless Discord interactions request', () => { + it('fails to validate a headerless Discord interactions request', async () => { const [config, context] = configContext(); const body: InteractionRequest = { @@ -55,10 +63,10 @@ describe('verifyRequest', () => { headers: {}, }); - expect(verifyRequest(context.config, request, body)).toBe(false); + expect(await verifyRequest(context.config, request, body)).toBe(false); }); - it('fails to validate a bad signature from Discord', () => { + it('fails to validate a bad signature from Discord', async () => { const [config, context] = configContext(); const timestamp = String(Date.now()); @@ -87,10 +95,10 @@ describe('verifyRequest', () => { }, }); - expect(verifyRequest(context.config, request, body)).toBe(false); + expect(await verifyRequest(context.config, request, body)).toBe(false); }); - it('fails to validate when signature differs from data', () => { + it('fails to validate when signature differs from data', async () => { const [config, context] = configContext(); const timestamp = String(Date.now()); @@ -118,6 +126,6 @@ describe('verifyRequest', () => { }, }); - expect(verifyRequest(context.config, request, body)).toBe(false); + expect(await verifyRequest(context.config, request, body)).toBe(false); }); }); diff --git a/packages/api/src/routes/interactions/helpers.ts b/packages/api/src/routes/interactions/helpers.ts index db07616..99f2d73 100644 --- a/packages/api/src/routes/interactions/helpers.ts +++ b/packages/api/src/routes/interactions/helpers.ts @@ -1,12 +1,11 @@ import { Config } from '@roleypoly/api/src/utils/config'; import { InteractionRequest } from '@roleypoly/types'; -import nacl from 'tweetnacl'; -export const verifyRequest = ( +export const verifyRequest = async ( config: Config, request: Request, interaction: InteractionRequest -): boolean => { +): Promise => { const timestamp = request.headers.get('x-signature-timestamp'); const signature = request.headers.get('x-signature-ed25519'); @@ -14,12 +13,21 @@ export const verifyRequest = ( return false; } + const key = await crypto.subtle.importKey( + 'raw', + Buffer.from(config.publicKey, 'hex'), + { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519', public: true } as any, + false, + ['verify'] + ); + if ( - !nacl.sign.detached.verify( - Buffer.from(timestamp + JSON.stringify(interaction)), + !(await crypto.subtle.verify( + 'NODE-ED25519', + key, Buffer.from(signature, 'hex'), - Buffer.from(config.publicKey, 'hex') - ) + Buffer.from(timestamp + JSON.stringify(interaction)) + )) ) { return false; } diff --git a/packages/api/src/routes/interactions/interactions.ts b/packages/api/src/routes/interactions/interactions.ts index bf8bd97..f526400 100644 --- a/packages/api/src/routes/interactions/interactions.ts +++ b/packages/api/src/routes/interactions/interactions.ts @@ -12,7 +12,7 @@ export const handleInteraction: RoleypolyHandler = async ( return invalid(); } - if (!verifyRequest(context.config, request, interaction)) { + if (!(await verifyRequest(context.config, request, interaction))) { return new Response('invalid request signature', { status: 401 }); }