@lakutata/security 3.0.0-beta.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 (61) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +143 -0
  3. package/THIRD_PARTY_NOTICES.md +36 -0
  4. package/dist/cjs/exceptions/DecryptionFailedException.d.ts +13 -0
  5. package/dist/cjs/exceptions/DecryptionFailedException.js +20 -0
  6. package/dist/cjs/exceptions/InvalidSignatureException.d.ts +13 -0
  7. package/dist/cjs/exceptions/InvalidSignatureException.js +20 -0
  8. package/dist/cjs/exceptions/PasswordTooLongException.d.ts +12 -0
  9. package/dist/cjs/exceptions/PasswordTooLongException.js +19 -0
  10. package/dist/cjs/exports/Security.d.ts +19 -0
  11. package/dist/cjs/exports/Security.js +38 -0
  12. package/dist/cjs/functions/Encryption.d.ts +95 -0
  13. package/dist/cjs/functions/Encryption.js +208 -0
  14. package/dist/cjs/functions/GenerateSecret.d.ts +20 -0
  15. package/dist/cjs/functions/GenerateSecret.js +26 -0
  16. package/dist/cjs/functions/HMAC.d.ts +50 -0
  17. package/dist/cjs/functions/HMAC.js +58 -0
  18. package/dist/cjs/functions/TimingSafeEqual.d.ts +18 -0
  19. package/dist/cjs/functions/TimingSafeEqual.js +30 -0
  20. package/dist/cjs/lib/AssertKey.d.ts +5 -0
  21. package/dist/cjs/lib/AssertKey.js +11 -0
  22. package/dist/cjs/lib/bcrypt/constants.d.ts +13 -0
  23. package/dist/cjs/lib/bcrypt/constants.js +213 -0
  24. package/dist/cjs/lib/bcrypt/index.d.ts +84 -0
  25. package/dist/cjs/lib/bcrypt/index.js +658 -0
  26. package/dist/cjs/package.json +1 -0
  27. package/dist/cjs/providers/Crypt.d.ts +71 -0
  28. package/dist/cjs/providers/Crypt.js +88 -0
  29. package/dist/cjs/providers/Mac.d.ts +86 -0
  30. package/dist/cjs/providers/Mac.js +114 -0
  31. package/dist/cjs/providers/PasswordHash.d.ts +78 -0
  32. package/dist/cjs/providers/PasswordHash.js +146 -0
  33. package/dist/esm/exceptions/DecryptionFailedException.js +16 -0
  34. package/dist/esm/exceptions/InvalidSignatureException.js +16 -0
  35. package/dist/esm/exceptions/PasswordTooLongException.js +15 -0
  36. package/dist/esm/exports/Security.js +19 -0
  37. package/dist/esm/functions/Encryption.js +202 -0
  38. package/dist/esm/functions/GenerateSecret.js +23 -0
  39. package/dist/esm/functions/HMAC.js +54 -0
  40. package/dist/esm/functions/TimingSafeEqual.js +27 -0
  41. package/dist/esm/lib/AssertKey.js +8 -0
  42. package/dist/esm/lib/bcrypt/constants.js +202 -0
  43. package/dist/esm/lib/bcrypt/index.js +645 -0
  44. package/dist/esm/providers/Crypt.js +84 -0
  45. package/dist/esm/providers/Mac.js +110 -0
  46. package/dist/esm/providers/PasswordHash.js +109 -0
  47. package/dist/types/exceptions/DecryptionFailedException.d.ts +13 -0
  48. package/dist/types/exceptions/InvalidSignatureException.d.ts +13 -0
  49. package/dist/types/exceptions/PasswordTooLongException.d.ts +12 -0
  50. package/dist/types/exports/Security.d.ts +19 -0
  51. package/dist/types/functions/Encryption.d.ts +95 -0
  52. package/dist/types/functions/GenerateSecret.d.ts +20 -0
  53. package/dist/types/functions/HMAC.d.ts +50 -0
  54. package/dist/types/functions/TimingSafeEqual.d.ts +18 -0
  55. package/dist/types/lib/AssertKey.d.ts +5 -0
  56. package/dist/types/lib/bcrypt/constants.d.ts +13 -0
  57. package/dist/types/lib/bcrypt/index.d.ts +84 -0
  58. package/dist/types/providers/Crypt.d.ts +71 -0
  59. package/dist/types/providers/Mac.d.ts +86 -0
  60. package/dist/types/providers/PasswordHash.d.ts +78 -0
  61. package/package.json +45 -0
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encryptByKey = encryptByKey;
4
+ exports.decryptByKey = decryptByKey;
5
+ exports.encryptByPassword = encryptByPassword;
6
+ exports.decryptByPassword = decryptByPassword;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const DecryptionFailedException_js_1 = require("../exceptions/DecryptionFailedException.js");
9
+ const AssertKey_js_1 = require("../lib/AssertKey.js");
10
+ /**
11
+ * The versions of the encrypted data: the key derivation (the cipher is AES-256-GCM)
12
+ */
13
+ const KEY_VERSION = 1;
14
+ const PASSWORD_VERSION = 2;
15
+ const SALT_LENGTH = 16;
16
+ const IV_LENGTH = 12;
17
+ const TAG_LENGTH = 16;
18
+ const HEADER_LENGTH = 1 + SALT_LENGTH + IV_LENGTH;
19
+ const KEY_LENGTH = 32;
20
+ /**
21
+ * The scrypt parameters of the password version (about 16 MB and a few tens of milliseconds)
22
+ */
23
+ const SCRYPT_OPTIONS = { N: 16384, r: 8, p: 1 };
24
+ /**
25
+ * The key of a message: derived from the key with the salt of the message (HKDF-sha256)
26
+ * @param key
27
+ * @param salt
28
+ */
29
+ function deriveKey(key, salt) {
30
+ return Buffer.from((0, node_crypto_1.hkdfSync)('sha256', key, salt, 'lakutata-encryption', KEY_LENGTH));
31
+ }
32
+ /**
33
+ * The key of a message: derived from the password with the salt of the message (scrypt)
34
+ * @param password
35
+ * @param salt
36
+ */
37
+ function derivePasswordKey(password, salt) {
38
+ return new Promise((resolve, reject) => (0, node_crypto_1.scrypt)(password, salt, KEY_LENGTH, SCRYPT_OPTIONS, (error, key) => error ? reject(error) : resolve(key)));
39
+ }
40
+ /**
41
+ * Encrypt with a key: version, salt, iv, tag and ciphertext, base64url encoded
42
+ * @param version
43
+ * @param salt
44
+ * @param key
45
+ * @param data
46
+ */
47
+ function seal(version, salt, key, data) {
48
+ const iv = (0, node_crypto_1.randomBytes)(IV_LENGTH);
49
+ const header = Buffer.concat([Buffer.from([version]), salt, iv]);
50
+ const cipher = (0, node_crypto_1.createCipheriv)('aes-256-gcm', key, iv);
51
+ //The header is authenticated too
52
+ cipher.setAAD(header);
53
+ const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
54
+ return Buffer.concat([header, cipher.getAuthTag(), ciphertext]).toString('base64url');
55
+ }
56
+ /**
57
+ * The parts of encrypted data (undefined: not encrypted data of the version)
58
+ * @param encrypted
59
+ * @param version
60
+ */
61
+ function parts(encrypted, version) {
62
+ const buffer = Buffer.from(encrypted, 'base64url');
63
+ if (buffer.length < HEADER_LENGTH + TAG_LENGTH || buffer[0] !== version)
64
+ return undefined;
65
+ return {
66
+ header: buffer.subarray(0, HEADER_LENGTH),
67
+ salt: buffer.subarray(1, 1 + SALT_LENGTH),
68
+ iv: buffer.subarray(1 + SALT_LENGTH, HEADER_LENGTH),
69
+ tag: buffer.subarray(HEADER_LENGTH, HEADER_LENGTH + TAG_LENGTH),
70
+ ciphertext: buffer.subarray(HEADER_LENGTH + TAG_LENGTH)
71
+ };
72
+ }
73
+ /**
74
+ * Decrypt the parts with a key (undefined: the key does not decrypt them, or they were changed)
75
+ * @param part
76
+ * @param key
77
+ */
78
+ function open(part, key) {
79
+ try {
80
+ const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', key, part.iv);
81
+ decipher.setAAD(part.header);
82
+ decipher.setAuthTag(part.tag);
83
+ return Buffer.concat([decipher.update(part.ciphertext), decipher.final()]);
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }
89
+ /**
90
+ * Encrypt data with a secret key, using AES-256-GCM (authenticated encryption): the output is confidential and
91
+ * tamper-proof. Each call derives a fresh key from the key and a random 16-byte salt (HKDF-sha256) and uses a random
92
+ * 12-byte IV, so encrypting the same data twice gives different outputs. The {@link Crypt} provider does the same with
93
+ * configured keys and key rotation.
94
+ *
95
+ * The key must be random (see {@link generateSecret}): its derivation is fast, so a password used as a key is open to
96
+ * brute force; use {@link encryptByPassword} for passwords. The function refuses an empty key, but does not check
97
+ * the key's length or strength otherwise.
98
+ * @param data The data to encrypt: a string (encrypted as its UTF-8 bytes) or bytes.
99
+ * @param key The secret key: a string (used as its UTF-8 bytes, not decoded from base64) or bytes, not empty; 32
100
+ * random bytes recommended.
101
+ * @returns The encrypted data, base64url encoded without padding: 1 version byte, the salt, the IV, a 16-byte
102
+ * authentication tag and the ciphertext (45 bytes more than the data, before base64url).
103
+ * @throws {RangeError} When the key is empty.
104
+ * @example
105
+ * ```typescript
106
+ * import {decryptByKey, encryptByKey, generateSecret} from 'lakutata/security'
107
+ *
108
+ * const key: string = process.env.CRYPT_KEY ?? generateSecret()
109
+ * const encrypted: string = encryptByKey('card ending 4242', key)
110
+ * console.log(decryptByKey(encrypted, key).toString('utf8'))
111
+ * ```
112
+ */
113
+ function encryptByKey(data, key) {
114
+ (0, AssertKey_js_1.assertKey)(key);
115
+ const salt = (0, node_crypto_1.randomBytes)(SALT_LENGTH);
116
+ return seal(KEY_VERSION, salt, deriveKey(key, salt), data);
117
+ }
118
+ /**
119
+ * Decrypt data encrypted by {@link encryptByKey} (or {@link Crypt.encrypt}) with one of the keys, tried in order: list
120
+ * the current key and the previous ones during a key rotation. The authentication tag is checked before anything is
121
+ * returned, so changed data is never decrypted.
122
+ * @param encrypted The encrypted data, base64url as {@link encryptByKey} returns it.
123
+ * @param keys The key, or the keys to try: strings (used as their UTF-8 bytes) or bytes, none empty.
124
+ * @returns The decrypted bytes (call `.toString('utf8')` for a string).
125
+ * @throws {DecryptionFailedException} When no key decrypts the data, or it was changed, truncated or not produced by
126
+ * {@link encryptByKey} (data encrypted by {@link encryptByPassword} included).
127
+ * @throws {RangeError} When a key is empty.
128
+ * @example
129
+ * ```typescript
130
+ * import {decryptByKey, DecryptionFailedException} from 'lakutata/security'
131
+ *
132
+ * declare const encrypted: string
133
+ * declare const currentKey: string
134
+ * declare const previousKey: string
135
+ *
136
+ * try {
137
+ * //The new key first, the previous one while old data remains
138
+ * const secret: string = decryptByKey(encrypted, [currentKey, previousKey]).toString('utf8')
139
+ * console.log(secret)
140
+ * } catch (error) {
141
+ * if (error instanceof DecryptionFailedException) console.log('Changed, or encrypted with an unknown key')
142
+ * }
143
+ * ```
144
+ */
145
+ function decryptByKey(encrypted, keys) {
146
+ const candidates = Array.isArray(keys) ? keys : [keys];
147
+ candidates.forEach(AssertKey_js_1.assertKey);
148
+ const part = parts(encrypted, KEY_VERSION);
149
+ if (part) {
150
+ for (const key of candidates) {
151
+ const decrypted = open(part, deriveKey(key, part.salt));
152
+ if (decrypted)
153
+ return decrypted;
154
+ }
155
+ }
156
+ throw new DecryptionFailedException_js_1.DecryptionFailedException('The data cannot be decrypted');
157
+ }
158
+ /**
159
+ * Encrypt data with a password chosen by a person, using AES-256-GCM (authenticated encryption) with a key derived
160
+ * from the password and a random 16-byte salt by scrypt (N = 16384, r = 8, p = 1: about 16 MB of memory and a few
161
+ * tens of milliseconds per call, off the main thread). The slow derivation makes guessing the password costly; a
162
+ * strong password still matters. For a random key, {@link encryptByKey} is faster. To check a login password, use
163
+ * {@link PasswordHash} instead.
164
+ * @param data The data to encrypt: a string (encrypted as its UTF-8 bytes) or bytes.
165
+ * @param password The password: a string (used as its UTF-8 bytes) or bytes. Its length is not checked.
166
+ * @returns The encrypted data, base64url encoded without padding, in the format of {@link encryptByKey} with another
167
+ * version byte: the two are not interchangeable.
168
+ * @example
169
+ * ```typescript
170
+ * import {decryptByPassword, encryptByPassword} from 'lakutata/security'
171
+ *
172
+ * const encrypted: string = await encryptByPassword('backup contents', 'a long passphrase of the user')
173
+ * console.log((await decryptByPassword(encrypted, 'a long passphrase of the user')).toString('utf8'))
174
+ * ```
175
+ */
176
+ async function encryptByPassword(data, password) {
177
+ const salt = (0, node_crypto_1.randomBytes)(SALT_LENGTH);
178
+ return seal(PASSWORD_VERSION, salt, await derivePasswordKey(password, salt), data);
179
+ }
180
+ /**
181
+ * Decrypt data encrypted by {@link encryptByPassword}, deriving the key from the password and the salt stored in the
182
+ * data (the same scrypt cost).
183
+ * @param encrypted The encrypted data, base64url as {@link encryptByPassword} returns it.
184
+ * @param password The password it was encrypted with: a string (used as its UTF-8 bytes) or bytes.
185
+ * @returns The decrypted bytes (call `.toString('utf8')` for a string).
186
+ * @throws {DecryptionFailedException} When the password is wrong, or the data was changed, truncated or not produced
187
+ * by {@link encryptByPassword} (data encrypted by {@link encryptByKey} included).
188
+ * @example
189
+ * ```typescript
190
+ * import {decryptByPassword, DecryptionFailedException} from 'lakutata/security'
191
+ *
192
+ * declare const encrypted: string
193
+ * declare const passphrase: string
194
+ *
195
+ * try {
196
+ * console.log((await decryptByPassword(encrypted, passphrase)).toString('utf8'))
197
+ * } catch (error) {
198
+ * if (error instanceof DecryptionFailedException) console.log('Wrong passphrase, or changed data')
199
+ * }
200
+ * ```
201
+ */
202
+ async function decryptByPassword(encrypted, password) {
203
+ const part = parts(encrypted, PASSWORD_VERSION);
204
+ const decrypted = part ? open(part, await derivePasswordKey(password, part.salt)) : undefined;
205
+ if (decrypted)
206
+ return decrypted;
207
+ throw new DecryptionFailedException_js_1.DecryptionFailedException('The data cannot be decrypted');
208
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Generate a random secret key from the operating system's cryptographically secure generator (`crypto.randomBytes`),
3
+ * suited to the keys of {@link Mac} and {@link Crypt}, {@link hmac} and {@link encryptByKey}, and to random tokens
4
+ * (session IDs, reset links). It is not a password and should be stored as a secret (an environment variable, a vault).
5
+ * @param bytes The number of random bytes; 32 (256 bits) by default, at least 16 for a secret (0 gives an empty
6
+ * string). The keys of {@link Mac} and {@link Crypt} need 12 bytes or more to reach their 16-character minimum.
7
+ * @returns The random bytes, base64url encoded without padding: 43 characters for 32 bytes, 22 for 16.
8
+ * @throws {RangeError} When `bytes` is negative or larger than 2^31 - 1.
9
+ * @example
10
+ * ```typescript
11
+ * import {generateSecret} from 'lakutata/security'
12
+ *
13
+ * //A 256-bit key, for MAC_KEY or CRYPT_KEY
14
+ * console.log(generateSecret())
15
+ * //A 128-bit token
16
+ * const token: string = generateSecret(16)
17
+ * console.log(token)
18
+ * ```
19
+ */
20
+ export declare function generateSecret(bytes?: number): string;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateSecret = generateSecret;
4
+ const node_crypto_1 = require("node:crypto");
5
+ /**
6
+ * Generate a random secret key from the operating system's cryptographically secure generator (`crypto.randomBytes`),
7
+ * suited to the keys of {@link Mac} and {@link Crypt}, {@link hmac} and {@link encryptByKey}, and to random tokens
8
+ * (session IDs, reset links). It is not a password and should be stored as a secret (an environment variable, a vault).
9
+ * @param bytes The number of random bytes; 32 (256 bits) by default, at least 16 for a secret (0 gives an empty
10
+ * string). The keys of {@link Mac} and {@link Crypt} need 12 bytes or more to reach their 16-character minimum.
11
+ * @returns The random bytes, base64url encoded without padding: 43 characters for 32 bytes, 22 for 16.
12
+ * @throws {RangeError} When `bytes` is negative or larger than 2^31 - 1.
13
+ * @example
14
+ * ```typescript
15
+ * import {generateSecret} from 'lakutata/security'
16
+ *
17
+ * //A 256-bit key, for MAC_KEY or CRYPT_KEY
18
+ * console.log(generateSecret())
19
+ * //A 128-bit token
20
+ * const token: string = generateSecret(16)
21
+ * console.log(token)
22
+ * ```
23
+ */
24
+ function generateSecret(bytes = 32) {
25
+ return (0, node_crypto_1.randomBytes)(bytes).toString('base64url');
26
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The hash functions of the HMAC signatures: `'sha256'` (43-character signatures), `'sha384'` (64) or `'sha512'` (86).
3
+ */
4
+ export type HMACAlgorithm = 'sha256' | 'sha384' | 'sha512';
5
+ /**
6
+ * Sign data with a key: compute its hmac, a keyed hash that only a holder of the key can produce. Use it to detect
7
+ * tampering (webhooks, signed links); it does not hide the data, and is not a password hash. The {@link Mac} provider
8
+ * does the same with configured keys and key rotation.
9
+ * @param data The data to sign: a string (signed as its UTF-8 bytes) or bytes.
10
+ * @param key The secret key: a string (used as its UTF-8 bytes, not decoded) or bytes, not empty. Use a random key of
11
+ * 32 bytes, such as {@link generateSecret} returns.
12
+ * @param algorithm The hash function; `'sha256'` by default.
13
+ * @returns The hmac, base64url encoded without padding.
14
+ * @throws {RangeError} When the key is empty.
15
+ * @example
16
+ * ```typescript
17
+ * import {generateSecret, hmac, verifyHMAC} from 'lakutata/security'
18
+ *
19
+ * const key: string = process.env.WEBHOOK_KEY ?? generateSecret()
20
+ * const body: string = JSON.stringify({order: 42, status: 'paid'})
21
+ * //Sent in a header along with the body
22
+ * const signature: string = hmac(body, key, 'sha256')
23
+ * console.log(verifyHMAC(body, signature, key, 'sha256'))
24
+ * ```
25
+ */
26
+ export declare function hmac(data: string | Uint8Array, key: string | Uint8Array, algorithm?: HMACAlgorithm): string;
27
+ /**
28
+ * Check that a signature is the HMAC of data with a key, in a time that does not depend on the signature (see
29
+ * {@link constantTimeEqual}).
30
+ * @param data The signed data, exactly as it was signed (the raw request body, not a re-serialized copy).
31
+ * @param signature The signature to check, base64url encoded without padding as {@link hmac} returns it; a hex or
32
+ * padded signature does not match.
33
+ * @param key The secret key the data was signed with: a string (used as its UTF-8 bytes) or bytes, not empty.
34
+ * @param algorithm The hash function the data was signed with; `'sha256'` by default.
35
+ * @returns `true` when the signature matches.
36
+ * @throws {RangeError} When the key is empty.
37
+ * @example
38
+ * ```typescript
39
+ * import {InvalidSignatureException, verifyHMAC} from 'lakutata/security'
40
+ *
41
+ * declare const rawBody: string
42
+ * declare const signatureHeader: string
43
+ * declare const webhookKey: string
44
+ *
45
+ * if (!verifyHMAC(rawBody, signatureHeader, webhookKey)) {
46
+ * throw new InvalidSignatureException('The signature of the webhook is not valid')
47
+ * }
48
+ * ```
49
+ */
50
+ export declare function verifyHMAC(data: string | Uint8Array, signature: string, key: string | Uint8Array, algorithm?: HMACAlgorithm): boolean;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hmac = hmac;
4
+ exports.verifyHMAC = verifyHMAC;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const TimingSafeEqual_js_1 = require("./TimingSafeEqual.js");
7
+ const AssertKey_js_1 = require("../lib/AssertKey.js");
8
+ /**
9
+ * Sign data with a key: compute its hmac, a keyed hash that only a holder of the key can produce. Use it to detect
10
+ * tampering (webhooks, signed links); it does not hide the data, and is not a password hash. The {@link Mac} provider
11
+ * does the same with configured keys and key rotation.
12
+ * @param data The data to sign: a string (signed as its UTF-8 bytes) or bytes.
13
+ * @param key The secret key: a string (used as its UTF-8 bytes, not decoded) or bytes, not empty. Use a random key of
14
+ * 32 bytes, such as {@link generateSecret} returns.
15
+ * @param algorithm The hash function; `'sha256'` by default.
16
+ * @returns The hmac, base64url encoded without padding.
17
+ * @throws {RangeError} When the key is empty.
18
+ * @example
19
+ * ```typescript
20
+ * import {generateSecret, hmac, verifyHMAC} from 'lakutata/security'
21
+ *
22
+ * const key: string = process.env.WEBHOOK_KEY ?? generateSecret()
23
+ * const body: string = JSON.stringify({order: 42, status: 'paid'})
24
+ * //Sent in a header along with the body
25
+ * const signature: string = hmac(body, key, 'sha256')
26
+ * console.log(verifyHMAC(body, signature, key, 'sha256'))
27
+ * ```
28
+ */
29
+ function hmac(data, key, algorithm = 'sha256') {
30
+ (0, AssertKey_js_1.assertKey)(key);
31
+ return (0, node_crypto_1.createHmac)(algorithm, key).update(data).digest('base64url');
32
+ }
33
+ /**
34
+ * Check that a signature is the HMAC of data with a key, in a time that does not depend on the signature (see
35
+ * {@link constantTimeEqual}).
36
+ * @param data The signed data, exactly as it was signed (the raw request body, not a re-serialized copy).
37
+ * @param signature The signature to check, base64url encoded without padding as {@link hmac} returns it; a hex or
38
+ * padded signature does not match.
39
+ * @param key The secret key the data was signed with: a string (used as its UTF-8 bytes) or bytes, not empty.
40
+ * @param algorithm The hash function the data was signed with; `'sha256'` by default.
41
+ * @returns `true` when the signature matches.
42
+ * @throws {RangeError} When the key is empty.
43
+ * @example
44
+ * ```typescript
45
+ * import {InvalidSignatureException, verifyHMAC} from 'lakutata/security'
46
+ *
47
+ * declare const rawBody: string
48
+ * declare const signatureHeader: string
49
+ * declare const webhookKey: string
50
+ *
51
+ * if (!verifyHMAC(rawBody, signatureHeader, webhookKey)) {
52
+ * throw new InvalidSignatureException('The signature of the webhook is not valid')
53
+ * }
54
+ * ```
55
+ */
56
+ function verifyHMAC(data, signature, key, algorithm = 'sha256') {
57
+ return (0, TimingSafeEqual_js_1.constantTimeEqual)(hmac(data, key, algorithm), signature);
58
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Compare two secrets (signatures, tokens, API keys) in a time that does not depend on their content or their lengths,
3
+ * so an attacker cannot guess a secret by timing the comparisons. Values of different lengths are compared too, and
4
+ * are unequal. Use it instead of `===` whenever one side is a secret; to check a password, use
5
+ * {@link PasswordHash.validate} instead.
6
+ * @param a The first value: a string (compared as its UTF-8 bytes) or bytes.
7
+ * @param b The second value: a string (compared as its UTF-8 bytes) or bytes.
8
+ * @returns `true` when both values hold the same bytes.
9
+ * @example
10
+ * ```typescript
11
+ * import {constantTimeEqual} from 'lakutata/security'
12
+ *
13
+ * declare const presentedApiKey: string
14
+ *
15
+ * if (!constantTimeEqual(presentedApiKey, process.env.API_KEY ?? '')) throw new Error('Unauthorized')
16
+ * ```
17
+ */
18
+ export declare function constantTimeEqual(a: string | Uint8Array, b: string | Uint8Array): boolean;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.constantTimeEqual = constantTimeEqual;
4
+ const node_crypto_1 = require("node:crypto");
5
+ /**
6
+ * A random key of the process: the compared values are digested with it, so the comparison does not reveal their length
7
+ */
8
+ const COMPARISON_KEY = (0, node_crypto_1.randomBytes)(32);
9
+ /**
10
+ * Compare two secrets (signatures, tokens, API keys) in a time that does not depend on their content or their lengths,
11
+ * so an attacker cannot guess a secret by timing the comparisons. Values of different lengths are compared too, and
12
+ * are unequal. Use it instead of `===` whenever one side is a secret; to check a password, use
13
+ * {@link PasswordHash.validate} instead.
14
+ * @param a The first value: a string (compared as its UTF-8 bytes) or bytes.
15
+ * @param b The second value: a string (compared as its UTF-8 bytes) or bytes.
16
+ * @returns `true` when both values hold the same bytes.
17
+ * @example
18
+ * ```typescript
19
+ * import {constantTimeEqual} from 'lakutata/security'
20
+ *
21
+ * declare const presentedApiKey: string
22
+ *
23
+ * if (!constantTimeEqual(presentedApiKey, process.env.API_KEY ?? '')) throw new Error('Unauthorized')
24
+ * ```
25
+ */
26
+ function constantTimeEqual(a, b) {
27
+ const digestA = (0, node_crypto_1.createHmac)('sha256', COMPARISON_KEY).update(a).digest();
28
+ const digestB = (0, node_crypto_1.createHmac)('sha256', COMPARISON_KEY).update(b).digest();
29
+ return (0, node_crypto_1.timingSafeEqual)(digestA, digestB);
30
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Refuse an empty key: HMAC and HKDF accept it, but data signed or encrypted with it is open to anyone
3
+ * @param key
4
+ */
5
+ export declare function assertKey(key: string | Uint8Array): void;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertKey = assertKey;
4
+ /**
5
+ * Refuse an empty key: HMAC and HKDF accept it, but data signed or encrypted with it is open to anyone
6
+ * @param key
7
+ */
8
+ function assertKey(key) {
9
+ if (key.length === 0)
10
+ throw new RangeError('The key is empty');
11
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Derived from the upstream index.js (see UPSTREAM.md; BSD-3-Clause, see THIRD_PARTY_NOTICES.md)
3
+ */
4
+ declare const BASE64_CODE: readonly string[];
5
+ declare const BASE64_INDEX: readonly number[];
6
+ declare const BCRYPT_SALT_LEN: number;
7
+ declare const GENSALT_DEFAULT_LOG2_ROUNDS: number;
8
+ declare const BLOWFISH_NUM_ROUNDS: number;
9
+ declare const MAX_EXECUTION_TIME: number;
10
+ declare const P_ORIG: readonly number[];
11
+ declare const S_ORIG: readonly number[];
12
+ declare const C_ORIG: readonly number[];
13
+ export { BASE64_CODE, BASE64_INDEX, BCRYPT_SALT_LEN, GENSALT_DEFAULT_LOG2_ROUNDS, BLOWFISH_NUM_ROUNDS, MAX_EXECUTION_TIME, P_ORIG, S_ORIG, C_ORIG };