@7h3/protocol 0.5.0 → 0.5.1

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.
Files changed (294) hide show
  1. package/README.md +6 -1317
  2. package/agentAdapter.d.ts +26 -0
  3. package/auditLog.d.ts +54 -0
  4. package/capability.d.ts +67 -0
  5. package/cborCodec.d.ts +34 -0
  6. package/conformanceVectors.d.ts +20 -0
  7. package/encryption.d.ts +85 -0
  8. package/envelopeCbor.d.ts +33 -0
  9. package/frameworkAdapters.d.ts +72 -0
  10. package/gateway.d.ts +53 -0
  11. package/grpcBinding.d.ts +29 -0
  12. package/httpBinding.d.ts +50 -0
  13. package/index.d.ts +39 -0
  14. package/index.js +3662 -0
  15. package/keyInfra.d.ts +64 -0
  16. package/keyRegistry.d.ts +9 -0
  17. package/keyRotation.d.ts +20 -0
  18. package/mcpGateway.d.ts +37 -0
  19. package/mcpTransports.d.ts +62 -0
  20. package/mcpWrapper.d.ts +83 -0
  21. package/otel.d.ts +65 -0
  22. package/package.json +13 -121
  23. package/policyEnforcer.d.ts +50 -0
  24. package/policyTelemetryFeedback.d.ts +11 -0
  25. package/{src/protocol.d.ts → protocol.d.ts} +1 -2
  26. package/protocolAgent.d.ts +58 -0
  27. package/protocolBinary.d.ts +8 -0
  28. package/protocolCapabilities.d.ts +24 -0
  29. package/protocolReplay.d.ts +35 -0
  30. package/protocolTransport.d.ts +73 -0
  31. package/queueBinding.d.ts +43 -0
  32. package/rateLimiter.d.ts +18 -0
  33. package/{src/redisClient.ts → redisClient.d.ts} +25 -75
  34. package/replayStores.d.ts +98 -0
  35. package/revocation.d.ts +71 -0
  36. package/routePolicy.d.ts +30 -0
  37. package/runtimePolicy.d.ts +24 -0
  38. package/runtimePolicyManager.d.ts +15 -0
  39. package/runtimePolicyPresets.d.ts +11 -0
  40. package/signedResponse.d.ts +21 -0
  41. package/stream.d.ts +59 -0
  42. package/telemetry.d.ts +79 -0
  43. package/webhookBinding.d.ts +30 -0
  44. package/wsBinding.d.ts +52 -0
  45. package/.dockerignore +0 -19
  46. package/.github/dependabot.yml +0 -32
  47. package/.github/workflows/ci.yml +0 -31
  48. package/.github/workflows/publish.yml +0 -59
  49. package/.github/workflows/scorecard.yml +0 -37
  50. package/7h3.example.yaml +0 -125
  51. package/CHANGELOG.md +0 -152
  52. package/CONTRIBUTING.md +0 -82
  53. package/Dockerfile +0 -73
  54. package/GOVERNANCE.md +0 -62
  55. package/SECURITY.md +0 -70
  56. package/bench-results/replay-cache-full-1777891033256.json +0 -10
  57. package/bench-results/replay-cache-full-1777896317488.json +0 -10
  58. package/bench-results/replay-cache-full-1777900993184.json +0 -10
  59. package/bench-results/replay-cache-full-1777901019285.json +0 -10
  60. package/bench-results/replay-cache-quick-1777870170126.json +0 -10
  61. package/bench-results/signature-profiles-quick-1775875160079.json +0 -85
  62. package/bench-results/signature-profiles-quick-1775983539716.json +0 -85
  63. package/bench-results/signature-profiles-quick-1776237913190.json +0 -85
  64. package/bench-results/wire-codecs-full-1777891019803.json +0 -93
  65. package/bench-results/wire-codecs-full-1777896260964.json +0 -93
  66. package/bench-results/wire-codecs-full-1777901004247.json +0 -93
  67. package/bench-results/wire-codecs-quick-1775972879056.json +0 -93
  68. package/bench-results/wire-codecs-quick-1775983541111.json +0 -93
  69. package/bench-results/wire-codecs-quick-1776237914299.json +0 -93
  70. package/bench-results/wire-codecs-quick-1777841285236.json +0 -93
  71. package/bench-results/wire-codecs-quick-1777841321772.json +0 -93
  72. package/bench-results/wire-codecs-quick-1777841330408.json +0 -93
  73. package/bench-results/wire-codecs-quick-1777852886082.json +0 -93
  74. package/bench-results/wire-codecs-quick-1777852988773.json +0 -93
  75. package/bench-results/wire-codecs-quick-1777870188095.json +0 -93
  76. package/bench-results/wire-codecs-quick-1777870263918.json +0 -93
  77. package/bench-results/wire-codecs-quick-1777870455034.json +0 -93
  78. package/bench-results/wire-codecs-quick-1778816163081.json +0 -93
  79. package/bench-results/wire-codecs-quick-1778843936130.json +0 -93
  80. package/bin/7h3.ts +0 -406
  81. package/conformance/7h3_v0_1.json +0 -77
  82. package/conformance/7h3_v0_1_binary.json +0 -20
  83. package/conformance/aip_v0_1_binary.json +0 -20
  84. package/docker-compose.yaml +0 -77
  85. package/docs/ADOPTION_PLAN.md +0 -120
  86. package/docs/AGENTS.md +0 -77
  87. package/docs/AIP_RFC_v0.1.md +0 -97
  88. package/docs/AI_DECISION_CARD.md +0 -122
  89. package/docs/AI_RUNTIME_POLICY.json +0 -126
  90. package/docs/AI_RUNTIME_POLICY.yaml +0 -110
  91. package/docs/BACKPRESSURE_TUNING.md +0 -65
  92. package/docs/BENCHMARK_CLAIM_MATRIX.md +0 -42
  93. package/docs/BENCHMARK_REPORT_TEMPLATE.md +0 -169
  94. package/docs/BINARY_CODEC_BENCH.md +0 -23
  95. package/docs/CLEAN_CLONE_RUNBOOK.md +0 -36
  96. package/docs/CLOCK_SKEW_POLICY.md +0 -30
  97. package/docs/DISTRIBUTED_REPLAY.md +0 -142
  98. package/docs/FUZZ_CAMPAIGN.md +0 -121
  99. package/docs/GATEWAY.md +0 -195
  100. package/docs/KEY_MANAGEMENT_POLICY.md +0 -53
  101. package/docs/KEY_REVOCATION.md +0 -69
  102. package/docs/MCP_WRAPPER.md +0 -159
  103. package/docs/MIGRATION_GUIDE.md +0 -40
  104. package/docs/OPERATORS.md +0 -184
  105. package/docs/PERF_REGRESSION_POLICY.md +0 -34
  106. package/docs/PROJECT_EXAMINATION_2026-05-31.md +0 -219
  107. package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +0 -135
  108. package/docs/RELEASE_GATE.md +0 -25
  109. package/docs/RELEASE_NOTES_v0.1.0.md +0 -54
  110. package/docs/SECURITY_REVIEW_2026-06-05.md +0 -165
  111. package/docs/TELEMETRY.md +0 -41
  112. package/docs/THREAT_MODEL.md +0 -89
  113. package/docs/VERSIONING_POLICY.md +0 -30
  114. package/docs/assets/banner-github.png +0 -0
  115. package/docs/assets/banner.png +0 -0
  116. package/docs/assets/banner.svg +0 -123
  117. package/eslint.config.js +0 -15
  118. package/fuzz/ts/harness-decode.ts +0 -136
  119. package/fuzz/ts/harness-verify.ts +0 -121
  120. package/fuzz/ts/run.ts +0 -35
  121. package/mcp-server/README.md +0 -38
  122. package/mcp-server/package-lock.json +0 -1187
  123. package/mcp-server/package.json +0 -35
  124. package/mcp-server/src/index.ts +0 -236
  125. package/mcp-server/tsconfig.json +0 -14
  126. package/scripts/aip-framework-quickstart.ts +0 -110
  127. package/scripts/aip-mcp-gateway.ts +0 -38
  128. package/scripts/aip-mcp-wrap-demo.ts +0 -72
  129. package/scripts/aip-quickstart.ts +0 -60
  130. package/scripts/bench-diff.ts +0 -118
  131. package/scripts/bench-protocol-e2e.ts +0 -937
  132. package/scripts/bench-protocol-openloop.ts +0 -1397
  133. package/scripts/bench-replay-cache.ts +0 -76
  134. package/scripts/bench-signature-profiles.ts +0 -180
  135. package/scripts/bench-wire-codecs.ts +0 -161
  136. package/scripts/build-binary-conformance.ts +0 -36
  137. package/scripts/build-release-dashboard.ts +0 -175
  138. package/scripts/canary-rollout.ts +0 -38
  139. package/scripts/mcpGatewayCli.test.ts +0 -116
  140. package/scripts/prepare-aip-package.ts +0 -88
  141. package/scripts/regen-conformance-sigs.ts +0 -18
  142. package/scripts/release-gate.ts +0 -19
  143. package/scripts/validate-runtime-policy.ts +0 -18
  144. package/sdk/browser/index.test.ts +0 -162
  145. package/sdk/browser/index.ts +0 -257
  146. package/sdk/browser/package.json +0 -13
  147. package/sdk/go/cbor.go +0 -551
  148. package/sdk/go/cbor_test.go +0 -232
  149. package/sdk/go/encryption.go +0 -280
  150. package/sdk/go/encryption_test.go +0 -318
  151. package/sdk/go/go.mod +0 -7
  152. package/sdk/go/go.sum +0 -4
  153. package/sdk/go/http.go +0 -135
  154. package/sdk/go/protocol.go +0 -324
  155. package/sdk/go/protocol_test.go +0 -334
  156. package/sdk/go/replay.go +0 -121
  157. package/sdk/go/replay_test.go +0 -149
  158. package/sdk/go/webhook.go +0 -136
  159. package/sdk/pq/package-lock.json +0 -1358
  160. package/sdk/pq/package.json +0 -42
  161. package/sdk/pq/src/index.test.ts +0 -143
  162. package/sdk/pq/src/index.ts +0 -166
  163. package/sdk/pq/tsconfig.json +0 -14
  164. package/sdk/pq/vitest.config.ts +0 -7
  165. package/sdk/python/README.md +0 -18
  166. package/sdk/python/protocol_7h3/__init__.py +0 -46
  167. package/sdk/python/protocol_7h3/encryption.py +0 -252
  168. package/sdk/python/protocol_7h3/http.py +0 -212
  169. package/sdk/python/protocol_7h3/keys.py +0 -149
  170. package/sdk/python/protocol_7h3/pq.py +0 -244
  171. package/sdk/python/protocol_7h3/protocol.py +0 -525
  172. package/sdk/python/protocol_7h3/queue.py +0 -118
  173. package/sdk/python/protocol_7h3/replay.py +0 -98
  174. package/sdk/python/protocol_7h3/webhook.py +0 -116
  175. package/sdk/python/pyproject.toml +0 -40
  176. package/sdk/python/tests/test_conformance.py +0 -110
  177. package/sdk/python/tests/test_encryption.py +0 -206
  178. package/sdk/python/tests/test_http.py +0 -305
  179. package/sdk/python/tests/test_keys.py +0 -417
  180. package/sdk/python/tests/test_queue.py +0 -120
  181. package/sdk/python/tests/test_webhook.py +0 -345
  182. package/sdk/rust/Cargo.lock +0 -371
  183. package/sdk/rust/Cargo.toml +0 -25
  184. package/sdk/rust/README.md +0 -31
  185. package/sdk/rust/fuzz/Cargo.toml +0 -29
  186. package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +0 -46
  187. package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +0 -11
  188. package/sdk/rust/src/bin/aip_mcp_gateway.rs +0 -59
  189. package/sdk/rust/src/http.rs +0 -145
  190. package/sdk/rust/src/keys.rs +0 -161
  191. package/sdk/rust/src/lib.rs +0 -688
  192. package/sdk/rust/src/queue.rs +0 -79
  193. package/sdk/rust/src/webhook.rs +0 -86
  194. package/sdk/rust/tests/conformance.rs +0 -148
  195. package/sdk/rust/tests/gateway.rs +0 -130
  196. package/sdk/rust/tests/http_webhook_queue.rs +0 -201
  197. package/sdk/rust/tests/keys.rs +0 -189
  198. package/sdk/threshold/index.d.ts +0 -68
  199. package/sdk/threshold/index.d.ts.map +0 -1
  200. package/sdk/threshold/index.js +0 -254
  201. package/sdk/threshold/package-lock.json +0 -1361
  202. package/sdk/threshold/package.json +0 -39
  203. package/sdk/threshold/src/index.d.ts +0 -68
  204. package/sdk/threshold/src/index.d.ts.map +0 -1
  205. package/sdk/threshold/src/index.js +0 -254
  206. package/sdk/threshold/src/index.test.ts +0 -238
  207. package/sdk/threshold/src/index.ts +0 -355
  208. package/sdk/threshold/tsconfig.json +0 -19
  209. package/sdk/threshold/vitest.config.ts +0 -12
  210. package/src/agentAdapter.test.ts +0 -48
  211. package/src/agentAdapter.ts +0 -56
  212. package/src/auditLog.test.ts +0 -145
  213. package/src/auditLog.ts +0 -147
  214. package/src/capability.test.ts +0 -504
  215. package/src/capability.ts +0 -380
  216. package/src/cborCodec.test.ts +0 -263
  217. package/src/cborCodec.ts +0 -339
  218. package/src/conformance.test.ts +0 -136
  219. package/src/conformanceVectors.ts +0 -99
  220. package/src/encryption.test.ts +0 -206
  221. package/src/encryption.ts +0 -245
  222. package/src/envelopeCbor.ts +0 -140
  223. package/src/frameworkAdapters.test.ts +0 -290
  224. package/src/frameworkAdapters.ts +0 -261
  225. package/src/gateway.test.ts +0 -343
  226. package/src/gateway.ts +0 -246
  227. package/src/grpcBinding.test.ts +0 -211
  228. package/src/grpcBinding.ts +0 -103
  229. package/src/httpBinding.test.ts +0 -376
  230. package/src/httpBinding.ts +0 -189
  231. package/src/index.ts +0 -39
  232. package/src/keyInfra.test.ts +0 -278
  233. package/src/keyInfra.ts +0 -228
  234. package/src/keyRegistry.ts +0 -59
  235. package/src/keyRotation.test.ts +0 -78
  236. package/src/keyRotation.ts +0 -72
  237. package/src/mcpGateway.test.ts +0 -129
  238. package/src/mcpGateway.ts +0 -250
  239. package/src/mcpTransports.test.ts +0 -92
  240. package/src/mcpTransports.ts +0 -169
  241. package/src/mcpWrapper.test.ts +0 -179
  242. package/src/mcpWrapper.ts +0 -206
  243. package/src/otel.ts +0 -136
  244. package/src/policyEnforcer.test.ts +0 -99
  245. package/src/policyEnforcer.ts +0 -169
  246. package/src/policyTelemetryFeedback.test.ts +0 -25
  247. package/src/policyTelemetryFeedback.ts +0 -38
  248. package/src/protocol.bench.ts +0 -37
  249. package/src/protocol.d.ts.map +0 -1
  250. package/src/protocol.js +0 -294
  251. package/src/protocol.test.ts +0 -155
  252. package/src/protocol.ts +0 -414
  253. package/src/protocolAgent.test.ts +0 -105
  254. package/src/protocolAgent.ts +0 -169
  255. package/src/protocolBinary.test.ts +0 -165
  256. package/src/protocolBinary.ts +0 -312
  257. package/src/protocolCapabilities.ts +0 -70
  258. package/src/protocolFuzz.advanced.test.ts +0 -235
  259. package/src/protocolFuzz.test.ts +0 -111
  260. package/src/protocolNegative.test.ts +0 -97
  261. package/src/protocolReplay.test.ts +0 -71
  262. package/src/protocolReplay.ts +0 -194
  263. package/src/protocolTransport.test.ts +0 -556
  264. package/src/protocolTransport.ts +0 -483
  265. package/src/queueBinding.test.ts +0 -130
  266. package/src/queueBinding.ts +0 -102
  267. package/src/rateLimiter.test.ts +0 -96
  268. package/src/rateLimiter.ts +0 -46
  269. package/src/redisIntegration.test.ts +0 -134
  270. package/src/replayStores.test.ts +0 -273
  271. package/src/replayStores.ts +0 -215
  272. package/src/revocation.test.ts +0 -98
  273. package/src/revocation.ts +0 -0
  274. package/src/routePolicy.test.ts +0 -87
  275. package/src/routePolicy.ts +0 -72
  276. package/src/runtimePolicy.test.ts +0 -49
  277. package/src/runtimePolicy.ts +0 -81
  278. package/src/runtimePolicyManager.test.ts +0 -29
  279. package/src/runtimePolicyManager.ts +0 -50
  280. package/src/runtimePolicyPresets.ts +0 -43
  281. package/src/signedResponse.test.ts +0 -111
  282. package/src/signedResponse.ts +0 -83
  283. package/src/stream.test.ts +0 -254
  284. package/src/stream.ts +0 -417
  285. package/src/telemetry.test.ts +0 -251
  286. package/src/telemetry.ts +0 -299
  287. package/src/webhookBinding.test.ts +0 -144
  288. package/src/webhookBinding.ts +0 -115
  289. package/src/wsBinding.test.ts +0 -221
  290. package/src/wsBinding.ts +0 -200
  291. package/tsconfig.json +0 -15
  292. package/tsconfig.lib.json +0 -23
  293. package/vite.lib.config.ts +0 -16
  294. package/vitest.config.ts +0 -11
