Vydané zdroje
Stáhnout zdroje ZIP
CH-J Server Manager
Procházejte adresáře a soubory konkrétního vydání aplikace.
Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.
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
}13
reason() { return REASONS[this.configStore.get().ui.language] || REASONS.en; }14
metadata() {15
try {16
const value = JSON.parse(fs.readFileSync(this.file, "utf8"));17
if (value.schema !== 1 || !/^[a-f0-9]{64}$/.test(value.entryId) || !/^[a-f0-9]{64}$/.test(value.vaultId)) return null;18
return value;19
} catch { return null; }20
}21
validMetadata() {22
const value = this.metadata();23
return value && value.vaultId === this.vaultStore.instanceId() && value.provider === this.adapter.info.provider ? value : null;24
}25
async getStatus() {26
const availability = await this.adapter.getAvailability();27
const enrolled = this.metadata(), valid = this.validMetadata();28
return { ...availability, enrolled: Boolean(enrolled), enabled: Boolean(valid), code: enrolled && !valid ? "BIOMETRIC_ENROLLMENT_INVALIDATED" : availability.code };29
}30
cancel(code = "BIOMETRIC_CANCELLED") { this.epoch++; this.active?.abort(biometricError(code)); }31
async run(work) {32
if (this.active) throw biometricError("BIOMETRIC_BUSY");33
const controller = new AbortController(), epoch = this.epoch; this.active = controller;34
const check = () => { if (controller.signal.aborted || epoch !== this.epoch) throw controller.signal.reason || biometricError("BIOMETRIC_CANCELLED"); };35
let abort;36
const interrupted = new Promise((_, reject) => { abort = () => reject(controller.signal.reason || biometricError("BIOMETRIC_CANCELLED")); controller.signal.addEventListener("abort", abort, { once: true }); });37
const timer = setTimeout(() => this.cancel("BIOMETRIC_TIMEOUT"), this.timeoutMs);38
try { return await Promise.race([Promise.resolve().then(() => work({ signal: controller.signal, check })), interrupted]); }39
finally { clearTimeout(timer); controller.signal.removeEventListener("abort", abort); if (this.active === controller) this.active = null; }40
}41
async enable() {42
this.vaultStore._assertUnlocked();43
return this.run(async options => {44
const availability = await this.adapter.getAvailability(); options.check();45
if (!availability.available) throw biometricError(availability.code || "BIOMETRIC_UNAVAILABLE");46
const vaultId = this.vaultStore.instanceId(), entryId = crypto.randomBytes(32).toString("hex"), previous = this.metadata();47
await this.adapter.enroll(entryId, this.reason(), options); options.check();48
let attempted = false;49
try {50
await this.vaultStore.withUnlockKey(async key => { options.check(); attempted = true; await this.adapter.storeProtectedKey(entryId, key, options); options.check(); });51
if (vaultId !== this.vaultStore.instanceId()) throw biometricError("BIOMETRIC_ENROLLMENT_INVALIDATED");52
options.check(); atomicWriteJson(this.file, { schema: 1, vaultId, entryId, provider: this.adapter.info.provider });53
} catch (error) { if (attempted) await this.adapter.removeEnrollment(entryId).catch(() => {}); throw error; }54
if (previous && previous.entryId !== entryId) await this.adapter.removeEnrollment(previous.entryId).catch(() => {});55
return this.getStatus();56
});57
}58
async disable({ reset = false } = {}) {59
if (!reset) this.vaultStore._assertUnlocked();60
this.cancel(); const previous = this.metadata();61
// Revocation takes effect locally even if the OS credential store is currently offline.62
try { fs.unlinkSync(this.file); } catch (error) { if (error.code !== "ENOENT") throw error; }63
if (previous) await this.adapter.removeEnrollment(previous.entryId).catch(() => {});64
return { enabled: false, enrolled: false };65
}66
async unlock() {67
if (this.vaultStore.status().unlocked) return this.vaultStore.status();68
const meta = this.validMetadata(); if (!meta) throw biometricError("BIOMETRIC_MASTER_PASSWORD_REQUIRED");69
return this.run(async options => {70
let key;71
try {72
key = await this.adapter.retrieveProtectedKey(meta.entryId, this.reason(), options); options.check();73
if (meta.vaultId !== this.vaultStore.instanceId() || this.validMetadata()?.entryId !== meta.entryId) throw biometricError("BIOMETRIC_ENROLLMENT_INVALIDATED");74
return this.vaultStore.unlockWithKey(key);75
} catch (error) {76
if (["BIOMETRIC_ENROLLMENT_INVALIDATED", "BIOMETRIC_INVALID_KEY"].includes(error.code)) { try { fs.unlinkSync(this.file); } catch {} await this.adapter.removeEnrollment(meta.entryId).catch(() => {}); }77
throw error;78
} finally { if (Buffer.isBuffer(key)) key.fill(0); }79
});80
}81
async authenticateSensitiveAction() {82
this.vaultStore._assertUnlocked();83
return this.run(async options => { await this.adapter.authenticate(this.reason(), options); options.check(); return { authenticated: true }; });84
}85
async requireSensitiveAction() {86
if (this.configStore.get().security.requireSystemAuthentication) await this.authenticateSensitiveAction();87
}88
}89
module.exports = { BiometricService, REASONS };SHA-256: 339fc4a53efe0510832ab193b7f78eda391e9b3598e2251d5de1e1048c4cb0a3
SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0