Skip to content

Commit 09f8f1e

Browse files
committed
Fix URL encoding of cipher data
1 parent 5652453 commit 09f8f1e

8 files changed

Lines changed: 479 additions & 84 deletions

File tree

packages/client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,4 @@
7777
"tslib": "^2.6.0",
7878
"typescript": "^5.1.6"
7979
}
80-
}
80+
}

packages/client/src/components/communicator/webBased/Communicator.test.ts

Lines changed: 189 additions & 22 deletions
Large diffs are not rendered by default.

packages/client/src/components/communicator/webBased/Communicator.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as WebBrowser from 'expo-web-browser';
22

3-
import { HashedContent } from './types';
3+
import { decodeResponseURLParams, encodeRequestURLParams } from './encoding';
44
import { standardErrors } from ':core/error';
55
import { MessageID, RPCRequestMessage, RPCResponseMessage } from ':core/message';
66

@@ -13,12 +13,8 @@ class WebBasedWalletCommunicatorClass {
1313
): Promise<RPCResponseMessage> => {
1414
return new Promise((resolve, reject) => {
1515
// 1. generate request URL
16-
const urlParams = new URLSearchParams();
17-
Object.entries(request).forEach(([key, value]) => {
18-
urlParams.append(key, JSON.stringify(value));
19-
});
2016
const requestUrl = new URL(walletScheme);
21-
requestUrl.search = urlParams.toString();
17+
requestUrl.search = encodeRequestURLParams(request);
2218

2319
// 2. save response
2420
this.responseHandlers.set(request.id, resolve);
@@ -43,22 +39,7 @@ class WebBasedWalletCommunicatorClass {
4339

4440
handleResponse = (responseUrl: string): boolean => {
4541
const { searchParams } = new URL(responseUrl);
46-
const parseParam = <T>(paramName: string) => {
47-
return JSON.parse(searchParams.get(paramName) as string) as T;
48-
};
49-
50-
const hashedContent = parseParam<HashedContent>('content');
51-
52-
// TODO: un-hash content
53-
const content = hashedContent as RPCResponseMessage['content'];
54-
55-
const response: RPCResponseMessage = {
56-
id: parseParam<MessageID>('id'),
57-
sender: parseParam<string>('sender'),
58-
requestId: parseParam<MessageID>('requestId'),
59-
content,
60-
timestamp: new Date(parseParam<string>('timestamp')),
61-
};
42+
const response = decodeResponseURLParams(searchParams);
6243

6344
const handler = this.responseHandlers.get(response.requestId);
6445
if (handler) {

packages/client/src/components/communicator/webBased/encoding.test.ts

Lines changed: 185 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
2+
3+
import type { SerializedEthereumRpcError } from ':core/error';
4+
import type { MessageID, RPCRequestMessage, RPCResponseMessage } from ':core/message';
5+
6+
type EncodedResponseContent =
7+
| { failure: SerializedEthereumRpcError }
8+
| {
9+
encrypted: {
10+
iv: string | Record<string, number>;
11+
cipherText: string | Record<string, number>;
12+
};
13+
};
14+
15+
export function decodeResponseURLParams(params: URLSearchParams): RPCResponseMessage {
16+
const parseParam = <T>(paramName: string) => {
17+
const encodedValue = params.get(paramName);
18+
if (!encodedValue) throw new Error(`Missing parameter: ${paramName}`);
19+
return JSON.parse(encodedValue) as T;
20+
};
21+
22+
const contentParam = parseParam<EncodedResponseContent>('content');
23+
24+
let content: RPCResponseMessage['content'];
25+
if ('failure' in contentParam) {
26+
content = contentParam;
27+
}
28+
29+
if ('encrypted' in contentParam) {
30+
const { iv, cipherText } = contentParam.encrypted;
31+
content = {
32+
encrypted: {
33+
iv: typeof iv === 'string' ? hexToBytes(iv) : convertObjectToUint8Array(iv),
34+
cipherText:
35+
typeof cipherText === 'string'
36+
? hexToBytes(cipherText)
37+
: convertObjectToUint8Array(cipherText),
38+
},
39+
};
40+
}
41+
42+
return {
43+
id: parseParam<MessageID>('id'),
44+
sender: parseParam<string>('sender'),
45+
requestId: parseParam<MessageID>('requestId'),
46+
timestamp: new Date(parseParam<string>('timestamp')),
47+
content: content!,
48+
};
49+
}
50+
51+
export function encodeRequestURLParams(request: RPCRequestMessage) {
52+
const urlParams = new URLSearchParams();
53+
const appendParam = (key: string, value: unknown) => {
54+
urlParams.append(key, JSON.stringify(value));
55+
};
56+
57+
appendParam('id', request.id);
58+
appendParam('sender', request.sender);
59+
appendParam('sdkVersion', request.sdkVersion);
60+
appendParam('callbackUrl', request.callbackUrl);
61+
appendParam('timestamp', request.timestamp);
62+
63+
if ('handshake' in request.content) {
64+
appendParam('content', request.content);
65+
}
66+
67+
if ('encrypted' in request.content) {
68+
const encrypted = request.content.encrypted;
69+
appendParam('content', {
70+
encrypted: {
71+
iv: bytesToHex(new Uint8Array(encrypted.iv)),
72+
cipherText: bytesToHex(new Uint8Array(encrypted.cipherText)),
73+
},
74+
});
75+
}
76+
77+
return urlParams.toString();
78+
}
79+
80+
/**
81+
* Converts from a JSON.stringify-ied object to a Uint8Array
82+
* `{ "0": 1, "1": 2, "2": 3 }` to `Uint8Array([1, 2, 3])`
83+
*/
84+
function convertObjectToUint8Array(obj: Record<string, number>): Uint8Array {
85+
const length = Object.keys(obj).length;
86+
const bytes = new Uint8Array(length);
87+
88+
for (let i = 0; i < length; i++) {
89+
bytes[i] = obj[i];
90+
}
91+
92+
return bytes;
93+
}

packages/client/src/core/cipher/cipher.test.ts

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -289,14 +289,6 @@ describe('cipher', () => {
289289
_key: Uint8Array;
290290
};
291291

292-
function serializeAndDeserialize(obj: any): any {
293-
Object.entries(obj).forEach(([key, value]) => {
294-
obj[key] = JSON.parse(JSON.stringify(value));
295-
});
296-
297-
return obj;
298-
}
299-
300292
beforeAll(async () => {
301293
const aliceKeyPair = await generateKeyPair();
302294
const bobKeyPair = await generateKeyPair();
@@ -315,9 +307,7 @@ describe('cipher', () => {
315307
expect(encrypted).toHaveProperty('iv');
316308
expect(encrypted).toHaveProperty('cipherText');
317309

318-
const transmittedContent = serializeAndDeserialize(encrypted);
319-
320-
const decrypted = await decryptContent<RPCRequest>(transmittedContent, sharedSecret);
310+
const decrypted = await decryptContent<RPCRequest>(encrypted, sharedSecret);
321311
expect(decrypted).toEqual(request);
322312
});
323313

@@ -332,9 +322,7 @@ describe('cipher', () => {
332322
expect(encrypted).toHaveProperty('iv');
333323
expect(encrypted).toHaveProperty('cipherText');
334324

335-
const transmittedContent = serializeAndDeserialize(encrypted);
336-
337-
const decrypted = await decryptContent<RPCResponse>(transmittedContent, sharedSecret);
325+
const decrypted = await decryptContent<RPCResponse>(encrypted, sharedSecret);
338326
expect(decrypted).toEqual(response);
339327
});
340328

@@ -352,16 +340,14 @@ describe('cipher', () => {
352340
expect(encrypted).toHaveProperty('iv');
353341
expect(encrypted).toHaveProperty('cipherText');
354342

355-
const transmittedContent = serializeAndDeserialize(encrypted);
356-
357-
const decrypted = await decryptContent<RPCResponse>(transmittedContent, sharedSecret);
343+
const decrypted = await decryptContent<RPCResponse>(encrypted, sharedSecret);
358344
expect(decrypted).toEqual(errorResponse);
359345
});
360346

361347
it('should throw an error when decrypting invalid data', async () => {
362348
const invalidEncryptedData = {
363-
iv: { 0: 1, 1: 2, 2: 3 },
364-
cipherText: { 0: 4, 1: 5, 2: 6 },
349+
iv: new Uint8Array([1, 2, 3]),
350+
cipherText: new Uint8Array([4, 5, 6]),
365351
};
366352

367353
await expect(decryptContent(invalidEncryptedData, sharedSecret)).rejects.toThrow();

packages/client/src/core/cipher/cipher.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -143,25 +143,12 @@ export async function encryptContent(
143143

144144
export async function decryptContent<R extends RPCRequest | RPCResponse>(
145145
encryptedData: {
146-
// TODO: this is temp
147-
iv: unknown;
148-
cipherText: unknown;
146+
iv: Uint8Array;
147+
cipherText: Uint8Array;
149148
},
150149
sharedSecret: CryptoKey
151150
): Promise<R> {
152-
function convertObjectToUint8Array(obj: Record<string, number>): Uint8Array {
153-
const sortedValues = Object.entries(obj)
154-
.sort(([a], [b]) => parseInt(a) - parseInt(b))
155-
.map(([_, value]) => value);
156-
return new Uint8Array(sortedValues);
157-
}
158-
159-
return JSON.parse(
160-
await decrypt(sharedSecret, {
161-
iv: convertObjectToUint8Array(encryptedData.iv as Record<string, number>),
162-
cipherText: convertObjectToUint8Array(encryptedData.cipherText as Record<string, number>),
163-
})
164-
);
151+
return JSON.parse(await decrypt(sharedSecret, encryptedData));
165152
}
166153

167154
function encodeLength(length: number): Uint8Array {

yarn.lock

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)