@fiscalmindset/blindfold 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/assets/SKILL.md +205 -0
- package/assets/blindfold_proxy.wasm +0 -0
- package/bin/blindfold.ts +90 -0
- package/bin/cli-shared.ts +72 -0
- package/bin/cmd-auth.ts +193 -0
- package/bin/cmd-enclave.ts +373 -0
- package/bin/cmd-lifecycle.ts +151 -0
- package/bin/cmd-secrets.ts +136 -0
- package/bin/cmd-serve.ts +165 -0
- package/bin/cmd-tenant.ts +84 -0
- package/dist/cli.mjs +4317 -0
- package/dist/lib/index.mjs +2362 -0
- package/dist/lib/proxy.mjs +1078 -0
- package/dist/lib/register.mjs +756 -0
- package/dist/lib/wrap.mjs +81 -0
- package/package.json +48 -0
- package/postinstall.mjs +21 -0
- package/src/attest.ts +235 -0
- package/src/color.ts +31 -0
- package/src/compat.ts +217 -0
- package/src/constants.ts +24 -0
- package/src/dashboard.ts +785 -0
- package/src/env.ts +261 -0
- package/src/index.ts +11 -0
- package/src/init.ts +391 -0
- package/src/keychain.ts +155 -0
- package/src/log.ts +51 -0
- package/src/migrate.ts +135 -0
- package/src/prompt.ts +114 -0
- package/src/providers.ts +224 -0
- package/src/proxy.ts +454 -0
- package/src/register.ts +81 -0
- package/src/release.ts +82 -0
- package/src/sealed-ledger.ts +158 -0
- package/src/t3-client.ts +580 -0
- package/src/types.ts +50 -0
- package/src/usage-log.ts +149 -0
- package/src/versions.ts +64 -0
- package/src/wrap.ts +122 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only, tamper-evident ledger of "what's sealed in the enclave right now".
|
|
3
|
+
*
|
|
4
|
+
* SAFETY: metadata only — never the value. Each line records the key
|
|
5
|
+
* name, its source, its byte-length (so you can sanity-check after
|
|
6
|
+
* sealing), the tenant DID + map name (so you know exactly which
|
|
7
|
+
* enclave + map holds it), and when. No plaintext, ever.
|
|
8
|
+
*
|
|
9
|
+
* INTEGRITY: each entry carries a `prev` + `hash` forming a hash-chain, so
|
|
10
|
+
* any edit or deletion of a past line is detectable (`verifyLedgerChain`).
|
|
11
|
+
* The enclave remains the source of truth — `blindfold audit` reconciles this
|
|
12
|
+
* ledger against it.
|
|
13
|
+
*
|
|
14
|
+
* Default path: ./.blindfold/sealed.jsonl (override via BLINDFOLD_SEALED_LOG)
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
19
|
+
import { stateDir, withFileLockSync } from "./env.ts";
|
|
20
|
+
|
|
21
|
+
export interface SealedEntry {
|
|
22
|
+
t: string; // ISO timestamp
|
|
23
|
+
name: string; // KV key inside z:<tid>:secrets
|
|
24
|
+
source: string; // "stdin" | "env:VAR" | "explicit"
|
|
25
|
+
length: number; // byte-count of the value (NOT the value)
|
|
26
|
+
mode: "real" | "mock";
|
|
27
|
+
tenant_did: string;
|
|
28
|
+
map_name: string; // z:<tid>:secrets
|
|
29
|
+
prev?: string; // hash of the previous chained entry ("" for the first)
|
|
30
|
+
hash?: string; // HMAC(key, prev + "\n" + core) — tamper-evidence
|
|
31
|
+
alg?: string; // "hmac-sha256" for keyed entries; absent = legacy sha256
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function defaultSealedLogPath(): string {
|
|
35
|
+
return process.env.BLINDFOLD_SEALED_LOG ?? path.join(stateDir(), "sealed.jsonl");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Local HMAC key for the ledger chain. Unlike the previous plain sha256 chain
|
|
40
|
+
* (which anyone could recompute), this key is not derivable from the ledger, so
|
|
41
|
+
* an attacker who edits a line cannot forge a valid chain. Persisted to
|
|
42
|
+
* .blindfold/ledger.key (0600), generated on first use. Returns null if the key
|
|
43
|
+
* can't be obtained — callers then fall back to the legacy sha256 chain.
|
|
44
|
+
*/
|
|
45
|
+
function ledgerKey(): Buffer | null {
|
|
46
|
+
try {
|
|
47
|
+
const keyPath = path.join(stateDir(), "ledger.key");
|
|
48
|
+
if (fs.existsSync(keyPath)) return Buffer.from(fs.readFileSync(keyPath, "utf8").trim(), "hex");
|
|
49
|
+
fs.mkdirSync(path.dirname(keyPath), { recursive: true });
|
|
50
|
+
const key = randomBytes(32);
|
|
51
|
+
fs.writeFileSync(keyPath, key.toString("hex"), { mode: 0o600 });
|
|
52
|
+
try { fs.chmodSync(keyPath, 0o600); } catch { /* best effort */ }
|
|
53
|
+
return key;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Canonical serialization of the metadata fields (excludes prev/hash). */
|
|
60
|
+
function coreString(e: SealedEntry): string {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
t: e.t, name: e.name, source: e.source, length: e.length,
|
|
63
|
+
mode: e.mode, tenant_did: e.tenant_did, map_name: e.map_name,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sha(s: string): string {
|
|
68
|
+
return createHash("sha256").update(s).digest("hex");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function chainHash(key: Buffer | null, prev: string, core: string): { hash: string; alg?: string } {
|
|
72
|
+
if (key) return { hash: createHmac("sha256", key).update(`${prev}\n${core}`).digest("hex"), alg: "hmac-sha256" };
|
|
73
|
+
return { hash: sha(`${prev}\n${core}`) }; // legacy fallback
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Efficiently read the last non-empty line without parsing the whole file. */
|
|
77
|
+
function readLastLine(p: string): string | null {
|
|
78
|
+
if (!fs.existsSync(p)) return null;
|
|
79
|
+
const fd = fs.openSync(p, "r");
|
|
80
|
+
try {
|
|
81
|
+
const size = fs.fstatSync(fd).size;
|
|
82
|
+
if (size === 0) return null;
|
|
83
|
+
const readLen = Math.min(size, 64 * 1024);
|
|
84
|
+
const buf = Buffer.alloc(readLen);
|
|
85
|
+
fs.readSync(fd, buf, 0, readLen, size - readLen);
|
|
86
|
+
const lines = buf.toString("utf8").split("\n").filter((l) => l.trim().length > 0);
|
|
87
|
+
return lines.length ? lines[lines.length - 1]! : null;
|
|
88
|
+
} finally {
|
|
89
|
+
fs.closeSync(fd);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function recordSealed(entry: SealedEntry): void {
|
|
94
|
+
const p = defaultSealedLogPath();
|
|
95
|
+
try {
|
|
96
|
+
// Serialize read-tail + append under an exclusive lock so two concurrent
|
|
97
|
+
// sealers can't read the same prevHash and fork the chain (false "TAMPERED").
|
|
98
|
+
withFileLockSync(p, () => {
|
|
99
|
+
let prevHash = "";
|
|
100
|
+
const last = readLastLine(p);
|
|
101
|
+
if (last) {
|
|
102
|
+
try { prevHash = (JSON.parse(last) as SealedEntry).hash ?? ""; } catch { prevHash = ""; }
|
|
103
|
+
}
|
|
104
|
+
const { hash, alg } = chainHash(ledgerKey(), prevHash, coreString(entry));
|
|
105
|
+
const chained: SealedEntry = { ...entry, prev: prevHash, hash, ...(alg ? { alg } : {}) };
|
|
106
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
107
|
+
fs.appendFileSync(p, JSON.stringify(chained) + "\n");
|
|
108
|
+
});
|
|
109
|
+
} catch {
|
|
110
|
+
/* never let logging crash the seal */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function readSealed(): SealedEntry[] {
|
|
115
|
+
const p = defaultSealedLogPath();
|
|
116
|
+
if (!fs.existsSync(p)) return [];
|
|
117
|
+
return fs.readFileSync(p, "utf8")
|
|
118
|
+
.split("\n").filter(Boolean)
|
|
119
|
+
.map(l => { try { return JSON.parse(l) as SealedEntry; } catch { return null; } })
|
|
120
|
+
.filter((e): e is SealedEntry => e !== null);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ChainResult {
|
|
124
|
+
ok: boolean;
|
|
125
|
+
total: number;
|
|
126
|
+
legacy: number; // entries with no hash (pre-chain) — unverifiable
|
|
127
|
+
firstBrokenIndex: number; // -1 if intact
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Verify the hash-chain. A broken chain means a line was edited or removed
|
|
132
|
+
* after it was written — the ledger has been tampered with.
|
|
133
|
+
*/
|
|
134
|
+
export function verifyLedgerChain(): ChainResult {
|
|
135
|
+
const entries = readSealed();
|
|
136
|
+
const key = ledgerKey();
|
|
137
|
+
let runningPrev = "";
|
|
138
|
+
let legacy = 0;
|
|
139
|
+
let firstBrokenIndex = -1;
|
|
140
|
+
entries.forEach((e, i) => {
|
|
141
|
+
if (!e.hash) { legacy++; return; } // no hash at all — pre-chain, unverifiable
|
|
142
|
+
if (e.alg === "hmac-sha256") {
|
|
143
|
+
// Keyed entry — real tamper detection (requires the local key).
|
|
144
|
+
const expected = key ? createHmac("sha256", key).update(`${runningPrev}\n${coreString(e)}`).digest("hex") : null;
|
|
145
|
+
if (firstBrokenIndex < 0 && (e.prev !== runningPrev || (expected !== null && e.hash !== expected))) {
|
|
146
|
+
firstBrokenIndex = i;
|
|
147
|
+
}
|
|
148
|
+
if (expected === null) legacy++; // key unavailable → can't verify, don't cry tamper
|
|
149
|
+
} else {
|
|
150
|
+
// Legacy plain-sha256 entry: recomputable by anyone, so treat as
|
|
151
|
+
// unverifiable rather than authoritative. The enclave (blindfold audit)
|
|
152
|
+
// remains the source of truth for these.
|
|
153
|
+
legacy++;
|
|
154
|
+
}
|
|
155
|
+
runningPrev = e.hash;
|
|
156
|
+
});
|
|
157
|
+
return { ok: firstBrokenIndex < 0, total: entries.length, legacy, firstBrokenIndex };
|
|
158
|
+
}
|