@zudojs/crypto 1.0.0 → 1.2.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
@@ -2,6 +2,12 @@
2
2
 
3
3
  Cryptographic primitives for hashing, authenticated encryption, password hashing, digital signatures, key derivation, opaque tokens, and secure random generation. Everything is backed by `node:crypto` through a swappable provider.
4
4
 
5
+ <!-- zudo-docs:start -->
6
+
7
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-crypto](https://zudojs.oyinlola.site/docs/packages-crypto) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-crypto.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
8
+
9
+ <!-- zudo-docs:end -->
10
+
5
11
  ## Installation
6
12
 
7
13
  ```bash
@@ -42,8 +48,8 @@ const sessionToken = await generateToken({ bytes: 32, prefix: "sess_" });
42
48
 
43
49
  - Hashing: SHA-256/384/512 and SHA3-256/384/512, HMAC (keys of at least 16 bytes)
44
50
  - Authenticated encryption: AES-256-GCM with strict IV (12 bytes) and tag (16 bytes) validation, plus a versioned string envelope
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)
51
+ - Password hashing: scrypt (default N=2^14, r=8, p=5 — the OWASP row for that N) and PBKDF2-HMAC (provider level), versioned self-describing encoding, bounded parameters on verification. New hashes must use a cost of at least `PASSWORD_HASH.SCRYPT.MIN_COST` (16 384); older stored hashes with a smaller cost still verify. Key derivation (`deriveScrypt`) keeps p=1 by default, so derived keys do not change
52
+ - 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
53
  - Digital signatures: Ed25519, RSA-SHA256/384/512, ECDSA-SHA256/384/512; the algorithm label is bound to the key type
48
54
  - Secure random: unbiased integers up to 2^48, UUID v4, bytes, alphabets, numeric codes
49
55
  - Opaque tokens (API keys, sessions, refresh, CSRF, OTP) with SHA-256 storage hashes
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Password hashing constants.
3
3
  *
4
- * Defaults follow current OWASP Password Storage Cheat Sheet guidance:
5
- * scrypt N=2^14, r=8, p=1 (minimum) and PBKDF2-HMAC-SHA256 with 600 000
6
- * iterations. `LIMITS` bounds parameters that are read back from stored
7
- * hashes so that a tampered or foreign record cannot force unbounded work.
4
+ * Defaults follow the OWASP Password Storage Cheat Sheet: scrypt N=2^14,
5
+ * r=8, p=5 (OWASP's N=2^14 row; p=1 is only adequate at N=2^17) and
6
+ * PBKDF2-HMAC-SHA256 with 600 000 iterations. `LIMITS` bounds parameters
7
+ * that are read back from stored hashes so that a tampered or foreign
8
+ * record cannot force unbounded work.
9
+ *
10
+ * New hashes are held to `SCRYPT.MIN_COST`; verification still accepts
11
+ * older stored hashes down to `LIMITS.MIN_SCRYPT_COST`.
8
12
  */
9
13
  export declare const PASSWORD_HASH: Readonly<{
10
14
  readonly SALT_BYTES: 16;
@@ -12,7 +16,15 @@ export declare const PASSWORD_HASH: Readonly<{
12
16
  readonly SCRYPT: Readonly<{
13
17
  COST: 16384;
14
18
  BLOCK_SIZE: 8;
19
+ /**
20
+ * Default `p` for key derivation (`deriveScrypt`). Left at 1: changing
21
+ * it would silently change every key derived with default options.
22
+ */
15
23
  PARALLELIZATION: 1;
24
+ /** Default `p` for new *password hashes* (OWASP N=2^14, r=8, p=5). */
25
+ PASSWORD_PARALLELIZATION: 5;
26
+ /** Floor on the cost of *new* hashes; `LIMITS.MIN_SCRYPT_COST` bounds verification. */
27
+ MIN_COST: 16384;
16
28
  }>;
17
29
  readonly PBKDF2: Readonly<{
18
30
  ITERATIONS: 600000;
@@ -41,6 +53,14 @@ export declare const PASSWORD_HASH: Readonly<{
41
53
  */
42
54
  MAX_SCRYPT_MEMORY_BYTES: number;
43
55
  MAX_PBKDF2_ITERATIONS: 10000000;
56
+ /**
57
+ * Upper bound on the output length of `derivePbkdf2` / `deriveScrypt`
58
+ * (and the provider's `deriveKey`). PBKDF2 cost scales linearly with
59
+ * the number of output blocks, so an unbounded `keyLength` multiplies
60
+ * the iteration count by an attacker-chosen factor. Password hashes
61
+ * are bounded separately by `MAX_KEY_BYTES`.
62
+ */
63
+ MAX_DERIVED_KEY_BYTES: 1024;
44
64
  }>;
45
65
  }>;
46
66
  /**
@@ -2,10 +2,14 @@ import { TimeMs } from "@zudojs/constants";
2
2
  /**
3
3
  * Password hashing constants.
4
4
  *
5
- * Defaults follow current OWASP Password Storage Cheat Sheet guidance:
6
- * scrypt N=2^14, r=8, p=1 (minimum) and PBKDF2-HMAC-SHA256 with 600 000
7
- * iterations. `LIMITS` bounds parameters that are read back from stored
8
- * hashes so that a tampered or foreign record cannot force unbounded work.
5
+ * Defaults follow the OWASP Password Storage Cheat Sheet: scrypt N=2^14,
6
+ * r=8, p=5 (OWASP's N=2^14 row; p=1 is only adequate at N=2^17) and
7
+ * PBKDF2-HMAC-SHA256 with 600 000 iterations. `LIMITS` bounds parameters
8
+ * that are read back from stored hashes so that a tampered or foreign
9
+ * record cannot force unbounded work.
10
+ *
11
+ * New hashes are held to `SCRYPT.MIN_COST`; verification still accepts
12
+ * older stored hashes down to `LIMITS.MIN_SCRYPT_COST`.
9
13
  */
10
14
  export const PASSWORD_HASH = Object.freeze({
11
15
  SALT_BYTES: 16,
@@ -13,7 +17,15 @@ export const PASSWORD_HASH = Object.freeze({
13
17
  SCRYPT: Object.freeze({
14
18
  COST: 16_384,
15
19
  BLOCK_SIZE: 8,
20
+ /**
21
+ * Default `p` for key derivation (`deriveScrypt`). Left at 1: changing
22
+ * it would silently change every key derived with default options.
23
+ */
16
24
  PARALLELIZATION: 1,
25
+ /** Default `p` for new *password hashes* (OWASP N=2^14, r=8, p=5). */
26
+ PASSWORD_PARALLELIZATION: 5,
27
+ /** Floor on the cost of *new* hashes; `LIMITS.MIN_SCRYPT_COST` bounds verification. */
28
+ MIN_COST: 16_384,
17
29
  }),
18
30
  PBKDF2: Object.freeze({
19
31
  ITERATIONS: 600_000,
@@ -42,6 +54,14 @@ export const PASSWORD_HASH = Object.freeze({
42
54
  */
43
55
  MAX_SCRYPT_MEMORY_BYTES: 1024 * 1024 * 1024,
44
56
  MAX_PBKDF2_ITERATIONS: 10_000_000,
57
+ /**
58
+ * Upper bound on the output length of `derivePbkdf2` / `deriveScrypt`
59
+ * (and the provider's `deriveKey`). PBKDF2 cost scales linearly with
60
+ * the number of output blocks, so an unbounded `keyLength` multiplies
61
+ * the iteration count by an attacker-chosen factor. Password hashes
62
+ * are bounded separately by `MAX_KEY_BYTES`.
63
+ */
64
+ MAX_DERIVED_KEY_BYTES: 1024,
45
65
  }),
46
66
  });
47
67
  /**
@@ -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.");
@@ -3,6 +3,7 @@ import { CryptoAlgorithm } from "../cryptoConstants/cryptoConstants.type.js";
3
3
  import { PASSWORD_HASH } from "../cryptoConstants/cryptoConstants.security.js";
4
4
  import { assertPassword, validateParameters, } from "./cryptoPassword.validate.js";
5
5
  import { decodePasswordHash, PASSWORD_FORMAT_VERSION, } from "./cryptoPassword.codec.js";
6
+ import { assertNewHashCost } from "./cryptoPassword.helper.js";
6
7
  /**
7
8
  * Hashes a password using scrypt.
8
9
  *
@@ -16,7 +17,8 @@ export async function hashPassword(password, options = {}) {
16
17
  const keyBytes = options.keyBytes ?? PASSWORD_HASH.KEY_BYTES;
17
18
  const cost = options.cost ?? PASSWORD_HASH.SCRYPT.COST;
18
19
  const blockSize = options.blockSize ?? PASSWORD_HASH.SCRYPT.BLOCK_SIZE;
19
- const parallelization = options.parallelization ?? PASSWORD_HASH.SCRYPT.PARALLELIZATION;
20
+ const parallelization = options.parallelization ?? PASSWORD_HASH.SCRYPT.PASSWORD_PARALLELIZATION;
21
+ assertNewHashCost(cost);
20
22
  validateParameters({
21
23
  saltBytes,
22
24
  keyBytes,
@@ -11,4 +11,15 @@ export declare function isPasswordHash(encoded: string): boolean;
11
11
  * Returns whether a password meets the basic requirements.
12
12
  */
13
13
  export declare function isValidPassword(password: string, minimumLength?: number): boolean;
14
+ /**
15
+ * Refuses an scrypt cost below `PASSWORD_HASH.SCRYPT.MIN_COST` for a *new*
16
+ * password hash.
17
+ *
18
+ * `LIMITS.MIN_SCRYPT_COST` (2) only bounds what verification accepts, so
19
+ * older stored hashes stay verifiable; minting a new hash with `cost: 2`
20
+ * produced a hash that is effectively free to crack.
21
+ *
22
+ * @throws {RangeError} when `cost` is below the floor.
23
+ */
24
+ export declare function assertNewHashCost(cost: number): void;
14
25
  //# sourceMappingURL=cryptoPassword.helper.d.ts.map
@@ -10,7 +10,7 @@ export function getDefaultPasswordHashOptions() {
10
10
  keyBytes: PASSWORD_HASH.KEY_BYTES,
11
11
  cost: PASSWORD_HASH.SCRYPT.COST,
12
12
  blockSize: PASSWORD_HASH.SCRYPT.BLOCK_SIZE,
13
- parallelization: PASSWORD_HASH.SCRYPT.PARALLELIZATION,
13
+ parallelization: PASSWORD_HASH.SCRYPT.PASSWORD_PARALLELIZATION,
14
14
  };
15
15
  }
16
16
  /**
@@ -31,4 +31,19 @@ export function isPasswordHash(encoded) {
31
31
  export function isValidPassword(password, minimumLength = PASSWORD_MINIMUM_DEFAULT_LENGTH) {
32
32
  return typeof password === "string" && password.length >= minimumLength;
33
33
  }
34
+ /**
35
+ * Refuses an scrypt cost below `PASSWORD_HASH.SCRYPT.MIN_COST` for a *new*
36
+ * password hash.
37
+ *
38
+ * `LIMITS.MIN_SCRYPT_COST` (2) only bounds what verification accepts, so
39
+ * older stored hashes stay verifiable; minting a new hash with `cost: 2`
40
+ * produced a hash that is effectively free to crack.
41
+ *
42
+ * @throws {RangeError} when `cost` is below the floor.
43
+ */
44
+ export function assertNewHashCost(cost) {
45
+ if (typeof cost === "number" && cost < PASSWORD_HASH.SCRYPT.MIN_COST) {
46
+ throw new RangeError(`scrypt cost for a new password hash must be at least ${PASSWORD_HASH.SCRYPT.MIN_COST}.`);
47
+ }
48
+ }
34
49
  //# sourceMappingURL=cryptoPassword.helper.js.map
@@ -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) {
@@ -3,9 +3,10 @@ import type { CryptoInput, PasswordHashProviderOptions } from "../../../cryptoPr
3
3
  * Hashes a password with scrypt (default) or PBKDF2 and returns the
4
4
  * self-describing, versioned encoding produced by `encodePasswordHash`.
5
5
  *
6
- * The encoded string is validated against `PASSWORD_HASH.LIMITS`, so the
7
- * provider refuses insecure parameters (short salts, tiny work factors)
8
- * instead of producing a hash that can never be verified.
6
+ * The encoded string is validated against `PASSWORD_HASH.LIMITS`, and a
7
+ * scrypt cost below `PASSWORD_HASH.SCRYPT.MIN_COST` (or PBKDF2 iterations
8
+ * below `PBKDF2.MIN_ITERATIONS`) is refused, so the provider does not mint
9
+ * hashes with short salts or tiny work factors.
9
10
  */
10
11
  export declare function hashPassword(password: CryptoInput, options?: PasswordHashProviderOptions): Promise<string>;
11
12
  /**
@@ -4,15 +4,17 @@ import { timingSafeEqual } from "../../../compare/compare.helper.js";
4
4
  import { CryptoAlgorithm } from "../../../cryptoConstants/cryptoConstants.type.js";
5
5
  import { PASSWORD_HASH } from "../../../cryptoConstants/cryptoConstants.security.js";
6
6
  import { PASSWORD_FORMAT_VERSION, decodePasswordHash, encodePasswordHash, pbkdf2PasswordAlgorithm, } from "../../../cryptoPassword/cryptoPassword.codec.js";
7
+ import { assertNewHashCost } from "../../../cryptoPassword/cryptoPassword.helper.js";
7
8
  import { deriveKey } from "./nodeCryptoProvider.derivation.js";
8
9
  import { keyDerivationError } from "../../../cryptoErrors/cryptoErrors.helper.js";
9
10
  /**
10
11
  * Hashes a password with scrypt (default) or PBKDF2 and returns the
11
12
  * self-describing, versioned encoding produced by `encodePasswordHash`.
12
13
  *
13
- * The encoded string is validated against `PASSWORD_HASH.LIMITS`, so the
14
- * provider refuses insecure parameters (short salts, tiny work factors)
15
- * instead of producing a hash that can never be verified.
14
+ * The encoded string is validated against `PASSWORD_HASH.LIMITS`, and a
15
+ * scrypt cost below `PASSWORD_HASH.SCRYPT.MIN_COST` (or PBKDF2 iterations
16
+ * below `PBKDF2.MIN_ITERATIONS`) is refused, so the provider does not mint
17
+ * hashes with short salts or tiny work factors.
16
18
  */
17
19
  export async function hashPassword(password, options) {
18
20
  const algorithm = options?.algorithm ?? "scrypt";
@@ -23,7 +25,8 @@ export async function hashPassword(password, options) {
23
25
  case "scrypt": {
24
26
  const cost = options?.memoryCost ?? PASSWORD_HASH.SCRYPT.COST;
25
27
  const blockSize = options?.blockSize ?? PASSWORD_HASH.SCRYPT.BLOCK_SIZE;
26
- const parallelization = options?.parallelism ?? PASSWORD_HASH.SCRYPT.PARALLELIZATION;
28
+ const parallelization = options?.parallelism ?? PASSWORD_HASH.SCRYPT.PASSWORD_PARALLELIZATION;
29
+ assertNewHashCost(cost);
27
30
  // Validate before deriving so invalid parameters fail fast.
28
31
  const placeholder = new Uint8Array(keyBytes);
29
32
  encodePasswordHash({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/crypto",
3
- "version": "1.0.0",
3
+ "version": "1.2.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.1.0",
27
+ "@zudojs/errors": "1.1.0"
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"