Published source
Download source ZIP
CH-J Server Manager
Browse directories and files for a specific application release.
Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.
1
"use strict";2
const test = require("node:test"), assert = require("node:assert/strict"), fs = require("node:fs"), os = require("node:os"), path = require("node:path"), crypto = require("node:crypto");3
const { VaultStore } = require("../src/main/security/vaultStore");4
const { ConfigStore } = require("../src/main/config/configStore");5
const { BiometricService } = require("../src/main/security/biometrics/biometricService");6
const { PlatformAdapter } = require("../src/main/security/biometrics/platformAdapter");7
const { NativeRunner } = require("../src/main/security/biometrics/nativeRunner");8
const { VaultLockController } = require("../src/main/security/vaultLockController");9
const { registerCoreIpc } = require("../src/main/ipc/registerCoreIpc");10
function failure(code) { return Object.assign(new Error(code), { code }); }11
async function setup(t, provider = "touch-id") {12
const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-biometric-test-")); t.after(() => fs.rmSync(root, { recursive: true, force: true }));13
const vault = new VaultStore(root), config = new ConfigStore(root); config.load(); await vault.create("test-password");14
vault.update(data => { data.profiles.push({ id: "kept-profile" }); data.hostKeys.host = "kept-fingerprint"; });15
const keys = new Map(), adapter = {16
info: { provider, protection: provider === "touch-id" ? "biometry-current-set" : "convenience" },17
getAvailability: async () => ({ available: true, code: "BIOMETRIC_AVAILABLE", ...adapter.info }),18
enroll: async () => {}, authenticate: async () => {}, removeEnrollment: async id => { keys.delete(id); },19
storeProtectedKey: async (id, key) => { keys.set(id, Buffer.from(key)); },20
retrieveProtectedKey: async id => Buffer.from(keys.get(id))21
};22
const service = new BiometricService({ storageRoot: root, vaultStore: vault, configStore: config, adapter });23
return { root, vault, config, keys, adapter, service };24
}25
test("biometric unlock authenticates existing encrypted data without migration or plaintext password", async t => {26
const { vault, service, adapter } = await setup(t);27
const before = [vault.metaPath, vault.dataPath].map(p => fs.readFileSync(p));28
let keyReference; const store = adapter.storeProtectedKey; adapter.storeProtectedKey = async (id, key) => { keyReference = key; await store(id, key); };29
assert.equal((await service.enable()).enabled, true); assert.equal(keyReference.equals(Buffer.alloc(32)), true);30
const metadata = fs.readFileSync(service.file, "utf8"); assert.ok(!metadata.includes("test-password"));31
vault.lock(); assert.equal((await service.unlock()).unlocked, true);32
assert.equal(vault.getData().profiles[0].id, "kept-profile"); assert.equal(vault.getData().hostKeys.host, "kept-fingerprint");33
[vault.metaPath, vault.dataPath].forEach((p, i) => assert.deepEqual(fs.readFileSync(p), before[i]));34
});35
for (const code of ["BIOMETRIC_CANCELLED", "BIOMETRIC_FAILED", "BIOMETRIC_CREDENTIAL_FAILED", "BIOMETRIC_DEVICE_UNAVAILABLE"]) {36
test(`${code} leaves Vault locked and master-password fallback works`, async t => {37
const { vault, service, adapter } = await setup(t); await service.enable(); vault.lock();38
adapter.retrieveProtectedKey = async () => { throw failure(code); };39
await assert.rejects(service.unlock(), { code }); assert.equal(vault.status().unlocked, false);40
assert.equal((await vault.unlock("test-password")).unlocked, true);41
});42
}43
test("wrong stored key fails GCM authentication, is zeroed and revokes enrollment", async t => {44
const { vault, service, adapter, keys } = await setup(t); await service.enable(); vault.lock();45
const bad = crypto.randomBytes(32); adapter.retrieveProtectedKey = async () => bad;46
await assert.rejects(service.unlock(), { code: "BIOMETRIC_ENROLLMENT_INVALIDATED" });47
assert.equal(vault.status().unlocked, false); assert.deepEqual(bad, Buffer.alloc(32)); assert.equal(keys.size, 0); assert.equal(service.metadata(), null);48
});49
for (const code of ["BIOMETRIC_UNAVAILABLE", "BIOMETRIC_NO_ENROLLMENT", "BIOMETRIC_KEYRING_UNAVAILABLE"]) {50
test(`enrollment refused for ${code}`, async t => {51
const { service, adapter, keys } = await setup(t); adapter.getAvailability = async () => ({ available: false, code });52
await assert.rejects(service.enable(), { code }); assert.equal(keys.size, 0); assert.equal(service.metadata(), null);53
});54
}55
test("macOS changed fingerprint set invalidates enrollment and keeps password recovery", async t => {56
const { service, adapter, vault, keys } = await setup(t); await service.enable(); vault.lock();57
adapter.retrieveProtectedKey = async () => { throw failure("BIOMETRIC_ENROLLMENT_INVALIDATED"); };58
await assert.rejects(service.unlock(), { code: "BIOMETRIC_ENROLLMENT_INVALIDATED" }); assert.equal(keys.size, 0);59
await vault.unlock("test-password"); assert.equal(vault.getData().profiles.length, 1);60
});61
test("reset revokes OS credential and old metadata cannot unlock a replacement Vault", async t => {62
const { service, vault, keys } = await setup(t); await service.enable(); const metadata = fs.readFileSync(service.file);63
await service.disable({ reset: true }); vault.reset("SMAZAT"); assert.equal(keys.size, 0);64
await vault.create("replacement-password"); fs.writeFileSync(service.file, metadata); vault.lock();65
assert.equal((await service.getStatus()).code, "BIOMETRIC_ENROLLMENT_INVALIDATED");66
await assert.rejects(service.unlock(), { code: "BIOMETRIC_MASTER_PASSWORD_REQUIRED" }); assert.equal(vault.status().unlocked, false);67
});68
test("failed OS store rolls back even if credential was written before failure", async t => {69
const { service, adapter, keys } = await setup(t);70
adapter.storeProtectedKey = async (id, key) => { keys.set(id, Buffer.from(key)); throw failure("BIOMETRIC_CREDENTIAL_FAILED"); };71
await assert.rejects(service.enable(), { code: "BIOMETRIC_CREDENTIAL_FAILED" }); assert.equal(keys.size, 0); assert.equal(service.metadata(), null);72
});73
test("timeout and late OS approval cannot unlock Vault", async t => {74
const { service, adapter, vault, keys } = await setup(t); await service.enable(); vault.lock(); service.timeoutMs = 10;75
let release; adapter.retrieveProtectedKey = () => new Promise(resolve => { release = resolve; });76
await assert.rejects(service.unlock(), { code: "BIOMETRIC_TIMEOUT" });77
const key = Buffer.from([...keys.values()][0]); release(key); await new Promise(resolve => setImmediate(resolve));78
assert.equal(vault.status().unlocked, false); assert.deepEqual(key, Buffer.alloc(32));79
});80
test("manual lock cancels delayed master-password derivation", async t => {81
const { vault } = await setup(t); vault.lock();82
const pending = vault.unlock("test-password"); vault.lock();83
await assert.rejects(pending, { code: "VAULT_UNLOCK_FAILED" }); assert.equal(vault.status().unlocked, false);84
});85
test("automatic lock zeroes key before awaiting active SSH cleanup and closes plugins", async t => {86
const { vault, service, config } = await setup(t); config.update({ security: { autoLockMinutes: 15, lockOnBlur: true } });87
let now = 0, release, reason, closed = false;88
const controller = new VaultLockController({ vaultStore: vault, configStore: config, biometricService: service, now: () => now,89
sessionManager: { disconnectAll: async value => { reason = value; await new Promise(resolve => { release = resolve; }); } },90
pluginRuntime: { closeAll: () => { closed = true; } } }); t.after(() => controller.dispose());91
now = 14 * 60000; await controller.check(); assert.equal(vault.status().unlocked, true);92
controller.activity(); now += 15 * 60000; const pending = controller.check();93
assert.equal(vault.status().unlocked, false); assert.equal(closed, true); assert.equal(reason, "vault-inactivity"); release(); await pending;94
await vault.unlock("test-password"); const focused = controller.lostFocus(); assert.equal(vault.status().unlocked, false); release(); await focused;95
});96
test("Windows Hello PIN approval uses desktop HWND and returns only main-process key", async () => {97
const requests = [], expected = crypto.randomBytes(32);98
const adapter = new PlatformAdapter({ platform: "win32", appPath: os.tmpdir(), getMainWindow: () => ({ isDestroyed: () => false, getNativeWindowHandle: () => { const b = Buffer.alloc(8); b.writeBigUInt64LE(12345n); return b; } }), runner: { run: async request => { requests.push(request); return { ok: true, key: expected.toString("base64"), method: "PIN" }; } } });99
await adapter.authenticate("Confirm"); const key = await adapter.retrieveProtectedKey("a".repeat(64), "Unlock");100
assert.deepEqual(key, expected); assert.equal(requests[0].hwnd, "12345"); assert.equal(requests[0].pid, process.pid); assert.equal(adapter.info.protection, "convenience");101
});102
test("Linux unavailable/insecure credential backend has no plaintext fallback", async () => {103
const adapter = new PlatformAdapter({ platform: "linux", appPath: os.tmpdir(), runner: { run: async () => { throw failure("BIOMETRIC_KEYRING_UNAVAILABLE"); } } });104
assert.equal((await adapter.getAvailability()).available, false);105
await assert.rejects(adapter.retrieveProtectedKey("a".repeat(64)), { code: "BIOMETRIC_KEYRING_UNAVAILABLE" });106
});107
test("native runner bounds output, handles missing helper and authentication timeout", async () => {108
const runner = code => new NativeRunner({ executable: process.execPath, args: ["-e", code], timeoutMs: 40 });109
await assert.rejects(runner("process.stdout.write('x'.repeat(20000))").run({ op: "retrieve" }), { code: "BIOMETRIC_FAILED" });110
await assert.rejects(runner("setInterval(()=>{},1000)").run({ op: "retrieve" }), { code: "BIOMETRIC_TIMEOUT" });111
await assert.rejects(new NativeRunner({ executable: path.join(os.tmpdir(), "no-such-chj-helper") }).run({ op: "status" }), { code: "BIOMETRIC_UNAVAILABLE" });112
});113
test("biometric IPC rejects plugin windows and child frames without exposing keys", async () => {114
const handlers = new Map(), mainFrame = {}, sender = { id: 1 }; let calls = 0;115
registerCoreIpc({ ipcMain: { handle: (id, fn) => handlers.set(id, fn), on() {} }, getMainWindow: () => ({ isDestroyed: () => false, webContents: { id: 1, mainFrame } }), biometricService: { getStatus: async () => { calls++; return { enabled: true, available: true }; } } });116
const handler = handlers.get("biometrics:status");117
await assert.rejects(handler({ sender: { id: 2 }, senderFrame: mainFrame }), { code: "UNTRUSTED_IPC_SENDER" });118
await assert.rejects(handler({ sender, senderFrame: {} }), { code: "UNTRUSTED_IPC_SENDER" });119
assert.deepEqual(await handler({ sender, senderFrame: mainFrame }), { ok: true, value: { enabled: true, available: true } }); assert.equal(calls, 1);120
});SHA-256: a07dd8fe3a4dafeb3c8990325354376deb4170bd75b39f5eba092a7922f9f121
Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0