@xivdyetools/auth 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hmac.d.ts CHANGED
@@ -15,6 +15,12 @@ export interface BotSignatureOptions {
15
15
  /** Allowed clock skew in milliseconds (default: 1 minute) */
16
16
  clockSkewMs?: number;
17
17
  }
18
+ /**
19
+ * Get or create a cached CryptoKey for the given secret and usage.
20
+ * Exported for use by jwt.ts — not part of the public package API.
21
+ * @internal
22
+ */
23
+ export declare function getOrCreateHmacKey(secret: string, usage: 'sign' | 'verify' | 'both'): Promise<CryptoKey>;
18
24
  /**
19
25
  * Create an HMAC-SHA256 CryptoKey from a secret string.
20
26
  *
@@ -1 +1 @@
1
- {"version":3,"file":"hmac.d.ts","sourceRoot":"","sources":["../src/hmac.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,MAAe,GACzC,OAAO,CAAC,SAAS,CAAC,CAcpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAelB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,OAAO,CAAC,CAiClB"}
1
+ {"version":3,"file":"hmac.d.ts","sourceRoot":"","sources":["../src/hmac.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAmBD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAChC,OAAO,CAAC,SAAS,CAAC,CAmBpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,MAAe,GACzC,OAAO,CAAC,SAAS,CAAC,CAmBpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAelB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,OAAO,CAAC,CAiClB"}
package/dist/hmac.js CHANGED
@@ -7,6 +7,43 @@
7
7
  * @module hmac
8
8
  */
9
9
  import { base64UrlEncodeBytes, base64UrlDecodeBytes, bytesToHex, hexToBytes, } from '@xivdyetools/crypto';
10
+ // ============================================================================
11
+ // CryptoKey Cache (OPT-002)
12
+ // ============================================================================
13
+ /**
14
+ * Module-level cache for CryptoKeys.
15
+ *
16
+ * In Cloudflare Workers, module-level state persists across requests within
17
+ * an isolate, making this safe and effective. Eliminates redundant
18
+ * `crypto.subtle.importKey()` calls when the same secret is reused.
19
+ *
20
+ * Cache key format: `${secret}:${usage}` — bounded to 10 entries max
21
+ * to prevent unbounded growth during key rotation.
22
+ */
23
+ const cryptoKeyCache = new Map();
24
+ const CRYPTO_KEY_CACHE_MAX = 10;
25
+ /**
26
+ * Get or create a cached CryptoKey for the given secret and usage.
27
+ * Exported for use by jwt.ts — not part of the public package API.
28
+ * @internal
29
+ */
30
+ export async function getOrCreateHmacKey(secret, usage) {
31
+ const cacheKey = `${secret}:${usage}`;
32
+ const cached = cryptoKeyCache.get(cacheKey);
33
+ if (cached) {
34
+ return cached;
35
+ }
36
+ const key = await createHmacKey(secret, usage);
37
+ // Evict oldest entries if cache is full (simple FIFO)
38
+ if (cryptoKeyCache.size >= CRYPTO_KEY_CACHE_MAX) {
39
+ const firstKey = cryptoKeyCache.keys().next().value;
40
+ if (firstKey !== undefined) {
41
+ cryptoKeyCache.delete(firstKey);
42
+ }
43
+ }
44
+ cryptoKeyCache.set(cacheKey, key);
45
+ return key;
46
+ }
10
47
  /**
11
48
  * Create an HMAC-SHA256 CryptoKey from a secret string.
12
49
  *
@@ -22,6 +59,10 @@ import { base64UrlEncodeBytes, base64UrlDecodeBytes, bytesToHex, hexToBytes, } f
22
59
  export async function createHmacKey(secret, usage = 'both') {
23
60
  const encoder = new TextEncoder();
24
61
  const keyData = encoder.encode(secret);
62
+ // FINDING-009: Enforce minimum key length for HMAC-SHA256 security
63
+ if (keyData.length < 32) {
64
+ throw new Error('HMAC secret must be at least 32 bytes (256 bits)');
65
+ }
25
66
  const keyUsages = usage === 'both' ? ['sign', 'verify'] : [usage];
26
67
  return crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, keyUsages);
27
68
  }
@@ -38,7 +79,7 @@ export async function createHmacKey(secret, usage = 'both') {
38
79
  * ```
39
80
  */
