Skip to content

Commit 5fcc5aa

Browse files
authored
Merge pull request #54 from geobrowser/encrypt-messages
encrypt and decrypt messages
2 parents 097b81f + 9dcfab4 commit 5fcc5aa

File tree

11 files changed

+166
-33
lines changed

11 files changed

+166
-33
lines changed

apps/events/src/routes/playground.tsx

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ import {
2828
createKey,
2929
createSpace,
3030
decryptKey,
31+
decryptMessage,
32+
deserialize,
3133
encryptKey,
34+
encryptMessage,
3235
generateId,
36+
serialize,
3337
} from 'graph-framework';
3438
import { useEffect, useState } from 'react';
3539

@@ -59,7 +63,7 @@ type SpaceStorageEntry = {
5963
events: SpaceEvent[];
6064
state: SpaceState | undefined;
6165
keys: { id: string; key: string }[];
62-
updates: string[];
66+
updates: Uint8Array[];
6367
lastUpdateClock: number;
6468
};
6569

@@ -119,7 +123,7 @@ const App = ({
119123
if (!websocketConnection) return;
120124

121125
const onMessage = async (event: MessageEvent) => {
122-
const data = JSON.parse(event.data);
126+
const data = deserialize(event.data);
123127
const message = decodeResponseMessage(data);
124128
if (message._tag === 'Right') {
125129
const response = message.right;
@@ -141,7 +145,7 @@ const App = ({
141145
// fetch all spaces (for debugging purposes)
142146
for (const space of response.spaces) {
143147
const message: RequestSubscribeToSpace = { type: 'subscribe-space', id: space.id };
144-
websocketConnection?.send(JSON.stringify(message));
148+
websocketConnection?.send(serialize(message));
145149
}
146150
break;
147151
}
@@ -176,10 +180,17 @@ const App = ({
176180
updates.push(...space.updates);
177181
}
178182
if (response.updates) {
179-
console.log('response.updates', response.updates, lastUpdateClock);
180183
if (response.updates.firstUpdateClock === lastUpdateClock + 1) {
181184
lastUpdateClock = response.updates.lastUpdateClock;
182-
updates.push(...response.updates.updates);
185+
186+
const newUpdates = (response.updates ? response.updates.updates : []).map((encryptedUpdate) => {
187+
return decryptMessage({
188+
nonceAndCiphertext: encryptedUpdate,
189+
secretKey: hexToBytes(keys[0].key),
190+
});
191+
});
192+
193+
updates.push(...newUpdates);
183194
} else {
184195
// TODO request missing updates from server
185196
}
@@ -254,9 +265,16 @@ const App = ({
254265
// TODO request missing updates from server
255266
}
256267

268+
const newUpdates = (response.updates ? response.updates.updates : []).map((encryptedUpdate) => {
269+
return decryptMessage({
270+
nonceAndCiphertext: encryptedUpdate,
271+
secretKey: hexToBytes(space.keys[0].key),
272+
});
273+
});
274+
257275
return {
258276
...space,
259-
updates: [...space.updates, ...response.updates.updates],
277+
updates: [...space.updates, ...newUpdates],
260278
lastUpdateClock,
261279
};
262280
}
@@ -308,7 +326,7 @@ const App = ({
308326
authorPublicKey: encryptionPublicKey,
309327
},
310328
};
311-
websocketConnection?.send(JSON.stringify(message));
329+
websocketConnection?.send(serialize(message));
312330
}}
313331
>
314332
Create space
@@ -317,7 +335,7 @@ const App = ({
317335
<Button
318336
onClick={() => {
319337
const message: RequestListSpaces = { type: 'list-spaces' };
320-
websocketConnection?.send(JSON.stringify(message));
338+
websocketConnection?.send(serialize(message));
321339
}}
322340
>
323341
List Spaces
@@ -326,7 +344,7 @@ const App = ({
326344
<Button
327345
onClick={() => {
328346
const message: RequestListInvitations = { type: 'list-invitations' };
329-
websocketConnection?.send(JSON.stringify(message));
347+
websocketConnection?.send(serialize(message));
330348
}}
331349
>
332350
List Invitations
@@ -355,12 +373,12 @@ const App = ({
355373
event: spaceEvent.value,
356374
spaceId: invitation.spaceId,
357375
};
358-
websocketConnection?.send(JSON.stringify(message));
376+
websocketConnection?.send(serialize(message));
359377

360378
// temporary until we have define a strategy for accepting invitations response
361379
setTimeout(() => {
362380
const message2: RequestListInvitations = { type: 'list-invitations' };
363-
websocketConnection?.send(JSON.stringify(message2));
381+
websocketConnection?.send(serialize(message2));
364382
}, 1000);
365383
}}
366384
/>
@@ -375,7 +393,7 @@ const App = ({
375393
<Button
376394
onClick={() => {
377395
const message: RequestSubscribeToSpace = { type: 'subscribe-space', id: space.id };
378-
websocketConnection?.send(JSON.stringify(message));
396+
websocketConnection?.send(serialize(message));
379397
}}
380398
>
381399
Get data and subscribe to Space
@@ -430,7 +448,7 @@ const App = ({
430448
spaceId: space.id,
431449
keyBoxes,
432450
};
433-
websocketConnection?.send(JSON.stringify(message));
451+
websocketConnection?.send(serialize(message));
434452
}}
435453
>
436454
Invite {invitee.accountId.substring(0, 4)}
@@ -445,18 +463,24 @@ const App = ({
445463
setSpaces((currentSpaces) =>
446464
currentSpaces.map((currentSpace) => {
447465
if (space.id === currentSpace.id) {
448-
return { ...currentSpace, updates: [...currentSpace.updates, 'a'] };
466+
return { ...currentSpace, updates: [...currentSpace.updates, new Uint8Array([0])] };
449467
}
450468
return currentSpace;
451469
}),
452470
);
471+
472+
const nonceAndCiphertext = encryptMessage({
473+
message: new Uint8Array([0]),
474+
secretKey: hexToBytes(space.keys[0].key),
475+
});
476+
453477
const message: RequestCreateUpdate = {
454478
type: 'create-update',
455479
ephemeralId,
456-
update: 'a',
480+
update: nonceAndCiphertext,
457481
spaceId: space.id,
458482
};
459-
websocketConnection?.send(JSON.stringify(message));
483+
websocketConnection?.send(serialize(message));
460484
}}
461485
>
462486
Create an update

apps/server/src/handlers/createUpdate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { prisma } from '../prisma.js';
22

33
type Params = {
44
accountId: string;
5-
update: string;
5+
update: Uint8Array;
66
spaceId: string;
77
};
88

apps/server/src/handlers/getSpace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const getSpace = async ({ spaceId, accountId }: Params) => {
6060
updates:
6161
space.updates.length > 0
6262
? {
63-
updates: space.updates.map((update) => update.content.toString()),
63+
updates: space.updates.map((update) => new Uint8Array(update.content)),
6464
firstUpdateClock: space.updates[0].clock,
6565
lastUpdateClock: space.updates[space.updates.length - 1].clock,
6666
}

apps/server/src/index.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
ResponseUpdatesNotification,
1313
Updates,
1414
} from 'graph-framework-messages';
15-
import { RequestMessage } from 'graph-framework-messages';
15+
import { RequestMessage, deserialize, serialize } from 'graph-framework-messages';
1616
import type { SpaceEvent } from 'graph-framework-space-events';
1717
import { applyEvent } from 'graph-framework-space-events';
1818
import WebSocket, { WebSocketServer } from 'ws';
@@ -65,7 +65,7 @@ function broadcastSpaceEvents({
6565
event,
6666
};
6767
if (client.readyState === WebSocket.OPEN && client.subscribedSpaces.has(spaceId)) {
68-
client.send(JSON.stringify(outgoingMessage));
68+
client.send(serialize(outgoingMessage));
6969
}
7070
}
7171
}
@@ -84,7 +84,7 @@ function broadcastUpdates({
8484
spaceId,
8585
};
8686
if (client.readyState === WebSocket.OPEN && client.subscribedSpaces.has(spaceId)) {
87-
client.send(JSON.stringify(outgoingMessage));
87+
client.send(serialize(outgoingMessage));
8888
}
8989
}
9090
}
@@ -101,7 +101,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req
101101

102102
console.log('Connection established', accountId);
103103
webSocket.on('message', async (message) => {
104-
const rawData = JSON.parse(message.toString());
104+
const rawData = deserialize(message.toString());
105105
const result = decodeRequestMessage(rawData);
106106
if (result._tag === 'Right') {
107107
const data = result.right;
@@ -113,19 +113,19 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req
113113
type: 'space',
114114
};
115115
webSocket.subscribedSpaces.add(data.id);
116-
webSocket.send(JSON.stringify(outgoingMessage));
116+
webSocket.send(serialize(outgoingMessage));
117117
break;
118118
}
119119
case 'list-spaces': {
120120
const spaces = await listSpaces({ accountId });
121121
const outgoingMessage: ResponseListSpaces = { type: 'list-spaces', spaces: spaces };
122-
webSocket.send(JSON.stringify(outgoingMessage));
122+
webSocket.send(serialize(outgoingMessage));
123123
break;
124124
}
125125
case 'list-invitations': {
126126
const invitations = await listInvitations({ accountId });
127127
const outgoingMessage: ResponseListInvitations = { type: 'list-invitations', invitations: invitations };
128-
webSocket.send(JSON.stringify(outgoingMessage));
128+
webSocket.send(serialize(outgoingMessage));
129129
break;
130130
}
131131
case 'create-space-event': {
@@ -137,7 +137,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req
137137
...spaceWithEvents,
138138
type: 'space',
139139
};
140-
webSocket.send(JSON.stringify(outgoingMessage));
140+
webSocket.send(serialize(outgoingMessage));
141141
}
142142
// TODO send back error
143143
break;
@@ -155,7 +155,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req
155155
...spaceWithEvents,
156156
type: 'space',
157157
};
158-
webSocket.send(JSON.stringify(outgoingMessage));
158+
webSocket.send(serialize(outgoingMessage));
159159
for (const client of webSocketServer.clients as Set<CustomWebSocket>) {
160160
if (
161161
client.readyState === WebSocket.OPEN &&
@@ -164,7 +164,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req
164164
const invitations = await listInvitations({ accountId: client.accountId });
165165
const outgoingMessage: ResponseListInvitations = { type: 'list-invitations', invitations: invitations };
166166
// for now sending the entire list of invitations to the client - we could send only a single one
167-
client.send(JSON.stringify(outgoingMessage));
167+
client.send(serialize(outgoingMessage));
168168
}
169169
}
170170

@@ -178,7 +178,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req
178178
...spaceWithEvents,
179179
type: 'space',
180180
};
181-
webSocket.send(JSON.stringify(outgoingMessage));
181+
webSocket.send(serialize(outgoingMessage));
182182
broadcastSpaceEvents({ spaceId: data.spaceId, event: data.event, currentClient: webSocket });
183183
break;
184184
}
@@ -190,12 +190,12 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req
190190
clock: update.clock,
191191
spaceId: data.spaceId,
192192
};
193-
webSocket.send(JSON.stringify(outgoingMessage));
193+
webSocket.send(serialize(outgoingMessage));
194194

195195
broadcastUpdates({
196196
spaceId: data.spaceId,
197197
updates: {
198-
updates: [update.content.toString()],
198+
updates: [new Uint8Array(update.content)],
199199
firstUpdateClock: update.clock,
200200
lastUpdateClock: update.clock,
201201
},
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { decryptMessage } from './decrypt-message.js';
3+
import { encryptMessage } from './encrypt-message.js';
4+
describe('decryptMessage', () => {
5+
const testKey = new Uint8Array(32).fill(1);
6+
7+
it('should successfully decrypt a valid message', () => {
8+
const nonceAndCiphertext = encryptMessage({
9+
message: new TextEncoder().encode('Hello, World!'),
10+
secretKey: testKey,
11+
});
12+
13+
const result = decryptMessage({
14+
nonceAndCiphertext,
15+
secretKey: testKey,
16+
});
17+
18+
expect(new TextDecoder().decode(result)).toBe('Hello, World!');
19+
});
20+
21+
it('should fail to decrypt with an invalid nonce', () => {
22+
const nonceAndCiphertext = encryptMessage({
23+
message: new TextEncoder().encode('Hello, World!'),
24+
secretKey: testKey,
25+
});
26+
27+
expect(() => {
28+
return decryptMessage({
29+
nonceAndCiphertext: new Uint8Array([...new Uint8Array(24).fill(0), ...nonceAndCiphertext.subarray(24)]),
30+
secretKey: testKey,
31+
});
32+
}).toThrow();
33+
});
34+
35+
it('should fail to decrypt with the wrong key', () => {
36+
const nonceAndCiphertext = encryptMessage({
37+
message: new TextEncoder().encode('Hello, World!'),
38+
secretKey: testKey,
39+
});
40+
41+
const wrongKey = new Uint8Array(32).fill(0);
42+
43+
expect(() => {
44+
return decryptMessage({
45+
nonceAndCiphertext,
46+
secretKey: wrongKey,
47+
});
48+
}).toThrow();
49+
});
50+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
2+
3+
interface Params {
4+
nonceAndCiphertext: Uint8Array;
5+
secretKey: Uint8Array;
6+
}
7+
8+
export function decryptMessage({ nonceAndCiphertext, secretKey }: Params) {
9+
const nonce = nonceAndCiphertext.subarray(0, 24);
10+
const ciphertext = nonceAndCiphertext.subarray(24);
11+
const cipher = xchacha20poly1305(secretKey, nonce);
12+
return cipher.decrypt(ciphertext);
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { encryptMessage } from './encrypt-message.js';
3+
describe('encryptMessage', () => {
4+
const tooShortKey = new Uint8Array(31).fill(1);
5+
6+
it('should fail to encrypt with a too short key', () => {
7+
expect(() => {
8+
encryptMessage({ message: new TextEncoder().encode('Hello, World!'), secretKey: tooShortKey });
9+
}).toThrow();
10+
});
11+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
2+
import { randomBytes } from '@noble/ciphers/webcrypto';
3+
4+
type Params = {
5+
message: Uint8Array;
6+
secretKey: Uint8Array;
7+
};
8+
9+
export function encryptMessage({ message, secretKey }: Params) {
10+
const nonce = randomBytes(24);
11+
const cipher = xchacha20poly1305(secretKey, nonce);
12+
const ciphertext = cipher.encrypt(message);
13+
return new Uint8Array([...nonce, ...ciphertext]);
14+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1+
export * from './decrypt-message.js';
2+
export * from './encrypt-message.js';
3+
export * from './serialize.js';
14
export * from './types.js';

0 commit comments

Comments
 (0)