walletpair-sdk 1.0.2 → 1.0.5
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 +13 -0
- package/dist/ble/framing.d.ts.map +1 -1
- package/dist/ble/framing.js +2 -2
- package/dist/ble/framing.js.map +1 -1
- package/dist/ble/index.d.ts +2 -2
- package/dist/ble/index.d.ts.map +1 -1
- package/dist/ble/index.js +2 -2
- package/dist/ble/index.js.map +1 -1
- package/dist/ble/web-ble-transport.d.ts +1 -1
- package/dist/ble/web-ble-transport.d.ts.map +1 -1
- package/dist/ble/web-ble-transport.js +23 -12
- package/dist/ble/web-ble-transport.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +29 -12
- package/dist/crypto.js.map +1 -1
- package/dist/dapp-session.d.ts.map +1 -1
- package/dist/dapp-session.js +15 -5
- package/dist/dapp-session.js.map +1 -1
- package/dist/emitter.d.ts +1 -3
- package/dist/emitter.d.ts.map +1 -1
- package/dist/emitter.js +4 -2
- package/dist/emitter.js.map +1 -1
- package/dist/evm/eip1193.d.ts +2 -2
- package/dist/evm/eip1193.d.ts.map +1 -1
- package/dist/evm/eip1193.js +32 -18
- package/dist/evm/eip1193.js.map +1 -1
- package/dist/evm/index.d.ts +2 -2
- package/dist/evm/index.d.ts.map +1 -1
- package/dist/evm/index.js.map +1 -1
- package/dist/wallet-session.d.ts.map +1 -1
- package/dist/wallet-session.js +4 -3
- package/dist/wallet-session.js.map +1 -1
- package/dist/ws-transport.d.ts +3 -2
- package/dist/ws-transport.d.ts.map +1 -1
- package/dist/ws-transport.js +13 -4
- package/dist/ws-transport.js.map +1 -1
- package/package.json +20 -1
- package/src/__tests__/adversarial/crypto-attacks.test.ts +240 -233
- package/src/__tests__/adversarial/malicious-dapp.test.ts +228 -194
- package/src/__tests__/adversarial/malicious-relay.test.ts +292 -220
- package/src/__tests__/adversarial/malicious-wallet.test.ts +246 -180
- package/src/__tests__/spec-compliance/canonical-json.test.ts +105 -105
- package/src/__tests__/spec-compliance/crypto-vectors.test.ts +149 -154
- package/src/__tests__/spec-compliance/message-format.test.ts +180 -151
- package/src/__tests__/spec-compliance/sequence-numbers.test.ts +142 -149
- package/src/__tests__/spec-compliance/state-machine.test.ts +203 -180
- package/src/ble/framing.test.ts +122 -114
- package/src/ble/framing.ts +48 -51
- package/src/ble/index.ts +7 -7
- package/src/ble/web-ble-transport.test.ts +93 -84
- package/src/ble/web-ble-transport.ts +70 -57
- package/src/ble/web-bluetooth.d.ts +19 -19
- package/src/canonical-json.test.ts +301 -285
- package/src/crypto-directional.test.ts +155 -129
- package/src/crypto-hardening.test.ts +292 -283
- package/src/crypto.test.ts +364 -346
- package/src/crypto.ts +185 -175
- package/src/dapp-session.test.ts +522 -385
- package/src/dapp-session.ts +17 -11
- package/src/emitter.test.ts +122 -122
- package/src/emitter.ts +20 -18
- package/src/evm/eip1193.test.ts +283 -205
- package/src/evm/eip1193.ts +162 -138
- package/src/evm/index.ts +5 -5
- package/src/evm/wagmi.test.ts +1 -1
- package/src/integration.test.ts +329 -201
- package/src/security.test.ts +331 -238
- package/src/sequence-validation.test.ts +6 -9
- package/src/test-helpers.ts +102 -78
- package/src/types.test.ts +45 -50
- package/src/wallet-session.test.ts +611 -383
- package/src/wallet-session.ts +7 -9
- package/src/ws-transport.test.ts +141 -139
- package/src/ws-transport.ts +52 -41
|
@@ -10,27 +10,26 @@
|
|
|
10
10
|
* - Protocol spec test vector SHA-256 verification
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { describe,
|
|
13
|
+
import { describe, expect, it } from 'vitest'
|
|
14
|
+
import type { AadHeader, SessionCryptoContext } from './crypto.js'
|
|
14
15
|
import {
|
|
16
|
+
b64urlDecode,
|
|
17
|
+
b64urlEncode,
|
|
18
|
+
bytesToHex,
|
|
15
19
|
canonicalJson,
|
|
16
|
-
sealPayload,
|
|
17
|
-
unsealPayload,
|
|
18
|
-
sealJoin,
|
|
19
|
-
unsealJoin,
|
|
20
|
-
deriveSessionKey,
|
|
21
|
-
deriveJoinEncryptionKey,
|
|
22
|
-
deriveDirectionalSessionKeys,
|
|
23
20
|
computeHandshakeTranscriptHash,
|
|
24
21
|
computeSharedSecret,
|
|
25
|
-
|
|
22
|
+
deriveDirectionalSessionKeys,
|
|
23
|
+
deriveJoinEncryptionKey,
|
|
24
|
+
deriveSessionKey,
|
|
26
25
|
generateChannelId,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
hexToBytes,
|
|
26
|
+
generateX25519KeyPair,
|
|
27
|
+
sealJoin,
|
|
28
|
+
sealPayload,
|
|
31
29
|
sha256Hex,
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
unsealJoin,
|
|
31
|
+
unsealPayload,
|
|
32
|
+
} from './crypto.js'
|
|
34
33
|
|
|
35
34
|
// ═══════════════════════════════════════════════════════════════════════
|
|
36
35
|
// canonicalJson — comprehensive edge cases
|
|
@@ -39,105 +38,105 @@ import type { AadHeader, SessionCryptoContext } from './crypto.js';
|
|
|
39
38
|
describe('canonicalJson — edge cases', () => {
|
|
40
39
|
// --- Object key sorting ---
|
|
41
40
|
it('sorts object keys lexicographically', () => {
|
|
42
|
-
expect(canonicalJson({ z: 1, a: 2, m: 3 })).toBe('{"a":2,"m":3,"z":1}')
|
|
43
|
-
})
|
|
41
|
+
expect(canonicalJson({ z: 1, a: 2, m: 3 })).toBe('{"a":2,"m":3,"z":1}')
|
|
42
|
+
})
|
|
44
43
|
|
|
45
44
|
it('sorts nested object keys recursively', () => {
|
|
46
|
-
expect(canonicalJson({ b: { z: 1, a: 2 }, a: 1 })).toBe('{"a":1,"b":{"a":2,"z":1}}')
|
|
47
|
-
})
|
|
45
|
+
expect(canonicalJson({ b: { z: 1, a: 2 }, a: 1 })).toBe('{"a":1,"b":{"a":2,"z":1}}')
|
|
46
|
+
})
|
|
48
47
|
|
|
49
48
|
it('sorts deeply nested objects (3+ levels)', () => {
|
|
50
|
-
const input = { c: { b: { z: 1, a: 2 }, a: 3 }, a: 0 }
|
|
51
|
-
expect(canonicalJson(input)).toBe('{"a":0,"c":{"a":3,"b":{"a":2,"z":1}}}')
|
|
52
|
-
})
|
|
49
|
+
const input = { c: { b: { z: 1, a: 2 }, a: 3 }, a: 0 }
|
|
50
|
+
expect(canonicalJson(input)).toBe('{"a":0,"c":{"a":3,"b":{"a":2,"z":1}}}')
|
|
51
|
+
})
|
|
53
52
|
|
|
54
53
|
it('does not sort array elements (preserves order)', () => {
|
|
55
|
-
expect(canonicalJson([3, 1, 2])).toBe('[3,1,2]')
|
|
56
|
-
expect(canonicalJson(['z', 'a', 'm'])).toBe('["z","a","m"]')
|
|
57
|
-
})
|
|
54
|
+
expect(canonicalJson([3, 1, 2])).toBe('[3,1,2]')
|
|
55
|
+
expect(canonicalJson(['z', 'a', 'm'])).toBe('["z","a","m"]')
|
|
56
|
+
})
|
|
58
57
|
|
|
59
58
|
it('sorts keys in objects inside arrays', () => {
|
|
60
|
-
expect(canonicalJson([{ b: 1, a: 2 }])).toBe('[{"a":2,"b":1}]')
|
|
61
|
-
})
|
|
59
|
+
expect(canonicalJson([{ b: 1, a: 2 }])).toBe('[{"a":2,"b":1}]')
|
|
60
|
+
})
|
|
62
61
|
|
|
63
62
|
// --- Primitives ---
|
|
64
63
|
it('handles null', () => {
|
|
65
|
-
expect(canonicalJson(null)).toBe('null')
|
|
66
|
-
})
|
|
64
|
+
expect(canonicalJson(null)).toBe('null')
|
|
65
|
+
})
|
|
67
66
|
|
|
68
67
|
it('handles undefined as null', () => {
|
|
69
|
-
expect(canonicalJson(undefined)).toBe('null')
|
|
70
|
-
})
|
|
68
|
+
expect(canonicalJson(undefined)).toBe('null')
|
|
69
|
+
})
|
|
71
70
|
|
|
72
71
|
it('handles booleans', () => {
|
|
73
|
-
expect(canonicalJson(true)).toBe('true')
|
|
74
|
-
expect(canonicalJson(false)).toBe('false')
|
|
75
|
-
})
|
|
72
|
+
expect(canonicalJson(true)).toBe('true')
|
|
73
|
+
expect(canonicalJson(false)).toBe('false')
|
|
74
|
+
})
|
|
76
75
|
|
|
77
76
|
it('handles integers', () => {
|
|
78
|
-
expect(canonicalJson(0)).toBe('0')
|
|
79
|
-
expect(canonicalJson(1)).toBe('1')
|
|
80
|
-
expect(canonicalJson(-1)).toBe('-1')
|
|
81
|
-
expect(canonicalJson(42)).toBe('42')
|
|
82
|
-
})
|
|
77
|
+
expect(canonicalJson(0)).toBe('0')
|
|
78
|
+
expect(canonicalJson(1)).toBe('1')
|
|
79
|
+
expect(canonicalJson(-1)).toBe('-1')
|
|
80
|
+
expect(canonicalJson(42)).toBe('42')
|
|
81
|
+
})
|
|
83
82
|
|
|
84
83
|
it('handles floating point numbers', () => {
|
|
85
|
-
expect(canonicalJson(1.5)).toBe('1.5')
|
|
86
|
-
expect(canonicalJson(0.1)).toBe('0.1')
|
|
87
|
-
})
|
|
84
|
+
expect(canonicalJson(1.5)).toBe('1.5')
|
|
85
|
+
expect(canonicalJson(0.1)).toBe('0.1')
|
|
86
|
+
})
|
|
88
87
|
|
|
89
88
|
it('handles negative zero as 0', () => {
|
|
90
89
|
// JSON.stringify(-0) produces "0" in JS
|
|
91
|
-
expect(canonicalJson(-0)).toBe('0')
|
|
92
|
-
})
|
|
90
|
+
expect(canonicalJson(-0)).toBe('0')
|
|
91
|
+
})
|
|
93
92
|
|
|
94
93
|
// --- Strings ---
|
|
95
94
|
it('handles empty string', () => {
|
|
96
|
-
expect(canonicalJson('')).toBe('""')
|
|
97
|
-
})
|
|
95
|
+
expect(canonicalJson('')).toBe('""')
|
|
96
|
+
})
|
|
98
97
|
|
|
99
98
|
it('handles unicode strings', () => {
|
|
100
|
-
expect(canonicalJson('你好')).toBe('"你好"')
|
|
101
|
-
expect(canonicalJson('🌍')).toBe('"🌍"')
|
|
102
|
-
})
|
|
99
|
+
expect(canonicalJson('你好')).toBe('"你好"')
|
|
100
|
+
expect(canonicalJson('🌍')).toBe('"🌍"')
|
|
101
|
+
})
|
|
103
102
|
|
|
104
103
|
it('escapes control characters', () => {
|
|
105
|
-
expect(canonicalJson('\n')).toBe('"\\n"')
|
|
106
|
-
expect(canonicalJson('\t')).toBe('"\\t"')
|
|
107
|
-
expect(canonicalJson('\r')).toBe('"\\r"')
|
|
108
|
-
})
|
|
104
|
+
expect(canonicalJson('\n')).toBe('"\\n"')
|
|
105
|
+
expect(canonicalJson('\t')).toBe('"\\t"')
|
|
106
|
+
expect(canonicalJson('\r')).toBe('"\\r"')
|
|
107
|
+
})
|
|
109
108
|
|
|
110
109
|
it('escapes backslash and double quote', () => {
|
|
111
|
-
expect(canonicalJson('\\')).toBe('"\\\\"')
|
|
112
|
-
expect(canonicalJson('"')).toBe('"\\""')
|
|
113
|
-
})
|
|
110
|
+
expect(canonicalJson('\\')).toBe('"\\\\"')
|
|
111
|
+
expect(canonicalJson('"')).toBe('"\\""')
|
|
112
|
+
})
|
|
114
113
|
|
|
115
114
|
it('does not escape forward slash', () => {
|
|
116
|
-
expect(canonicalJson('a/b')).toBe('"a/b"')
|
|
117
|
-
})
|
|
115
|
+
expect(canonicalJson('a/b')).toBe('"a/b"')
|
|
116
|
+
})
|
|
118
117
|
|
|
119
118
|
// --- Empty containers ---
|
|
120
119
|
it('handles empty object', () => {
|
|
121
|
-
expect(canonicalJson({})).toBe('{}')
|
|
122
|
-
})
|
|
120
|
+
expect(canonicalJson({})).toBe('{}')
|
|
121
|
+
})
|
|
123
122
|
|
|
124
123
|
it('handles empty array', () => {
|
|
125
|
-
expect(canonicalJson([])).toBe('[]')
|
|
126
|
-
})
|
|
124
|
+
expect(canonicalJson([])).toBe('[]')
|
|
125
|
+
})
|
|
127
126
|
|
|
128
127
|
// --- No whitespace ---
|
|
129
128
|
it('produces no whitespace', () => {
|
|
130
|
-
const result = canonicalJson({ a: [1, 2], b: { c: 3 } })
|
|
131
|
-
expect(result).not.toMatch(/\s/)
|
|
132
|
-
expect(result).toBe('{"a":[1,2],"b":{"c":3}}')
|
|
133
|
-
})
|
|
129
|
+
const result = canonicalJson({ a: [1, 2], b: { c: 3 } })
|
|
130
|
+
expect(result).not.toMatch(/\s/)
|
|
131
|
+
expect(result).toBe('{"a":[1,2],"b":{"c":3}}')
|
|
132
|
+
})
|
|
134
133
|
|
|
135
134
|
// --- Omits undefined values in objects ---
|
|
136
135
|
it('omits keys with undefined values (matches JSON.stringify)', () => {
|
|
137
|
-
const input = { a: 1, b: undefined, c: 3 }
|
|
138
|
-
const result = canonicalJson(input)
|
|
139
|
-
expect(result).toBe('{"a":1,"c":3}')
|
|
140
|
-
})
|
|
136
|
+
const input = { a: 1, b: undefined, c: 3 }
|
|
137
|
+
const result = canonicalJson(input)
|
|
138
|
+
expect(result).toBe('{"a":1,"c":3}')
|
|
139
|
+
})
|
|
141
140
|
|
|
142
141
|
// --- Spec test vector with SHA-256 ---
|
|
143
142
|
it('matches protocol spec test vector byte-for-byte with correct SHA-256', () => {
|
|
@@ -145,26 +144,26 @@ describe('canonicalJson — edge cases', () => {
|
|
|
145
144
|
methods: ['wallet_signTransaction', 'wallet_signMessage'],
|
|
146
145
|
events: ['accountsChanged', 'chainChanged'],
|
|
147
146
|
chains: ['eip155:1', 'eip155:137'],
|
|
148
|
-
}
|
|
149
|
-
const output = canonicalJson(input)
|
|
147
|
+
}
|
|
148
|
+
const output = canonicalJson(input)
|
|
150
149
|
const expected =
|
|
151
|
-
'{"chains":["eip155:1","eip155:137"],"events":["accountsChanged","chainChanged"],"methods":["wallet_signTransaction","wallet_signMessage"]}'
|
|
152
|
-
expect(output).toBe(expected)
|
|
150
|
+
'{"chains":["eip155:1","eip155:137"],"events":["accountsChanged","chainChanged"],"methods":["wallet_signTransaction","wallet_signMessage"]}'
|
|
151
|
+
expect(output).toBe(expected)
|
|
153
152
|
|
|
154
153
|
// Verify SHA-256 from protocol spec
|
|
155
|
-
const hash = sha256Hex(new TextEncoder().encode(output))
|
|
156
|
-
expect(hash).toBe('4da366e2aae26b47b3d90fff52410752348733350ce2525dce7d64510f571333')
|
|
157
|
-
})
|
|
154
|
+
const hash = sha256Hex(new TextEncoder().encode(output))
|
|
155
|
+
expect(hash).toBe('4da366e2aae26b47b3d90fff52410752348733350ce2525dce7d64510f571333')
|
|
156
|
+
})
|
|
158
157
|
|
|
159
158
|
// --- Determinism ---
|
|
160
159
|
it('is deterministic across multiple calls', () => {
|
|
161
|
-
const obj = { z: [3, 1], a: { y: 'hello', x: true } }
|
|
162
|
-
const r1 = canonicalJson(obj)
|
|
163
|
-
const r2 = canonicalJson(obj)
|
|
164
|
-
const r3 = canonicalJson(JSON.parse(JSON.stringify(obj)))
|
|
165
|
-
expect(r1).toBe(r2)
|
|
166
|
-
expect(r1).toBe(r3)
|
|
167
|
-
})
|
|
160
|
+
const obj = { z: [3, 1], a: { y: 'hello', x: true } }
|
|
161
|
+
const r1 = canonicalJson(obj)
|
|
162
|
+
const r2 = canonicalJson(obj)
|
|
163
|
+
const r3 = canonicalJson(JSON.parse(JSON.stringify(obj)))
|
|
164
|
+
expect(r1).toBe(r2)
|
|
165
|
+
expect(r1).toBe(r3)
|
|
166
|
+
})
|
|
168
167
|
|
|
169
168
|
// --- Complex mixed structures ---
|
|
170
169
|
it('handles mixed nested structures', () => {
|
|
@@ -175,13 +174,13 @@ describe('canonicalJson — edge cases', () => {
|
|
|
175
174
|
chains: ['eip155:1'],
|
|
176
175
|
},
|
|
177
176
|
meta: { name: 'MyWallet' },
|
|
178
|
-
}
|
|
179
|
-
const result = canonicalJson(input)
|
|
177
|
+
}
|
|
178
|
+
const result = canonicalJson(input)
|
|
180
179
|
// Keys sorted: capabilities < meta; inner keys sorted too
|
|
181
180
|
expect(result).toBe(
|
|
182
181
|
'{"capabilities":{"chains":["eip155:1"],"events":["accountsChanged"],"methods":["wallet_signTransaction"]},"meta":{"name":"MyWallet"}}',
|
|
183
|
-
)
|
|
184
|
-
})
|
|
182
|
+
)
|
|
183
|
+
})
|
|
185
184
|
|
|
186
185
|
// --- Matches test vector for join plaintext ---
|
|
187
186
|
it('matches join plaintext test vector from protocol appendix', () => {
|
|
@@ -192,223 +191,223 @@ describe('canonicalJson — edge cases', () => {
|
|
|
192
191
|
chains: ['eip155:1', 'eip155:137'],
|
|
193
192
|
},
|
|
194
193
|
meta: { name: 'MyWallet' },
|
|
195
|
-
}
|
|
196
|
-
const result = canonicalJson(input)
|
|
194
|
+
}
|
|
195
|
+
const result = canonicalJson(input)
|
|
197
196
|
expect(result).toBe(
|
|
198
197
|
'{"capabilities":{"chains":["eip155:1","eip155:137"],"events":["accountsChanged","chainChanged"],"methods":["wallet_signTransaction","wallet_signMessage"]},"meta":{"name":"MyWallet"}}',
|
|
199
|
-
)
|
|
200
|
-
})
|
|
201
|
-
})
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
202
201
|
|
|
203
202
|
// ═══════════════════════════════════════════════════════════════════════
|
|
204
203
|
// Nonce determinism and uniqueness
|
|
205
204
|
// ═══════════════════════════════════════════════════════════════════════
|
|
206
205
|
|
|
207
206
|
describe('sealPayload nonce determinism', () => {
|
|
208
|
-
const key = new Uint8Array(32).fill(0xaa)
|
|
209
|
-
const ch = 'bb'.repeat(32)
|
|
207
|
+
const key = new Uint8Array(32).fill(0xaa)
|
|
208
|
+
const ch = 'bb'.repeat(32)
|
|
210
209
|
|
|
211
210
|
it('same key + same seq + same data = same ciphertext (deterministic nonce)', () => {
|
|
212
|
-
const data = { test: true }
|
|
213
|
-
const s1 = sealPayload(key, ch, 0, data)
|
|
214
|
-
const s2 = sealPayload(key, ch, 0, data)
|
|
215
|
-
expect(s1).toBe(s2)
|
|
216
|
-
})
|
|
211
|
+
const data = { test: true }
|
|
212
|
+
const s1 = sealPayload(key, ch, 0, data)
|
|
213
|
+
const s2 = sealPayload(key, ch, 0, data)
|
|
214
|
+
expect(s1).toBe(s2)
|
|
215
|
+
})
|
|
217
216
|
|
|
218
217
|
it('same key + different seq = different ciphertext (different nonce)', () => {
|
|
219
|
-
const data = { test: true }
|
|
220
|
-
const s0 = sealPayload(key, ch, 0, data)
|
|
221
|
-
const s1 = sealPayload(key, ch, 1, data)
|
|
222
|
-
expect(s0).not.toBe(s1)
|
|
223
|
-
})
|
|
218
|
+
const data = { test: true }
|
|
219
|
+
const s0 = sealPayload(key, ch, 0, data)
|
|
220
|
+
const s1 = sealPayload(key, ch, 1, data)
|
|
221
|
+
expect(s0).not.toBe(s1)
|
|
222
|
+
})
|
|
224
223
|
|
|
225
224
|
it('different key + same seq = different ciphertext (different nonce derivation)', () => {
|
|
226
|
-
const key2 = new Uint8Array(32).fill(0xcc)
|
|
227
|
-
const data = { test: true }
|
|
228
|
-
const s1 = sealPayload(key, ch, 0, data)
|
|
229
|
-
const s2 = sealPayload(key2, ch, 0, data)
|
|
230
|
-
expect(s1).not.toBe(s2)
|
|
231
|
-
})
|
|
225
|
+
const key2 = new Uint8Array(32).fill(0xcc)
|
|
226
|
+
const data = { test: true }
|
|
227
|
+
const s1 = sealPayload(key, ch, 0, data)
|
|
228
|
+
const s2 = sealPayload(key2, ch, 0, data)
|
|
229
|
+
expect(s1).not.toBe(s2)
|
|
230
|
+
})
|
|
232
231
|
|
|
233
232
|
it('seq=0 and seq=2^31-1 both work', () => {
|
|
234
|
-
const data = { x: 1 }
|
|
235
|
-
const s0 = sealPayload(key, ch, 0, data)
|
|
236
|
-
const sMax = sealPayload(key, ch, 2 ** 31 - 1, data)
|
|
237
|
-
expect(unsealPayload(key, ch, s0).seq).toBe(0)
|
|
238
|
-
expect(unsealPayload(key, ch, sMax).seq).toBe(2 ** 31 - 1)
|
|
239
|
-
})
|
|
240
|
-
})
|
|
233
|
+
const data = { x: 1 }
|
|
234
|
+
const s0 = sealPayload(key, ch, 0, data)
|
|
235
|
+
const sMax = sealPayload(key, ch, 2 ** 31 - 1, data)
|
|
236
|
+
expect(unsealPayload(key, ch, s0).seq).toBe(0)
|
|
237
|
+
expect(unsealPayload(key, ch, sMax).seq).toBe(2 ** 31 - 1)
|
|
238
|
+
})
|
|
239
|
+
})
|
|
241
240
|
|
|
242
241
|
// ═══════════════════════════════════════════════════════════════════════
|
|
243
242
|
// AAD header type differentiation
|
|
244
243
|
// ═══════════════════════════════════════════════════════════════════════
|
|
245
244
|
|
|
246
245
|
describe('seal/unseal with AAD headers', () => {
|
|
247
|
-
const key = new Uint8Array(32).fill(0x55)
|
|
248
|
-
const ch = 'cc'.repeat(32)
|
|
249
|
-
const data = { method: 'eth_sign', params: [] }
|
|
246
|
+
const key = new Uint8Array(32).fill(0x55)
|
|
247
|
+
const ch = 'cc'.repeat(32)
|
|
248
|
+
const data = { method: 'eth_sign', params: [] }
|
|
250
249
|
|
|
251
250
|
it('req type AAD produces different ciphertext than res type', () => {
|
|
252
|
-
const reqHdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
|
|
253
|
-
const resHdr: AadHeader = { type: 'res', from: 'peer1', id: 'r1' }
|
|
254
|
-
const sealed1 = sealPayload(key, ch, 0, data, reqHdr)
|
|
255
|
-
const sealed2 = sealPayload(key, ch, 0, data, resHdr)
|
|
256
|
-
expect(sealed1).not.toBe(sealed2)
|
|
257
|
-
})
|
|
251
|
+
const reqHdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
|
|
252
|
+
const resHdr: AadHeader = { type: 'res', from: 'peer1', id: 'r1' }
|
|
253
|
+
const sealed1 = sealPayload(key, ch, 0, data, reqHdr)
|
|
254
|
+
const sealed2 = sealPayload(key, ch, 0, data, resHdr)
|
|
255
|
+
expect(sealed1).not.toBe(sealed2)
|
|
256
|
+
})
|
|
258
257
|
|
|
259
258
|
it('different from in AAD produces different ciphertext', () => {
|
|
260
|
-
const hdr1: AadHeader = { type: 'req', from: 'peerA', id: 'r1' }
|
|
261
|
-
const hdr2: AadHeader = { type: 'req', from: 'peerB', id: 'r1' }
|
|
262
|
-
const sealed1 = sealPayload(key, ch, 0, data, hdr1)
|
|
263
|
-
const sealed2 = sealPayload(key, ch, 0, data, hdr2)
|
|
264
|
-
expect(sealed1).not.toBe(sealed2)
|
|
265
|
-
})
|
|
259
|
+
const hdr1: AadHeader = { type: 'req', from: 'peerA', id: 'r1' }
|
|
260
|
+
const hdr2: AadHeader = { type: 'req', from: 'peerB', id: 'r1' }
|
|
261
|
+
const sealed1 = sealPayload(key, ch, 0, data, hdr1)
|
|
262
|
+
const sealed2 = sealPayload(key, ch, 0, data, hdr2)
|
|
263
|
+
expect(sealed1).not.toBe(sealed2)
|
|
264
|
+
})
|
|
266
265
|
|
|
267
266
|
it('different id in AAD produces different ciphertext', () => {
|
|
268
|
-
const hdr1: AadHeader = { type: 'req', from: 'peer1', id: 'id-1' }
|
|
269
|
-
const hdr2: AadHeader = { type: 'req', from: 'peer1', id: 'id-2' }
|
|
270
|
-
const sealed1 = sealPayload(key, ch, 0, data, hdr1)
|
|
271
|
-
const sealed2 = sealPayload(key, ch, 0, data, hdr2)
|
|
272
|
-
expect(sealed1).not.toBe(sealed2)
|
|
273
|
-
})
|
|
267
|
+
const hdr1: AadHeader = { type: 'req', from: 'peer1', id: 'id-1' }
|
|
268
|
+
const hdr2: AadHeader = { type: 'req', from: 'peer1', id: 'id-2' }
|
|
269
|
+
const sealed1 = sealPayload(key, ch, 0, data, hdr1)
|
|
270
|
+
const sealed2 = sealPayload(key, ch, 0, data, hdr2)
|
|
271
|
+
expect(sealed1).not.toBe(sealed2)
|
|
272
|
+
})
|
|
274
273
|
|
|
275
274
|
it('decrypt fails if AAD header type mismatches', () => {
|
|
276
|
-
const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
|
|
277
|
-
const sealed = sealPayload(key, ch, 0, data, hdr)
|
|
278
|
-
const wrongHdr: AadHeader = { type: 'res', from: 'peer1', id: 'r1' }
|
|
279
|
-
expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
|
|
280
|
-
})
|
|
275
|
+
const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
|
|
276
|
+
const sealed = sealPayload(key, ch, 0, data, hdr)
|
|
277
|
+
const wrongHdr: AadHeader = { type: 'res', from: 'peer1', id: 'r1' }
|
|
278
|
+
expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
|
|
279
|
+
})
|
|
281
280
|
|
|
282
281
|
it('decrypt fails if AAD from field mismatches', () => {
|
|
283
|
-
const hdr: AadHeader = { type: 'req', from: 'peerA', id: 'r1' }
|
|
284
|
-
const sealed = sealPayload(key, ch, 0, data, hdr)
|
|
285
|
-
const wrongHdr: AadHeader = { type: 'req', from: 'peerB', id: 'r1' }
|
|
286
|
-
expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
|
|
287
|
-
})
|
|
282
|
+
const hdr: AadHeader = { type: 'req', from: 'peerA', id: 'r1' }
|
|
283
|
+
const sealed = sealPayload(key, ch, 0, data, hdr)
|
|
284
|
+
const wrongHdr: AadHeader = { type: 'req', from: 'peerB', id: 'r1' }
|
|
285
|
+
expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
|
|
286
|
+
})
|
|
288
287
|
|
|
289
288
|
it('decrypt fails if AAD id field mismatches', () => {
|
|
290
|
-
const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
|
|
291
|
-
const sealed = sealPayload(key, ch, 0, data, hdr)
|
|
292
|
-
const wrongHdr: AadHeader = { type: 'req', from: 'peer1', id: 'r2' }
|
|
293
|
-
expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
|
|
294
|
-
})
|
|
289
|
+
const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
|
|
290
|
+
const sealed = sealPayload(key, ch, 0, data, hdr)
|
|
291
|
+
const wrongHdr: AadHeader = { type: 'req', from: 'peer1', id: 'r2' }
|
|
292
|
+
expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
|
|
293
|
+
})
|
|
295
294
|
|
|
296
295
|
it('decrypt succeeds with matching AAD header', () => {
|
|
297
|
-
const hdr: AadHeader = { type: 'evt', from: 'wallet', id: 'e1' }
|
|
298
|
-
const sealed = sealPayload(key, ch, 5, data, hdr)
|
|
299
|
-
const { seq, data: d } = unsealPayload(key, ch, sealed, hdr)
|
|
300
|
-
expect(seq).toBe(5)
|
|
301
|
-
expect(d).toEqual(data)
|
|
302
|
-
})
|
|
296
|
+
const hdr: AadHeader = { type: 'evt', from: 'wallet', id: 'e1' }
|
|
297
|
+
const sealed = sealPayload(key, ch, 5, data, hdr)
|
|
298
|
+
const { seq, data: d } = unsealPayload(key, ch, sealed, hdr)
|
|
299
|
+
expect(seq).toBe(5)
|
|
300
|
+
expect(d).toEqual(data)
|
|
301
|
+
})
|
|
303
302
|
|
|
304
303
|
it('sealed with AAD cannot be decrypted without AAD', () => {
|
|
305
|
-
const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
|
|
306
|
-
const sealed = sealPayload(key, ch, 0, data, hdr)
|
|
304
|
+
const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
|
|
305
|
+
const sealed = sealPayload(key, ch, 0, data, hdr)
|
|
307
306
|
// Decrypt without AAD — should fail because AAD mismatch
|
|
308
|
-
expect(() => unsealPayload(key, ch, sealed)).toThrow()
|
|
309
|
-
})
|
|
307
|
+
expect(() => unsealPayload(key, ch, sealed)).toThrow()
|
|
308
|
+
})
|
|
310
309
|
|
|
311
310
|
it('sealed without AAD cannot be decrypted with AAD', () => {
|
|
312
|
-
const sealed = sealPayload(key, ch, 0, data)
|
|
313
|
-
const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
|
|
314
|
-
expect(() => unsealPayload(key, ch, sealed, hdr)).toThrow()
|
|
315
|
-
})
|
|
316
|
-
})
|
|
311
|
+
const sealed = sealPayload(key, ch, 0, data)
|
|
312
|
+
const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
|
|
313
|
+
expect(() => unsealPayload(key, ch, sealed, hdr)).toThrow()
|
|
314
|
+
})
|
|
315
|
+
})
|
|
317
316
|
|
|
318
317
|
// ═══════════════════════════════════════════════════════════════════════
|
|
319
318
|
// unsealPayload error paths
|
|
320
319
|
// ═══════════════════════════════════════════════════════════════════════
|
|
321
320
|
|
|
322
321
|
describe('unsealPayload error paths', () => {
|
|
323
|
-
const key = new Uint8Array(32).fill(0x77)
|
|
324
|
-
const ch = 'dd'.repeat(32)
|
|
322
|
+
const key = new Uint8Array(32).fill(0x77)
|
|
323
|
+
const ch = 'dd'.repeat(32)
|
|
325
324
|
|
|
326
325
|
it('throws on empty sealed string', () => {
|
|
327
|
-
expect(() => unsealPayload(key, ch, '')).toThrow()
|
|
328
|
-
})
|
|
326
|
+
expect(() => unsealPayload(key, ch, '')).toThrow()
|
|
327
|
+
})
|
|
329
328
|
|
|
330
329
|
it('throws on truncated sealed (too short for seq + tag)', () => {
|
|
331
|
-
const truncated = b64urlEncode(new Uint8Array(4))
|
|
332
|
-
expect(() => unsealPayload(key, ch, truncated)).toThrow()
|
|
333
|
-
})
|
|
330
|
+
const truncated = b64urlEncode(new Uint8Array(4)) // only seq, no ciphertext
|
|
331
|
+
expect(() => unsealPayload(key, ch, truncated)).toThrow()
|
|
332
|
+
})
|
|
334
333
|
|
|
335
334
|
it('throws on tampered seq bytes', () => {
|
|
336
|
-
const sealed = sealPayload(key, ch, 5, { test: true })
|
|
337
|
-
const bytes = b64urlDecode(sealed)
|
|
338
|
-
bytes[0]
|
|
339
|
-
const tampered = b64urlEncode(bytes)
|
|
335
|
+
const sealed = sealPayload(key, ch, 5, { test: true })
|
|
336
|
+
const bytes = b64urlDecode(sealed)
|
|
337
|
+
bytes[0] = (bytes[0] ?? 0) ^ 0xff // tamper seq byte
|
|
338
|
+
const tampered = b64urlEncode(bytes)
|
|
340
339
|
// Changing seq changes nonce → decryption fails
|
|
341
|
-
expect(() => unsealPayload(key, ch, tampered)).toThrow()
|
|
342
|
-
})
|
|
340
|
+
expect(() => unsealPayload(key, ch, tampered)).toThrow()
|
|
341
|
+
})
|
|
343
342
|
|
|
344
343
|
it('throws on single-bit flip in ciphertext', () => {
|
|
345
|
-
const sealed = sealPayload(key, ch, 0, { x: 1 })
|
|
346
|
-
const bytes = b64urlDecode(sealed)
|
|
347
|
-
bytes[bytes.length - 1]
|
|
348
|
-
expect(() => unsealPayload(key, ch, b64urlEncode(bytes))).toThrow()
|
|
349
|
-
})
|
|
350
|
-
})
|
|
344
|
+
const sealed = sealPayload(key, ch, 0, { x: 1 })
|
|
345
|
+
const bytes = b64urlDecode(sealed)
|
|
346
|
+
bytes[bytes.length - 1] = (bytes[bytes.length - 1] ?? 0) ^ 0x01 // flip 1 bit in tag
|
|
347
|
+
expect(() => unsealPayload(key, ch, b64urlEncode(bytes))).toThrow()
|
|
348
|
+
})
|
|
349
|
+
})
|
|
351
350
|
|
|
352
351
|
// ═══════════════════════════════════════════════════════════════════════
|
|
353
352
|
// sealJoin / unsealJoin error paths
|
|
354
353
|
// ═══════════════════════════════════════════════════════════════════════
|
|
355
354
|
|
|
356
355
|
describe('sealJoin / unsealJoin error paths', () => {
|
|
357
|
-
const rootKey = new Uint8Array(32).fill(0x33)
|
|
358
|
-
const ch = 'ee'.repeat(32)
|
|
359
|
-
const joinKey = deriveJoinEncryptionKey(rootKey, ch)
|
|
356
|
+
const rootKey = new Uint8Array(32).fill(0x33)
|
|
357
|
+
const ch = 'ee'.repeat(32)
|
|
358
|
+
const joinKey = deriveJoinEncryptionKey(rootKey, ch)
|
|
360
359
|
|
|
361
360
|
it('decrypt fails with wrong key', () => {
|
|
362
|
-
const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
|
|
363
|
-
const sealed = sealJoin(joinKey, ch, caps, { name: 'W' })
|
|
364
|
-
const wrongKey = new Uint8Array(32).fill(0x99)
|
|
365
|
-
expect(() => unsealJoin(wrongKey, ch, sealed)).toThrow()
|
|
366
|
-
})
|
|
361
|
+
const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
|
|
362
|
+
const sealed = sealJoin(joinKey, ch, caps, { name: 'W' })
|
|
363
|
+
const wrongKey = new Uint8Array(32).fill(0x99)
|
|
364
|
+
expect(() => unsealJoin(wrongKey, ch, sealed)).toThrow()
|
|
365
|
+
})
|
|
367
366
|
|
|
368
367
|
it('decrypt fails with wrong channel ID', () => {
|
|
369
|
-
const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
|
|
370
|
-
const sealed = sealJoin(joinKey, ch, caps, { name: 'W' })
|
|
371
|
-
const wrongCh = 'ff'.repeat(32)
|
|
372
|
-
expect(() => unsealJoin(joinKey, wrongCh, sealed)).toThrow()
|
|
373
|
-
})
|
|
368
|
+
const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
|
|
369
|
+
const sealed = sealJoin(joinKey, ch, caps, { name: 'W' })
|
|
370
|
+
const wrongCh = 'ff'.repeat(32)
|
|
371
|
+
expect(() => unsealJoin(joinKey, wrongCh, sealed)).toThrow()
|
|
372
|
+
})
|
|
374
373
|
|
|
375
374
|
it('decrypt fails with tampered ciphertext', () => {
|
|
376
|
-
const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
|
|
377
|
-
const sealed = sealJoin(joinKey, ch, caps, { name: 'W' })
|
|
378
|
-
const bytes = b64urlDecode(sealed)
|
|
379
|
-
bytes[bytes.length - 1]
|
|
380
|
-
expect(() => unsealJoin(joinKey, ch, b64urlEncode(bytes))).toThrow()
|
|
381
|
-
})
|
|
375
|
+
const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
|
|
376
|
+
const sealed = sealJoin(joinKey, ch, caps, { name: 'W' })
|
|
377
|
+
const bytes = b64urlDecode(sealed)
|
|
378
|
+
bytes[bytes.length - 1] = (bytes[bytes.length - 1] ?? 0) ^ 0xff
|
|
379
|
+
expect(() => unsealJoin(joinKey, ch, b64urlEncode(bytes))).toThrow()
|
|
380
|
+
})
|
|
382
381
|
|
|
383
382
|
it('throws on envelope smaller than nonce + tag', () => {
|
|
384
|
-
const tiny = b64urlEncode(new Uint8Array(10))
|
|
385
|
-
expect(() => unsealJoin(joinKey, ch, tiny)).toThrow('Invalid sealed_join')
|
|
386
|
-
})
|
|
383
|
+
const tiny = b64urlEncode(new Uint8Array(10))
|
|
384
|
+
expect(() => unsealJoin(joinKey, ch, tiny)).toThrow('Invalid sealed_join')
|
|
385
|
+
})
|
|
387
386
|
|
|
388
387
|
it('round-trips with null meta', () => {
|
|
389
|
-
const caps = { methods: ['test'], events: [], chains: [] }
|
|
390
|
-
const sealed = sealJoin(joinKey, ch, caps)
|
|
391
|
-
const { capabilities, meta } = unsealJoin(joinKey, ch, sealed)
|
|
392
|
-
expect(capabilities).toEqual(caps)
|
|
393
|
-
expect(meta).toBeNull()
|
|
394
|
-
})
|
|
395
|
-
})
|
|
388
|
+
const caps = { methods: ['test'], events: [], chains: [] }
|
|
389
|
+
const sealed = sealJoin(joinKey, ch, caps)
|
|
390
|
+
const { capabilities, meta } = unsealJoin(joinKey, ch, sealed)
|
|
391
|
+
expect(capabilities).toEqual(caps)
|
|
392
|
+
expect(meta).toBeNull()
|
|
393
|
+
})
|
|
394
|
+
})
|
|
396
395
|
|
|
397
396
|
// ═══════════════════════════════════════════════════════════════════════
|
|
398
397
|
// Key separation guarantees
|
|
399
398
|
// ═══════════════════════════════════════════════════════════════════════
|
|
400
399
|
|
|
401
400
|
describe('Key separation', () => {
|
|
402
|
-
const alice = generateX25519KeyPair()
|
|
403
|
-
const bob = generateX25519KeyPair()
|
|
404
|
-
const ch = generateChannelId()
|
|
405
|
-
const shared = computeSharedSecret(alice.privateKey, bob.publicKey)
|
|
406
|
-
const rootKey = deriveSessionKey(shared, ch)
|
|
401
|
+
const alice = generateX25519KeyPair()
|
|
402
|
+
const bob = generateX25519KeyPair()
|
|
403
|
+
const ch = generateChannelId()
|
|
404
|
+
const shared = computeSharedSecret(alice.privateKey, bob.publicKey)
|
|
405
|
+
const rootKey = deriveSessionKey(shared, ch)
|
|
407
406
|
|
|
408
407
|
it('joinEncryptionKey differs from rootKey', () => {
|
|
409
|
-
const joinKey = deriveJoinEncryptionKey(rootKey, ch)
|
|
410
|
-
expect(bytesToHex(joinKey)).not.toBe(bytesToHex(rootKey))
|
|
411
|
-
})
|
|
408
|
+
const joinKey = deriveJoinEncryptionKey(rootKey, ch)
|
|
409
|
+
expect(bytesToHex(joinKey)).not.toBe(bytesToHex(rootKey))
|
|
410
|
+
})
|
|
412
411
|
|
|
413
412
|
it('dappToWalletKey differs from walletToDappKey', () => {
|
|
414
413
|
const ctx: SessionCryptoContext = {
|
|
@@ -416,23 +415,23 @@ describe('Key separation', () => {
|
|
|
416
415
|
walletPubKeyB64: bob.publicKeyB64,
|
|
417
416
|
capabilities: { methods: ['test'], events: [], chains: [] },
|
|
418
417
|
dappName: 'Test',
|
|
419
|
-
}
|
|
420
|
-
const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
|
|
421
|
-
expect(bytesToHex(keys.dappToWalletKey)).not.toBe(bytesToHex(keys.walletToDappKey))
|
|
422
|
-
})
|
|
418
|
+
}
|
|
419
|
+
const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
|
|
420
|
+
expect(bytesToHex(keys.dappToWalletKey)).not.toBe(bytesToHex(keys.walletToDappKey))
|
|
421
|
+
})
|
|
423
422
|
|
|
424
423
|
it('joinEncryptionKey differs from both directional keys', () => {
|
|
425
|
-
const joinKey = deriveJoinEncryptionKey(rootKey, ch)
|
|
424
|
+
const joinKey = deriveJoinEncryptionKey(rootKey, ch)
|
|
426
425
|
const ctx: SessionCryptoContext = {
|
|
427
426
|
dappPubKeyB64: alice.publicKeyB64,
|
|
428
427
|
walletPubKeyB64: bob.publicKeyB64,
|
|
429
428
|
capabilities: { methods: ['test'], events: [], chains: [] },
|
|
430
429
|
dappName: 'Test',
|
|
431
|
-
}
|
|
432
|
-
const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
|
|
433
|
-
expect(bytesToHex(joinKey)).not.toBe(bytesToHex(keys.dappToWalletKey))
|
|
434
|
-
expect(bytesToHex(joinKey)).not.toBe(bytesToHex(keys.walletToDappKey))
|
|
435
|
-
})
|
|
430
|
+
}
|
|
431
|
+
const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
|
|
432
|
+
expect(bytesToHex(joinKey)).not.toBe(bytesToHex(keys.dappToWalletKey))
|
|
433
|
+
expect(bytesToHex(joinKey)).not.toBe(bytesToHex(keys.walletToDappKey))
|
|
434
|
+
})
|
|
436
435
|
|
|
437
436
|
it('different capabilities produce different directional keys', () => {
|
|
438
437
|
const ctx1: SessionCryptoContext = {
|
|
@@ -440,31 +439,35 @@ describe('Key separation', () => {
|
|
|
440
439
|
walletPubKeyB64: bob.publicKeyB64,
|
|
441
440
|
capabilities: { methods: ['wallet_signMessage'], events: [], chains: [] },
|
|
442
441
|
dappName: 'Test',
|
|
443
|
-
}
|
|
442
|
+
}
|
|
444
443
|
const ctx2: SessionCryptoContext = {
|
|
445
444
|
dappPubKeyB64: alice.publicKeyB64,
|
|
446
445
|
walletPubKeyB64: bob.publicKeyB64,
|
|
447
446
|
capabilities: { methods: ['wallet_sendTransaction'], events: [], chains: [] },
|
|
448
447
|
dappName: 'Test',
|
|
449
|
-
}
|
|
450
|
-
const k1 = deriveDirectionalSessionKeys(rootKey, ch, ctx1)
|
|
451
|
-
const k2 = deriveDirectionalSessionKeys(rootKey, ch, ctx2)
|
|
452
|
-
expect(bytesToHex(k1.dappToWalletKey)).not.toBe(bytesToHex(k2.dappToWalletKey))
|
|
453
|
-
})
|
|
448
|
+
}
|
|
449
|
+
const k1 = deriveDirectionalSessionKeys(rootKey, ch, ctx1)
|
|
450
|
+
const k2 = deriveDirectionalSessionKeys(rootKey, ch, ctx2)
|
|
451
|
+
expect(bytesToHex(k1.dappToWalletKey)).not.toBe(bytesToHex(k2.dappToWalletKey))
|
|
452
|
+
})
|
|
454
453
|
|
|
455
454
|
it('different dappName produces different transcript hash', () => {
|
|
456
455
|
const ctx1: SessionCryptoContext = {
|
|
457
|
-
dappPubKeyB64: 'pub1',
|
|
458
|
-
|
|
459
|
-
|
|
456
|
+
dappPubKeyB64: 'pub1',
|
|
457
|
+
walletPubKeyB64: 'pub2',
|
|
458
|
+
capabilities: null,
|
|
459
|
+
dappName: 'AppA',
|
|
460
|
+
}
|
|
460
461
|
const ctx2: SessionCryptoContext = {
|
|
461
|
-
dappPubKeyB64: 'pub1',
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
462
|
+
dappPubKeyB64: 'pub1',
|
|
463
|
+
walletPubKeyB64: 'pub2',
|
|
464
|
+
capabilities: null,
|
|
465
|
+
dappName: 'AppB',
|
|
466
|
+
}
|
|
467
|
+
const h1 = computeHandshakeTranscriptHash(ch, ctx1)
|
|
468
|
+
const h2 = computeHandshakeTranscriptHash(ch, ctx2)
|
|
469
|
+
expect(bytesToHex(h1)).not.toBe(bytesToHex(h2))
|
|
470
|
+
})
|
|
468
471
|
|
|
469
472
|
it('cross-direction decryption fails', () => {
|
|
470
473
|
const ctx: SessionCryptoContext = {
|
|
@@ -472,16 +475,16 @@ describe('Key separation', () => {
|
|
|
472
475
|
walletPubKeyB64: bob.publicKeyB64,
|
|
473
476
|
capabilities: null,
|
|
474
477
|
dappName: 'Test',
|
|
475
|
-
}
|
|
476
|
-
const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
|
|
478
|
+
}
|
|
479
|
+
const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
|
|
477
480
|
// Encrypt with dapp→wallet key
|
|
478
|
-
const sealed = sealPayload(keys.dappToWalletKey, ch, 0, { msg: 'hello' })
|
|
481
|
+
const sealed = sealPayload(keys.dappToWalletKey, ch, 0, { msg: 'hello' })
|
|
479
482
|
// Decrypt with wallet→dapp key should fail
|
|
480
|
-
expect(() => unsealPayload(keys.walletToDappKey, ch, sealed)).toThrow()
|
|
483
|
+
expect(() => unsealPayload(keys.walletToDappKey, ch, sealed)).toThrow()
|
|
481
484
|
// Decrypt with correct key should succeed
|
|
482
|
-
expect(unsealPayload(keys.dappToWalletKey, ch, sealed).data).toEqual({ msg: 'hello' })
|
|
483
|
-
})
|
|
484
|
-
})
|
|
485
|
+
expect(unsealPayload(keys.dappToWalletKey, ch, sealed).data).toEqual({ msg: 'hello' })
|
|
486
|
+
})
|
|
487
|
+
})
|
|
485
488
|
|
|
486
489
|
// ═══════════════════════════════════════════════════════════════════════
|
|
487
490
|
// Transcript hash determinism
|
|
@@ -489,41 +492,47 @@ describe('Key separation', () => {
|
|
|
489
492
|
|
|
490
493
|
describe('transcriptHash determinism', () => {
|
|
491
494
|
it('same inputs always produce same transcript hash', () => {
|
|
492
|
-
const ch = 'aa'.repeat(32)
|
|
495
|
+
const ch = 'aa'.repeat(32)
|
|
493
496
|
const ctx: SessionCryptoContext = {
|
|
494
497
|
dappPubKeyB64: 'dappPub',
|
|
495
498
|
walletPubKeyB64: 'walletPub',
|
|
496
499
|
capabilities: { methods: ['test'], events: [], chains: ['eip155:1'] },
|
|
497
500
|
walletMeta: { name: 'W' },
|
|
498
501
|
dappName: 'D',
|
|
499
|
-
}
|
|
500
|
-
const h1 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx))
|
|
501
|
-
const h2 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx))
|
|
502
|
-
expect(h1).toBe(h2)
|
|
503
|
-
})
|
|
502
|
+
}
|
|
503
|
+
const h1 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx))
|
|
504
|
+
const h2 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx))
|
|
505
|
+
expect(h1).toBe(h2)
|
|
506
|
+
})
|
|
504
507
|
|
|
505
508
|
it('different channel ID produces different hash', () => {
|
|
506
509
|
const ctx: SessionCryptoContext = {
|
|
507
|
-
dappPubKeyB64: 'pub1',
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
510
|
+
dappPubKeyB64: 'pub1',
|
|
511
|
+
walletPubKeyB64: 'pub2',
|
|
512
|
+
capabilities: null,
|
|
513
|
+
dappName: 'X',
|
|
514
|
+
}
|
|
515
|
+
const h1 = bytesToHex(computeHandshakeTranscriptHash('aa'.repeat(32), ctx))
|
|
516
|
+
const h2 = bytesToHex(computeHandshakeTranscriptHash('bb'.repeat(32), ctx))
|
|
517
|
+
expect(h1).not.toBe(h2)
|
|
518
|
+
})
|
|
514
519
|
|
|
515
520
|
it('swapped pub keys produce different hash', () => {
|
|
516
|
-
const ch = 'cc'.repeat(32)
|
|
521
|
+
const ch = 'cc'.repeat(32)
|
|
517
522
|
const ctx1: SessionCryptoContext = {
|
|
518
|
-
dappPubKeyB64: 'keyA',
|
|
519
|
-
|
|
520
|
-
|
|
523
|
+
dappPubKeyB64: 'keyA',
|
|
524
|
+
walletPubKeyB64: 'keyB',
|
|
525
|
+
capabilities: null,
|
|
526
|
+
dappName: 'X',
|
|
527
|
+
}
|
|
521
528
|
const ctx2: SessionCryptoContext = {
|
|
522
|
-
dappPubKeyB64: 'keyB',
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
529
|
+
dappPubKeyB64: 'keyB',
|
|
530
|
+
walletPubKeyB64: 'keyA',
|
|
531
|
+
capabilities: null,
|
|
532
|
+
dappName: 'X',
|
|
533
|
+
}
|
|
534
|
+
const h1 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx1))
|
|
535
|
+
const h2 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx2))
|
|
536
|
+
expect(h1).not.toBe(h2)
|
|
537
|
+
})
|
|
538
|
+
})
|