瀏覽代碼

feat: add message encryption between Puter peers

KernelDeimos 9 月之前
父節點
當前提交
cea29645fe

+ 15 - 0
package-lock.json

@@ -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",

+ 1 - 0
src/backend/package.json

@@ -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",

+ 215 - 3
src/backend/src/services/BroadcastService.js

@@ -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 };

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

@@ -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 - 0
tools/keygen/gen-peer-keys.js

@@ -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 - 0
tools/keygen/package.json

@@ -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": ""
+}

+ 1 - 1
tools/run-selfhosted.js

@@ -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();
 };
 };