enigma-memory 0.1.18 → 0.1.22
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/README.md +76 -24
- package/apps/cli/bin/enigma-desktop.mjs +140 -0
- package/apps/cli/bin/enigma-terminal.mjs +78 -0
- package/apps/cli/bin/enigma.mjs +1923 -285
- package/apps/desktop/electron-main.cjs +217 -0
- package/apps/desktop/package.json +12 -0
- package/apps/desktop/src/app.js +264 -7
- package/apps/desktop/src/index.html +3514 -1373
- package/apps/desktop/src/launch-electron.mjs +51 -0
- package/apps/desktop/src/server.mjs +2914 -0
- package/apps/desktop/src/styles.css +2972 -260
- package/apps/desktop/src/zk-browser-prove.mjs +53 -0
- package/apps/desktop/src/zk-state.mjs +1789 -0
- package/apps/gateway/bin/enigma-gateway.mjs +102 -5
- package/apps/gateway/src/server.mjs +271 -8
- package/apps/ios/EnigmaCore/Package.swift +12 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/EnigmaAPIClient.swift +227 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/Models.swift +278 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/PKCE.swift +96 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/PrivacyMinimizer.swift +187 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/ToolModels.swift +129 -0
- package/apps/ios/EnigmaCore/Tests/EnigmaCoreTests/EnigmaCoreTests.swift +42 -0
- package/apps/ios/EnigmaIOS/Enigma/AppModel.swift +346 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AccentColor.colorset/Contents.json +12 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/Contents.json +11 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/EnigmaAppIcon.png +0 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/Contents.json +3 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/LaunchBackground.colorset/Contents.json +12 -0
- package/apps/ios/EnigmaIOS/Enigma/ChatView.swift +181 -0
- package/apps/ios/EnigmaIOS/Enigma/CouncilView.swift +78 -0
- package/apps/ios/EnigmaIOS/Enigma/CreateView.swift +152 -0
- package/apps/ios/EnigmaIOS/Enigma/EnigmaApp.swift +52 -0
- package/apps/ios/EnigmaIOS/Enigma/Info.plist +52 -0
- package/apps/ios/EnigmaIOS/Enigma/NaturalLanguagePrivacyTagger.swift +26 -0
- package/apps/ios/EnigmaIOS/Enigma/OAuthClient.swift +321 -0
- package/apps/ios/EnigmaIOS/Enigma/OnboardingView.swift +105 -0
- package/apps/ios/EnigmaIOS/Enigma/PrivateVaultView.swift +275 -0
- package/apps/ios/EnigmaIOS/Enigma/SecureStore.swift +76 -0
- package/apps/ios/EnigmaIOS/Enigma/SettingsView.swift +60 -0
- package/apps/ios/EnigmaIOS/Enigma/Theme.swift +80 -0
- package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/project.pbxproj +211 -0
- package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/xcshareddata/xcschemes/Enigma.xcscheme +23 -0
- package/apps/native-host/README.md +19 -8
- package/apps/native-host/bin/enigma-native-host.mjs +229 -13
- package/apps/relay/bin/enigma-relay.mjs +103 -5
- package/apps/relay/src/federation-runtime.mjs +618 -0
- package/apps/relay/src/server.mjs +310 -9
- package/apps/verifier/bin/enigma-verify.mjs +327 -11
- package/cortex-v3/circuits/build/intent_vk_bytes.json +35 -0
- package/cortex-v3/circuits/build/sale_vk_bytes.json +35 -0
- package/cortex-v3/circuits/build/vk_bytes.json +32 -0
- package/cortex-v3/proving-assets.json +64 -0
- package/cortex-v3/zk/BUILD-CONTRACT.md +87 -0
- package/cortex-v3/zk/action-transition-vk.json +119 -0
- package/cortex-v3/zk/alias-adversarial.test.mjs +220 -0
- package/cortex-v3/zk/groth16-verify-child.mjs +17 -0
- package/cortex-v3/zk/intent-witness.mjs +365 -0
- package/cortex-v3/zk/intent-witness.test.mjs +485 -0
- package/cortex-v3/zk/proving-assets.mjs +203 -0
- package/cortex-v3/zk/sale-witness.mjs +783 -0
- package/cortex-v3/zk/sale-witness.test.mjs +784 -0
- package/cortex-v3/zk/sealed-sale-release-vk.json +119 -0
- package/cortex-v3/zk/settlement-evidence.mjs +722 -0
- package/cortex-v3/zk/setup-intent.mjs +688 -0
- package/cortex-v3/zk/setup-sale.mjs +666 -0
- package/cortex-v3/zk/setup.mjs +594 -0
- package/cortex-v3/zk/witness.mjs +184 -0
- package/cortex-v3/zk/zk-codec.mjs +232 -0
- package/cortex-v3/zk/zk-codec.test.mjs +293 -0
- package/cortex-v3/zk/zk-settle.mjs +370 -0
- package/cortex-v3/zk/zk-tree.mjs +256 -0
- package/cortex-v3/zk/zk-tree.test.mjs +419 -0
- package/deploy/docker-compose.local-production-simulation.yml +36 -0
- package/docs/browser-extension-install.md +8 -6
- package/docs/client-connectors.md +15 -11
- package/docs/developer-ecosystem.md +15 -13
- package/docs/enigma-memory-ready-conformance.md +11 -9
- package/docs/install-anywhere.md +61 -28
- package/docs/installers-and-desktop.md +8 -7
- package/docs/novelty-invention-candidates.md +161 -161
- package/docs/proof-network-claim-boundaries.md +320 -318
- package/examples/01-quickstart-agent/index.mjs +49 -0
- package/examples/01_agent_memory_quickstart.mjs +57 -0
- package/examples/02-multi-agent-swarm/index.mjs +57 -0
- package/examples/02_cross_model_passport.mjs +64 -0
- package/examples/03-langchain-memory/index.mjs +41 -0
- package/examples/03_poseidon_commitment_verification.mjs +71 -0
- package/examples/04-python-trading-agent/trader.py +49 -0
- package/examples/README.md +27 -0
- package/examples/ci/github-actions.yml +7 -2
- package/package.json +142 -11
- package/packages/adapters/PACKAGE_CONTRACT.md +1 -1
- package/packages/connectors/src/index.js +196 -4
- package/packages/connectors/swarm-router.mjs +168 -0
- package/packages/core/src/index.js +248 -1
- package/packages/core/src/version.mjs +7 -0
- package/packages/dev-tools/package.json +19 -0
- package/packages/dev-tools/src/index.js +4 -0
- package/packages/dev-tools/src/memory-benchmark-suite.js +112 -0
- package/packages/dev-tools/src/swarm-simulator.js +101 -0
- package/packages/dev-tools/src/vault-inspector.js +114 -0
- package/packages/dev-tools/src/vector-benchmark.js +100 -0
- package/packages/developer-platform/src/access-credentials.js +341 -0
- package/packages/developer-platform/src/http.js +132 -0
- package/packages/developer-platform/src/index.js +4 -0
- package/packages/developer-platform/src/usage-http.js +60 -0
- package/packages/developer-platform/src/usage.js +295 -0
- package/packages/enclave-runtime/attestation.mjs +159 -0
- package/packages/enclave-runtime/index.mjs +47 -0
- package/packages/enclave-runtime/session-manager.mjs +253 -0
- package/packages/enclave-runtime/zeroization-proof.mjs +227 -0
- package/packages/enigma-reflex/package.json +14 -0
- package/packages/enigma-reflex/src/index.js +204 -0
- package/packages/enigma-reflex/training/generate-dataset.mjs +40 -0
- package/packages/enigma-reflex/training/requirements.txt +8 -0
- package/packages/enigma-reflex/training/train.py +314 -0
- package/packages/enigma-weave/LICENSE +22 -0
- package/packages/enigma-weave/UPSTREAM.json +21 -0
- package/packages/enigma-weave/package.json +14 -0
- package/packages/enigma-weave/src/index.js +286 -0
- package/packages/hosted-cloud/src/index.js +80 -5
- package/packages/importers/src/index.js +432 -0
- package/packages/inference-runtime/src/browser.js +401 -0
- package/packages/inference-runtime/src/chat.js +265 -0
- package/packages/inference-runtime/src/code.js +407 -0
- package/packages/inference-runtime/src/contracts.js +162 -0
- package/packages/inference-runtime/src/http.js +232 -0
- package/packages/inference-runtime/src/image.js +186 -0
- package/packages/inference-runtime/src/index.js +10 -0
- package/packages/inference-runtime/src/model-router.js +320 -0
- package/packages/inference-runtime/src/platform.js +125 -0
- package/packages/inference-runtime/src/privacy.js +400 -0
- package/packages/inference-runtime/src/video.js +253 -0
- package/packages/mcp-server/README.md +22 -6
- package/packages/mcp-server/bin/enigma-mcp.mjs +2 -1
- package/packages/mcp-server/src/index.js +1418 -105
- package/packages/mcp-server/src/oauth.js +561 -0
- package/packages/mcp-server/src/private-handoff.js +84 -0
- package/packages/mcp-server/src/remote-http.js +273 -0
- package/packages/mcp-server/src/remote-policy.js +72 -0
- package/packages/mcp-server/swarm-bridge.mjs +361 -0
- package/packages/mesh/index.d.ts +283 -0
- package/packages/mesh/package.json +23 -0
- package/packages/mesh/src/crypto.js +189 -0
- package/packages/mesh/src/federation-packets.js +353 -0
- package/packages/mesh/src/gossip.js +311 -0
- package/packages/mesh/src/index.js +6 -0
- package/packages/mesh/src/protocol.js +255 -0
- package/packages/mesh/src/router.js +279 -0
- package/packages/mesh/src/transport.js +306 -0
- package/packages/passport/src/index.js +426 -1
- package/packages/private-economy/src/credits-http.js +100 -0
- package/packages/private-economy/src/credits.js +447 -0
- package/packages/private-economy/src/index.js +5 -0
- package/packages/private-economy/src/payments-http.js +120 -0
- package/packages/private-economy/src/payments.js +509 -0
- package/packages/private-economy/src/x402.js +346 -0
- package/packages/proof-network/PACKAGE_CONTRACT.md +21 -0
- package/packages/rag/index.d.ts +182 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/THIRD_PARTY_LICENSES.txt +207 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/config.json +25 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/onnx/model_quantized.onnx +0 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/sha256-manifest.json +28 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer.json +30686 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer_config.json +15 -0
- package/packages/rag/package.json +27 -0
- package/packages/rag/src/blinded-search.js +109 -0
- package/packages/rag/src/bm25.js +169 -0
- package/packages/rag/src/embeddings.js +459 -0
- package/packages/rag/src/hybrid.js +76 -0
- package/packages/rag/src/index.js +38 -0
- package/packages/rag/src/reranker.js +61 -0
- package/packages/rag/src/research.js +107 -0
- package/packages/rag/src/vector-store.js +430 -0
- package/packages/rag/src/verify-model-artifacts.mjs +4 -0
- package/packages/sdk/index.d.ts +760 -0
- package/packages/sdk/package.json +33 -0
- package/packages/sdk/python/README.md +24 -0
- package/packages/sdk/python/enigma_sdk.py +250 -0
- package/packages/sdk/python/pyproject.toml +34 -0
- package/packages/sdk/python/requirements.txt +1 -0
- package/packages/sdk/python/setup.py +20 -0
- package/packages/sdk/src/federation/capability-grant.js +389 -0
- package/packages/sdk/src/federation/federation-router.js +360 -0
- package/packages/sdk/src/federation/ghostmesh-bridge.js +497 -0
- package/packages/sdk/src/federation/index.js +3 -0
- package/packages/sdk/src/index.js +1796 -0
- package/packages/sdk/src/intelligence/contradiction.js +337 -0
- package/packages/sdk/src/intelligence/decision-engine.js +155 -0
- package/packages/sdk/src/intelligence/index.js +4 -0
- package/packages/sdk/src/intelligence/ontology.js +122 -0
- package/packages/sdk/src/intelligence/temporal.js +123 -0
- package/packages/sdk/src/market-client.js +142 -0
- package/packages/sdk/src/mesh-client.js +110 -0
- package/packages/sdk/src/middleware/index.js +3 -0
- package/packages/sdk/src/middleware/langchain.js +159 -0
- package/packages/sdk/src/middleware/llamaindex.js +101 -0
- package/packages/sdk/src/middleware/vercel-ai.js +112 -0
- package/packages/sdk/src/rag-client.js +85 -0
- package/packages/sdk/src/swarm-orchestrator.js +260 -0
- package/packages/settlement/PACKAGE_CONTRACT.md +1 -1
- package/packages/snapcompact/THIRD_PARTY_LICENSES.txt +40 -0
- package/packages/snapcompact/assets/8x13-latin1.bdf +3837 -0
- package/packages/snapcompact/index.d.ts +284 -0
- package/packages/snapcompact/package.json +25 -0
- package/packages/snapcompact/src/index.js +716 -0
- package/packages/storage/PACKAGE_CONTRACT.md +1 -1
- package/packages/terminal-console/animations.mjs +240 -0
- package/packages/terminal-console/auto-anchor.mjs +220 -0
- package/packages/terminal-console/banner.mjs +91 -0
- package/packages/terminal-console/commands.mjs +459 -0
- package/packages/terminal-console/delegation.mjs +152 -0
- package/packages/terminal-console/index.mjs +5 -0
- package/packages/terminal-console/outbox.mjs +143 -0
- package/packages/terminal-console/phantom-bridge.mjs +637 -0
- package/packages/terminal-console/repl.mjs +136 -0
- package/packages/terminal-console/signer-store.mjs +130 -0
- package/packages/terminal-console/solana-rpc.mjs +214 -0
- package/packages/terminal-console/solana-transport.mjs +189 -0
- package/packages/terminal-tui/dashboard.mjs +214 -0
- package/packages/terminal-tui/index.mjs +28 -0
- package/packages/terminal-tui/merkle-tree-renderer.mjs +268 -0
- package/packages/terminal-tui/telemetry-hud.mjs +137 -0
- package/packages/vault/index.d.ts +449 -0
- package/packages/vault/package.json +27 -0
- package/packages/vault/src/e2ee.mjs +393 -0
- package/packages/vault/src/enclave.js +481 -0
- package/packages/vault/src/erasure.js +207 -0
- package/packages/vault/src/index.js +1018 -155
- package/packages/vault/src/persistence.js +307 -0
- package/packages/vault/src/poseidon.js +354 -0
- package/packages/vault/src/receipt.js +459 -0
- package/scripts/benchmark-optical-context.mjs +166 -0
- package/scripts/bootstrap-enigma.mjs +502 -0
- package/scripts/build-edge-backend-workers.mjs +20 -5
- package/scripts/build-goal-completion-audit.mjs +72 -25
- package/scripts/build-hosted-api-key-lifecycle.mjs +26 -8
- package/scripts/build-hosted-customer-lifecycle.mjs +20 -3
- package/scripts/build-hosted-probe-worker.mjs +19 -4
- package/scripts/build-installer-assets.mjs +41 -21
- package/scripts/build-operator-evidence-starter.mjs +59 -1
- package/scripts/build-production-backend-env-kit.mjs +2 -0
- package/scripts/build-production-unblocker.mjs +3 -0
- package/scripts/check.mjs +17 -3
- package/scripts/collect-hosted-backend-live-evidence.mjs +49 -12
- package/scripts/install-enigma-local.mjs +18 -5
- package/scripts/release-audit.mjs +65 -109
- package/scripts/release-provenance.mjs +6 -0
- package/scripts/run-backend-readiness-smoke.mjs +112 -10
- package/scripts/scan-secrets.mjs +1 -0
- package/scripts/simulate-production-env.mjs +7 -2
- package/scripts/validate-hosted-backend-live.mjs +112 -1
- package/specs/antibody-pack-v1.schema.json +95 -0
- package/specs/antigen-envelope-v1.schema.json +81 -0
- package/specs/boundary-manifest-v1.schema.json +35 -35
- package/specs/capsule-v1.schema.json +55 -55
- package/specs/claim-boundary-manifest-v1.schema.json +22 -22
- package/specs/claim-ledger-v1.schema.json +291 -0
- package/specs/context-passport-v1.schema.json +59 -0
- package/specs/deletion-tombstone-v1.schema.json +26 -26
- package/specs/evidence-packet-v1.schema.json +177 -0
- package/specs/hosted-backend-live-evidence-v1.schema.json +72 -3
- package/specs/immune-scan-report-v1.schema.json +112 -0
- package/specs/lifecycle-receipt-log-v1.schema.json +67 -0
- package/specs/memory-atom-v1.schema.json +59 -0
- package/specs/memory-event-v1.schema.json +42 -42
- package/specs/passport-v1.schema.json +50 -50
- package/specs/proof-of-non-use-v1.schema.json +65 -0
- package/specs/quarantine-record-v1.schema.json +126 -0
- package/specs/receipt-v1.schema.json +61 -61
- package/specs/state-checkpoint-v1.schema.json +37 -37
- package/specs/trust-bundle-v1.schema.json +56 -56
- package/specs/trust-card-v1.schema.json +119 -0
- package/docs/proof-network-launch-plan.md +0 -421
- package/packages/metering/PACKAGE_CONTRACT.md +0 -20
- package/scripts/build-ai-orchestration-plan.mjs +0 -248
|
@@ -13,18 +13,22 @@ import {
|
|
|
13
13
|
} from 'node:crypto';
|
|
14
14
|
import {
|
|
15
15
|
canonicalize,
|
|
16
|
-
createMemoryAddress,
|
|
17
16
|
createReceipt,
|
|
18
17
|
generateSigningKeyPair,
|
|
19
18
|
MerkleSet,
|
|
20
19
|
hmacSha256Hex,
|
|
21
20
|
receiptHash,
|
|
22
21
|
sha256Hex,
|
|
22
|
+
verifyReceiptChain,
|
|
23
23
|
} from '../../core/src/index.js';
|
|
24
24
|
|
|
25
|
-
const VAULT_SCHEMA = 'enigma.vault.local.v1';
|
|
26
|
-
const
|
|
27
|
-
const
|
|
25
|
+
export const VAULT_SCHEMA = 'enigma.vault.local.v1';
|
|
26
|
+
export const VAULT_BUNDLE_SCHEMA = 'enigma.vault_bundle.v1';
|
|
27
|
+
export const MEMORY_OBJECT_SCHEMA = 'enigma.memory_object.v1';
|
|
28
|
+
export const EVENT_SCHEMA = 'enigma.memory_event.v1';
|
|
29
|
+
const MEMORY_DOMAIN = 'enigma.memory.v2';
|
|
30
|
+
const CONTENT_AAD_DOMAIN = 'enigma.memory.content_aad.v1';
|
|
31
|
+
const PRIVATE_MEMORY_SCHEMA = 'enigma.memory_private.v1';
|
|
28
32
|
const ZERO_ROOT = new MerkleSet().root();
|
|
29
33
|
const RECEIPT_PLAINTEXT_KEYS = new Set(['content', 'plaintext', 'text', 'memory', 'prompt', 'response']);
|
|
30
34
|
const PUBLIC_PLAINTEXT_KEYS = new Set([
|
|
@@ -49,9 +53,20 @@ const PUBLIC_PLAINTEXT_KEYS = new Set([
|
|
|
49
53
|
]);
|
|
50
54
|
const SAFE_METADATA_DIGEST_RE = /^(?:sha256:)?[a-f0-9]{64}$|^hmac-sha256:[a-f0-9]{64}$/;
|
|
51
55
|
const SOURCE_REF_ROOT_RE = /^sha256:[a-f0-9]{64}$/;
|
|
52
|
-
|
|
53
56
|
export const KEYRING_ENCRYPTED_TYPE = 'local_secret_encrypted';
|
|
54
57
|
export const KEYRING_PLAINTEXT_TYPE = 'local_secret';
|
|
58
|
+
export const DEFAULT_KDF_SPEC = {
|
|
59
|
+
algorithm: 'scrypt',
|
|
60
|
+
n: 16384,
|
|
61
|
+
r: 8,
|
|
62
|
+
p: 1,
|
|
63
|
+
maxmem: 64 * 1024 * 1024,
|
|
64
|
+
};
|
|
65
|
+
export const PBKDF2_KDF_SPEC = {
|
|
66
|
+
algorithm: 'pbkdf2',
|
|
67
|
+
iterations: 600000,
|
|
68
|
+
hash: 'sha256',
|
|
69
|
+
};
|
|
55
70
|
const DERIVED_KEY_LENGTH = 128; // 32 bytes each: KEK, vaultKey, addressKey, signingSeed
|
|
56
71
|
const SALT_LENGTH = 16;
|
|
57
72
|
const IV_LENGTH = 12;
|
|
@@ -99,44 +114,54 @@ function isSafeMetadataPrimitive(key, value) {
|
|
|
99
114
|
return false;
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
function
|
|
103
|
-
|
|
117
|
+
function privateValueCommitment(key, value) {
|
|
118
|
+
const encoded = typeof value === 'string' ? value : canonical(value);
|
|
119
|
+
return key ? `sha256:${asHmacSha256(key, encoded)}` : asSha256(encoded);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function commitMetadataValue(out, key, value, commitmentKey) {
|
|
123
|
+
out[metadataCommitmentKey(key)] = privateValueCommitment(
|
|
124
|
+
commitmentKey,
|
|
125
|
+
isMetadataPrimitive(value) ? String(value) : value,
|
|
126
|
+
);
|
|
104
127
|
}
|
|
105
128
|
|
|
106
|
-
function sanitizeSourceRefValue(value) {
|
|
129
|
+
function sanitizeSourceRefValue(value, commitmentKey) {
|
|
107
130
|
if (value === undefined || value === null) return undefined;
|
|
108
|
-
if (typeof value === 'string')
|
|
131
|
+
if (typeof value === 'string') {
|
|
132
|
+
return SAFE_METADATA_DIGEST_RE.test(value) ? value : privateValueCommitment(commitmentKey, value);
|
|
133
|
+
}
|
|
109
134
|
if (['number', 'boolean'].includes(typeof value)) return value;
|
|
110
|
-
if (Array.isArray(value)) return value.map(sanitizeSourceRefValue).filter((item) => item !== undefined);
|
|
135
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeSourceRefValue(item, commitmentKey)).filter((item) => item !== undefined);
|
|
111
136
|
if (typeof value !== 'object') return undefined;
|
|
112
137
|
|
|
113
138
|
const out = {};
|
|
114
139
|
for (const [key, item] of Object.entries(value)) {
|
|
115
140
|
if (isSourceRefPlaintextLikeKey(key)) {
|
|
116
141
|
if (item !== undefined && item !== null && ['string', 'number', 'boolean'].includes(typeof item)) {
|
|
117
|
-
out[privateCommitmentKey(key)] =
|
|
142
|
+
out[privateCommitmentKey(key)] = privateValueCommitment(commitmentKey, String(item));
|
|
118
143
|
}
|
|
119
144
|
continue;
|
|
120
145
|
}
|
|
121
|
-
if (key === 'source_hash' && typeof item === 'string' &&
|
|
146
|
+
if (key === 'source_hash' && typeof item === 'string' && SAFE_METADATA_DIGEST_RE.test(item)) {
|
|
122
147
|
out[key] = item;
|
|
123
148
|
continue;
|
|
124
149
|
}
|
|
125
|
-
const sanitized = sanitizeSourceRefValue(item);
|
|
150
|
+
const sanitized = sanitizeSourceRefValue(item, commitmentKey);
|
|
126
151
|
if (sanitized !== undefined) out[key] = sanitized;
|
|
127
152
|
}
|
|
128
153
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
129
154
|
}
|
|
130
155
|
|
|
131
|
-
function sanitizeSourceRefs(refs) {
|
|
156
|
+
function sanitizeSourceRefs(refs, commitmentKey) {
|
|
132
157
|
if (!Array.isArray(refs)) return [];
|
|
133
158
|
const out = [];
|
|
134
159
|
for (const ref of refs) {
|
|
135
160
|
if (typeof ref === 'string') {
|
|
136
|
-
out.push({ source_hash:
|
|
161
|
+
out.push({ source_hash: privateValueCommitment(commitmentKey, ref) });
|
|
137
162
|
continue;
|
|
138
163
|
}
|
|
139
|
-
const sanitized = sanitizeSourceRefValue(ref);
|
|
164
|
+
const sanitized = sanitizeSourceRefValue(ref, commitmentKey);
|
|
140
165
|
if (sanitized !== undefined) out.push(sanitized);
|
|
141
166
|
}
|
|
142
167
|
return out;
|
|
@@ -187,12 +212,15 @@ function receiptEvent(receipt) {
|
|
|
187
212
|
function deriveReceiptLifecycle(receipts) {
|
|
188
213
|
const active = new Set();
|
|
189
214
|
const tombstones = new Map();
|
|
215
|
+
let activeRoot = computeSetRoot(active);
|
|
190
216
|
for (const receipt of receipts) {
|
|
191
217
|
const event = receiptEvent(receipt);
|
|
192
218
|
if (!event) continue;
|
|
219
|
+
let activeChanged = false;
|
|
193
220
|
if (event.operation === 'create' && event.memory_addr) {
|
|
194
221
|
active.add(event.memory_addr);
|
|
195
222
|
tombstones.delete(event.memory_addr);
|
|
223
|
+
activeChanged = true;
|
|
196
224
|
} else if (event.operation === 'update' && event.memory_addr) {
|
|
197
225
|
if (event.source_addr) {
|
|
198
226
|
active.delete(event.source_addr);
|
|
@@ -207,6 +235,7 @@ function deriveReceiptLifecycle(receipts) {
|
|
|
207
235
|
}
|
|
208
236
|
active.add(event.memory_addr);
|
|
209
237
|
tombstones.delete(event.memory_addr);
|
|
238
|
+
activeChanged = true;
|
|
210
239
|
} else if (event.operation === 'delete' && event.memory_addr) {
|
|
211
240
|
active.delete(event.memory_addr);
|
|
212
241
|
tombstones.set(event.memory_addr, {
|
|
@@ -216,8 +245,14 @@ function deriveReceiptLifecycle(receipts) {
|
|
|
216
245
|
tombstoned_at: event.timestamp,
|
|
217
246
|
reason: 'deleted',
|
|
218
247
|
});
|
|
248
|
+
activeChanged = true;
|
|
249
|
+
} else if (event.operation === 'key_reset') {
|
|
250
|
+
active.clear();
|
|
251
|
+
tombstones.clear();
|
|
252
|
+
activeChanged = true;
|
|
219
253
|
}
|
|
220
|
-
if (
|
|
254
|
+
if (activeChanged) activeRoot = computeSetRoot(active);
|
|
255
|
+
if (typeof receipt.active_set_root === 'string' && receipt.active_set_root !== activeRoot) {
|
|
221
256
|
throw new Error('importBundle receipt active_set_root mismatch');
|
|
222
257
|
}
|
|
223
258
|
}
|
|
@@ -339,19 +374,55 @@ function buildEd25519Pkcs8(seed) {
|
|
|
339
374
|
]);
|
|
340
375
|
}
|
|
341
376
|
|
|
342
|
-
function
|
|
377
|
+
export function validateKdfSpec(kdfSpec = DEFAULT_KDF_SPEC) {
|
|
378
|
+
const algorithm = kdfSpec?.algorithm ?? 'scrypt';
|
|
379
|
+
if (algorithm === 'scrypt') {
|
|
380
|
+
const n = kdfSpec?.n ?? 16384;
|
|
381
|
+
const r = kdfSpec?.r ?? 8;
|
|
382
|
+
const p = kdfSpec?.p ?? 1;
|
|
383
|
+
const maxmem = kdfSpec?.maxmem ?? 64 * 1024 * 1024;
|
|
384
|
+
if (!Number.isInteger(n) || n < 16384 || n > 131072 || (n & (n - 1)) !== 0) {
|
|
385
|
+
throw new Error('scrypt n must be a power of two from 16384 through 131072');
|
|
386
|
+
}
|
|
387
|
+
if (!Number.isInteger(r) || r < 1 || r > 32) throw new Error('scrypt r must be an integer from 1 through 32');
|
|
388
|
+
if (!Number.isInteger(p) || p < 1 || p > 8) throw new Error('scrypt p must be an integer from 1 through 8');
|
|
389
|
+
const requiredMemory = 128 * n * r + 1024 * 1024;
|
|
390
|
+
if (!Number.isInteger(maxmem) || maxmem < requiredMemory || maxmem > 256 * 1024 * 1024) {
|
|
391
|
+
throw new Error(`scrypt maxmem must be an integer from ${requiredMemory} through 268435456`);
|
|
392
|
+
}
|
|
393
|
+
return { ...kdfSpec, algorithm, n, r, p, maxmem };
|
|
394
|
+
}
|
|
395
|
+
if (algorithm === 'pbkdf2') {
|
|
396
|
+
const iterations = kdfSpec?.iterations ?? 600000;
|
|
397
|
+
const hash = kdfSpec?.hash ?? 'sha256';
|
|
398
|
+
if (!Number.isInteger(iterations) || iterations < 100000 || iterations > 2000000) {
|
|
399
|
+
throw new Error('PBKDF2 iterations must be an integer from 100000 through 2000000');
|
|
400
|
+
}
|
|
401
|
+
if (!['sha256', 'sha512'].includes(hash)) throw new Error('PBKDF2 hash must be sha256 or sha512');
|
|
402
|
+
return { ...kdfSpec, algorithm, iterations, hash };
|
|
403
|
+
}
|
|
404
|
+
throw new Error(`Unsupported KDF algorithm: ${algorithm}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function deriveMasterKeyFromPassphrase(passphrase, salt, kdfSpec = DEFAULT_KDF_SPEC) {
|
|
343
408
|
const normalized = Buffer.from(String(passphrase), 'utf8');
|
|
409
|
+
if (typeof salt === 'string' && salt.length > 128) throw new Error('Encoded salt is too large');
|
|
344
410
|
const saltBuf = typeof salt === 'string' ? Buffer.from(salt, 'base64') : Buffer.from(salt);
|
|
345
|
-
if (saltBuf.length < 8) throw new Error('Salt must be
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
411
|
+
if (saltBuf.length < 8 || saltBuf.length > 64) throw new Error('Salt must be from 8 through 64 bytes');
|
|
412
|
+
const validated = validateKdfSpec(kdfSpec);
|
|
413
|
+
if (validated.algorithm === 'scrypt') {
|
|
414
|
+
return scryptSync(normalized, saltBuf, DERIVED_KEY_LENGTH, {
|
|
415
|
+
N: validated.n,
|
|
416
|
+
r: validated.r,
|
|
417
|
+
p: validated.p,
|
|
418
|
+
maxmem: validated.maxmem,
|
|
419
|
+
});
|
|
350
420
|
}
|
|
421
|
+
return pbkdf2Sync(normalized, saltBuf, validated.iterations, DERIVED_KEY_LENGTH, validated.hash);
|
|
351
422
|
}
|
|
352
423
|
|
|
353
|
-
function deriveKeysFromPassphrase(passphrase, salt) {
|
|
354
|
-
const master = deriveMasterKeyFromPassphrase(passphrase, salt);
|
|
424
|
+
export function deriveKeysFromPassphrase(passphrase, salt, kdfSpec = DEFAULT_KDF_SPEC) {
|
|
425
|
+
const master = deriveMasterKeyFromPassphrase(passphrase, salt, kdfSpec);
|
|
355
426
|
return {
|
|
356
427
|
kek: master.subarray(0, 32),
|
|
357
428
|
vaultKey: master.subarray(32, 64),
|
|
@@ -360,9 +431,29 @@ function deriveKeysFromPassphrase(passphrase, salt) {
|
|
|
360
431
|
};
|
|
361
432
|
}
|
|
362
433
|
|
|
363
|
-
function
|
|
434
|
+
export function canonicalKeyringAad(meta = {}) {
|
|
435
|
+
const aad = {
|
|
436
|
+
schema: meta.schema ?? 'enigma.vault_keyring.v2',
|
|
437
|
+
type: meta.type ?? KEYRING_ENCRYPTED_TYPE,
|
|
438
|
+
kdf: meta.kdf ?? DEFAULT_KDF_SPEC,
|
|
439
|
+
salt: String(meta.salt ?? ''),
|
|
440
|
+
vault_schema: meta.vault_schema ?? VAULT_SCHEMA,
|
|
441
|
+
vault_id: meta.vault_id ?? null,
|
|
442
|
+
active_key_epoch: meta.active_key_epoch ?? 1,
|
|
443
|
+
key_epochs: Array.isArray(meta.key_epochs) ? meta.key_epochs : [],
|
|
444
|
+
old_epoch_read_policy: meta.old_epoch_read_policy ?? 'retained_keys_allowed',
|
|
445
|
+
signer: meta.signer ?? 'ed25519',
|
|
446
|
+
publicKey: meta.publicKey ?? null,
|
|
447
|
+
};
|
|
448
|
+
return Buffer.from(canonical(aad), 'utf8');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function encryptKeyring(plaintextKeyring, kek, aadMeta = undefined) {
|
|
364
452
|
const iv = randomBytes(IV_LENGTH);
|
|
365
453
|
const cipher = createCipheriv('aes-256-gcm', kek, iv);
|
|
454
|
+
if (aadMeta) {
|
|
455
|
+
cipher.setAAD(canonicalKeyringAad(aadMeta));
|
|
456
|
+
}
|
|
366
457
|
const plaintext = Buffer.from(JSON.stringify(plaintextKeyring), 'utf8');
|
|
367
458
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
368
459
|
const tag = cipher.getAuthTag();
|
|
@@ -373,19 +464,48 @@ function encryptKeyring(plaintextKeyring, kek) {
|
|
|
373
464
|
};
|
|
374
465
|
}
|
|
375
466
|
|
|
467
|
+
function zeroDerivedKeys(keys) {
|
|
468
|
+
for (const value of Object.values(keys ?? {})) value?.fill?.(0);
|
|
469
|
+
}
|
|
470
|
+
|
|
376
471
|
export function decryptKeyring(keyring, passphrase) {
|
|
377
472
|
if (keyring?.type !== KEYRING_ENCRYPTED_TYPE) throw new Error('Keyring is not encrypted');
|
|
378
|
-
if (passphrase === undefined || passphrase === null || String(passphrase).length === 0) {
|
|
473
|
+
if (passphrase === undefined || passphrase === null || String(passphrase).trim().length === 0) {
|
|
379
474
|
throw new Error('Encrypted keyring requires passphrase');
|
|
380
475
|
}
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
476
|
+
const kdf = keyring.kdf || DEFAULT_KDF_SPEC;
|
|
477
|
+
const salt = keyring.kdf?.salt ?? keyring.salt;
|
|
478
|
+
const derived = deriveKeysFromPassphrase(passphrase, salt, kdf);
|
|
479
|
+
try {
|
|
480
|
+
if (keyring.kdf) {
|
|
481
|
+
const decipher = createDecipheriv('aes-256-gcm', derived.kek, Buffer.from(keyring.iv, 'base64'));
|
|
482
|
+
decipher.setAuthTag(Buffer.from(keyring.tag, 'base64'));
|
|
483
|
+
decipher.setAAD(canonicalKeyringAad(keyring));
|
|
484
|
+
const plaintext = Buffer.concat([
|
|
485
|
+
decipher.update(Buffer.from(keyring.ciphertext, 'base64')),
|
|
486
|
+
decipher.final(),
|
|
487
|
+
]);
|
|
488
|
+
try {
|
|
489
|
+
return JSON.parse(plaintext.toString('utf8'));
|
|
490
|
+
} finally {
|
|
491
|
+
plaintext.fill(0);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const legacyDecipher = createDecipheriv('aes-256-gcm', derived.kek, Buffer.from(keyring.iv, 'base64'));
|
|
496
|
+
legacyDecipher.setAuthTag(Buffer.from(keyring.tag, 'base64'));
|
|
497
|
+
const plaintext = Buffer.concat([
|
|
498
|
+
legacyDecipher.update(Buffer.from(keyring.ciphertext, 'base64')),
|
|
499
|
+
legacyDecipher.final(),
|
|
500
|
+
]);
|
|
501
|
+
try {
|
|
502
|
+
return JSON.parse(plaintext.toString('utf8'));
|
|
503
|
+
} finally {
|
|
504
|
+
plaintext.fill(0);
|
|
505
|
+
}
|
|
506
|
+
} finally {
|
|
507
|
+
zeroDerivedKeys(derived);
|
|
508
|
+
}
|
|
389
509
|
}
|
|
390
510
|
|
|
391
511
|
function signingPair(seed) {
|
|
@@ -456,7 +576,7 @@ function nowIso(now) {
|
|
|
456
576
|
return new Date().toISOString();
|
|
457
577
|
}
|
|
458
578
|
|
|
459
|
-
function metadata(input) {
|
|
579
|
+
function metadata(input, commitmentKey) {
|
|
460
580
|
const out = {};
|
|
461
581
|
if (!input || typeof input !== 'object') return out;
|
|
462
582
|
for (const [key, value] of Object.entries(input)) {
|
|
@@ -465,12 +585,12 @@ function metadata(input) {
|
|
|
465
585
|
if (isSafeMetadataPrimitive(key, value)) {
|
|
466
586
|
out[key] = value;
|
|
467
587
|
} else {
|
|
468
|
-
commitMetadataValue(out, key, value);
|
|
588
|
+
commitMetadataValue(out, key, value, commitmentKey);
|
|
469
589
|
}
|
|
470
590
|
continue;
|
|
471
591
|
}
|
|
472
592
|
if (Array.isArray(value) || typeof value === 'object') {
|
|
473
|
-
commitMetadataValue(out, key, value);
|
|
593
|
+
commitMetadataValue(out, key, value, commitmentKey);
|
|
474
594
|
}
|
|
475
595
|
}
|
|
476
596
|
return out;
|
|
@@ -558,7 +678,7 @@ function appendEvent(vault, fields) {
|
|
|
558
678
|
sequence,
|
|
559
679
|
policy_id: fields.policy_id ?? vault.policy_id,
|
|
560
680
|
decision: fields.decision,
|
|
561
|
-
metadata: metadata(fields.metadata),
|
|
681
|
+
metadata: metadata(fields.metadata, vault.addressKey),
|
|
562
682
|
});
|
|
563
683
|
const eventCanonical = canonical(event);
|
|
564
684
|
const eventHash = asSha256(eventCanonical);
|
|
@@ -570,45 +690,75 @@ function appendEvent(vault, fields) {
|
|
|
570
690
|
vault.receipts.push(receipt);
|
|
571
691
|
vault.sequence += 1;
|
|
572
692
|
vault.updated_at = event.timestamp;
|
|
573
|
-
vault
|
|
574
|
-
vault.receipt_log_root = computeReceiptLogRootFrom(vault.receipts);
|
|
693
|
+
Object.assign(vault, lifecycleRoots(vault));
|
|
575
694
|
return { event, receipt };
|
|
576
695
|
}
|
|
577
696
|
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
});
|
|
595
|
-
if (typeof addr === 'string' && addr.length > 0) return addr;
|
|
596
|
-
} catch {
|
|
597
|
-
// Local HMAC construction below follows the protocol domain separation.
|
|
598
|
-
}
|
|
697
|
+
function contentCommitment(vault, plaintext) {
|
|
698
|
+
return `hmac-sha256:${asHmacSha256(vault.addressKey, canonical({
|
|
699
|
+
domain: `${MEMORY_DOMAIN}/content_commitment`,
|
|
700
|
+
vault_id: vault.vault_id,
|
|
701
|
+
content: String(plaintext),
|
|
702
|
+
}))}`;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function memoryAddress(vault, subjectId, memoryId, commitment) {
|
|
706
|
+
const payload = canonical({
|
|
707
|
+
domain: MEMORY_DOMAIN,
|
|
708
|
+
vault_id: vault.vault_id,
|
|
709
|
+
subject_id: subjectId,
|
|
710
|
+
memory_id: memoryId,
|
|
711
|
+
content_commitment: commitment,
|
|
712
|
+
});
|
|
599
713
|
return `mem:${asHmacSha256(vault.addressKey, payload)}`;
|
|
600
714
|
}
|
|
601
715
|
|
|
602
|
-
function
|
|
716
|
+
function canonicalContentAad(vault, memoryAddr, epoch, keyId) {
|
|
717
|
+
return canonical({
|
|
718
|
+
domain: CONTENT_AAD_DOMAIN,
|
|
719
|
+
schema: MEMORY_OBJECT_SCHEMA,
|
|
720
|
+
vault_id: vault.vault_id,
|
|
721
|
+
memory_addr: memoryAddr,
|
|
722
|
+
key_epoch: epoch,
|
|
723
|
+
key_id: keyId,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function normalizedPrivateTags(value) {
|
|
728
|
+
if (!Array.isArray(value)) return [];
|
|
729
|
+
return value.map((tag) => String(tag)).filter((tag) => tag.length > 0);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function setPrivateTags(record, tags) {
|
|
733
|
+
Object.defineProperty(record, 'purpose_tags', {
|
|
734
|
+
value: normalizedPrivateTags(tags),
|
|
735
|
+
enumerable: false,
|
|
736
|
+
configurable: true,
|
|
737
|
+
writable: true,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function privateTags(record) {
|
|
742
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, 'purpose_tags');
|
|
743
|
+
return descriptor && descriptor.enumerable === false ? normalizedPrivateTags(descriptor.value) : [];
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function encryptContent(vault, memoryAddr, plaintext, epochEntry = activeContentEpoch(vault), privateFields = {}) {
|
|
603
747
|
const iv = randomBytes(12);
|
|
604
|
-
const
|
|
605
|
-
cipher
|
|
606
|
-
|
|
748
|
+
const aad = canonicalContentAad(vault, memoryAddr, epochEntry.epoch, epochEntry.key_id);
|
|
749
|
+
const cipher = createCipheriv('aes-256-gcm', epochEntry.key, iv);
|
|
750
|
+
cipher.setAAD(Buffer.from(aad, 'utf8'));
|
|
751
|
+
const privatePayload = JSON.stringify({
|
|
752
|
+
schema: PRIVATE_MEMORY_SCHEMA,
|
|
753
|
+
content: String(plaintext),
|
|
754
|
+
purpose_tags: normalizedPrivateTags(privateFields.purpose_tags),
|
|
755
|
+
});
|
|
756
|
+
const ciphertext = Buffer.concat([cipher.update(privatePayload, 'utf8'), cipher.final()]);
|
|
607
757
|
const tag = cipher.getAuthTag();
|
|
608
758
|
return {
|
|
609
759
|
alg: 'AES-256-GCM',
|
|
610
760
|
iv: iv.toString('base64'),
|
|
611
|
-
aad
|
|
761
|
+
aad,
|
|
612
762
|
ciphertext: ciphertext.toString('base64'),
|
|
613
763
|
tag: tag.toString('base64'),
|
|
614
764
|
};
|
|
@@ -635,16 +785,31 @@ function getPlaintextCache(record) {
|
|
|
635
785
|
}
|
|
636
786
|
|
|
637
787
|
function decryptContent(vault, record) {
|
|
788
|
+
if (!record) throw new Error('memory record is unavailable');
|
|
789
|
+
const epochEntry = contentKeyForRecord(vault, record);
|
|
638
790
|
const cached = getPlaintextCache(record);
|
|
639
791
|
if (cached !== undefined) return cached;
|
|
640
792
|
const encrypted = record.content_ciphertext;
|
|
641
|
-
|
|
642
|
-
|
|
793
|
+
if (!encrypted || encrypted.alg !== 'AES-256-GCM') throw new Error('memory ciphertext envelope is invalid');
|
|
794
|
+
const expectedAad = canonicalContentAad(vault, record.memory_addr, epochEntry.epoch, epochEntry.key_id);
|
|
795
|
+
if (encrypted.aad !== expectedAad) throw new Error('memory ciphertext AAD mismatch');
|
|
796
|
+
const decipher = createDecipheriv('aes-256-gcm', epochEntry.key, Buffer.from(encrypted.iv, 'base64'));
|
|
797
|
+
decipher.setAAD(Buffer.from(expectedAad, 'utf8'));
|
|
643
798
|
decipher.setAuthTag(Buffer.from(encrypted.tag, 'base64'));
|
|
644
|
-
const
|
|
799
|
+
const decrypted = Buffer.concat([
|
|
645
800
|
decipher.update(Buffer.from(encrypted.ciphertext, 'base64')),
|
|
646
801
|
decipher.final(),
|
|
647
802
|
]).toString('utf8');
|
|
803
|
+
let plaintext = decrypted;
|
|
804
|
+
try {
|
|
805
|
+
const payload = JSON.parse(decrypted);
|
|
806
|
+
if (payload?.schema === PRIVATE_MEMORY_SCHEMA && typeof payload.content === 'string') {
|
|
807
|
+
plaintext = payload.content;
|
|
808
|
+
setPrivateTags(record, payload.purpose_tags);
|
|
809
|
+
}
|
|
810
|
+
} catch {
|
|
811
|
+
// Pre-private-envelope ciphertext stores the content string directly.
|
|
812
|
+
}
|
|
648
813
|
return setPlaintextCache(record, plaintext);
|
|
649
814
|
}
|
|
650
815
|
|
|
@@ -652,22 +817,277 @@ function publicRecord(record) {
|
|
|
652
817
|
return pruneUndefined(sanitizePublicValue(record));
|
|
653
818
|
}
|
|
654
819
|
|
|
820
|
+
function requireVault(input, functionName) {
|
|
821
|
+
const vault = input?.vault ?? input;
|
|
822
|
+
if (!vault) throw new Error(`${functionName} requires a vault`);
|
|
823
|
+
return vault;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function signerRef(signer) {
|
|
827
|
+
if (!signer || typeof signer !== 'object') return undefined;
|
|
828
|
+
return pruneUndefined({
|
|
829
|
+
key_id: signer.key_id ?? signer.keyId,
|
|
830
|
+
alg: signer.alg,
|
|
831
|
+
publicKey: signer.publicKey,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function signatureRef(signature) {
|
|
836
|
+
if (!signature || typeof signature !== 'object') return undefined;
|
|
837
|
+
return pruneUndefined({
|
|
838
|
+
alg: signature.alg,
|
|
839
|
+
value: signature.value,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function sortedMapValues(mapLike) {
|
|
844
|
+
if (!mapLike) return [];
|
|
845
|
+
if (typeof mapLike.values === 'function') return [...mapLike.values()];
|
|
846
|
+
if (Array.isArray(mapLike)) return mapLike;
|
|
847
|
+
return [];
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function quarantineAddresses(vault) {
|
|
851
|
+
const out = new Set(vault.quarantineAddresses ?? []);
|
|
852
|
+
for (const [memoryAddr, record] of vault.memories ?? []) {
|
|
853
|
+
if (record?.state === 'quarantine' || record?.state === 'quarantined') out.add(memoryAddr);
|
|
854
|
+
}
|
|
855
|
+
return sortedStrings(out);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function publicTombstone(tombstone) {
|
|
859
|
+
if (!tombstone || typeof tombstone !== 'object') return undefined;
|
|
860
|
+
return pruneUndefined({
|
|
861
|
+
schema: tombstone.schema ?? 'enigma.deletion_tombstone.v1',
|
|
862
|
+
memory_addr: tombstone.memory_addr,
|
|
863
|
+
operation: tombstone.operation,
|
|
864
|
+
successor_addr: tombstone.successor_addr,
|
|
865
|
+
tombstoned_at: tombstone.tombstoned_at,
|
|
866
|
+
reason_hash: tombstone.reason ? asSha256(String(tombstone.reason)) : undefined,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function tombstoneRootItems(vault) {
|
|
871
|
+
return sortedMapValues(vault.tombstones)
|
|
872
|
+
.map(publicTombstone)
|
|
873
|
+
.filter(Boolean)
|
|
874
|
+
.sort((a, b) => String(a.memory_addr).localeCompare(String(b.memory_addr)))
|
|
875
|
+
.map((tombstone) => asSha256(canonical(tombstone)));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function lifecycleRoots(vault) {
|
|
879
|
+
const active = sortedStrings(vault.activeAddresses ?? []);
|
|
880
|
+
const quarantine = quarantineAddresses(vault);
|
|
881
|
+
const tombstones = tombstoneRootItems(vault);
|
|
882
|
+
const activeSetRoot = computeSetRoot(active);
|
|
883
|
+
const quarantineSetRoot = computeSetRoot(quarantine);
|
|
884
|
+
const tombstoneSetRoot = computeSetRoot(tombstones);
|
|
885
|
+
const receiptLogRoot = computeReceiptLogRootFrom(vault.receipts ?? []);
|
|
886
|
+
return {
|
|
887
|
+
active_set_root: activeSetRoot,
|
|
888
|
+
quarantine_set_root: quarantineSetRoot,
|
|
889
|
+
tombstone_set_root: tombstoneSetRoot,
|
|
890
|
+
receipt_log_root: receiptLogRoot,
|
|
891
|
+
lifecycle_root: computeSetRoot([activeSetRoot, quarantineSetRoot, tombstoneSetRoot, receiptLogRoot]),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function applyLifecycleRoots(vault) {
|
|
896
|
+
return Object.assign(vault, lifecycleRoots(vault));
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function projectLifecycleReceipt(receipt) {
|
|
900
|
+
if (!receipt || typeof receipt !== 'object') return undefined;
|
|
901
|
+
return pruneUndefined({
|
|
902
|
+
schema: 'enigma.lifecycle_receipt_projection.v1',
|
|
903
|
+
receipt_id: receipt.receipt_id,
|
|
904
|
+
receipt_hash: hashReceipt(receipt),
|
|
905
|
+
event_hash: receipt.event_hash,
|
|
906
|
+
operation: receipt.operation,
|
|
907
|
+
tenant_id: receipt.tenant_id,
|
|
908
|
+
subject_id: receipt.subject_id,
|
|
909
|
+
memory_addr: receipt.memory_addr,
|
|
910
|
+
source_addr: receipt.source_addr,
|
|
911
|
+
policy_id: receipt.policy_id,
|
|
912
|
+
sequence: receipt.sequence,
|
|
913
|
+
previous_receipt_hash: receipt.previous_receipt_hash,
|
|
914
|
+
active_set_root: receipt.active_set_root,
|
|
915
|
+
receipt_log_root: receipt.receipt_log_root,
|
|
916
|
+
timestamp: receipt.timestamp,
|
|
917
|
+
signer: signerRef(receipt.signer),
|
|
918
|
+
signature: signatureRef(receipt.signature),
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function lifecycleStateForReceipt(receipt) {
|
|
923
|
+
if (receipt?.operation === 'delete') return 'tombstone';
|
|
924
|
+
if (receipt?.operation === 'quarantine') return 'quarantine';
|
|
925
|
+
return 'active';
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function projectLifecycleReceiptRef(receipt) {
|
|
929
|
+
if (!receipt || typeof receipt !== 'object') return undefined;
|
|
930
|
+
const receiptHash = hashReceipt(receipt);
|
|
931
|
+
return pruneUndefined({
|
|
932
|
+
sequence: receipt.sequence,
|
|
933
|
+
receipt_ref: receipt.receipt_id ?? receiptHash,
|
|
934
|
+
receipt_hash: receiptHash,
|
|
935
|
+
previous_receipt_hash: receipt.previous_receipt_hash,
|
|
936
|
+
lifecycle_state: lifecycleStateForReceipt(receipt),
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function lifecycleLogId(vault, roots, issuedAt) {
|
|
941
|
+
return `log_${sha256Hex(canonical({
|
|
942
|
+
schema: 'enigma.lifecycle_receipt_log.id.v1',
|
|
943
|
+
vault_id: vault.vault_id,
|
|
944
|
+
receipt_log_root: roots.receipt_log_root,
|
|
945
|
+
lifecycle_root: roots.lifecycle_root,
|
|
946
|
+
issued_at: issuedAt,
|
|
947
|
+
})).slice(0, 32)}`;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function refHash(value) {
|
|
951
|
+
return asSha256(canonical(value));
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
export function buildLifecycleRootSummary(args = {}) {
|
|
956
|
+
const vault = requireVault(args, 'buildLifecycleRootSummary');
|
|
957
|
+
const roots = lifecycleRoots(vault);
|
|
958
|
+
const active = sortedStrings(vault.activeAddresses ?? []);
|
|
959
|
+
const quarantine = quarantineAddresses(vault);
|
|
960
|
+
const tombstones = tombstoneRootItems(vault);
|
|
961
|
+
return pruneUndefined({
|
|
962
|
+
schema: 'enigma.lifecycle_root_summary.v1',
|
|
963
|
+
vault_id: vault.vault_id,
|
|
964
|
+
tenant_id: vault.tenant_id,
|
|
965
|
+
subject_id: vault.subject_id,
|
|
966
|
+
policy_id: vault.policy_id,
|
|
967
|
+
sequence: vault.sequence,
|
|
968
|
+
active_count: active.length,
|
|
969
|
+
quarantine_count: quarantine.length,
|
|
970
|
+
tombstone_count: tombstones.length,
|
|
971
|
+
receipt_count: Array.isArray(vault.receipts) ? vault.receipts.length : 0,
|
|
972
|
+
active_set_root: roots.active_set_root,
|
|
973
|
+
quarantine_set_root: roots.quarantine_set_root,
|
|
974
|
+
tombstone_set_root: roots.tombstone_set_root,
|
|
975
|
+
active_root: roots.active_set_root,
|
|
976
|
+
quarantine_root: roots.quarantine_set_root,
|
|
977
|
+
tombstone_root: roots.tombstone_set_root,
|
|
978
|
+
receipt_log_root: roots.receipt_log_root,
|
|
979
|
+
lifecycle_root: roots.lifecycle_root,
|
|
980
|
+
updated_at: vault.updated_at,
|
|
981
|
+
signer: signerRef(vault.signer),
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export function buildMemoryBoundaryTransactionSummary(args = {}) {
|
|
986
|
+
const vault = requireVault(args, 'buildMemoryBoundaryTransactionSummary');
|
|
987
|
+
const receipt = args.receipt ?? args.transaction ?? vault.receipts?.[vault.receipts.length - 1];
|
|
988
|
+
const projection = projectLifecycleReceipt(receipt);
|
|
989
|
+
if (!projection) throw new Error('buildMemoryBoundaryTransactionSummary requires at least one receipt');
|
|
990
|
+
return pruneUndefined({
|
|
991
|
+
schema: 'enigma.memory_boundary_transaction_summary.v1',
|
|
992
|
+
vault_id: vault.vault_id,
|
|
993
|
+
tenant_id: vault.tenant_id,
|
|
994
|
+
subject_id: vault.subject_id,
|
|
995
|
+
operation: projection.operation,
|
|
996
|
+
sequence: projection.sequence,
|
|
997
|
+
timestamp: projection.timestamp,
|
|
998
|
+
memory_addr: projection.memory_addr,
|
|
999
|
+
source_addr: projection.source_addr,
|
|
1000
|
+
policy_id: projection.policy_id,
|
|
1001
|
+
event_hash: projection.event_hash,
|
|
1002
|
+
receipt_hash: projection.receipt_hash,
|
|
1003
|
+
previous_receipt_hash: projection.previous_receipt_hash,
|
|
1004
|
+
active_set_root: projection.active_set_root,
|
|
1005
|
+
receipt_log_root: projection.receipt_log_root,
|
|
1006
|
+
lifecycle_root: buildLifecycleRootSummary({ vault }).lifecycle_root,
|
|
1007
|
+
signer: projection.signer,
|
|
1008
|
+
signature: projection.signature,
|
|
1009
|
+
public_safe: true,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
export function buildPublicLifecycleReceiptLog(args = {}) {
|
|
1014
|
+
const vault = requireVault(args, 'buildPublicLifecycleReceiptLog');
|
|
1015
|
+
const receiptRefs = (vault.receipts ?? []).map(projectLifecycleReceiptRef).filter(Boolean);
|
|
1016
|
+
if (receiptRefs.length === 0) throw new Error('buildPublicLifecycleReceiptLog requires at least one receipt');
|
|
1017
|
+
const roots = buildLifecycleRootSummary({ vault });
|
|
1018
|
+
const issuedAt = args.now ? nowIso(args.now) : vault.updated_at;
|
|
1019
|
+
const latestReceipt = vault.receipts?.[vault.receipts.length - 1];
|
|
1020
|
+
const logId = lifecycleLogId(vault, roots, issuedAt);
|
|
1021
|
+
return pruneUndefined({
|
|
1022
|
+
schema: 'enigma.lifecycle_receipt_log.v1',
|
|
1023
|
+
log_id: logId,
|
|
1024
|
+
log_ref: refHash({ schema: 'enigma.lifecycle_receipt_log.ref.v1', log_id: logId, receipt_log_root: roots.receipt_log_root }),
|
|
1025
|
+
subject_ref: refHash({ schema: 'enigma.subject_ref.v1', subject_id: vault.subject_id }),
|
|
1026
|
+
tenant_ref: refHash({ schema: 'enigma.tenant_ref.v1', tenant_id: vault.tenant_id }),
|
|
1027
|
+
epoch: vault.sequence,
|
|
1028
|
+
previous_log_root: latestReceipt?.receipt_log_root ?? ZERO_ROOT,
|
|
1029
|
+
receipt_refs: receiptRefs,
|
|
1030
|
+
active_root: roots.active_set_root,
|
|
1031
|
+
quarantine_root: roots.quarantine_set_root,
|
|
1032
|
+
tombstone_root: roots.tombstone_set_root,
|
|
1033
|
+
receipt_log_root: roots.receipt_log_root,
|
|
1034
|
+
issued_at: issuedAt,
|
|
1035
|
+
signer_ref: refHash(signerRef(vault.signer) ?? { key_id: vault.signer?.key_id ?? null }),
|
|
1036
|
+
signature_ref: latestReceipt?.signature ? refHash(signatureRef(latestReceipt.signature)) : undefined,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
|
|
655
1041
|
function attachInternals(vault) {
|
|
656
1042
|
Object.defineProperties(vault, {
|
|
657
1043
|
__recordEvent: { value: (fields) => appendEvent(vault, fields), enumerable: false },
|
|
658
1044
|
__getRecord: { value: (memoryAddr) => vault.memories.get(memoryAddr), enumerable: false },
|
|
659
1045
|
__getPlaintext: { value: (memoryAddr) => decryptContent(vault, vault.memories.get(memoryAddr)), enumerable: false },
|
|
660
1046
|
__computeRoots: {
|
|
661
|
-
value: () => (
|
|
662
|
-
active_set_root: computeSetRoot(vault.activeAddresses),
|
|
663
|
-
receipt_log_root: computeReceiptLogRootFrom(vault.receipts),
|
|
664
|
-
}),
|
|
1047
|
+
value: () => lifecycleRoots(vault),
|
|
665
1048
|
enumerable: false,
|
|
666
1049
|
},
|
|
667
1050
|
});
|
|
668
1051
|
return vault;
|
|
669
1052
|
}
|
|
670
1053
|
|
|
1054
|
+
function normalizeKeyEpoch(entry, fallbackKey, fallbackKeyId, createdAt) {
|
|
1055
|
+
const epoch = Number(entry?.epoch ?? 1);
|
|
1056
|
+
if (!Number.isInteger(epoch) || epoch < 1) throw new Error('content key epoch must be a positive integer');
|
|
1057
|
+
const key = keyBytes(entry?.key ?? entry?.key_b64 ?? fallbackKey, `content key for epoch ${epoch}`);
|
|
1058
|
+
const keyId = entry?.key_id ?? entry?.keyId ?? fallbackKeyId;
|
|
1059
|
+
if (typeof keyId !== 'string' || keyId.length === 0) throw new Error(`content key epoch ${epoch} requires key_id`);
|
|
1060
|
+
const status = entry?.status ?? 'active';
|
|
1061
|
+
if (!['active', 'retained', 'sealed'].includes(status)) throw new Error(`unsupported content key epoch status: ${status}`);
|
|
1062
|
+
return {
|
|
1063
|
+
epoch,
|
|
1064
|
+
key_id: keyId,
|
|
1065
|
+
key,
|
|
1066
|
+
status,
|
|
1067
|
+
created_at: entry?.created_at ?? createdAt,
|
|
1068
|
+
sealed_at: entry?.sealed_at,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function activeContentEpoch(vault) {
|
|
1073
|
+
const entry = vault.keyEpochs.get(vault.activeKeyEpoch);
|
|
1074
|
+
if (!entry) throw new Error(`active content key epoch ${vault.activeKeyEpoch} is unavailable`);
|
|
1075
|
+
if (entry.status === 'sealed') throw new Error(`content key epoch ${entry.epoch} is sealed and rejects writes`);
|
|
1076
|
+
if (entry.status !== 'active') throw new Error(`content key epoch ${entry.epoch} is not active`);
|
|
1077
|
+
return entry;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function contentKeyForRecord(vault, record) {
|
|
1081
|
+
const epoch = Number(record.key_epoch ?? 1);
|
|
1082
|
+
const entry = vault.keyEpochs.get(epoch);
|
|
1083
|
+
if (!entry || !entry.key) throw new Error(`content key epoch ${epoch} has been destroyed or is unavailable`);
|
|
1084
|
+
if (entry.key_id !== record.key_id) throw new Error(`content key id mismatch for epoch ${epoch}`);
|
|
1085
|
+
if (epoch !== vault.activeKeyEpoch && vault.allowOldEpochReads !== true) {
|
|
1086
|
+
throw new Error(`read policy rejects old content key epoch ${epoch}`);
|
|
1087
|
+
}
|
|
1088
|
+
return entry;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
671
1091
|
export function createVault(args = {}) {
|
|
672
1092
|
const createdAt = nowIso(args.now);
|
|
673
1093
|
const passphrase = args.passphrase;
|
|
@@ -676,8 +1096,13 @@ export function createVault(args = {}) {
|
|
|
676
1096
|
let addressKey;
|
|
677
1097
|
let signingSeed;
|
|
678
1098
|
let keyringType = KEYRING_PLAINTEXT_TYPE;
|
|
679
|
-
if (passphrase !== undefined && passphrase !== null
|
|
680
|
-
const
|
|
1099
|
+
if (passphrase !== undefined && passphrase !== null) {
|
|
1100
|
+
const rawPass = String(passphrase);
|
|
1101
|
+
if (rawPass.length === 0 || rawPass.trim().length === 0) {
|
|
1102
|
+
throw new Error('Passphrase provided to createVault cannot be empty.');
|
|
1103
|
+
}
|
|
1104
|
+
const kdf = args.kdf || DEFAULT_KDF_SPEC;
|
|
1105
|
+
const derived = deriveKeysFromPassphrase(rawPass, salt, kdf);
|
|
681
1106
|
vaultKey = derived.vaultKey;
|
|
682
1107
|
addressKey = derived.addressKey;
|
|
683
1108
|
signingSeed = derived.signingSeed;
|
|
@@ -687,6 +1112,20 @@ export function createVault(args = {}) {
|
|
|
687
1112
|
addressKey = keyBytes(args.address_key ?? args.addressKey, 'address key');
|
|
688
1113
|
}
|
|
689
1114
|
const signingKeyPair = args.signingKeyPair ?? signingPair(signingSeed);
|
|
1115
|
+
const activeKeyEpoch = Number(args.active_key_epoch ?? args.activeKeyEpoch ?? 1);
|
|
1116
|
+
const initialKeyId = args.key_id ?? args.keyId ?? `vault-key-${randomUUID()}`;
|
|
1117
|
+
const suppliedEpochs = args.key_epochs ?? args.keyEpochs;
|
|
1118
|
+
const epochEntries = Array.isArray(suppliedEpochs) && suppliedEpochs.length > 0
|
|
1119
|
+
? suppliedEpochs.map((entry) => normalizeKeyEpoch(entry, undefined, undefined, createdAt))
|
|
1120
|
+
: [normalizeKeyEpoch({ epoch: activeKeyEpoch, key_id: initialKeyId, key: vaultKey, status: 'active' }, undefined, undefined, createdAt)];
|
|
1121
|
+
const keyEpochs = new Map(epochEntries.map((entry) => [entry.epoch, entry]));
|
|
1122
|
+
if (keyEpochs.size !== epochEntries.length) throw new Error('duplicate content key epoch');
|
|
1123
|
+
const activeEpochEntry = keyEpochs.get(activeKeyEpoch);
|
|
1124
|
+
if (!activeEpochEntry) throw new Error(`active content key epoch ${activeKeyEpoch} is missing`);
|
|
1125
|
+
if (!['active', 'sealed'].includes(activeEpochEntry.status)) {
|
|
1126
|
+
throw new Error(`active content key epoch ${activeKeyEpoch} cannot have ${activeEpochEntry.status} status`);
|
|
1127
|
+
}
|
|
1128
|
+
vaultKey = activeEpochEntry.key;
|
|
690
1129
|
const vault = {
|
|
691
1130
|
schema: VAULT_SCHEMA,
|
|
692
1131
|
vault_id: args.vault_id ?? args.vaultId ?? `vault_${randomUUID()}`,
|
|
@@ -697,7 +1136,10 @@ export function createVault(args = {}) {
|
|
|
697
1136
|
created_at: createdAt,
|
|
698
1137
|
updated_at: createdAt,
|
|
699
1138
|
encryption: 'local_aead',
|
|
700
|
-
key_id:
|
|
1139
|
+
key_id: activeEpochEntry.key_id,
|
|
1140
|
+
activeKeyEpoch,
|
|
1141
|
+
keyEpochs,
|
|
1142
|
+
allowOldEpochReads: args.allow_old_epoch_reads ?? args.allowOldEpochReads ?? true,
|
|
701
1143
|
vaultKey,
|
|
702
1144
|
addressKey,
|
|
703
1145
|
signingKeyPair,
|
|
@@ -707,14 +1149,127 @@ export function createVault(args = {}) {
|
|
|
707
1149
|
receipts: [],
|
|
708
1150
|
activeAddresses: new Set(),
|
|
709
1151
|
tombstones: new Map(),
|
|
1152
|
+
quarantineAddresses: new Set(),
|
|
710
1153
|
sequence: 0,
|
|
711
1154
|
active_set_root: ZERO_ROOT,
|
|
1155
|
+
quarantine_set_root: ZERO_ROOT,
|
|
1156
|
+
tombstone_set_root: ZERO_ROOT,
|
|
712
1157
|
receipt_log_root: ZERO_ROOT,
|
|
1158
|
+
lifecycle_root: ZERO_ROOT,
|
|
713
1159
|
keyringType,
|
|
714
1160
|
};
|
|
1161
|
+
applyLifecycleRoots(vault);
|
|
715
1162
|
return attachInternals(vault);
|
|
716
1163
|
}
|
|
717
1164
|
|
|
1165
|
+
export function rotateContentKey(args = {}) {
|
|
1166
|
+
const vault = requireVault(args, 'rotateContentKey');
|
|
1167
|
+
const previous = vault.keyEpochs.get(vault.activeKeyEpoch);
|
|
1168
|
+
if (previous.status === 'active') previous.status = 'retained';
|
|
1169
|
+
const epoch = Math.max(...vault.keyEpochs.keys()) + 1;
|
|
1170
|
+
const createdAt = nowIso(args.now);
|
|
1171
|
+
const next = normalizeKeyEpoch({
|
|
1172
|
+
epoch,
|
|
1173
|
+
key_id: args.key_id ?? args.keyId ?? `vault-key-${randomUUID()}`,
|
|
1174
|
+
key: args.key,
|
|
1175
|
+
status: 'active',
|
|
1176
|
+
created_at: createdAt,
|
|
1177
|
+
}, undefined, undefined, createdAt);
|
|
1178
|
+
vault.keyEpochs.set(epoch, next);
|
|
1179
|
+
vault.activeKeyEpoch = epoch;
|
|
1180
|
+
vault.key_id = next.key_id;
|
|
1181
|
+
vault.vaultKey = next.key;
|
|
1182
|
+
const { event, receipt } = appendEvent(vault, {
|
|
1183
|
+
operation: 'key_rotate',
|
|
1184
|
+
actor_id: args.actor_id ?? args.actorId,
|
|
1185
|
+
now: args.now,
|
|
1186
|
+
metadata: { prior_epoch: previous.epoch, new_epoch: epoch, prior_key_id: previous.key_id, new_key_id: next.key_id },
|
|
1187
|
+
});
|
|
1188
|
+
return {
|
|
1189
|
+
epoch,
|
|
1190
|
+
key_id: next.key_id,
|
|
1191
|
+
prior_epoch: previous.epoch,
|
|
1192
|
+
prior_key_status: previous.status,
|
|
1193
|
+
old_key_retained: true,
|
|
1194
|
+
external_copies_destroyed: false,
|
|
1195
|
+
event,
|
|
1196
|
+
receipt,
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
export function sealContentKeyEpoch(args = {}) {
|
|
1201
|
+
const vault = requireVault(args, 'sealContentKeyEpoch');
|
|
1202
|
+
const epoch = Number(args.epoch ?? vault.activeKeyEpoch);
|
|
1203
|
+
const entry = vault.keyEpochs.get(epoch);
|
|
1204
|
+
if (!entry) throw new Error(`unknown content key epoch ${epoch}`);
|
|
1205
|
+
if (entry.status === 'sealed') throw new Error(`content key epoch ${epoch} is already sealed`);
|
|
1206
|
+
entry.status = 'sealed';
|
|
1207
|
+
entry.sealed_at = nowIso(args.now);
|
|
1208
|
+
const { event, receipt } = appendEvent(vault, {
|
|
1209
|
+
operation: 'key_seal',
|
|
1210
|
+
actor_id: args.actor_id ?? args.actorId,
|
|
1211
|
+
now: args.now,
|
|
1212
|
+
metadata: { epoch, key_id: entry.key_id, key_retained: true },
|
|
1213
|
+
});
|
|
1214
|
+
return {
|
|
1215
|
+
epoch,
|
|
1216
|
+
key_id: entry.key_id,
|
|
1217
|
+
status: entry.status,
|
|
1218
|
+
key_retained_for_reads: true,
|
|
1219
|
+
cryptographic_erasure_performed: false,
|
|
1220
|
+
external_copies_destroyed: false,
|
|
1221
|
+
event,
|
|
1222
|
+
receipt,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
export function resetVaultContentKeys(args = {}) {
|
|
1227
|
+
const vault = requireVault(args, 'resetVaultContentKeys');
|
|
1228
|
+
const destroyedEpochs = [...vault.keyEpochs.keys()].sort((a, b) => a - b);
|
|
1229
|
+
const oldEpochEntries = [...vault.keyEpochs.values()];
|
|
1230
|
+
const removedRecords = vault.memories.size;
|
|
1231
|
+
const nextEpoch = Math.max(0, ...destroyedEpochs) + 1;
|
|
1232
|
+
const createdAt = nowIso(args.now);
|
|
1233
|
+
const next = normalizeKeyEpoch({
|
|
1234
|
+
epoch: nextEpoch,
|
|
1235
|
+
key_id: args.key_id ?? args.keyId ?? `vault-key-${randomUUID()}`,
|
|
1236
|
+
key: args.key,
|
|
1237
|
+
status: 'active',
|
|
1238
|
+
created_at: createdAt,
|
|
1239
|
+
}, undefined, undefined, createdAt);
|
|
1240
|
+
const oldAddressKey = vault.addressKey;
|
|
1241
|
+
vault.memories.clear();
|
|
1242
|
+
vault.activeAddresses.clear();
|
|
1243
|
+
vault.tombstones.clear();
|
|
1244
|
+
vault.quarantineAddresses.clear();
|
|
1245
|
+
vault.keyEpochs = new Map([[nextEpoch, next]]);
|
|
1246
|
+
vault.activeKeyEpoch = nextEpoch;
|
|
1247
|
+
vault.key_id = next.key_id;
|
|
1248
|
+
vault.vaultKey = next.key;
|
|
1249
|
+
vault.addressKey = keyBytes(args.address_key ?? args.addressKey, 'address key');
|
|
1250
|
+
applyLifecycleRoots(vault);
|
|
1251
|
+
const { event, receipt } = appendEvent(vault, {
|
|
1252
|
+
operation: 'key_reset',
|
|
1253
|
+
actor_id: args.actor_id ?? args.actorId,
|
|
1254
|
+
now: args.now,
|
|
1255
|
+
metadata: { destroyed_epoch_count: destroyedEpochs.length, removed_record_count: removedRecords, new_epoch: nextEpoch, new_key_id: next.key_id },
|
|
1256
|
+
});
|
|
1257
|
+
for (const entry of oldEpochEntries) entry.key?.fill?.(0);
|
|
1258
|
+
oldAddressKey?.fill?.(0);
|
|
1259
|
+
return {
|
|
1260
|
+
destroyed_epochs: destroyedEpochs,
|
|
1261
|
+
removed_record_count: removedRecords,
|
|
1262
|
+
epoch: nextEpoch,
|
|
1263
|
+
key_id: next.key_id,
|
|
1264
|
+
software_key_material_zeroized_in_process: true,
|
|
1265
|
+
process_memory_overwrite_guaranteed: false,
|
|
1266
|
+
backups_and_external_copies_destroyed: false,
|
|
1267
|
+
physical_media_erasure_guaranteed: false,
|
|
1268
|
+
event,
|
|
1269
|
+
receipt,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
718
1273
|
function normalizeImportance(value) {
|
|
719
1274
|
if (value === undefined || value === null) return undefined;
|
|
720
1275
|
const num = Number(value);
|
|
@@ -731,30 +1286,38 @@ export function remember(args = {}) {
|
|
|
731
1286
|
const memoryId = args.memory_id ?? args.memoryId ?? `mem_${randomUUID()}`;
|
|
732
1287
|
const subjectId = args.subject_id ?? args.subjectId ?? vault.subject_id;
|
|
733
1288
|
const createdAt = nowIso(args.now);
|
|
734
|
-
const
|
|
735
|
-
const
|
|
1289
|
+
const epochEntry = activeContentEpoch(vault);
|
|
1290
|
+
const requestedEpoch = args.key_epoch ?? args.keyEpoch;
|
|
1291
|
+
if (requestedEpoch !== undefined && Number(requestedEpoch) !== epochEntry.epoch) {
|
|
1292
|
+
throw new Error(`writes require active content key epoch ${epochEntry.epoch}`);
|
|
1293
|
+
}
|
|
1294
|
+
const commitment = contentCommitment(vault, plaintext);
|
|
1295
|
+
const memoryAddr = memoryAddress(vault, subjectId, memoryId, commitment);
|
|
736
1296
|
if (vault.tombstones.has(memoryAddr)) throw new Error(`memory address is tombstoned: ${memoryAddr}`);
|
|
1297
|
+
const rawPurposeTags = normalizedPrivateTags(args.purpose_tags ?? args.purposeTags);
|
|
737
1298
|
|
|
738
1299
|
const record = pruneUndefined({
|
|
739
|
-
schema:
|
|
1300
|
+
schema: MEMORY_OBJECT_SCHEMA,
|
|
740
1301
|
memory_id: memoryId,
|
|
741
1302
|
memory_addr: memoryAddr,
|
|
742
1303
|
subject_id: subjectId,
|
|
743
|
-
kind: args.kind
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1304
|
+
kind: ['fact', 'preference', 'goal', 'instruction', 'episode'].includes(args.kind) ? args.kind : 'fact',
|
|
1305
|
+
key_epoch: epochEntry.epoch,
|
|
1306
|
+
key_id: epochEntry.key_id,
|
|
1307
|
+
content_ciphertext: encryptContent(vault, memoryAddr, plaintext, epochEntry, { purpose_tags: rawPurposeTags }),
|
|
1308
|
+
content_commitment: commitment,
|
|
1309
|
+
source_refs: sanitizeSourceRefs(args.source_refs ?? args.sourceRefs, vault.addressKey),
|
|
1310
|
+
confidence: ['user_confirmed', 'inferred', 'verified'].includes(args.confidence) ? args.confidence : 'user_confirmed',
|
|
1311
|
+
sensitivity: ['normal', 'sensitive', 'restricted'].includes(args.sensitivity) ? args.sensitivity : 'normal',
|
|
1312
|
+
purpose_tag_commitments: rawPurposeTags.map((tag) => privateValueCommitment(vault.addressKey, tag)),
|
|
1313
|
+
retention: ['durable', 'session', 'ephemeral'].includes(args.retention) ? args.retention : 'durable',
|
|
752
1314
|
importance: normalizeImportance(args.importance),
|
|
753
1315
|
state: 'active',
|
|
754
|
-
metadata: metadata(args.metadata),
|
|
1316
|
+
metadata: metadata(args.metadata, vault.addressKey),
|
|
755
1317
|
created_at: createdAt,
|
|
756
1318
|
updated_at: createdAt,
|
|
757
1319
|
});
|
|
1320
|
+
setPrivateTags(record, rawPurposeTags);
|
|
758
1321
|
|
|
759
1322
|
vault.memories.set(memoryAddr, record);
|
|
760
1323
|
vault.activeAddresses.add(memoryAddr);
|
|
@@ -814,22 +1377,34 @@ export function updateMemory(args = {}) {
|
|
|
814
1377
|
|
|
815
1378
|
const oldRecord = vault.memories.get(oldAddr);
|
|
816
1379
|
if (!oldRecord) throw new Error(`unknown memory: ${oldAddr}`);
|
|
1380
|
+
const epochEntry = activeContentEpoch(vault);
|
|
1381
|
+
const requestedEpoch = args.key_epoch ?? args.keyEpoch;
|
|
1382
|
+
if (requestedEpoch !== undefined && Number(requestedEpoch) !== epochEntry.epoch) {
|
|
1383
|
+
throw new Error(`writes require active content key epoch ${epochEntry.epoch}`);
|
|
1384
|
+
}
|
|
1385
|
+
decryptContent(vault, oldRecord);
|
|
1386
|
+
const rawPurposeTags = args.purpose_tags !== undefined || args.purposeTags !== undefined
|
|
1387
|
+
? normalizedPrivateTags(args.purpose_tags ?? args.purposeTags)
|
|
1388
|
+
: privateTags(oldRecord);
|
|
817
1389
|
oldRecord.state = 'superseded';
|
|
818
1390
|
oldRecord.updated_at = nowIso(args.now);
|
|
819
1391
|
vault.activeAddresses.delete(oldAddr);
|
|
820
1392
|
|
|
821
|
-
const
|
|
822
|
-
const newAddr = memoryAddress(vault, oldRecord.subject_id, oldRecord.memory_id,
|
|
1393
|
+
const commitment = contentCommitment(vault, plaintext);
|
|
1394
|
+
const newAddr = memoryAddress(vault, oldRecord.subject_id, oldRecord.memory_id, commitment);
|
|
823
1395
|
const updatedAt = nowIso(args.now);
|
|
824
1396
|
const newRecord = pruneUndefined({
|
|
825
1397
|
...publicRecord(oldRecord),
|
|
826
1398
|
memory_addr: newAddr,
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1399
|
+
key_epoch: epochEntry.epoch,
|
|
1400
|
+
key_id: epochEntry.key_id,
|
|
1401
|
+
content_ciphertext: encryptContent(vault, newAddr, plaintext, epochEntry, { purpose_tags: rawPurposeTags }),
|
|
1402
|
+
content_commitment: commitment,
|
|
1403
|
+
purpose_tag_commitments: rawPurposeTags.map((tag) => privateValueCommitment(vault.addressKey, tag)),
|
|
830
1404
|
state: 'active',
|
|
831
1405
|
updated_at: updatedAt,
|
|
832
1406
|
});
|
|
1407
|
+
setPrivateTags(newRecord, rawPurposeTags);
|
|
833
1408
|
|
|
834
1409
|
vault.memories.set(newAddr, newRecord);
|
|
835
1410
|
vault.activeAddresses.add(newAddr);
|
|
@@ -903,39 +1478,86 @@ export function deleteMemory(args = {}) {
|
|
|
903
1478
|
return { memory_addr: memoryAddr, tombstone, event, receipt };
|
|
904
1479
|
}
|
|
905
1480
|
|
|
906
|
-
function
|
|
907
|
-
|
|
1481
|
+
function publicKeyEpochs(vault) {
|
|
1482
|
+
return [...vault.keyEpochs.values()]
|
|
1483
|
+
.sort((left, right) => left.epoch - right.epoch)
|
|
1484
|
+
.map((entry) => pruneUndefined({
|
|
1485
|
+
epoch: entry.epoch,
|
|
1486
|
+
key_id: entry.key_id,
|
|
1487
|
+
status: entry.status,
|
|
1488
|
+
created_at: entry.created_at,
|
|
1489
|
+
sealed_at: entry.sealed_at,
|
|
1490
|
+
}));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function privateKeyEpochs(vault) {
|
|
1494
|
+
return [...vault.keyEpochs.values()]
|
|
1495
|
+
.sort((left, right) => left.epoch - right.epoch)
|
|
1496
|
+
.map((entry) => ({
|
|
1497
|
+
epoch: entry.epoch,
|
|
1498
|
+
key_id: entry.key_id,
|
|
1499
|
+
status: entry.status,
|
|
1500
|
+
created_at: entry.created_at,
|
|
1501
|
+
sealed_at: entry.sealed_at,
|
|
1502
|
+
key_b64: entry.key.toString('base64'),
|
|
1503
|
+
}));
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function buildKeyring(vault, passphrase, options = {}) {
|
|
1507
|
+
const signer = vault.signer;
|
|
1508
|
+
const publicKey = exportPublicKey(vault.signingKeyPair);
|
|
1509
|
+
const keyEpochs = publicKeyEpochs(vault);
|
|
1510
|
+
const common = {
|
|
1511
|
+
schema: 'enigma.vault_keyring.v2',
|
|
1512
|
+
vault_schema: vault.schema,
|
|
1513
|
+
vault_id: vault.vault_id,
|
|
1514
|
+
active_key_epoch: vault.activeKeyEpoch,
|
|
1515
|
+
key_epochs: keyEpochs,
|
|
1516
|
+
old_epoch_read_policy: vault.allowOldEpochReads === true ? 'retained_keys_allowed' : 'active_epoch_only',
|
|
1517
|
+
signer,
|
|
1518
|
+
publicKey,
|
|
1519
|
+
};
|
|
1520
|
+
if (passphrase !== undefined && passphrase !== null) {
|
|
1521
|
+
const rawPass = String(passphrase);
|
|
1522
|
+
if (rawPass.length === 0 || rawPass.trim().length === 0) {
|
|
1523
|
+
throw new Error('Passphrase provided to buildKeyring cannot be empty.');
|
|
1524
|
+
}
|
|
908
1525
|
const salt = randomBytes(SALT_LENGTH).toString('base64');
|
|
909
|
-
const {
|
|
1526
|
+
const kdf = { ...(options.kdf || DEFAULT_KDF_SPEC), salt };
|
|
1527
|
+
const derived = deriveKeysFromPassphrase(rawPass, salt, kdf);
|
|
1528
|
+
const aadMeta = { ...common, type: KEYRING_ENCRYPTED_TYPE, kdf, salt };
|
|
910
1529
|
const plaintextKeyring = {
|
|
911
1530
|
vault_key_b64: vault.vaultKey.toString('base64'),
|
|
912
1531
|
address_key_b64: vault.addressKey.toString('base64'),
|
|
913
1532
|
privateKey: exportPrivateKey(vault.signingKeyPair),
|
|
1533
|
+
content_keys: privateKeyEpochs(vault),
|
|
914
1534
|
};
|
|
915
|
-
|
|
1535
|
+
let encrypted;
|
|
1536
|
+
try {
|
|
1537
|
+
encrypted = encryptKeyring(plaintextKeyring, derived.kek, aadMeta);
|
|
1538
|
+
} finally {
|
|
1539
|
+
zeroDerivedKeys(derived);
|
|
1540
|
+
}
|
|
916
1541
|
return {
|
|
1542
|
+
...aadMeta,
|
|
917
1543
|
type: KEYRING_ENCRYPTED_TYPE,
|
|
918
1544
|
warning: ENCRYPTED_KEYRING_WARNING,
|
|
919
|
-
salt,
|
|
920
1545
|
iv: encrypted.iv,
|
|
921
1546
|
tag: encrypted.tag,
|
|
922
1547
|
ciphertext: encrypted.ciphertext,
|
|
923
|
-
signer: vault.signer,
|
|
924
|
-
publicKey: exportPublicKey(vault.signingKeyPair),
|
|
925
1548
|
};
|
|
926
1549
|
}
|
|
927
1550
|
return {
|
|
1551
|
+
...common,
|
|
928
1552
|
type: KEYRING_PLAINTEXT_TYPE,
|
|
929
1553
|
unencrypted: true,
|
|
930
1554
|
warning: PLAINTEXT_KEYRING_WARNING,
|
|
931
1555
|
vault_key_b64: vault.vaultKey.toString('base64'),
|
|
932
1556
|
address_key_b64: vault.addressKey.toString('base64'),
|
|
933
|
-
|
|
934
|
-
publicKey: exportPublicKey(vault.signingKeyPair),
|
|
1557
|
+
content_keys: privateKeyEpochs(vault),
|
|
935
1558
|
privateKey: exportPrivateKey(vault.signingKeyPair),
|
|
936
1559
|
};
|
|
937
1560
|
}
|
|
938
|
-
|
|
939
1561
|
export function exportBundle(args = {}) {
|
|
940
1562
|
const vault = args.vault;
|
|
941
1563
|
if (!vault) throw new Error('exportBundle requires a vault');
|
|
@@ -953,7 +1575,7 @@ export function exportBundle(args = {}) {
|
|
|
953
1575
|
|
|
954
1576
|
const roots = vault.__computeRoots();
|
|
955
1577
|
const bundle = {
|
|
956
|
-
schema:
|
|
1578
|
+
schema: VAULT_BUNDLE_SCHEMA,
|
|
957
1579
|
exported_at: nowIso(args.now),
|
|
958
1580
|
vault: {
|
|
959
1581
|
schema: vault.schema,
|
|
@@ -966,11 +1588,15 @@ export function exportBundle(args = {}) {
|
|
|
966
1588
|
updated_at: vault.updated_at,
|
|
967
1589
|
encryption: vault.encryption,
|
|
968
1590
|
key_id: vault.key_id,
|
|
1591
|
+
active_key_epoch: vault.activeKeyEpoch,
|
|
1592
|
+
key_epochs: publicKeyEpochs(vault),
|
|
1593
|
+
old_epoch_read_policy: vault.allowOldEpochReads === true ? 'retained_keys_allowed' : 'active_epoch_only',
|
|
1594
|
+
key_erasure_scope: 'software_process_and_persisted_bundle_only',
|
|
969
1595
|
active_set_root: roots.active_set_root,
|
|
970
1596
|
receipt_log_root: roots.receipt_log_root,
|
|
971
1597
|
sequence: vault.sequence,
|
|
972
1598
|
},
|
|
973
|
-
keyring: buildKeyring(vault, args.passphrase),
|
|
1599
|
+
keyring: buildKeyring(vault, args.passphrase, { kdf: args.kdf }),
|
|
974
1600
|
memory_objects: [...vault.memories.values()].map(publicRecord),
|
|
975
1601
|
active_memory_addresses: [...vault.activeAddresses].sort(),
|
|
976
1602
|
tombstones: [...vault.tombstones.values()].sort((a, b) => a.memory_addr.localeCompare(b.memory_addr)),
|
|
@@ -986,81 +1612,318 @@ export function exportBundle(args = {}) {
|
|
|
986
1612
|
return bundle;
|
|
987
1613
|
}
|
|
988
1614
|
|
|
1615
|
+
function isPlainObject(value) {
|
|
1616
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function assertExactKeys(value, allowed, label) {
|
|
1620
|
+
if (!isPlainObject(value)) throw new Error(`${label} must be an object`);
|
|
1621
|
+
for (const key of Object.keys(value)) {
|
|
1622
|
+
if (!allowed.has(key)) throw new Error(`${label} contains unsupported field: ${key}`);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
function assertNonEmptyString(value, label) {
|
|
1627
|
+
if (typeof value !== 'string' || value.length === 0) throw new Error(`${label} must be a non-empty string`);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function derivedPublicKeyPem(privateKey) {
|
|
1631
|
+
return createPublicKey(createPrivateKey(privateKey)).export({ type: 'spki', format: 'pem' }).trim();
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function validateRecordCiphertext(bundle, record, epochEntries) {
|
|
1635
|
+
if (record?.schema !== MEMORY_OBJECT_SCHEMA) throw new Error(`unsupported memory object schema: ${record?.schema ?? '<missing>'}`);
|
|
1636
|
+
assertNonEmptyString(record.memory_addr, 'memory_object.memory_addr');
|
|
1637
|
+
assertNonEmptyString(record.key_id, 'memory_object.key_id');
|
|
1638
|
+
if (Object.prototype.hasOwnProperty.call(record, 'content_hash')) throw new Error('memory object exposes a forbidden unkeyed content hash');
|
|
1639
|
+
for (const key of Object.keys(record)) {
|
|
1640
|
+
if (isPlaintextLikeKey(key)) throw new Error(`memory object exposes forbidden plaintext field: ${key}`);
|
|
1641
|
+
}
|
|
1642
|
+
if (!/^hmac-sha256:[a-f0-9]{64}$/.test(record.content_commitment ?? '')) {
|
|
1643
|
+
throw new Error('memory object content commitment must be keyed');
|
|
1644
|
+
}
|
|
1645
|
+
if (!Number.isInteger(record.key_epoch) || record.key_epoch < 1) throw new Error('memory_object.key_epoch must be a positive integer');
|
|
1646
|
+
const epoch = epochEntries.get(record.key_epoch);
|
|
1647
|
+
if (!epoch || epoch.key_id !== record.key_id) throw new Error(`memory object references unknown key epoch/id: ${record.key_epoch}/${record.key_id}`);
|
|
1648
|
+
const encrypted = record.content_ciphertext;
|
|
1649
|
+
if (!isPlainObject(encrypted) || encrypted.alg !== 'AES-256-GCM') throw new Error('memory object ciphertext envelope is invalid');
|
|
1650
|
+
const expectedAad = canonicalContentAad({ vault_id: bundle.vault.vault_id }, record.memory_addr, record.key_epoch, record.key_id);
|
|
1651
|
+
if (encrypted.aad !== expectedAad) throw new Error(`memory object AAD mismatch: ${record.memory_addr}`);
|
|
1652
|
+
let plaintext;
|
|
1653
|
+
try {
|
|
1654
|
+
const decipher = createDecipheriv('aes-256-gcm', epoch.key, Buffer.from(encrypted.iv, 'base64'));
|
|
1655
|
+
decipher.setAAD(Buffer.from(expectedAad, 'utf8'));
|
|
1656
|
+
decipher.setAuthTag(Buffer.from(encrypted.tag, 'base64'));
|
|
1657
|
+
plaintext = Buffer.concat([
|
|
1658
|
+
decipher.update(Buffer.from(encrypted.ciphertext, 'base64')),
|
|
1659
|
+
decipher.final(),
|
|
1660
|
+
]);
|
|
1661
|
+
} catch {
|
|
1662
|
+
throw new Error(`memory object authentication failed: ${record.memory_addr}`);
|
|
1663
|
+
} finally {
|
|
1664
|
+
plaintext?.fill(0);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function validateStrictBundle(bundle, keyring, decrypted, epochEntries) {
|
|
1669
|
+
assertExactKeys(bundle, new Set([
|
|
1670
|
+
'schema', 'exported_at', 'vault', 'keyring', 'memory_objects',
|
|
1671
|
+
'active_memory_addresses', 'tombstones', 'events', 'receipts', 'export_receipt',
|
|
1672
|
+
]), 'vault bundle');
|
|
1673
|
+
assertExactKeys(bundle.vault, new Set([
|
|
1674
|
+
'schema', 'vault_id', 'tenant_id', 'subject_id', 'actor_id', 'policy_id',
|
|
1675
|
+
'created_at', 'updated_at', 'encryption', 'key_id', 'active_key_epoch',
|
|
1676
|
+
'key_epochs', 'old_epoch_read_policy', 'key_erasure_scope', 'active_set_root',
|
|
1677
|
+
'receipt_log_root', 'sequence',
|
|
1678
|
+
]), 'vault bundle manifest');
|
|
1679
|
+
const encryptedKeyringFields = new Set([
|
|
1680
|
+
'schema', 'vault_schema', 'vault_id', 'active_key_epoch', 'key_epochs',
|
|
1681
|
+
'old_epoch_read_policy', 'signer', 'publicKey', 'type', 'warning', 'kdf', 'salt', 'iv', 'tag', 'ciphertext',
|
|
1682
|
+
]);
|
|
1683
|
+
const plaintextKeyringFields = new Set([
|
|
1684
|
+
'schema', 'vault_schema', 'vault_id', 'active_key_epoch', 'key_epochs',
|
|
1685
|
+
'old_epoch_read_policy', 'signer', 'publicKey', 'type', 'warning', 'unencrypted', 'vault_key_b64',
|
|
1686
|
+
'address_key_b64', 'content_keys', 'privateKey',
|
|
1687
|
+
]);
|
|
1688
|
+
assertExactKeys(
|
|
1689
|
+
keyring,
|
|
1690
|
+
keyring.type === KEYRING_ENCRYPTED_TYPE ? encryptedKeyringFields : plaintextKeyringFields,
|
|
1691
|
+
'vault keyring',
|
|
1692
|
+
);
|
|
1693
|
+
assertExactKeys(
|
|
1694
|
+
decrypted,
|
|
1695
|
+
keyring.type === KEYRING_ENCRYPTED_TYPE
|
|
1696
|
+
? new Set(['vault_key_b64', 'address_key_b64', 'privateKey', 'content_keys'])
|
|
1697
|
+
: new Set(['vault_key_b64', 'address_key_b64', 'privateKey', 'content_keys']),
|
|
1698
|
+
'decrypted vault keyring',
|
|
1699
|
+
);
|
|
1700
|
+
if (bundle.vault.schema !== VAULT_SCHEMA) throw new Error('vault manifest schema mismatch');
|
|
1701
|
+
if (bundle.vault.encryption !== 'local_aead') throw new Error('vault manifest encryption mismatch');
|
|
1702
|
+
assertNonEmptyString(bundle.vault.vault_id, 'vault.vault_id');
|
|
1703
|
+
if (!Number.isInteger(bundle.vault.active_key_epoch) || bundle.vault.active_key_epoch < 1) {
|
|
1704
|
+
throw new Error('vault.active_key_epoch must be a positive integer');
|
|
1705
|
+
}
|
|
1706
|
+
if (!Number.isInteger(bundle.vault.sequence) || bundle.vault.sequence < 0) throw new Error('vault.sequence must be a non-negative integer');
|
|
1707
|
+
if (keyring.schema !== 'enigma.vault_keyring.v2') throw new Error('keyring schema mismatch');
|
|
1708
|
+
if (keyring.vault_schema !== bundle.vault.schema || keyring.vault_id !== bundle.vault.vault_id) {
|
|
1709
|
+
throw new Error('keyring vault binding mismatch');
|
|
1710
|
+
}
|
|
1711
|
+
if (keyring.active_key_epoch !== bundle.vault.active_key_epoch) throw new Error('keyring active key epoch mismatch');
|
|
1712
|
+
if (keyring.old_epoch_read_policy !== bundle.vault.old_epoch_read_policy) {
|
|
1713
|
+
throw new Error('keyring old-epoch read policy mismatch');
|
|
1714
|
+
}
|
|
1715
|
+
const decryptedEpochDescriptors = [...epochEntries.values()]
|
|
1716
|
+
.sort((left, right) => left.epoch - right.epoch)
|
|
1717
|
+
.map((entry) => pruneUndefined({
|
|
1718
|
+
epoch: entry.epoch,
|
|
1719
|
+
key_id: entry.key_id,
|
|
1720
|
+
status: entry.status,
|
|
1721
|
+
created_at: entry.created_at,
|
|
1722
|
+
sealed_at: entry.sealed_at,
|
|
1723
|
+
}));
|
|
1724
|
+
if (canonical(decryptedEpochDescriptors) !== canonical(keyring.key_epochs)) {
|
|
1725
|
+
throw new Error('decrypted content keys do not match signed key epoch descriptors');
|
|
1726
|
+
}
|
|
1727
|
+
if (bundle.vault.key_id !== epochEntries.get(bundle.vault.active_key_epoch)?.key_id) throw new Error('vault active key id mismatch');
|
|
1728
|
+
if (canonical(keyring.key_epochs) !== canonical(bundle.vault.key_epochs)) throw new Error('key epoch manifest mismatch');
|
|
1729
|
+
if (derivedPublicKeyPem(decrypted.privateKey) !== String(keyring.publicKey).trim()) throw new Error('keyring public/private signing key mismatch');
|
|
1730
|
+
if (keyring.signer?.key_id === undefined) throw new Error('keyring signer key id is missing');
|
|
1731
|
+
if (!Array.isArray(keyring.key_epochs) || keyring.key_epochs.length !== epochEntries.size) {
|
|
1732
|
+
throw new Error('keyring key epoch descriptor count mismatch');
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (!Array.isArray(bundle.receipts) || !Array.isArray(bundle.events)) throw new Error('bundle receipts and events must be arrays');
|
|
1736
|
+
if (bundle.receipts.length !== bundle.events.length || bundle.vault.sequence !== bundle.receipts.length) {
|
|
1737
|
+
throw new Error('bundle event/receipt sequence length mismatch');
|
|
1738
|
+
}
|
|
1739
|
+
for (let index = 0; index < bundle.events.length; index += 1) {
|
|
1740
|
+
const entry = bundle.events[index];
|
|
1741
|
+
assertExactKeys(entry, new Set(['event', 'canonical', 'hash']), `event entry ${index}`);
|
|
1742
|
+
if (entry.event?.schema !== EVENT_SCHEMA || entry.event?.sequence !== index) throw new Error(`event ${index} schema or sequence mismatch`);
|
|
1743
|
+
const expectedCanonical = canonical(entry.event);
|
|
1744
|
+
const expectedHash = asSha256(expectedCanonical);
|
|
1745
|
+
if (entry.canonical !== expectedCanonical || entry.hash !== expectedHash) throw new Error(`event ${index} canonical/hash mismatch`);
|
|
1746
|
+
const receipt = bundle.receipts[index];
|
|
1747
|
+
if (receipt?.tenant_id !== bundle.vault.tenant_id || receipt?.subject_id !== bundle.vault.subject_id) {
|
|
1748
|
+
throw new Error(`receipt ${index} vault identity binding mismatch`);
|
|
1749
|
+
}
|
|
1750
|
+
if (receipt?.event_hash !== expectedHash || receipt?.operation !== entry.event.operation) throw new Error(`event ${index} receipt binding mismatch`);
|
|
1751
|
+
}
|
|
1752
|
+
const chain = verifyReceiptChain({
|
|
1753
|
+
receipts: bundle.receipts,
|
|
1754
|
+
publicKey: keyring.publicKey,
|
|
1755
|
+
expectedSignerKeyId: keyring.signer.key_id,
|
|
1756
|
+
expectedReceiptLogRoot: bundle.vault.receipt_log_root,
|
|
1757
|
+
expectedActiveSetRoot: bundle.vault.active_set_root,
|
|
1758
|
+
verifyEmbeddedReceiptLogRoot: false,
|
|
1759
|
+
});
|
|
1760
|
+
if (!chain.ok) throw new Error(`receipt chain verification failed: ${chain.errors.join('; ')}`);
|
|
1761
|
+
if (bundle.receipts.length === 0 || canonical(bundle.export_receipt) !== canonical(bundle.receipts.at(-1))) {
|
|
1762
|
+
throw new Error('export receipt does not match the signed receipt chain');
|
|
1763
|
+
}
|
|
1764
|
+
if (computeSetRoot(bundle.active_memory_addresses ?? []) !== bundle.vault.active_set_root) {
|
|
1765
|
+
throw new Error('active memory address root mismatch');
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
if (!Array.isArray(bundle.memory_objects) || !Array.isArray(bundle.active_memory_addresses) || !Array.isArray(bundle.tombstones)) {
|
|
1769
|
+
throw new Error('bundle lifecycle collections must be arrays');
|
|
1770
|
+
}
|
|
1771
|
+
const recordByAddress = new Map();
|
|
1772
|
+
const activeAddressSet = new Set(bundle.active_memory_addresses);
|
|
1773
|
+
if (activeAddressSet.size !== bundle.active_memory_addresses.length) throw new Error('duplicate active memory address');
|
|
1774
|
+
for (const record of bundle.memory_objects) {
|
|
1775
|
+
if (recordByAddress.has(record.memory_addr)) throw new Error(`duplicate memory address: ${record.memory_addr}`);
|
|
1776
|
+
recordByAddress.set(record.memory_addr, record);
|
|
1777
|
+
validateRecordCiphertext(bundle, record, epochEntries);
|
|
1778
|
+
}
|
|
1779
|
+
for (const address of bundle.active_memory_addresses) {
|
|
1780
|
+
if (!recordByAddress.has(address)) throw new Error(`active memory address has no record: ${address}`);
|
|
1781
|
+
const record = recordByAddress.get(address);
|
|
1782
|
+
if (record?.state !== 'active') throw new Error(`active memory address references non-active record: ${address}`);
|
|
1783
|
+
}
|
|
1784
|
+
for (const record of bundle.memory_objects) {
|
|
1785
|
+
if (record.state === 'active' && !activeAddressSet.has(record.memory_addr)) {
|
|
1786
|
+
throw new Error(`active memory record is absent from active address set: ${record.memory_addr}`);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
989
1791
|
export function importBundle(args = {}) {
|
|
990
|
-
const source = args.schema ===
|
|
1792
|
+
const source = args.schema === VAULT_BUNDLE_SCHEMA ? args : (args.bundle?.bundle ?? args.bundle ?? args);
|
|
991
1793
|
const bundle = typeof source === 'string' ? JSON.parse(source) : source;
|
|
992
|
-
if (!bundle || bundle.schema !==
|
|
1794
|
+
if (!bundle || bundle.schema !== VAULT_BUNDLE_SCHEMA) throw new Error(`importBundle requires an ${VAULT_BUNDLE_SCHEMA} bundle`);
|
|
1795
|
+
const strict = args.strict === true;
|
|
1796
|
+
const insecureDevMode = args.insecureDevMode === true || args.insecure_dev_mode === true;
|
|
993
1797
|
const warnings = [];
|
|
994
|
-
let vaultKey;
|
|
995
|
-
let addressKey;
|
|
996
|
-
let privateKey;
|
|
997
1798
|
const keyring = bundle.keyring;
|
|
1799
|
+
let decrypted;
|
|
998
1800
|
if (keyring?.type === KEYRING_ENCRYPTED_TYPE) {
|
|
999
|
-
if (args.passphrase === undefined || args.passphrase === null || String(args.passphrase).length === 0) {
|
|
1801
|
+
if (args.passphrase === undefined || args.passphrase === null || String(args.passphrase).trim().length === 0) {
|
|
1000
1802
|
throw new Error('Encrypted keyring requires passphrase');
|
|
1001
1803
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1804
|
+
decrypted = decryptKeyring(keyring, args.passphrase);
|
|
1805
|
+
} else if (keyring?.type === KEYRING_PLAINTEXT_TYPE) {
|
|
1806
|
+
if (strict && !insecureDevMode) throw new Error('Strict vault import rejects plaintext keyrings outside explicit insecure-dev mode');
|
|
1807
|
+
warnings.push(DEPRECATION_PLAINTEXT_KEYRING);
|
|
1808
|
+
decrypted = {
|
|
1809
|
+
vault_key_b64: keyring.vault_key_b64,
|
|
1810
|
+
address_key_b64: keyring.address_key_b64,
|
|
1811
|
+
privateKey: keyring.privateKey,
|
|
1812
|
+
content_keys: keyring.content_keys,
|
|
1813
|
+
};
|
|
1006
1814
|
} else {
|
|
1007
|
-
|
|
1008
|
-
warnings.push(DEPRECATION_PLAINTEXT_KEYRING);
|
|
1009
|
-
}
|
|
1010
|
-
vaultKey = keyring?.vault_key_b64;
|
|
1011
|
-
addressKey = keyring?.address_key_b64;
|
|
1012
|
-
privateKey = keyring?.privateKey;
|
|
1815
|
+
throw new Error(`Unsupported vault keyring type: ${keyring?.type ?? '<missing>'}`);
|
|
1013
1816
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
:
|
|
1817
|
+
assertNonEmptyString(decrypted?.address_key_b64, 'keyring address key');
|
|
1818
|
+
assertNonEmptyString(decrypted?.privateKey, 'keyring private signing key');
|
|
1819
|
+
keyBytes(decrypted.address_key_b64, 'address key');
|
|
1820
|
+
|
|
1821
|
+
const activeKeyEpoch = Number(bundle.vault?.active_key_epoch ?? keyring.active_key_epoch ?? 1);
|
|
1822
|
+
const rawContentKeys = Array.isArray(decrypted.content_keys) && decrypted.content_keys.length > 0
|
|
1823
|
+
? decrypted.content_keys
|
|
1824
|
+
: [{
|
|
1825
|
+
epoch: activeKeyEpoch,
|
|
1826
|
+
key_id: bundle.vault?.key_id,
|
|
1827
|
+
status: 'active',
|
|
1828
|
+
created_at: bundle.vault?.created_at,
|
|
1829
|
+
key_b64: decrypted.vault_key_b64,
|
|
1830
|
+
}];
|
|
1831
|
+
const epochs = rawContentKeys.map((entry) => normalizeKeyEpoch(entry, undefined, undefined, bundle.vault?.created_at));
|
|
1832
|
+
const epochEntries = new Map(epochs.map((entry) => [entry.epoch, entry]));
|
|
1833
|
+
if (epochEntries.size !== epochs.length) throw new Error('duplicate content key epoch');
|
|
1834
|
+
if (!epochEntries.has(activeKeyEpoch)) throw new Error(`active content key epoch ${activeKeyEpoch} is unavailable`);
|
|
1835
|
+
if (decrypted.vault_key_b64 !== undefined
|
|
1836
|
+
&& decrypted.vault_key_b64 !== epochEntries.get(activeKeyEpoch).key.toString('base64')) {
|
|
1837
|
+
throw new Error('active vault key alias does not match the active content key epoch');
|
|
1838
|
+
}
|
|
1839
|
+
if (strict) validateStrictBundle(bundle, keyring, decrypted, epochEntries);
|
|
1840
|
+
|
|
1841
|
+
const restoredSigningKeyPair = {
|
|
1842
|
+
key_id: keyring.signer?.key_id,
|
|
1843
|
+
signer: keyring.signer,
|
|
1844
|
+
publicKey: keyring.publicKey,
|
|
1845
|
+
privateKey: decrypted.privateKey,
|
|
1846
|
+
};
|
|
1022
1847
|
const vault = createVault({
|
|
1023
1848
|
vault_id: bundle.vault?.vault_id,
|
|
1024
1849
|
tenant_id: bundle.vault?.tenant_id,
|
|
1025
1850
|
subject_id: bundle.vault?.subject_id,
|
|
1026
1851
|
actor_id: args.actor_id ?? args.actorId ?? bundle.vault?.actor_id,
|
|
1027
1852
|
policy_id: bundle.vault?.policy_id,
|
|
1028
|
-
vault_key:
|
|
1029
|
-
address_key:
|
|
1853
|
+
vault_key: epochEntries.get(activeKeyEpoch).key,
|
|
1854
|
+
address_key: decrypted.address_key_b64,
|
|
1855
|
+
key_epochs: epochs,
|
|
1856
|
+
active_key_epoch: activeKeyEpoch,
|
|
1857
|
+
allow_old_epoch_reads: args.allowOldEpochReads ?? args.allow_old_epoch_reads
|
|
1858
|
+
?? (keyring.old_epoch_read_policy ?? bundle.vault?.old_epoch_read_policy) !== 'active_epoch_only',
|
|
1030
1859
|
signingKeyPair: restoredSigningKeyPair,
|
|
1031
|
-
signer: keyring
|
|
1860
|
+
signer: keyring.signer,
|
|
1032
1861
|
now: bundle.vault?.created_at,
|
|
1033
1862
|
});
|
|
1034
1863
|
vault.created_at = bundle.vault?.created_at ?? vault.created_at;
|
|
1035
1864
|
vault.updated_at = bundle.vault?.updated_at ?? vault.updated_at;
|
|
1036
1865
|
vault.sequence = Number.isInteger(bundle.vault?.sequence) ? bundle.vault.sequence : 0;
|
|
1037
|
-
vault.events = Array.isArray(bundle.events)
|
|
1866
|
+
vault.events = Array.isArray(bundle.events)
|
|
1867
|
+
? (strict
|
|
1868
|
+
? bundle.events.map((entry) => ({ ...entry, event: { ...entry.event } }))
|
|
1869
|
+
: bundle.events.map(sanitizeImportedEventEntry).filter(Boolean))
|
|
1870
|
+
: [];
|
|
1038
1871
|
vault.receipts = Array.isArray(bundle.receipts) ? bundle.receipts.map((receipt) => ({
|
|
1039
|
-
...sanitizePublicValue(receipt),
|
|
1040
|
-
signer: receipt.signer ? { ...sanitizePublicValue(receipt.signer) } : receipt.signer,
|
|
1041
|
-
signature: receipt.signature ? { ...sanitizePublicValue(receipt.signature) } : receipt.signature,
|
|
1872
|
+
...(strict ? receipt : sanitizePublicValue(receipt)),
|
|
1873
|
+
signer: receipt.signer ? { ...(strict ? receipt.signer : sanitizePublicValue(receipt.signer)) } : receipt.signer,
|
|
1874
|
+
signature: receipt.signature ? { ...(strict ? receipt.signature : sanitizePublicValue(receipt.signature)) } : receipt.signature,
|
|
1042
1875
|
})) : [];
|
|
1043
1876
|
vault.memories = new Map();
|
|
1044
1877
|
for (const record of bundle.memory_objects ?? []) {
|
|
1045
|
-
|
|
1878
|
+
const imported = strict ? record : publicRecord(record);
|
|
1879
|
+
vault.memories.set(record.memory_addr, { ...imported, content_ciphertext: { ...imported.content_ciphertext } });
|
|
1046
1880
|
}
|
|
1047
1881
|
const lifecycle = reconcileImportedLifecycle(bundle, vault.memories, vault.receipts);
|
|
1048
1882
|
vault.activeAddresses = lifecycle.active;
|
|
1049
1883
|
vault.tombstones = lifecycle.tombstones;
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
return {
|
|
1065
|
-
|
|
1884
|
+
applyLifecycleRoots(vault);
|
|
1885
|
+
if (args.recordImport !== false) {
|
|
1886
|
+
appendEvent(vault, {
|
|
1887
|
+
operation: 'import',
|
|
1888
|
+
actor_id: args.actor_id ?? args.actorId,
|
|
1889
|
+
policy_id: args.policy_id ?? args.policyId,
|
|
1890
|
+
now: args.now,
|
|
1891
|
+
metadata: {
|
|
1892
|
+
imported_memory_count: vault.memories.size,
|
|
1893
|
+
imported_active_count: vault.activeAddresses.size,
|
|
1894
|
+
imported_tombstone_count: vault.tombstones.size,
|
|
1895
|
+
},
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
return {
|
|
1899
|
+
vault,
|
|
1900
|
+
active_memory_addresses: [...vault.activeAddresses].sort(),
|
|
1901
|
+
tombstones: [...vault.tombstones.values()],
|
|
1902
|
+
warnings,
|
|
1903
|
+
verified: strict,
|
|
1904
|
+
};
|
|
1066
1905
|
}
|
|
1906
|
+
|
|
1907
|
+
export * from './poseidon.js';
|
|
1908
|
+
export * from './enclave.js';
|
|
1909
|
+
// Named (not star) export: e2ee.mjs also defines poseidon(), which would conflict
|
|
1910
|
+
// with poseidon.js under export *.
|
|
1911
|
+
export {
|
|
1912
|
+
E2EE_SCHEMA,
|
|
1913
|
+
PAYLOAD_COMMIT_DOMAIN,
|
|
1914
|
+
HPKE_WRAP_INFO,
|
|
1915
|
+
BN254_SCALAR_FIELD,
|
|
1916
|
+
canonicalJson,
|
|
1917
|
+
normalizeFieldElement32,
|
|
1918
|
+
deriveScopeCommit,
|
|
1919
|
+
deriveNoteContext,
|
|
1920
|
+
deriveAad,
|
|
1921
|
+
generateRecipientKeyPair,
|
|
1922
|
+
derivePayloadCommit,
|
|
1923
|
+
deriveFinalNote,
|
|
1924
|
+
e2eeEncrypt,
|
|
1925
|
+
e2eeDecrypt,
|
|
1926
|
+
} from './e2ee.mjs';
|
|
1927
|
+
export * from './receipt.js';
|
|
1928
|
+
export * from './erasure.js';
|
|
1929
|
+
export * from './persistence.js';
|