| 1 | import {
|
| 2 | createCipheriv,
|
| 3 | createDecipheriv,
|
| 4 | createHash,
|
| 5 | createHmac,
|
| 6 | randomBytes,
|
| 7 | timingSafeEqual,
|
| 8 | } from 'node:crypto';
|
| 9 |
|
| 10 | export type GatewayGrant = {
|
| 11 | interactionId: string;
|
| 12 | senderAgentId: string;
|
| 13 | recipientAgentId: string;
|
| 14 | expiresAt: number;
|
| 15 | };
|
| 16 |
|
| 17 | const keyFromSecret = (secret: string) => createHash('sha256').update(secret).digest();
|
| 18 |
|
| 19 | export function encryptGatewayPayload(secret: string, payload: unknown) {
|
| 20 | const iv = randomBytes(12);
|
| 21 | const cipher = createCipheriv('aes-256-gcm', keyFromSecret(secret), iv);
|
| 22 | const ciphertext = Buffer.concat([
|
| 23 | cipher.update(JSON.stringify(payload), 'utf8'),
|
| 24 | cipher.final(),
|
| 25 | ]);
|
| 26 | return {
|
| 27 | ciphertext: ciphertext.toString('base64url'),
|
| 28 | iv: iv.toString('base64url'),
|
| 29 | authTag: cipher.getAuthTag().toString('base64url'),
|
| 30 | };
|
| 31 | }
|
| 32 |
|
| 33 | export function decryptGatewayPayload(
|
| 34 | secret: string,
|
| 35 | encrypted: { ciphertext: string; iv: string; authTag: string },
|
| 36 | ): unknown {
|
| 37 | const decipher = createDecipheriv(
|
| 38 | 'aes-256-gcm',
|
| 39 | keyFromSecret(secret),
|
| 40 | Buffer.from(encrypted.iv, 'base64url'),
|
| 41 | );
|
| 42 | decipher.setAuthTag(Buffer.from(encrypted.authTag, 'base64url'));
|
| 43 | return JSON.parse(
|
| 44 | Buffer.concat([
|
| 45 | decipher.update(Buffer.from(encrypted.ciphertext, 'base64url')),
|
| 46 | decipher.final(),
|
| 47 | ]).toString('utf8'),
|
| 48 | );
|
| 49 | }
|
| 50 |
|
| 51 | export function issueGatewayGrant(secret: string, grant: GatewayGrant): string {
|
| 52 | const payload = Buffer.from(JSON.stringify(grant)).toString('base64url');
|
| 53 | const signature = createHmac('sha256', secret).update(payload).digest('base64url');
|
| 54 | return `${payload}.${signature}`;
|
| 55 | }
|
| 56 |
|
| 57 | export function verifyGatewayGrant(secret: string, token: string): GatewayGrant {
|
| 58 | const [payload, provided] = token.split('.');
|
| 59 | if (!payload || !provided) throw new Error('Invalid gateway token');
|
| 60 | const expected = createHmac('sha256', secret).update(payload).digest();
|
| 61 | const actual = Buffer.from(provided, 'base64url');
|
| 62 | if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
|
| 63 | throw new Error('Invalid gateway token');
|
| 64 | const grant = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as GatewayGrant;
|
| 65 | if (grant.expiresAt <= Date.now()) throw new Error('Gateway token expired');
|
| 66 | if (!grant.interactionId || !grant.senderAgentId || !grant.recipientAgentId)
|
| 67 | throw new Error('Invalid gateway token');
|
| 68 | return grant;
|
| 69 | }
|