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";3
const crypto = require("node:crypto");4
const fs = require("node:fs");5
const path = require("node:path");6
const { promisify } = require("node:util");7
const { atomicWriteJson } = require("../storage/atomicFile");9
const scryptAsync = promisify(crypto.scrypt);10
const KEY_LENGTH = 32;11
const SALT_LENGTH = 16;12
const IV_LENGTH = 12;13
const AAD = Buffer.from("CHJ_CORE_VAULT_V1", "utf8");14
const SCRYPT = Object.freeze({ N: 1 << 15, r: 8, p: 1, maxmem: 128 * 1024 * 1024 });15
const MIN_PASSWORD_LENGTH = 4;16
const MAX_PASSWORD_LENGTH = 64;18
function clone(value) {19
return JSON.parse(JSON.stringify(value));20
}22
function initialVault() {23
return {24
schemaVersion: 1,25
profiles: [],26
hostKeys: {},27
secrets: {}28
};29
}31
function validatePassword(password) {32
const value = String(password || "");33
if (value.includes("\0")) throw new Error("Password contains an invalid character.");34
const length = Array.from(value).length;35
if (length < MIN_PASSWORD_LENGTH || length > MAX_PASSWORD_LENGTH) {36
throw new Error(`Vault password must contain ${MIN_PASSWORD_LENGTH} to ${MAX_PASSWORD_LENGTH} characters.`);37
}38
return value;39
}41
function validateVaultData(value) {42
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Vault data is invalid.");43
if (Number(value.schemaVersion) !== 1) throw new Error("Unsupported vault data schema.");44
if (!Array.isArray(value.profiles)) throw new Error("Vault profiles are invalid.");45
if (!value.hostKeys || typeof value.hostKeys !== "object" || Array.isArray(value.hostKeys)) throw new Error("Vault host keys are invalid.");46
if (!value.secrets || typeof value.secrets !== "object" || Array.isArray(value.secrets)) value.secrets = {};47
return value;48
}50
class VaultStore {51
constructor(rootDir) {52
this.rootDir = path.join(rootDir, "vault");53
this.metaPath = path.join(this.rootDir, "core-v1.meta.json");54
this.dataPath = path.join(this.rootDir, "core-v1.data.json");55
this.key = null;56
this.data = null;57
this.generation = 0;58
}60
status() {61
const metaExists = fs.existsSync(this.metaPath);62
const dataExists = fs.existsSync(this.dataPath);63
const initialized = metaExists && dataExists;64
return {65
initialized,66
needsSetup: !metaExists && !dataExists,67
damaged: metaExists !== dataExists,68
unlocked: Boolean(this.key && this.data)69
};70
}72
async create(password) {73
const generation = this.generation;74
const status = this.status();75
if (status.initialized) throw new Error("Vault is already initialized.");76
if (status.damaged) throw new Error("Vault files are incomplete. Restore or remove them before setup.");77
const normalized = validatePassword(password);78
const salt = crypto.randomBytes(SALT_LENGTH);79
const key = await this._deriveKey(normalized, salt);80
if (generation !== this.generation || !this.status().needsSetup) { key.fill(0); throw new Error("Vault operation cancelled."); }81
const data = initialVault();82
const now = new Date().toISOString();83
try {84
atomicWriteJson(this.metaPath, {85
formatVersion: 1,86
dataSchemaVersion: 1,87
createdAt: now,88
updatedAt: now,89
cipher: "aes-256-gcm",90
kdf: { name: "scrypt", N: SCRYPT.N, r: SCRYPT.r, p: SCRYPT.p, keyLength: KEY_LENGTH },91
salt: salt.toString("base64")92
});93
this._writeEncryptedData(data, key);94
this._setSession(key, data);95
return this.status();96
} catch (error) {97
key.fill(0);98
try { fs.unlinkSync(this.metaPath); } catch {}99
try { fs.unlinkSync(this.dataPath); } catch {}100
throw error;101
}102
}104
async unlock(password) {105
const generation = this.generation, instance = this.instanceId();106
if (!this.status().initialized) throw new Error("Vault setup is required or its files are incomplete.");107
const normalized = validatePassword(password);108
let key;109
try {110
const meta = JSON.parse(fs.readFileSync(this.metaPath, "utf8"));111
if (meta.formatVersion !== 1 || meta.cipher !== "aes-256-gcm" || meta.kdf?.name !== "scrypt") {112
throw new Error("Unsupported vault format.");113
}114
const salt = Buffer.from(String(meta.salt || ""), "base64");115
if (salt.length !== SALT_LENGTH) throw new Error("Vault salt is invalid.");116
key = await this._deriveKey(normalized, salt, meta.kdf);117
if (generation !== this.generation || instance !== this.instanceId()) throw new Error("Vault operation cancelled.");118
const data = this._readEncryptedData(key);119
this._setSession(key, data);120
return this.status();121
} catch (error) {122
key?.fill(0);123
const failure = new Error("Vault password is incorrect or the vault is damaged.");124
failure.code = "VAULT_UNLOCK_FAILED";125
throw failure;126
}127
}129
// Main-process-only key operations. No IPC route exposes these buffers.130
instanceId() {131
if (!this.status().initialized) return null;132
const meta = JSON.parse(fs.readFileSync(this.metaPath, "utf8"));133
return crypto.createHash("sha256").update(JSON.stringify([meta.formatVersion, meta.createdAt, meta.salt])).digest("hex");134
}136
async withUnlockKey(operation) {137
this._assertUnlocked();138
const copy = Buffer.from(this.key);139
try { return await operation(copy); } finally { copy.fill(0); }140
}142
async verifyPassword(password) {143
this._assertUnlocked();144
const generation = this.generation;145
const meta = JSON.parse(fs.readFileSync(this.metaPath, "utf8"));146
const key = await this._deriveKey(validatePassword(password), Buffer.from(meta.salt, "base64"), meta.kdf);147
try {148
this._assertUnlocked();149
if (generation !== this.generation) throw Object.assign(new Error("Master password required."), { code: "BIOMETRIC_MASTER_PASSWORD_REQUIRED" });150
if (!crypto.timingSafeEqual(key, this.key)) throw Object.assign(new Error("Master password required."), { code: "BIOMETRIC_MASTER_PASSWORD_REQUIRED" });151
this._readEncryptedData(key);152
return true;153
} finally { key.fill(0); }154
}156
unlockWithKey(key) {157
if (!this.status().initialized || !Buffer.isBuffer(key) || key.length !== KEY_LENGTH) {158
throw Object.assign(new Error("Invalid protected unlocking key."), { code: "BIOMETRIC_INVALID_KEY" });159
}160
const copy = Buffer.from(key);161
try {162
const data = this._readEncryptedData(copy); // AES-GCM authentication must succeed first.163
this._setSession(copy, data);164
return this.status();165
} catch (_) {166
copy.fill(0);167
throw Object.assign(new Error("Biometric enrollment is no longer valid. Use the master password."), { code: "BIOMETRIC_ENROLLMENT_INVALIDATED" });168
}169
}171
lock() {172
this.generation++;173
if (Buffer.isBuffer(this.key)) this.key.fill(0);174
this.key = null;175
this.data = null;176
return this.status();177
}179
reset(confirmation) {180
if (confirmation !== "SMAZAT") {181
const error = new Error("Vault reset confirmation is invalid.");182
error.code = "VAULT_RESET_CONFIRMATION_REQUIRED";183
throw error;184
}185
this.lock();186
fs.rmSync(this.rootDir, { recursive: true, force: true });187
const status = this.status();188
if (!status.needsSetup) {189
const error = new Error("Vault data could not be removed completely.");190
error.code = "VAULT_RESET_FAILED";191
throw error;192
}193
return status;194
}196
getData() {197
this._assertUnlocked();198
return clone(this.data);199
}201
replaceData(nextData) {202
this._assertUnlocked();203
const normalized = validateVaultData(clone(nextData));204
this._writeEncryptedData(normalized, this.key);205
this.data = normalized;206
return this.getData();207
}209
update(mutator) {210
this._assertUnlocked();211
const draft = this.getData();212
const result = mutator(draft);213
this.replaceData(draft);214
return result;215
}217
_assertUnlocked() {218
if (!this.key || !this.data) {219
const error = new Error("Vault is locked.");220
error.code = "VAULT_LOCKED";221
throw error;222
}223
}225
async _deriveKey(password, salt, params = SCRYPT) {226
return Buffer.from(await scryptAsync(password, salt, KEY_LENGTH, {227
N: Number(params.N || SCRYPT.N),228
r: Number(params.r || SCRYPT.r),229
p: Number(params.p || SCRYPT.p),230
maxmem: SCRYPT.maxmem231
}));232
}234
_writeEncryptedData(data, key) {235
const plaintext = Buffer.from(JSON.stringify(validateVaultData(clone(data))), "utf8");236
const iv = crypto.randomBytes(IV_LENGTH);237
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);238
cipher.setAAD(AAD);239
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);240
const authTag = cipher.getAuthTag();241
try {242
atomicWriteJson(this.dataPath, {243
formatVersion: 1,244
updatedAt: new Date().toISOString(),245
iv: iv.toString("base64"),246
authTag: authTag.toString("base64"),247
ciphertext: ciphertext.toString("base64")248
});249
} finally {250
plaintext.fill(0);251
ciphertext.fill(0);252
}253
}255
_readEncryptedData(key) {256
const blob = JSON.parse(fs.readFileSync(this.dataPath, "utf8"));257
if (blob.formatVersion !== 1) throw new Error("Unsupported encrypted vault data.");258
const iv = Buffer.from(String(blob.iv || ""), "base64");259
const authTag = Buffer.from(String(blob.authTag || ""), "base64");260
const ciphertext = Buffer.from(String(blob.ciphertext || ""), "base64");261
if (iv.length !== IV_LENGTH || authTag.length !== 16 || ciphertext.length === 0) throw new Error("Encrypted vault data is invalid.");262
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);263
decipher.setAAD(AAD);264
decipher.setAuthTag(authTag);265
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);266
try {267
return validateVaultData(JSON.parse(plaintext.toString("utf8")));268
} finally {269
plaintext.fill(0);270
ciphertext.fill(0);271
}272
}274
_setSession(key, data) {275
this.lock();276
this.key = key;277
this.data = validateVaultData(clone(data));278
}279
}281
module.exports = {282
MAX_PASSWORD_LENGTH,283
MIN_PASSWORD_LENGTH,284
VaultStore,285
initialVault,286
validatePassword,287
validateVaultData288
};SHA-256: ce8848f4e2c0a57c830bff42c2296d389d1cb1353812b5c53e264829e38d70b4
SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0