nevermind x2, tweetnacl is bad but SubtleCrypto has what we need apparently

This commit is contained in:
41666 2022-01-29 02:22:47 -05:00
parent ea691bb56e
commit 6583471f40
6 changed files with 50 additions and 20 deletions

View file

@ -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',

View file

@ -0,0 +1,5 @@
describe('PUT /guilds/:id/roles', () => {
it('returns Not Implemented when called', () => {
expect(true).toBe(true);
});
});

View file

@ -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();
};

View file

@ -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);
});
});

View file

@ -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<boolean> => {
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;
}

View file

@ -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 });
}