@zudojs/crypto 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cryptoConstants/cryptoConstants.security.d.ts +8 -0
- package/dist/cryptoConstants/cryptoConstants.security.js +8 -0
- package/dist/cryptoKeyDerivation/cryptoKeyDerivation.validate.d.ts +11 -0
- package/dist/cryptoKeyDerivation/cryptoKeyDerivation.validate.js +39 -12
- package/dist/node/nodeCryptoProvider/operations/nodeCryptoProvider.derivation.js +33 -10
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ const sessionToken = await generateToken({ bytes: 32, prefix: "sess_" });
|
|
|
43
43
|
- Hashing: SHA-256/384/512 and SHA3-256/384/512, HMAC (keys of at least 16 bytes)
|
|
44
44
|
- Authenticated encryption: AES-256-GCM with strict IV (12 bytes) and tag (16 bytes) validation, plus a versioned string envelope
|
|
45
45
|
- Password hashing: scrypt (default, OWASP parameters) and PBKDF2-HMAC (provider level), versioned self-describing encoding, bounded parameters on verification
|
|
46
|
-
- Key derivation: PBKDF2 (sha256/384/512, 600 000 iterations by default) and scrypt (cost, block size, parallelization, memory bound)
|
|
46
|
+
- Key derivation: PBKDF2 (sha256/384/512, 600 000 iterations by default) and scrypt (cost, block size, parallelization, memory bound); every work factor and the output length are capped by `PASSWORD_HASH.LIMITS`, so a value read from configuration cannot request unbounded CPU or memory
|
|
47
47
|
- Digital signatures: Ed25519, RSA-SHA256/384/512, ECDSA-SHA256/384/512; the algorithm label is bound to the key type
|
|
48
48
|
- Secure random: unbiased integers up to 2^48, UUID v4, bytes, alphabets, numeric codes
|
|
49
49
|
- Opaque tokens (API keys, sessions, refresh, CSRF, OTP) with SHA-256 storage hashes
|
|
@@ -41,6 +41,14 @@ export declare const PASSWORD_HASH: Readonly<{
|
|
|
41
41
|
*/
|
|
42
42
|
MAX_SCRYPT_MEMORY_BYTES: number;
|
|
43
43
|
MAX_PBKDF2_ITERATIONS: 10000000;
|
|
44
|
+
/**
|
|
45
|
+
* Upper bound on the output length of `derivePbkdf2` / `deriveScrypt`
|
|
46
|
+
* (and the provider's `deriveKey`). PBKDF2 cost scales linearly with
|
|
47
|
+
* the number of output blocks, so an unbounded `keyLength` multiplies
|
|
48
|
+
* the iteration count by an attacker-chosen factor. Password hashes
|
|
49
|
+
* are bounded separately by `MAX_KEY_BYTES`.
|
|
50
|
+
*/
|
|
51
|
+
MAX_DERIVED_KEY_BYTES: 1024;
|
|
44
52
|
}>;
|
|
45
53
|
}>;
|
|
46
54
|
/**
|
|
@@ -42,6 +42,14 @@ export const PASSWORD_HASH = Object.freeze({
|
|
|
42
42
|
*/
|
|
43
43
|
MAX_SCRYPT_MEMORY_BYTES: 1024 * 1024 * 1024,
|
|
44
44
|
MAX_PBKDF2_ITERATIONS: 10_000_000,
|
|
45
|
+
/**
|
|
46
|
+
* Upper bound on the output length of `derivePbkdf2` / `deriveScrypt`
|
|
47
|
+
* (and the provider's `deriveKey`). PBKDF2 cost scales linearly with
|
|
48
|
+
* the number of output blocks, so an unbounded `keyLength` multiplies
|
|
49
|
+
* the iteration count by an attacker-chosen factor. Password hashes
|
|
50
|
+
* are bounded separately by `MAX_KEY_BYTES`.
|
|
51
|
+
*/
|
|
52
|
+
MAX_DERIVED_KEY_BYTES: 1024,
|
|
45
53
|
}),
|
|
46
54
|
});
|
|
47
55
|
/**
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validates PBKDF2 key derivation options.
|
|
3
|
+
*
|
|
4
|
+
* Both floors and ceilings are enforced: `iterations` is bounded by
|
|
5
|
+
* `PASSWORD_HASH.LIMITS.MAX_PBKDF2_ITERATIONS` and `keyLength` by
|
|
6
|
+
* `PASSWORD_HASH.LIMITS.MAX_DERIVED_KEY_BYTES`, so a work factor read from
|
|
7
|
+
* configuration cannot request unbounded CPU time.
|
|
3
8
|
*/
|
|
4
9
|
export declare function validatePbkdf2Options(iterations: number, keyLength: number, salt: Uint8Array, digest?: unknown): void;
|
|
5
10
|
/**
|
|
6
11
|
* Validates scrypt key derivation options.
|
|
12
|
+
*
|
|
13
|
+
* `cost`, `blockSize` and `parallelization` are bounded by
|
|
14
|
+
* `PASSWORD_HASH.LIMITS`, and `128 * cost * blockSize` (the scrypt working
|
|
15
|
+
* memory) by `MAX_SCRYPT_MEMORY_BYTES`. Without these ceilings the memory
|
|
16
|
+
* "bound" passed to the provider was derived from the very parameters it
|
|
17
|
+
* was meant to bound, so a cost of 2^30 allocated terabytes.
|
|
7
18
|
*/
|
|
8
19
|
export declare function validateScryptOptions(keyLength: number, cost: number, blockSize: number, parallelization: number, salt: Uint8Array, maxMemory?: number): void;
|
|
9
20
|
//# sourceMappingURL=cryptoKeyDerivation.validate.d.ts.map
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { PASSWORD_HASH } from "../cryptoConstants/cryptoConstants.security.js";
|
|
2
2
|
import { isPbkdf2Digest } from "../cryptoProvider/cryptoProvider.type.js";
|
|
3
|
+
const LIMITS = PASSWORD_HASH.LIMITS;
|
|
3
4
|
/**
|
|
4
5
|
* Validates PBKDF2 key derivation options.
|
|
6
|
+
*
|
|
7
|
+
* Both floors and ceilings are enforced: `iterations` is bounded by
|
|
8
|
+
* `PASSWORD_HASH.LIMITS.MAX_PBKDF2_ITERATIONS` and `keyLength` by
|
|
9
|
+
* `PASSWORD_HASH.LIMITS.MAX_DERIVED_KEY_BYTES`, so a work factor read from
|
|
10
|
+
* configuration cannot request unbounded CPU time.
|
|
5
11
|
*/
|
|
6
12
|
export function validatePbkdf2Options(iterations, keyLength, salt, digest = "sha256") {
|
|
7
13
|
if (!Number.isInteger(iterations) ||
|
|
8
|
-
iterations < PASSWORD_HASH.PBKDF2.MIN_ITERATIONS
|
|
9
|
-
|
|
14
|
+
iterations < PASSWORD_HASH.PBKDF2.MIN_ITERATIONS ||
|
|
15
|
+
iterations > LIMITS.MAX_PBKDF2_ITERATIONS) {
|
|
16
|
+
throw new RangeError(`PBKDF2 iterations must be an integer between ${PASSWORD_HASH.PBKDF2.MIN_ITERATIONS} and ${LIMITS.MAX_PBKDF2_ITERATIONS}.`);
|
|
10
17
|
}
|
|
11
|
-
if (!Number.isInteger(keyLength) ||
|
|
12
|
-
|
|
18
|
+
if (!Number.isInteger(keyLength) ||
|
|
19
|
+
keyLength < 16 ||
|
|
20
|
+
keyLength > LIMITS.MAX_DERIVED_KEY_BYTES) {
|
|
21
|
+
throw new RangeError(`PBKDF2 keyLength must be an integer between 16 and ${LIMITS.MAX_DERIVED_KEY_BYTES} bytes.`);
|
|
13
22
|
}
|
|
14
23
|
if (!(salt instanceof Uint8Array) || salt.byteLength < 16) {
|
|
15
24
|
throw new RangeError("PBKDF2 salt must be at least 16 bytes.");
|
|
@@ -20,19 +29,37 @@ export function validatePbkdf2Options(iterations, keyLength, salt, digest = "sha
|
|
|
20
29
|
}
|
|
21
30
|
/**
|
|
22
31
|
* Validates scrypt key derivation options.
|
|
32
|
+
*
|
|
33
|
+
* `cost`, `blockSize` and `parallelization` are bounded by
|
|
34
|
+
* `PASSWORD_HASH.LIMITS`, and `128 * cost * blockSize` (the scrypt working
|
|
35
|
+
* memory) by `MAX_SCRYPT_MEMORY_BYTES`. Without these ceilings the memory
|
|
36
|
+
* "bound" passed to the provider was derived from the very parameters it
|
|
37
|
+
* was meant to bound, so a cost of 2^30 allocated terabytes.
|
|
23
38
|
*/
|
|
24
39
|
export function validateScryptOptions(keyLength, cost, blockSize, parallelization, salt, maxMemory) {
|
|
25
|
-
if (!Number.isInteger(keyLength) ||
|
|
26
|
-
|
|
40
|
+
if (!Number.isInteger(keyLength) ||
|
|
41
|
+
keyLength < 16 ||
|
|
42
|
+
keyLength > LIMITS.MAX_DERIVED_KEY_BYTES) {
|
|
43
|
+
throw new RangeError(`scrypt keyLength must be an integer between 16 and ${LIMITS.MAX_DERIVED_KEY_BYTES} bytes.`);
|
|
27
44
|
}
|
|
28
|
-
if (!Number.isInteger(cost) ||
|
|
29
|
-
|
|
45
|
+
if (!Number.isInteger(cost) ||
|
|
46
|
+
cost < 2 ||
|
|
47
|
+
cost > LIMITS.MAX_SCRYPT_COST ||
|
|
48
|
+
(cost & (cost - 1)) !== 0) {
|
|
49
|
+
throw new RangeError(`scrypt cost must be a power of two between 2 and ${LIMITS.MAX_SCRYPT_COST}.`);
|
|
30
50
|
}
|
|
31
|
-
if (!Number.isInteger(blockSize) ||
|
|
32
|
-
|
|
51
|
+
if (!Number.isInteger(blockSize) ||
|
|
52
|
+
blockSize <= 0 ||
|
|
53
|
+
blockSize > LIMITS.MAX_SCRYPT_BLOCK_SIZE) {
|
|
54
|
+
throw new RangeError(`scrypt blockSize must be an integer between 1 and ${LIMITS.MAX_SCRYPT_BLOCK_SIZE}.`);
|
|
33
55
|
}
|
|
34
|
-
if (!Number.isInteger(parallelization) ||
|
|
35
|
-
|
|
56
|
+
if (!Number.isInteger(parallelization) ||
|
|
57
|
+
parallelization <= 0 ||
|
|
58
|
+
parallelization > LIMITS.MAX_SCRYPT_PARALLELIZATION) {
|
|
59
|
+
throw new RangeError(`scrypt parallelization must be an integer between 1 and ${LIMITS.MAX_SCRYPT_PARALLELIZATION}.`);
|
|
60
|
+
}
|
|
61
|
+
if (128 * cost * blockSize > LIMITS.MAX_SCRYPT_MEMORY_BYTES) {
|
|
62
|
+
throw new RangeError(`scrypt cost * blockSize exceeds the memory bound of ${LIMITS.MAX_SCRYPT_MEMORY_BYTES} bytes.`);
|
|
36
63
|
}
|
|
37
64
|
if (!(salt instanceof Uint8Array) || salt.byteLength < 16) {
|
|
38
65
|
throw new RangeError("scrypt salt must be at least 16 bytes.");
|
|
@@ -2,10 +2,20 @@ import { isPbkdf2Digest } from "../../../cryptoProvider/cryptoProvider.type.js";
|
|
|
2
2
|
import { pbkdf2, scrypt } from "node:crypto";
|
|
3
3
|
import { toBytes } from "../nodeCryptoProvider.helper.js";
|
|
4
4
|
import { keyDerivationError } from "../../../cryptoErrors/cryptoErrors.helper.js";
|
|
5
|
+
import { PASSWORD_HASH } from "../../../cryptoConstants/cryptoConstants.security.js";
|
|
5
6
|
/** Hard floors applied at the provider boundary. */
|
|
6
7
|
const PROVIDER_MIN_SALT_BYTES = 16;
|
|
7
8
|
const PROVIDER_MIN_KEY_BYTES = 16;
|
|
8
9
|
const PROVIDER_MIN_PBKDF2_ITERATIONS = 1_000;
|
|
10
|
+
/**
|
|
11
|
+
* Hard ceilings applied at the provider boundary.
|
|
12
|
+
*
|
|
13
|
+
* The provider is reachable directly (`provider.deriveKey`) and through
|
|
14
|
+
* custom wrappers, so the same `PASSWORD_HASH.LIMITS` that bound stored
|
|
15
|
+
* password hashes are enforced here too. Without them the scrypt memory
|
|
16
|
+
* bound was computed from the requested cost, which is no bound at all.
|
|
17
|
+
*/
|
|
18
|
+
const PROVIDER_LIMITS = PASSWORD_HASH.LIMITS;
|
|
9
19
|
const DEFAULT_PBKDF2_ITERATIONS = 600_000;
|
|
10
20
|
const DEFAULT_SCRYPT_COST = 16_384;
|
|
11
21
|
const DEFAULT_SCRYPT_BLOCK_SIZE = 8;
|
|
@@ -27,8 +37,10 @@ function validateCommon(options, algorithm) {
|
|
|
27
37
|
if (!(salt instanceof Uint8Array) || salt.byteLength < PROVIDER_MIN_SALT_BYTES) {
|
|
28
38
|
throw keyDerivationError(`Salt must be at least ${PROVIDER_MIN_SALT_BYTES} bytes.`, algorithm);
|
|
29
39
|
}
|
|
30
|
-
if (!Number.isInteger(keyLength) ||
|
|
31
|
-
|
|
40
|
+
if (!Number.isInteger(keyLength) ||
|
|
41
|
+
keyLength < PROVIDER_MIN_KEY_BYTES ||
|
|
42
|
+
keyLength > PROVIDER_LIMITS.MAX_DERIVED_KEY_BYTES) {
|
|
43
|
+
throw keyDerivationError(`keyLength must be an integer between ${PROVIDER_MIN_KEY_BYTES} and ${PROVIDER_LIMITS.MAX_DERIVED_KEY_BYTES}.`, algorithm);
|
|
32
44
|
}
|
|
33
45
|
return { password, salt, keyLength };
|
|
34
46
|
}
|
|
@@ -39,8 +51,9 @@ export async function deriveKey(options) {
|
|
|
39
51
|
const iterations = options.iterations ?? DEFAULT_PBKDF2_ITERATIONS;
|
|
40
52
|
const digest = options.digest ?? "sha256";
|
|
41
53
|
if (!Number.isInteger(iterations) ||
|
|
42
|
-
iterations < PROVIDER_MIN_PBKDF2_ITERATIONS
|
|
43
|
-
|
|
54
|
+
iterations < PROVIDER_MIN_PBKDF2_ITERATIONS ||
|
|
55
|
+
iterations > PROVIDER_LIMITS.MAX_PBKDF2_ITERATIONS) {
|
|
56
|
+
throw keyDerivationError(`PBKDF2 iterations must be an integer between ${PROVIDER_MIN_PBKDF2_ITERATIONS} and ${PROVIDER_LIMITS.MAX_PBKDF2_ITERATIONS}.`, "pbkdf2");
|
|
44
57
|
}
|
|
45
58
|
if (!isPbkdf2Digest(digest)) {
|
|
46
59
|
throw keyDerivationError(`Unsupported PBKDF2 digest: ${String(digest)}.`, "pbkdf2");
|
|
@@ -66,14 +79,24 @@ export async function deriveKey(options) {
|
|
|
66
79
|
const N = options.memoryCost ?? DEFAULT_SCRYPT_COST;
|
|
67
80
|
const r = options.blockSize ?? DEFAULT_SCRYPT_BLOCK_SIZE;
|
|
68
81
|
const p = options.parallelism ?? DEFAULT_SCRYPT_PARALLELISM;
|
|
69
|
-
if (!Number.isInteger(N) ||
|
|
70
|
-
|
|
82
|
+
if (!Number.isInteger(N) ||
|
|
83
|
+
N < 2 ||
|
|
84
|
+
N > PROVIDER_LIMITS.MAX_SCRYPT_COST ||
|
|
85
|
+
(N & (N - 1)) !== 0) {
|
|
86
|
+
throw keyDerivationError(`scrypt cost must be a power of two between 2 and ${PROVIDER_LIMITS.MAX_SCRYPT_COST}.`, "scrypt");
|
|
87
|
+
}
|
|
88
|
+
if (!Number.isInteger(r) ||
|
|
89
|
+
r <= 0 ||
|
|
90
|
+
r > PROVIDER_LIMITS.MAX_SCRYPT_BLOCK_SIZE) {
|
|
91
|
+
throw keyDerivationError(`scrypt blockSize must be an integer between 1 and ${PROVIDER_LIMITS.MAX_SCRYPT_BLOCK_SIZE}.`, "scrypt");
|
|
71
92
|
}
|
|
72
|
-
if (!Number.isInteger(
|
|
73
|
-
|
|
93
|
+
if (!Number.isInteger(p) ||
|
|
94
|
+
p <= 0 ||
|
|
95
|
+
p > PROVIDER_LIMITS.MAX_SCRYPT_PARALLELIZATION) {
|
|
96
|
+
throw keyDerivationError(`scrypt parallelism must be an integer between 1 and ${PROVIDER_LIMITS.MAX_SCRYPT_PARALLELIZATION}.`, "scrypt");
|
|
74
97
|
}
|
|
75
|
-
if (
|
|
76
|
-
throw keyDerivationError(
|
|
98
|
+
if (128 * N * r > PROVIDER_LIMITS.MAX_SCRYPT_MEMORY_BYTES) {
|
|
99
|
+
throw keyDerivationError(`scrypt cost * blockSize exceeds the memory bound of ${PROVIDER_LIMITS.MAX_SCRYPT_MEMORY_BYTES} bytes.`, "scrypt");
|
|
77
100
|
}
|
|
78
101
|
const maxmem = options.maxMemory ?? defaultScryptMaxMemory(N, r, p);
|
|
79
102
|
if (!Number.isInteger(maxmem) || maxmem <= 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/crypto",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Cryptographic primitives for hashing, encryption, tokens, and secure random generation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,10 +23,14 @@
|
|
|
23
23
|
"node": ">=24.0.0"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@zudojs/constants": "1.0.
|
|
27
|
-
"@zudojs/errors": "1.0.
|
|
26
|
+
"@zudojs/constants": "1.0.1",
|
|
27
|
+
"@zudojs/errors": "1.0.1"
|
|
28
28
|
},
|
|
29
29
|
"license": "MIT",
|
|
30
|
+
"author": {
|
|
31
|
+
"name": "Oluwayemi Oyinlola",
|
|
32
|
+
"url": "https://github.com/oyinlola-tech"
|
|
33
|
+
},
|
|
30
34
|
"devDependencies": {
|
|
31
35
|
"typescript": "7.0.2",
|
|
32
36
|
"vitest": "^4.1.11"
|