@xivdyetools/auth 1.0.2 → 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/README.md +171 -158
- package/dist/hmac.d.ts +6 -0
- package/dist/hmac.d.ts.map +1 -1
- package/dist/hmac.js +49 -7
- package/dist/hmac.js.map +1 -1
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +44 -51
- package/dist/jwt.js.map +1 -1
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +4 -2
- package/dist/timing.js.map +1 -1
- package/package.json +75 -71
- package/src/discord.test.ts +243 -243
- package/src/discord.ts +143 -143
- package/src/hmac.test.ts +339 -325
- package/src/hmac.ts +274 -222
- package/src/index.ts +54 -54
- package/src/jwt.test.ts +337 -337
- package/src/jwt.ts +248 -265
- package/src/timing.test.ts +114 -117
- package/src/timing.ts +86 -84
package/README.md
CHANGED
|
@@ -1,158 +1,171 @@
|
|
|
1
|
-
# @xivdyetools/auth
|
|
2
|
-
|
|
3
|
-
Shared authentication utilities for the xivdyetools ecosystem. Provides secure JWT verification, HMAC signing, timing-safe comparison, and Discord signature verification.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @xivdyetools/auth
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Features
|
|
12
|
-
|
|
13
|
-
- **JWT Verification** - HMAC-SHA256 JWT verification with algorithm validation
|
|
14
|
-
- **HMAC Signing** - Create and verify HMAC-SHA256 signatures
|
|
15
|
-
- **Timing-Safe Comparison** - Constant-time string comparison to prevent timing attacks
|
|
16
|
-
- **Discord Verification** - Ed25519 signature verification for Discord interactions
|
|
17
|
-
- **Tree-Shakeable** - Subpath exports for minimal bundle size
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
### JWT Verification
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
import { verifyJWT, decodeJWT, isJWTExpired } from '@xivdyetools/auth';
|
|
25
|
-
|
|
26
|
-
// Verify JWT with signature and expiration checking
|
|
27
|
-
const payload = await verifyJWT(token, process.env.JWT_SECRET);
|
|
28
|
-
if (!payload) {
|
|
29
|
-
// Invalid signature, expired, or wrong algorithm
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Decode without verification (debugging only)
|
|
33
|
-
const decoded = decodeJWT(token);
|
|
34
|
-
|
|
35
|
-
// Check if JWT is expired
|
|
36
|
-
if (isJWTExpired(payload)) {
|
|
37
|
-
// Token has expired
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### HMAC Signing
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
import { hmacSign, hmacVerify, verifyBotSignature } from '@xivdyetools/auth';
|
|
45
|
-
|
|
46
|
-
// Sign data with HMAC-SHA256 (base64url output)
|
|
47
|
-
const signature = await hmacSign(data, secret);
|
|
48
|
-
|
|
49
|
-
// Verify signature
|
|
50
|
-
const isValid = await hmacVerify(data, signature, secret);
|
|
51
|
-
|
|
52
|
-
// Verify bot request signature (with timestamp validation)
|
|
53
|
-
const isValidBot = await verifyBotSignature(
|
|
54
|
-
signature, // X-Request-Signature header
|
|
55
|
-
timestamp, // X-Request-Timestamp header
|
|
56
|
-
userDiscordId,
|
|
57
|
-
userName,
|
|
58
|
-
secret,
|
|
59
|
-
{ maxAgeMs: 5 * 60 * 1000 } // Optional: 5 minute max age
|
|
60
|
-
);
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### Timing-Safe Comparison
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
import { timingSafeEqual } from '@xivdyetools/auth';
|
|
67
|
-
|
|
68
|
-
// Constant-time string comparison (prevents timing attacks)
|
|
69
|
-
const isEqual = await timingSafeEqual(userInput, expectedValue);
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Discord Signature Verification
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
import { verifyDiscordRequest } from '@xivdyetools/auth';
|
|
76
|
-
|
|
77
|
-
// Verify Discord interaction signature
|
|
78
|
-
const result = await verifyDiscordRequest(request, env.DISCORD_PUBLIC_KEY);
|
|
79
|
-
|
|
80
|
-
if (!result.valid) {
|
|
81
|
-
return new Response('Unauthorized', { status: 401 });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// result.body contains the parsed interaction
|
|
85
|
-
const interaction = result.body;
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Subpath Exports
|
|
89
|
-
|
|
90
|
-
Import only what you need for optimal tree-shaking:
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
// JWT utilities only
|
|
94
|
-
import { verifyJWT, decodeJWT } from '@xivdyetools/auth/jwt';
|
|
95
|
-
|
|
96
|
-
// HMAC utilities only
|
|
97
|
-
import { hmacSign, hmacVerify } from '@xivdyetools/auth/hmac';
|
|
98
|
-
|
|
99
|
-
// Timing utilities only
|
|
100
|
-
import { timingSafeEqual } from '@xivdyetools/auth/timing';
|
|
101
|
-
|
|
102
|
-
// Discord utilities only
|
|
103
|
-
import { verifyDiscordRequest } from '@xivdyetools/auth/discord';
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## API Reference
|
|
107
|
-
|
|
108
|
-
### JWT (`@xivdyetools/auth/jwt`)
|
|
109
|
-
|
|
110
|
-
| Function | Description |
|
|
111
|
-
|----------|-------------|
|
|
112
|
-
| `verifyJWT(token, secret)` | Verify JWT signature, algorithm (HS256 only), and expiration |
|
|
113
|
-
| `verifyJWTSignatureOnly(token, secret, maxAgeMs?)` | Verify signature only (for refresh token grace periods) |
|
|
114
|
-
| `decodeJWT(token)` | Decode JWT without verification (debugging only) |
|
|
115
|
-
| `isJWTExpired(payload)` | Check if JWT payload is expired |
|
|
116
|
-
| `getJWTTimeToExpiry(payload)` | Get milliseconds until JWT expires |
|
|
117
|
-
|
|
118
|
-
### HMAC (`@xivdyetools/auth/hmac`)
|
|
119
|
-
|
|
120
|
-
| Function | Description |
|
|
121
|
-
|----------|-------------|
|
|
122
|
-
| `createHmacKey(secret, usage)` | Create CryptoKey for HMAC operations |
|
|
123
|
-
| `hmacSign(data, secret)` | Sign data, return base64url signature |
|
|
124
|
-
| `hmacSignHex(data, secret)` | Sign data, return hex signature |
|
|
125
|
-
| `hmacVerify(data, signature, secret)` | Verify base64url signature |
|
|
126
|
-
| `hmacVerifyHex(data, signature, secret)` | Verify hex signature |
|
|
127
|
-
| `verifyBotSignature(sig, ts, userId, userName, secret, opts?)` | Verify bot request signature |
|
|
128
|
-
|
|
129
|
-
### Timing (`@xivdyetools/auth/timing`)
|
|
130
|
-
|
|
131
|
-
| Function | Description |
|
|
132
|
-
|----------|-------------|
|
|
133
|
-
| `timingSafeEqual(a, b)` | Constant-time string comparison |
|
|
134
|
-
| `timingSafeEqualBytes(a, b)` | Constant-time Uint8Array comparison |
|
|
135
|
-
|
|
136
|
-
### Discord (`@xivdyetools/auth/discord`)
|
|
137
|
-
|
|
138
|
-
| Function | Description |
|
|
139
|
-
|----------|-------------|
|
|
140
|
-
| `verifyDiscordRequest(request, publicKey, opts?)` | Verify Discord Ed25519 signature |
|
|
141
|
-
| `unauthorizedResponse()` | Return 401 response |
|
|
142
|
-
| `badRequestResponse(message?)` | Return 400 response |
|
|
143
|
-
|
|
144
|
-
## Security Features
|
|
145
|
-
|
|
146
|
-
- **Algorithm Validation**: JWT verification only accepts HS256, preventing algorithm confusion attacks
|
|
147
|
-
- **Timing-Safe Comparison**: Uses `crypto.subtle.timingSafeEqual()` with XOR fallback
|
|
148
|
-
- **Timestamp Validation**: Bot signatures include clock skew tolerance and max age checks
|
|
149
|
-
- **Body Size Limits**: Discord verification enforces 100KB max body size by default
|
|
150
|
-
|
|
151
|
-
## Dependencies
|
|
152
|
-
|
|
153
|
-
- `@xivdyetools/crypto` - Base64URL and hex encoding utilities
|
|
154
|
-
- `discord-interactions` - Discord Ed25519 signature verification
|
|
155
|
-
|
|
156
|
-
##
|
|
157
|
-
|
|
158
|
-
|
|
1
|
+
# @xivdyetools/auth
|
|
2
|
+
|
|
3
|
+
Shared authentication utilities for the xivdyetools ecosystem. Provides secure JWT verification, HMAC signing, timing-safe comparison, and Discord signature verification.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @xivdyetools/auth
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **JWT Verification** - HMAC-SHA256 JWT verification with algorithm validation
|
|
14
|
+
- **HMAC Signing** - Create and verify HMAC-SHA256 signatures
|
|
15
|
+
- **Timing-Safe Comparison** - Constant-time string comparison to prevent timing attacks
|
|
16
|
+
- **Discord Verification** - Ed25519 signature verification for Discord interactions
|
|
17
|
+
- **Tree-Shakeable** - Subpath exports for minimal bundle size
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### JWT Verification
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { verifyJWT, decodeJWT, isJWTExpired } from '@xivdyetools/auth';
|
|
25
|
+
|
|
26
|
+
// Verify JWT with signature and expiration checking
|
|
27
|
+
const payload = await verifyJWT(token, process.env.JWT_SECRET);
|
|
28
|
+
if (!payload) {
|
|
29
|
+
// Invalid signature, expired, or wrong algorithm
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Decode without verification (debugging only)
|
|
33
|
+
const decoded = decodeJWT(token);
|
|
34
|
+
|
|
35
|
+
// Check if JWT is expired
|
|
36
|
+
if (isJWTExpired(payload)) {
|
|
37
|
+
// Token has expired
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### HMAC Signing
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { hmacSign, hmacVerify, verifyBotSignature } from '@xivdyetools/auth';
|
|
45
|
+
|
|
46
|
+
// Sign data with HMAC-SHA256 (base64url output)
|
|
47
|
+
const signature = await hmacSign(data, secret);
|
|
48
|
+
|
|
49
|
+
// Verify signature
|
|
50
|
+
const isValid = await hmacVerify(data, signature, secret);
|
|
51
|
+
|
|
52
|
+
// Verify bot request signature (with timestamp validation)
|
|
53
|
+
const isValidBot = await verifyBotSignature(
|
|
54
|
+
signature, // X-Request-Signature header
|
|
55
|
+
timestamp, // X-Request-Timestamp header
|
|
56
|
+
userDiscordId,
|
|
57
|
+
userName,
|
|
58
|
+
secret,
|
|
59
|
+
{ maxAgeMs: 5 * 60 * 1000 } // Optional: 5 minute max age
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Timing-Safe Comparison
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { timingSafeEqual } from '@xivdyetools/auth';
|
|
67
|
+
|
|
68
|
+
// Constant-time string comparison (prevents timing attacks)
|
|
69
|
+
const isEqual = await timingSafeEqual(userInput, expectedValue);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Discord Signature Verification
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { verifyDiscordRequest } from '@xivdyetools/auth';
|
|
76
|
+
|
|
77
|
+
// Verify Discord interaction signature
|
|
78
|
+
const result = await verifyDiscordRequest(request, env.DISCORD_PUBLIC_KEY);
|
|
79
|
+
|
|
80
|
+
if (!result.valid) {
|
|
81
|
+
return new Response('Unauthorized', { status: 401 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// result.body contains the parsed interaction
|
|
85
|
+
const interaction = result.body;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Subpath Exports
|
|
89
|
+
|
|
90
|
+
Import only what you need for optimal tree-shaking:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// JWT utilities only
|
|
94
|
+
import { verifyJWT, decodeJWT } from '@xivdyetools/auth/jwt';
|
|
95
|
+
|
|
96
|
+
// HMAC utilities only
|
|
97
|
+
import { hmacSign, hmacVerify } from '@xivdyetools/auth/hmac';
|
|
98
|
+
|
|
99
|
+
// Timing utilities only
|
|
100
|
+
import { timingSafeEqual } from '@xivdyetools/auth/timing';
|
|
101
|
+
|
|
102
|
+
// Discord utilities only
|
|
103
|
+
import { verifyDiscordRequest } from '@xivdyetools/auth/discord';
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## API Reference
|
|
107
|
+
|
|
108
|
+
### JWT (`@xivdyetools/auth/jwt`)
|
|
109
|
+
|
|
110
|
+
| Function | Description |
|
|
111
|
+
|----------|-------------|
|
|
112
|
+
| `verifyJWT(token, secret)` | Verify JWT signature, algorithm (HS256 only), and expiration |
|
|
113
|
+
| `verifyJWTSignatureOnly(token, secret, maxAgeMs?)` | Verify signature only (for refresh token grace periods) |
|
|
114
|
+
| `decodeJWT(token)` | Decode JWT without verification (debugging only) |
|
|
115
|
+
| `isJWTExpired(payload)` | Check if JWT payload is expired |
|
|
116
|
+
| `getJWTTimeToExpiry(payload)` | Get milliseconds until JWT expires |
|
|
117
|
+
|
|
118
|
+
### HMAC (`@xivdyetools/auth/hmac`)
|
|
119
|
+
|
|
120
|
+
| Function | Description |
|
|
121
|
+
|----------|-------------|
|
|
122
|
+
| `createHmacKey(secret, usage)` | Create CryptoKey for HMAC operations |
|
|
123
|
+
| `hmacSign(data, secret)` | Sign data, return base64url signature |
|
|
124
|
+
| `hmacSignHex(data, secret)` | Sign data, return hex signature |
|
|
125
|
+
| `hmacVerify(data, signature, secret)` | Verify base64url signature |
|
|
126
|
+
| `hmacVerifyHex(data, signature, secret)` | Verify hex signature |
|
|
127
|
+
| `verifyBotSignature(sig, ts, userId, userName, secret, opts?)` | Verify bot request signature |
|
|
128
|
+
|
|
129
|
+
### Timing (`@xivdyetools/auth/timing`)
|
|
130
|
+
|
|
131
|
+
| Function | Description |
|
|
132
|
+
|----------|-------------|
|
|
133
|
+
| `timingSafeEqual(a, b)` | Constant-time string comparison |
|
|
134
|
+
| `timingSafeEqualBytes(a, b)` | Constant-time Uint8Array comparison |
|
|
135
|
+
|
|
136
|
+
### Discord (`@xivdyetools/auth/discord`)
|
|
137
|
+
|
|
138
|
+
| Function | Description |
|
|
139
|
+
|----------|-------------|
|
|
140
|
+
| `verifyDiscordRequest(request, publicKey, opts?)` | Verify Discord Ed25519 signature |
|
|
141
|
+
| `unauthorizedResponse()` | Return 401 response |
|
|
142
|
+
| `badRequestResponse(message?)` | Return 400 response |
|
|
143
|
+
|
|
144
|
+
## Security Features
|
|
145
|
+
|
|
146
|
+
- **Algorithm Validation**: JWT verification only accepts HS256, preventing algorithm confusion attacks
|
|
147
|
+
- **Timing-Safe Comparison**: Uses `crypto.subtle.timingSafeEqual()` with XOR fallback
|
|
148
|
+
- **Timestamp Validation**: Bot signatures include clock skew tolerance and max age checks
|
|
149
|
+
- **Body Size Limits**: Discord verification enforces 100KB max body size by default
|
|
150
|
+
|
|
151
|
+
## Dependencies
|
|
152
|
+
|
|
153
|
+
- `@xivdyetools/crypto` - Base64URL and hex encoding utilities
|
|
154
|
+
- `discord-interactions` - Discord Ed25519 signature verification
|
|
155
|
+
|
|
156
|
+
## Connect With Me
|
|
157
|
+
|
|
158
|
+
**Flash Galatine** | Midgardsormr (Aether)
|
|
159
|
+
|
|
160
|
+
🎮 **FFXIV**: [Lodestone Character](https://na.finalfantasyxiv.com/lodestone/character/7677106/)
|
|
161
|
+
📝 **Blog**: [Project Galatine](https://blog.projectgalatine.com/)
|
|
162
|
+
💻 **GitHub**: [@FlashGalatine](https://github.com/FlashGalatine)
|
|
163
|
+
📺 **Twitch**: [flashgalatine](https://www.twitch.tv/flashgalatine)
|
|
164
|
+
🌐 **BlueSky**: [projectgalatine.com](https://bsky.app/profile/projectgalatine.com)
|
|
165
|
+
❤️ **Patreon**: [ProjectGalatine](https://patreon.com/ProjectGalatine)
|
|
166
|
+
☕ **Ko-Fi**: [flashgalatine](https://ko-fi.com/flashgalatine)
|
|
167
|
+
💬 **Discord**: [Join Server](https://discord.gg/5VUSKTZCe5)
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT © 2025-2026 Flash Galatine
|
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
|
*
|
package/dist/hmac.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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));
|
|
@@ -127,8 +168,9 @@ export async function hmacVerifyHex(data, signature, secret) {
|
|
|
127
168
|
*/
|
|
128
169
|
export async function verifyBotSignature(signature, timestamp, userDiscordId, userName, secret, options = {}) {
|
|
129
170
|
const { maxAgeMs = 5 * 60 * 1000, clockSkewMs = 60 * 1000 } = options;
|
|
130
|
-
// Validate required fields
|
|
131
|
-
|
|
171
|
+
// Validate required fields (signature and timestamp are required;
|
|
172
|
+
// userDiscordId and userName are optional for system-level bot requests)
|
|
173
|
+
if (!signature || !timestamp) {
|
|
132
174
|
return false;
|
|
133
175
|
}
|
|
134
176
|
// Validate timestamp format
|
|
@@ -149,7 +191,7 @@ export async function verifyBotSignature(signature, timestamp, userDiscordId, us
|
|
|
149
191
|
return false;
|
|
150
192
|
}
|
|
151
193
|
// Verify the signature
|
|
152
|
-
const message = `${timestamp}:${userDiscordId}:${userName}`;
|
|
194
|
+
const message = `${timestamp}:${userDiscordId ?? ''}:${userName ?? ''}`;
|
|
153
195
|
return hmacVerifyHex(message, signature, secret);
|
|
154
196
|
}
|
|
155
197
|
//# sourceMappingURL=hmac.js.map
|
package/dist/hmac.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hmac.js","sourceRoot":"","sources":["../src/hmac.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,UAAU,EACV,UAAU,GACX,MAAM,qBAAqB,CAAC;AAY7B;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAc,EACd,QAAoC,MAAM;IAE1C,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAEvC,MAAM,SAAS,GACb,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAElD,OAAO,MAAM,CAAC,MAAM,CAAC,SAAS,CAC5B,KAAK,EACL,OAAO,EACP,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,SAAS,CACV,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY,EAAE,MAAc;IACzD,MAAM,GAAG,GAAG,MAAM,
|
|
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;
|
|
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 {
|
|
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
|
|
68
|
-
if (
|
|
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
|
|
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
|
|
127
|
-
if (
|
|
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":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,eAAe,EACf,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"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"}
|
package/dist/timing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;GAcG;AAEH,wBAAsB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA4B5E;AAED;;;;;;GAMG;AAEH,wBAAsB,oBAAoB,CACxC,CAAC,EAAE,UAAU,EACb,CAAC,EAAE,UAAU,GACZ,OAAO,CAAC,OAAO,CAAC,CAkBlB"}
|