Skip to content

Commit cea2964

Browse files
committed
feat: add message encryption between Puter peers
1 parent 476acae commit cea2964

File tree

7 files changed

+264
-5
lines changed

7 files changed

+264
-5
lines changed

package-lock.json

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/backend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"string-length": "^6.0.0",
6969
"svgo": "^3.0.2",
7070
"tiktoken": "^1.0.11",
71+
"tweetnacl": "^1.0.3",
7172
"ua-parser-js": "^1.0.38",
7273
"uglify-js": "^3.17.4",
7374
"uuid": "^9.0.0",

src/backend/src/services/BroadcastService.js

+215-3
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,81 @@ const { Endpoint } = require("../util/expressutil");
2121
const { UserActorType } = require("./auth/Actor");
2222
const BaseService = require("./BaseService");
2323

24+
class KeyPairHelper extends AdvancedBase {
25+
static MODULES = {
26+
tweetnacl: require('tweetnacl'),
27+
};
28+
29+
constructor ({
30+
kpublic,
31+
ksecret,
32+
}) {
33+
super();
34+
this.kpublic = kpublic;
35+
this.ksecret = ksecret;
36+
this.nonce_ = 0;
37+
}
38+
39+
to_nacl_key_ (key) {
40+
console.log('WUT', key);
41+
const full_buffer = Buffer.from(key, 'base64');
42+
43+
// Remove version byte (assumed to be 0x31 and ignored for now)
44+
const buffer = full_buffer.slice(1);
45+
46+
return new Uint8Array(buffer);
47+
}
48+
49+
get naclSecret () {
50+
return this.naclSecret_ ?? (
51+
this.naclSecret_ = this.to_nacl_key_(this.ksecret));
52+
}
53+
get naclPublic () {
54+
return this.naclPublic_ ?? (
55+
this.naclPublic_ = this.to_nacl_key_(this.kpublic));
56+
}
57+
58+
write (text) {
59+
const require = this.require;
60+
const nacl = require('tweetnacl');
61+
62+
const nonce = nacl.randomBytes(nacl.box.nonceLength);
63+
const message = {};
64+
65+
const textUint8 = new Uint8Array(Buffer.from(text, 'utf-8'));
66+
const encryptedText = nacl.box(
67+
textUint8, nonce,
68+
this.naclPublic, this.naclSecret
69+
);
70+
message.text = Buffer.from(encryptedText);
71+
message.nonce = Buffer.from(nonce);
72+
73+
return message;
74+
}
75+
76+
read (message) {
77+
const require = this.require;
78+
const nacl = require('tweetnacl');
79+
80+
const arr = nacl.box.open(
81+
new Uint8Array(message.text),
82+
new Uint8Array(message.nonce),
83+
this.naclPublic,
84+
this.naclSecret,
85+
);
86+
87+
return Buffer.from(arr).toString('utf-8');
88+
}
89+
}
90+
2491
class Peer extends AdvancedBase {
92+
static AUTHENTICATING = Symbol('AUTHENTICATING');
2593
static ONLINE = Symbol('ONLINE');
2694
static OFFLINE = Symbol('OFFLINE');
2795

2896
static MODULES = {
2997
sioclient: require('socket.io-client'),
98+
crypto: require('crypto'),
3099
};
31100

32101
constructor (svc_broadcast, config) {
@@ -38,7 +107,23 @@ class Peer extends AdvancedBase {
38107

39108
send (data) {
40109
if ( ! this.socket ) return;
41-
this.socket.send(data)
110+
const require = this.require;
111+
const crypto = require('crypto');
112+
const iv = crypto.randomBytes(16);
113+
const cipher = crypto.createCipheriv(
114+
'aes-256-cbc',
115+
this.aesKey,
116+
iv,
117+
);
118+
const jsonified = JSON.stringify(data);
119+
let buffers = [];
120+
buffers.push(cipher.update(Buffer.from(jsonified, 'utf-8')));
121+
buffers.push(cipher.final());
122+
const buffer = Buffer.concat(buffers);
123+
this.socket.send({
124+
iv,
125+
message: buffer,
126+
});
42127
}
43128

44129
get state () {
@@ -66,6 +151,22 @@ class Peer extends AdvancedBase {
66151
this.log.info(`connected`, {
67152
address: this.config.address
68153
});
154+
155+
const require = this.require;
156+
const crypto = require('crypto');
157+
this.aesKey = crypto.randomBytes(32);
158+
159+
const kp_helper = new KeyPairHelper({
160+
kpublic: this.config.key,
161+
ksecret: this.svc_broadcast.config.keys.secret,
162+
});
163+
socket.send({
164+
$: 'take-my-key',
165+
key: this.svc_broadcast.config.keys.public,
166+
message: kp_helper.write(
167+
this.aesKey.toString('base64')
168+
),
169+
});
69170
});
70171
socket.on('disconnect', () => {
71172
this.log.info(`disconnected`, {
@@ -88,6 +189,89 @@ class Peer extends AdvancedBase {
88189
}
89190
}
90191

192+
class Connection extends AdvancedBase {
193+
static MODULES = {
194+
crypto: require('crypto'),
195+
}
196+
197+
static AUTHENTICATING = {
198+
on_message (data) {
199+
if ( data.$ !== 'take-my-key' ) {
200+
this.disconnect();
201+
return;
202+
}
203+
204+
const hasKey = this.svc_broadcast.trustedPublicKeys_[data.key];
205+
if ( ! hasKey ) {
206+
this.disconnect();
207+
return;
208+
}
209+
210+
const is_trusted =
211+
this.svc_broadcast.trustedPublicKeys_
212+
.hasOwnProperty(data.key)
213+
if ( ! is_trusted ) {
214+
this.disconnect();
215+
return;
216+
}
217+
218+
const kp_helper = new KeyPairHelper({
219+
kpublic: data.key,
220+
ksecret: this.svc_broadcast.config.keys.secret,
221+
});
222+
223+
const message = kp_helper.read(data.message);
224+
this.aesKey = Buffer.from(message, 'base64');
225+
226+
this.state = this.constructor.ONLINE;
227+
}
228+
}
229+
static ONLINE = {
230+
on_message (data) {
231+
if ( ! this.on_message ) return;
232+
233+
const require = this.require;
234+
const crypto = require('crypto');
235+
const decipher = crypto.createDecipheriv(
236+
'aes-256-cbc',
237+
this.aesKey,
238+
data.iv,
239+
)
240+
const buffers = [];
241+
buffers.push(decipher.update(data.message));
242+
buffers.push(decipher.final());
243+
244+
const rawjson = Buffer.concat(buffers).toString('utf-8');
245+
246+
const output = JSON.parse(rawjson);
247+
248+
this.on_message(output);
249+
}
250+
}
251+
static OFFLINE = {
252+
on_message () {
253+
throw new Error('unexpected message');
254+
}
255+
}
256+
257+
constructor (svc_broadcast, socket) {
258+
super();
259+
this.state = this.constructor.AUTHENTICATING;
260+
this.svc_broadcast = svc_broadcast;
261+
this.log = this.svc_broadcast.log;
262+
this.socket = socket;
263+
264+
socket.on('message', data => {
265+
this.state.on_message.call(this, data);
266+
});
267+
}
268+
269+
disconnect () {
270+
this.socket.disconnect(true);
271+
this.state = this.constructor.OFFLINE;
272+
}
273+
}
274+
91275
class BroadcastService extends BaseService {
92276
static MODULES = {
93277
express: require('express'),
@@ -96,16 +280,21 @@ class BroadcastService extends BaseService {
96280

97281
_construct () {
98282
this.peers_ = [];
283+
this.connections_ = [];
284+
this.trustedPublicKeys_ = {};
99285
}
100286

101287
async _init () {
102288
const peers = this.config.peers ?? [];
103289
for ( const peer_config of peers ) {
290+
this.trustedPublicKeys_[peer_config.key] = true;
104291
const peer = new Peer(this, peer_config);
105292
this.peers_.push(peer);
106293
peer.connect();
107294
}
108295

296+
this._register_commands(this.services.get('commands'));
297+
109298
const svc_event = this.services.get('event');
110299
svc_event.on('outer.*', this.on_event.bind(this));
111300
}
@@ -131,22 +320,45 @@ class BroadcastService extends BaseService {
131320
});
132321

133322
io.on('connection', async socket => {
134-
socket.on('message', ({ key, data, meta }) => {
323+
const conn = new Connection(this, socket);
324+
this.connections_.push(conn);
325+
326+
conn.on_message = ({ key, data, meta }) => {
135327
if ( meta.from_outside ) {
136328
this.log.noticeme('possible over-sending');
137329
return;
138330
}
139331

332+
if ( key === 'test' ) {
333+
this.log.noticeme(`test message: ` +
334+
JSON.stringify(data)
335+
);
336+
}
337+
140338
meta.from_outside = true;
141339
svc_event.emit(key, data, meta);
142-
});
340+
};
143341
});
144342

145343

146344
this.log.noticeme(
147345
require('node:util').inspect(this.config)
148346
);
149347
}
348+
349+
_register_commands (commands) {
350+
commands.registerCommands('broadcast', [
351+
{
352+
id: 'test',
353+
description: 'send a test message',
354+
handler: async (args, ctx) => {
355+
this.on_event('test', {
356+
contents: 'I am a test message',
357+
}, {})
358+
}
359+
}
360+
])
361+
}
150362
}
151363

152364
module.exports = { BroadcastService };

src/backend/src/services/runtime-analysis/AlarmService.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ class AlarmService extends BaseService {
252252
svc_devConsole.add_widget(this.alarm_widget);
253253
}
254254

255-
const args = Context.get('args');
255+
const args = Context.get('args') ?? {};
256256
if ( args['quit-on-alarm'] ) {
257257
const svc_shutdown = this.services.get('shutdown');
258258
svc_shutdown.shutdown({

tools/keygen/gen-peer-keys.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const nacl = require('tweetnacl');
2+
3+
const pair = nacl.box.keyPair();
4+
5+
const format_key = key => {
6+
const version = new Uint8Array([0x31]);
7+
const buffer = Buffer.concat([
8+
Buffer.from(version),
9+
Buffer.from(key),
10+
]);
11+
return buffer.toString('base64');
12+
};
13+
14+
console.log(JSON.stringify({
15+
keys: {
16+
public: format_key(pair.publicKey),
17+
secret: format_key(pair.secretKey),
18+
},
19+
}, undefined, ' '));

tools/keygen/package.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "keygen",
3+
"version": "1.0.0",
4+
"main": "gen-peer-keys.js",
5+
"scripts": {
6+
"test": "echo \"Error: no test specified\" && exit 1"
7+
},
8+
"keywords": [],
9+
"author": "",
10+
"license": "AGPL-3.0-only",
11+
"description": ""
12+
}

tools/run-selfhosted.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const main = async () => {
9696
k.add_module(new LocalDiskStorageModule());
9797
k.add_module(new SelfHostedModule());
9898
k.add_module(new TestDriversModule());
99-
k.add_module(new PuterAIModule());
99+
// k.add_module(new PuterAIModule());
100100
k.boot();
101101
};
102102

0 commit comments

Comments
 (0)