@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 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
- throw new RangeError(`PBKDF2 iterations must be at least ${PASSWORD_HASH.PBKDF2.MIN_ITERATIONS}.`);
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) || keyLength < 16) {
12
- throw new RangeError("PBKDF2 keyLength must be at least 16 bytes.");
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) || keyLength < 16) {
26
- throw new RangeError("scrypt keyLength must be at least 16 bytes.");
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) || cost < 2 || (cost & (cost - 1)) !== 0) {
29
- throw new RangeError("scrypt cost must be a power of two greater than or equal to 2.");
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) || blockSize <= 0) {
32
- throw new RangeError("scrypt blockSize must be a positive integer.");
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) || parallelization <= 0) {
35
- throw new RangeError("scrypt parallelization must be a positive integer.");
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) || keyLength < PROVIDER_MIN_KEY_BYTES) {
31
- throw keyDerivationError(`keyLength must be an integer of at least ${PROVIDER_MIN_KEY_BYTES}.`, algorithm);
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
- throw keyDerivationError(`PBKDF2 iterations must be an integer of at least ${PROVIDER_MIN_PBKDF2_ITERATIONS}.`, "pbkdf2");
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) || N < 2 || (N & (N - 1)) !== 0) {
70
- throw keyDerivationError("scrypt cost must be a power of two greater than or equal to 2.", "scrypt");
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(r) || r <= 0) {
73
- throw keyDerivationError("scrypt blockSize must be a positive integer.", "scrypt");
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 (!Number.isInteger(p) || p <= 0) {
76
- throw keyDerivationError("scrypt parallelism must be a positive integer.", "scrypt");
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.0.0",
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.0",
27
- "@zudojs/errors": "1.0.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"