security: make DEBUG a build-time flag defaulting to off (closes #149)
All checks were successful
check / check (push) Successful in 29s
All checks were successful
check / check (push) Successful in 29s
DEBUG was hardcoded to true in src/shared/constants.js, so every wallet created from a build of main received the publicly committed test recovery phrase and was instantly drainable. There was no way to produce a non-debug build at all: the real entropy path in generateMnemonic() was dead code in every artifact. DEBUG is now a build-time constant injected by esbuild's define in build.js, alongside the existing __BUILD_* defines, and read by constants.js with the same typeof guard buildInfo.js uses. It is false unless the build was run with AUTISTMASK_DEBUG=1 — an exact match, so an unset, empty or mistyped value fails safe towards a release build. The build prints which mode it used, and make build-debug is a shim for the debug case. What DEBUG does when enabled is unchanged: the red banner plus the hardcoded test phrase, no new conditionals. Mnemonic generation deliberately keeps reading the compile-time constant rather than isDebug() from log.js, which also ORs in the runtime debugMode flag the settings toggle drives; routing it through isDebug() would let a user of a release build re-enable the known test phrase for real wallets. That is now recorded at the call site, in the README DEBUG Mode Policy, and covered by a regression test. New tests/wallet.test.js covers both build modes: with the flag off, two successive generateMnemonic() calls differ, both validate as BIP-39 phrases, both are 12 words, neither is DEBUG_MNEMONIC, and the result still derives a usable HD wallet — including with the runtime toggle forced on. With the flag on, DEBUG is true and the test phrase is returned, so the debug path stays proven rather than silently removed. Verified with make check (55 tests, lint, fmt-check all green), and with make build and make build-debug: all four bundles across dist/chrome and dist/firefox export DEBUG:!1 in a release build and DEBUG:!0 in a debug build, and AUTISTMASK_DEBUG=true likewise yields DEBUG:!1.
This commit is contained in:
94
tests/wallet.test.js
Normal file
94
tests/wallet.test.js
Normal file
@@ -0,0 +1,94 @@
|
||||
// Tests for the DEBUG build flag as it gates mnemonic generation.
|
||||
//
|
||||
// The modules read the __BUILD_DEBUG__ global that esbuild replaces at bundle
|
||||
// time. Under jest the global is absent, which is exactly the release-build
|
||||
// case; the debug-build case is exercised by defining the global and
|
||||
// re-requiring the modules with a fresh registry.
|
||||
|
||||
const WORDS_IN_12_WORD_PHRASE = 12;
|
||||
|
||||
function loadWallet() {
|
||||
const constants = require("../src/shared/constants");
|
||||
const wallet = require("../src/shared/wallet");
|
||||
const log = require("../src/shared/log");
|
||||
return { constants, wallet, log };
|
||||
}
|
||||
|
||||
describe("generateMnemonic in a release build", () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
delete globalThis.__BUILD_DEBUG__;
|
||||
});
|
||||
|
||||
test("DEBUG defaults to false when the build define is absent", () => {
|
||||
const { constants } = loadWallet();
|
||||
expect(constants.DEBUG).toBe(false);
|
||||
});
|
||||
|
||||
test("returns fresh, valid 12-word phrases that are not the test phrase", () => {
|
||||
const { constants, wallet } = loadWallet();
|
||||
|
||||
const first = wallet.generateMnemonic();
|
||||
const second = wallet.generateMnemonic();
|
||||
|
||||
expect(first).not.toBe(second);
|
||||
for (const phrase of [first, second]) {
|
||||
expect(wallet.isValidMnemonic(phrase)).toBe(true);
|
||||
expect(phrase.split(" ")).toHaveLength(WORDS_IN_12_WORD_PHRASE);
|
||||
expect(phrase).not.toBe(constants.DEBUG_MNEMONIC);
|
||||
}
|
||||
});
|
||||
|
||||
test("derives a usable HD wallet from the generated phrase", () => {
|
||||
const { wallet } = loadWallet();
|
||||
|
||||
const { xpub, firstAddress } = wallet.hdWalletFromMnemonic(
|
||||
wallet.generateMnemonic(),
|
||||
);
|
||||
|
||||
expect(xpub.startsWith("xpub")).toBe(true);
|
||||
expect(firstAddress).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
||||
});
|
||||
|
||||
test("the runtime debug toggle cannot re-enable the test phrase", () => {
|
||||
const { constants, wallet, log } = loadWallet();
|
||||
|
||||
// What the settings easter-egg toggle does at runtime.
|
||||
log.setRuntimeDebug(true);
|
||||
expect(log.isDebug()).toBe(true);
|
||||
|
||||
const phrase = wallet.generateMnemonic();
|
||||
expect(phrase).not.toBe(constants.DEBUG_MNEMONIC);
|
||||
expect(wallet.isValidMnemonic(phrase)).toBe(true);
|
||||
expect(phrase).not.toBe(wallet.generateMnemonic());
|
||||
|
||||
log.setRuntimeDebug(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateMnemonic in a debug build", () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
globalThis.__BUILD_DEBUG__ = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete globalThis.__BUILD_DEBUG__;
|
||||
});
|
||||
|
||||
test("DEBUG is true and the test phrase is returned", () => {
|
||||
const { constants, wallet } = loadWallet();
|
||||
|
||||
expect(constants.DEBUG).toBe(true);
|
||||
expect(wallet.generateMnemonic()).toBe(constants.DEBUG_MNEMONIC);
|
||||
});
|
||||
|
||||
test("the test phrase is itself a valid 12-word BIP-39 phrase", () => {
|
||||
const { constants, wallet } = loadWallet();
|
||||
|
||||
expect(wallet.isValidMnemonic(constants.DEBUG_MNEMONIC)).toBe(true);
|
||||
expect(constants.DEBUG_MNEMONIC.split(" ")).toHaveLength(
|
||||
WORDS_IN_12_WORD_PHRASE,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user