@zudojs/crypto 1.1.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,7 +48,7 @@ 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
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
46
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
@@ -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;
@@ -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,
@@ -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
@@ -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.1.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,8 +23,8 @@
23
23
  "node": ">=24.0.0"
24
24
  },
25
25
  "dependencies": {
26
- "@zudojs/constants": "1.0.1",
27
- "@zudojs/errors": "1.0.1"
26
+ "@zudojs/constants": "1.1.0",
27
+ "@zudojs/errors": "1.1.0"
28
28
  },
29
29
  "license": "MIT",
30
30
  "author": {