From 0b63ad31bfc5a72567c73c9c79655c1db84aea5b Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:57:54 +0800 Subject: [PATCH] fix(wallet): protect mnemonic file permissions --- src/cli.test.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ src/cli.ts | 23 +++++++++++++++------ 2 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 src/cli.test.ts diff --git a/src/cli.test.ts b/src/cli.test.ts new file mode 100644 index 0000000..a617dfb --- /dev/null +++ b/src/cli.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { initializeWallet } from "./cli"; + +const tempDirs: string[] = []; + +function makeTempDir(): string { + const dir = mkdtempSync(join(tmpdir(), "routstrd-wallet-test-")); + tempDirs.push(dir); + return dir; +} + +function permissions(path: string): number { + return statSync(path).mode & 0o777; +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +describe("initializeWallet", () => { + test("creates the wallet directory and config with restrictive permissions", () => { + const walletDir = join(makeTempDir(), ".cocod"); + + initializeWallet(walletDir); + + const walletConfig = join(walletDir, "config.json"); + expect(permissions(walletDir)).toBe(0o700); + expect(permissions(walletConfig)).toBe(0o600); + + const config = JSON.parse(readFileSync(walletConfig, "utf8")); + expect(config.encrypted).toBe(false); + expect(typeof config.mnemonic).toBe("string"); + expect(config.mnemonic.length).toBeGreaterThan(0); + }); + + test("repairs permissions without replacing an existing wallet", () => { + const walletDir = join(makeTempDir(), ".cocod"); + const walletConfig = join(walletDir, "config.json"); + const existingConfig = JSON.stringify({ mnemonic: "existing seed" }); + + mkdirSync(walletDir, { mode: 0o755 }); + writeFileSync(walletConfig, existingConfig, { mode: 0o644 }); + + initializeWallet(walletDir); + + expect(readFileSync(walletConfig, "utf8")).toBe(existingConfig); + expect(permissions(walletDir)).toBe(0o700); + expect(permissions(walletConfig)).toBe(0o600); + }); +}); diff --git a/src/cli.ts b/src/cli.ts index eb47b59..26e7411 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,7 +15,7 @@ import { deleteClientAction, addClientAction, } from "./utils/clients"; -import { existsSync, mkdirSync, writeFileSync } from "fs"; +import { chmodSync, existsSync, mkdirSync, writeFileSync } from "fs"; import { execSync } from "child_process"; import { CONFIG_DIR, @@ -79,16 +79,24 @@ async function printLightningInvoice(invoice: string): Promise { console.log(`${qr}\nInvoice:\n${invoice}`); } -function initializeWallet(): void { - const walletDir = +export function initializeWallet( + walletDir = process.env.COCOD_DIR || - `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`; + `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`, +): void { const walletConfig = `${walletDir}/config.json`; + + // The wallet directory and config contain the plaintext seed phrase. Correct + // permissions on existing installations as well as newly created ones. + mkdirSync(walletDir, { recursive: true, mode: 0o700 }); + chmodSync(walletDir, 0o700); + if (existsSync(walletConfig)) { + chmodSync(walletConfig, 0o600); console.log("Wallet already initialized."); return; } - mkdirSync(walletDir, { recursive: true }); + const mnemonic = generateMnemonic(wordlist); const config = { version: 1, @@ -96,7 +104,10 @@ function initializeWallet(): void { encrypted: false, createdAt: new Date().toISOString(), }; - writeFileSync(walletConfig, JSON.stringify(config, null, 2)); + writeFileSync(walletConfig, JSON.stringify(config, null, 2), { + mode: 0o600, + flag: "wx", + }); console.log("Initialized. Mnemonic:", mnemonic); console.log("IMPORTANT: Write down this mnemonic and keep it safe!"); }