@sigil-security/core 0.0.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/LICENSE +201 -0
- package/dist/index.cjs +639 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +546 -0
- package/dist/index.d.ts +546 -0
- package/dist/index.js +575 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CryptoProvider abstraction for all cryptographic operations.
|
|
3
|
+
*
|
|
4
|
+
* All crypto operations in sigil-core go through this interface —
|
|
5
|
+
* never call `crypto.subtle` directly from token/validation code.
|
|
6
|
+
*
|
|
7
|
+
* Default implementation: WebCryptoCryptoProvider (ships with core, zero deps).
|
|
8
|
+
* Extension point for KMS/HSM/Node native — documented, not built until needed.
|
|
9
|
+
*/
|
|
10
|
+
interface CryptoProvider {
|
|
11
|
+
/**
|
|
12
|
+
* Signs data with HMAC-SHA256.
|
|
13
|
+
* Returns the full 256-bit MAC (NO truncation).
|
|
14
|
+
*/
|
|
15
|
+
sign(key: CryptoKey, data: Uint8Array): Promise<ArrayBuffer>;
|
|
16
|
+
/**
|
|
17
|
+
* Verifies an HMAC-SHA256 signature.
|
|
18
|
+
* MUST be constant-time (crypto.subtle.verify is inherently constant-time).
|
|
19
|
+
*/
|
|
20
|
+
verify(key: CryptoKey, signature: ArrayBuffer, data: Uint8Array): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Derives an HMAC signing key from a master secret using HKDF-SHA256.
|
|
23
|
+
*
|
|
24
|
+
* @param master - Master secret as raw bytes
|
|
25
|
+
* @param salt - HKDF salt string (e.g., "sigil-v1")
|
|
26
|
+
* @param info - HKDF info string with domain separation (e.g., "csrf-signing-key-1")
|
|
27
|
+
*/
|
|
28
|
+
deriveKey(master: ArrayBuffer, salt: string, info: string): Promise<CryptoKey>;
|
|
29
|
+
/**
|
|
30
|
+
* Generates cryptographically secure random bytes.
|
|
31
|
+
* Uses crypto.getRandomValues (NOT Math.random).
|
|
32
|
+
*/
|
|
33
|
+
randomBytes(length: number): Uint8Array;
|
|
34
|
+
/**
|
|
35
|
+
* Computes SHA-256 hash of the input data.
|
|
36
|
+
* Returns the full 256-bit (32-byte) digest.
|
|
37
|
+
*/
|
|
38
|
+
hash(data: Uint8Array): Promise<ArrayBuffer>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare const TOKEN_BRAND: unique symbol;
|
|
42
|
+
declare const ONESHOT_TOKEN_BRAND: unique symbol;
|
|
43
|
+
/** Branded string type for regular CSRF tokens */
|
|
44
|
+
type TokenString = string & {
|
|
45
|
+
readonly [TOKEN_BRAND]: 'SigilToken';
|
|
46
|
+
};
|
|
47
|
+
/** Branded string type for one-shot tokens */
|
|
48
|
+
type OneShotTokenString = string & {
|
|
49
|
+
readonly [ONESHOT_TOKEN_BRAND]: 'SigilOneShotToken';
|
|
50
|
+
};
|
|
51
|
+
/** Key ID size in bytes (8-bit key identifier) */
|
|
52
|
+
declare const KID_SIZE = 1;
|
|
53
|
+
/** Nonce size in bytes (128-bit random) */
|
|
54
|
+
declare const NONCE_SIZE = 16;
|
|
55
|
+
/** Timestamp size in bytes (int64 big-endian) */
|
|
56
|
+
declare const TIMESTAMP_SIZE = 8;
|
|
57
|
+
/** Context hash size in bytes (SHA-256 output) */
|
|
58
|
+
declare const CONTEXT_SIZE = 32;
|
|
59
|
+
/** MAC size in bytes (HMAC-SHA256, full 256-bit, NO truncation) */
|
|
60
|
+
declare const MAC_SIZE = 32;
|
|
61
|
+
/** Action hash size in bytes (SHA-256 of action string) */
|
|
62
|
+
declare const ACTION_SIZE = 32;
|
|
63
|
+
/**
|
|
64
|
+
* Regular token raw size: kid(1) + nonce(16) + ts(8) + ctx(32) + mac(32) = 89 bytes FIXED
|
|
65
|
+
*/
|
|
66
|
+
declare const TOKEN_RAW_SIZE: number;
|
|
67
|
+
/**
|
|
68
|
+
* One-shot token raw size: nonce(16) + ts(8) + action(32) + ctx(32) + mac(32) = 120 bytes FIXED
|
|
69
|
+
*/
|
|
70
|
+
declare const ONESHOT_RAW_SIZE: number;
|
|
71
|
+
/** Regular token field offsets */
|
|
72
|
+
declare const TOKEN_OFFSETS: {
|
|
73
|
+
/** kid starts at byte 0 */
|
|
74
|
+
readonly KID: 0;
|
|
75
|
+
/** nonce starts at byte 1 */
|
|
76
|
+
readonly NONCE: 1;
|
|
77
|
+
/** timestamp starts at byte 17 */
|
|
78
|
+
readonly TIMESTAMP: number;
|
|
79
|
+
/** context starts at byte 25 */
|
|
80
|
+
readonly CONTEXT: number;
|
|
81
|
+
/** mac starts at byte 57 */
|
|
82
|
+
readonly MAC: number;
|
|
83
|
+
};
|
|
84
|
+
/** One-shot token field offsets */
|
|
85
|
+
declare const ONESHOT_OFFSETS: {
|
|
86
|
+
/** nonce starts at byte 0 */
|
|
87
|
+
readonly NONCE: 0;
|
|
88
|
+
/** timestamp starts at byte 16 */
|
|
89
|
+
readonly TIMESTAMP: 16;
|
|
90
|
+
/** action starts at byte 24 */
|
|
91
|
+
readonly ACTION: number;
|
|
92
|
+
/** context starts at byte 56 */
|
|
93
|
+
readonly CONTEXT: number;
|
|
94
|
+
/** mac starts at byte 88 */
|
|
95
|
+
readonly MAC: number;
|
|
96
|
+
};
|
|
97
|
+
/** Parsed regular token (extracted from wire format) */
|
|
98
|
+
interface ParsedToken {
|
|
99
|
+
readonly kid: number;
|
|
100
|
+
readonly nonce: Uint8Array;
|
|
101
|
+
readonly timestamp: number;
|
|
102
|
+
readonly context: Uint8Array;
|
|
103
|
+
readonly mac: Uint8Array;
|
|
104
|
+
}
|
|
105
|
+
/** Parsed one-shot token (extracted from wire format) */
|
|
106
|
+
interface ParsedOneShotToken {
|
|
107
|
+
readonly nonce: Uint8Array;
|
|
108
|
+
readonly timestamp: number;
|
|
109
|
+
readonly action: Uint8Array;
|
|
110
|
+
readonly context: Uint8Array;
|
|
111
|
+
readonly mac: Uint8Array;
|
|
112
|
+
}
|
|
113
|
+
/** Token validation result */
|
|
114
|
+
type ValidationResult = {
|
|
115
|
+
readonly valid: true;
|
|
116
|
+
} | {
|
|
117
|
+
readonly valid: false;
|
|
118
|
+
readonly reason: string;
|
|
119
|
+
};
|
|
120
|
+
/** Token generation result */
|
|
121
|
+
type GenerationResult = {
|
|
122
|
+
readonly success: true;
|
|
123
|
+
readonly token: TokenString;
|
|
124
|
+
readonly expiresAt: number;
|
|
125
|
+
} | {
|
|
126
|
+
readonly success: false;
|
|
127
|
+
readonly reason: string;
|
|
128
|
+
};
|
|
129
|
+
/** One-shot token generation result */
|
|
130
|
+
type OneShotGenerationResult = {
|
|
131
|
+
readonly success: true;
|
|
132
|
+
readonly token: OneShotTokenString;
|
|
133
|
+
readonly expiresAt: number;
|
|
134
|
+
} | {
|
|
135
|
+
readonly success: false;
|
|
136
|
+
readonly reason: string;
|
|
137
|
+
};
|
|
138
|
+
/** Default token TTL in milliseconds (20 minutes) */
|
|
139
|
+
declare const DEFAULT_TOKEN_TTL_MS: number;
|
|
140
|
+
/** Default grace window in milliseconds (60 seconds) */
|
|
141
|
+
declare const DEFAULT_GRACE_WINDOW_MS: number;
|
|
142
|
+
/** Default one-shot token TTL in milliseconds (5 minutes) */
|
|
143
|
+
declare const DEFAULT_ONESHOT_TTL_MS: number;
|
|
144
|
+
/** Default nonce cache max entries */
|
|
145
|
+
declare const DEFAULT_NONCE_CACHE_MAX = 10000;
|
|
146
|
+
/** Default nonce cache TTL in milliseconds (5 minutes) */
|
|
147
|
+
declare const DEFAULT_NONCE_CACHE_TTL_MS: number;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Domain separation for HKDF key derivation.
|
|
151
|
+
*
|
|
152
|
+
* Different token types derive keys from different HKDF paths,
|
|
153
|
+
* closing the cross-protocol attack surface:
|
|
154
|
+
*
|
|
155
|
+
* - `csrf`: Regular CSRF token signing keys
|
|
156
|
+
* - `oneshot`: One-shot token signing keys
|
|
157
|
+
* - `internal`: Internal/service-to-service signing keys
|
|
158
|
+
*/
|
|
159
|
+
type KeyDomain = 'csrf' | 'oneshot' | 'internal';
|
|
160
|
+
/**
|
|
161
|
+
* Derives a signing key using HKDF-SHA256 with domain separation.
|
|
162
|
+
*
|
|
163
|
+
* Key derivation path:
|
|
164
|
+
* ```
|
|
165
|
+
* HKDF(master, salt="sigil-v1", info="{domain}-signing-key-{kid}")
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* A key derived for one domain CANNOT validate tokens from another domain.
|
|
169
|
+
* This closes the cross-protocol attack surface.
|
|
170
|
+
*
|
|
171
|
+
* @param cryptoProvider - CryptoProvider for HKDF operations
|
|
172
|
+
* @param master - Master secret as raw bytes
|
|
173
|
+
* @param kid - Key identifier (8-bit)
|
|
174
|
+
* @param domain - Key domain for separation (csrf/oneshot/internal)
|
|
175
|
+
* @returns Derived HMAC-SHA256 CryptoKey
|
|
176
|
+
*/
|
|
177
|
+
declare function deriveSigningKey(cryptoProvider: CryptoProvider, master: ArrayBuffer, kid: number, domain: KeyDomain): Promise<CryptoKey>;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A single key entry in the keyring.
|
|
181
|
+
*/
|
|
182
|
+
interface KeyEntry {
|
|
183
|
+
/** Key identifier (8-bit, embedded in token) */
|
|
184
|
+
readonly kid: number;
|
|
185
|
+
/** Derived HMAC-SHA256 CryptoKey */
|
|
186
|
+
readonly cryptoKey: CryptoKey;
|
|
187
|
+
/** Timestamp when this key was created */
|
|
188
|
+
readonly createdAt: number;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Keyring holds max 3 keys (active + 2 previous) per domain.
|
|
192
|
+
*
|
|
193
|
+
* - Token generation: ALWAYS uses the active key
|
|
194
|
+
* - Token validation: tries ALL keys in the keyring (match by kid from token)
|
|
195
|
+
* - Rotation: new key becomes active, oldest dropped if > MAX_KEYS
|
|
196
|
+
*/
|
|
197
|
+
interface Keyring {
|
|
198
|
+
/** All keys in the keyring, ordered newest-first */
|
|
199
|
+
readonly keys: readonly KeyEntry[];
|
|
200
|
+
/** The kid of the currently active key */
|
|
201
|
+
readonly activeKid: number;
|
|
202
|
+
/** The domain this keyring belongs to */
|
|
203
|
+
readonly domain: KeyDomain;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Creates a new keyring with an initial key.
|
|
207
|
+
*
|
|
208
|
+
* @param cryptoProvider - CryptoProvider for key derivation
|
|
209
|
+
* @param masterSecret - Master secret as raw bytes
|
|
210
|
+
* @param initialKid - Initial key identifier (8-bit)
|
|
211
|
+
* @param domain - Key domain for HKDF separation
|
|
212
|
+
* @returns A new Keyring with one key
|
|
213
|
+
*/
|
|
214
|
+
declare function createKeyring(cryptoProvider: CryptoProvider, masterSecret: ArrayBuffer, initialKid: number, domain: KeyDomain): Promise<Keyring>;
|
|
215
|
+
/**
|
|
216
|
+
* Rotates the keyring: new key becomes active, oldest dropped if > MAX_KEYS.
|
|
217
|
+
*
|
|
218
|
+
* @param keyring - Current keyring to rotate
|
|
219
|
+
* @param cryptoProvider - CryptoProvider for key derivation
|
|
220
|
+
* @param masterSecret - Master secret as raw bytes
|
|
221
|
+
* @param newKid - New key identifier (must be unique in keyring)
|
|
222
|
+
* @returns Updated Keyring with the new active key
|
|
223
|
+
*/
|
|
224
|
+
declare function rotateKey(keyring: Keyring, cryptoProvider: CryptoProvider, masterSecret: ArrayBuffer, newKid: number): Promise<Keyring>;
|
|
225
|
+
/**
|
|
226
|
+
* Resolves a key by kid from the keyring.
|
|
227
|
+
* Returns undefined if no matching key is found.
|
|
228
|
+
*
|
|
229
|
+
* Token validation tries ALL keys to support key rotation overlap.
|
|
230
|
+
*/
|
|
231
|
+
declare function resolveKey(keyring: Keyring, kid: number): KeyEntry | undefined;
|
|
232
|
+
/**
|
|
233
|
+
* Returns the active key entry from the keyring.
|
|
234
|
+
* Token generation ALWAYS uses the active key.
|
|
235
|
+
*/
|
|
236
|
+
declare function getActiveKey(keyring: Keyring): KeyEntry | undefined;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* NonceCache interface for one-shot token replay detection.
|
|
240
|
+
*
|
|
241
|
+
* - In-memory LRU + TTL (custom implementation, no external dependency)
|
|
242
|
+
* - Atomic compare-and-swap for `markUsed` flag — prevents race conditions
|
|
243
|
+
* - Cache is an optimization, NOT a security guarantee — must fail-open if unavailable
|
|
244
|
+
* - NO external storage (Redis, DB) — in-memory only
|
|
245
|
+
*/
|
|
246
|
+
interface NonceCache {
|
|
247
|
+
/**
|
|
248
|
+
* Checks if a nonce exists in the cache (has been seen).
|
|
249
|
+
*/
|
|
250
|
+
has(nonce: Uint8Array): boolean;
|
|
251
|
+
/**
|
|
252
|
+
* Atomic compare-and-swap: marks a nonce as used.
|
|
253
|
+
* Returns true if successfully marked (nonce was NOT previously used).
|
|
254
|
+
* Returns false if the nonce was already used (replay detected).
|
|
255
|
+
*/
|
|
256
|
+
markUsed(nonce: Uint8Array): boolean;
|
|
257
|
+
/**
|
|
258
|
+
* Adds a nonce to the cache with a TTL.
|
|
259
|
+
* If the cache is at capacity, the oldest (LRU) entry is evicted.
|
|
260
|
+
*/
|
|
261
|
+
add(nonce: Uint8Array, ttlMs: number): void;
|
|
262
|
+
/** Current number of entries in the cache */
|
|
263
|
+
readonly size: number;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Configuration for the nonce cache.
|
|
267
|
+
*/
|
|
268
|
+
interface NonceCacheConfig {
|
|
269
|
+
/** Maximum number of entries (default: 10,000) */
|
|
270
|
+
readonly maxEntries?: number;
|
|
271
|
+
/** Default TTL for entries in milliseconds (default: 5 minutes) */
|
|
272
|
+
readonly defaultTTLMs?: number;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Creates an in-memory LRU + TTL nonce cache.
|
|
276
|
+
*
|
|
277
|
+
* Design constraints:
|
|
278
|
+
* - Max 10k entries (~1MB memory at ~100 bytes per entry)
|
|
279
|
+
* - 5 minute TTL (matches one-shot token TTL)
|
|
280
|
+
* - LRU eviction when capacity is reached
|
|
281
|
+
* - Atomic CAS for markUsed (single-threaded JS = naturally atomic)
|
|
282
|
+
* - Non-distributed, non-persistent
|
|
283
|
+
*
|
|
284
|
+
* @param config - Optional cache configuration
|
|
285
|
+
* @returns NonceCache instance
|
|
286
|
+
*/
|
|
287
|
+
declare function createNonceCache(config?: NonceCacheConfig): NonceCache;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Default CryptoProvider implementation using the WebCrypto API.
|
|
291
|
+
*
|
|
292
|
+
* - HMAC-SHA256 for sign/verify (full 256-bit, NO truncation)
|
|
293
|
+
* - HKDF-SHA256 for key derivation (RFC 5869)
|
|
294
|
+
* - crypto.getRandomValues for secure randomness
|
|
295
|
+
* - SHA-256 for hashing
|
|
296
|
+
*
|
|
297
|
+
* Zero external dependencies. Works in Node 18+, Bun, Deno, and Edge runtimes.
|
|
298
|
+
*/
|
|
299
|
+
declare class WebCryptoCryptoProvider implements CryptoProvider {
|
|
300
|
+
/**
|
|
301
|
+
* Signs data with HMAC-SHA256 using WebCrypto.
|
|
302
|
+
* Returns full 256-bit (32-byte) MAC, NO truncation.
|
|
303
|
+
*/
|
|
304
|
+
sign(key: CryptoKey, data: Uint8Array): Promise<ArrayBuffer>;
|
|
305
|
+
/**
|
|
306
|
+
* Verifies an HMAC-SHA256 signature using WebCrypto.
|
|
307
|
+
* Inherently constant-time via crypto.subtle.verify.
|
|
308
|
+
*/
|
|
309
|
+
verify(key: CryptoKey, signature: ArrayBuffer, data: Uint8Array): Promise<boolean>;
|
|
310
|
+
/**
|
|
311
|
+
* Derives an HMAC-SHA256 signing key from a master secret via HKDF-SHA256.
|
|
312
|
+
*
|
|
313
|
+
* HKDF (RFC 5869) with:
|
|
314
|
+
* - Hash: SHA-256
|
|
315
|
+
* - Salt: encoded string
|
|
316
|
+
* - Info: encoded string (includes domain separation)
|
|
317
|
+
* - Output: HMAC-SHA256 key, 256-bit, non-extractable
|
|
318
|
+
*/
|
|
319
|
+
deriveKey(master: ArrayBuffer, salt: string, info: string): Promise<CryptoKey>;
|
|
320
|
+
/**
|
|
321
|
+
* Generates cryptographically secure random bytes via crypto.getRandomValues.
|
|
322
|
+
* NEVER uses Math.random.
|
|
323
|
+
*/
|
|
324
|
+
randomBytes(length: number): Uint8Array;
|
|
325
|
+
/**
|
|
326
|
+
* Computes SHA-256 hash via WebCrypto.
|
|
327
|
+
* Returns full 256-bit (32-byte) digest.
|
|
328
|
+
*/
|
|
329
|
+
hash(data: Uint8Array): Promise<ArrayBuffer>;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Generates a CSRF token.
|
|
334
|
+
*
|
|
335
|
+
* Steps:
|
|
336
|
+
* 1. Generate nonce: crypto.randomBytes(16)
|
|
337
|
+
* 2. Get current timestamp
|
|
338
|
+
* 3. Compute context (provided or SHA-256(0x00), ALWAYS 32 bytes)
|
|
339
|
+
* 4. Assemble payload: kid | nonce | ts | ctx
|
|
340
|
+
* 5. Sign: HMAC-SHA256(derived_key, payload)
|
|
341
|
+
* 6. Encode: base64url(kid | nonce | ts | ctx | mac)
|
|
342
|
+
*
|
|
343
|
+
* Token wire format: 89 bytes raw → base64url encoded
|
|
344
|
+
* ```
|
|
345
|
+
* [ kid:1 ][ nonce:16 ][ ts:8 ][ ctx:32 ][ mac:32 ]
|
|
346
|
+
* ```
|
|
347
|
+
*
|
|
348
|
+
* @param cryptoProvider - CryptoProvider for crypto operations
|
|
349
|
+
* @param key - Active KeyEntry from the keyring
|
|
350
|
+
* @param context - Optional 32-byte context binding hash
|
|
351
|
+
* @param ttlMs - Token TTL in milliseconds (default: 20 minutes)
|
|
352
|
+
* @param now - Current timestamp override for testing
|
|
353
|
+
*/
|
|
354
|
+
declare function generateToken(cryptoProvider: CryptoProvider, key: KeyEntry, context?: Uint8Array, ttlMs?: number, now?: number): Promise<GenerationResult>;
|
|
355
|
+
/**
|
|
356
|
+
* Parses a token string into its constituent fields.
|
|
357
|
+
*
|
|
358
|
+
* Uses fixed offsets — no length oracle. Token must be exactly 89 bytes raw.
|
|
359
|
+
*
|
|
360
|
+
* Parse offsets:
|
|
361
|
+
* - kid @ 0 (1 byte)
|
|
362
|
+
* - nonce @ 1 (16 bytes)
|
|
363
|
+
* - ts @ 17 (8 bytes)
|
|
364
|
+
* - ctx @ 25 (32 bytes)
|
|
365
|
+
* - mac @ 57 (32 bytes)
|
|
366
|
+
*
|
|
367
|
+
* @returns ParsedToken or null if the token cannot be parsed
|
|
368
|
+
*/
|
|
369
|
+
declare function parseToken(tokenString: string): ParsedToken | null;
|
|
370
|
+
/**
|
|
371
|
+
* Assembles the payload bytes from a parsed token for MAC verification.
|
|
372
|
+
* Returns: kid | nonce | ts | ctx
|
|
373
|
+
*/
|
|
374
|
+
declare function assemblePayload(parsed: ParsedToken): Uint8Array;
|
|
375
|
+
/**
|
|
376
|
+
* Serializes token fields into a TokenString.
|
|
377
|
+
*
|
|
378
|
+
* @param kid - Key identifier (8-bit)
|
|
379
|
+
* @param nonce - 16-byte nonce
|
|
380
|
+
* @param ts - Timestamp as milliseconds
|
|
381
|
+
* @param ctx - 32-byte context hash
|
|
382
|
+
* @param mac - 32-byte MAC
|
|
383
|
+
*/
|
|
384
|
+
declare function serializeToken(kid: number, nonce: Uint8Array, ts: number, ctx: Uint8Array, mac: Uint8Array): TokenString;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Validates a CSRF token using the Deterministic Failure Model.
|
|
388
|
+
*
|
|
389
|
+
* **CRITICAL (spec Section 5.8):**
|
|
390
|
+
* ALL validation steps MUST complete. No early return. Single exit point.
|
|
391
|
+
* Timing is deterministic regardless of which step fails.
|
|
392
|
+
*
|
|
393
|
+
* Steps:
|
|
394
|
+
* 1. Parse token (constant-time length check)
|
|
395
|
+
* 2. Resolve key from keyring (match by kid)
|
|
396
|
+
* 3. TTL check (within TTL or grace window)
|
|
397
|
+
* 4. HMAC verify (constant-time via crypto.subtle.verify) — runs even if earlier steps failed
|
|
398
|
+
* 5. Context check (if expected context provided)
|
|
399
|
+
*
|
|
400
|
+
* `reason` captures the LAST failure (internal logging only).
|
|
401
|
+
* Client receives ONLY `{ valid: false, reason: "CSRF validation failed" }`.
|
|
402
|
+
*
|
|
403
|
+
* @param cryptoProvider - CryptoProvider for HMAC verification
|
|
404
|
+
* @param keyring - Keyring to resolve keys from
|
|
405
|
+
* @param tokenString - The token string to validate
|
|
406
|
+
* @param expectedContext - Optional expected 32-byte context hash
|
|
407
|
+
* @param ttlMs - Token TTL in milliseconds (default: 20 minutes)
|
|
408
|
+
* @param graceWindowMs - Grace window in milliseconds (default: 60 seconds)
|
|
409
|
+
* @param now - Current timestamp override for testing
|
|
410
|
+
*/
|
|
411
|
+
declare function validateToken(cryptoProvider: CryptoProvider, keyring: Keyring, tokenString: string, expectedContext?: Uint8Array, ttlMs?: number, graceWindowMs?: number, now?: number): Promise<ValidationResult>;
|
|
412
|
+
/**
|
|
413
|
+
* Validates token TTL against the current time.
|
|
414
|
+
*
|
|
415
|
+
* @param tokenTimestamp - Token creation timestamp (milliseconds)
|
|
416
|
+
* @param ttlMs - Token TTL in milliseconds
|
|
417
|
+
* @param graceWindowMs - Grace window after TTL (for in-flight requests)
|
|
418
|
+
* @param now - Current timestamp
|
|
419
|
+
* @returns Whether the token is within TTL or within the grace window
|
|
420
|
+
*/
|
|
421
|
+
declare function validateTTL(tokenTimestamp: number, ttlMs: number, graceWindowMs: number, now: number): {
|
|
422
|
+
withinTTL: boolean;
|
|
423
|
+
inGraceWindow: boolean;
|
|
424
|
+
};
|
|
425
|
+
/**
|
|
426
|
+
* Constant-time buffer comparison.
|
|
427
|
+
*
|
|
428
|
+
* Compares all bytes regardless of where a mismatch occurs.
|
|
429
|
+
* Length difference is also detected without early return.
|
|
430
|
+
*
|
|
431
|
+
* @param a - First buffer
|
|
432
|
+
* @param b - Second buffer
|
|
433
|
+
* @returns true if buffers are identical, false otherwise
|
|
434
|
+
*/
|
|
435
|
+
declare function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Computes context binding hash from string bindings.
|
|
439
|
+
*
|
|
440
|
+
* Context is ALWAYS 32 bytes (SHA-256 output) — eliminates length oracle.
|
|
441
|
+
*
|
|
442
|
+
* - If bindings provided: `SHA-256(len1 + binding1 + \0 + len2 + binding2 + \0 + ...)`
|
|
443
|
+
* - If no bindings: `SHA-256(0x00)` — zero-padded, NEVER empty
|
|
444
|
+
*
|
|
445
|
+
* Uses length-prefixed encoding with null-byte separators to prevent
|
|
446
|
+
* concatenation collisions: `ctx("ab","cd") !== ctx("a","bcd") !== ctx("abcd")`.
|
|
447
|
+
*
|
|
448
|
+
* @param cryptoProvider - CryptoProvider for hashing
|
|
449
|
+
* @param bindings - Strings to bind into context (e.g., sessionId, userId, origin)
|
|
450
|
+
* @returns 32-byte context hash
|
|
451
|
+
*/
|
|
452
|
+
declare function computeContext(cryptoProvider: CryptoProvider, ...bindings: string[]): Promise<Uint8Array>;
|
|
453
|
+
/**
|
|
454
|
+
* Returns the empty context value: SHA-256(0x00).
|
|
455
|
+
*
|
|
456
|
+
* Used when no context binding is specified.
|
|
457
|
+
* Always returns exactly 32 bytes — never an empty buffer.
|
|
458
|
+
*
|
|
459
|
+
* @param cryptoProvider - CryptoProvider for hashing
|
|
460
|
+
* @returns 32-byte empty context hash
|
|
461
|
+
*/
|
|
462
|
+
declare function emptyContext(cryptoProvider: CryptoProvider): Promise<Uint8Array>;
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Computes the action binding hash: SHA-256(actionString).
|
|
466
|
+
*
|
|
467
|
+
* Example: SHA-256("POST:/api/account/delete") → 32 bytes.
|
|
468
|
+
* Token is bound to a specific action — cross-action replay is impossible.
|
|
469
|
+
*/
|
|
470
|
+
declare function computeAction(cryptoProvider: CryptoProvider, action: string): Promise<Uint8Array>;
|
|
471
|
+
/**
|
|
472
|
+
* Generates a one-shot token.
|
|
473
|
+
*
|
|
474
|
+
* One-shot tokens are:
|
|
475
|
+
* - Bound to a specific action (e.g., "POST:/api/account/delete")
|
|
476
|
+
* - Used exactly once (replay protection via nonce cache)
|
|
477
|
+
* - Signed with a domain-separated key (oneshot HKDF path)
|
|
478
|
+
*
|
|
479
|
+
* Wire format: 120 bytes raw → base64url encoded
|
|
480
|
+
* ```
|
|
481
|
+
* [ nonce:16 ][ ts:8 ][ action:32 ][ ctx:32 ][ mac:32 ]
|
|
482
|
+
* ```
|
|
483
|
+
*
|
|
484
|
+
* @param cryptoProvider - CryptoProvider for crypto operations
|
|
485
|
+
* @param key - KeyEntry derived with 'oneshot' domain
|
|
486
|
+
* @param action - Action string to bind (e.g., "POST:/api/account/delete")
|
|
487
|
+
* @param context - Optional 32-byte context binding hash
|
|
488
|
+
* @param ttlMs - Token TTL in milliseconds (default: 5 minutes)
|
|
489
|
+
* @param now - Current timestamp override for testing
|
|
490
|
+
*/
|
|
491
|
+
declare function generateOneShotToken(cryptoProvider: CryptoProvider, key: KeyEntry, action: string, context?: Uint8Array, ttlMs?: number, now?: number): Promise<OneShotGenerationResult>;
|
|
492
|
+
/**
|
|
493
|
+
* Parses a one-shot token string into its constituent fields.
|
|
494
|
+
*
|
|
495
|
+
* Uses fixed offsets — no length oracle. Token must be exactly 120 bytes raw.
|
|
496
|
+
*
|
|
497
|
+
* Parse offsets:
|
|
498
|
+
* - nonce @ 0 (16 bytes)
|
|
499
|
+
* - ts @ 16 (8 bytes)
|
|
500
|
+
* - action @ 24 (32 bytes)
|
|
501
|
+
* - ctx @ 56 (32 bytes)
|
|
502
|
+
* - mac @ 88 (32 bytes)
|
|
503
|
+
*
|
|
504
|
+
* @returns ParsedOneShotToken or null if the token cannot be parsed
|
|
505
|
+
*/
|
|
506
|
+
declare function parseOneShotToken(tokenString: string): ParsedOneShotToken | null;
|
|
507
|
+
/**
|
|
508
|
+
* Validates a one-shot token using the Deterministic Failure Model.
|
|
509
|
+
*
|
|
510
|
+
* Additional checks over regular validation:
|
|
511
|
+
* - Action binding: token must match the expected action
|
|
512
|
+
* - Nonce replay: token nonce must not have been used before (via NonceCache)
|
|
513
|
+
*
|
|
514
|
+
* ALL validation steps MUST complete. No early return. Single exit point.
|
|
515
|
+
*
|
|
516
|
+
* @param cryptoProvider - CryptoProvider for HMAC verification
|
|
517
|
+
* @param key - KeyEntry derived with 'oneshot' domain
|
|
518
|
+
* @param tokenString - The one-shot token string to validate
|
|
519
|
+
* @param expectedAction - The expected action string (e.g., "POST:/api/account/delete")
|
|
520
|
+
* @param nonceCache - NonceCache for replay detection
|
|
521
|
+
* @param expectedContext - Optional expected 32-byte context hash
|
|
522
|
+
* @param ttlMs - Token TTL in milliseconds (default: 5 minutes)
|
|
523
|
+
* @param now - Current timestamp override for testing
|
|
524
|
+
*/
|
|
525
|
+
declare function validateOneShotToken(cryptoProvider: CryptoProvider, key: KeyEntry, tokenString: string, expectedAction: string, nonceCache: NonceCache, expectedContext?: Uint8Array, ttlMs?: number, now?: number): Promise<ValidationResult>;
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Encodes a Uint8Array to base64url string (RFC 4648, no padding).
|
|
529
|
+
* Pure function, zero dependencies.
|
|
530
|
+
*/
|
|
531
|
+
declare function toBase64Url(buffer: Uint8Array): string;
|
|
532
|
+
/**
|
|
533
|
+
* Decodes a base64url string (RFC 4648, no padding) to Uint8Array.
|
|
534
|
+
* Pure function, zero dependencies.
|
|
535
|
+
*
|
|
536
|
+
* @throws {Error} If the input is not valid base64url
|
|
537
|
+
*/
|
|
538
|
+
declare function fromBase64Url(encoded: string): Uint8Array;
|
|
539
|
+
/**
|
|
540
|
+
* Converts a Uint8Array to a proper ArrayBuffer.
|
|
541
|
+
* Handles the Uint8Array.buffer -> ArrayBufferLike type issue.
|
|
542
|
+
* Always creates a clean copy with its own ArrayBuffer.
|
|
543
|
+
*/
|
|
544
|
+
declare function toArrayBuffer(uint8: Uint8Array): ArrayBuffer;
|
|
545
|
+
|
|
546
|
+
export { ACTION_SIZE, CONTEXT_SIZE, type CryptoProvider, DEFAULT_GRACE_WINDOW_MS, DEFAULT_NONCE_CACHE_MAX, DEFAULT_NONCE_CACHE_TTL_MS, DEFAULT_ONESHOT_TTL_MS, DEFAULT_TOKEN_TTL_MS, type GenerationResult, KID_SIZE, type KeyDomain, type KeyEntry, type Keyring, MAC_SIZE, NONCE_SIZE, type NonceCache, type NonceCacheConfig, ONESHOT_OFFSETS, ONESHOT_RAW_SIZE, type OneShotGenerationResult, type OneShotTokenString, type ParsedOneShotToken, type ParsedToken, TIMESTAMP_SIZE, TOKEN_OFFSETS, TOKEN_RAW_SIZE, type TokenString, type ValidationResult, WebCryptoCryptoProvider, assemblePayload, computeAction, computeContext, constantTimeEqual, createKeyring, createNonceCache, deriveSigningKey, emptyContext, fromBase64Url, generateOneShotToken, generateToken, getActiveKey, parseOneShotToken, parseToken, resolveKey, rotateKey, serializeToken, toArrayBuffer, toBase64Url, validateOneShotToken, validateTTL, validateToken };
|