Skip to content

Commit ba56440

Browse files
committed
feat(entropy): add secure entropy generator with injectable provider
1 parent 7b70ecb commit ba56440

8 files changed

Lines changed: 127 additions & 0 deletions

File tree

src/DESIGN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- `src/bip39/` contains core BIP39 conversion functions.
77
- `src/errors/` defines standard error codes and priority ordering.
88
- `src/crypto/` wraps SHA-256 and PBKDF2-HMAC-SHA512 using standard libraries.
9+
- `src/normalize/` provides a compatibility input adapter (trim, NFKD, lowercase).
910
- `src/parser/` implements strict mnemonic parsing contracts.
11+
- `src/entropy/` generates entropy via secure randomness with injectable providers for tests.
1012
- `src/types/` defines shared DTOs such as `ValidationResult`.
1113
- `src/index.ts` re-exports the public surface for these foundational modules.

src/entropy/DESIGN.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# entropy design
2+
3+
- Purpose: Provide secure entropy generation with injectable providers for testing.
4+
- Scope: Validation of allowed byte lengths and delegation to a randomness provider.
5+
- Output: JavaScript is emitted to `dist/`; keep this directory TypeScript-only.

src/entropy/entropyGenerator.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { randomBytes } from "node:crypto";
2+
3+
import { ENTROPY_BYTES } from "../constants/bip39.js";
4+
5+
export class InvalidEntropyLengthError extends Error {
6+
constructor(message = "Invalid entropy length") {
7+
super(message);
8+
this.name = "InvalidEntropyLengthError";
9+
}
10+
}
11+
12+
export type EntropyGenerator = {
13+
generate: (bytes: number) => Uint8Array;
14+
};
15+
16+
const isValidEntropyLength = (bytes: number): boolean =>
17+
(ENTROPY_BYTES as readonly number[]).includes(bytes);
18+
19+
export const createEntropyGenerator = (
20+
provider: (bytes: number) => Uint8Array,
21+
): EntropyGenerator => ({
22+
generate: (bytes: number) => {
23+
if (!isValidEntropyLength(bytes)) {
24+
throw new InvalidEntropyLengthError(
25+
`Entropy must be ${ENTROPY_BYTES.join("/")} bytes`,
26+
);
27+
}
28+
const output = provider(bytes);
29+
if (!(output instanceof Uint8Array) || output.length !== bytes) {
30+
throw new Error("Entropy provider returned invalid output");
31+
}
32+
return output;
33+
},
34+
});
35+
36+
export const generateEntropy = (bytes: number): Uint8Array =>
37+
createEntropyGenerator(
38+
(length) => new Uint8Array(randomBytes(length)),
39+
).generate(bytes);

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export * from "./bip39/validateMnemonic.js";
55
export * from "./bits/bitOps.js";
66
export * from "./constants/bip39.js";
77
export * from "./crypto/crypto.js";
8+
export * from "./entropy/entropyGenerator.js";
89
export * from "./errors/errorCodes.js";
10+
export * from "./normalize/normalizeMnemonicInput.js";
911
export * from "./parser/strictMnemonic.js";
1012
export * from "./types/validationResult.js";

src/normalize/DESIGN.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# normalize design
2+
3+
- Purpose: Provide a compatibility adapter for mnemonic input.
4+
- Scope: Trim, whitespace normalization, NFKD normalization, and lowercase for English profile.
5+
- Output: JavaScript is emitted to `dist/`; keep this directory TypeScript-only.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const normalizeMnemonicInput = (input: string): string => {
2+
if (typeof input !== "string") {
3+
throw new TypeError("Mnemonic input must be a string");
4+
}
5+
6+
const replaced = input.replace(/[\t\n\r]/gu, " ");
7+
const trimmed = replaced.trim();
8+
if (trimmed.length === 0) {
9+
return "";
10+
}
11+
const collapsed = trimmed.replace(/\s+/gu, " ");
12+
const normalized = collapsed.normalize("NFKD");
13+
return normalized.toLowerCase();
14+
};

test/entropy-generator.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import {
5+
createEntropyGenerator,
6+
type EntropyGenerator,
7+
generateEntropy,
8+
InvalidEntropyLengthError,
9+
} from "../src/entropy/entropyGenerator.ts";
10+
11+
const allowed = [16, 20, 24, 28, 32];
12+
13+
test("generateEntropy returns allowed lengths", () => {
14+
for (const length of allowed) {
15+
const entropy = generateEntropy(length);
16+
assert.equal(entropy.length, length);
17+
}
18+
});
19+
20+
test("generateEntropy rejects invalid lengths", () => {
21+
assert.throws(() => generateEntropy(15), InvalidEntropyLengthError);
22+
assert.throws(() => generateEntropy(33), InvalidEntropyLengthError);
23+
});
24+
25+
test("EntropyGenerator allows deterministic output in tests", () => {
26+
const fixed = Uint8Array.from({ length: 16 }, (_, i) => i);
27+
const generator = createEntropyGenerator(() => fixed);
28+
const entropy = generator.generate(16);
29+
assert.equal(entropy.length, 16);
30+
assert.deepEqual(Array.from(entropy), Array.from(fixed));
31+
});
32+
33+
// Validate the interface shape used by callers
34+
const _typecheck: EntropyGenerator = {
35+
generate: (bytes) => new Uint8Array(bytes),
36+
};
37+
void _typecheck;

test/normalize-mnemonic.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import { normalizeMnemonicInput } from "../src/normalize/normalizeMnemonicInput.ts";
5+
6+
test("normalizeMnemonicInput trims and collapses whitespace", () => {
7+
const input = " Abandon\tabandon\nABOUT ";
8+
assert.equal(normalizeMnemonicInput(input), "abandon abandon about");
9+
});
10+
11+
test("normalizeMnemonicInput replaces CR and multiple spaces", () => {
12+
const input = "abandon\rabandon about";
13+
assert.equal(normalizeMnemonicInput(input), "abandon abandon about");
14+
});
15+
16+
test("normalizeMnemonicInput applies NFKD and lowercases", () => {
17+
const input = "\u00c1"; // Á
18+
assert.equal(normalizeMnemonicInput(input), "a\u0301");
19+
});
20+
21+
test("normalizeMnemonicInput rejects non-string input", () => {
22+
assert.throws(() => normalizeMnemonicInput(123 as unknown as string));
23+
});

0 commit comments

Comments
 (0)