@@ -1,206 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { createEnvelope, generateEd25519KeypairBase64Url, type ProtocolBody } from './protocol.js'
3
- import {
4
- decryptBody,
5
- encryptBody,
6
- generateX25519KeyPair,
7
- openEnvelope,
8
- sealEnvelope,
9
- } from './encryption.js'
10
-
11
- // ---------------------------------------------------------------------------
12
- // Helpers
13
- // ---------------------------------------------------------------------------
14
-
15
- function makeEnvelope(body: ProtocolBody) {
16
- return createEnvelope({
17
- sender: 'agent-alice',
18
- recipient: 'agent-bob',
19
- intent: body.intent,
20
- content: body.content,
21
- capability: body.capability,
22
- correlationId: body.correlationId,
23
- ttlMs: 60_000,
24
- })
25
- }
26
-
27
- // ---------------------------------------------------------------------------
28
- // Tests
29
- // ---------------------------------------------------------------------------
30
-
31
- describe('generateX25519KeyPair', () => {
32
- it('returns 32-byte base64url keys (~43 chars, no padding)', () => {
33
- const kp = generateX25519KeyPair()
34
- // base64url of 32 bytes = ceil(32 * 4/3) = 43 chars (no padding)
35
- expect(kp.publicKey).toMatch(/^[A-Za-z0-9_-]{43}$/)
36
- expect(kp.privateKey).toMatch(/^[A-Za-z0-9_-]{43}$/)
37
- // Decoded must be exactly 32 bytes
38
- expect(Buffer.from(kp.publicKey, 'base64url').length).toBe(32)
39
- expect(Buffer.from(kp.privateKey, 'base64url').length).toBe(32)
40
- })
41
- })
42
-
43
- describe('sealEnvelope + openEnvelope', () => {
44
- it('round-trip recovers original body exactly', async () => {
45
- const recipientKp = generateX25519KeyPair()
46
- const senderEd = await generateEd25519KeypairBase64Url()
47
-
48
- const originalBody: ProtocolBody = {
49
- intent: 'TASK',
50
- content: 'Hello encrypted world!',
51
- capability: 'some-cap',
52
- correlationId: 'corr-123',
53
- }
54
- const envelope = makeEnvelope(originalBody)
55
-
56
- const sealed = await sealEnvelope(envelope, {
57
- recipientX25519PublicKey: recipientKp.publicKey,
58
- senderEd25519PrivateKey: senderEd.privateKey,
59
- })
60
-
61
- const { body } = await openEnvelope(sealed, {
62
- recipientX25519PrivateKey: recipientKp.privateKey,
63
- senderEd25519PublicKey: senderEd.publicKey,
64
- })
65
-
66
- expect(body.intent).toBe(originalBody.intent)
67
- expect(body.content).toBe(originalBody.content)
68
- expect(body.capability).toBe(originalBody.capability)
69
- expect(body.correlationId).toBe(originalBody.correlationId)
70
- })
71
-
72
- it('fails with wrong recipient private key (AEAD tag mismatch)', async () => {
73
- const recipientKp = generateX25519KeyPair()
74
- const wrongKp = generateX25519KeyPair()
75
- const senderEd = await generateEd25519KeypairBase64Url()
76
-
77
- // Sign with correct recipient public key but try to open with wrong private key
78
- // To bypass the signature check we need to seal with wrongKp pubkey too,
79
- // but the spec says "wrong recipient key" meaning sealed to correct pub key but opened with wrong priv.
80
- const envelope = makeEnvelope({ intent: 'PING', content: 'secret' })
81
- const sealed = await sealEnvelope(envelope, {
82
- recipientX25519PublicKey: recipientKp.publicKey,
83
- senderEd25519PrivateKey: senderEd.privateKey,
84
- })
85
-
86
- // We must tamper with the encrypted content to force the wrong key path,
87
- // but the signature would fail. Instead, bypass openEnvelope and call decryptBody directly
88
- // with the wrong key — which is what the spec tests.
89
- expect(() => decryptBody(sealed.body.content, wrongKp.privateKey)).toThrow()
90
- })
91
-
92
- it('fails if envelope signature is tampered', async () => {
93
- const recipientKp = generateX25519KeyPair()
94
- const senderEd = await generateEd25519KeypairBase64Url()
95
-
96
- const envelope = makeEnvelope({ intent: 'PING', content: 'secret' })
97
- const sealed = await sealEnvelope(envelope, {
98
- recipientX25519PublicKey: recipientKp.publicKey,
99
- senderEd25519PrivateKey: senderEd.privateKey,
100
- })
101
-
102
- // Tamper with the signature value
103
- const tampered = {
104
- ...sealed,
105
- signature: {
106
- ...sealed.signature!,
107
- value: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
108
- },
109
- }
110
-
111
- await expect(
112
- openEnvelope(tampered, {
113
- recipientX25519PrivateKey: recipientKp.privateKey,
114
- senderEd25519PublicKey: senderEd.publicKey,
115
- }),
116
- ).rejects.toThrow('signature verification failed')
117
- })
118
-
119
- it('fails if ciphertext is tampered (AEAD auth tag fails)', async () => {
120
- const recipientKp = generateX25519KeyPair()
121
- const senderEd = await generateEd25519KeypairBase64Url()
122
-
123
- const envelope = makeEnvelope({ intent: 'PING', content: 'secret' })
124
- const sealed = await sealEnvelope(envelope, {
125
- recipientX25519PublicKey: recipientKp.publicKey,
126
- senderEd25519PrivateKey: senderEd.privateKey,
127
- })
128
-
129
- // Decode the encrypted payload, flip a bit in ciphertext, re-encode
130
- const payloadJson = Buffer.from(sealed.body.content, 'base64url').toString('utf8')
131
- const payload = JSON.parse(payloadJson) as { ephemeralPublic: string; nonce: string; ciphertext: string; tag: string }
132
- const ctBuf = Buffer.from(payload.ciphertext, 'base64url')
133
- ctBuf[0] ^= 0xff // flip bits in first byte
134
- const tamperedPayload = { ...payload, ciphertext: ctBuf.toString('base64url') }
135
- const tamperedContent = Buffer.from(JSON.stringify(tamperedPayload), 'utf8').toString('base64url')
136
-
137
- // We need to re-sign with the same key for the tamper to get past sig verification
138
- // Instead: call decryptBody directly with tampered content
139
- expect(() => decryptBody(tamperedContent, recipientKp.privateKey)).toThrow()
140
- })
141
-
142
- it('two sealEnvelope calls on same body produce different ciphertexts (ephemeral randomness)', async () => {
143
- const recipientKp = generateX25519KeyPair()
144
- const senderEd = await generateEd25519KeypairBase64Url()
145
-
146
- const envelope1 = makeEnvelope({ intent: 'PING', content: 'same content' })
147
- const envelope2 = makeEnvelope({ intent: 'PING', content: 'same content' })
148
-
149
- const sealed1 = await sealEnvelope(envelope1, {
150
- recipientX25519PublicKey: recipientKp.publicKey,
151
- senderEd25519PrivateKey: senderEd.privateKey,
152
- })
153
- const sealed2 = await sealEnvelope(envelope2, {
154
- recipientX25519PublicKey: recipientKp.publicKey,
155
- senderEd25519PrivateKey: senderEd.privateKey,
156
- })
157
-
158
- // Different ephemeral keys + different nonces → different ciphertexts
159
- expect(sealed1.body.content).not.toBe(sealed2.body.content)
160
- })
161
-
162
- it('encrypted content is opaque (does not contain original body.content as plaintext)', async () => {
163
- const recipientKp = generateX25519KeyPair()
164
- const senderEd = await generateEd25519KeypairBase64Url()
165
-
166
- const originalContent = 'super-secret-data-12345'
167
- const envelope = makeEnvelope({ intent: 'TASK', content: originalContent })
168
-
169
- const sealed = await sealEnvelope(envelope, {
170
- recipientX25519PublicKey: recipientKp.publicKey,
171
- senderEd25519PrivateKey: senderEd.privateKey,
172
- })
173
-
174
- // The encrypted content blob should not contain the original plaintext string
175
- const encryptedContentDecoded = Buffer.from(sealed.body.content, 'base64url').toString('utf8')
176
- expect(encryptedContentDecoded).not.toContain(originalContent)
177
- // Also raw base64url should not contain it
178
- expect(sealed.body.content).not.toContain(originalContent)
179
- })
180
-
181
- it('encrypted envelope body has correct structure', async () => {
182
- const recipientKp = generateX25519KeyPair()
183
- const senderEd = await generateEd25519KeypairBase64Url()
184
-
185
- const envelope = makeEnvelope({ intent: 'PING', content: 'test' })
186
- const sealed = await sealEnvelope(envelope, {
187
- recipientX25519PublicKey: recipientKp.publicKey,
188
- senderEd25519PrivateKey: senderEd.privateKey,
189
- })
190
-
191
- expect(sealed.body.intent).toBe('ENCRYPTED')
192
- expect(sealed.body.capability).toBe('x25519-chacha20poly1305')
193
- expect(sealed.signature).toBeDefined()
194
- expect(sealed.signature?.alg).toBe('ED25519')
195
- })
196
- })
197
-
198
- describe('encryptBody + decryptBody', () => {
199
- it('round-trips a body with all optional fields', () => {
200
- const kp = generateX25519KeyPair()
201
- const body: ProtocolBody = { intent: 'RESULT', content: 'data', capability: 'cap', correlationId: 'cid-42' }
202
- const { encryptedContent } = encryptBody(body, kp.publicKey)
203
- const decrypted = decryptBody(encryptedContent, kp.privateKey)
204
- expect(decrypted).toEqual(body)
205
- })
206
- })
package/src/encryption.ts DELETED
@@ -1,245 +0,0 @@
1
- /**
2
- * E2E Encryption for 7h3 Protocol
3
- *
4
- * Uses X25519 Diffie-Hellman key exchange + ChaCha20-Poly1305 AEAD.
5
- * All operations via Node.js built-in `node:crypto` — zero new dependencies.
6
- *
7
- * Architecture:
8
- * EncryptedEnvelope = SignedEnvelope where body.content is a base64url-encoded
9
- * EncryptedPayload, body.intent = 'ENCRYPTED', body.capability = 'x25519-chacha20poly1305'
10
- */
11
-
12
- import { createCipheriv, createDecipheriv, createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes } from 'node:crypto'
13
- import type { KeyObject } from 'node:crypto'
14
- import {
15
- signEnvelopeEd25519,
16
- verifyEnvelopeEd25519,
17
- type ProtocolBody,
18
- type ProtocolEnvelope,
19
- } from './protocol.js'
20
-
21
- // ---------------------------------------------------------------------------
22
- // Types
23
- // ---------------------------------------------------------------------------
24
-
25
- export interface X25519KeyPair {
26
- /** Raw 32-byte X25519 public key, base64url-encoded (no padding) */
27
- publicKey: string
28
- /** Raw 32-byte X25519 private key, base64url-encoded (no padding) */
29
- privateKey: string
30
- }
31
-
32
- export interface EncryptedPayload {
33
- /** Ephemeral X25519 public key (base64url, raw 32 bytes) */
34
- ephemeralPublic: string
35
- /** ChaCha20 nonce / HKDF salt (base64url, 12 bytes) */
36
- nonce: string
37
- /** Ciphertext without auth tag (base64url) */
38
- ciphertext: string
39
- /** 16-byte Poly1305 auth tag (base64url) */
40
- tag: string
41
- }
42
-
43
- // ---------------------------------------------------------------------------
44
- // Key helpers
45
- // ---------------------------------------------------------------------------
46
-
47
- function toBase64Url(buf: Buffer | Uint8Array): string {
48
- return Buffer.from(buf).toString('base64url')
49
- }
50
-
51
- function fromBase64Url(s: string): Buffer {
52
- return Buffer.from(s, 'base64url')
53
- }
54
-
55
- /**
56
- * X25519 PKCS8 DER header: 30 2e 02 01 00 30 05 06 03 2b 65 6e 04 22 04 20
57
- * (RFC 5958 / RFC 8410 encoding for X25519, OID 1.3.101.110 = 2b 65 6e)
58
- */
59
- const X25519_PKCS8_HEADER = Buffer.from('302e020100300506032b656e04220420', 'hex')
60
-
61
- /**
62
- * X25519 SPKI DER header: 30 2a 30 05 06 03 2b 65 6e 03 21 00
63
- * (RFC 5480 SubjectPublicKeyInfo for X25519)
64
- */
65
- const X25519_SPKI_HEADER = Buffer.from('302a300506032b656e032100', 'hex')
66
-
67
- function importX25519Private(privRaw32Base64Url: string): KeyObject {
68
- const rawBytes = fromBase64Url(privRaw32Base64Url)
69
- // Build PKCS8 DER: fixed 16-byte header + 32-byte raw private key
70
- const der = Buffer.concat([X25519_PKCS8_HEADER, rawBytes])
71
- return createPrivateKey({ key: der, format: 'der', type: 'pkcs8' })
72
- }
73
-
74
- function importX25519Public(pubRaw32Base64Url: string): KeyObject {
75
- const rawBytes = fromBase64Url(pubRaw32Base64Url)
76
- // Build SPKI DER: fixed 12-byte header + 32-byte raw public key
77
- const der = Buffer.concat([X25519_SPKI_HEADER, rawBytes])
78
- return createPublicKey({ key: der, format: 'der', type: 'spki' })
79
- }
80
-
81
- // ---------------------------------------------------------------------------
82
- // Public API
83
- // ---------------------------------------------------------------------------
84
-
85
- /**
86
- * Generate a fresh X25519 keypair.
87
- * Both keys are raw 32-byte values encoded as base64url (no padding).
88
- */
89
- export function generateX25519KeyPair(): X25519KeyPair {
90
- const { privateKey, publicKey } = generateKeyPairSync('x25519')
91
- const privJwk = privateKey.export({ format: 'jwk' }) as { d: string }
92
- const pubJwk = publicKey.export({ format: 'jwk' }) as { x: string }
93
- return {
94
- publicKey: pubJwk.x,
95
- privateKey: privJwk.d,
96
- }
97
- }
98
-
99
- /**
100
- * Perform X25519 DH + HKDF-SHA256 to derive a 32-byte ChaCha20-Poly1305 key.
101
- *
102
- * @param privateKeyBase64Url - raw 32-byte X25519 private key (base64url)
103
- * @param peerPublicKeyBase64Url - raw 32-byte X25519 public key (base64url)
104
- * @param nonce - raw 12-byte nonce (base64url); used as HKDF salt
105
- * @returns 32-byte Buffer ready for use with createCipheriv/createDecipheriv
106
- */
107
- export function deriveEncryptionKey(
108
- privateKeyBase64Url: string,
109
- peerPublicKeyBase64Url: string,
110
- nonce: string,
111
- ): Buffer {
112
- const privKey = importX25519Private(privateKeyBase64Url)
113
- const pubKey = importX25519Public(peerPublicKeyBase64Url)
114
-
115
- const sharedSecret = diffieHellman({ privateKey: privKey, publicKey: pubKey })
116
- const salt = fromBase64Url(nonce)
117
- const info = Buffer.from('7h3-enc/1', 'utf8')
118
-
119
- const derived = hkdfSync('sha256', sharedSecret, salt, info, 32)
120
- return Buffer.from(derived)
121
- }
122
-
123
- /**
124
- * Encrypt a ProtocolBody with the recipient's X25519 public key.
125
- *
126
- * @returns encryptedContent (base64url-encoded EncryptedPayload JSON) and ephemeralPublic
127
- */
128
- export function encryptBody(
129
- body: ProtocolBody,
130
- recipientX25519PublicKey: string,
131
- ): { encryptedContent: string; ephemeralPublic: string } {
132
- // 1. Generate ephemeral X25519 keypair for forward secrecy
133
- const ephemeral = generateX25519KeyPair()
134
-
135
- // 2. Generate 12-byte ChaCha nonce (also used as HKDF salt)
136
- const nonce12 = randomBytes(12)
137
- const nonceBase64Url = toBase64Url(nonce12)
138
-
139
- // 3. Derive encryption key
140
- const key = deriveEncryptionKey(ephemeral.privateKey, recipientX25519PublicKey, nonceBase64Url)
141
-
142
- // 4. Encrypt body as JSON
143
- const plaintext = Buffer.from(JSON.stringify(body), 'utf8')
144
- const cipher = createCipheriv('chacha20-poly1305', key, nonce12, { authTagLength: 16 })
145
- const ciphertextBuf = Buffer.concat([cipher.update(plaintext), cipher.final()])
146
- const tag = cipher.getAuthTag()
147
-
148
- // 5. Build EncryptedPayload
149
- const payload: EncryptedPayload = {
150
- ephemeralPublic: ephemeral.publicKey,
151
- nonce: nonceBase64Url,
152
- ciphertext: toBase64Url(ciphertextBuf),
153
- tag: toBase64Url(tag),
154
- }
155
-
156
- return {
157
- encryptedContent: toBase64Url(Buffer.from(JSON.stringify(payload), 'utf8')),
158
- ephemeralPublic: ephemeral.publicKey,
159
- }
160
- }
161
-
162
- /**
163
- * Decrypt an encrypted ProtocolBody.
164
- *
165
- * @param encryptedContent - base64url-encoded EncryptedPayload
166
- * @param recipientX25519PrivateKey - raw 32-byte X25519 private key (base64url)
167
- * @returns decrypted ProtocolBody
168
- * @throws if AEAD tag verification fails
169
- */
170
- export function decryptBody(encryptedContent: string, recipientX25519PrivateKey: string): ProtocolBody {
171
- const payloadJson = fromBase64Url(encryptedContent).toString('utf8')
172
- const payload = JSON.parse(payloadJson) as EncryptedPayload
173
-
174
- const { ephemeralPublic, nonce, ciphertext, tag } = payload
175
-
176
- // Derive the same key using recipient's private key + ephemeral public key
177
- const key = deriveEncryptionKey(recipientX25519PrivateKey, ephemeralPublic, nonce)
178
-
179
- const nonce12 = fromBase64Url(nonce)
180
- const ciphertextBuf = fromBase64Url(ciphertext)
181
- const tagBuf = fromBase64Url(tag)
182
-
183
- const decipher = createDecipheriv('chacha20-poly1305', key, nonce12, { authTagLength: 16 })
184
- decipher.setAuthTag(tagBuf)
185
-
186
- const plaintext = Buffer.concat([decipher.update(ciphertextBuf), decipher.final()])
187
- return JSON.parse(plaintext.toString('utf8')) as ProtocolBody
188
- }
189
-
190
- /**
191
- * Encrypt and sign an envelope (full pipeline).
192
- *
193
- * The original body is encrypted; the envelope body is replaced with:
194
- * { intent: 'ENCRYPTED', content: <encrypted-payload>, capability: 'x25519-chacha20poly1305' }
195
- * The modified envelope is then signed with Ed25519.
196
- */
197
- export async function sealEnvelope(
198
- envelope: Omit<ProtocolEnvelope, 'signature'>,
199
- opts: {
200
- recipientX25519PublicKey: string
201
- senderEd25519PrivateKey: string
202
- },
203
- ): Promise<ProtocolEnvelope> {
204
- const { encryptedContent } = encryptBody(envelope.body, opts.recipientX25519PublicKey)
205
-
206
- const encryptedEnvelope: Omit<ProtocolEnvelope, 'signature'> = {
207
- header: envelope.header,
208
- body: {
209
- intent: 'ENCRYPTED' as const,
210
- content: encryptedContent,
211
- capability: 'x25519-chacha20poly1305',
212
- correlationId: envelope.body.correlationId,
213
- },
214
- }
215
-
216
- // Sign the envelope containing the encrypted body
217
- return signEnvelopeEd25519(encryptedEnvelope, opts.senderEd25519PrivateKey)
218
- }
219
-
220
- /**
221
- * Verify signature and decrypt an envelope (full pipeline).
222
- *
223
- * Signature is verified FIRST; decryption only proceeds if valid.
224
- *
225
- * @returns The signed envelope (with encrypted body) and the decrypted original body
226
- * @throws if Ed25519 signature invalid or AEAD tag fails
227
- */
228
- export async function openEnvelope(
229
- envelope: ProtocolEnvelope,
230
- opts: {
231
- recipientX25519PrivateKey: string
232
- senderEd25519PublicKey: string
233
- },
234
- ): Promise<{ envelope: ProtocolEnvelope; body: ProtocolBody }> {
235
- // 1. Verify Ed25519 signature FIRST — reject before decrypting
236
- const valid = await verifyEnvelopeEd25519(envelope, opts.senderEd25519PublicKey)
237
- if (!valid) {
238
- throw new Error('7h3/encryption: Ed25519 signature verification failed')
239
- }
240
-
241
- // 2. Decrypt body
242
- const body = decryptBody(envelope.body.content, opts.recipientX25519PrivateKey)
243
-
244
- return { envelope, body }
245
- }
@@ -1,140 +0,0 @@
1
- /**
2
- * Higher-level CBOR codec for ProtocolEnvelope.
3
- * Uses numeric field keys for maximum compactness.
4
- *
5
- * Envelope map structure:
6
- * 1 → header map
7
- * 2 → body map
8
- * 3 → signature map (omitted if no signature)
9
- *
10
- * Header map:
11
- * 1 → version (string)
12
- * 2 → messageId (string)
13
- * 3 → timestampMs (int)
14
- * 4 → ttlMs (int)
15
- * 5 → sender (string)
16
- * 6 → recipient (string, omit if absent)
17
- * 7 → nonce (string)
18
- *
19
- * Body map:
20
- * 1 → intent (string)
21
- * 2 → content (string)
22
- * 3 → capability (string, omit if absent)
23
- * 4 → correlationId (string, omit if absent)
24
- *
25
- * Signature map:
26
- * 1 → alg (string)
27
- * 2 → keyId (string)
28
- * 3 → value (string — base64url signature)
29
- */
30
-
31
- import type { ProtocolEnvelope, ProtocolSignature } from './protocol'
32
- import { encodeCbor, decodeCbor } from './cborCodec'
33
-
34
- export const CBOR_CONTENT_TYPE = 'application/7h3-cbor'
35
-
36
- export function encodeEnvelopeCbor(env: ProtocolEnvelope): Uint8Array {
37
- // Build header map
38
- const headerMap = new Map<number, unknown>()
39
- headerMap.set(1, env.header.version)
40
- headerMap.set(2, env.header.messageId)
41
- headerMap.set(3, env.header.timestampMs)
42
- headerMap.set(4, env.header.ttlMs)
43
- headerMap.set(5, env.header.sender)
44
- if (env.header.recipient !== undefined) {
45
- headerMap.set(6, env.header.recipient)
46
- }
47
- headerMap.set(7, env.header.nonce)
48
-
49
- // Build body map
50
- const bodyMap = new Map<number, unknown>()
51
- bodyMap.set(1, env.body.intent)
52
- bodyMap.set(2, env.body.content)
53
- if (env.body.capability !== undefined) {
54
- bodyMap.set(3, env.body.capability)
55
- }
56
- if (env.body.correlationId !== undefined) {
57
- bodyMap.set(4, env.body.correlationId)
58
- }
59
-
60
- // Build top-level map
61
- const topMap = new Map<number, unknown>()
62
- topMap.set(1, headerMap)
63
- topMap.set(2, bodyMap)
64
-
65
- const sig = env.signature
66
- if (sig) {
67
- const sigMap = new Map<number, unknown>()
68
- sigMap.set(1, sig.alg)
69
- sigMap.set(2, sig.keyId)
70
- sigMap.set(3, sig.value)
71
- topMap.set(3, sigMap)
72
- }
73
-
74
- return encodeCbor(topMap)
75
- }
76
-
77
- export function decodeEnvelopeCbor(data: Uint8Array): ProtocolEnvelope {
78
- const top = decodeCbor(data)
79
-
80
- if (!top || typeof top !== 'object' || Array.isArray(top)) {
81
- throw new Error('decodeEnvelopeCbor: expected a map at top level')
82
- }
83
-
84
- const topRecord = top as Record<string, unknown>
85
-
86
- // Header
87
- const headerRaw = topRecord['1']
88
- if (!headerRaw || typeof headerRaw !== 'object' || Array.isArray(headerRaw)) {
89
- throw new Error('decodeEnvelopeCbor: missing header map (key 1)')
90
- }
91
- const hr = headerRaw as Record<string, unknown>
92
-
93
- // Body
94
- const bodyRaw = topRecord['2']
95
- if (!bodyRaw || typeof bodyRaw !== 'object' || Array.isArray(bodyRaw)) {
96
- throw new Error('decodeEnvelopeCbor: missing body map (key 2)')
97
- }
98
- const br = bodyRaw as Record<string, unknown>
99
-
100
- const env: ProtocolEnvelope = {
101
- header: {
102
- version: hr['1'] as ProtocolEnvelope['header']['version'],
103
- messageId: hr['2'] as string,
104
- timestampMs: hr['3'] as number,
105
- ttlMs: hr['4'] as number,
106
- sender: hr['5'] as string,
107
- nonce: hr['7'] as string,
108
- },
109
- body: {
110
- intent: br['1'] as ProtocolEnvelope['body']['intent'],
111
- content: br['2'] as string,
112
- },
113
- }
114
-
115
- if (hr['6'] !== undefined) {
116
- env.header.recipient = hr['6'] as string
117
- }
118
- if (br['3'] !== undefined) {
119
- env.body.capability = br['3'] as string
120
- }
121
- if (br['4'] !== undefined) {
122
- env.body.correlationId = br['4'] as string
123
- }
124
-
125
- // Signature
126
- const sigRaw = topRecord['3']
127
- if (sigRaw !== undefined && sigRaw !== null) {
128
- if (typeof sigRaw !== 'object' || Array.isArray(sigRaw)) {
129
- throw new Error('decodeEnvelopeCbor: expected signature map at key 3')
130
- }
131
- const sr = sigRaw as Record<string, unknown>
132
- env.signature = {
133
- alg: sr['1'] as ProtocolSignature['alg'],
134
- keyId: sr['2'] as string,
135
- value: sr['3'] as string,
136
- }
137
- }
138
-
139
- return env
140
- }