CH-J Server Managerserverhantering över SSH
Menyn
Publicerad källa

CH-J Server Manager

Bläddra i kataloger och filer för en specifik programutgåva.

Ladda ner källa ZIP
CH-J / Server Manager 377 filer
core-20261019T111616Z-160a00bfcddc Tillagd: · Byggd:
biometrics.test.js Tillagd: Datum inte noterat 15,0 KB · 174 linjer
Kod Ladda ner fil
1"use strict";
2const 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");
3const { VaultStore } = require("../src/main/security/vaultStore");
4const { ConfigStore } = require("../src/main/config/configStore");
5const { BiometricService } = require("../src/main/security/biometrics/biometricService");
6const { PlatformAdapter } = require("../src/main/security/biometrics/platformAdapter");
7const { NativeRunner } = require("../src/main/security/biometrics/nativeRunner");
8const { VaultLockController } = require("../src/main/security/vaultLockController");
9const { registerCoreIpc } = require("../src/main/ipc/registerCoreIpc");
10function failure(code) { return Object.assign(new Error(code), { code }); }
11async 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 };
25test("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});
35for (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 });
43test("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});
49for (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 });
55test("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});
61test("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});
68test("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});
73test("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});
80test("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});
85test("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});
96test("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});
102test("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});
107test("native runner bounds output, handles missing helper and authentication timeout", async () => {
108 const runner = (code, timeoutMs = 40) => new NativeRunner({ executable: process.execPath, args: ["-e", code], timeoutMs });
109 // Output limit must win regardless of interpreter start-up time (Node alone can exceed 40 ms).
110 await assert.rejects(runner("process.stdout.write('x'.repeat(20000)); setInterval(()=>{},1000)", 10000).run({ op: "retrieve" }), { code: "BIOMETRIC_FAILED" });
111 await assert.rejects(runner("setInterval(()=>{},1000)").run({ op: "retrieve" }), { code: "BIOMETRIC_TIMEOUT" });
112 await assert.rejects(new NativeRunner({ executable: path.join(os.tmpdir(), "no-such-chj-helper") }).run({ op: "status" }), { code: "BIOMETRIC_UNAVAILABLE" });
113});
114test("biometric IPC rejects plugin windows and child frames without exposing keys", async () => {
115 const handlers = new Map(), mainFrame = {}, sender = { id: 1 }; let calls = 0;
116 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 }; } } });
117 const handler = handlers.get("biometrics:status");
118 await assert.rejects(handler({ sender: { id: 2 }, senderFrame: mainFrame }), { code: "UNTRUSTED_IPC_SENDER" });
119 await assert.rejects(handler({ sender, senderFrame: {} }), { code: "UNTRUSTED_IPC_SENDER" });
120 assert.deepEqual(await handler({ sender, senderFrame: mainFrame }), { ok: true, value: { enabled: true, available: true } }); assert.equal(calls, 1);
121});
122test("macOS Secure Enclave wrapped key is persisted in metadata and passed back on unlock", async t => {
123 const { vault, service, adapter } = await setup(t); const native = new Map(), wrappedSeen = [];
124 adapter.storeProtectedKey = async (id, key) => { native.set(id, Buffer.from(key)); return { wrapped: Buffer.from("sealed:" + id).toString("base64") }; };
125 adapter.retrieveProtectedKey = async (id, _reason, _options, wrapped) => { wrappedSeen.push(wrapped); return Buffer.from(native.get(id)); };
126 await service.enable(); const meta = JSON.parse(fs.readFileSync(service.file, "utf8"));
127 assert.equal(meta.wrapped, Buffer.from("sealed:" + meta.entryId).toString("base64"));
128 vault.lock(); await service.unlock(); assert.equal(vault.status().unlocked, true); assert.deepEqual(wrappedSeen, [meta.wrapped]);
129 const requests = []; const mac = new PlatformAdapter({ platform: "darwin", appPath: os.tmpdir(), runner: { run: async request => { requests.push(request); return request.op === "store" ? { ok: true, wrapped: "QUJD" } : { ok: true, key: crypto.randomBytes(32).toString("base64") }; } } });
130 assert.deepEqual(await mac.storeProtectedKey("a".repeat(64), crypto.randomBytes(32)), { wrapped: "QUJD" });
131 await mac.retrieveProtectedKey("a".repeat(64), "Unlock", undefined, "QUJD"); assert.equal(requests[1].wrapped, "QUJD");
132 const bad = new PlatformAdapter({ platform: "darwin", appPath: os.tmpdir(), runner: { run: async () => ({ ok: true, wrapped: "not base64!" }) } });
133 await assert.rejects(bad.storeProtectedKey("a".repeat(64), crypto.randomBytes(32)), { code: "BIOMETRIC_CREDENTIAL_FAILED" });
134});
136test("automatic biometric unlock: armed at launch, single-flight, not repeated after cancel, re-armed only by automatic lock", async t => {
137 const f = await setup(t);
138 await f.service.enable(); f.vault.lock();
139 let prompts = 0; let release;
140 f.adapter.retrieveProtectedKey = async id => { prompts++; await new Promise(resolve => { release = resolve; }); return Buffer.from(f.keys.get(id)); };
141 const first = f.service.autoUnlock(), second = f.service.autoUnlock();
142 await new Promise(resolve => setImmediate(resolve)); await new Promise(resolve => setImmediate(resolve));
143 assert.equal(f.vault.status().unlocked, false, "vault stays locked until the OS approves");
144 release();
145 const [a, b] = await Promise.all([first, second]);
146 assert.equal(prompts, 1, "concurrent requests share one OS prompt"); assert.equal(a, b); assert.equal(a.status.unlocked, true);
147 const lock = new VaultLockController({ vaultStore: f.vault, configStore: f.config, biometricService: f.service, pluginRuntime: { closeAll() {} }, sessionManager: { disconnectAll: async () => {} } });
148 t.after(() => lock.dispose());
149 await lock.lock("manual");
150 assert.deepEqual(await f.service.autoUnlock(), { attempted: false, reason: "NOT_ARMED" }, "manual lock does not prompt again by itself");
151 f.adapter.retrieveProtectedKey = async () => { prompts++; throw failure("BIOMETRIC_CANCELLED"); };
152 await lock.lock("inactivity");
153 const cancelled = await f.service.autoUnlock();
154 assert.equal(cancelled.attempted, true); assert.equal(cancelled.error.code, "BIOMETRIC_CANCELLED"); assert.equal(f.vault.status().unlocked, false);
155 assert.deepEqual(await f.service.autoUnlock(), { attempted: false, reason: "NOT_ARMED" }, "a cancelled prompt is not re-shown automatically");
156 assert.equal(prompts, 2);
157 await assert.rejects(() => f.service.unlock(), { code: "BIOMETRIC_CANCELLED" }, "the explicit button still works as a new user action");
158 assert.equal((await f.vault.unlock("test-password")).unlocked, true, "password fallback keeps working");
159});
161test("unlocking any other way consumes the launch prompt", async t => {
162 const f = await setup(t); await f.service.enable(); f.vault.lock();
163 await f.service.unlock(); f.vault.lock();
164 assert.deepEqual(await f.service.autoUnlock(), { attempted: false, reason: "NOT_ARMED" });
165});
167test("automatic biometric unlock is skipped when not enabled or unavailable", async t => {
168 const f = await setup(t); f.vault.lock();
169 assert.deepEqual(await f.service.autoUnlock(), { attempted: false, reason: "NOT_ENABLED" });
170 const g = await setup(t); await g.service.enable(); g.vault.lock();
171 g.adapter.getAvailability = async () => ({ available: false, code: "BIOMETRIC_UNAVAILABLE" });
172 assert.deepEqual(await g.service.autoUnlock(), { attempted: false, reason: "BIOMETRIC_UNAVAILABLE" });
173 assert.equal(g.vault.status().unlocked, false);
174});
SHA-256df1255a0bfb1b6b9640d4e634f116e64ac6557b12f3cbae9c18b324afac20c87

CH-J Proprietary Software License 1.14 · Källan finns under CH-J Proprietary Software License 1.14. Dess tillgänglighet ändrar inte licensvillkoren eller ger ytterligare rättigheter.

Arkiv SHA-256a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
Filer