Initial Commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
type ByteSource,
|
||||
defaultByteSource,
|
||||
type RandomBytesOptions,
|
||||
randomBytes,
|
||||
} from "./random-bytes/random-bytes.js";
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
type ByteSource,
|
||||
defaultByteSource,
|
||||
type RandomBytesOptions,
|
||||
randomBytes,
|
||||
} from "./random-bytes";
|
||||
@@ -0,0 +1,57 @@
|
||||
# randomBytes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Produce cryptographically-secure random bytes. Every `@suaveplan/crypto-*` package routes entropy through this single helper so there is exactly one callsite of `globalThis.crypto.getRandomValues` to audit, and callers that need deterministic entropy in tests (or a hardware-RNG adapter in production) can swap it with dependency injection rather than monkey-patching `globalThis`.
|
||||
|
||||
## Features
|
||||
|
||||
- Wraps `globalThis.crypto.getRandomValues` (Web Crypto) — works in Node 20+, Bun, browsers, Deno, Cloudflare Workers.
|
||||
- Dependency-injectable `ByteSource` for tests and hardware RNG adapters.
|
||||
- Explicit `n` range check; throws stable `CRYPTO.UTILS.RNG.INVALID_LENGTH` on bad input.
|
||||
- Returns a fresh `Uint8Array` every call.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomBytes } from "@suaveplan/crypto-utils/random";
|
||||
|
||||
const nonce = randomBytes(12);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { randomBytes, type ByteSource } from "@suaveplan/crypto-utils/random";
|
||||
|
||||
// Deterministic source for tests — feeds a known sequence.
|
||||
function fixedSequence(bytes: Uint8Array): ByteSource {
|
||||
let pos = 0;
|
||||
return (n) => {
|
||||
const out = bytes.slice(pos, pos + n);
|
||||
pos += n;
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
function generateKey(opts?: { byteSource?: ByteSource }): Uint8Array {
|
||||
return randomBytes(32, opts);
|
||||
}
|
||||
|
||||
const testKey = generateKey({
|
||||
byteSource: fixedSequence(new Uint8Array(32).fill(0xaa)),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomBytes(n: number, options?: { byteSource?: ByteSource }): Uint8Array` — returns `n` random bytes. Throws `CryptoUtilsError(CRYPTO.UTILS.RNG.INVALID_LENGTH)` if `n` is not a non-negative integer.
|
||||
- `defaultByteSource: ByteSource` — the `getRandomValues`-backed source. Exported for adapters that want to delegate to it.
|
||||
- `type ByteSource = (n: number) => Uint8Array` — the injection surface.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`globalThis.crypto` is a WHATWG Web Crypto `Crypto` instance on every supported runtime. In Node we rely on the polyfill injected at test-setup time (`node:crypto` `webcrypto`). The test-only `byteSource` option is deliberately on the call, not a module-level setter, so there is never a case where a test leaks a deterministic source into production code. The length check rejects `NaN`, non-integers, and negatives — all would previously have been forwarded to `getRandomValues`, which throws a less-precise `RangeError`.
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { CryptoUtilsError } from "../../errors/crypto-utils-error/crypto-utils-error.js";
|
||||
import { defaultByteSource, randomBytes } from "./random-bytes.js";
|
||||
|
||||
describe("randomBytes", () => {
|
||||
it("returns requested length", { timeout: 5000 }, () => {
|
||||
for (const n of [0, 1, 16, 32, 4096]) {
|
||||
expect(randomBytes(n).length).toBe(n);
|
||||
}
|
||||
});
|
||||
|
||||
it("produces different output on consecutive calls", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const a = randomBytes(32);
|
||||
const b = randomBytes(32);
|
||||
expect(a).not.toEqual(b);
|
||||
});
|
||||
|
||||
it("accepts a deterministic byteSource", { timeout: 5000 }, () => {
|
||||
let counter = 0;
|
||||
const stub = (n: number): Uint8Array => {
|
||||
const out = new Uint8Array(n);
|
||||
for (let i = 0; i < n; i++) out[i] = counter++;
|
||||
return out;
|
||||
};
|
||||
expect(randomBytes(4, { byteSource: stub })).toEqual(
|
||||
new Uint8Array([0, 1, 2, 3])
|
||||
);
|
||||
expect(randomBytes(2, { byteSource: stub })).toEqual(
|
||||
new Uint8Array([4, 5])
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects non-integer and negative lengths", { timeout: 5000 }, () => {
|
||||
expect(() => randomBytes(-1)).toThrow(CryptoUtilsError);
|
||||
expect(() => randomBytes(1.5)).toThrow(CryptoUtilsError);
|
||||
expect(() => randomBytes(Number.NaN)).toThrow(CryptoUtilsError);
|
||||
});
|
||||
|
||||
it("defaultByteSource returns the right length", { timeout: 5000 }, () => {
|
||||
expect(defaultByteSource(16).length).toBe(16);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { CRYPTO_UTILS_CODES } from "../../errors/codes/codes.js";
|
||||
import { CryptoUtilsError } from "../../errors/crypto-utils-error/crypto-utils-error.js";
|
||||
|
||||
/**
|
||||
* A function that produces `n` cryptographically-random bytes.
|
||||
*/
|
||||
export type ByteSource = (n: number) => Uint8Array;
|
||||
|
||||
/**
|
||||
* The single callsite for `crypto.getRandomValues` in this package. Every
|
||||
* consumer that needs entropy MUST call `randomBytes` or accept a
|
||||
* `ByteSource` — never call `crypto.getRandomValues` directly.
|
||||
*/
|
||||
export const defaultByteSource: ByteSource = (n) => {
|
||||
const out = new Uint8Array(n);
|
||||
crypto.getRandomValues(out);
|
||||
return out;
|
||||
};
|
||||
|
||||
export interface RandomBytesOptions {
|
||||
readonly byteSource?: ByteSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `n` cryptographically-random bytes.
|
||||
*
|
||||
* The default source is `globalThis.crypto.getRandomValues`. Pass
|
||||
* `{ byteSource }` to inject a deterministic or alternative source
|
||||
* (useful for tests and hardware RNG adapters).
|
||||
*
|
||||
* @throws CryptoUtilsError(CRYPTO.UTILS.RNG.INVALID_LENGTH) if `n` is not a
|
||||
* non-negative integer.
|
||||
*/
|
||||
export function randomBytes(
|
||||
n: number,
|
||||
options: RandomBytesOptions = {}
|
||||
): Uint8Array {
|
||||
if (!Number.isInteger(n) || n < 0) {
|
||||
throw new CryptoUtilsError(
|
||||
CRYPTO_UTILS_CODES["RNG.INVALID_LENGTH"],
|
||||
"error.crypto.utils.rng.invalid-length",
|
||||
{ context: { requested: n } }
|
||||
);
|
||||
}
|
||||
const source = options.byteSource ?? defaultByteSource;
|
||||
return source(n);
|
||||
}
|
||||
Reference in New Issue
Block a user