Skip to content

Commit d1491ff

Browse files
authored
Merge pull request #2 from MobileWalletProtocol/vishnu/url-encoding-fix
Encode encrypted URL params as hex string
2 parents 5652453 + 9b59636 commit d1491ff

8 files changed

Lines changed: 462 additions & 85 deletions

File tree

packages/client/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"main": "dist/index.js",
1616
"types": "dist/index.d.ts",
1717
"sideEffects": false,
18-
"repository": "https://github.com/MobileWalletProtocol/wallet-mobile-sdk.git",
18+
"repository": "https://github.com/MobileWalletProtocol/react-native-client",
1919
"author": "Coinbase, Inc.",
2020
"license": "Apache-2.0",
2121
"scripts": {
@@ -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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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;
11+
cipherText: string;
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: hexToBytes(iv),
34+
cipherText: hexToBytes(cipherText),
35+
},
36+
};
37+
}
38+
39+
return {
40+
id: parseParam<MessageID>('id'),
41+
sender: parseParam<string>('sender'),
42+
requestId: parseParam<MessageID>('requestId'),
43+
timestamp: new Date(parseParam<string>('timestamp')),
44+
content: content!,
45+
};
46+
}
47+
48+
export function encodeRequestURLParams(request: RPCRequestMessage) {
49+
const urlParams = new URLSearchParams();
50+
const appendParam = (key: string, value: unknown) => {
51+
urlParams.append(key, JSON.stringify(value));
52+
};
53+
54+
appendParam('id', request.id);
55+
appendParam('sender', request.sender);
56+
appendParam('sdkVersion', request.sdkVersion);
57+
appendParam('callbackUrl', request.callbackUrl);
58+
appendParam('timestamp', request.timestamp);
59+
60+
if ('handshake' in request.content) {
61+
appendParam('content', request.content);
62+
}
63+
64+
if ('encrypted' in request.content) {
65+
const encrypted = request.content.encrypted;
66+
appendParam('content', {
67+
encrypted: {
68+
iv: bytesToHex(new Uint8Array(encrypted.iv)),
69+
cipherText: bytesToHex(new Uint8Array(encrypted.cipherText)),
70+
},
71+
});
72+
}
73+
74+
return urlParams.toString();
75+
}

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)