mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-25 03:49:11 +00:00
nevermind x2, tweetnacl is bad but SubtleCrypto has what we need apparently
This commit is contained in:
parent
ea691bb56e
commit
6583471f40
6 changed files with 50 additions and 20 deletions
|
@ -5,8 +5,10 @@ import { authCallback } from '@roleypoly/api/src/routes/auth/callback';
|
||||||
import { authSessionDelete } from '@roleypoly/api/src/routes/auth/delete-session';
|
import { authSessionDelete } from '@roleypoly/api/src/routes/auth/delete-session';
|
||||||
import { authSession } from '@roleypoly/api/src/routes/auth/session';
|
import { authSession } from '@roleypoly/api/src/routes/auth/session';
|
||||||
import { guildsGuild } from '@roleypoly/api/src/routes/guilds/guild';
|
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 { guildsGuildPatch } from '@roleypoly/api/src/routes/guilds/guilds-patch';
|
||||||
import { guildsSlug } from '@roleypoly/api/src/routes/guilds/slug';
|
import { guildsSlug } from '@roleypoly/api/src/routes/guilds/slug';
|
||||||
|
import { handleInteraction } from '@roleypoly/api/src/routes/interactions/interactions';
|
||||||
import {
|
import {
|
||||||
requireSession,
|
requireSession,
|
||||||
withAuthMode,
|
withAuthMode,
|
||||||
|
@ -33,14 +35,12 @@ const guildsCommon = [injectParams, withSession, requireSession, requireMember];
|
||||||
router.get('/guilds/:guildId', ...guildsCommon, guildsGuild);
|
router.get('/guilds/:guildId', ...guildsCommon, guildsGuild);
|
||||||
router.patch('/guilds/:guildId', ...guildsCommon, requireEditor, guildsGuildPatch);
|
router.patch('/guilds/:guildId', ...guildsCommon, requireEditor, guildsGuildPatch);
|
||||||
router.delete('/guilds/:guildId/cache', ...guildsCommon, requireEditor, notImplemented);
|
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...
|
// Slug is unauthenticated...
|
||||||
router.get('/guilds/slug/:guildId', injectParams, guildsSlug);
|
router.get('/guilds/slug/:guildId', injectParams, guildsSlug);
|
||||||
|
|
||||||
// TODO: move this to another worker.
|
router.post('/interactions', handleInteraction);
|
||||||
// It inflates the output by way too much.
|
|
||||||
// router.post('/interactions', handleInteraction);
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/legacy/preflight/:guildId',
|
'/legacy/preflight/:guildId',
|
||||||
|
|
5
packages/api/src/routes/guilds/guild-roles-put.spec.ts
Normal file
5
packages/api/src/routes/guilds/guild-roles-put.spec.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
describe('PUT /guilds/:id/roles', () => {
|
||||||
|
it('returns Not Implemented when called', () => {
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
9
packages/api/src/routes/guilds/guild-roles-put.ts
Normal file
9
packages/api/src/routes/guilds/guild-roles-put.ts
Normal 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();
|
||||||
|
};
|
|
@ -3,8 +3,16 @@ import nacl from 'tweetnacl';
|
||||||
import { configContext } from '../../utils/testHelpers';
|
import { configContext } from '../../utils/testHelpers';
|
||||||
import { verifyRequest } from './helpers';
|
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', () => {
|
describe('verifyRequest', () => {
|
||||||
it('validates a successful Discord interactions request', () => {
|
it('validates a successful Discord interactions request', async () => {
|
||||||
const [config, context] = configContext();
|
const [config, context] = configContext();
|
||||||
|
|
||||||
const timestamp = String(Date.now());
|
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 [config, context] = configContext();
|
||||||
|
|
||||||
const body: InteractionRequest = {
|
const body: InteractionRequest = {
|
||||||
|
@ -55,10 +63,10 @@ describe('verifyRequest', () => {
|
||||||
headers: {},
|
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 [config, context] = configContext();
|
||||||
|
|
||||||
const timestamp = String(Date.now());
|
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 [config, context] = configContext();
|
||||||
|
|
||||||
const timestamp = String(Date.now());
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { Config } from '@roleypoly/api/src/utils/config';
|
import { Config } from '@roleypoly/api/src/utils/config';
|
||||||
import { InteractionRequest } from '@roleypoly/types';
|
import { InteractionRequest } from '@roleypoly/types';
|
||||||
import nacl from 'tweetnacl';
|
|
||||||
|
|
||||||
export const verifyRequest = (
|
export const verifyRequest = async (
|
||||||
config: Config,
|
config: Config,
|
||||||
request: Request,
|
request: Request,
|
||||||
interaction: InteractionRequest
|
interaction: InteractionRequest
|
||||||
): boolean => {
|
): Promise<boolean> => {
|
||||||
const timestamp = request.headers.get('x-signature-timestamp');
|
const timestamp = request.headers.get('x-signature-timestamp');
|
||||||
const signature = request.headers.get('x-signature-ed25519');
|
const signature = request.headers.get('x-signature-ed25519');
|
||||||
|
|
||||||
|
@ -14,12 +13,21 @@ export const verifyRequest = (
|
||||||
return false;
|
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 (
|
if (
|
||||||
!nacl.sign.detached.verify(
|
!(await crypto.subtle.verify(
|
||||||
Buffer.from(timestamp + JSON.stringify(interaction)),
|
'NODE-ED25519',
|
||||||
|
key,
|
||||||
Buffer.from(signature, 'hex'),
|
Buffer.from(signature, 'hex'),
|
||||||
Buffer.from(config.publicKey, 'hex')
|
Buffer.from(timestamp + JSON.stringify(interaction))
|
||||||
)
|
))
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const handleInteraction: RoleypolyHandler = async (
|
||||||
return invalid();
|
return invalid();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!verifyRequest(context.config, request, interaction)) {
|
if (!(await verifyRequest(context.config, request, interaction))) {
|
||||||
return new Response('invalid request signature', { status: 401 });
|
return new Response('invalid request signature', { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue