/** * Tamper-evident in-session audit chain (hash-linked NDJSON rows; no persistence unless host appends). */ import bareCrypto from 'bare-crypto' import b4a from 'b4a' const { createHash } = bareCrypto let nodeCreateHash = null try { const nodeCrypto = await import('crypto') if (typeof nodeCrypto?.createHash === 'function') { nodeCreateHash = nodeCrypto.createHash } } catch { /* Bare/Pear runtime: no Node crypto module */ } /** @type {string | null} */ let chainHeadHex = null let seq = 0 /** @type {Array<{ seq:number, payload: Record, entryHash:string, prevHash:string|null }>} */ const auditRows = [] function hexU8(u8) { let s = '' for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0') return s } /** * @param {string | null} prev * @param {string} payload * @returns {Uint8Array} */ function sha256LinkDigest(prev, payload) { if (nodeCreateHash) { const h = nodeCreateHash('sha256') if (prev) h.update(prev, 'utf8') h.update('\n', 'utf8') h.update(payload, 'utf8') const d = h.digest() return d instanceof Uint8Array ? d : new Uint8Array(d) } const h = createHash('sha256') if (prev) h.update(b4a.from(String(prev), 'utf8')) h.update(b4a.from('\n', 'utf8')) h.update(b4a.from(String(payload), 'utf8')) const digest = h.digest() return digest instanceof Uint8Array ? digest : new Uint8Array(digest) } /** * @param {Record} entry * @returns {{ seq: number, entryHash: string, prevHash: string | null }} */ export function bareOsAuditAppend(entry) { seq++ const prev = chainHeadHex const safeEntry = /** @type {Record} */ (entry || {}) const payload = JSON.stringify(safeEntry) const u8 = sha256LinkDigest(prev, payload) chainHeadHex = hexU8(u8) auditRows.push({ seq, payload: safeEntry, entryHash: chainHeadHex, prevHash: prev }) return { seq, entryHash: chainHeadHex, prevHash: prev } } /** * Append a single chain link whose payload is a batch of sub-entries (one Merkle-style update). * @param {Record[]} entries * @returns {{ seq: number, entryHash: string, prevHash: string | null } | null} */ export function bareOsAuditAppendBatch(entries) { if (!Array.isArray(entries) || entries.length === 0) return null return bareOsAuditAppend({ kind: 'batch', batchSize: entries.length, entries: entries.slice(0, 256) }) } export function bareOsAuditChainHead() { return chainHeadHex } export function bareOsAuditChainLength() { return seq } export function bareOsAuditChainSnapshot() { return { schema: 1, length: seq, headHex: chainHeadHex, atMs: Date.now() } } /** * Persist current audit rows as NDJSON (append-only host/logical sink path). * @param {Record} ctx * @param {string} logicalPath */ export async function bareOsAuditPersistRows(ctx, logicalPath) { const vfs = ctx?.vfs if (!vfs || typeof vfs.writeFile !== 'function' || typeof ctx?.b4a?.from !== 'function') { return false } const out = auditRows .map((r) => JSON.stringify({ schema: 1, ...r })) .join('\n') await vfs.writeFile(String(logicalPath || '/var/log/bare-os/audit-chain.ndjson'), ctx.b4a.from(out + '\n')) return true } /** * Verify a sequence of audit rows with hash-link integrity. * Rows must contain a stable payload object at `row.payload` and expected hash at `row.entryHash`. * @param {{ payload: Record, entryHash: string }[]} rows * @returns {{ ok: boolean, badIndex: number, expected?: string, got?: string }} */ export function bareOsAuditVerifyRows(rows) { let prev = null for (let i = 0; i < rows.length; i++) { const r = rows[i] const payload = JSON.stringify(r.payload || {}) const u8 = sha256LinkDigest(prev, payload) const want = hexU8(u8) const got = String(r.entryHash || '') if (want !== got) return { ok: false, badIndex: i, expected: want, got } prev = got } return { ok: true, badIndex: -1 } }