fix(wallet): protect mnemonic file permissions

This commit is contained in:
redshift
2026-07-13 19:57:54 +08:00
parent 52e5d9b1d6
commit 0b63ad31bf
2 changed files with 72 additions and 6 deletions
+55
View File
@@ -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);
});
});
+17 -6
View File
@@ -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<void> {
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!");
}