Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/flags/src/lib/serialization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest';
import { deserialize, serialize } from './serialization';

const invalidSecret = 'short';

describe('serialization secret validation', () => {
it('rejects signing with a secret that is not 32 bytes', async () => {
await expect(
serialize({ feature: true }, [{ key: 'feature' }], invalidSecret),
).rejects.toThrow('flags: Invalid secret');
});

it('rejects verification with a secret that is not 32 bytes', async () => {
await expect(
deserialize('invalid.code', [{ key: 'feature' }], invalidSecret),
).rejects.toThrow('flags: Invalid secret');
});
});
18 changes: 16 additions & 2 deletions packages/flags/src/lib/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { JsonValue } from '..';
import type { FlagOption } from '../types';
import { memoizeOne } from './async-memoize-one';

const SECRET_BYTE_LENGTH = 32;

// 252 max options length allows storing index 0 to 251,
// so 252 is the first SPECIAL_INTEGER
export const MAX_OPTION_LENGTH = 252;
Expand All @@ -15,9 +17,21 @@ enum SPECIAL_INTEGERS {
UNLISTED_VALUE = 255,
}

function getSecretKey(secret: string): Uint8Array {
const encodedSecret = base64url.decode(secret);

if (encodedSecret.length !== SECRET_BYTE_LENGTH) {
throw new Error(
'flags: Invalid secret, it must be a 256-bit key (32 bytes)',
);
}

return encodedSecret;
}

const memoizedVerify = memoizeOne(
(code: string, secret: string) =>
compactVerify(code, base64url.decode(secret), {
compactVerify(code, getSecretKey(secret), {
algorithms: ['HS256'],
}),
(a, b) => a[0] === b[0] && a[1] === b[1], // only first two args matter
Expand All @@ -28,7 +42,7 @@ const memoizedSign = memoizeOne(
(uint8Array: Uint8Array, secret) =>
new CompactSign(uint8Array)
.setProtectedHeader({ alg: 'HS256' })
.sign(base64url.decode(secret)),
.sign(getSecretKey(secret)),
(a, b) =>
// matchedIndices array must be equal
a[0].length === b[0].length &&
Expand Down