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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Enigma Encrypted Mesh Protocol ("GhostMesh") — Cryptographic Engine.
|
|
2
|
+
// Provides true X25519 ECDH session key agreement, AES-256-GCM authenticated
|
|
3
|
+
// payload encryption with AAD binding, and Ed25519 envelope signing for opaque mesh relaying.
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { getSignableBytes, encodePacket, decodePacket, deriveDestination, HEADER_SIZE } from './protocol.js';
|
|
6
|
+
|
|
7
|
+
// ASN.1 DER prefixes for raw 32-byte Ed25519 and X25519 keys in Node crypto
|
|
8
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
9
|
+
const ED25519_PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
10
|
+
const X25519_SPKI_PREFIX = Buffer.from('302a300506032b656e032100', 'hex');
|
|
11
|
+
const X25519_PKCS8_PREFIX = Buffer.from('302e020100300506032b656e04220420', 'hex');
|
|
12
|
+
|
|
13
|
+
export function exportRawEd25519PublicKey(keyObject) {
|
|
14
|
+
const der = keyObject.export({ type: 'spki', format: 'der' });
|
|
15
|
+
return der.subarray(der.length - 32);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function exportRawEd25519PrivateKey(keyObject) {
|
|
19
|
+
const der = keyObject.export({ type: 'pkcs8', format: 'der' });
|
|
20
|
+
return der.subarray(der.length - 32);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function importRawEd25519PublicKey(raw32) {
|
|
24
|
+
const buf = Buffer.isBuffer(raw32) ? raw32 : Buffer.from(raw32, 'hex');
|
|
25
|
+
if (buf.length !== 32) throw new RangeError(`Ed25519 public key must be exactly 32 bytes; got ${buf.length}`);
|
|
26
|
+
const der = Buffer.concat([ED25519_SPKI_PREFIX, buf]);
|
|
27
|
+
return crypto.createPublicKey({ key: der, format: 'der', type: 'spki' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function importRawEd25519PrivateKey(raw32) {
|
|
31
|
+
const buf = Buffer.isBuffer(raw32) ? raw32 : Buffer.from(raw32, 'hex');
|
|
32
|
+
if (buf.length !== 32) throw new RangeError(`Ed25519 private key must be exactly 32 bytes; got ${buf.length}`);
|
|
33
|
+
const der = Buffer.concat([ED25519_PKCS8_PREFIX, buf]);
|
|
34
|
+
return crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function exportRawX25519PublicKey(keyObject) {
|
|
38
|
+
const der = keyObject.export({ type: 'spki', format: 'der' });
|
|
39
|
+
return der.subarray(der.length - 32);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function exportRawX25519PrivateKey(keyObject) {
|
|
43
|
+
const der = keyObject.export({ type: 'pkcs8', format: 'der' });
|
|
44
|
+
return der.subarray(der.length - 32);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function importRawX25519PublicKey(raw32) {
|
|
48
|
+
const buf = Buffer.isBuffer(raw32) ? raw32 : Buffer.from(raw32, 'hex');
|
|
49
|
+
if (buf.length !== 32) throw new RangeError(`X25519 public key must be exactly 32 bytes; got ${buf.length}`);
|
|
50
|
+
const der = Buffer.concat([X25519_SPKI_PREFIX, buf]);
|
|
51
|
+
return crypto.createPublicKey({ key: der, format: 'der', type: 'spki' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function importRawX25519PrivateKey(raw32) {
|
|
55
|
+
const buf = Buffer.isBuffer(raw32) ? raw32 : Buffer.from(raw32, 'hex');
|
|
56
|
+
if (buf.length !== 32) throw new RangeError(`X25519 private key must be exactly 32 bytes; got ${buf.length}`);
|
|
57
|
+
const der = Buffer.concat([X25519_PKCS8_PREFIX, buf]);
|
|
58
|
+
return crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generates an Ed25519 signing identity and an X25519 encryption keypair.
|
|
63
|
+
* @returns {{publicKey: Buffer, secretKey: Buffer, encryptionPublicKey: Buffer, encryptionSecretKey: Buffer, destination: Buffer, destinationHex: string}}
|
|
64
|
+
*/
|
|
65
|
+
export function generateMeshIdentity(scope = 'enigma.global') {
|
|
66
|
+
const ed = crypto.generateKeyPairSync('ed25519');
|
|
67
|
+
const edPub = exportRawEd25519PublicKey(ed.publicKey);
|
|
68
|
+
const edPriv = exportRawEd25519PrivateKey(ed.privateKey);
|
|
69
|
+
|
|
70
|
+
const x = crypto.generateKeyPairSync('x25519');
|
|
71
|
+
const xPub = exportRawX25519PublicKey(x.publicKey);
|
|
72
|
+
const xPriv = exportRawX25519PrivateKey(x.privateKey);
|
|
73
|
+
|
|
74
|
+
const dest = deriveDestination(edPub, xPub, scope);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
publicKey: edPub,
|
|
78
|
+
secretKey: edPriv,
|
|
79
|
+
encryptionPublicKey: xPub,
|
|
80
|
+
encryptionSecretKey: xPriv,
|
|
81
|
+
destination: dest,
|
|
82
|
+
destinationHex: dest.toString('hex'),
|
|
83
|
+
scope,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function deriveSymmetricKey(sharedSecret, salt, info = 'ghostmesh.e2ee.v1') {
|
|
87
|
+
const raw = crypto.hkdfSync('sha256', sharedSecret, salt, Buffer.from(info, 'utf8'), 32);
|
|
88
|
+
return Buffer.from(raw);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const GHOSTMESH_E2EE_SALT = Buffer.from('ghostmesh.e2ee.v1', 'utf8');
|
|
92
|
+
|
|
93
|
+
export function sealPayload(plaintext, recipientX25519PubBytes, opts = {}) {
|
|
94
|
+
const plainBuf = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext, 'utf8');
|
|
95
|
+
const recipientPubObj = importRawX25519PublicKey(recipientX25519PubBytes);
|
|
96
|
+
|
|
97
|
+
// Generate ephemeral X25519 keypair
|
|
98
|
+
const eph = crypto.generateKeyPairSync('x25519');
|
|
99
|
+
const ephPubRaw = exportRawX25519PublicKey(eph.publicKey);
|
|
100
|
+
|
|
101
|
+
// Real X25519 ECDH Diffie-Hellman shared secret
|
|
102
|
+
const sharedSecret = crypto.diffieHellman({
|
|
103
|
+
privateKey: eph.privateKey,
|
|
104
|
+
publicKey: recipientPubObj,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const symKey = deriveSymmetricKey(sharedSecret, GHOSTMESH_E2EE_SALT, 'ghostmesh.key.v1');
|
|
108
|
+
const iv = Buffer.from(crypto.hkdfSync('sha256', sharedSecret, GHOSTMESH_E2EE_SALT, Buffer.from('ghostmesh.iv.v1', 'utf8'), 12));
|
|
109
|
+
|
|
110
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', symKey, iv);
|
|
111
|
+
if (opts.aad) {
|
|
112
|
+
cipher.setAAD(Buffer.isBuffer(opts.aad) ? opts.aad : Buffer.from(opts.aad));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ciphertext = Buffer.concat([cipher.update(plainBuf), cipher.final()]);
|
|
116
|
+
const authTag = cipher.getAuthTag();
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
ciphertext,
|
|
120
|
+
authTag,
|
|
121
|
+
ephemeralPubkey: ephPubRaw, // exactly 32 bytes
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Unseals an opaque payload using the recipient's X25519 private key.
|
|
127
|
+
* Intermediate relay nodes without recipientX25519PrivBytes cannot compute the ECDH shared secret.
|
|
128
|
+
* @param {Buffer} ciphertext
|
|
129
|
+
* @param {Buffer} authTag - 16-byte GCM auth tag
|
|
130
|
+
* @param {Buffer} ephemeralPubkey - 32-byte header field (Ephemeral X25519 Pub)
|
|
131
|
+
* @param {Buffer} recipientX25519PrivBytes - Recipient's 32-byte X25519 private key
|
|
132
|
+
* @param {Object} [opts]
|
|
133
|
+
* @param {Buffer|string} [opts.aad]
|
|
134
|
+
* @returns {Buffer} Decrypted plaintext buffer
|
|
135
|
+
*/
|
|
136
|
+
export function unsealPayload(ciphertext, authTag, ephemeralPubkey, recipientX25519PrivBytes, opts = {}) {
|
|
137
|
+
const recipientPrivObj = importRawX25519PrivateKey(recipientX25519PrivBytes);
|
|
138
|
+
const ephPubObj = importRawX25519PublicKey(ephemeralPubkey);
|
|
139
|
+
|
|
140
|
+
// Real X25519 ECDH Diffie-Hellman shared secret
|
|
141
|
+
const sharedSecret = crypto.diffieHellman({
|
|
142
|
+
privateKey: recipientPrivObj,
|
|
143
|
+
publicKey: ephPubObj,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const symKey = deriveSymmetricKey(sharedSecret, GHOSTMESH_E2EE_SALT, 'ghostmesh.key.v1');
|
|
147
|
+
const iv = Buffer.from(crypto.hkdfSync('sha256', sharedSecret, GHOSTMESH_E2EE_SALT, Buffer.from('ghostmesh.iv.v1', 'utf8'), 12));
|
|
148
|
+
|
|
149
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', symKey, iv);
|
|
150
|
+
if (opts.aad) {
|
|
151
|
+
decipher.setAAD(Buffer.isBuffer(opts.aad) ? opts.aad : Buffer.from(opts.aad));
|
|
152
|
+
}
|
|
153
|
+
decipher.setAuthTag(authTag);
|
|
154
|
+
|
|
155
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Signs signable packet bytes with an Ed25519 secret key.
|
|
159
|
+
* @param {Buffer} packetBuffer
|
|
160
|
+
* @param {Buffer} secretKey
|
|
161
|
+
* @returns {Buffer} Packet with signature appended
|
|
162
|
+
*/
|
|
163
|
+
export function signPacket(packetBuffer, secretKey) {
|
|
164
|
+
const privKeyObj = importRawEd25519PrivateKey(secretKey);
|
|
165
|
+
const signable = getSignableBytes(packetBuffer);
|
|
166
|
+
const signature = crypto.sign(null, signable, privKeyObj);
|
|
167
|
+
const out = Buffer.from(packetBuffer);
|
|
168
|
+
signature.copy(out, out.length - 64);
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Verifies an Ed25519 signature over a signed packet buffer.
|
|
174
|
+
* @param {Buffer} signedPacketBuffer
|
|
175
|
+
* @returns {boolean}
|
|
176
|
+
*/
|
|
177
|
+
export function verifyPacketSignature(signedPacketBuffer) {
|
|
178
|
+
if (signedPacketBuffer.length < HEADER_SIZE + 16 + 64) return false;
|
|
179
|
+
const signature = signedPacketBuffer.subarray(signedPacketBuffer.length - 64);
|
|
180
|
+
const signable = getSignableBytes(signedPacketBuffer);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const decoded = decodePacket(signedPacketBuffer);
|
|
184
|
+
const pubKeyObj = importRawEd25519PublicKey(decoded.senderPubkey);
|
|
185
|
+
return crypto.verify(null, signable, pubKeyObj, signature);
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
PACKET_TYPES,
|
|
4
|
+
decodePacket,
|
|
5
|
+
deriveDestination,
|
|
6
|
+
encodePacket,
|
|
7
|
+
getPacketReplayIdentity,
|
|
8
|
+
} from './protocol.js';
|
|
9
|
+
import { sealPayload, signPacket, unsealPayload, verifyPacketSignature } from './crypto.js';
|
|
10
|
+
|
|
11
|
+
export const GHOSTMESH_FEDERATION_PACKET_SCHEMA = 'enigma.ghostmesh_federation_packet.v1';
|
|
12
|
+
|
|
13
|
+
export const FEDERATION_PACKET_KINDS = Object.freeze({
|
|
14
|
+
CAPABILITY_GRANT: 'capability_grant',
|
|
15
|
+
QUERY_REQUEST: 'query_request',
|
|
16
|
+
QUERY_RESPONSE: 'query_response',
|
|
17
|
+
CAPABILITY_REVOCATION: 'capability_revocation',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const KIND_TO_TYPE = Object.freeze({
|
|
21
|
+
[FEDERATION_PACKET_KINDS.CAPABILITY_GRANT]: PACKET_TYPES.CAPABILITY_GRANT,
|
|
22
|
+
[FEDERATION_PACKET_KINDS.QUERY_REQUEST]: PACKET_TYPES.FEDERATED_QUERY_REQUEST,
|
|
23
|
+
[FEDERATION_PACKET_KINDS.QUERY_RESPONSE]: PACKET_TYPES.FEDERATED_QUERY_RESPONSE,
|
|
24
|
+
[FEDERATION_PACKET_KINDS.CAPABILITY_REVOCATION]: PACKET_TYPES.CAPABILITY_REVOCATION,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const TYPE_TO_KIND = Object.freeze(
|
|
28
|
+
Object.fromEntries(Object.entries(KIND_TO_TYPE).map(([kind, type]) => [type, kind]))
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const HEX_32_RE = /^[0-9a-f]{64}$/;
|
|
32
|
+
const PACKET_ID_RE = /^[0-9a-f]{32}$/;
|
|
33
|
+
const ENVELOPE_KEYS = [
|
|
34
|
+
'body',
|
|
35
|
+
'correlation_id',
|
|
36
|
+
'created_at',
|
|
37
|
+
'kind',
|
|
38
|
+
'packet_id',
|
|
39
|
+
'recipient_destination',
|
|
40
|
+
'schema',
|
|
41
|
+
'sender',
|
|
42
|
+
'version',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function isRecord(value) {
|
|
46
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) && !Buffer.isBuffer(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertExactKeys(value, expected, name) {
|
|
50
|
+
if (!isRecord(value)) throw new Error(`${name} must be an object`);
|
|
51
|
+
const actual = Object.keys(value).sort();
|
|
52
|
+
const wanted = expected.slice().sort();
|
|
53
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
54
|
+
throw new Error(`${name} fields must be exactly: ${wanted.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toBuffer32(value, name) {
|
|
59
|
+
let buffer;
|
|
60
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
61
|
+
buffer = Buffer.from(value);
|
|
62
|
+
} else if (typeof value === 'string' && HEX_32_RE.test(value.toLowerCase())) {
|
|
63
|
+
buffer = Buffer.from(value, 'hex');
|
|
64
|
+
} else {
|
|
65
|
+
throw new Error(`${name} must be a 32-byte value`);
|
|
66
|
+
}
|
|
67
|
+
if (buffer.length !== 32) throw new Error(`${name} must be a 32-byte value`);
|
|
68
|
+
return buffer;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function canonicalize(value) {
|
|
72
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') return JSON.stringify(value);
|
|
73
|
+
if (typeof value === 'number') {
|
|
74
|
+
if (!Number.isFinite(value)) throw new Error('Federation packet values must contain only finite numbers');
|
|
75
|
+
return JSON.stringify(value);
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(value)) return `[${value.map(canonicalize).join(',')}]`;
|
|
78
|
+
if (!isRecord(value)) throw new Error('Federation packet values must be JSON-compatible objects');
|
|
79
|
+
return `{${Object.keys(value).sort().map((key) => {
|
|
80
|
+
if (value[key] === undefined) throw new Error(`Federation packet field ${key} cannot be undefined`);
|
|
81
|
+
return `${JSON.stringify(key)}:${canonicalize(value[key])}`;
|
|
82
|
+
}).join(',')}}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function hashFederationValue(value) {
|
|
86
|
+
return crypto.createHash('sha256').update(canonicalize(value), 'utf8').digest('hex');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function assertIsoTimestamp(value, name) {
|
|
90
|
+
if (typeof value !== 'string') throw new Error(`${name} must be an ISO timestamp string`);
|
|
91
|
+
const date = new Date(value);
|
|
92
|
+
if (!Number.isFinite(date.getTime()) || date.toISOString() !== value) {
|
|
93
|
+
throw new Error(`${name} must be a canonical ISO timestamp`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeEndpoint(endpoint, name, { requireSecret = false } = {}) {
|
|
98
|
+
if (!endpoint || typeof endpoint !== 'object') throw new Error(`${name} is required`);
|
|
99
|
+
const publicKey = toBuffer32(endpoint.publicKey ?? endpoint.public_key, `${name}.publicKey`);
|
|
100
|
+
const encryptionPublicKey = toBuffer32(
|
|
101
|
+
endpoint.encryptionPublicKey ?? endpoint.encryption_public_key,
|
|
102
|
+
`${name}.encryptionPublicKey`
|
|
103
|
+
);
|
|
104
|
+
const scope = endpoint.scope;
|
|
105
|
+
if (typeof scope !== 'string' || scope.length === 0 || scope.length > 256) {
|
|
106
|
+
throw new Error(`${name}.scope must be a non-empty string of at most 256 characters`);
|
|
107
|
+
}
|
|
108
|
+
const derivedDestination = deriveDestination(publicKey, encryptionPublicKey, scope);
|
|
109
|
+
const declaredDestination = endpoint.destination ?? endpoint.destinationHex ?? endpoint.destination_hex;
|
|
110
|
+
if (declaredDestination !== undefined && declaredDestination !== null) {
|
|
111
|
+
const destination = toBuffer32(declaredDestination, `${name}.destination`);
|
|
112
|
+
if (!destination.equals(derivedDestination)) {
|
|
113
|
+
throw new Error(`${name}.destination does not match its signing key, encryption key, and scope`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let encryptionSecretKey;
|
|
118
|
+
if (requireSecret) {
|
|
119
|
+
encryptionSecretKey = toBuffer32(
|
|
120
|
+
endpoint.encryptionSecretKey ?? endpoint.encryption_secret_key,
|
|
121
|
+
`${name}.encryptionSecretKey`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
publicKey,
|
|
127
|
+
encryptionPublicKey,
|
|
128
|
+
encryptionSecretKey,
|
|
129
|
+
destination: derivedDestination,
|
|
130
|
+
destinationHex: derivedDestination.toString('hex'),
|
|
131
|
+
scope,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateBody(kind, body) {
|
|
136
|
+
switch (kind) {
|
|
137
|
+
case FEDERATION_PACKET_KINDS.CAPABILITY_GRANT:
|
|
138
|
+
assertExactKeys(body, ['grant'], 'capability grant body');
|
|
139
|
+
if (!isRecord(body.grant)) throw new Error('capability grant body.grant must be an object');
|
|
140
|
+
break;
|
|
141
|
+
case FEDERATION_PACKET_KINDS.QUERY_REQUEST:
|
|
142
|
+
assertExactKeys(body, ['request', 'top_k'], 'query request body');
|
|
143
|
+
if (!isRecord(body.request)) throw new Error('query request body.request must be an object');
|
|
144
|
+
if (!Number.isSafeInteger(body.top_k) || body.top_k < 1 || body.top_k > 100) {
|
|
145
|
+
throw new Error('query request body.top_k must be an integer between 1 and 100');
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
case FEDERATION_PACKET_KINDS.QUERY_RESPONSE:
|
|
149
|
+
assertExactKeys(body, ['error', 'ok', 'request_hash', 'result'], 'query response body');
|
|
150
|
+
if (typeof body.ok !== 'boolean') throw new Error('query response body.ok must be boolean');
|
|
151
|
+
if (!HEX_32_RE.test(String(body.request_hash || ''))) {
|
|
152
|
+
throw new Error('query response body.request_hash must be a 64-hex SHA-256 hash');
|
|
153
|
+
}
|
|
154
|
+
if (body.ok) {
|
|
155
|
+
if (!isRecord(body.result) || body.error !== null) {
|
|
156
|
+
throw new Error('successful query response must contain an object result and null error');
|
|
157
|
+
}
|
|
158
|
+
} else if (body.result !== null || typeof body.error !== 'string' || body.error.length === 0) {
|
|
159
|
+
throw new Error('failed query response must contain null result and a non-empty error');
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case FEDERATION_PACKET_KINDS.CAPABILITY_REVOCATION:
|
|
163
|
+
assertExactKeys(body, ['grant_id', 'revoked_at'], 'capability revocation body');
|
|
164
|
+
if (typeof body.grant_id !== 'string' || body.grant_id.length === 0) {
|
|
165
|
+
throw new Error('capability revocation body.grant_id must be a non-empty string');
|
|
166
|
+
}
|
|
167
|
+
assertIsoTimestamp(body.revoked_at, 'capability revocation body.revoked_at');
|
|
168
|
+
break;
|
|
169
|
+
default:
|
|
170
|
+
throw new Error(`Unsupported federation packet kind: ${kind}`);
|
|
171
|
+
}
|
|
172
|
+
canonicalize(body);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function federationAad(type, destination, senderPublicKey) {
|
|
176
|
+
return Buffer.concat([
|
|
177
|
+
Buffer.from('ghostmesh.federation.aad.v1\0', 'utf8'),
|
|
178
|
+
Buffer.from([type]),
|
|
179
|
+
destination,
|
|
180
|
+
senderPublicKey,
|
|
181
|
+
]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Creates an encrypted and signed GhostMesh federation frame.
|
|
186
|
+
*/
|
|
187
|
+
export function createFederationPacket(options = {}) {
|
|
188
|
+
const kind = options.kind;
|
|
189
|
+
const type = KIND_TO_TYPE[kind];
|
|
190
|
+
if (!type) throw new Error(`Unsupported federation packet kind: ${kind}`);
|
|
191
|
+
validateBody(kind, options.body);
|
|
192
|
+
|
|
193
|
+
const sender = normalizeEndpoint(
|
|
194
|
+
{ ...options.senderIdentity, scope: options.senderScope ?? options.senderIdentity?.scope },
|
|
195
|
+
'senderIdentity'
|
|
196
|
+
);
|
|
197
|
+
const senderSecretKey = toBuffer32(options.senderIdentity?.secretKey, 'senderIdentity.secretKey');
|
|
198
|
+
const recipient = normalizeEndpoint(options.recipient, 'recipient');
|
|
199
|
+
const packetId = options.packetId ?? crypto.randomBytes(16).toString('hex');
|
|
200
|
+
if (!PACKET_ID_RE.test(packetId)) throw new Error('packetId must be 32 lowercase hex characters');
|
|
201
|
+
|
|
202
|
+
const correlationId = options.correlationId ?? null;
|
|
203
|
+
if (correlationId !== null && !PACKET_ID_RE.test(correlationId)) {
|
|
204
|
+
throw new Error('correlationId must be null or 32 lowercase hex characters');
|
|
205
|
+
}
|
|
206
|
+
if (kind === FEDERATION_PACKET_KINDS.QUERY_RESPONSE && correlationId === null) {
|
|
207
|
+
throw new Error('query response packets require correlationId');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const createdAt = options.createdAt ?? new Date().toISOString();
|
|
211
|
+
assertIsoTimestamp(createdAt, 'createdAt');
|
|
212
|
+
const envelope = {
|
|
213
|
+
schema: GHOSTMESH_FEDERATION_PACKET_SCHEMA,
|
|
214
|
+
version: 1,
|
|
215
|
+
kind,
|
|
216
|
+
packet_id: packetId,
|
|
217
|
+
correlation_id: correlationId,
|
|
218
|
+
created_at: createdAt,
|
|
219
|
+
sender: {
|
|
220
|
+
destination: sender.destinationHex,
|
|
221
|
+
encryption_pubkey: sender.encryptionPublicKey.toString('hex'),
|
|
222
|
+
scope: sender.scope,
|
|
223
|
+
},
|
|
224
|
+
recipient_destination: recipient.destinationHex,
|
|
225
|
+
body: options.body,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const aad = federationAad(type, recipient.destination, sender.publicKey);
|
|
229
|
+
const sealed = sealPayload(Buffer.from(canonicalize(envelope), 'utf8'), recipient.encryptionPublicKey, { aad });
|
|
230
|
+
const unsigned = encodePacket({
|
|
231
|
+
type,
|
|
232
|
+
destination: recipient.destination,
|
|
233
|
+
senderPubkey: sender.publicKey,
|
|
234
|
+
ephemeralPubkey: sealed.ephemeralPubkey,
|
|
235
|
+
payload: sealed.ciphertext,
|
|
236
|
+
authTag: sealed.authTag,
|
|
237
|
+
hopTtl: options.hopTtl,
|
|
238
|
+
seqId: options.seqId,
|
|
239
|
+
});
|
|
240
|
+
const packetBuffer = signPacket(unsigned, senderSecretKey);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
packetId,
|
|
244
|
+
kind,
|
|
245
|
+
correlationId,
|
|
246
|
+
destinationHex: recipient.destinationHex,
|
|
247
|
+
packetBuffer,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Authenticates, recipient-binds, decrypts, and validates a federation frame.
|
|
253
|
+
*/
|
|
254
|
+
export function openFederationPacket(packetBuffer, options = {}) {
|
|
255
|
+
const decoded = decodePacket(packetBuffer);
|
|
256
|
+
const kind = TYPE_TO_KIND[decoded.type];
|
|
257
|
+
if (!kind) throw new Error(`Packet type ${decoded.typeName} is not a federation packet`);
|
|
258
|
+
if (!verifyPacketSignature(packetBuffer)) throw new Error('Federation packet signature is invalid');
|
|
259
|
+
|
|
260
|
+
const recipient = normalizeEndpoint(
|
|
261
|
+
{ ...options.recipientIdentity, scope: options.recipientScope ?? options.recipientIdentity?.scope },
|
|
262
|
+
'recipientIdentity',
|
|
263
|
+
{ requireSecret: true }
|
|
264
|
+
);
|
|
265
|
+
if (!decoded.destination.equals(recipient.destination)) {
|
|
266
|
+
throw new Error(`Federation packet recipient mismatch: expected ${recipient.destinationHex}, got ${decoded.destinationHex}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const aad = federationAad(decoded.type, decoded.destination, decoded.senderPubkey);
|
|
270
|
+
let plaintext;
|
|
271
|
+
try {
|
|
272
|
+
plaintext = unsealPayload(
|
|
273
|
+
decoded.payload,
|
|
274
|
+
decoded.authTag,
|
|
275
|
+
decoded.ephemeralPubkey,
|
|
276
|
+
recipient.encryptionSecretKey,
|
|
277
|
+
{ aad }
|
|
278
|
+
);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw new Error(`Federation packet decryption failed: ${error.message}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let envelope;
|
|
284
|
+
try {
|
|
285
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(plaintext);
|
|
286
|
+
envelope = JSON.parse(text);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
throw new Error(`Federation packet payload is not canonical JSON: ${error.message}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
assertExactKeys(envelope, ENVELOPE_KEYS, 'federation envelope');
|
|
292
|
+
if (envelope.schema !== GHOSTMESH_FEDERATION_PACKET_SCHEMA || envelope.version !== 1) {
|
|
293
|
+
throw new Error('Unsupported federation envelope schema or version');
|
|
294
|
+
}
|
|
295
|
+
if (envelope.kind !== kind) {
|
|
296
|
+
throw new Error(`Federation envelope kind ${envelope.kind} does not match frame type ${kind}`);
|
|
297
|
+
}
|
|
298
|
+
if (!PACKET_ID_RE.test(String(envelope.packet_id || ''))) {
|
|
299
|
+
throw new Error('Federation envelope packet_id must be 32 lowercase hex characters');
|
|
300
|
+
}
|
|
301
|
+
if (envelope.correlation_id !== null && !PACKET_ID_RE.test(String(envelope.correlation_id || ''))) {
|
|
302
|
+
throw new Error('Federation envelope correlation_id must be null or 32 lowercase hex characters');
|
|
303
|
+
}
|
|
304
|
+
if (kind === FEDERATION_PACKET_KINDS.QUERY_RESPONSE && envelope.correlation_id === null) {
|
|
305
|
+
throw new Error('Federation query response is missing correlation_id');
|
|
306
|
+
}
|
|
307
|
+
assertIsoTimestamp(envelope.created_at, 'federation envelope created_at');
|
|
308
|
+
if (envelope.recipient_destination !== decoded.destinationHex) {
|
|
309
|
+
throw new Error('Federation envelope recipient destination does not match frame destination');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
assertExactKeys(envelope.sender, ['destination', 'encryption_pubkey', 'scope'], 'federation envelope sender');
|
|
313
|
+
const sender = normalizeEndpoint({
|
|
314
|
+
publicKey: decoded.senderPubkey,
|
|
315
|
+
encryptionPublicKey: envelope.sender.encryption_pubkey,
|
|
316
|
+
destination: envelope.sender.destination,
|
|
317
|
+
scope: envelope.sender.scope,
|
|
318
|
+
}, 'federation envelope sender');
|
|
319
|
+
|
|
320
|
+
if (options.expectedKind && options.expectedKind !== kind) {
|
|
321
|
+
throw new Error(`Expected federation packet kind ${options.expectedKind}, got ${kind}`);
|
|
322
|
+
}
|
|
323
|
+
if (options.expectedCorrelationId !== undefined && envelope.correlation_id !== options.expectedCorrelationId) {
|
|
324
|
+
throw new Error(`Federation response correlation mismatch: expected ${options.expectedCorrelationId}, got ${envelope.correlation_id}`);
|
|
325
|
+
}
|
|
326
|
+
if (options.expectedSenderPublicKey) {
|
|
327
|
+
const expectedSender = toBuffer32(options.expectedSenderPublicKey, 'expectedSenderPublicKey');
|
|
328
|
+
if (!decoded.senderPubkey.equals(expectedSender)) throw new Error('Federation packet sender mismatch');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
validateBody(kind, envelope.body);
|
|
332
|
+
if (canonicalize(envelope) !== plaintext.toString('utf8')) {
|
|
333
|
+
throw new Error('Federation packet payload is not in canonical JSON form');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
packetId: envelope.packet_id,
|
|
338
|
+
correlationId: envelope.correlation_id,
|
|
339
|
+
kind,
|
|
340
|
+
body: envelope.body,
|
|
341
|
+
createdAt: envelope.created_at,
|
|
342
|
+
sender: {
|
|
343
|
+
publicKey: Buffer.from(decoded.senderPubkey),
|
|
344
|
+
encryptionPublicKey: sender.encryptionPublicKey,
|
|
345
|
+
destination: sender.destination,
|
|
346
|
+
destinationHex: sender.destinationHex,
|
|
347
|
+
scope: sender.scope,
|
|
348
|
+
},
|
|
349
|
+
recipientDestinationHex: decoded.destinationHex,
|
|
350
|
+
replayIdentity: getPacketReplayIdentity(packetBuffer),
|
|
351
|
+
decoded,
|
|
352
|
+
};
|
|
353
|
+
}
|