已发布的源代码
下载源代码 ZIP
CH-J Server Manager
浏览特定应用版本的目录和文件。
CH-J / Server Manager
377 个文件
core-20261019T111616Z-160a00bfcddc
添加时间: · 构建时间:
1
"use strict";2
const fs = require("node:fs");3
const path = require("node:path");4
const crypto = require("node:crypto");5
const { atomicWriteJson } = require("../../storage/atomicFile");6
const { biometricError } = require("./nativeRunner");7
const REASONS = Object.freeze({ cs: "Ověřte svou totožnost pro CH-J Server Manager", de: "Identität für CH-J Server Manager bestätigen", en: "Verify your identity for CH-J Server Manager" });8
class BiometricService {9
constructor({ storageRoot, vaultStore, configStore, adapter, timeoutMs = 70000 }) {10
Object.assign(this, { vaultStore, configStore, adapter, timeoutMs });11
this.file = path.join(storageRoot, "security", "biometrics.json"); this.epoch = 0; this.active = null;12
// Automatic prompt: armed at launch and after an automatic lock, consumed by the first attempt,13
// so a cancelled prompt is never re-shown on its own (the button stays available).14
this.autoArmed = true; this.autoPending = null;15
}16
armAutoUnlock() { this.autoArmed = true; }17
disarmAutoUnlock() { this.autoArmed = false; }18
// Main-process decision whether to show the OS prompt now; single-flight across renderer calls.19
autoUnlock() {20
if (this.vaultStore.status().unlocked) return Promise.resolve({ attempted: false, reason: "UNLOCKED", status: this.vaultStore.status() });21
if (this.autoPending) return this.autoPending;22
if (!this.autoArmed) return Promise.resolve({ attempted: false, reason: "NOT_ARMED" });23
if (!this.validMetadata()) return Promise.resolve({ attempted: false, reason: "NOT_ENABLED" });24
this.autoArmed = false;25
this.autoPending = (async () => {26
const availability = await this.adapter.getAvailability();27
if (!availability.available) return { attempted: false, reason: availability.code || "BIOMETRIC_UNAVAILABLE" };28
try { return { attempted: true, status: await this.unlock() }; }29
catch (error) { return { attempted: true, error: { code: error.code || "BIOMETRIC_FAILED" } }; }30
})().finally(() => { this.autoPending = null; });31
return this.autoPending;32
}33
reason() { return REASONS[this.configStore.get().ui.language] || REASONS.en; }34
metadata() {35
try {36
const value = JSON.parse(fs.readFileSync(this.file, "utf8"));37
if (value.schema !== 1 || !/^[a-f0-9]{64}$/.test(value.entryId) || !/^[a-f0-9]{64}$/.test(value.vaultId)) return null;38
if (value.wrapped !== undefined && (typeof value.wrapped !== "string" || value.wrapped.length > 8192)) return null;39
return value;40
} catch { return null; }41
}42
validMetadata() {43
const value = this.metadata();44
return value && value.vaultId === this.vaultStore.instanceId() && value.provider === this.adapter.info.provider ? value : null;45
}46
async getStatus() {47
const availability = await this.adapter.getAvailability();48
const enrolled = this.metadata(), valid = this.validMetadata();49
return { ...availability, enrolled: Boolean(enrolled), enabled: Boolean(valid), code: enrolled && !valid ? "BIOMETRIC_ENROLLMENT_INVALIDATED" : availability.code };50
}51
cancel(code = "BIOMETRIC_CANCELLED") { this.epoch++; this.active?.abort(biometricError(code)); }52
async run(work) {53
if (this.active) throw biometricError("BIOMETRIC_BUSY");54
const controller = new AbortController(), epoch = this.epoch; this.active = controller;55
const check = () => { if (controller.signal.aborted || epoch !== this.epoch) throw controller.signal.reason || biometricError("BIOMETRIC_CANCELLED"); };56
let abort;57
const interrupted = new Promise((_, reject) => { abort = () => reject(controller.signal.reason || biometricError("BIOMETRIC_CANCELLED")); controller.signal.addEventListener("abort", abort, { once: true }); });58
const timer = setTimeout(() => this.cancel("BIOMETRIC_TIMEOUT"), this.timeoutMs);59
try { return await Promise.race([Promise.resolve().then(() => work({ signal: controller.signal, check })), interrupted]); }60
finally { clearTimeout(timer); controller.signal.removeEventListener("abort", abort); if (this.active === controller) this.active = null; }61
}62
async enable() {63
this.vaultStore._assertUnlocked();64
return this.run(async options => {65
const availability = await this.adapter.getAvailability(); options.check();66
if (!availability.available) throw biometricError(availability.code || "BIOMETRIC_UNAVAILABLE");67
const vaultId = this.vaultStore.instanceId(), entryId = crypto.randomBytes(32).toString("hex"), previous = this.metadata();68
await this.adapter.enroll(entryId, this.reason(), options); options.check();69
let attempted = false;70
try {71
let stored;72
await this.vaultStore.withUnlockKey(async key => { options.check(); attempted = true; stored = await this.adapter.storeProtectedKey(entryId, key, options); options.check(); });73
if (vaultId !== this.vaultStore.instanceId()) throw biometricError("BIOMETRIC_ENROLLMENT_INVALIDATED");74
// `wrapped` (macOS) is ciphertext only openable by this Mac's Secure Enclave after Touch ID.75
options.check(); atomicWriteJson(this.file, { schema: 1, vaultId, entryId, provider: this.adapter.info.provider, ...(stored?.wrapped ? { wrapped: stored.wrapped } : {}) });76
} catch (error) { if (attempted) await this.adapter.removeEnrollment(entryId).catch(() => {}); throw error; }77
if (previous && previous.entryId !== entryId) await this.adapter.removeEnrollment(previous.entryId).catch(() => {});78
return this.getStatus();79
});80
}81
async disable({ reset = false } = {}) {82
if (!reset) this.vaultStore._assertUnlocked();83
this.cancel(); const previous = this.metadata();84
// Revocation takes effect locally even if the OS credential store is currently offline.85
try { fs.unlinkSync(this.file); } catch (error) { if (error.code !== "ENOENT") throw error; }86
if (previous) await this.adapter.removeEnrollment(previous.entryId).catch(() => {});87
return { enabled: false, enrolled: false };88
}89
async unlock() {90
if (this.vaultStore.status().unlocked) return this.vaultStore.status();91
const meta = this.validMetadata(); if (!meta) throw biometricError("BIOMETRIC_MASTER_PASSWORD_REQUIRED");92
return this.run(async options => {93
let key;94
try {95
key = await this.adapter.retrieveProtectedKey(meta.entryId, this.reason(), options, meta.wrapped); options.check();96
if (meta.vaultId !== this.vaultStore.instanceId() || this.validMetadata()?.entryId !== meta.entryId) throw biometricError("BIOMETRIC_ENROLLMENT_INVALIDATED");97
const status = await this.vaultStore.unlockWithKey(key);98
this.disarmAutoUnlock();99
return status;100
} catch (error) {101
if (["BIOMETRIC_ENROLLMENT_INVALIDATED", "BIOMETRIC_INVALID_KEY"].includes(error.code)) { try { fs.unlinkSync(this.file); } catch {} await this.adapter.removeEnrollment(meta.entryId).catch(() => {}); }102
throw error;103
} finally { if (Buffer.isBuffer(key)) key.fill(0); }104
});105
}106
async authenticateSensitiveAction() {107
this.vaultStore._assertUnlocked();108
return this.run(async options => { await this.adapter.authenticate(this.reason(), options); options.check(); return { authenticated: true }; });109
}110
async requireSensitiveAction() {111
if (this.configStore.get().security.requireSystemAuthentication) await this.authenticateSensitiveAction();112
}113
}114
module.exports = { BiometricService, REASONS };SHA-256
e02b4dccf186103683ee541b3ebf3ebe1d558c10a9f7a38576f5165237cb066aCH-J Proprietary Software License 1.14 · 源代码依据 CH-J Proprietary Software License 1.14 提供。公开源代码不会改变许可条款,也不会授予额外权利。
压缩包 SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a