40
81
  export async function hmacSign(data, secret) {
41
- const key = await createHmacKey(secret, 'sign');
82
+ const key = await getOrCreateHmacKey(secret, 'sign');
42
83
  const encoder = new TextEncoder();
43
84
  const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
44
85
  return base64UrlEncodeBytes(new Uint8Array(signature));
@@ -56,7 +97,7 @@ export async function hmacSign(data, secret) {
56
97
  * ```
57
98
  */
58
99
  export async function hmacSignHex(data, secret) {
59
- const key = await createHmacKey(secret, 'sign');
100
+ const key = await getOrCreateHmacKey(secret, 'sign');
60
101
  const encoder = new TextEncoder();
61
102
  const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
62
103
  return bytesToHex(new Uint8Array(signature));
@@ -71,7 +112,7 @@ export async function hmacSignHex(data, secret) {
71
112
  */
72
113
  export async function hmacVerify(data, signature, secret) {
73
114
  try {
74
- const key = await createHmacKey(secret, 'verify');
115
+ const key = await getOrCreateHmacKey(secret, 'verify');
75
116
  const encoder = new TextEncoder();
76
117
  const signatureBytes = base64UrlDecodeBytes(signature);
77
118
  // Use crypto.subtle.verify() which is inherently timing-safe
@@ -91,7 +132,7 @@ export async function hmacVerify(data, signature, secret) {
91
132
  */
92
133
  export async function hmacVerifyHex(data, signature, secret) {
93
134
  try {
94
- const key = await createHmacKey(secret, 'verify');
135
+ const key = await getOrCreateHmacKey(secret, 'verify');
95
136
  const encoder = new TextEncoder();
96
137
  const signatureBytes = hexToBytes(signature);
97
138
  return crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(data));
package/dist/hmac.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"hmac.js","sourceRoot":"","sources":["../src/hmac.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hmac.js","sourceRoot":"","sources":["../src/hmac.ts"],"names":[],"mappings":"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"}
package/dist/jwt.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,uBAAuB;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAY1D;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAkD5B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAqD5B;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOxD"}
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,uBAAuB;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAY1D;AAuDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAe5B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAkB5B;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOxD"}
package/dist/jwt.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * @module jwt
13
13
  */
14
14
  import { base64UrlDecode, base64UrlDecodeBytes, } from '@xivdyetools/crypto';
15
- import { createHmacKey } from './hmac.js';
15
+ import { getOrCreateHmacKey } from './hmac.js';
16
16
  /**
17
17
  * Decode a JWT without verifying the signature.
18
18
  *
@@ -41,6 +41,43 @@ export function decodeJWT(token) {
41
41
  return null;
42
42
  }
43
43
  }
44
+ /**
45
+ * Shared JWT signature verification helper (REFACTOR-003).
46
+ *
47
+ * Validates token structure, ensures HS256 algorithm, and verifies
48
+ * HMAC-SHA256 signature. Used by both `verifyJWT()` and `verifyJWTSignatureOnly()`.
49
+ *
50
+ * @param token - The JWT string
51
+ * @param secret - The HMAC secret
52
+ * @returns Decoded payload if signature is valid, null otherwise
53
+ */
54
+ async function verifyJWTSignature(token, secret) {
55
+ const parts = token.split('.');
56
+ if (parts.length !== 3) {
57
+ return null;
58
+ }
59
+ const [headerB64, payloadB64, signatureB64] = parts;
60
+ // Decode and validate header
61
+ const headerJson = base64UrlDecode(headerB64);
62
+ const header = JSON.parse(headerJson);
63
+ // SECURITY: Reject non-HS256 algorithms (prevents algorithm confusion attacks)
64
+ if (header.alg !== 'HS256') {
65
+ return null;
66
+ }
67
+ // SECURITY: Verify signature using crypto.subtle.verify() which is
68
+ // inherently timing-safe (comparison happens in native crypto, not JS)
69
+ const signatureInput = `${headerB64}.${payloadB64}`;
70
+ const key = await getOrCreateHmacKey(secret, 'verify');
71
+ const encoder = new TextEncoder();
72
+ const signatureBytes = base64UrlDecodeBytes(signatureB64);
73
+ const isValid = await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(signatureInput));
74
+ if (!isValid) {
75
+ return null;
76
+ }
77
+ // Decode payload
78
+ const payloadJson = base64UrlDecode(payloadB64);
79
+ return JSON.parse(payloadJson);
80
+ }
44
81
  /**
45
82
  * Verify a JWT and return the payload if valid.
46
83
  *
@@ -64,34 +101,12 @@ export function decodeJWT(token) {
64
101
  */
65
102
  export async function verifyJWT(token, secret) {
66
103
  try {
67
- const parts = token.split('.');
68
- if (parts.length !== 3) {
69
- return null;
70
- }
71
- const [headerB64, payloadB64, signatureB64] = parts;
72
- // Decode and validate header
73
- const headerJson = base64UrlDecode(headerB64);
74
- const header = JSON.parse(headerJson);
75
- // SECURITY: Reject non-HS256 algorithms (prevents algorithm confusion attacks)
76
- if (header.alg !== 'HS256') {
104
+ const payload = await verifyJWTSignature(token, secret);
105
+ if (!payload)
77
106
  return null;
78
- }
79
- // SECURITY: Verify signature using crypto.subtle.verify() which is
80
- // inherently timing-safe (comparison happens in native crypto, not JS)
81
- const signatureInput = `${headerB64}.${payloadB64}`;
82
- const key = await createHmacKey(secret, 'verify');
83
- const encoder = new TextEncoder();
84
- const signatureBytes = base64UrlDecodeBytes(signatureB64);
85
- const isValid = await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(signatureInput));
86
- if (!isValid) {
87
- return null;
88
- }
89
- // Decode payload
90
- const payloadJson = base64UrlDecode(payloadB64);
91
- const payload = JSON.parse(payloadJson);
92
- // Check expiration
107
+ // FINDING-003: Require exp claim — tokens without expiration are rejected
93
108
  const now = Math.floor(Date.now() / 1000);
94
- if (payload.exp && payload.exp < now) {
109
+ if (!payload.exp || payload.exp < now) {
95
110
  return null;
96
111
  }
97
112
  return payload;
@@ -123,31 +138,9 @@ export async function verifyJWT(token, secret) {
123
138
  */
124
139
  export async function verifyJWTSignatureOnly(token, secret, maxAgeMs) {
125
140
  try {
126
- const parts = token.split('.');
127
- if (parts.length !== 3) {
128
- return null;
129
- }
130
- const [headerB64, payloadB64, signatureB64] = parts;
131
- // Decode and validate header
132
- const headerJson = base64UrlDecode(headerB64);
133
- const header = JSON.parse(headerJson);
134
- // SECURITY: Still reject non-HS256 algorithms
135
- if (header.alg !== 'HS256') {
141
+ const payload = await verifyJWTSignature(token, secret);
142
+ if (!payload)
136
143
  return null;
137
- }
138
- // SECURITY: Verify signature using crypto.subtle.verify() which is
139
- // inherently timing-safe (comparison happens in native crypto, not JS)
140
- const signatureInput = `${headerB64}.${payloadB64}`;
141
- const key = await createHmacKey(secret, 'verify');
142
- const encoder = new TextEncoder();
143
- const signatureBytes = base64UrlDecodeBytes(signatureB64);
144
- const isValid = await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(signatureInput));
145
- if (!isValid) {
146
- return null;
147
- }
148
- // Decode payload
149
- const payloadJson = base64UrlDecode(payloadB64);
150
- const payload = JSON.parse(payloadJson);
151
144
  // Check max age if specified
152
145
  if (maxAgeMs !== undefined && payload.iat) {
153
146
  const now = Date.now();
package/dist/jwt.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xivdyetools/auth",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Shared authentication utilities for xivdyetools ecosystem",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "discord-interactions": "^4.4.0",
36
- "@xivdyetools/crypto": "1.0.0"
36
+ "@xivdyetools/crypto": "1.1.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@cloudflare/workers-types": "^4.20260207.0",
package/src/hmac.test.ts CHANGED
@@ -12,33 +12,49 @@ import {
12
12
  } from './hmac.js';
13
13
 
14
14
  describe('hmac.ts', () => {
15
+ // FINDING-009: All test secrets must be at least 32 bytes
16
+ const TEST_SECRET = 'test-secret-that-is-at-least-32-bytes!';
17
+ const TEST_SECRET_2 = 'another-secret-at-least-32-bytes-long!';
18
+
15
19
  describe('createHmacKey', () => {
16
20
  it('should create a CryptoKey for signing', async () => {
17
- const key = await createHmacKey('test-secret', 'sign');
21
+ const key = await createHmacKey(TEST_SECRET, 'sign');
18
22
  expect(key).toBeDefined();
19
23
  expect(key.algorithm.name).toBe('HMAC');
20
24
  });
21
25
 
22
26
  it('should create a CryptoKey for verification', async () => {
23
- const key = await createHmacKey('test-secret', 'verify');
27
+ const key = await createHmacKey(TEST_SECRET, 'verify');
24
28
  expect(key).toBeDefined();
25
29
  expect(key.algorithm.name).toBe('HMAC');
26
30
  });
27
31
 
28
32
  it('should create a CryptoKey for both operations', async () => {
29
- const key = await createHmacKey('test-secret', 'both');
33
+ const key = await createHmacKey(TEST_SECRET, 'both');
30
34
  expect(key).toBeDefined();
31
35
  });
32
36
 
33
37
  it('should default to both operations', async () => {
34
- const key = await createHmacKey('test-secret');
38
+ const key = await createHmacKey(TEST_SECRET);
35
39
  expect(key).toBeDefined();
36
40
  });
41
+
42
+ it('should reject secrets shorter than 32 bytes (FINDING-009)', async () => {
43
+ await expect(createHmacKey('short-secret', 'sign')).rejects.toThrow(
44
+ 'HMAC secret must be at least 32 bytes'
45
+ );
46
+ });
47
+
48
+ it('should reject empty string secret (FINDING-009)', async () => {
49
+ await expect(createHmacKey('', 'sign')).rejects.toThrow(
50
+ 'HMAC secret must be at least 32 bytes'
51
+ );
52
+ });
37
53
  });
38
54
 
39
55
  describe('hmacSign', () => {
40
56
  it('should return a base64url-encoded signature', async () => {
41
- const signature = await hmacSign('test-data', 'test-secret');
57
+ const signature = await hmacSign('test-data', TEST_SECRET);
42
58
  expect(signature).toBeDefined();
43
59
  expect(typeof signature).toBe('string');
44
60
  // Base64URL should not contain + or /
@@ -46,27 +62,27 @@ describe('hmac.ts', () => {
46
62
  });
47
63
 
48
64
  it('should produce consistent signatures for same input', async () => {
49
- const sig1 = await hmacSign('test-data', 'test-secret');
50
- const sig2 = await hmacSign('test-data', 'test-secret');
65
+ const sig1 = await hmacSign('test-data', TEST_SECRET);
66
+ const sig2 = await hmacSign('test-data', TEST_SECRET);
51
67
  expect(sig1).toBe(sig2);
52
68
  });
53
69
 
54
70
  it('should produce different signatures for different data', async () => {
55
- const sig1 = await hmacSign('data1', 'test-secret');
56
- const sig2 = await hmacSign('data2', 'test-secret');
71
+ const sig1 = await hmacSign('data1', TEST_SECRET);
72
+ const sig2 = await hmacSign('data2', TEST_SECRET);
57
73
  expect(sig1).not.toBe(sig2);
58
74
  });
59
75
 
60
76
  it('should produce different signatures for different secrets', async () => {
61
- const sig1 = await hmacSign('test-data', 'secret1');
62
- const sig2 = await hmacSign('test-data', 'secret2');
77
+ const sig1 = await hmacSign('test-data', TEST_SECRET);
78
+ const sig2 = await hmacSign('test-data', TEST_SECRET_2);
63
79
  expect(sig1).not.toBe(sig2);
64
80
  });
65
81
  });
66
82
 
67
83
  describe('hmacSignHex', () => {
68
84
  it('should return a hex-encoded signature', async () => {
69
- const signature = await hmacSignHex('test-data', 'test-secret');
85
+ const signature = await hmacSignHex('test-data', TEST_SECRET);
70
86
  expect(signature).toBeDefined();
71
87
  expect(typeof signature).toBe('string');
72
88
  // Should only contain hex characters
@@ -74,8 +90,8 @@ describe('hmac.ts', () => {
74
90
  });
75
91
 
76
92
  it('should produce consistent signatures', async () => {
77
- const sig1 = await hmacSignHex('test-data', 'test-secret');
78
- const sig2 = await hmacSignHex('test-data', 'test-secret');
93
+ const sig1 = await hmacSignHex('test-data', TEST_SECRET);
94
+ const sig2 = await hmacSignHex('test-data', TEST_SECRET);
79
95
  expect(sig1).toBe(sig2);
80
96
  });
81
97
  });
@@ -83,27 +99,26 @@ describe('hmac.ts', () => {
83
99
  describe('hmacVerify', () => {
84
100
  it('should return true for valid signature', async () => {
85
101
  const data = 'test-data';
86
- const secret = 'test-secret';
87
- const signature = await hmacSign(data, secret);
88
- const isValid = await hmacVerify(data, signature, secret);
102
+ const signature = await hmacSign(data, TEST_SECRET);
103
+ const isValid = await hmacVerify(data, signature, TEST_SECRET);
89
104
  expect(isValid).toBe(true);
90
105
  });
91
106
 
92
107
  it('should return false for invalid signature', async () => {
93
- const isValid = await hmacVerify('test-data', 'invalid-signature', 'test-secret');
108
+ const isValid = await hmacVerify('test-data', 'invalid-signature', TEST_SECRET);
94
109
  expect(isValid).toBe(false);
95
110
  });
96
111
 
97
112
  it('should return false for wrong secret', async () => {
98
113
  const data = 'test-data';
99
- const signature = await hmacSign(data, 'secret1');
100
- const isValid = await hmacVerify(data, signature, 'secret2');
114
+ const signature = await hmacSign(data, TEST_SECRET);
115
+ const isValid = await hmacVerify(data, signature, TEST_SECRET_2);
101
116
  expect(isValid).toBe(false);
102
117
  });
103
118
 
104
119
  it('should return false for tampered data', async () => {
105
- const signature = await hmacSign('original-data', 'test-secret');
106
- const isValid = await hmacVerify('tampered-data', signature, 'test-secret');
120
+ const signature = await hmacSign('original-data', TEST_SECRET);
121
+ const isValid = await hmacVerify('tampered-data', signature, TEST_SECRET);
107
122
  expect(isValid).toBe(false);
108
123
  });
109
124
  });
@@ -111,25 +126,24 @@ describe('hmac.ts', () => {
111
126
  describe('hmacVerifyHex', () => {
112
127
  it('should return true for valid hex signature', async () => {
113
128
  const data = 'test-data';
114
- const secret = 'test-secret';
115
- const signature = await hmacSignHex(data, secret);
116
- const isValid = await hmacVerifyHex(data, signature, secret);
129
+ const signature = await hmacSignHex(data, TEST_SECRET);
130
+ const isValid = await hmacVerifyHex(data, signature, TEST_SECRET);
117
131
  expect(isValid).toBe(true);
118
132
  });
119
133
 
120
134
  it('should return false for invalid hex signature', async () => {
121
- const isValid = await hmacVerifyHex('test-data', 'deadbeef', 'test-secret');
135
+ const isValid = await hmacVerifyHex('test-data', 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', TEST_SECRET);
122
136
  expect(isValid).toBe(false);
123
137
  });
124
138
 
125
139
  it('should return false for malformed hex', async () => {
126
- const isValid = await hmacVerifyHex('test-data', 'not-hex!', 'test-secret');
140
+ const isValid = await hmacVerifyHex('test-data', 'not-hex!', TEST_SECRET);
127
141
  expect(isValid).toBe(false);
128
142
  });
129
143
  });
130
144
 
131
145
  describe('verifyBotSignature', () => {
132
- const secret = 'bot-signing-secret';
146
+ const secret = 'bot-signing-secret-at-least-32-bytes!!';
133
147
 
134
148
  beforeEach(() => {
135
149
  vi.useFakeTimers();
package/src/hmac.ts CHANGED
@@ -24,6 +24,52 @@ export interface BotSignatureOptions {
24
24
  clockSkewMs?: number;
25
25
  }
26
26
 
27
+ // ============================================================================
28
+ // CryptoKey Cache (OPT-002)
29
+ // ============================================================================
30
+
31
+ /**
32
+ * Module-level cache for CryptoKeys.
33
+ *
34
+ * In Cloudflare Workers, module-level state persists across requests within
35
+ * an isolate, making this safe and effective. Eliminates redundant
36
+ * `crypto.subtle.importKey()` calls when the same secret is reused.
37
+ *
38
+ * Cache key format: `${secret}:${usage}` — bounded to 10 entries max
39
+ * to prevent unbounded growth during key rotation.
40
+ */
41
+ const cryptoKeyCache = new Map<string, CryptoKey>();
42
+ const CRYPTO_KEY_CACHE_MAX = 10;
43
+
44
+ /**
45
+ * Get or create a cached CryptoKey for the given secret and usage.
46
+ * Exported for use by jwt.ts — not part of the public package API.
47
+ * @internal
48
+ */
49
+ export async function getOrCreateHmacKey(
50
+ secret: string,
51
+ usage: 'sign' | 'verify' | 'both'
52
+ ): Promise<CryptoKey> {
53
+ const cacheKey = `${secret}:${usage}`;
54
+ const cached = cryptoKeyCache.get(cacheKey);
55
+ if (cached) {
56
+ return cached;
57
+ }
58
+
59
+ const key = await createHmacKey(secret, usage);
60
+
61
+ // Evict oldest entries if cache is full (simple FIFO)
62
+ if (cryptoKeyCache.size >= CRYPTO_KEY_CACHE_MAX) {
63
+ const firstKey = cryptoKeyCache.keys().next().value;
64
+ if (firstKey !== undefined) {
65
+ cryptoKeyCache.delete(firstKey);
66
+ }
67
+ }
68
+
69
+ cryptoKeyCache.set(cacheKey, key);
70
+ return key;
71
+ }
72
+
27
73
  /**
28
74
  * Create an HMAC-SHA256 CryptoKey from a secret string.
29
75
  *
@@ -43,6 +89,11 @@ export async function createHmacKey(
43
89
  const encoder = new TextEncoder();
44
90
  const keyData = encoder.encode(secret);
45
91
 
92
+ // FINDING-009: Enforce minimum key length for HMAC-SHA256 security
93
+ if (keyData.length < 32) {
94
+ throw new Error('HMAC secret must be at least 32 bytes (256 bits)');
95
+ }
96
+
46
97
  const keyUsages: ('sign' | 'verify')[] =
47
98
  usage === 'both' ? ['sign', 'verify'] : [usage];
48
99
 
@@ -68,7 +119,7 @@ export async function createHmacKey(
68
119
  * ```
69
120
  */
70
121
  export async function hmacSign(data: string, secret: string): Promise<string> {
71
- const key = await createHmacKey(secret, 'sign');
122
+ const key = await getOrCreateHmacKey(secret, 'sign');
72
123
  const encoder = new TextEncoder();
73
124
  const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
74
125
  return base64UrlEncodeBytes(new Uint8Array(signature));
@@ -90,7 +141,7 @@ export async function hmacSignHex(
90
141
  data: string,
91
142
  secret: string
92
143
  ): Promise<string> {
93
- const key = await createHmacKey(secret, 'sign');
144
+ const key = await getOrCreateHmacKey(secret, 'sign');
94
145
  const encoder = new TextEncoder();
95
146
  const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
96
147
  return bytesToHex(new Uint8Array(signature));
@@ -110,7 +161,7 @@ export async function hmacVerify(
110
161
  secret: string
111
162
  ): Promise<boolean> {
112
163
  try {
113
- const key = await createHmacKey(secret, 'verify');
164
+ const key = await getOrCreateHmacKey(secret, 'verify');
114
165
  const encoder = new TextEncoder();
115
166
  const signatureBytes = base64UrlDecodeBytes(signature);
116
167
 
@@ -140,7 +191,7 @@ export async function hmacVerifyHex(
140
191
  secret: string
141
192
  ): Promise<boolean> {
142
193
  try {
143
- const key = await createHmacKey(secret, 'verify');
194
+ const key = await getOrCreateHmacKey(secret, 'verify');
144
195
  const encoder = new TextEncoder();
145
196
  const signatureBytes = hexToBytes(signature);
146
197
 
package/src/jwt.test.ts CHANGED
@@ -36,7 +36,7 @@ async function createTestJWT(
36
36
  }
37
37
 
38
38
  describe('jwt.ts', () => {
39
- const secret = 'test-jwt-secret-key-123';
39
+ const secret = 'test-jwt-secret-key-that-is-at-least-32-bytes!';
40
40
 
41
41
  beforeEach(() => {
42
42
  vi.useFakeTimers();
@@ -125,7 +125,7 @@ describe('jwt.ts', () => {
125
125
  };
126
126
  const token = await createTestJWT(payload, secret);
127
127
 
128
- const verified = await verifyJWT(token, 'wrong-secret');
128
+ const verified = await verifyJWT(token, 'wrong-secret-that-is-at-least-32-bytes!!');
129
129
 
130
130
  expect(verified).toBeNull();
131
131
  });
@@ -170,8 +170,8 @@ describe('jwt.ts', () => {
170
170
  expect(verified).toBeNull();
171
171
  });
172
172
 
173
- it('should handle token without exp claim', async () => {
174
- // Create a token without exp - should still work if signature is valid
173
+ it('should reject token without exp claim (FINDING-003)', async () => {
174
+ // FINDING-003: Tokens without exp claim must be rejected
175
175
  const header = { alg: 'HS256', typ: 'JWT' };
176
176
  const payload = {
177
177
  sub: '123456789',
@@ -192,8 +192,8 @@ describe('jwt.ts', () => {
192
192
 
193
193
  const verified = await verifyJWT(token, secret);
194
194
 
195
- // Should pass since no exp means no expiration check
196
- expect(verified).not.toBeNull();
195
+ // FINDING-003: No exp means token is rejected
196
+ expect(verified).toBeNull();
197
197
  });
198
198
  });
199
199
 
@@ -222,7 +222,7 @@ describe('jwt.ts', () => {
222
222
  };
223
223
  const token = await createTestJWT(payload, secret);
224
224
 
225
- const verified = await verifyJWTSignatureOnly(token, 'wrong-secret');
225
+ const verified = await verifyJWTSignatureOnly(token, 'wrong-secret-that-is-at-least-32-bytes!!');
226
226
 
227
227
  expect(verified).toBeNull();
228
228
  });
package/src/jwt.ts CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  base64UrlDecode,
17
17
  base64UrlDecodeBytes,
18
18
  } from '@xivdyetools/crypto';
19
- import { createHmacKey } from './hmac.js';
19
+ import { getOrCreateHmacKey } from './hmac.js';
20
20
 
21
21
  /**
22
22
  * JWT payload structure
@@ -76,6 +76,59 @@ export function decodeJWT(token: string): JWTPayload | null {
76
76
  }
77
77
  }
78
78
 
79
+ /**
80
+ * Shared JWT signature verification helper (REFACTOR-003).
81
+ *
82
+ * Validates token structure, ensures HS256 algorithm, and verifies
83
+ * HMAC-SHA256 signature. Used by both `verifyJWT()` and `verifyJWTSignatureOnly()`.
84
+ *
85
+ * @param token - The JWT string
86
+ * @param secret - The HMAC secret
87
+ * @returns Decoded payload if signature is valid, null otherwise
88
+ */
89
+ async function verifyJWTSignature(
90
+ token: string,
91
+ secret: string
92
+ ): Promise<JWTPayload | null> {
93
+ const parts = token.split('.');
94
+ if (parts.length !== 3) {
95
+ return null;
96
+ }
97
+
98
+ const [headerB64, payloadB64, signatureB64] = parts;
99
+
100
+ // Decode and validate header
101
+ const headerJson = base64UrlDecode(headerB64);
102
+ const header: JWTHeader = JSON.parse(headerJson) as JWTHeader;
103
+
104
+ // SECURITY: Reject non-HS256 algorithms (prevents algorithm confusion attacks)
105
+ if (header.alg !== 'HS256') {
106
+ return null;
107
+ }
108
+
109
+ // SECURITY: Verify signature using crypto.subtle.verify() which is
110
+ // inherently timing-safe (comparison happens in native crypto, not JS)
111
+ const signatureInput = `${headerB64}.${payloadB64}`;
112
+ const key = await getOrCreateHmacKey(secret, 'verify');
113
+ const encoder = new TextEncoder();
114
+ const signatureBytes = base64UrlDecodeBytes(signatureB64);
115
+
116
+ const isValid = await crypto.subtle.verify(
117
+ 'HMAC',
118
+ key,
119
+ signatureBytes,
120
+ encoder.encode(signatureInput)
121
+ );
122
+
123
+ if (!isValid) {
124
+ return null;
125
+ }
126
+
127
+ // Decode payload
128
+ const payloadJson = base64UrlDecode(payloadB64);
129
+ return JSON.parse(payloadJson) as JWTPayload;
130
+ }
131
+
79
132
  /**
80
133
  * Verify a JWT and return the payload if valid.
81
134
  *
@@ -102,47 +155,12 @@ export async function verifyJWT(
102
155
  secret: string
103
156
  ): Promise<JWTPayload | null> {
104
157
  try {
105
- const parts = token.split('.');
106
- if (parts.length !== 3) {
107
- return null;
108
- }
158
+ const payload = await verifyJWTSignature(token, secret);
159
+ if (!payload) return null;
109
160
 
110
- const [headerB64, payloadB64, signatureB64] = parts;
111
-
112
- // Decode and validate header
113
- const headerJson = base64UrlDecode(headerB64);
114
- const header: JWTHeader = JSON.parse(headerJson) as JWTHeader;
115
-
116
- // SECURITY: Reject non-HS256 algorithms (prevents algorithm confusion attacks)
117
- if (header.alg !== 'HS256') {
118
- return null;
119
- }
120
-
121
- // SECURITY: Verify signature using crypto.subtle.verify() which is
122
- // inherently timing-safe (comparison happens in native crypto, not JS)
123
- const signatureInput = `${headerB64}.${payloadB64}`;
124
- const key = await createHmacKey(secret, 'verify');
125
- const encoder = new TextEncoder();
126
- const signatureBytes = base64UrlDecodeBytes(signatureB64);
127
-
128
- const isValid = await crypto.subtle.verify(
129
- 'HMAC',
130
- key,
131
- signatureBytes,
132
- encoder.encode(signatureInput)
133
- );
134
-
135
- if (!isValid) {
136
- return null;
137
- }
138
-
139
- // Decode payload
140
- const payloadJson = base64UrlDecode(payloadB64);
141
- const payload: JWTPayload = JSON.parse(payloadJson) as JWTPayload;
142
-
143
- // Check expiration
161
+ // FINDING-003: Require exp claim — tokens without expiration are rejected
144
162
  const now = Math.floor(Date.now() / 1000);
145
- if (payload.exp && payload.exp < now) {
163
+ if (!payload.exp || payload.exp < now) {
146
164
  return null;
147
165
  }
148
166
 
@@ -179,43 +197,8 @@ export async function verifyJWTSignatureOnly(
179
197
  maxAgeMs?: number
180
198
  ): Promise<JWTPayload | null> {
181
199
  try {
182
- const parts = token.split('.');
183
- if (parts.length !== 3) {
184
- return null;
185
- }
186
-
187
- const [headerB64, payloadB64, signatureB64] = parts;
188
-
189
- // Decode and validate header
190
- const headerJson = base64UrlDecode(headerB64);
191
- const header: JWTHeader = JSON.parse(headerJson) as JWTHeader;
192
-
193
- // SECURITY: Still reject non-HS256 algorithms
194
- if (header.alg !== 'HS256') {
195
- return null;
196
- }
197
-
198
- // SECURITY: Verify signature using crypto.subtle.verify() which is
199
- // inherently timing-safe (comparison happens in native crypto, not JS)
200
- const signatureInput = `${headerB64}.${payloadB64}`;
201
- const key = await createHmacKey(secret, 'verify');
202
- const encoder = new TextEncoder();
203
- const signatureBytes = base64UrlDecodeBytes(signatureB64);
204
-
205
- const isValid = await crypto.subtle.verify(
206
- 'HMAC',
207
- key,
208
- signatureBytes,
209
- encoder.encode(signatureInput)
210
- );
211
-
212
- if (!isValid) {
213
- return null;
214
- }
215
-
216
- // Decode payload
217
- const payloadJson = base64UrlDecode(payloadB64);
218
- const payload: JWTPayload = JSON.parse(payloadJson) as JWTPayload;
200
+ const payload = await verifyJWTSignature(token, secret);
201
+ if (!payload) return null;
219
202
 
220
203
  // Check max age if specified
221
204
  if (maxAgeMs !== undefined && payload.iat) {