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
|
@@ -8,25 +8,23 @@
|
|
|
8
8
|
* Uses real crypto operations from the SDK — no mocks for crypto.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { describe,
|
|
11
|
+
import { describe, expect, it } from 'vitest'
|
|
12
|
+
import type { AadHeader, SessionCryptoContext } from '../../crypto.js'
|
|
12
13
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
b64urlDecode,
|
|
15
|
+
b64urlEncode,
|
|
16
|
+
bytesToHex,
|
|
15
17
|
computeSharedSecret,
|
|
16
|
-
deriveSessionKey,
|
|
17
|
-
deriveJoinEncryptionKey,
|
|
18
18
|
deriveDirectionalSessionKeys,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
deriveJoinEncryptionKey,
|
|
20
|
+
deriveSessionKey,
|
|
21
|
+
generateChannelId,
|
|
22
|
+
generateX25519KeyPair,
|
|
22
23
|
sealJoin,
|
|
24
|
+
sealPayload,
|
|
23
25
|
unsealJoin,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
bytesToHex,
|
|
27
|
-
hexToBytes,
|
|
28
|
-
} from '../../crypto.js';
|
|
29
|
-
import type { AadHeader, SessionCryptoContext } from '../../crypto.js';
|
|
26
|
+
unsealPayload,
|
|
27
|
+
} from '../../crypto.js'
|
|
30
28
|
|
|
31
29
|
// ---------------------------------------------------------------------------
|
|
32
30
|
// Attack 1: Nonce reuse detection
|
|
@@ -43,42 +41,42 @@ describe('Crypto Attack: Nonce reuse', () => {
|
|
|
43
41
|
// PREVENTS: Nonce reuse in ChaCha20-Poly1305, which would break
|
|
44
42
|
// confidentiality (Section 6.6.1).
|
|
45
43
|
|
|
46
|
-
const key = new Uint8Array(32)
|
|
47
|
-
crypto.getRandomValues(key)
|
|
48
|
-
const ch = generateChannelId()
|
|
44
|
+
const key = new Uint8Array(32)
|
|
45
|
+
crypto.getRandomValues(key)
|
|
46
|
+
const ch = generateChannelId()
|
|
49
47
|
|
|
50
48
|
// Two messages with same seq produce same ciphertext (deterministic nonce)
|
|
51
|
-
const s1 = sealPayload(key, ch, 42, { data: 'same' })
|
|
52
|
-
const s2 = sealPayload(key, ch, 42, { data: 'same' })
|
|
53
|
-
expect(s1).toBe(s2)
|
|
49
|
+
const s1 = sealPayload(key, ch, 42, { data: 'same' })
|
|
50
|
+
const s2 = sealPayload(key, ch, 42, { data: 'same' })
|
|
51
|
+
expect(s1).toBe(s2) // same key + seq + data = same output
|
|
54
52
|
|
|
55
53
|
// Receiver would accept the first and reject the second (seq not increasing).
|
|
56
54
|
// At the crypto level, verify the first decrypts correctly:
|
|
57
|
-
const { seq, data } = unsealPayload(key, ch, s1)
|
|
58
|
-
expect(seq).toBe(42)
|
|
59
|
-
expect(data).toEqual({ data: 'same' })
|
|
60
|
-
})
|
|
55
|
+
const { seq, data } = unsealPayload(key, ch, s1)
|
|
56
|
+
expect(seq).toBe(42)
|
|
57
|
+
expect(data).toEqual({ data: 'same' })
|
|
58
|
+
})
|
|
61
59
|
|
|
62
60
|
it('same seq with different data produces different ciphertext but same nonce (dangerous without seq check)', () => {
|
|
63
61
|
// This demonstrates WHY the protocol must enforce strictly increasing seq:
|
|
64
62
|
// if two different plaintexts share the same nonce, the XOR of the two
|
|
65
63
|
// ciphertexts leaks the XOR of the two plaintexts.
|
|
66
64
|
|
|
67
|
-
const key = new Uint8Array(32)
|
|
68
|
-
crypto.getRandomValues(key)
|
|
69
|
-
const ch = generateChannelId()
|
|
65
|
+
const key = new Uint8Array(32)
|
|
66
|
+
crypto.getRandomValues(key)
|
|
67
|
+
const ch = generateChannelId()
|
|
70
68
|
|
|
71
|
-
const s1 = sealPayload(key, ch, 0, { secret: 'alpha' })
|
|
72
|
-
const s2 = sealPayload(key, ch, 0, { secret: 'bravo' })
|
|
69
|
+
const s1 = sealPayload(key, ch, 0, { secret: 'alpha' })
|
|
70
|
+
const s2 = sealPayload(key, ch, 0, { secret: 'bravo' })
|
|
73
71
|
|
|
74
72
|
// Different plaintext -> different ciphertext (even with same nonce)
|
|
75
|
-
expect(s1).not.toBe(s2)
|
|
73
|
+
expect(s1).not.toBe(s2)
|
|
76
74
|
|
|
77
75
|
// Both decrypt individually (attacker with key could see both)
|
|
78
|
-
expect(unsealPayload(key, ch, s1).data).toEqual({ secret: 'alpha' })
|
|
79
|
-
expect(unsealPayload(key, ch, s2).data).toEqual({ secret: 'bravo' })
|
|
80
|
-
})
|
|
81
|
-
})
|
|
76
|
+
expect(unsealPayload(key, ch, s1).data).toEqual({ secret: 'alpha' })
|
|
77
|
+
expect(unsealPayload(key, ch, s2).data).toEqual({ secret: 'bravo' })
|
|
78
|
+
})
|
|
79
|
+
})
|
|
82
80
|
|
|
83
81
|
// ---------------------------------------------------------------------------
|
|
84
82
|
// Attack 2: Bit-flip in ciphertext (AEAD integrity)
|
|
@@ -93,37 +91,37 @@ describe('Crypto Attack: Ciphertext bit-flip', () => {
|
|
|
93
91
|
// PREVENTS: Ciphertext malleability. Attacker cannot modify
|
|
94
92
|
// encrypted data without detection.
|
|
95
93
|
|
|
96
|
-
const key = new Uint8Array(32)
|
|
97
|
-
crypto.getRandomValues(key)
|
|
98
|
-
const ch = generateChannelId()
|
|
94
|
+
const key = new Uint8Array(32)
|
|
95
|
+
crypto.getRandomValues(key)
|
|
96
|
+
const ch = generateChannelId()
|
|
99
97
|
|
|
100
|
-
const sealed = sealPayload(key, ch, 0, { amount: '1000000', to: '0xVictim' })
|
|
101
|
-
const bytes = b64urlDecode(sealed)
|
|
98
|
+
const sealed = sealPayload(key, ch, 0, { amount: '1000000', to: '0xVictim' })
|
|
99
|
+
const bytes = b64urlDecode(sealed)
|
|
102
100
|
|
|
103
101
|
// Flip a bit in the ciphertext body (byte 10, well past the 4-byte seq)
|
|
104
|
-
const tampered = new Uint8Array(bytes)
|
|
105
|
-
tampered[10] = tampered[10]
|
|
102
|
+
const tampered = new Uint8Array(bytes)
|
|
103
|
+
tampered[10] = (tampered[10] ?? 0) ^ 0x01
|
|
106
104
|
|
|
107
|
-
expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow()
|
|
108
|
-
})
|
|
105
|
+
expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow()
|
|
106
|
+
})
|
|
109
107
|
|
|
110
108
|
it('bit flip in Poly1305 tag causes decryption failure', () => {
|
|
111
109
|
// ATTACK: Attacker flips the last byte (part of the 16-byte tag).
|
|
112
110
|
//
|
|
113
111
|
// PREVENTS: Tag forgery.
|
|
114
112
|
|
|
115
|
-
const key = new Uint8Array(32)
|
|
116
|
-
crypto.getRandomValues(key)
|
|
117
|
-
const ch = generateChannelId()
|
|
113
|
+
const key = new Uint8Array(32)
|
|
114
|
+
crypto.getRandomValues(key)
|
|
115
|
+
const ch = generateChannelId()
|
|
118
116
|
|
|
119
|
-
const sealed = sealPayload(key, ch, 0, { data: 'protected' })
|
|
120
|
-
const bytes = b64urlDecode(sealed)
|
|
117
|
+
const sealed = sealPayload(key, ch, 0, { data: 'protected' })
|
|
118
|
+
const bytes = b64urlDecode(sealed)
|
|
121
119
|
|
|
122
|
-
const tampered = new Uint8Array(bytes)
|
|
123
|
-
tampered[tampered.length - 1] = tampered[tampered.length - 1]
|
|
120
|
+
const tampered = new Uint8Array(bytes)
|
|
121
|
+
tampered[tampered.length - 1] = (tampered[tampered.length - 1] ?? 0) ^ 0xff
|
|
124
122
|
|
|
125
|
-
expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow()
|
|
126
|
-
})
|
|
123
|
+
expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow()
|
|
124
|
+
})
|
|
127
125
|
|
|
128
126
|
it('bit flip in seq bytes changes nonce and causes decryption failure', () => {
|
|
129
127
|
// ATTACK: Attacker modifies the seq bytes (first 4 bytes of sealed).
|
|
@@ -131,19 +129,19 @@ describe('Crypto Attack: Ciphertext bit-flip', () => {
|
|
|
131
129
|
//
|
|
132
130
|
// PREVENTS: Sequence number tampering.
|
|
133
131
|
|
|
134
|
-
const key = new Uint8Array(32)
|
|
135
|
-
crypto.getRandomValues(key)
|
|
136
|
-
const ch = generateChannelId()
|
|
132
|
+
const key = new Uint8Array(32)
|
|
133
|
+
crypto.getRandomValues(key)
|
|
134
|
+
const ch = generateChannelId()
|
|
137
135
|
|
|
138
|
-
const sealed = sealPayload(key, ch, 7, { msg: 'test' })
|
|
139
|
-
const bytes = b64urlDecode(sealed)
|
|
136
|
+
const sealed = sealPayload(key, ch, 7, { msg: 'test' })
|
|
137
|
+
const bytes = b64urlDecode(sealed)
|
|
140
138
|
|
|
141
|
-
const tampered = new Uint8Array(bytes)
|
|
142
|
-
tampered[3] = tampered[3]
|
|
139
|
+
const tampered = new Uint8Array(bytes)
|
|
140
|
+
tampered[3] = (tampered[3] ?? 0) ^ 0x01 // flip low bit of seq
|
|
143
141
|
|
|
144
|
-
expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow()
|
|
145
|
-
})
|
|
146
|
-
})
|
|
142
|
+
expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow()
|
|
143
|
+
})
|
|
144
|
+
})
|
|
147
145
|
|
|
148
146
|
// ---------------------------------------------------------------------------
|
|
149
147
|
// Attack 3: Bit-flip in AAD fields
|
|
@@ -157,17 +155,17 @@ describe('Crypto Attack: AAD field manipulation', () => {
|
|
|
157
155
|
//
|
|
158
156
|
// PREVENTS: Type confusion attacks where a req is reinterpreted as res.
|
|
159
157
|
|
|
160
|
-
const key = new Uint8Array(32)
|
|
161
|
-
crypto.getRandomValues(key)
|
|
162
|
-
const ch = generateChannelId()
|
|
158
|
+
const key = new Uint8Array(32)
|
|
159
|
+
crypto.getRandomValues(key)
|
|
160
|
+
const ch = generateChannelId()
|
|
163
161
|
|
|
164
|
-
const hdr: AadHeader = { type: 'req', from: 'dapp-key', id: 'req-1' }
|
|
165
|
-
const sealed = sealPayload(key, ch, 0, { _method: 'wallet_signMessage' }, hdr)
|
|
162
|
+
const hdr: AadHeader = { type: 'req', from: 'dapp-key', id: 'req-1' }
|
|
163
|
+
const sealed = sealPayload(key, ch, 0, { _method: 'wallet_signMessage' }, hdr)
|
|
166
164
|
|
|
167
165
|
// Try to decrypt with 'res' type — AAD mismatch
|
|
168
|
-
const wrongHdr: AadHeader = { type: 'res', from: 'dapp-key', id: 'req-1' }
|
|
169
|
-
expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
|
|
170
|
-
})
|
|
166
|
+
const wrongHdr: AadHeader = { type: 'res', from: 'dapp-key', id: 'req-1' }
|
|
167
|
+
expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
|
|
168
|
+
})
|
|
171
169
|
|
|
172
170
|
it('changing from in AAD causes decryption failure', () => {
|
|
173
171
|
// ATTACK: Relay substitutes the from field in the envelope.
|
|
@@ -175,16 +173,16 @@ describe('Crypto Attack: AAD field manipulation', () => {
|
|
|
175
173
|
//
|
|
176
174
|
// PREVENTS: Sender impersonation at the AEAD level.
|
|
177
175
|
|
|
178
|
-
const key = new Uint8Array(32)
|
|
179
|
-
crypto.getRandomValues(key)
|
|
180
|
-
const ch = generateChannelId()
|
|
176
|
+
const key = new Uint8Array(32)
|
|
177
|
+
crypto.getRandomValues(key)
|
|
178
|
+
const ch = generateChannelId()
|
|
181
179
|
|
|
182
|
-
const hdr: AadHeader = { type: 'req', from: 'real-dapp-key', id: 'r1' }
|
|
183
|
-
const sealed = sealPayload(key, ch, 0, { _method: 'test' }, hdr)
|
|
180
|
+
const hdr: AadHeader = { type: 'req', from: 'real-dapp-key', id: 'r1' }
|
|
181
|
+
const sealed = sealPayload(key, ch, 0, { _method: 'test' }, hdr)
|
|
184
182
|
|
|
185
|
-
const spoofed: AadHeader = { type: 'req', from: 'relay-fake-key', id: 'r1' }
|
|
186
|
-
expect(() => unsealPayload(key, ch, sealed, spoofed)).toThrow()
|
|
187
|
-
})
|
|
183
|
+
const spoofed: AadHeader = { type: 'req', from: 'relay-fake-key', id: 'r1' }
|
|
184
|
+
expect(() => unsealPayload(key, ch, sealed, spoofed)).toThrow()
|
|
185
|
+
})
|
|
188
186
|
|
|
189
187
|
it('changing id in AAD causes decryption failure', () => {
|
|
190
188
|
// ATTACK: Relay swaps the request ID to redirect a response to
|
|
@@ -192,16 +190,16 @@ describe('Crypto Attack: AAD field manipulation', () => {
|
|
|
192
190
|
//
|
|
193
191
|
// PREVENTS: Request ID substitution.
|
|
194
192
|
|
|
195
|
-
const key = new Uint8Array(32)
|
|
196
|
-
crypto.getRandomValues(key)
|
|
197
|
-
const ch = generateChannelId()
|
|
193
|
+
const key = new Uint8Array(32)
|
|
194
|
+
crypto.getRandomValues(key)
|
|
195
|
+
const ch = generateChannelId()
|
|
198
196
|
|
|
199
|
-
const hdr: AadHeader = { type: 'res', from: 'wallet-key', id: 'req-42' }
|
|
200
|
-
const sealed = sealPayload(key, ch, 0, { _ok: true, _result: 'secret' }, hdr)
|
|
197
|
+
const hdr: AadHeader = { type: 'res', from: 'wallet-key', id: 'req-42' }
|
|
198
|
+
const sealed = sealPayload(key, ch, 0, { _ok: true, _result: 'secret' }, hdr)
|
|
201
199
|
|
|
202
|
-
const swapped: AadHeader = { type: 'res', from: 'wallet-key', id: 'req-1' }
|
|
203
|
-
expect(() => unsealPayload(key, ch, sealed, swapped)).toThrow()
|
|
204
|
-
})
|
|
200
|
+
const swapped: AadHeader = { type: 'res', from: 'wallet-key', id: 'req-1' }
|
|
201
|
+
expect(() => unsealPayload(key, ch, sealed, swapped)).toThrow()
|
|
202
|
+
})
|
|
205
203
|
|
|
206
204
|
it('changing channel ID causes decryption failure', () => {
|
|
207
205
|
// ATTACK: Relay forwards a message from one channel to another.
|
|
@@ -209,15 +207,15 @@ describe('Crypto Attack: AAD field manipulation', () => {
|
|
|
209
207
|
//
|
|
210
208
|
// PREVENTS: Cross-channel message injection.
|
|
211
209
|
|
|
212
|
-
const key = new Uint8Array(32)
|
|
213
|
-
crypto.getRandomValues(key)
|
|
214
|
-
const ch1 = generateChannelId()
|
|
215
|
-
const ch2 = generateChannelId()
|
|
210
|
+
const key = new Uint8Array(32)
|
|
211
|
+
crypto.getRandomValues(key)
|
|
212
|
+
const ch1 = generateChannelId()
|
|
213
|
+
const ch2 = generateChannelId()
|
|
216
214
|
|
|
217
|
-
const sealed = sealPayload(key, ch1, 0, { data: 'channel-1' })
|
|
218
|
-
expect(() => unsealPayload(key, ch2, sealed)).toThrow()
|
|
219
|
-
})
|
|
220
|
-
})
|
|
215
|
+
const sealed = sealPayload(key, ch1, 0, { data: 'channel-1' })
|
|
216
|
+
expect(() => unsealPayload(key, ch2, sealed)).toThrow()
|
|
217
|
+
})
|
|
218
|
+
})
|
|
221
219
|
|
|
222
220
|
// ---------------------------------------------------------------------------
|
|
223
221
|
// Attack 4: Wrong traffic key direction
|
|
@@ -232,57 +230,63 @@ describe('Crypto Attack: Directional key confusion', () => {
|
|
|
232
230
|
//
|
|
233
231
|
// PREVENTS: Direction reversal and reflection attacks (Section 6.2).
|
|
234
232
|
|
|
235
|
-
const dappKp = generateX25519KeyPair()
|
|
236
|
-
const walletKp = generateX25519KeyPair()
|
|
237
|
-
const ch = generateChannelId()
|
|
233
|
+
const dappKp = generateX25519KeyPair()
|
|
234
|
+
const walletKp = generateX25519KeyPair()
|
|
235
|
+
const ch = generateChannelId()
|
|
238
236
|
|
|
239
|
-
const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
|
|
240
|
-
const rootKey = deriveSessionKey(shared, ch)
|
|
237
|
+
const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
|
|
238
|
+
const rootKey = deriveSessionKey(shared, ch)
|
|
241
239
|
const ctx: SessionCryptoContext = {
|
|
242
240
|
dappPubKeyB64: dappKp.publicKeyB64,
|
|
243
241
|
walletPubKeyB64: walletKp.publicKeyB64,
|
|
244
242
|
capabilities: { methods: ['test'], events: [], chains: [] },
|
|
245
243
|
dappName: 'Test',
|
|
246
|
-
}
|
|
247
|
-
const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
|
|
244
|
+
}
|
|
245
|
+
const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
|
|
248
246
|
|
|
249
247
|
// Encrypt with dappToWalletKey
|
|
250
|
-
const sealed = sealPayload(keys.dappToWalletKey, ch, 0, { secret: 'hello' })
|
|
248
|
+
const sealed = sealPayload(keys.dappToWalletKey, ch, 0, { secret: 'hello' })
|
|
251
249
|
|
|
252
250
|
// Cannot decrypt with walletToDappKey
|
|
253
|
-
expect(() => unsealPayload(keys.walletToDappKey, ch, sealed)).toThrow()
|
|
251
|
+
expect(() => unsealPayload(keys.walletToDappKey, ch, sealed)).toThrow()
|
|
254
252
|
|
|
255
253
|
// Can decrypt with correct key
|
|
256
|
-
const { data } = unsealPayload(keys.dappToWalletKey, ch, sealed)
|
|
257
|
-
expect(data).toEqual({ secret: 'hello' })
|
|
258
|
-
})
|
|
254
|
+
const { data } = unsealPayload(keys.dappToWalletKey, ch, sealed)
|
|
255
|
+
expect(data).toEqual({ secret: 'hello' })
|
|
256
|
+
})
|
|
259
257
|
|
|
260
258
|
it('wallet response encrypted with dappToWallet key cannot be decrypted by dApp', () => {
|
|
261
259
|
// If a wallet mistakenly uses the wrong key direction for responses,
|
|
262
260
|
// the dApp cannot decrypt them.
|
|
263
261
|
|
|
264
|
-
const dappKp = generateX25519KeyPair()
|
|
265
|
-
const walletKp = generateX25519KeyPair()
|
|
266
|
-
const ch = generateChannelId()
|
|
262
|
+
const dappKp = generateX25519KeyPair()
|
|
263
|
+
const walletKp = generateX25519KeyPair()
|
|
264
|
+
const ch = generateChannelId()
|
|
267
265
|
|
|
268
|
-
const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
|
|
269
|
-
const rootKey = deriveSessionKey(shared, ch)
|
|
266
|
+
const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
|
|
267
|
+
const rootKey = deriveSessionKey(shared, ch)
|
|
270
268
|
const ctx: SessionCryptoContext = {
|
|
271
269
|
dappPubKeyB64: dappKp.publicKeyB64,
|
|
272
270
|
walletPubKeyB64: walletKp.publicKeyB64,
|
|
273
271
|
capabilities: null,
|
|
274
272
|
dappName: 'Test',
|
|
275
|
-
}
|
|
276
|
-
const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
|
|
273
|
+
}
|
|
274
|
+
const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
|
|
277
275
|
|
|
278
276
|
// Wallet "accidentally" encrypts response with dappToWalletKey (wrong direction)
|
|
279
|
-
const hdr: AadHeader = { type: 'res', from: walletKp.publicKeyB64, id: 'r1' }
|
|
280
|
-
const sealedWrong = sealPayload(
|
|
277
|
+
const hdr: AadHeader = { type: 'res', from: walletKp.publicKeyB64, id: 'r1' }
|
|
278
|
+
const sealedWrong = sealPayload(
|
|
279
|
+
keys.dappToWalletKey,
|
|
280
|
+
ch,
|
|
281
|
+
0,
|
|
282
|
+
{ _ok: true, _result: 'oops' },
|
|
283
|
+
hdr,
|
|
284
|
+
)
|
|
281
285
|
|
|
282
286
|
// DApp tries to decrypt with walletToDappKey (correct for responses)
|
|
283
|
-
expect(() => unsealPayload(keys.walletToDappKey, ch, sealedWrong, hdr)).toThrow()
|
|
284
|
-
})
|
|
285
|
-
})
|
|
287
|
+
expect(() => unsealPayload(keys.walletToDappKey, ch, sealedWrong, hdr)).toThrow()
|
|
288
|
+
})
|
|
289
|
+
})
|
|
286
290
|
|
|
287
291
|
// ---------------------------------------------------------------------------
|
|
288
292
|
// Attack 5: All-zero shared secret (low-order point)
|
|
@@ -298,23 +302,23 @@ describe('Crypto Attack: Low-order point rejection', () => {
|
|
|
298
302
|
// PREVENTS: Key exchange with degenerate public keys that produce
|
|
299
303
|
// trivial shared secrets (RFC 7748 Section 6, Protocol Section 6.2).
|
|
300
304
|
|
|
301
|
-
const myKp = generateX25519KeyPair()
|
|
305
|
+
const myKp = generateX25519KeyPair()
|
|
302
306
|
|
|
303
307
|
// Known low-order point: the all-zero point
|
|
304
|
-
const allZeroKey = new Uint8Array(32)
|
|
308
|
+
const allZeroKey = new Uint8Array(32)
|
|
305
309
|
|
|
306
310
|
// The noble library may throw its own error before our all-zero check,
|
|
307
311
|
// or our code throws "all-zero shared secret". Either way, it must throw.
|
|
308
|
-
expect(() => computeSharedSecret(myKp.privateKey, allZeroKey)).toThrow()
|
|
309
|
-
})
|
|
312
|
+
expect(() => computeSharedSecret(myKp.privateKey, allZeroKey)).toThrow()
|
|
313
|
+
})
|
|
310
314
|
|
|
311
315
|
it('another low-order point (order 2: point at x=1) is rejected', () => {
|
|
312
316
|
// The point with x-coordinate = 1 has order 2 on Curve25519.
|
|
313
317
|
// X25519 with this point should produce all-zero output.
|
|
314
318
|
|
|
315
|
-
const myKp = generateX25519KeyPair()
|
|
316
|
-
const lowOrderPoint = new Uint8Array(32)
|
|
317
|
-
lowOrderPoint[0] = 1
|
|
319
|
+
const myKp = generateX25519KeyPair()
|
|
320
|
+
const lowOrderPoint = new Uint8Array(32)
|
|
321
|
+
lowOrderPoint[0] = 1 // x = 1
|
|
318
322
|
|
|
319
323
|
// This should either throw due to all-zero result or be safe
|
|
320
324
|
// depending on the X25519 implementation. The important thing
|
|
@@ -322,37 +326,31 @@ describe('Crypto Attack: Low-order point rejection', () => {
|
|
|
322
326
|
// The noble library may reject this at the X25519 level or our
|
|
323
327
|
// code rejects the all-zero output. Either way, it must throw.
|
|
324
328
|
try {
|
|
325
|
-
computeSharedSecret(myKp.privateKey, lowOrderPoint)
|
|
329
|
+
computeSharedSecret(myKp.privateKey, lowOrderPoint)
|
|
326
330
|
// If it didn't throw, the implementation clamped and produced
|
|
327
331
|
// non-zero output — this is also acceptable behavior.
|
|
328
332
|
} catch {
|
|
329
333
|
// Expected: low-order point rejected
|
|
330
334
|
}
|
|
331
|
-
})
|
|
335
|
+
})
|
|
332
336
|
|
|
333
337
|
it('remote public key must be exactly 32 bytes', () => {
|
|
334
338
|
// ATTACK: Attacker sends a malformed public key (wrong length).
|
|
335
339
|
//
|
|
336
340
|
// PREVENTS: Buffer overflows or unexpected behavior from wrong-sized keys.
|
|
337
341
|
|
|
338
|
-
const myKp = generateX25519KeyPair()
|
|
342
|
+
const myKp = generateX25519KeyPair()
|
|
339
343
|
|
|
340
344
|
// Too short
|
|
341
|
-
expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(16))).toThrow(
|
|
342
|
-
'32 bytes',
|
|
343
|
-
);
|
|
345
|
+
expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(16))).toThrow('32 bytes')
|
|
344
346
|
|
|
345
347
|
// Too long
|
|
346
|
-
expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(64))).toThrow(
|
|
347
|
-
'32 bytes',
|
|
348
|
-
);
|
|
348
|
+
expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(64))).toThrow('32 bytes')
|
|
349
349
|
|
|
350
350
|
// Empty
|
|
351
|
-
expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(0))).toThrow(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
});
|
|
355
|
-
});
|
|
351
|
+
expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(0))).toThrow('32 bytes')
|
|
352
|
+
})
|
|
353
|
+
})
|
|
356
354
|
|
|
357
355
|
// ---------------------------------------------------------------------------
|
|
358
356
|
// Attack 6: Modified sealed_join
|
|
@@ -366,25 +364,30 @@ describe('Crypto Attack: Sealed join tampering', () => {
|
|
|
366
364
|
// PREVENTS: Capability injection — relay cannot grant methods that
|
|
367
365
|
// the wallet did not authorize.
|
|
368
366
|
|
|
369
|
-
const dappKp = generateX25519KeyPair()
|
|
370
|
-
const walletKp = generateX25519KeyPair()
|
|
371
|
-
const ch = generateChannelId()
|
|
367
|
+
const dappKp = generateX25519KeyPair()
|
|
368
|
+
const walletKp = generateX25519KeyPair()
|
|
369
|
+
const ch = generateChannelId()
|
|
372
370
|
|
|
373
|
-
const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey)
|
|
374
|
-
const rootKey = deriveSessionKey(shared, ch)
|
|
375
|
-
const joinKey = deriveJoinEncryptionKey(rootKey, ch)
|
|
371
|
+
const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey)
|
|
372
|
+
const rootKey = deriveSessionKey(shared, ch)
|
|
373
|
+
const joinKey = deriveJoinEncryptionKey(rootKey, ch)
|
|
376
374
|
|
|
377
|
-
const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
|
|
378
|
-
const meta = {
|
|
379
|
-
|
|
375
|
+
const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
|
|
376
|
+
const meta = {
|
|
377
|
+
name: 'W',
|
|
378
|
+
description: 'W',
|
|
379
|
+
url: 'https://w.test',
|
|
380
|
+
icon: 'https://w.test/i.png',
|
|
381
|
+
}
|
|
382
|
+
const sealed = sealJoin(joinKey, ch, caps, meta)
|
|
380
383
|
|
|
381
384
|
// Tamper with the ciphertext
|
|
382
|
-
const bytes = b64urlDecode(sealed)
|
|
383
|
-
const tampered = new Uint8Array(bytes)
|
|
384
|
-
tampered[20] = tampered[20]
|
|
385
|
+
const bytes = b64urlDecode(sealed)
|
|
386
|
+
const tampered = new Uint8Array(bytes)
|
|
387
|
+
tampered[20] = (tampered[20] ?? 0) ^ 0xff // flip a byte in ciphertext
|
|
385
388
|
|
|
386
|
-
expect(() => unsealJoin(joinKey, ch, b64urlEncode(tampered))).toThrow()
|
|
387
|
-
})
|
|
389
|
+
expect(() => unsealJoin(joinKey, ch, b64urlEncode(tampered))).toThrow()
|
|
390
|
+
})
|
|
388
391
|
|
|
389
392
|
it('sealed_join with wrong channel ID fails decryption', () => {
|
|
390
393
|
// ATTACK: Relay forwards a sealed_join from one channel to another.
|
|
@@ -392,21 +395,21 @@ describe('Crypto Attack: Sealed join tampering', () => {
|
|
|
392
395
|
//
|
|
393
396
|
// PREVENTS: Cross-channel sealed_join replay.
|
|
394
397
|
|
|
395
|
-
const dappKp = generateX25519KeyPair()
|
|
396
|
-
const walletKp = generateX25519KeyPair()
|
|
397
|
-
const ch1 = generateChannelId()
|
|
398
|
-
const ch2 = generateChannelId()
|
|
398
|
+
const dappKp = generateX25519KeyPair()
|
|
399
|
+
const walletKp = generateX25519KeyPair()
|
|
400
|
+
const ch1 = generateChannelId()
|
|
401
|
+
const ch2 = generateChannelId()
|
|
399
402
|
|
|
400
|
-
const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey)
|
|
401
|
-
const rootKey = deriveSessionKey(shared, ch1)
|
|
402
|
-
const joinKey = deriveJoinEncryptionKey(rootKey, ch1)
|
|
403
|
+
const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey)
|
|
404
|
+
const rootKey = deriveSessionKey(shared, ch1)
|
|
405
|
+
const joinKey = deriveJoinEncryptionKey(rootKey, ch1)
|
|
403
406
|
|
|
404
|
-
const caps = { methods: ['wallet_signMessage'], events: [], chains: [] }
|
|
405
|
-
const sealed = sealJoin(joinKey, ch1, caps)
|
|
407
|
+
const caps = { methods: ['wallet_signMessage'], events: [], chains: [] }
|
|
408
|
+
const sealed = sealJoin(joinKey, ch1, caps)
|
|
406
409
|
|
|
407
410
|
// Same key but different channel ID → AAD mismatch
|
|
408
|
-
expect(() => unsealJoin(joinKey, ch2, sealed)).toThrow()
|
|
409
|
-
})
|
|
411
|
+
expect(() => unsealJoin(joinKey, ch2, sealed)).toThrow()
|
|
412
|
+
})
|
|
410
413
|
|
|
411
414
|
it('modified sealed_join produces different transcript hash and traffic keys', () => {
|
|
412
415
|
// If sealed_join content were somehow different (e.g., different
|
|
@@ -416,12 +419,12 @@ describe('Crypto Attack: Sealed join tampering', () => {
|
|
|
416
419
|
//
|
|
417
420
|
// PREVENTS: Capability downgrade attack at the transcript level.
|
|
418
421
|
|
|
419
|
-
const dappKp = generateX25519KeyPair()
|
|
420
|
-
const walletKp = generateX25519KeyPair()
|
|
421
|
-
const ch = generateChannelId()
|
|
422
|
+
const dappKp = generateX25519KeyPair()
|
|
423
|
+
const walletKp = generateX25519KeyPair()
|
|
424
|
+
const ch = generateChannelId()
|
|
422
425
|
|
|
423
|
-
const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
|
|
424
|
-
const rootKey = deriveSessionKey(shared, ch)
|
|
426
|
+
const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
|
|
427
|
+
const rootKey = deriveSessionKey(shared, ch)
|
|
425
428
|
|
|
426
429
|
// Transcript with capabilities A
|
|
427
430
|
const ctx1: SessionCryptoContext = {
|
|
@@ -429,44 +432,48 @@ describe('Crypto Attack: Sealed join tampering', () => {
|
|
|
429
432
|
walletPubKeyB64: walletKp.publicKeyB64,
|
|
430
433
|
capabilities: { methods: ['wallet_signMessage'], events: [], chains: [] },
|
|
431
434
|
dappName: 'App',
|
|
432
|
-
}
|
|
433
|
-
const keys1 = deriveDirectionalSessionKeys(new Uint8Array(rootKey), ch, ctx1)
|
|
435
|
+
}
|
|
436
|
+
const keys1 = deriveDirectionalSessionKeys(new Uint8Array(rootKey), ch, ctx1)
|
|
434
437
|
|
|
435
438
|
// Transcript with capabilities B (relay tried to modify sealed_join)
|
|
436
439
|
const ctx2: SessionCryptoContext = {
|
|
437
440
|
dappPubKeyB64: dappKp.publicKeyB64,
|
|
438
441
|
walletPubKeyB64: walletKp.publicKeyB64,
|
|
439
|
-
capabilities: {
|
|
442
|
+
capabilities: {
|
|
443
|
+
methods: ['wallet_signMessage', 'wallet_drainFunds'],
|
|
444
|
+
events: [],
|
|
445
|
+
chains: [],
|
|
446
|
+
},
|
|
440
447
|
dappName: 'App',
|
|
441
|
-
}
|
|
442
|
-
const keys2 = deriveDirectionalSessionKeys(new Uint8Array(rootKey), ch, ctx2)
|
|
448
|
+
}
|
|
449
|
+
const keys2 = deriveDirectionalSessionKeys(new Uint8Array(rootKey), ch, ctx2)
|
|
443
450
|
|
|
444
451
|
// Traffic keys must differ — peers would not be able to communicate
|
|
445
|
-
expect(bytesToHex(keys1.dappToWalletKey)).not.toBe(bytesToHex(keys2.dappToWalletKey))
|
|
446
|
-
expect(bytesToHex(keys1.walletToDappKey)).not.toBe(bytesToHex(keys2.walletToDappKey))
|
|
447
|
-
expect(bytesToHex(keys1.transcriptHash)).not.toBe(bytesToHex(keys2.transcriptHash))
|
|
448
|
-
})
|
|
452
|
+
expect(bytesToHex(keys1.dappToWalletKey)).not.toBe(bytesToHex(keys2.dappToWalletKey))
|
|
453
|
+
expect(bytesToHex(keys1.walletToDappKey)).not.toBe(bytesToHex(keys2.walletToDappKey))
|
|
454
|
+
expect(bytesToHex(keys1.transcriptHash)).not.toBe(bytesToHex(keys2.transcriptHash))
|
|
455
|
+
})
|
|
449
456
|
|
|
450
457
|
it('sealed_join with truncated data fails', () => {
|
|
451
458
|
// ATTACK: Relay truncates the sealed_join envelope.
|
|
452
459
|
|
|
453
|
-
const dappKp = generateX25519KeyPair()
|
|
454
|
-
const walletKp = generateX25519KeyPair()
|
|
455
|
-
const ch = generateChannelId()
|
|
460
|
+
const dappKp = generateX25519KeyPair()
|
|
461
|
+
const walletKp = generateX25519KeyPair()
|
|
462
|
+
const ch = generateChannelId()
|
|
456
463
|
|
|
457
|
-
const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey)
|
|
458
|
-
const rootKey = deriveSessionKey(shared, ch)
|
|
459
|
-
const joinKey = deriveJoinEncryptionKey(rootKey, ch)
|
|
464
|
+
const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey)
|
|
465
|
+
const rootKey = deriveSessionKey(shared, ch)
|
|
466
|
+
const joinKey = deriveJoinEncryptionKey(rootKey, ch)
|
|
460
467
|
|
|
461
|
-
const caps = { methods: ['test'], events: [], chains: [] }
|
|
462
|
-
const sealed = sealJoin(joinKey, ch, caps)
|
|
463
|
-
const bytes = b64urlDecode(sealed)
|
|
468
|
+
const caps = { methods: ['test'], events: [], chains: [] }
|
|
469
|
+
const sealed = sealJoin(joinKey, ch, caps)
|
|
470
|
+
const bytes = b64urlDecode(sealed)
|
|
464
471
|
|
|
465
472
|
// Truncate to just nonce (12 bytes) + partial ciphertext
|
|
466
|
-
const truncated = b64urlEncode(bytes.slice(0, 20))
|
|
467
|
-
expect(() => unsealJoin(joinKey, ch, truncated)).toThrow()
|
|
468
|
-
})
|
|
469
|
-
})
|
|
473
|
+
const truncated = b64urlEncode(bytes.slice(0, 20))
|
|
474
|
+
expect(() => unsealJoin(joinKey, ch, truncated)).toThrow()
|
|
475
|
+
})
|
|
476
|
+
})
|
|
470
477
|
|
|
471
478
|
// ---------------------------------------------------------------------------
|
|
472
479
|
// Attack 7: Cross-session key isolation
|
|
@@ -481,43 +488,43 @@ describe('Crypto Attack: Cross-session key isolation', () => {
|
|
|
481
488
|
//
|
|
482
489
|
// PREVENTS: Cross-session key reuse.
|
|
483
490
|
|
|
484
|
-
const dappKp = generateX25519KeyPair()
|
|
485
|
-
const walletKp = generateX25519KeyPair()
|
|
486
|
-
const ch1 = generateChannelId()
|
|
487
|
-
const ch2 = generateChannelId()
|
|
491
|
+
const dappKp = generateX25519KeyPair()
|
|
492
|
+
const walletKp = generateX25519KeyPair()
|
|
493
|
+
const ch1 = generateChannelId()
|
|
494
|
+
const ch2 = generateChannelId()
|
|
488
495
|
|
|
489
|
-
const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
|
|
496
|
+
const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
|
|
490
497
|
|
|
491
|
-
const root1 = deriveSessionKey(shared, ch1)
|
|
492
|
-
const root2 = deriveSessionKey(shared, ch2)
|
|
498
|
+
const root1 = deriveSessionKey(shared, ch1)
|
|
499
|
+
const root2 = deriveSessionKey(shared, ch2)
|
|
493
500
|
|
|
494
501
|
// Root keys differ (different channel ID salt)
|
|
495
|
-
expect(bytesToHex(root1)).not.toBe(bytesToHex(root2))
|
|
502
|
+
expect(bytesToHex(root1)).not.toBe(bytesToHex(root2))
|
|
496
503
|
|
|
497
504
|
const ctx: SessionCryptoContext = {
|
|
498
505
|
dappPubKeyB64: dappKp.publicKeyB64,
|
|
499
506
|
walletPubKeyB64: walletKp.publicKeyB64,
|
|
500
507
|
capabilities: null,
|
|
501
508
|
dappName: 'App',
|
|
502
|
-
}
|
|
509
|
+
}
|
|
503
510
|
|
|
504
|
-
const keys1 = deriveDirectionalSessionKeys(root1, ch1, ctx)
|
|
505
|
-
const keys2 = deriveDirectionalSessionKeys(root2, ch2, ctx)
|
|
511
|
+
const keys1 = deriveDirectionalSessionKeys(root1, ch1, ctx)
|
|
512
|
+
const keys2 = deriveDirectionalSessionKeys(root2, ch2, ctx)
|
|
506
513
|
|
|
507
514
|
// Encrypt with session 1 key
|
|
508
|
-
const sealed = sealPayload(keys1.dappToWalletKey, ch1, 0, { secret: 'session1' })
|
|
515
|
+
const sealed = sealPayload(keys1.dappToWalletKey, ch1, 0, { secret: 'session1' })
|
|
509
516
|
|
|
510
517
|
// Cannot decrypt with session 2 key
|
|
511
|
-
expect(() => unsealPayload(keys2.dappToWalletKey, ch2, sealed)).toThrow()
|
|
518
|
+
expect(() => unsealPayload(keys2.dappToWalletKey, ch2, sealed)).toThrow()
|
|
512
519
|
|
|
513
520
|
// Cannot decrypt with session 2 key + session 1 channel
|
|
514
|
-
expect(() => unsealPayload(keys2.dappToWalletKey, ch1, sealed)).toThrow()
|
|
521
|
+
expect(() => unsealPayload(keys2.dappToWalletKey, ch1, sealed)).toThrow()
|
|
515
522
|
|
|
516
523
|
// Can decrypt with correct key + channel
|
|
517
|
-
const { data } = unsealPayload(keys1.dappToWalletKey, ch1, sealed)
|
|
518
|
-
expect(data).toEqual({ secret: 'session1' })
|
|
519
|
-
})
|
|
520
|
-
})
|
|
524
|
+
const { data } = unsealPayload(keys1.dappToWalletKey, ch1, sealed)
|
|
525
|
+
expect(data).toEqual({ secret: 'session1' })
|
|
526
|
+
})
|
|
527
|
+
})
|
|
521
528
|
|
|
522
529
|
// ---------------------------------------------------------------------------
|
|
523
530
|
// Attack 8: Empty and malformed sealed payloads
|
|
@@ -525,33 +532,33 @@ describe('Crypto Attack: Cross-session key isolation', () => {
|
|
|
525
532
|
|
|
526
533
|
describe('Crypto Attack: Malformed sealed payloads', () => {
|
|
527
534
|
it('empty string sealed payload throws', () => {
|
|
528
|
-
const key = new Uint8Array(32)
|
|
529
|
-
crypto.getRandomValues(key)
|
|
530
|
-
const ch = generateChannelId()
|
|
531
|
-
expect(() => unsealPayload(key, ch, '')).toThrow()
|
|
532
|
-
})
|
|
535
|
+
const key = new Uint8Array(32)
|
|
536
|
+
crypto.getRandomValues(key)
|
|
537
|
+
const ch = generateChannelId()
|
|
538
|
+
expect(() => unsealPayload(key, ch, '')).toThrow()
|
|
539
|
+
})
|
|
533
540
|
|
|
534
541
|
it('sealed payload with only seq bytes (no ciphertext) throws', () => {
|
|
535
|
-
const key = new Uint8Array(32)
|
|
536
|
-
crypto.getRandomValues(key)
|
|
537
|
-
const ch = generateChannelId()
|
|
538
|
-
const onlySeq = b64urlEncode(new Uint8Array(4))
|
|
539
|
-
expect(() => unsealPayload(key, ch, onlySeq)).toThrow()
|
|
540
|
-
})
|
|
542
|
+
const key = new Uint8Array(32)
|
|
543
|
+
crypto.getRandomValues(key)
|
|
544
|
+
const ch = generateChannelId()
|
|
545
|
+
const onlySeq = b64urlEncode(new Uint8Array(4))
|
|
546
|
+
expect(() => unsealPayload(key, ch, onlySeq)).toThrow()
|
|
547
|
+
})
|
|
541
548
|
|
|
542
549
|
it('sealed payload with random garbage throws', () => {
|
|
543
|
-
const key = new Uint8Array(32)
|
|
544
|
-
crypto.getRandomValues(key)
|
|
545
|
-
const ch = generateChannelId()
|
|
546
|
-
const garbage = b64urlEncode(crypto.getRandomValues(new Uint8Array(100)))
|
|
547
|
-
expect(() => unsealPayload(key, ch, garbage)).toThrow()
|
|
548
|
-
})
|
|
550
|
+
const key = new Uint8Array(32)
|
|
551
|
+
crypto.getRandomValues(key)
|
|
552
|
+
const ch = generateChannelId()
|
|
553
|
+
const garbage = b64urlEncode(crypto.getRandomValues(new Uint8Array(100)))
|
|
554
|
+
expect(() => unsealPayload(key, ch, garbage)).toThrow()
|
|
555
|
+
})
|
|
549
556
|
|
|
550
557
|
it('sealed_join with envelope smaller than nonce+tag rejects early', () => {
|
|
551
|
-
const key = new Uint8Array(32)
|
|
552
|
-
crypto.getRandomValues(key)
|
|
553
|
-
const ch = generateChannelId()
|
|
554
|
-
const tiny = b64urlEncode(new Uint8Array(10))
|
|
555
|
-
expect(() => unsealJoin(key, ch, tiny)).toThrow('Invalid sealed_join')
|
|
556
|
-
})
|
|
557
|
-
})
|
|
558
|
+
const key = new Uint8Array(32)
|
|
559
|
+
crypto.getRandomValues(key)
|
|
560
|
+
const ch = generateChannelId()
|
|
561
|
+
const tiny = b64urlEncode(new Uint8Array(10)) // < 12 + 16
|
|
562
|
+
expect(() => unsealJoin(key, ch, tiny)).toThrow('Invalid sealed_join')
|
|
563
|
+
})
|
|
564
|
+
})
|