mirror of
https://github.com/HeyPuter/puter
synced 2024-11-14 22:06:00 +00:00
feat: add message encryption between Puter peers
This commit is contained in:
parent
476acae0e0
commit
cea29645fe
15
package-lock.json
generated
15
package-lock.json
generated
@ -11704,6 +11704,10 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/keygen": {
|
||||||
|
"resolved": "tools/keygen",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@ -16622,6 +16626,7 @@
|
|||||||
"string-length": "^6.0.0",
|
"string-length": "^6.0.0",
|
||||||
"svgo": "^3.0.2",
|
"svgo": "^3.0.2",
|
||||||
"tiktoken": "^1.0.11",
|
"tiktoken": "^1.0.11",
|
||||||
|
"tweetnacl": "^1.0.3",
|
||||||
"ua-parser-js": "^1.0.38",
|
"ua-parser-js": "^1.0.38",
|
||||||
"uglify-js": "^3.17.4",
|
"uglify-js": "^3.17.4",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
@ -16640,6 +16645,12 @@
|
|||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"src/backend/node_modules/tweetnacl": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"src/contextlink": {
|
"src/contextlink": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
@ -16914,6 +16925,10 @@
|
|||||||
"handlebars": "^4.7.8"
|
"handlebars": "^4.7.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tools/keygen": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "AGPL-3.0-only"
|
||||||
|
},
|
||||||
"tools/license-headers": {
|
"tools/license-headers": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
|
@ -68,6 +68,7 @@
|
|||||||
"string-length": "^6.0.0",
|
"string-length": "^6.0.0",
|
||||||
"svgo": "^3.0.2",
|
"svgo": "^3.0.2",
|
||||||
"tiktoken": "^1.0.11",
|
"tiktoken": "^1.0.11",
|
||||||
|
"tweetnacl": "^1.0.3",
|
||||||
"ua-parser-js": "^1.0.38",
|
"ua-parser-js": "^1.0.38",
|
||||||
"uglify-js": "^3.17.4",
|
"uglify-js": "^3.17.4",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
|
@ -21,12 +21,81 @@ const { Endpoint } = require("../util/expressutil");
|
|||||||
const { UserActorType } = require("./auth/Actor");
|
const { UserActorType } = require("./auth/Actor");
|
||||||
const BaseService = require("./BaseService");
|
const BaseService = require("./BaseService");
|
||||||
|
|
||||||
|
class KeyPairHelper extends AdvancedBase {
|
||||||
|
static MODULES = {
|
||||||
|
tweetnacl: require('tweetnacl'),
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor ({
|
||||||
|
kpublic,
|
||||||
|
ksecret,
|
||||||
|
}) {
|
||||||
|
super();
|
||||||
|
this.kpublic = kpublic;
|
||||||
|
this.ksecret = ksecret;
|
||||||
|
this.nonce_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to_nacl_key_ (key) {
|
||||||
|
console.log('WUT', key);
|
||||||
|
const full_buffer = Buffer.from(key, 'base64');
|
||||||
|
|
||||||
|
// Remove version byte (assumed to be 0x31 and ignored for now)
|
||||||
|
const buffer = full_buffer.slice(1);
|
||||||
|
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
get naclSecret () {
|
||||||
|
return this.naclSecret_ ?? (
|
||||||
|
this.naclSecret_ = this.to_nacl_key_(this.ksecret));
|
||||||
|
}
|
||||||
|
get naclPublic () {
|
||||||
|
return this.naclPublic_ ?? (
|
||||||
|
this.naclPublic_ = this.to_nacl_key_(this.kpublic));
|
||||||
|
}
|
||||||
|
|
||||||
|
write (text) {
|
||||||
|
const require = this.require;
|
||||||
|
const nacl = require('tweetnacl');
|
||||||
|
|
||||||
|
const nonce = nacl.randomBytes(nacl.box.nonceLength);
|
||||||
|
const message = {};
|
||||||
|
|
||||||
|
const textUint8 = new Uint8Array(Buffer.from(text, 'utf-8'));
|
||||||
|
const encryptedText = nacl.box(
|
||||||
|
textUint8, nonce,
|
||||||
|
this.naclPublic, this.naclSecret
|
||||||
|
);
|
||||||
|
message.text = Buffer.from(encryptedText);
|
||||||
|
message.nonce = Buffer.from(nonce);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
read (message) {
|
||||||
|
const require = this.require;
|
||||||
|
const nacl = require('tweetnacl');
|
||||||
|
|
||||||
|
const arr = nacl.box.open(
|
||||||
|
new Uint8Array(message.text),
|
||||||
|
new Uint8Array(message.nonce),
|
||||||
|
this.naclPublic,
|
||||||
|
this.naclSecret,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Buffer.from(arr).toString('utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Peer extends AdvancedBase {
|
class Peer extends AdvancedBase {
|
||||||
|
static AUTHENTICATING = Symbol('AUTHENTICATING');
|
||||||
static ONLINE = Symbol('ONLINE');
|
static ONLINE = Symbol('ONLINE');
|
||||||
static OFFLINE = Symbol('OFFLINE');
|
static OFFLINE = Symbol('OFFLINE');
|
||||||
|
|
||||||
static MODULES = {
|
static MODULES = {
|
||||||
sioclient: require('socket.io-client'),
|
sioclient: require('socket.io-client'),
|
||||||
|
crypto: require('crypto'),
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor (svc_broadcast, config) {
|
constructor (svc_broadcast, config) {
|
||||||
@ -38,7 +107,23 @@ class Peer extends AdvancedBase {
|
|||||||
|
|
||||||
send (data) {
|
send (data) {
|
||||||
if ( ! this.socket ) return;
|
if ( ! this.socket ) return;
|
||||||
this.socket.send(data)
|
const require = this.require;
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const cipher = crypto.createCipheriv(
|
||||||
|
'aes-256-cbc',
|
||||||
|
this.aesKey,
|
||||||
|
iv,
|
||||||
|
);
|
||||||
|
const jsonified = JSON.stringify(data);
|
||||||
|
let buffers = [];
|
||||||
|
buffers.push(cipher.update(Buffer.from(jsonified, 'utf-8')));
|
||||||
|
buffers.push(cipher.final());
|
||||||
|
const buffer = Buffer.concat(buffers);
|
||||||
|
this.socket.send({
|
||||||
|
iv,
|
||||||
|
message: buffer,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get state () {
|
get state () {
|
||||||
@ -66,6 +151,22 @@ class Peer extends AdvancedBase {
|
|||||||
this.log.info(`connected`, {
|
this.log.info(`connected`, {
|
||||||
address: this.config.address
|
address: this.config.address
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const require = this.require;
|
||||||
|
const crypto = require('crypto');
|
||||||
|
this.aesKey = crypto.randomBytes(32);
|
||||||
|
|
||||||
|
const kp_helper = new KeyPairHelper({
|
||||||
|
kpublic: this.config.key,
|
||||||
|
ksecret: this.svc_broadcast.config.keys.secret,
|
||||||
|
});
|
||||||
|
socket.send({
|
||||||
|
$: 'take-my-key',
|
||||||
|
key: this.svc_broadcast.config.keys.public,
|
||||||
|
message: kp_helper.write(
|
||||||
|
this.aesKey.toString('base64')
|
||||||
|
),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
this.log.info(`disconnected`, {
|
this.log.info(`disconnected`, {
|
||||||
@ -88,6 +189,89 @@ class Peer extends AdvancedBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Connection extends AdvancedBase {
|
||||||
|
static MODULES = {
|
||||||
|
crypto: require('crypto'),
|
||||||
|
}
|
||||||
|
|
||||||
|
static AUTHENTICATING = {
|
||||||
|
on_message (data) {
|
||||||
|
if ( data.$ !== 'take-my-key' ) {
|
||||||
|
this.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasKey = this.svc_broadcast.trustedPublicKeys_[data.key];
|
||||||
|
if ( ! hasKey ) {
|
||||||
|
this.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_trusted =
|
||||||
|
this.svc_broadcast.trustedPublicKeys_
|
||||||
|
.hasOwnProperty(data.key)
|
||||||
|
if ( ! is_trusted ) {
|
||||||
|
this.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kp_helper = new KeyPairHelper({
|
||||||
|
kpublic: data.key,
|
||||||
|
ksecret: this.svc_broadcast.config.keys.secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = kp_helper.read(data.message);
|
||||||
|
this.aesKey = Buffer.from(message, 'base64');
|
||||||
|
|
||||||
|
this.state = this.constructor.ONLINE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static ONLINE = {
|
||||||
|
on_message (data) {
|
||||||
|
if ( ! this.on_message ) return;
|
||||||
|
|
||||||
|
const require = this.require;
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const decipher = crypto.createDecipheriv(
|
||||||
|
'aes-256-cbc',
|
||||||
|
this.aesKey,
|
||||||
|
data.iv,
|
||||||
|
)
|
||||||
|
const buffers = [];
|
||||||
|
buffers.push(decipher.update(data.message));
|
||||||
|
buffers.push(decipher.final());
|
||||||
|
|
||||||
|
const rawjson = Buffer.concat(buffers).toString('utf-8');
|
||||||
|
|
||||||
|
const output = JSON.parse(rawjson);
|
||||||
|
|
||||||
|
this.on_message(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static OFFLINE = {
|
||||||
|
on_message () {
|
||||||
|
throw new Error('unexpected message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (svc_broadcast, socket) {
|
||||||
|
super();
|
||||||
|
this.state = this.constructor.AUTHENTICATING;
|
||||||
|
this.svc_broadcast = svc_broadcast;
|
||||||
|
this.log = this.svc_broadcast.log;
|
||||||
|
this.socket = socket;
|
||||||
|
|
||||||
|
socket.on('message', data => {
|
||||||
|
this.state.on_message.call(this, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect () {
|
||||||
|
this.socket.disconnect(true);
|
||||||
|
this.state = this.constructor.OFFLINE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class BroadcastService extends BaseService {
|
class BroadcastService extends BaseService {
|
||||||
static MODULES = {
|
static MODULES = {
|
||||||
express: require('express'),
|
express: require('express'),
|
||||||
@ -96,16 +280,21 @@ class BroadcastService extends BaseService {
|
|||||||
|
|
||||||
_construct () {
|
_construct () {
|
||||||
this.peers_ = [];
|
this.peers_ = [];
|
||||||
|
this.connections_ = [];
|
||||||
|
this.trustedPublicKeys_ = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async _init () {
|
async _init () {
|
||||||
const peers = this.config.peers ?? [];
|
const peers = this.config.peers ?? [];
|
||||||
for ( const peer_config of peers ) {
|
for ( const peer_config of peers ) {
|
||||||
|
this.trustedPublicKeys_[peer_config.key] = true;
|
||||||
const peer = new Peer(this, peer_config);
|
const peer = new Peer(this, peer_config);
|
||||||
this.peers_.push(peer);
|
this.peers_.push(peer);
|
||||||
peer.connect();
|
peer.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._register_commands(this.services.get('commands'));
|
||||||
|
|
||||||
const svc_event = this.services.get('event');
|
const svc_event = this.services.get('event');
|
||||||
svc_event.on('outer.*', this.on_event.bind(this));
|
svc_event.on('outer.*', this.on_event.bind(this));
|
||||||
}
|
}
|
||||||
@ -131,15 +320,24 @@ class BroadcastService extends BaseService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
io.on('connection', async socket => {
|
io.on('connection', async socket => {
|
||||||
socket.on('message', ({ key, data, meta }) => {
|
const conn = new Connection(this, socket);
|
||||||
|
this.connections_.push(conn);
|
||||||
|
|
||||||
|
conn.on_message = ({ key, data, meta }) => {
|
||||||
if ( meta.from_outside ) {
|
if ( meta.from_outside ) {
|
||||||
this.log.noticeme('possible over-sending');
|
this.log.noticeme('possible over-sending');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( key === 'test' ) {
|
||||||
|
this.log.noticeme(`test message: ` +
|
||||||
|
JSON.stringify(data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
meta.from_outside = true;
|
meta.from_outside = true;
|
||||||
svc_event.emit(key, data, meta);
|
svc_event.emit(key, data, meta);
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -147,6 +345,20 @@ class BroadcastService extends BaseService {
|
|||||||
require('node:util').inspect(this.config)
|
require('node:util').inspect(this.config)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_register_commands (commands) {
|
||||||
|
commands.registerCommands('broadcast', [
|
||||||
|
{
|
||||||
|
id: 'test',
|
||||||
|
description: 'send a test message',
|
||||||
|
handler: async (args, ctx) => {
|
||||||
|
this.on_event('test', {
|
||||||
|
contents: 'I am a test message',
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { BroadcastService };
|
module.exports = { BroadcastService };
|
||||||
|
@ -252,7 +252,7 @@ class AlarmService extends BaseService {
|
|||||||
svc_devConsole.add_widget(this.alarm_widget);
|
svc_devConsole.add_widget(this.alarm_widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = Context.get('args');
|
const args = Context.get('args') ?? {};
|
||||||
if ( args['quit-on-alarm'] ) {
|
if ( args['quit-on-alarm'] ) {
|
||||||
const svc_shutdown = this.services.get('shutdown');
|
const svc_shutdown = this.services.get('shutdown');
|
||||||
svc_shutdown.shutdown({
|
svc_shutdown.shutdown({
|
||||||
|
19
tools/keygen/gen-peer-keys.js
Normal file
19
tools/keygen/gen-peer-keys.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const nacl = require('tweetnacl');
|
||||||
|
|
||||||
|
const pair = nacl.box.keyPair();
|
||||||
|
|
||||||
|
const format_key = key => {
|
||||||
|
const version = new Uint8Array([0x31]);
|
||||||
|
const buffer = Buffer.concat([
|
||||||
|
Buffer.from(version),
|
||||||
|
Buffer.from(key),
|
||||||
|
]);
|
||||||
|
return buffer.toString('base64');
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
keys: {
|
||||||
|
public: format_key(pair.publicKey),
|
||||||
|
secret: format_key(pair.secretKey),
|
||||||
|
},
|
||||||
|
}, undefined, ' '));
|
12
tools/keygen/package.json
Normal file
12
tools/keygen/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "keygen",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "gen-peer-keys.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"description": ""
|
||||||
|
}
|
@ -96,7 +96,7 @@ const main = async () => {
|
|||||||
k.add_module(new LocalDiskStorageModule());
|
k.add_module(new LocalDiskStorageModule());
|
||||||
k.add_module(new SelfHostedModule());
|
k.add_module(new SelfHostedModule());
|
||||||
k.add_module(new TestDriversModule());
|
k.add_module(new TestDriversModule());
|
||||||
k.add_module(new PuterAIModule());
|
// k.add_module(new PuterAIModule());
|
||||||
k.boot();
|
k.boot();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user