@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.
- package/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- 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
|
+
};
|