@ix-xs/node-comfort 1.2.0 → 2.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
package/src/Crypto.js ADDED
@@ -0,0 +1,1082 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Crypto without the footguns, built on `node:crypto`: hashes, HMAC,
5
+ * password hashing (scrypt), encryption (AES-256-GCM), JWT, two-factor
6
+ * codes (TOTP) and secure random values.
7
+ *
8
+ * @example
9
+ * const stored = await crypto.hashPassword("hunter2");
10
+ * await crypto.verifyPassword("hunter2", stored); // true
11
+ *
12
+ * const token = crypto.signJWT({ sub: user.id }, SECRET, { expiresIn: "1h" });
13
+ * const claims = crypto.verifyJWT(token, SECRET);
14
+ */
15
+
16
+ const { types } = require("node:util");
17
+ const { JWTError, NodeComfortError } = require("./errors.js");
18
+ const { toMs } = require("./internal/shared.js");
19
+
20
+ /** @type {typeof import("node:crypto") | undefined} */
21
+ let _nodeCrypto;
22
+ // node:crypto takes a few milliseconds to initialize: load it on first use.
23
+ const _crypto = () => (_nodeCrypto ??= require("node:crypto"));
24
+
25
+ /**
26
+ * @param {import("node:crypto").BinaryLike} password
27
+ * @param {import("node:crypto").BinaryLike} salt
28
+ * @param {number} keylen
29
+ * @param {import("node:crypto").ScryptOptions} options
30
+ * @returns {Promise<Buffer>}
31
+ */
32
+ const _scrypt = (password, salt, keylen, options) =>
33
+ new Promise((resolve, reject) => {
34
+ _crypto().scrypt(password, salt, keylen, options, (error, key) => (error ? reject(error) : resolve(key)));
35
+ });
36
+
37
+ /**
38
+ * Text encodings for digests.
39
+ * @typedef {"hex" | "base64" | "base64url"} DigestEncoding
40
+ */
41
+
42
+ /**
43
+ * Options for `hash()` and `hmac()`.
44
+ * @typedef {object} HashOptions
45
+ * @property {"sha256" | "sha384" | "sha512" | "sha1" | "md5" | "sha3-256" | "sha3-512" | (string & {})} [algorithm] Any algorithm OpenSSL supports. Defaults to `"sha256"`.
46
+ * @property {DigestEncoding} [encoding] Defaults to `"hex"`.
47
+ */
48
+
49
+ /**
50
+ * Options for `hashPassword()`.
51
+ * @typedef {object} HashPasswordOptions
52
+ * @property {number} [cost] Work factor as a power of two; each +1 doubles the time. Defaults to `15`.
53
+ * @property {number} [blockSize] scrypt `r`. Defaults to `8`.
54
+ * @property {number} [parallelization] scrypt `p`. Defaults to `1`.
55
+ * @property {number} [saltLength] In bytes. Defaults to `16`.
56
+ * @property {number} [keyLength] In bytes. Defaults to `32`.
57
+ */
58
+
59
+ /**
60
+ * Options for `encrypt()`.
61
+ * @typedef {object} EncryptOptions
62
+ * @property {string | Buffer} [associatedData] Data that's authenticated but not encrypted, like a user id. Pass the same value to `decrypt()`.
63
+ */
64
+
65
+ /**
66
+ * Options for `decrypt()`.
67
+ * @typedef {object} DecryptOptions
68
+ * @property {string | Buffer} [associatedData] The value given to `encrypt()`, if any.
69
+ * @property {"utf8" | "buffer"} [output] Get a string or a `Buffer`. Defaults to `"utf8"`.
70
+ */
71
+
72
+ /**
73
+ * A key for `signJWT()` and `verifyJWT()`. For the HS algorithms it's a
74
+ * shared secret. For the others it's a key pair: a PEM string, a
75
+ * `KeyObject` or a JWK, private to sign and public to verify.
76
+ * @typedef {string | Buffer | Uint8Array | import("node:crypto").KeyObject | import("node:crypto").JsonWebKey} JWTKey
77
+ */
78
+
79
+ /**
80
+ * A pair of keys in PEM, as made by `generateKeyPair()`.
81
+ * @typedef {object} KeyPair
82
+ * @property {string} publicKey Give this one away: it only checks signatures.
83
+ * @property {string} privateKey Keep this one secret: it signs.
84
+ */
85
+
86
+ /**
87
+ * Standard JWT claims.
88
+ * @typedef {object} JWTClaims
89
+ * @property {string} [iss] Issuer.
90
+ * @property {string} [sub] Subject, usually the user id.
91
+ * @property {string | string[]} [aud] Audience.
92
+ * @property {number} [exp] Expiry, in Unix seconds.
93
+ * @property {number} [nbf] Not valid before, in Unix seconds.
94
+ * @property {number} [iat] Issued at, in Unix seconds.
95
+ * @property {string} [jti] Unique token id.
96
+ */
97
+
98
+ /**
99
+ * Supported JWT algorithms. HS signs and verifies with one shared secret.
100
+ * RS, PS, ES and EdDSA sign with a private key and verify with the public
101
+ * one, so services that only check tokens never hold the signing key.
102
+ * @typedef {"HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512" | "PS256" | "PS384" | "PS512" | "ES256" | "ES384" | "ES512" | "EdDSA"} JWTAlgorithm
103
+ */
104
+
105
+ /**
106
+ * Options for `signJWT()`.
107
+ * @typedef {object} SignJWTOptions
108
+ * @property {JWTAlgorithm} [algorithm] Defaults to what fits the key: `"HS256"` for a secret, `"RS256"` for an RSA key, `"ES256"` for a P-256 key, `"EdDSA"` for an Ed25519 key.
109
+ * @property {string | number} [expiresIn] Lifetime in seconds or as a duration like `"15m"`. Sets `exp`.
110
+ * @property {string | number} [notBefore] Delay before the token is valid. Sets `nbf`.
111
+ * @property {string} [issuer] Sets `iss`.
112
+ * @property {string} [subject] Sets `sub`.
113
+ * @property {string | string[]} [audience] Sets `aud`.
114
+ * @property {string} [jwtId] Sets `jti`, handy for revocation lists.
115
+ * @property {boolean} [noTimestamp] Don't add `iat`.
116
+ * @property {Record<string, unknown>} [header] Extra header fields, like `{ kid: "2024-key" }`.
117
+ */
118
+
119
+ /**
120
+ * Options for `verifyJWT()`.
121
+ * @typedef {object} VerifyJWTOptions
122
+ * @property {JWTAlgorithm[]} [algorithms] Accepted algorithms. Defaults to the ones that fit the key, so a token can't pick a weaker one.
123
+ * @property {string | string[]} [issuer] Required issuer.
124
+ * @property {string | string[]} [audience] Required audience; one match is enough.
125
+ * @property {string} [subject] Required subject.
126
+ * @property {number} [clockTolerance] Seconds of slack for `exp` and `nbf` when server clocks differ.
127
+ * @property {string | number} [maxAge] Reject tokens issued longer ago than this.
128
+ * @property {boolean} [ignoreExpiration] Skip the `exp` check.
129
+ * @property {Date | number} [now] What time it is. Defaults to now. Handy in tests.
130
+ */
131
+
132
+ /**
133
+ * A decoded JWT.
134
+ * @template [P=Record<string, unknown>]
135
+ * @typedef {object} DecodedJWT
136
+ * @property {{ alg: string, typ?: string, [key: string]: unknown }} header
137
+ * @property {P & JWTClaims} payload
138
+ * @property {string} signature Base64URL.
139
+ */
140
+
141
+ /**
142
+ * Options for `totp()` and `verifyTOTP()`.
143
+ * @typedef {object} TOTPOptions
144
+ * @property {number} [digits] Defaults to `6`.
145
+ * @property {number} [period] How long a code lasts, in seconds. Defaults to `30`.
146
+ * @property {"SHA1" | "SHA256" | "SHA512"} [algorithm] Defaults to `"SHA1"`, which is what authenticator apps expect.
147
+ * @property {Date | number} [time] Compute the code for this moment. Defaults to now.
148
+ */
149
+
150
+ /**
151
+ * Options for `verifyTOTP()`. `window` is how many periods before and after
152
+ * now are accepted (defaults to `1`).
153
+ * @typedef {TOTPOptions & { window?: number }} VerifyTOTPOptions
154
+ */
155
+
156
+ /**
157
+ * Options for `totpURI()`.
158
+ * @typedef {object} TOTPURIOptions
159
+ * @property {string} label The account shown in the app, usually the email.
160
+ * @property {string} [issuer] Your app's name.
161
+ * @property {number} [digits] Defaults to `6`.
162
+ * @property {number} [period] Defaults to `30`.
163
+ * @property {"SHA1" | "SHA256" | "SHA512"} [algorithm] Defaults to `"SHA1"`.
164
+ */
165
+
166
+ const _BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
167
+
168
+ /**
169
+ * @param {Buffer} buffer
170
+ * @returns {string}
171
+ */
172
+ const _base32Encode = (buffer) => {
173
+ let bits = 0;
174
+ let value = 0;
175
+ let out = "";
176
+ for (const byte of buffer) {
177
+ value = (value << 8) | byte;
178
+ bits += 8;
179
+ while (bits >= 5) {
180
+ out += _BASE32[(value >>> (bits - 5)) & 31];
181
+ bits -= 5;
182
+ }
183
+ }
184
+ if (bits > 0) out += _BASE32[(value << (5 - bits)) & 31];
185
+ return out;
186
+ };
187
+
188
+ /**
189
+ * @param {string} text
190
+ * @returns {Buffer}
191
+ */
192
+ const _base32Decode = (text) => {
193
+ const clean = text.toUpperCase().replace(/[\s=-]/g, "");
194
+ let bits = 0;
195
+ let value = 0;
196
+ const out = [];
197
+ for (const char of clean) {
198
+ const index = _BASE32.indexOf(char);
199
+ if (index === -1) throw new TypeError(`Invalid Base32 character "${char}"`);
200
+ value = (value << 5) | index;
201
+ bits += 5;
202
+ if (bits >= 8) {
203
+ out.push((value >>> (bits - 8)) & 255);
204
+ bits -= 8;
205
+ }
206
+ }
207
+ return Buffer.from(out);
208
+ };
209
+
210
+ /**
211
+ * @param {unknown} data
212
+ * @returns {Buffer | undefined} `undefined` for what has no obvious bytes: `null`, objects...
213
+ */
214
+ const _maybeBuffer = (data) => {
215
+ if (typeof data === "string") return Buffer.from(data, "utf8");
216
+ if (Buffer.isBuffer(data)) return data;
217
+ if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
218
+ if (types.isAnyArrayBuffer(data)) return Buffer.from(data);
219
+ if (typeof data === "number" || typeof data === "bigint") return Buffer.from(String(data), "utf8");
220
+ return undefined;
221
+ };
222
+
223
+ /**
224
+ * @param {unknown} data
225
+ * @param {string} [what]
226
+ * @returns {Buffer}
227
+ */
228
+ const _toBuffer = (data, what = "the data") => {
229
+ const buffer = _maybeBuffer(data);
230
+ // Hashing "undefined" because a variable was missing is how secrets end up guessable.
231
+ if (!buffer) throw new TypeError(`Expected ${what} to be a string, a Buffer or a Uint8Array, got ${data === null ? "null" : typeof data}`);
232
+ return buffer;
233
+ };
234
+
235
+ /**
236
+ * @param {unknown} secret
237
+ * @param {string} what
238
+ * @returns {Buffer}
239
+ */
240
+ const _secret = (secret, what) => {
241
+ const buffer = _toBuffer(secret, what);
242
+ if (!buffer.length) throw new TypeError(`Expected ${what} not to be empty`);
243
+ return buffer;
244
+ };
245
+
246
+ /**
247
+ * @param {string | number} value
248
+ * @param {string} what
249
+ * @returns {number}
250
+ */
251
+ const _seconds = (value, what) => {
252
+ const seconds = typeof value === "number" ? value : Math.round(toMs(value, what) / 1000);
253
+ if (!Number.isFinite(seconds)) throw new TypeError(`Invalid ${what}: ${String(value)}`);
254
+ return seconds;
255
+ };
256
+
257
+ /**
258
+ * @param {Date | number | undefined} value
259
+ * @returns {number} Milliseconds.
260
+ */
261
+ const _now = (value) => (value === undefined ? Date.now() : +value);
262
+
263
+ /**
264
+ * Hashes data, SHA-256 in hex by default.
265
+ *
266
+ * @example
267
+ * crypto.hash("hello");
268
+ * crypto.hash("hello", { algorithm: "sha512", encoding: "base64url" });
269
+ *
270
+ * @param {string | Buffer | Uint8Array} data Strings are read as UTF-8.
271
+ * @param {HashOptions} [options]
272
+ * @returns {string}
273
+ */
274
+ function hash(data, options = {}) {
275
+ return _crypto().hash(options.algorithm ?? "sha256", typeof data === "string" ? data : _toBuffer(data), options.encoding ?? "hex");
276
+ }
277
+
278
+ /**
279
+ * An HMAC signature: a hash keyed with a secret. The usual way to sign and
280
+ * check webhooks.
281
+ *
282
+ * @example
283
+ * const signature = crypto.hmac(rawBody, process.env.WEBHOOK_SECRET);
284
+ * crypto.safeEqual(signature, req.headers["x-signature"]);
285
+ *
286
+ * @param {string | Buffer | Uint8Array} data
287
+ * @param {string | Buffer | Uint8Array} secret
288
+ * @param {HashOptions} [options]
289
+ * @returns {string}
290
+ */
291
+ function hmac(data, secret, options = {}) {
292
+ return _crypto()
293
+ .createHmac(options.algorithm ?? "sha256", _secret(secret, "the secret"))
294
+ .update(typeof data === "string" ? data : _toBuffer(data))
295
+ .digest(options.encoding ?? "hex");
296
+ }
297
+
298
+ /**
299
+ * Checks an HMAC signature you received, in constant time. A leading
300
+ * `sha256=`, as GitHub and others send, is ignored.
301
+ *
302
+ * @example
303
+ * // an Express webhook route: sign the raw body, not the parsed JSON
304
+ * if (!crypto.verifyHmac(req.rawBody, req.headers["x-hub-signature-256"], process.env.WEBHOOK_SECRET)) {
305
+ * return res.status(401).end();
306
+ * }
307
+ *
308
+ * @param {string | Buffer | Uint8Array} data
309
+ * @param {unknown} signature What the sender computed. Anything but a matching string gives `false`.
310
+ * @param {string | Buffer | Uint8Array} secret
311
+ * @param {HashOptions} [options] Must match what the sender used.
312
+ * @returns {boolean}
313
+ */
314
+ function verifyHmac(data, signature, secret, options = {}) {
315
+ const expected = hmac(data, secret, options);
316
+ if (typeof signature !== "string") return false;
317
+ const given = signature.replace(/^sha(?:1|256|384|512)=/i, "");
318
+ return safeEqual(expected, (options.encoding ?? "hex") === "hex" ? given.toLowerCase() : given);
319
+ }
320
+
321
+ /**
322
+ * Compares two secrets in constant time, so the comparison can't leak
323
+ * anything through timing, not even their lengths. Use it instead of `===`
324
+ * for tokens, signatures and API keys. A missing value (`undefined`, `null`)
325
+ * never equals anything.
326
+ *
327
+ * @example
328
+ * if (!crypto.safeEqual(providedKey, process.env.API_KEY)) throw new Error("Forbidden");
329
+ *
330
+ * @param {string | Buffer | Uint8Array} a
331
+ * @param {string | Buffer | Uint8Array} b
332
+ * @returns {boolean}
333
+ */
334
+ function safeEqual(a, b) {
335
+ const x = _maybeBuffer(a);
336
+ const y = _maybeBuffer(b);
337
+ if (!x || !y) return false;
338
+ const crypto = _crypto();
339
+ // Digests have one length, whatever went in.
340
+ return crypto.timingSafeEqual(crypto.hash("sha256", x, "buffer"), crypto.hash("sha256", y, "buffer"));
341
+ }
342
+
343
+ const _SCRYPT_HASH = /^\$scrypt\$ln=(\d{1,2}),r=(\d{1,4}),p=(\d{1,4})\$([\w-]+)\$([\w-]+)$/;
344
+ // scrypt needs 128 * N * r bytes. Past 1 GiB it's a mistake, or a doctored hash meant to stall the server.
345
+ const _SCRYPT_MAX_MEMORY = 2 ** 30;
346
+
347
+ /**
348
+ * @param {number} ln
349
+ * @param {number} r
350
+ * @param {number} p
351
+ * @returns {import("node:crypto").ScryptOptions | undefined} `undefined` if the settings are out of bounds.
352
+ */
353
+ const _scryptOptions = (ln, r, p) => {
354
+ if (![ln, r, p].every(Number.isInteger) || ln < 1 || ln > 30 || r < 1 || p < 1 || p > 1024) return undefined;
355
+ const N = 2 ** ln;
356
+ if (128 * N * r > _SCRYPT_MAX_MEMORY) return undefined;
357
+ return { N, r, p, maxmem: 256 * N * r + 128 * r * p + 1024 * 1024 };
358
+ };
359
+
360
+ /**
361
+ * Hashes a password with scrypt and a random salt. The result records its
362
+ * own settings, so you can raise the cost later without breaking old
363
+ * hashes.
364
+ *
365
+ * @example
366
+ * const stored = await crypto.hashPassword(password);
367
+ * // "$scrypt$ln=15,r=8,p=1$4xY...$kq9...", store this
368
+ *
369
+ * @param {string} password
370
+ * @param {HashPasswordOptions} [options]
371
+ * @returns {Promise<string>}
372
+ * @throws {RangeError} If the settings are out of bounds, like a cost that needs more than 1 GiB of memory.
373
+ */
374
+ async function hashPassword(password, options = {}) {
375
+ if (typeof password !== "string") throw new TypeError(`hashPassword(): expected the password to be a string, got ${password === null ? "null" : typeof password}`);
376
+ const ln = options.cost ?? 15;
377
+ const r = options.blockSize ?? 8;
378
+ const p = options.parallelization ?? 1;
379
+ const saltLength = options.saltLength ?? 16;
380
+ const keyLength = options.keyLength ?? 32;
381
+ const scrypt = _scryptOptions(ln, r, p);
382
+ if (!scrypt) throw new RangeError(`hashPassword(): cost ${ln}, blockSize ${r}, parallelization ${p} is out of bounds`);
383
+ if (!Number.isInteger(saltLength) || saltLength < 8) throw new RangeError("hashPassword(): saltLength must be at least 8 bytes");
384
+ if (!Number.isInteger(keyLength) || keyLength < 16 || keyLength > 128) throw new RangeError("hashPassword(): keyLength must be between 16 and 128 bytes");
385
+ const salt = _crypto().randomBytes(saltLength);
386
+ const key = await _scrypt(password.normalize("NFKC"), salt, keyLength, scrypt);
387
+ return `$scrypt$ln=${ln},r=${r},p=${p}$${salt.toString("base64url")}$${key.toString("base64url")}`;
388
+ }
389
+
390
+ /**
391
+ * Checks a password against a stored hash, in constant time. A wrong
392
+ * password or a broken hash gives `false`, never an error.
393
+ *
394
+ * @example
395
+ * if (!(await crypto.verifyPassword(input, user.passwordHash))) throw new Error("Invalid credentials");
396
+ *
397
+ * @param {string} password
398
+ * @param {string} stored
399
+ * @returns {Promise<boolean>}
400
+ */
401
+ async function verifyPassword(password, stored) {
402
+ const match = typeof password === "string" && typeof stored === "string" ? _SCRYPT_HASH.exec(stored) : null;
403
+ if (!match) return false;
404
+ const scrypt = _scryptOptions(Number(match[1]), Number(match[2]), Number(match[3]));
405
+ const expected = Buffer.from(match[5], "base64url");
406
+ if (!scrypt || !expected.length || expected.length > 128) return false;
407
+ try {
408
+ const actual = await _scrypt(password.normalize("NFKC"), Buffer.from(match[4], "base64url"), expected.length, scrypt);
409
+ return _crypto().timingSafeEqual(actual, expected);
410
+ } catch {
411
+ return false;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Was this hash made with weaker settings than yours? If so, re-hash the
417
+ * password right after a successful login.
418
+ *
419
+ * @example
420
+ * if (await crypto.verifyPassword(input, user.hash) && crypto.needsRehash(user.hash, { cost: 16 })) {
421
+ * user.hash = await crypto.hashPassword(input, { cost: 16 });
422
+ * }
423
+ *
424
+ * @param {string} stored
425
+ * @param {HashPasswordOptions} [options] The settings you use today.
426
+ * @returns {boolean}
427
+ */
428
+ function needsRehash(stored, options = {}) {
429
+ const match = _SCRYPT_HASH.exec(String(stored));
430
+ if (!match) return true;
431
+ return Number(match[1]) < (options.cost ?? 15) || Number(match[2]) < (options.blockSize ?? 8) || Number(match[3]) < (options.parallelization ?? 1);
432
+ }
433
+
434
+ /**
435
+ * A random 256-bit key for `encrypt()`, as text you can put in an
436
+ * environment variable.
437
+ *
438
+ * @returns {string}
439
+ */
440
+ function generateKey() {
441
+ return _crypto().randomBytes(32).toString("base64url");
442
+ }
443
+
444
+ /**
445
+ * @param {string | Buffer | Uint8Array} secret
446
+ * @param {Buffer} salt
447
+ * @returns {Buffer}
448
+ */
449
+ const _deriveKey = (secret, salt) => {
450
+ const raw = typeof secret === "string" && /^[A-Za-z0-9_-]{43}$/.test(secret) ? Buffer.from(secret, "base64url") : _secret(secret, "the encryption secret");
451
+ return Buffer.from(_crypto().hkdfSync("sha256", raw, salt, "node-comfort:aes-256-gcm", 32));
452
+ };
453
+
454
+ /**
455
+ * Encrypts data with AES-256-GCM, which also detects tampering. Every call
456
+ * uses a fresh salt and nonce, so the same text never encrypts the same way
457
+ * twice.
458
+ *
459
+ * Use a key from `generateKey()` or another long random secret, not a
460
+ * password someone typed: that would be easy to brute-force.
461
+ *
462
+ * @example
463
+ * const box = crypto.encrypt(JSON.stringify(card), process.env.ENCRYPTION_KEY);
464
+ * const card = JSON.parse(crypto.decrypt(box, process.env.ENCRYPTION_KEY));
465
+ *
466
+ * @param {string | Buffer | Uint8Array} data
467
+ * @param {string | Buffer | Uint8Array} secret
468
+ * @param {EncryptOptions} [options]
469
+ * @returns {string} A compact, URL-safe string.
470
+ */
471
+ function encrypt(data, secret, options = {}) {
472
+ const crypto = _crypto();
473
+ const salt = crypto.randomBytes(28);
474
+ const iv = salt.subarray(16);
475
+ const cipher = crypto.createCipheriv("aes-256-gcm", _deriveKey(secret, salt.subarray(0, 16)), iv);
476
+ if (options.associatedData !== undefined) cipher.setAAD(_toBuffer(options.associatedData, "associatedData"));
477
+ const head = cipher.update(_toBuffer(data));
478
+ const tail = cipher.final();
479
+ const encrypted = tail.length ? Buffer.concat([head, tail]) : head;
480
+ return `v1.${salt.toString("base64url", 0, 16)}.${iv.toString("base64url")}.${cipher.getAuthTag().toString("base64url")}.${encrypted.toString("base64url")}`;
481
+ }
482
+
483
+ /**
484
+ * Decrypts what `encrypt()` produced. Throws an error with code
485
+ * `ERR_DECRYPT` if the secret is wrong or the data was changed.
486
+ *
487
+ * @example
488
+ * const text = crypto.decrypt(box, SECRET);
489
+ * const bytes = crypto.decrypt(box, SECRET, { output: "buffer" });
490
+ *
491
+ * @overload
492
+ * @param {string} payload
493
+ * @param {string | Buffer | Uint8Array} secret
494
+ * @param {DecryptOptions & { output: "buffer" }} options
495
+ * @returns {Buffer}
496
+ */
497
+ /**
498
+ * Decrypts what `encrypt()` produced, as text.
499
+ *
500
+ * @overload
501
+ * @param {string} payload
502
+ * @param {string | Buffer | Uint8Array} secret
503
+ * @param {DecryptOptions} [options]
504
+ * @returns {string}
505
+ */
506
+ /**
507
+ * @param {string} payload
508
+ * @param {string | Buffer | Uint8Array} secret
509
+ * @param {DecryptOptions} [options]
510
+ * @returns {string | Buffer}
511
+ * @throws {NodeComfortError} With code `ERR_DECRYPT` if the secret is wrong or the payload was modified.
512
+ */
513
+ function decrypt(payload, secret, options = {}) {
514
+ const parts = typeof payload === "string" ? payload.split(".") : [];
515
+ const [salt, iv, tag, encrypted] = parts.slice(1).map((part) => Buffer.from(part, "base64url"));
516
+ // A tag cut short is far easier to forge, so only whole ones are read.
517
+ if (parts.length !== 5 || parts[0] !== "v1" || salt.length !== 16 || iv.length !== 12 || tag.length !== 16) {
518
+ throw new NodeComfortError("Unable to decrypt: this isn't something encrypt() produced", { code: "ERR_DECRYPT" });
519
+ }
520
+ const key = _deriveKey(secret, salt);
521
+ const associated = options.associatedData === undefined ? undefined : _toBuffer(options.associatedData, "associatedData");
522
+ let decrypted;
523
+ try {
524
+ const decipher = _crypto().createDecipheriv("aes-256-gcm", key, iv, { authTagLength: 16 });
525
+ decipher.setAuthTag(tag);
526
+ if (associated) decipher.setAAD(associated);
527
+ decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
528
+ } catch (error) {
529
+ throw new NodeComfortError("Unable to decrypt: wrong secret or tampered data", { code: "ERR_DECRYPT", cause: error });
530
+ }
531
+ return options.output === "buffer" ? decrypted : decrypted.toString("utf8");
532
+ }
533
+
534
+ /** @type {readonly JWTAlgorithm[]} */
535
+ const _HS = ["HS256", "HS384", "HS512"];
536
+ /** @type {Record<string, JWTAlgorithm>} */
537
+ const _CURVE_ALGORITHMS = { prime256v1: "ES256", secp384r1: "ES384", secp521r1: "ES512" };
538
+ /** @type {Record<string, string>} */
539
+ const _ALGORITHM_CURVES = { ES256: "prime256v1", ES384: "secp384r1", ES512: "secp521r1" };
540
+
541
+ /**
542
+ * A key, ready to use, with the algorithms it may be used with.
543
+ * @typedef {{ secret: Buffer, key?: undefined, algorithms: readonly JWTAlgorithm[] } | { secret?: undefined, key: import("node:crypto").KeyObject, algorithms: readonly JWTAlgorithm[] }} _ResolvedKey
544
+ */
545
+
546
+ /**
547
+ * @param {string} message
548
+ * @param {unknown} [cause]
549
+ */
550
+ const _keyError = (message, cause) => new TypeError(message, { cause });
551
+
552
+ /**
553
+ * @param {import("node:crypto").KeyObject} key
554
+ * @returns {_ResolvedKey}
555
+ */
556
+ const _asymmetric = (key) => {
557
+ const type = key.asymmetricKeyType;
558
+ if (type === "rsa" || type === "rsa-pss") {
559
+ if ((key.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) throw _keyError("RSA keys must be at least 2048 bits long");
560
+ return { key, algorithms: type === "rsa" ? ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"] : ["PS256", "PS384", "PS512"] };
561
+ }
562
+ if (type === "ec") {
563
+ const algorithm = _CURVE_ALGORITHMS[key.asymmetricKeyDetails?.namedCurve ?? ""];
564
+ if (!algorithm) throw _keyError(`Unsupported curve "${key.asymmetricKeyDetails?.namedCurve}": use P-256, P-384 or P-521`);
565
+ return { key, algorithms: [algorithm] };
566
+ }
567
+ if (type === "ed25519" || type === "ed448") return { key, algorithms: ["EdDSA"] };
568
+ throw _keyError(`Unsupported key type "${type}"`);
569
+ };
570
+
571
+ /** @param {unknown} key */
572
+ const _isPEM = (key) => {
573
+ const start = typeof key === "string" ? key.slice(0, 64) : Buffer.isBuffer(key) ? key.toString("latin1", 0, 64) : "";
574
+ return start.trimStart().startsWith("-----BEGIN ");
575
+ };
576
+
577
+ /**
578
+ * @param {JWTKey} key
579
+ * @param {"sign" | "verify"} use
580
+ * @returns {_ResolvedKey}
581
+ */
582
+ const _readKey = (key, use) => {
583
+ const crypto = _crypto();
584
+ if (types.isKeyObject(key)) {
585
+ if (key.type === "secret") return { secret: key.export(), algorithms: _HS };
586
+ if (use === "sign" && key.type !== "private") throw _keyError("Signing takes a private key, this one is public");
587
+ return _asymmetric(use === "verify" && key.type === "private" ? crypto.createPublicKey(key) : key);
588
+ }
589
+ const jwk = key !== null && typeof key === "object" && !ArrayBuffer.isView(key) && typeof (/** @type {{ kty?: unknown }} */ (key).kty) === "string";
590
+ if (jwk && /** @type {import("node:crypto").JsonWebKey} */ (key).kty === "oct") {
591
+ return { secret: _secret(Buffer.from(String(/** @type {import("node:crypto").JsonWebKey} */ (key).k ?? ""), "base64url"), "the JWT secret"), algorithms: _HS };
592
+ }
593
+ if (jwk || _isPEM(key)) {
594
+ const input = /** @type {any} */ (jwk ? { key, format: "jwk" } : key);
595
+ let object;
596
+ try {
597
+ object = use === "sign" ? crypto.createPrivateKey(input) : crypto.createPublicKey(input);
598
+ } catch (error) {
599
+ throw _keyError(use === "sign" ? "Signing takes a private key, and this can't be read as one" : "This key can't be read", error);
600
+ }
601
+ return _asymmetric(object);
602
+ }
603
+ return { secret: _secret(key, "the JWT secret"), algorithms: _HS };
604
+ };
605
+
606
+ // Reading a PEM takes far longer than checking a signature, and a server
607
+ // uses the same few keys for every request.
608
+ /** @type {Record<"sign" | "verify", Map<string, _ResolvedKey>>} */
609
+ const _textKeys = { sign: new Map(), verify: new Map() };
610
+ /** @type {Record<"sign" | "verify", WeakMap<object, _ResolvedKey>>} */
611
+ const _objectKeys = { sign: new WeakMap(), verify: new WeakMap() };
612
+
613
+ /**
614
+ * @param {JWTKey} key
615
+ * @param {"sign" | "verify"} use
616
+ * @returns {_ResolvedKey}
617
+ */
618
+ const _jwtKey = (key, use) => {
619
+ if (typeof key === "string") {
620
+ if (!_isPEM(key)) return _readKey(key, use);
621
+ let resolved = _textKeys[use].get(key);
622
+ if (!resolved) {
623
+ resolved = _readKey(key, use);
624
+ if (_textKeys[use].size >= 20) _textKeys[use].clear();
625
+ _textKeys[use].set(key, resolved);
626
+ }
627
+ return resolved;
628
+ }
629
+ // Buffers can change after the call, so only immutable key objects and JWKs are remembered.
630
+ if (key === null || typeof key !== "object" || ArrayBuffer.isView(key)) return _readKey(key, use);
631
+ let resolved = _objectKeys[use].get(key);
632
+ if (!resolved) _objectKeys[use].set(key, (resolved = _readKey(key, use)));
633
+ return resolved;
634
+ };
635
+
636
+ /**
637
+ * @param {JWTAlgorithm} algorithm
638
+ * @param {import("node:crypto").KeyObject} key
639
+ * @returns {any}
640
+ */
641
+ const _signingKey = (algorithm, key) => {
642
+ if (algorithm[0] === "P") {
643
+ const { constants } = _crypto();
644
+ return { key, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST };
645
+ }
646
+ // JWTs carry ECDSA signatures as r and s side by side, not in DER.
647
+ return algorithm[0] === "E" && algorithm !== "EdDSA" ? { key, dsaEncoding: "ieee-p1363" } : key;
648
+ };
649
+
650
+ /** @param {JWTAlgorithm} algorithm */
651
+ const _digestOf = (algorithm) => (algorithm === "EdDSA" ? null : `sha${algorithm.slice(2)}`);
652
+
653
+ /**
654
+ * @param {JWTAlgorithm} algorithm
655
+ * @param {string} signed The header and the payload, as sent.
656
+ * @param {_ResolvedKey} key
657
+ * @returns {string}
658
+ */
659
+ const _signature = (algorithm, signed, key) => {
660
+ const crypto = _crypto();
661
+ if (key.secret) return crypto.createHmac(/** @type {string} */ (_digestOf(algorithm)), key.secret).update(signed).digest("base64url");
662
+ return crypto.sign(_digestOf(algorithm), Buffer.from(signed), _signingKey(algorithm, key.key)).toString("base64url");
663
+ };
664
+
665
+ /**
666
+ * @param {JWTAlgorithm} algorithm
667
+ * @param {string} signed
668
+ * @param {string} signature
669
+ * @param {_ResolvedKey} key
670
+ * @returns {boolean}
671
+ */
672
+ const _signatureMatches = (algorithm, signed, signature, key) => {
673
+ const crypto = _crypto();
674
+ if (key.secret) {
675
+ const expected = Buffer.from(_signature(algorithm, signed, key));
676
+ const given = Buffer.from(signature);
677
+ return expected.length === given.length && crypto.timingSafeEqual(expected, given);
678
+ }
679
+ try {
680
+ return crypto.verify(_digestOf(algorithm), Buffer.from(signed), _signingKey(algorithm, key.key), Buffer.from(signature, "base64url"));
681
+ } catch {
682
+ return false;
683
+ }
684
+ };
685
+
686
+ /** @type {Map<JWTAlgorithm, string>} */
687
+ const _plainHeaders = new Map();
688
+
689
+ /**
690
+ * @param {JWTAlgorithm} algorithm
691
+ * @param {Record<string, unknown> | undefined} extra
692
+ * @returns {string}
693
+ */
694
+ const _header = (algorithm, extra) => {
695
+ if (extra) return Buffer.from(JSON.stringify({ typ: "JWT", ...extra, alg: algorithm })).toString("base64url");
696
+ let header = _plainHeaders.get(algorithm);
697
+ if (!header) _plainHeaders.set(algorithm, (header = Buffer.from(JSON.stringify({ alg: algorithm, typ: "JWT" })).toString("base64url")));
698
+ return header;
699
+ };
700
+
701
+ /**
702
+ * @param {unknown} token
703
+ * @returns {{ header: { alg: string, typ?: string, [key: string]: unknown }, payload: Record<string, unknown>, signature: string, signed: string }}
704
+ */
705
+ const _decode = (token) => {
706
+ const text = typeof token === "string" ? token : "";
707
+ const first = text.indexOf(".");
708
+ const second = text.indexOf(".", first + 1);
709
+ if (first < 1 || second === -1 || text.includes(".", second + 1)) throw new JWTError("Malformed JWT: expected 3 parts", { code: "ERR_JWT_MALFORMED" });
710
+ try {
711
+ const header = JSON.parse(Buffer.from(text.slice(0, first), "base64url").toString("utf8"));
712
+ const payload = JSON.parse(Buffer.from(text.slice(first + 1, second), "base64url").toString("utf8"));
713
+ if (!header || typeof header !== "object" || Array.isArray(header) || !payload || typeof payload !== "object" || Array.isArray(payload)) {
714
+ throw new TypeError("not an object");
715
+ }
716
+ return { header, payload, signature: text.slice(second + 1), signed: text.slice(0, second) };
717
+ } catch (error) {
718
+ throw new JWTError("Malformed JWT: header or payload is not valid JSON", { code: "ERR_JWT_MALFORMED", cause: error });
719
+ }
720
+ };
721
+
722
+ /**
723
+ * Creates a signed JSON Web Token. `iat` is added for you.
724
+ *
725
+ * With a secret, the token is signed with HMAC. With a private key, it's
726
+ * signed with that key, and anyone holding the public key can verify it
727
+ * without being able to issue tokens.
728
+ *
729
+ * @example
730
+ * const token = crypto.signJWT({ sub: String(user.id), role: user.role }, process.env.JWT_SECRET, {
731
+ * expiresIn: "15m",
732
+ * issuer: "api.example.com",
733
+ * });
734
+ *
735
+ * // key pair: the auth service signs, every other service verifies
736
+ * const { publicKey, privateKey } = await crypto.generateKeyPair("ES256");
737
+ * const signed = crypto.signJWT({ sub: "42" }, privateKey, { expiresIn: "15m" });
738
+ * crypto.verifyJWT(signed, publicKey);
739
+ *
740
+ * @param {Record<string, unknown>} payload
741
+ * @param {JWTKey} secret For HS algorithms, at least 32 random bytes. Otherwise a private key.
742
+ * @param {SignJWTOptions} [options]
743
+ * @returns {string}
744
+ * @throws {TypeError} If the key can't be used, or doesn't fit the algorithm.
745
+ */
746
+ function signJWT(payload, secret, options = {}) {
747
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) throw new TypeError("signJWT(): the payload must be an object");
748
+ const key = _jwtKey(secret, "sign");
749
+ const algorithm = options.algorithm ?? key.algorithms[0];
750
+ if (!key.algorithms.includes(algorithm)) {
751
+ throw new TypeError(`signJWT(): algorithm "${algorithm}" doesn't fit this key, use ${key.algorithms.join(", ")}`);
752
+ }
753
+ const now = Math.floor(Date.now() / 1000);
754
+ /** @type {Record<string, unknown>} */
755
+ const claims = { ...payload };
756
+ if (!options.noTimestamp && claims.iat === undefined) claims.iat = now;
757
+ if (options.expiresIn !== undefined) claims.exp = now + _seconds(options.expiresIn, "expiresIn");
758
+ if (options.notBefore !== undefined) claims.nbf = now + _seconds(options.notBefore, "notBefore");
759
+ if (options.issuer !== undefined) claims.iss = options.issuer;
760
+ if (options.subject !== undefined) claims.sub = options.subject;
761
+ if (options.audience !== undefined) claims.aud = options.audience;
762
+ if (options.jwtId !== undefined) claims.jti = options.jwtId;
763
+ const signed = `${_header(algorithm, options.header)}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
764
+ return `${signed}.${_signature(algorithm, signed, key)}`;
765
+ }
766
+
767
+ /**
768
+ * Reads a JWT without checking it. Fine for display; call `verifyJWT()`
769
+ * before trusting anything in it.
770
+ *
771
+ * @example
772
+ * crypto.decodeJWT(token).payload.exp;
773
+ *
774
+ * @template [P=Record<string, unknown>]
775
+ * @param {string} token
776
+ * @returns {DecodedJWT<P>}
777
+ * @throws {JWTError} With code `ERR_JWT_MALFORMED` if the token can't be read.
778
+ */
779
+ function decodeJWT(token) {
780
+ const { header, payload, signature } = _decode(token);
781
+ return { header, payload: /** @type {P & JWTClaims} */ (payload), signature };
782
+ }
783
+
784
+ /**
785
+ * Verifies a JWT: algorithm, signature, expiry, and whichever claims you
786
+ * require. Returns the payload, or throws a `JWTError` whose `code` says
787
+ * what's wrong.
788
+ *
789
+ * The accepted algorithms follow from the key you pass, so a token signed
790
+ * with HMAC is never accepted where a public key is expected.
791
+ *
792
+ * @example
793
+ * try {
794
+ * const { sub } = crypto.verifyJWT(token, process.env.JWT_SECRET, { issuer: "api.example.com" });
795
+ * } catch (error) {
796
+ * res.status(401).json({ error: error.code }); // "ERR_JWT_EXPIRED"...
797
+ * }
798
+ *
799
+ * @template [P=Record<string, unknown>]
800
+ * @param {string} token
801
+ * @param {JWTKey} secret The shared secret, or the public key for RS, PS, ES and EdDSA tokens.
802
+ * @param {VerifyJWTOptions} [options]
803
+ * @returns {P & JWTClaims}
804
+ * @throws {JWTError}
805
+ */
806
+ function verifyJWT(token, secret, options = {}) {
807
+ const { header, payload, signature, signed } = _decode(token);
808
+ const key = _jwtKey(secret, "verify");
809
+ const algorithm = /** @type {JWTAlgorithm} */ (header.alg);
810
+ if (typeof algorithm !== "string" || !key.algorithms.includes(algorithm) || (options.algorithms !== undefined && !options.algorithms.includes(algorithm))) {
811
+ throw new JWTError(`Algorithm "${String(header.alg)}" is not allowed`, { code: "ERR_JWT_ALGORITHM" });
812
+ }
813
+ if (!_signatureMatches(algorithm, signed, signature, key)) throw new JWTError("Invalid signature", { code: "ERR_JWT_SIGNATURE" });
814
+
815
+ const claims = /** @type {JWTClaims & Record<string, unknown>} */ (payload);
816
+ for (const name of /** @type {const} */ (["exp", "nbf", "iat"])) {
817
+ if (claims[name] !== undefined && !Number.isFinite(claims[name])) throw new JWTError(`Invalid "${name}" claim: expected a number`, { code: "ERR_JWT_CLAIM" });
818
+ }
819
+ const now = Math.floor(_now(options.now) / 1000);
820
+ const tolerance = options.clockTolerance ?? 0;
821
+ if (!options.ignoreExpiration && claims.exp !== undefined && now - tolerance >= claims.exp) {
822
+ throw new JWTError("Token expired", { code: "ERR_JWT_EXPIRED", expiredAt: new Date(claims.exp * 1000) });
823
+ }
824
+ if (claims.nbf !== undefined && now + tolerance < claims.nbf) throw new JWTError("Token not active yet", { code: "ERR_JWT_NOT_BEFORE" });
825
+ if (options.maxAge !== undefined && (claims.iat === undefined || now - tolerance > claims.iat + _seconds(options.maxAge, "maxAge"))) {
826
+ throw new JWTError("Token is too old", { code: "ERR_JWT_EXPIRED" });
827
+ }
828
+ if (options.issuer !== undefined && !(typeof claims.iss === "string" && (Array.isArray(options.issuer) ? options.issuer.includes(claims.iss) : options.issuer === claims.iss))) {
829
+ throw new JWTError("Invalid issuer", { code: "ERR_JWT_CLAIM" });
830
+ }
831
+ if (options.subject !== undefined && claims.sub !== options.subject) throw new JWTError("Invalid subject", { code: "ERR_JWT_CLAIM" });
832
+ if (options.audience !== undefined) {
833
+ const wanted = Array.isArray(options.audience) ? options.audience : [options.audience];
834
+ const actual = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
835
+ if (!actual.some((audience) => typeof audience === "string" && wanted.includes(audience))) throw new JWTError("Invalid audience", { code: "ERR_JWT_CLAIM" });
836
+ }
837
+ return /** @type {P & JWTClaims} */ (payload);
838
+ }
839
+
840
+ /**
841
+ * Makes a pair of keys for signing JWTs: the private one signs, the public
842
+ * one verifies. Both come as PEM text, ready for an environment variable or
843
+ * a file.
844
+ *
845
+ * `"EdDSA"` (Ed25519) is the fastest with the shortest keys and signatures.
846
+ * `"ES256"` is the most widely supported after RSA. Pick `"RS256"` when the
847
+ * other side asks for it.
848
+ *
849
+ * @example
850
+ * const { publicKey, privateKey } = await crypto.generateKeyPair();
851
+ * const token = crypto.signJWT({ sub: "42" }, privateKey);
852
+ * crypto.verifyJWT(token, publicKey);
853
+ *
854
+ * @param {Exclude<JWTAlgorithm, "HS256" | "HS384" | "HS512">} [algorithm="EdDSA"] The JWT algorithm the keys are for.
855
+ * @param {{ modulusLength?: number }} [options] RSA key size in bits. Defaults to `2048`.
856
+ * @returns {Promise<KeyPair>}
857
+ */
858
+ function generateKeyPair(algorithm = "EdDSA", options = {}) {
859
+ /** @type {[string, object]} */
860
+ let spec;
861
+ if (algorithm === "EdDSA") spec = ["ed25519", {}];
862
+ else if (Object.hasOwn(_ALGORITHM_CURVES, algorithm)) spec = ["ec", { namedCurve: _ALGORITHM_CURVES[algorithm] }];
863
+ else if (/^(RS|PS)(256|384|512)$/.test(algorithm)) {
864
+ const modulusLength = options.modulusLength ?? 2048;
865
+ if (!Number.isInteger(modulusLength) || modulusLength < 2048) return Promise.reject(new RangeError("generateKeyPair(): modulusLength must be at least 2048"));
866
+ spec = ["rsa", { modulusLength }];
867
+ } else {
868
+ return Promise.reject(new TypeError(`generateKeyPair(): expected an RS, PS, ES or EdDSA algorithm, got "${String(algorithm)}"`));
869
+ }
870
+ const encoding = { publicKeyEncoding: { type: "spki", format: "pem" }, privateKeyEncoding: { type: "pkcs8", format: "pem" } };
871
+ return new Promise((resolve, reject) => {
872
+ /** @type {any} */ (_crypto().generateKeyPair)(spec[0], { ...spec[1], ...encoding }, (/** @type {Error | null} */ error, /** @type {string} */ publicKey, /** @type {string} */ privateKey) =>
873
+ error ? reject(error) : resolve({ publicKey, privateKey }),
874
+ );
875
+ });
876
+ }
877
+
878
+ /**
879
+ * A new two-factor secret, in the Base32 format authenticator apps expect.
880
+ * Store it encrypted.
881
+ *
882
+ * @example
883
+ * user.totpSecret = crypto.totpSecret();
884
+ *
885
+ * @param {number} [bytes=20]
886
+ * @returns {string}
887
+ */
888
+ function totpSecret(bytes = 20) {
889
+ return _base32Encode(_crypto().randomBytes(bytes));
890
+ }
891
+
892
+ /**
893
+ * @param {Buffer} key
894
+ * @param {number} counter
895
+ * @param {number} digits
896
+ * @param {string} algorithm
897
+ * @returns {string}
898
+ */
899
+ const _hotp = (key, counter, digits, algorithm) => {
900
+ const buffer = Buffer.alloc(8);
901
+ buffer.writeUInt32BE(Math.floor(counter / 2 ** 32), 0);
902
+ buffer.writeUInt32BE(counter >>> 0, 4);
903
+ const digest = _crypto().createHmac(algorithm.toLowerCase(), key).update(buffer).digest();
904
+ const off = digest[digest.length - 1] & 0x0f;
905
+ const code = ((digest[off] & 0x7f) << 24) | (digest[off + 1] << 16) | (digest[off + 2] << 8) | digest[off + 3];
906
+ return String(code % 10 ** digits).padStart(digits, "0");
907
+ };
908
+
909
+ /**
910
+ * @param {TOTPOptions} options
911
+ * @returns {{ digits: number, period: number, algorithm: string, time: number }}
912
+ */
913
+ const _totpOptions = (options) => {
914
+ const digits = options.digits ?? 6;
915
+ const period = options.period ?? 30;
916
+ if (!Number.isInteger(digits) || digits < 4 || digits > 10) throw new RangeError(`TOTP codes have 4 to 10 digits, got ${digits}`);
917
+ if (!(period > 0)) throw new RangeError(`The TOTP period must be a positive number of seconds, got ${period}`);
918
+ return { digits, period, algorithm: options.algorithm ?? "SHA1", time: _now(options.time) };
919
+ };
920
+
921
+ /**
922
+ * The current 6-digit code for a two-factor secret, as shown by
923
+ * authenticator apps.
924
+ *
925
+ * @example
926
+ * crypto.totp(user.totpSecret); // "492039"
927
+ *
928
+ * @param {string} secret
929
+ * @param {TOTPOptions} [options]
930
+ * @returns {string}
931
+ */
932
+ function totp(secret, options = {}) {
933
+ const { digits, period, algorithm, time } = _totpOptions(options);
934
+ return _hotp(_base32Decode(secret), Math.floor(time / 1000 / period), digits, algorithm);
935
+ }
936
+
937
+ /**
938
+ * Checks a code typed by a user. Codes from the neighbouring periods are
939
+ * accepted too, to cope with clock drift.
940
+ *
941
+ * @example
942
+ * if (!crypto.verifyTOTP(req.body.code, user.totpSecret)) throw new Error("Invalid code");
943
+ *
944
+ * @param {string} token Spaces are ignored.
945
+ * @param {string} secret
946
+ * @param {VerifyTOTPOptions} [options]
947
+ * @returns {boolean}
948
+ */
949
+ function verifyTOTP(token, secret, options = {}) {
950
+ const { digits, period, algorithm, time } = _totpOptions(options);
951
+ const key = _base32Decode(secret);
952
+ const code = Buffer.from(String(token).replace(/\s/g, ""));
953
+ if (code.length !== digits) return false;
954
+ const window = Math.max(0, Math.floor(options.window ?? 1));
955
+ const counter = Math.floor(time / 1000 / period);
956
+ let valid = false;
957
+ // Every candidate is checked, so the time taken says nothing about which one matched.
958
+ for (let i = -window; i <= window; i++) {
959
+ if (counter + i >= 0 && _crypto().timingSafeEqual(Buffer.from(_hotp(key, counter + i, digits, algorithm)), code)) valid = true;
960
+ }
961
+ return valid;
962
+ }
963
+
964
+ /**
965
+ * The `otpauth://` link to show as a QR code when a user turns on 2FA.
966
+ *
967
+ * @example
968
+ * const uri = crypto.totpURI(user.totpSecret, { label: user.email, issuer: "My App" });
969
+ *
970
+ * @param {string} secret
971
+ * @param {TOTPURIOptions} options
972
+ * @returns {string}
973
+ */
974
+ function totpURI(secret, options) {
975
+ const label = options.issuer ? `${encodeURIComponent(options.issuer)}:${encodeURIComponent(options.label)}` : encodeURIComponent(options.label);
976
+ const params = new URLSearchParams({ secret: secret.replace(/\s/g, "").toUpperCase() });
977
+ if (options.issuer) params.set("issuer", options.issuer);
978
+ params.set("algorithm", options.algorithm ?? "SHA1");
979
+ params.set("digits", String(options.digits ?? 6));
980
+ params.set("period", String(options.period ?? 30));
981
+ return `otpauth://totp/${label}?${params.toString().replace(/\+/g, "%20")}`;
982
+ }
983
+
984
+ /**
985
+ * Secure random bytes.
986
+ *
987
+ * @param {number} size
988
+ * @returns {Buffer}
989
+ */
990
+ function randomBytes(size) {
991
+ return _crypto().randomBytes(size);
992
+ }
993
+
994
+ /**
995
+ * A secure random integer from `min` to `max`, both included, with no bias.
996
+ * For anything that has to be fair or unpredictable.
997
+ *
998
+ * @example
999
+ * crypto.randomInt(1, 6); // a fair dice roll
1000
+ * crypto.randomInt(0, 999_999); // a 6-digit code
1001
+ *
1002
+ * @param {number} min
1003
+ * @param {number} max
1004
+ * @returns {number}
1005
+ */
1006
+ function randomInt(min, max) {
1007
+ const low = Math.ceil(min);
1008
+ const high = Math.floor(max);
1009
+ if (!(high >= low)) throw new RangeError(`randomInt(): expected min <= max, got ${min} and ${max}`);
1010
+ return _crypto().randomInt(low, high + 1);
1011
+ }
1012
+
1013
+ /**
1014
+ * Encodes to Base64, or URL-safe Base64.
1015
+ *
1016
+ * @example
1017
+ * crypto.toBase64("héllo"); // "aMOpbGxv"
1018
+ * crypto.toBase64("héllo", { urlSafe: true });
1019
+ *
1020
+ * @param {string | Buffer | Uint8Array} data
1021
+ * @param {{ urlSafe?: boolean }} [options]
1022
+ * @returns {string}
1023
+ */
1024
+ function toBase64(data, options = {}) {
1025
+ return _toBuffer(data).toString(options.urlSafe ? "base64url" : "base64");
1026
+ }
1027
+
1028
+ /**
1029
+ * Decodes Base64 or URL-safe Base64.
1030
+ *
1031
+ * @example
1032
+ * crypto.fromBase64("aMOpbGxv"); // "héllo"
1033
+ * crypto.fromBase64("aMOpbGxv", { output: "buffer" }); // <Buffer 68 c3 a9 6c 6c 6f>
1034
+ *
1035
+ * @overload
1036
+ * @param {string} text
1037
+ * @param {{ output: "buffer" }} options
1038
+ * @returns {Buffer}
1039
+ */
1040
+ /**
1041
+ * Decodes Base64 or URL-safe Base64 into text.
1042
+ *
1043
+ * @overload
1044
+ * @param {string} text
1045
+ * @param {{ output?: "utf8" }} [options]
1046
+ * @returns {string}
1047
+ */
1048
+ /**
1049
+ * @param {string} text
1050
+ * @param {{ output?: "utf8" | "buffer" }} [options]
1051
+ * @returns {string | Buffer}
1052
+ */
1053
+ function fromBase64(text, options = {}) {
1054
+ // Node reads both alphabets, padded or not.
1055
+ const buffer = Buffer.from(String(text), "base64");
1056
+ return options.output === "buffer" ? buffer : buffer.toString("utf8");
1057
+ }
1058
+
1059
+ module.exports = {
1060
+ hash,
1061
+ hmac,
1062
+ verifyHmac,
1063
+ safeEqual,
1064
+ hashPassword,
1065
+ verifyPassword,
1066
+ needsRehash,
1067
+ generateKey,
1068
+ encrypt,
1069
+ decrypt,
1070
+ signJWT,
1071
+ decodeJWT,
1072
+ verifyJWT,
1073
+ generateKeyPair,
1074
+ totpSecret,
1075
+ totp,
1076
+ verifyTOTP,
1077
+ totpURI,
1078
+ randomBytes,
1079
+ randomInt,
1080
+ toBase64,
1081
+ fromBase64,
1082
+ };