walletpair-sdk 1.0.0
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/LICENSE +21 -0
- package/README.md +415 -0
- package/dist/ble/framing.d.ts +23 -0
- package/dist/ble/framing.d.ts.map +1 -0
- package/dist/ble/framing.js +83 -0
- package/dist/ble/framing.js.map +1 -0
- package/dist/ble/index.d.ts +9 -0
- package/dist/ble/index.d.ts.map +1 -0
- package/dist/ble/index.js +9 -0
- package/dist/ble/index.js.map +1 -0
- package/dist/ble/web-ble-transport.d.ts +29 -0
- package/dist/ble/web-ble-transport.d.ts.map +1 -0
- package/dist/ble/web-ble-transport.js +93 -0
- package/dist/ble/web-ble-transport.js.map +1 -0
- package/dist/crypto.d.ts +102 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +279 -0
- package/dist/crypto.js.map +1 -0
- package/dist/dapp-session.d.ts +106 -0
- package/dist/dapp-session.d.ts.map +1 -0
- package/dist/dapp-session.js +918 -0
- package/dist/dapp-session.js.map +1 -0
- package/dist/emitter.d.ts +16 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +41 -0
- package/dist/emitter.js.map +1 -0
- package/dist/evm/eip1193.d.ts +83 -0
- package/dist/evm/eip1193.d.ts.map +1 -0
- package/dist/evm/eip1193.js +270 -0
- package/dist/evm/eip1193.js.map +1 -0
- package/dist/evm/index.d.ts +8 -0
- package/dist/evm/index.d.ts.map +1 -0
- package/dist/evm/index.js +8 -0
- package/dist/evm/index.js.map +1 -0
- package/dist/evm/wagmi.d.ts +118 -0
- package/dist/evm/wagmi.d.ts.map +1 -0
- package/dist/evm/wagmi.js +205 -0
- package/dist/evm/wagmi.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +225 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +31 -0
- package/dist/types.js.map +1 -0
- package/dist/wallet-session.d.ts +107 -0
- package/dist/wallet-session.d.ts.map +1 -0
- package/dist/wallet-session.js +794 -0
- package/dist/wallet-session.js.map +1 -0
- package/dist/ws-transport.d.ts +29 -0
- package/dist/ws-transport.d.ts.map +1 -0
- package/dist/ws-transport.js +79 -0
- package/dist/ws-transport.js.map +1 -0
- package/package.json +55 -0
- package/src/__tests__/adversarial/crypto-attacks.test.ts +557 -0
- package/src/__tests__/adversarial/malicious-dapp.test.ts +505 -0
- package/src/__tests__/adversarial/malicious-relay.test.ts +528 -0
- package/src/__tests__/adversarial/malicious-wallet.test.ts +467 -0
- package/src/__tests__/spec-compliance/canonical-json.test.ts +227 -0
- package/src/__tests__/spec-compliance/crypto-vectors.test.ts +321 -0
- package/src/__tests__/spec-compliance/message-format.test.ts +356 -0
- package/src/__tests__/spec-compliance/sequence-numbers.test.ts +300 -0
- package/src/__tests__/spec-compliance/state-machine.test.ts +364 -0
- package/src/ble/framing.test.ts +196 -0
- package/src/ble/framing.ts +100 -0
- package/src/ble/index.ts +18 -0
- package/src/ble/web-ble-transport.test.ts +192 -0
- package/src/ble/web-ble-transport.ts +116 -0
- package/src/ble/web-bluetooth.d.ts +47 -0
- package/src/canonical-json.test.ts +612 -0
- package/src/crypto-directional.test.ts +263 -0
- package/src/crypto-hardening.test.ts +529 -0
- package/src/crypto.test.ts +635 -0
- package/src/crypto.ts +405 -0
- package/src/dapp-session.test.ts +647 -0
- package/src/dapp-session.ts +1004 -0
- package/src/emitter.test.ts +169 -0
- package/src/emitter.ts +45 -0
- package/src/evm/eip1193.test.ts +365 -0
- package/src/evm/eip1193.ts +346 -0
- package/src/evm/index.ts +19 -0
- package/src/evm/wagmi.test.ts +396 -0
- package/src/evm/wagmi.ts +321 -0
- package/src/index.ts +86 -0
- package/src/integration.test.ts +385 -0
- package/src/security.test.ts +430 -0
- package/src/sequence-validation.test.ts +1185 -0
- package/src/test-helpers.ts +216 -0
- package/src/types.test.ts +82 -0
- package/src/types.ts +305 -0
- package/src/wallet-session.test.ts +683 -0
- package/src/wallet-session.ts +922 -0
- package/src/ws-transport.test.ts +231 -0
- package/src/ws-transport.ts +92 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directional session key derivation tests.
|
|
3
|
+
*
|
|
4
|
+
* Covers: deriveDirectionalSessionKeys asymmetry, cross-side consistency,
|
|
5
|
+
* transcript hash determinism, canonicalJson ordering, context sensitivity,
|
|
6
|
+
* and cross-key decryption failures.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import {
|
|
11
|
+
generateX25519KeyPair,
|
|
12
|
+
generateChannelId,
|
|
13
|
+
computeSharedSecret,
|
|
14
|
+
deriveSessionKey,
|
|
15
|
+
sealPayload,
|
|
16
|
+
unsealPayload,
|
|
17
|
+
bytesToHex,
|
|
18
|
+
b64urlEncode,
|
|
19
|
+
} from './crypto.js';
|
|
20
|
+
import {
|
|
21
|
+
deriveDirectionalSessionKeys,
|
|
22
|
+
computeHandshakeTranscriptHash,
|
|
23
|
+
} from './crypto.js';
|
|
24
|
+
import type { SessionCryptoContext } from './crypto.js';
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function makeContext(overrides?: Partial<SessionCryptoContext>): SessionCryptoContext {
|
|
31
|
+
return {
|
|
32
|
+
dappPubKeyB64: overrides?.dappPubKeyB64 ?? 'dappPubKey123',
|
|
33
|
+
walletPubKeyB64: overrides?.walletPubKeyB64 ?? 'walletPubKey456',
|
|
34
|
+
capabilities: overrides?.capabilities ?? { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] },
|
|
35
|
+
walletMeta: overrides?.walletMeta ?? { name: 'TestWallet' },
|
|
36
|
+
dappName: overrides?.dappName ?? 'TestDApp',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeRootKey(): Uint8Array {
|
|
41
|
+
const key = new Uint8Array(32);
|
|
42
|
+
crypto.getRandomValues(key);
|
|
43
|
+
return key;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Tests
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
describe('Directional session keys', () => {
|
|
51
|
+
|
|
52
|
+
it('deriveDirectionalSessionKeys produces different keys for each direction', () => {
|
|
53
|
+
const rootKey = makeRootKey();
|
|
54
|
+
const channelId = generateChannelId();
|
|
55
|
+
const context = makeContext();
|
|
56
|
+
|
|
57
|
+
const keys = deriveDirectionalSessionKeys(rootKey, channelId, context);
|
|
58
|
+
|
|
59
|
+
expect(keys.dappToWalletKey).toHaveLength(32);
|
|
60
|
+
expect(keys.walletToDappKey).toHaveLength(32);
|
|
61
|
+
expect(bytesToHex(keys.dappToWalletKey)).not.toBe(bytesToHex(keys.walletToDappKey));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('dappToWalletKey != walletToDappKey (even with identical root key and context)', () => {
|
|
65
|
+
const rootKey = new Uint8Array(32).fill(0x42);
|
|
66
|
+
const channelId = '00'.repeat(32);
|
|
67
|
+
const context = makeContext();
|
|
68
|
+
|
|
69
|
+
const keys = deriveDirectionalSessionKeys(rootKey, channelId, context);
|
|
70
|
+
|
|
71
|
+
// They must differ because different HKDF info strings are used
|
|
72
|
+
expect(bytesToHex(keys.dappToWalletKey)).not.toBe(bytesToHex(keys.walletToDappKey));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('same keys derived from both sides (dApp and wallet perspectives)', () => {
|
|
76
|
+
// Simulate full key exchange: both sides derive the same shared secret,
|
|
77
|
+
// then the same directional keys.
|
|
78
|
+
const dappKp = generateX25519KeyPair();
|
|
79
|
+
const walletKp = generateX25519KeyPair();
|
|
80
|
+
const channelId = generateChannelId();
|
|
81
|
+
|
|
82
|
+
const capabilities = { methods: ['wallet_getAccounts'], events: ['accountsChanged'], chains: ['eip155:1'] };
|
|
83
|
+
const walletMeta = { name: 'My Wallet' };
|
|
84
|
+
const dappName = 'My dApp';
|
|
85
|
+
|
|
86
|
+
// DApp side
|
|
87
|
+
const sharedDapp = computeSharedSecret(dappKp.privateKey, walletKp.publicKey);
|
|
88
|
+
const rootKeyDapp = deriveSessionKey(sharedDapp, channelId);
|
|
89
|
+
const contextDapp: SessionCryptoContext = {
|
|
90
|
+
dappPubKeyB64: dappKp.publicKeyB64,
|
|
91
|
+
walletPubKeyB64: walletKp.publicKeyB64,
|
|
92
|
+
capabilities,
|
|
93
|
+
walletMeta,
|
|
94
|
+
dappName,
|
|
95
|
+
};
|
|
96
|
+
const keysDapp = deriveDirectionalSessionKeys(rootKeyDapp, channelId, contextDapp);
|
|
97
|
+
|
|
98
|
+
// Wallet side (same context fields, same shared secret via DH symmetry)
|
|
99
|
+
const sharedWallet = computeSharedSecret(walletKp.privateKey, dappKp.publicKey);
|
|
100
|
+
const rootKeyWallet = deriveSessionKey(sharedWallet, channelId);
|
|
101
|
+
const contextWallet: SessionCryptoContext = {
|
|
102
|
+
dappPubKeyB64: dappKp.publicKeyB64,
|
|
103
|
+
walletPubKeyB64: walletKp.publicKeyB64,
|
|
104
|
+
capabilities,
|
|
105
|
+
walletMeta,
|
|
106
|
+
dappName,
|
|
107
|
+
};
|
|
108
|
+
const keysWallet = deriveDirectionalSessionKeys(rootKeyWallet, channelId, contextWallet);
|
|
109
|
+
|
|
110
|
+
// Both sides must agree on all keys
|
|
111
|
+
expect(bytesToHex(keysDapp.dappToWalletKey)).toBe(bytesToHex(keysWallet.dappToWalletKey));
|
|
112
|
+
expect(bytesToHex(keysDapp.walletToDappKey)).toBe(bytesToHex(keysWallet.walletToDappKey));
|
|
113
|
+
expect(bytesToHex(keysDapp.transcriptHash)).toBe(bytesToHex(keysWallet.transcriptHash));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('different capabilities produce different directional keys', () => {
|
|
117
|
+
const rootKey = makeRootKey();
|
|
118
|
+
const channelId = generateChannelId();
|
|
119
|
+
|
|
120
|
+
const context1 = makeContext({ capabilities: { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] } });
|
|
121
|
+
const context2 = makeContext({ capabilities: { methods: ['wallet_signMessage'], events: [], chains: ['eip155:1'] } });
|
|
122
|
+
|
|
123
|
+
const keys1 = deriveDirectionalSessionKeys(rootKey, channelId, context1);
|
|
124
|
+
const keys2 = deriveDirectionalSessionKeys(rootKey, channelId, context2);
|
|
125
|
+
|
|
126
|
+
expect(bytesToHex(keys1.dappToWalletKey)).not.toBe(bytesToHex(keys2.dappToWalletKey));
|
|
127
|
+
expect(bytesToHex(keys1.walletToDappKey)).not.toBe(bytesToHex(keys2.walletToDappKey));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('different walletMeta produce different directional keys', () => {
|
|
131
|
+
const rootKey = makeRootKey();
|
|
132
|
+
const channelId = generateChannelId();
|
|
133
|
+
|
|
134
|
+
const context1 = makeContext({ walletMeta: { name: 'Wallet A' } });
|
|
135
|
+
const context2 = makeContext({ walletMeta: { name: 'Wallet B' } });
|
|
136
|
+
|
|
137
|
+
const keys1 = deriveDirectionalSessionKeys(rootKey, channelId, context1);
|
|
138
|
+
const keys2 = deriveDirectionalSessionKeys(rootKey, channelId, context2);
|
|
139
|
+
|
|
140
|
+
expect(bytesToHex(keys1.dappToWalletKey)).not.toBe(bytesToHex(keys2.dappToWalletKey));
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('computeHandshakeTranscriptHash', () => {
|
|
145
|
+
|
|
146
|
+
it('is deterministic for the same inputs', () => {
|
|
147
|
+
const channelId = generateChannelId();
|
|
148
|
+
const context = makeContext();
|
|
149
|
+
|
|
150
|
+
const hash1 = computeHandshakeTranscriptHash(channelId, context);
|
|
151
|
+
const hash2 = computeHandshakeTranscriptHash(channelId, context);
|
|
152
|
+
|
|
153
|
+
expect(bytesToHex(hash1)).toBe(bytesToHex(hash2));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('canonicalJson ordering is consistent regardless of object key insertion order', () => {
|
|
157
|
+
const channelId = generateChannelId();
|
|
158
|
+
|
|
159
|
+
// Two contexts with same data but different key insertion order
|
|
160
|
+
const context1: SessionCryptoContext = {
|
|
161
|
+
dappPubKeyB64: 'abc',
|
|
162
|
+
walletPubKeyB64: 'def',
|
|
163
|
+
capabilities: { methods: ['a'], events: ['b'], chains: ['c'] },
|
|
164
|
+
walletMeta: { name: 'W', address: '0x1' },
|
|
165
|
+
dappName: 'D',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const context2: SessionCryptoContext = {
|
|
169
|
+
walletPubKeyB64: 'def',
|
|
170
|
+
dappName: 'D',
|
|
171
|
+
dappPubKeyB64: 'abc',
|
|
172
|
+
walletMeta: { address: '0x1', name: 'W' },
|
|
173
|
+
capabilities: { chains: ['c'], events: ['b'], methods: ['a'] },
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const hash1 = computeHandshakeTranscriptHash(channelId, context1);
|
|
177
|
+
const hash2 = computeHandshakeTranscriptHash(channelId, context2);
|
|
178
|
+
|
|
179
|
+
expect(bytesToHex(hash1)).toBe(bytesToHex(hash2));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('changing any context field changes the transcript hash', () => {
|
|
183
|
+
const channelId = generateChannelId();
|
|
184
|
+
const baseContext = makeContext();
|
|
185
|
+
const baseHash = bytesToHex(computeHandshakeTranscriptHash(channelId, baseContext));
|
|
186
|
+
|
|
187
|
+
// Change dappPubKeyB64
|
|
188
|
+
const h1 = bytesToHex(computeHandshakeTranscriptHash(channelId, makeContext({ dappPubKeyB64: 'differentDappKey' })));
|
|
189
|
+
expect(h1).not.toBe(baseHash);
|
|
190
|
+
|
|
191
|
+
// Change walletPubKeyB64
|
|
192
|
+
const h2 = bytesToHex(computeHandshakeTranscriptHash(channelId, makeContext({ walletPubKeyB64: 'differentWalletKey' })));
|
|
193
|
+
expect(h2).not.toBe(baseHash);
|
|
194
|
+
|
|
195
|
+
// Change capabilities
|
|
196
|
+
const h3 = bytesToHex(computeHandshakeTranscriptHash(channelId, makeContext({ capabilities: { methods: ['other_method'], events: [], chains: [] } })));
|
|
197
|
+
expect(h3).not.toBe(baseHash);
|
|
198
|
+
|
|
199
|
+
// Change walletMeta
|
|
200
|
+
const h4 = bytesToHex(computeHandshakeTranscriptHash(channelId, makeContext({ walletMeta: { name: 'DifferentWallet' } })));
|
|
201
|
+
expect(h4).not.toBe(baseHash);
|
|
202
|
+
|
|
203
|
+
// Change dappName
|
|
204
|
+
const h5 = bytesToHex(computeHandshakeTranscriptHash(channelId, makeContext({ dappName: 'DifferentDApp' })));
|
|
205
|
+
expect(h5).not.toBe(baseHash);
|
|
206
|
+
|
|
207
|
+
// Change channelId
|
|
208
|
+
const h6 = bytesToHex(computeHandshakeTranscriptHash(generateChannelId(), baseContext));
|
|
209
|
+
expect(h6).not.toBe(baseHash);
|
|
210
|
+
|
|
211
|
+
// All hashes must be unique
|
|
212
|
+
const allHashes = [baseHash, h1, h2, h3, h4, h5, h6];
|
|
213
|
+
expect(new Set(allHashes).size).toBe(allHashes.length);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('Cross-key decryption', () => {
|
|
218
|
+
const channelId = generateChannelId();
|
|
219
|
+
const rootKey = makeRootKey();
|
|
220
|
+
const context = makeContext();
|
|
221
|
+
const keys = deriveDirectionalSessionKeys(rootKey, channelId, context);
|
|
222
|
+
|
|
223
|
+
it('sealPayload with dappToWalletKey can be unsealed with dappToWalletKey', () => {
|
|
224
|
+
const data = { action: 'test' };
|
|
225
|
+
const sealed = sealPayload(keys.dappToWalletKey, channelId, 0, data);
|
|
226
|
+
const { data: decrypted } = unsealPayload(keys.dappToWalletKey, channelId, sealed);
|
|
227
|
+
expect(decrypted).toEqual(data);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('sealPayload with dappToWalletKey cannot be unsealed with walletToDappKey', () => {
|
|
231
|
+
const data = { secret: 'from-dapp' };
|
|
232
|
+
const sealed = sealPayload(keys.dappToWalletKey, channelId, 0, data);
|
|
233
|
+
|
|
234
|
+
// Attempting to decrypt with the wrong directional key must fail
|
|
235
|
+
expect(() => unsealPayload(keys.walletToDappKey, channelId, sealed)).toThrow();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('cross-key decryption fails: wallet key cannot decrypt dapp-encrypted message', () => {
|
|
239
|
+
const data = { request: 'wallet_getAccounts', params: {} };
|
|
240
|
+
const hdr = { type: 'req' as const, from: 'dapp', id: 'req-1', method: 'wallet_getAccounts' };
|
|
241
|
+
|
|
242
|
+
const sealed = sealPayload(keys.dappToWalletKey, channelId, 0, data, hdr);
|
|
243
|
+
|
|
244
|
+
// walletToDappKey must not be able to open it
|
|
245
|
+
expect(() => unsealPayload(keys.walletToDappKey, channelId, sealed, hdr)).toThrow();
|
|
246
|
+
|
|
247
|
+
// But dappToWalletKey can
|
|
248
|
+
const { data: decrypted } = unsealPayload(keys.dappToWalletKey, channelId, sealed, hdr);
|
|
249
|
+
expect(decrypted).toEqual(data);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('walletToDappKey sealed message cannot be opened with dappToWalletKey', () => {
|
|
253
|
+
const data = { result: ['0xabc'] };
|
|
254
|
+
const hdr = { type: 'res' as const, from: 'wallet', id: 'req-1' };
|
|
255
|
+
|
|
256
|
+
const sealed = sealPayload(keys.walletToDappKey, channelId, 0, data, hdr);
|
|
257
|
+
|
|
258
|
+
expect(() => unsealPayload(keys.dappToWalletKey, channelId, sealed, hdr)).toThrow();
|
|
259
|
+
|
|
260
|
+
const { data: decrypted } = unsealPayload(keys.walletToDappKey, channelId, sealed, hdr);
|
|
261
|
+
expect(decrypted).toEqual(data);
|
|
262
|
+
});
|
|
263
|
+
});
|