import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, statSync } from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import os from 'node:os'; const normalizeComparable = (p) => process.platform === 'win32' ? p.toLowerCase() : p; // JSON 的确定性编码用于 daemon HMAC;对象键排序,undefined 按 JSON 语义省略。 export function stableStringify(value) { if (value === null || typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) { return '[' + value.map(v => v === undefined ? 'null' : stableStringify(v)).join(',') + ']'; } const parts = []; for (const k of Object.keys(value).sort()) { if (value[k] === undefined) continue; parts.push(JSON.stringify(k) + ':' + stableStringify(value[k])); } return '{' + parts.join(',') + '}'; } export function daemonInstanceIdFor(dir) { return crypto.createHash('sha256') .update(normalizeComparable(path.resolve(dir))).digest('hex').slice(0, 24); } export function resolveStateDir(scriptDir, custom = '') { const secure = (dir) => { mkdirSync(dir, { recursive: true, mode: 0o700 }); const st = statSync(dir); if (!st.isDirectory()) throw new Error('gatekeeper state 路径不是目录'); if (typeof process.getuid === 'function' && st.uid !== process.getuid()) { throw new Error('gatekeeper state 目录不属于当前用户'); } try { chmodSync(dir, 0o700); } catch { /* Windows 或不支持 chmod */ } return dir; }; const preferred = custom ? path.resolve(custom) : path.join(scriptDir, 'state'); try { return secure(preferred); } catch { const fallback = path.join(os.tmpdir(), 'zcode-gatekeeper-' + daemonInstanceIdFor(scriptDir)); return secure(fallback); } } const secretCache = new Map(); export function daemonSecretFor(stateDir) { const dir = path.resolve(stateDir); if (secretCache.has(dir)) return secretCache.get(dir); mkdirSync(dir, { recursive: true, mode: 0o700 }); const file = path.join(dir, 'daemon-secret'); const readExisting = () => { const s = readFileSync(file, 'utf8').trim(); if (!/^[a-f0-9]{64}$/i.test(s)) throw new Error('daemon secret 文件格式无效'); return s; }; let secret; try { secret = readExisting(); } catch (firstErr) { if (existsSync(file)) throw firstErr; const fresh = crypto.randomBytes(32).toString('hex'); try { writeFileSync(file, fresh + '\n', { encoding: 'utf8', mode: 0o600, flag: 'wx' }); } catch (e) { if (e?.code !== 'EEXIST') throw e; } secret = existsSync(file) ? readExisting() : fresh; } try { chmodSync(file, 0o600); } catch { /* Windows 或不支持 chmod */ } secretCache.set(dir, secret); return secret; } function daemonMac(secret, direction, nonce, value) { return crypto.createHmac('sha256', secret) .update(direction + '\0' + nonce + '\0' + stableStringify(value)).digest('hex'); } function sameMac(a, b) { if (!/^[a-f0-9]{64}$/i.test(String(a)) || !/^[a-f0-9]{64}$/i.test(String(b))) return false; return crypto.timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex')); } export function makeDaemonRequest(secret, payload) { const nonce = crypto.randomBytes(16).toString('hex'); const issuedAt = Date.now(); return { nonce, issuedAt, payload, auth: daemonMac(secret, 'request', nonce, { issuedAt, payload }) }; } export function verifyDaemonRequest(secret, envelope) { return Boolean(envelope && typeof envelope.nonce === 'string' && Number.isFinite(envelope.issuedAt) && Math.abs(Date.now() - envelope.issuedAt) <= 60_000 && envelope.payload && sameMac(envelope.auth, daemonMac(secret, 'request', envelope.nonce, { issuedAt: envelope.issuedAt, payload: envelope.payload }))); } export function makeDaemonResponse(secret, nonce, result) { return { nonce, result, auth: daemonMac(secret, 'response', nonce, result) }; } export function verifyDaemonResponse(secret, envelope, nonce) { return Boolean(envelope && envelope.nonce === nonce && envelope.result && sameMac(envelope.auth, daemonMac(secret, 'response', nonce, envelope.result))); }