diff --git a/apps/agent-worker/src/terminal-token.ts b/apps/agent-worker/src/terminal-token.ts index 29a70fb..98aeace 100644 --- a/apps/agent-worker/src/terminal-token.ts +++ b/apps/agent-worker/src/terminal-token.ts @@ -27,3 +27,28 @@ export const verifyTerminalToken = ( timingSafeEqual(providedBuf, expectedBuf) ); }; + +// User-scoped variant authorizing a terminal connection to a user's box. +// Embeds a literal `box` segment so its four parts never collide with the +// three-part job token above. Format: +// `${expiresAtMs}.box.${username}.${hmacSha256Hex}` +export const verifyBoxTerminalToken = ( + token: string, + username: string, + secret: string, +): boolean => { + if (!token || !secret) return false; + const parts = token.split('.'); + if (parts.length !== 4 || parts[1] !== 'box') return false; + const [expRaw, , tokenUsername, provided] = parts; + if (tokenUsername !== username) return false; + const exp = Number.parseInt(expRaw ?? '', 10); + if (!Number.isFinite(exp) || Date.now() > exp) return false; + const expected = signature(`${expRaw}.box.${tokenUsername}`, secret); + const providedBuf = Buffer.from(provided ?? '', 'hex'); + const expectedBuf = Buffer.from(expected, 'hex'); + return ( + providedBuf.length === expectedBuf.length && + timingSafeEqual(providedBuf, expectedBuf) + ); +}; diff --git a/apps/agent-worker/tests/unit/box-terminal-token.test.ts b/apps/agent-worker/tests/unit/box-terminal-token.test.ts new file mode 100644 index 0000000..147aeaf --- /dev/null +++ b/apps/agent-worker/tests/unit/box-terminal-token.test.ts @@ -0,0 +1,49 @@ +import { createHmac } from 'node:crypto'; +import { describe, expect, test } from 'vitest'; + +import { verifyBoxTerminalToken } from '../../src/terminal-token'; + +const mintBox = (username: string, expiresAt: number, secret: string) => { + const payload = `${expiresAt}.box.${username}`; + const sig = createHmac('sha256', secret).update(payload).digest('hex'); + return `${payload}.${sig}`; +}; + +describe('verifyBoxTerminalToken', () => { + const secret = 'test-secret'; + + test('accepts a valid, unexpired, username-matched token', () => { + const token = mintBox('alice', Date.now() + 60_000, secret); + expect(verifyBoxTerminalToken(token, 'alice', secret)).toBe(true); + }); + + test('rejects an expired token', () => { + const token = mintBox('alice', Date.now() - 1, secret); + expect(verifyBoxTerminalToken(token, 'alice', secret)).toBe(false); + }); + + test('rejects a token minted for another username', () => { + const token = mintBox('alice', Date.now() + 60_000, secret); + expect(verifyBoxTerminalToken(token, 'bob', secret)).toBe(false); + }); + + test('rejects a token signed with a different secret', () => { + const token = mintBox('alice', Date.now() + 60_000, 'other-secret'); + expect(verifyBoxTerminalToken(token, 'alice', secret)).toBe(false); + }); + + test('rejects malformed input and an empty secret', () => { + expect(verifyBoxTerminalToken('garbage', 'alice', secret)).toBe(false); + expect(verifyBoxTerminalToken('', 'alice', secret)).toBe(false); + expect( + verifyBoxTerminalToken(mintBox('alice', Date.now() + 1000, ''), 'alice', ''), + ).toBe(false); + }); + + test('rejects a 3-part job token (cross-scheme confusion guard)', () => { + const payload = `${Date.now() + 60_000}.alice`; + const sig = createHmac('sha256', secret).update(payload).digest('hex'); + const jobToken = `${payload}.${sig}`; + expect(verifyBoxTerminalToken(jobToken, 'alice', secret)).toBe(false); + }); +});