From d826c58b51ff455ba045a3a952d3d06beea4d216 Mon Sep 17 00:00:00 2001 From: aikitori Date: Sat, 12 Mar 2022 16:48:52 +0100 Subject: [PATCH 01/21] Add Delete Message Node --- package.json | 1 + src/matrix-delete-message.html | 84 ++++++++++++++++++++++++++++++++++ src/matrix-delete-message.js | 74 ++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 src/matrix-delete-message.html create mode 100644 src/matrix-delete-message.js diff --git a/package.json b/package.json index 4d84251..6f464e2 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "matrix-server-config": "src/matrix-server-config.js", "matrix-receive": "src/matrix-receive.js", "matrix-send-message": "src/matrix-send-message.js", + "matrix-send-message": "src/matrix-delete-message.js", "matrix-send-file": "src/matrix-send-file.js", "matrix-send-image": "src/matrix-send-image.js", "matrix-react": "src/matrix-react.js", diff --git a/src/matrix-delete-message.html b/src/matrix-delete-message.html new file mode 100644 index 0000000..e12e08f --- /dev/null +++ b/src/matrix-delete-message.html @@ -0,0 +1,84 @@ + + + + + diff --git a/src/matrix-delete-message.js b/src/matrix-delete-message.js new file mode 100644 index 0000000..362064d --- /dev/null +++ b/src/matrix-delete-message.js @@ -0,0 +1,74 @@ +module.exports = function(RED) { + function MatrixDeleteMessage(n) { + RED.nodes.createNode(this,n); + + var node = this; + + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + this.roomId = n.roomId; + this.reason = n.reason + + if (!node.server) { + node.warn("No configuration node"); + return; + } + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + node.server.on("disconnected", function(){ + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + node.server.on("connected", function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + node.on('input', function(msg) { + + if(!msg.eventId) { + node.error("eventId is missing"); + return; + } + + if (!node.server || !node.server.matrixClient) { + node.warn("No matrix server selected"); + return; + } + + if(!node.server.isConnected()) { + node.error("Matrix server connection is currently closed"); + node.send([null, msg]); + return; + } + + msg.topic = node.roomId || msg.topic; + if(!msg.topic) { + node.warn("Room must be specified in msg.topic or in configuration"); + return; + } + + msg.reason = node.reason || msg.reason; + + if(!msg.reason) { + msg.reason = ''; + } + + node.server.matrixClient.redactEvent(msg.topic,msg.eventId,undefined ,{ + reason: msg.reason + }) + + .then(function(e) { + msg.deleted = true + node.send([msg, null]); + }) + .catch(function(e){ + node.warn("Error sending message " + e); + msg.error = e; + msg.deleted = false + node.send([null, msg]); + }); + }); + } + RED.nodes.registerType("matrix-delete-message",MatrixDeleteMessage); +} From 33823dea259a179f4699ee0c980bf00990e06a29 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sat, 12 Mar 2022 16:23:43 -0700 Subject: [PATCH 02/21] - We now use /whoami to validate the auth token since it gives us the user_id and device_id for the given auth_token (device_id is only available on Synapse 1.40 onwards) - Error is now thrown if device_id cannot be automatically detected - Do not store `null` into my_device_id and ignore if it has already been set. Fixes #51 --- src/matrix-server-config.js | 87 +++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 27 deletions(-) diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index 6ad6918..993932f 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -64,32 +64,37 @@ module.exports = function(RED) { // store Device ID internally let stored_device_id = getStoredDeviceId(localStorage), device_id = this.matrixClient.getDeviceId(); - if(!stored_device_id || stored_device_id !== device_id) { - node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`); - storeDeviceId(localStorage, device_id); - } - // update device label - if(node.deviceLabel) { - node.matrixClient - .getDevice(device_id) - .then( - function(response) { - if(response.display_name !== node.deviceLabel) { - node.matrixClient.setDeviceDetails(device_id, { - display_name: node.deviceLabel - }).then( - function(response) {}, - function(error) { - node.error("Failed to set device label: " + error); - } - ); + if(!device_id && node.enableE2ee) { + node.error("Failed to auto detect deviceId for this auth token. You will need to manually specify one. You may need to login to create a new deviceId.") + } else { + if(!stored_device_id || stored_device_id !== device_id) { + node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`); + storeDeviceId(localStorage, device_id); + } + + // update device label + if(node.deviceLabel) { + node.matrixClient + .getDevice(device_id) + .then( + function(response) { + if(response.display_name !== node.deviceLabel) { + node.matrixClient.setDeviceDetails(device_id, { + display_name: node.deviceLabel + }).then( + function(response) {}, + function(error) { + node.error("Failed to set device label: " + error); + } + ); + } + }, + function(error) { + node.error("Failed to fetch device: " + error); } - }, - function(error) { - node.error("Failed to fetch device: " + error); - } - ); + ); + } } initialSetup = true; @@ -326,9 +331,29 @@ module.exports = function(RED) { return; } - node.matrixClient.getAccountDataFromServer() + /** + * We do a /whoami request before starting for a few reasons: + * - validate our auth token + * - make sure auth token belongs to provided node.userId + * - fetch device_id if possible (only available on Synapse >= v1.40.0 under MSC2033) + */ + node.matrixClient.whoami() .then( - function() { + function(data) { + if((typeof data['device_id'] === undefined || !data['device_id']) && !node.deviceId && !getStoredDeviceId(localStorage)) { + node.error("/whoami request did not return device_id. You will need to manually set one in your configuration because this cannot be automatically fetched."); + } + if('device_id' in data && data['device_id'] && !node.deviceId) { + // if we have no device_id configured lets use the one + // returned by /whoami for this access_token + node.matrixClient.deviceId = data['device_id']; + } + + // make sure our userId matches the access token's + if(data['user_id'].toLowerCase() !== node.userId.toLowerCase()) { + node.error(`User ID provided is ${node.userId} but token belongs to ${data['user_id']}`); + return; + } run().catch((error) => node.error(error)); }, function(err) { @@ -436,10 +461,18 @@ module.exports = function(RED) { * If a device ID is stored we will use that for the client */ function getStoredDeviceId(localStorage) { - return localStorage.getItem('my_device_id'); + let deviceId = localStorage.getItem('my_device_id'); + if(deviceId === "null" || !deviceId) { + return null; + } + return deviceId; } function storeDeviceId(localStorage, deviceId) { + if(!deviceId) { + return false; + } localStorage.setItem('my_device_id', deviceId); + return true; } } \ No newline at end of file From 050be29d6433862b374d4f0f5eb6317a71632534 Mon Sep 17 00:00:00 2001 From: aikitori Date: Sun, 13 Mar 2022 10:58:17 +0100 Subject: [PATCH 03/21] rename node --- package.json | 2 +- ...-message.html => matrix-delete-event.html} | 26 +++++++++---------- ...lete-message.js => matrix-delete-event.js} | 7 ++--- 3 files changed, 18 insertions(+), 17 deletions(-) rename src/{matrix-delete-message.html => matrix-delete-event.html} (71%) rename src/{matrix-delete-message.js => matrix-delete-event.js} (90%) diff --git a/package.json b/package.json index 6f464e2..1d1b2e3 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "matrix-server-config": "src/matrix-server-config.js", "matrix-receive": "src/matrix-receive.js", "matrix-send-message": "src/matrix-send-message.js", - "matrix-send-message": "src/matrix-delete-message.js", + "matrix-delete-event": "src/matrix-delete-event.js", "matrix-send-file": "src/matrix-send-file.js", "matrix-send-image": "src/matrix-send-image.js", "matrix-react": "src/matrix-react.js", diff --git a/src/matrix-delete-message.html b/src/matrix-delete-event.html similarity index 71% rename from src/matrix-delete-message.html rename to src/matrix-delete-event.html index e12e08f..6509bdb 100644 --- a/src/matrix-delete-message.html +++ b/src/matrix-delete-event.html @@ -1,5 +1,5 @@ - - diff --git a/src/matrix-delete-event.html b/src/matrix-delete-event.html index 5393373..920402d 100644 --- a/src/matrix-delete-event.html +++ b/src/matrix-delete-event.html @@ -29,7 +29,7 @@
- +
diff --git a/src/matrix-invite-room.html b/src/matrix-invite-room.html index e1a5f75..2541382 100644 --- a/src/matrix-invite-room.html +++ b/src/matrix-invite-room.html @@ -20,7 +20,7 @@ diff --git a/src/matrix-join-room.html b/src/matrix-join-room.html index 4b2e8ce..cc04770 100644 --- a/src/matrix-join-room.html +++ b/src/matrix-join-room.html @@ -19,7 +19,7 @@ diff --git a/src/matrix-delete-event.html b/src/matrix-delete-event.html index 920402d..657fc71 100644 --- a/src/matrix-delete-event.html +++ b/src/matrix-delete-event.html @@ -31,6 +31,19 @@
+ + +
diff --git a/src/matrix-invite-room.html b/src/matrix-invite-room.html index 2541382..13844e3 100644 --- a/src/matrix-invite-room.html +++ b/src/matrix-invite-room.html @@ -20,7 +20,7 @@ +
diff --git a/src/matrix-join-room.html b/src/matrix-join-room.html index cc04770..c3c75f5 100644 --- a/src/matrix-join-room.html +++ b/src/matrix-join-room.html @@ -19,7 +19,7 @@ +
diff --git a/src/matrix-receive.html b/src/matrix-receive.html index aba500e..b08a7a7 100644 --- a/src/matrix-receive.html +++ b/src/matrix-receive.html @@ -36,6 +36,18 @@
+ +
Enter a single room, comma separated list of rooms, or leave blank to get from all
diff --git a/src/matrix-room-ban.html b/src/matrix-room-ban.html index 192ee1e..726a0d1 100644 --- a/src/matrix-room-ban.html +++ b/src/matrix-room-ban.html @@ -21,7 +21,7 @@ +
diff --git a/src/matrix-room-kick.html b/src/matrix-room-kick.html index b1b3210..e303335 100644 --- a/src/matrix-room-kick.html +++ b/src/matrix-room-kick.html @@ -21,7 +21,7 @@ +
diff --git a/src/matrix-room-users.html b/src/matrix-room-users.html index df1813d..27cf138 100644 --- a/src/matrix-room-users.html +++ b/src/matrix-room-users.html @@ -20,7 +20,7 @@ +
diff --git a/src/matrix-send-file.html b/src/matrix-send-file.html index 83006f5..7355728 100644 --- a/src/matrix-send-file.html +++ b/src/matrix-send-file.html @@ -21,7 +21,7 @@ +
diff --git a/src/matrix-send-image.html b/src/matrix-send-image.html index 21c1d11..aaef024 100644 --- a/src/matrix-send-image.html +++ b/src/matrix-send-image.html @@ -21,7 +21,7 @@ +
diff --git a/src/matrix-send-message.html b/src/matrix-send-message.html index 9cd5af1..ac6d31b 100644 --- a/src/matrix-send-message.html +++ b/src/matrix-send-message.html @@ -24,7 +24,7 @@ +
diff --git a/src/matrix-synapse-create-edit-user.html b/src/matrix-synapse-create-edit-user.html index d2fffcd..110aa9d 100644 --- a/src/matrix-synapse-create-edit-user.html +++ b/src/matrix-synapse-create-edit-user.html @@ -19,7 +19,7 @@ +
diff --git a/src/matrix-synapse-register.html b/src/matrix-synapse-register.html index 6191399..78d4349 100644 --- a/src/matrix-synapse-register.html +++ b/src/matrix-synapse-register.html @@ -22,7 +22,7 @@ + + + + diff --git a/src/matrix-device-verification.js b/src/matrix-device-verification.js new file mode 100644 index 0000000..98a7b7d --- /dev/null +++ b/src/matrix-device-verification.js @@ -0,0 +1,234 @@ +const {Phase} = require("matrix-js-sdk/lib/crypto/verification/request/VerificationRequest"); +const {CryptoEvent} = require("matrix-js-sdk/lib/crypto"); + +module.exports = function(RED) { + const verificationRequests = new Map(); + + function MatrixDeviceVerification(n) { + RED.nodes.createNode(this, n); + + var node = this; + + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + this.mode = n.mode; + + if (!node.server) { + node.warn("No configuration node"); + return; + } + + if(!node.server.e2ee) { + node.error("End-to-end encryption needs to be enabled to use this."); + } + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + node.server.on("disconnected", function(){ + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + node.server.on("connected", function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + function getKeyByValue(object, value) { + return Object.keys(object).find(key => object[key] === value); + } + + switch(node.mode) { + default: + node.error("Node not configured with a mode"); + break; + + case 'request': + node.on('input', async function(msg){ + if(!msg.userId) { + node.error("msg.userId is required for start verification mode"); + } + + node.server.matrixClient.requestVerification(msg.userId, msg.devices || null) + .then(function(e) { + node.log("Successfully requested verification"); + let verifyRequestId = msg.userId + ':' + e.channel.deviceId; + verificationRequests.set(verifyRequestId, e); + node.send({ + verifyRequestId: verifyRequestId, // internally used to reference between nodes + verifyMethods: e.methods, + userId: msg.userId, + deviceIds: e.channel.devices, + selfVerification: e.isSelfVerification, + phase: getKeyByValue(Phase, e.phase) + }); + }) + .catch(function(e){ + node.warn("Error requesting device verification: " + e); + msg.error = e; + node.send([null, msg]); + }); + }); + break; + + case 'receive': + /** + * Fires when a key verification is requested. + * @event module:client~MatrixClient#"crypto.verification.request" + * @param {object} data + * @param {MatrixEvent} data.event the original verification request message + * @param {Array} data.methods the verification methods that can be used + * @param {Number} data.timeout the amount of milliseconds that should be waited + * before cancelling the request automatically. + * @param {Function} data.beginKeyVerification a function to call if a key + * verification should be performed. The function takes one argument: the + * name of the key verification method (taken from data.methods) to use. + * @param {Function} data.cancel a function to call if the key verification is + * rejected. + */ + node.server.matrixClient.on(CryptoEvent.VerificationRequest, async function(data){ + if(data.phase === Phase.Cancelled || data.phase === Phase.Done) { + return; + } + + if(data.requested || true) { + let verifyRequestId = data.targetDevice.userId + ':' + data.targetDevice.deviceId; + verificationRequests.set(verifyRequestId, data); + node.send({ + verifyRequestId: verifyRequestId, // internally used to reference between nodes + verifyMethods: data.methods, + userId: data.targetDevice.userId, + deviceId: data.targetDevice.deviceId, + selfVerification: data.isSelfVerification, + phase: getKeyByValue(Phase, data.phase) + }); + } + }); + + node.on('close', function(done) { + // clear verification requests + verificationRequests.clear(); + done(); + }); + break; + + case 'start': + node.on('input', async function(msg){ + if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) { + // if(msg.userId && msg.deviceId) { + // node.server.beginKeyVerification("m.sas.v1", msg.userId, msg.deviceId); + // } + + node.error("invalid verification request (invalid msg.verifyRequestId): " + (msg.verifyRequestId || null)); + } + + var data = verificationRequests.get(msg.verifyRequestId); + if(msg.cancel) { + await data._verifier.cancel(); + verificationRequests.delete(msg.verifyRequestId); + } else { + try { + data.on('change', async function() { + var that = this; + if(this.phase === Phase.Started) { + let verifierCancel = function(){ + let verifyRequestId = that.targetDevice.userId + ':' + that.targetDevice.deviceId; + if(verificationRequests.has(verifyRequestId)) { + verificationRequests.delete(verifyRequestId); + } + }; + + data._verifier.on('cancel', function(e){ + node.warn("Device verification cancelled " + e); + verifierCancel(); + }); + + let show_sas = function(e) { + // e = { + // sas: { + // decimal: [ 8641, 3153, 2357 ], + // emoji: [ + // [Array], [Array], + // [Array], [Array], + // [Array], [Array], + // [Array] + // ] + // }, + // confirm: [AsyncFunction: confirm], + // cancel: [Function: cancel], + // mismatch: [Function: mismatch] + // } + msg.payload = e.sas; + msg.emojis = e.sas.emoji.map(function(emoji, i) { + return emoji[0]; + }); + msg.emojis_text = e.sas.emoji.map(function(emoji, i) { + return emoji[1]; + }); + node.send(msg); + }; + data._verifier.on('show_sas', show_sas); + data._verifier.verify() + .then(function(e){ + data._verifier.off('show_sas', show_sas); + data._verifier.done(); + }, function(e) { + verifierCancel(); + node.warn(e); + // @todo return over second output + }); + } + }); + + data.emit("change"); + await data.accept(); + } catch(e) { + console.log("ERROR", e); + } + } + }); + break; + + case 'cancel': + node.on('input', async function(msg){ + if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) { + node.error("Invalid verification request: " + (msg.verifyRequestId || null)); + } + + var data = verificationRequests.get(msg.verifyRequestId); + if(data) { + data.cancel() + .then(function(e){ + node.send([msg, null]); + }) + .catch(function(e) { + msg.error = e; + node.send([null, msg]); + }); + } + }); + break; + + case 'accept': + node.on('input', async function(msg){ + if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) { + node.error("Invalid verification request: " + (msg.verifyRequestId || null)); + } + + var data = verificationRequests.get(msg.verifyRequestId); + if(data._verifier && data._verifier.sasEvent) { + data._verifier.sasEvent.confirm() + .then(function(e){ + node.send([msg, null]); + }) + .catch(function(e) { + msg.error = e; + node.send([null, msg]); + }); + } else { + node.error("Verification must be started"); + } + }); + break; + } + } + RED.nodes.registerType("matrix-device-verification", MatrixDeviceVerification); +} \ No newline at end of file From 68cb5a026eedc02fdda3afba17200def0ffd1220 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Thu, 17 Mar 2022 19:51:14 -0600 Subject: [PATCH 12/21] Update examples to include one for receiving and requesting device verification --- examples/README.md | 22 +++++ examples/request-device-verification.json | 92 ++++++++++++++++++ examples/request-device-verification.png | Bin 0 -> 17055 bytes .../start-accept-verification-from-user.json | 86 ++++++++++++++++ .../start-accept-verification-from-user.png | Bin 0 -> 15513 bytes 5 files changed, 200 insertions(+) create mode 100644 examples/request-device-verification.json create mode 100644 examples/request-device-verification.png create mode 100644 examples/start-accept-verification-from-user.json create mode 100644 examples/start-accept-verification-from-user.png diff --git a/examples/README.md b/examples/README.md index 5456690..569ad12 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,6 +10,8 @@ Build something cool with these nodes? Feel free to submit a pull request to sha - [Create User with Shared Secret Registration](#create-user-with-shared-secret-registration) - [Create/Edit Synapse User](#createedit-synapse-user) - [Use function node to run any command](#use-function-node-to-run-any-command) +- [Start and accept device verification from specific user](#start-and-accept-device-verification-from-specific-user) +- [Request device verification & immediately accept](#request-device-verification--immediately-accept) - [Respond to "ping" with "pong"](#respond-to-ping-with-pong) - [Respond to "html" with an HTML message](#respond-to-html-with-an-html-message) - [Respond to "image" with an uploaded image](#respond-to-image-with-an-uploaded-image) @@ -49,6 +51,26 @@ Allows an administrator to create or modify a user account with a specified `msg ![add-user-with-admin-user.png](add-user-with-admin-user.png) +### Request device verification & immediately accept + +[View JSON](request-device-verification.json) + +Edit the inject node to match the details of a user & device you would like to request verification from. +After the end user starts verification the bot automatically accepts the result (note: you should be validating the result and not just blindly accepting them, this is just an example) + +![add-user-with-admin-user.png](request-device-verification.png) + + +### Start and accept device verification from specific user + +[View JSON](start-accept-verification-from-user.json) + +Edit the switch node labeled "is from me" to match whatever user ID you would like to accept verification requests from. +After verification starts the bot automatically accepts the result (note: you should be validating the result and not just blindly accepting them, this is just an example) + +![add-user-with-admin-user.png](start-accept-verification-from-user.png) + + ### Use function node to run any command [View JSON](custom-redact-function-node.json) diff --git a/examples/request-device-verification.json b/examples/request-device-verification.json new file mode 100644 index 0000000..1029338 --- /dev/null +++ b/examples/request-device-verification.json @@ -0,0 +1,92 @@ +[ + { + "id": "9345e8c42e327dba", + "type": "matrix-device-verification", + "z": "f025a8b9fbd1b054", + "name": "", + "server": null, + "mode": "request", + "inputs": 1, + "outputs": 2, + "x": 480, + "y": 1660, + "wires": [ + [ + "b676082d56430aec" + ], + [] + ] + }, + { + "id": "b676082d56430aec", + "type": "matrix-device-verification", + "z": "f025a8b9fbd1b054", + "name": "", + "server": null, + "mode": "start", + "inputs": 1, + "outputs": 1, + "x": 740, + "y": 1660, + "wires": [ + [ + "23a0225f2f2615a3" + ] + ] + }, + { + "id": "23a0225f2f2615a3", + "type": "matrix-device-verification", + "z": "f025a8b9fbd1b054", + "name": "", + "server": null, + "mode": "accept", + "inputs": 1, + "outputs": 1, + "x": 970, + "y": 1660, + "wires": [ + [] + ] + }, + { + "id": "3eced60b58c999eb", + "type": "inject", + "z": "f025a8b9fbd1b054", + "name": "", + "props": [ + { + "p": "userId", + "v": "@bot:example.com", + "vt": "str" + }, + { + "p": "devices", + "v": "[\"ZRRJKASJDUK\"]", + "vt": "json" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "x": 290, + "y": 1660, + "wires": [ + [ + "9345e8c42e327dba" + ] + ] + }, + { + "id": "f58ceba2a8819c09", + "type": "comment", + "z": "f025a8b9fbd1b054", + "name": "Request verification from a specific userId and device", + "info": "", + "x": 440, + "y": 1620, + "wires": [] + } +] \ No newline at end of file diff --git a/examples/request-device-verification.png b/examples/request-device-verification.png new file mode 100644 index 0000000000000000000000000000000000000000..0c1cc114c16f46e972499a46f5338c0169b0405d GIT binary patch literal 17055 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 Date: Thu, 17 Mar 2022 19:54:13 -0600 Subject: [PATCH 13/21] Update examples to move function example above verification ones. --- examples/README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/README.md b/examples/README.md index 569ad12..845ab78 100644 --- a/examples/README.md +++ b/examples/README.md @@ -51,6 +51,19 @@ Allows an administrator to create or modify a user account with a specified `msg ![add-user-with-admin-user.png](add-user-with-admin-user.png) +### Use function node to run any command + +[View JSON](custom-redact-function-node.json) + +If we do not have a node for something you want to do you can do this manually with a function node. We now have a node for removing events but this is still a good example. + +**Note:** You should make sure to catch any errors in your function node otherwise you could cause Node-RED to crash. + +To view what sort of functions you have access to check out the `client.ts` file from `matrix-js-sdk` [here](https://github.com/matrix-org/matrix-js-sdk/blob/master/src/client.ts). + +![custom-redact-function-node.png](custom-redact-function-node.png) + + ### Request device verification & immediately accept [View JSON](request-device-verification.json) @@ -70,19 +83,6 @@ After verification starts the bot automatically accepts the result (note: you sh ![add-user-with-admin-user.png](start-accept-verification-from-user.png) - -### Use function node to run any command - -[View JSON](custom-redact-function-node.json) - -If we do not have a node for something you want to do (such as redacting events/messages) you can do this manually with a function node. - -**Note:** You should make sure to catch any errors in your function node otherwise you could cause Node-RED to crash. - -To view what sort of functions you have access to check out the `client.ts` file from `matrix-js-sdk` [here](https://github.com/matrix-org/matrix-js-sdk/blob/master/src/client.ts). - -![custom-redact-function-node.png](custom-redact-function-node.png) - ### Respond to "ping" with "pong" [View JSON](respond-ping-pong.json) From 595fbca3df0697130c69f725f527183c37db6625 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Thu, 17 Mar 2022 19:58:25 -0600 Subject: [PATCH 14/21] - Update main readme with new verification notes --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bb78883..3ff77e9 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,9 @@ You are not limited by just the nodes we have created. If you turn on global acc View an example [here](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#use-function-node-to-run-any-command) ### End-to-End Encryption Notes -Currently, this module has no way of getting encryption keys from other devices on the same account. Therefore it is recommended you use the bot exclusively with Node-RED after it's creation. Failure to do so will lead to your bot being unable to receive messages from e2ee rooms it joined from another client. Shared secret registration makes this super easy since it returns a token and device ID. +It is recommended you use the bot exclusively with Node-RED after it's creation if using e2ee. Failure to do so will lead to your bot being unable to receive messages from e2ee rooms it joined from another client. Shared secret registration makes this super easy since it returns a token and device ID. + +We now have a device verification node that will help in sharing keys (check the [examples](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#readme) for more info). This node is currently in beta and is still experimental. This module stores a folder in your Node-RED directory called `matrix-client-storage` and is it vital that you periodically back this up if you are using e2ee. This is where the client stores all the keys necessary to decrypt messages and if lost you will lose access to e2e rooms. If you move your client to another NR install make sure to migrate this folder as well (and do not let both the old and new client run at same time). From fef40f4ea998d3437a15ba9381568b2236ecc3f4 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Thu, 17 Mar 2022 20:03:48 -0600 Subject: [PATCH 15/21] - Update matrix-device-verification node description with super basic info on how to use it --- src/matrix-device-verification.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/matrix-device-verification.html b/src/matrix-device-verification.html index e7fe732..a4ca23b 100644 --- a/src/matrix-device-verification.html +++ b/src/matrix-device-verification.html @@ -92,8 +92,16 @@ -

+ - + -
+
Enter a single room, comma separated list of rooms, or leave blank to get from all
@@ -113,6 +102,17 @@ Accept images m.image
+ -
+ -
+ - + -
@@ -52,6 +40,17 @@
Must be a valid MIME Type (ex: application/pdf) or left empty
+ -
@@ -52,6 +40,17 @@
Must be a valid MIME Type (ex: image/png) or left empty
+ -
@@ -91,6 +79,17 @@
+ -
User must be an admin to use this endpoint.
+ - - - - diff --git a/src/matrix-device-verification.js b/src/matrix-device-verification.js deleted file mode 100644 index 98a7b7d..0000000 --- a/src/matrix-device-verification.js +++ /dev/null @@ -1,234 +0,0 @@ -const {Phase} = require("matrix-js-sdk/lib/crypto/verification/request/VerificationRequest"); -const {CryptoEvent} = require("matrix-js-sdk/lib/crypto"); - -module.exports = function(RED) { - const verificationRequests = new Map(); - - function MatrixDeviceVerification(n) { - RED.nodes.createNode(this, n); - - var node = this; - - this.name = n.name; - this.server = RED.nodes.getNode(n.server); - this.mode = n.mode; - - if (!node.server) { - node.warn("No configuration node"); - return; - } - - if(!node.server.e2ee) { - node.error("End-to-end encryption needs to be enabled to use this."); - } - - node.status({ fill: "red", shape: "ring", text: "disconnected" }); - - node.server.on("disconnected", function(){ - node.status({ fill: "red", shape: "ring", text: "disconnected" }); - }); - - node.server.on("connected", function() { - node.status({ fill: "green", shape: "ring", text: "connected" }); - }); - - function getKeyByValue(object, value) { - return Object.keys(object).find(key => object[key] === value); - } - - switch(node.mode) { - default: - node.error("Node not configured with a mode"); - break; - - case 'request': - node.on('input', async function(msg){ - if(!msg.userId) { - node.error("msg.userId is required for start verification mode"); - } - - node.server.matrixClient.requestVerification(msg.userId, msg.devices || null) - .then(function(e) { - node.log("Successfully requested verification"); - let verifyRequestId = msg.userId + ':' + e.channel.deviceId; - verificationRequests.set(verifyRequestId, e); - node.send({ - verifyRequestId: verifyRequestId, // internally used to reference between nodes - verifyMethods: e.methods, - userId: msg.userId, - deviceIds: e.channel.devices, - selfVerification: e.isSelfVerification, - phase: getKeyByValue(Phase, e.phase) - }); - }) - .catch(function(e){ - node.warn("Error requesting device verification: " + e); - msg.error = e; - node.send([null, msg]); - }); - }); - break; - - case 'receive': - /** - * Fires when a key verification is requested. - * @event module:client~MatrixClient#"crypto.verification.request" - * @param {object} data - * @param {MatrixEvent} data.event the original verification request message - * @param {Array} data.methods the verification methods that can be used - * @param {Number} data.timeout the amount of milliseconds that should be waited - * before cancelling the request automatically. - * @param {Function} data.beginKeyVerification a function to call if a key - * verification should be performed. The function takes one argument: the - * name of the key verification method (taken from data.methods) to use. - * @param {Function} data.cancel a function to call if the key verification is - * rejected. - */ - node.server.matrixClient.on(CryptoEvent.VerificationRequest, async function(data){ - if(data.phase === Phase.Cancelled || data.phase === Phase.Done) { - return; - } - - if(data.requested || true) { - let verifyRequestId = data.targetDevice.userId + ':' + data.targetDevice.deviceId; - verificationRequests.set(verifyRequestId, data); - node.send({ - verifyRequestId: verifyRequestId, // internally used to reference between nodes - verifyMethods: data.methods, - userId: data.targetDevice.userId, - deviceId: data.targetDevice.deviceId, - selfVerification: data.isSelfVerification, - phase: getKeyByValue(Phase, data.phase) - }); - } - }); - - node.on('close', function(done) { - // clear verification requests - verificationRequests.clear(); - done(); - }); - break; - - case 'start': - node.on('input', async function(msg){ - if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) { - // if(msg.userId && msg.deviceId) { - // node.server.beginKeyVerification("m.sas.v1", msg.userId, msg.deviceId); - // } - - node.error("invalid verification request (invalid msg.verifyRequestId): " + (msg.verifyRequestId || null)); - } - - var data = verificationRequests.get(msg.verifyRequestId); - if(msg.cancel) { - await data._verifier.cancel(); - verificationRequests.delete(msg.verifyRequestId); - } else { - try { - data.on('change', async function() { - var that = this; - if(this.phase === Phase.Started) { - let verifierCancel = function(){ - let verifyRequestId = that.targetDevice.userId + ':' + that.targetDevice.deviceId; - if(verificationRequests.has(verifyRequestId)) { - verificationRequests.delete(verifyRequestId); - } - }; - - data._verifier.on('cancel', function(e){ - node.warn("Device verification cancelled " + e); - verifierCancel(); - }); - - let show_sas = function(e) { - // e = { - // sas: { - // decimal: [ 8641, 3153, 2357 ], - // emoji: [ - // [Array], [Array], - // [Array], [Array], - // [Array], [Array], - // [Array] - // ] - // }, - // confirm: [AsyncFunction: confirm], - // cancel: [Function: cancel], - // mismatch: [Function: mismatch] - // } - msg.payload = e.sas; - msg.emojis = e.sas.emoji.map(function(emoji, i) { - return emoji[0]; - }); - msg.emojis_text = e.sas.emoji.map(function(emoji, i) { - return emoji[1]; - }); - node.send(msg); - }; - data._verifier.on('show_sas', show_sas); - data._verifier.verify() - .then(function(e){ - data._verifier.off('show_sas', show_sas); - data._verifier.done(); - }, function(e) { - verifierCancel(); - node.warn(e); - // @todo return over second output - }); - } - }); - - data.emit("change"); - await data.accept(); - } catch(e) { - console.log("ERROR", e); - } - } - }); - break; - - case 'cancel': - node.on('input', async function(msg){ - if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) { - node.error("Invalid verification request: " + (msg.verifyRequestId || null)); - } - - var data = verificationRequests.get(msg.verifyRequestId); - if(data) { - data.cancel() - .then(function(e){ - node.send([msg, null]); - }) - .catch(function(e) { - msg.error = e; - node.send([null, msg]); - }); - } - }); - break; - - case 'accept': - node.on('input', async function(msg){ - if(!msg.verifyRequestId || !verificationRequests.has(msg.verifyRequestId)) { - node.error("Invalid verification request: " + (msg.verifyRequestId || null)); - } - - var data = verificationRequests.get(msg.verifyRequestId); - if(data._verifier && data._verifier.sasEvent) { - data._verifier.sasEvent.confirm() - .then(function(e){ - node.send([msg, null]); - }) - .catch(function(e) { - msg.error = e; - node.send([null, msg]); - }); - } else { - node.error("Verification must be started"); - } - }); - break; - } - } - RED.nodes.registerType("matrix-device-verification", MatrixDeviceVerification); -} \ No newline at end of file diff --git a/src/matrix-server-config.html b/src/matrix-server-config.html index fee1d90..97259cb 100644 --- a/src/matrix-server-config.html +++ b/src/matrix-server-config.html @@ -30,7 +30,6 @@ deviceLabel: { type: "text", required: false }, accessToken: { type: "password", required: true }, deviceId: { type: "text", required: false }, - secretStoragePassphrase: { type: "password", required: false }, url: { type: "text", required: true }, }, defaults: { @@ -87,14 +86,6 @@ You can either provide/generate an access token yourself or use the login button above to do it automatically. View the node docs to figure out how to generate an Access Token manually. If you generated a user with shared secret registration you will already have an access token you can place here. -
- - -
-
- You can either provide/generate an access token yourself or use the login button above to do it automatically. View the node docs to figure out how to generate an Access Token manually. If you generated a user with shared secret registration you will already have an access token you can place here. -
-
diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index 99b2c82..0403916 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -30,10 +30,9 @@ module.exports = function(RED) { this.userId = this.credentials.userId; this.deviceLabel = this.credentials.deviceLabel || null; this.deviceId = this.credentials.deviceId || null; - this.secretStoragePassphrase = this.credentials.secretStoragePassphrase || null; this.url = this.credentials.url; this.autoAcceptRoomInvites = n.autoAcceptRoomInvites; - this.e2ee = this.enableE2ee = n.enableE2ee || false; + this.e2ee = n.enableE2ee || false; this.globalAccess = n.global; this.initializedAt = new Date(); @@ -43,53 +42,6 @@ module.exports = function(RED) { return; } - let cryptoCallbacks = undefined; - if(node.enableE2ee && node.secretStoragePassphrase && false) { - // cryptoCallbacks = { - // getSecretStorageKey: async function({ keys }, name) { - // const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; - // for (const [keyName, keyInfo] of Object.entries(keys)) { - // const key = await deriveKey(node.secretStoragePassphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations); - // // const key = Uint8Array.of(36, 47, 159, 193, 29, 188, 180, 86, 189, 180, 207, 101, 79, 255, 93, 159, 228, 43, 160, 158, 98, 209, 84, 196, 137, 122, 119, 118, 11, 131, 75, 87); - // const { mac } = await encryptAES(ZERO_STR, key, "", keyInfo.iv); - // if (keyInfo.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '')) { - // return [keyName, key]; - // } - // } - // return null; - // }, - // async getDehydrationKey() { - // return node.secretStoragePassphrase; - // }, - // async generateDehydrationKey() { - // return {key: node.secretStoragePassphrase}; - // } - // }; - - cryptoCallbacks = { - getSecretStorageKey: async ({ keys }) => { - const backupPassphrase = node.secretStoragePassphrase; - if (!backupPassphrase) { - node.WARN("Missing secret storage key"); - return null; - } - let keyId = await node.matrixClient.getDefaultSecretStorageKeyId(); - if (keyId && !keys[keyId]) { - keyId = undefined; - } - if (!keyId) { - keyId = keys[0][0]; - } - const backupInfo = await node.matrixClient.getKeyBackupVersion(); - const key = await node.matrixClient.keyBackupKeyFromPassword( - backupPassphrase, - backupInfo - ); - return [keyId, key]; - }, - } - } - let localStorageDir = storageDir + '/' + MatrixFolderNameFromUserId(this.userId), localStorage = new LocalStorage(localStorageDir), initialSetup = false; @@ -112,16 +64,6 @@ module.exports = function(RED) { node.log("Matrix server connection ready."); node.emit("connected"); if(!initialSetup) { - if(node.enableE2ee && node.secretStoragePassphrase && !await node.matrixClient.isCrossSigningReady() && false) { - // bootstrap cross-signing - await node.matrixClient.bootstrapCrossSigning({ - // maybe we can skip this? - authUploadDeviceSigningKeys: () => { - return true; - } - }); - } - // store Device ID internally let stored_device_id = getStoredDeviceId(localStorage), device_id = this.matrixClient.getDeviceId(); @@ -180,8 +122,7 @@ module.exports = function(RED) { cryptoStore: new LocalStorageCryptoStore(localStorage), userId: this.userId, deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined, - verificationMethods: ["m.sas.v1"], - cryptoCallbacks: cryptoCallbacks + // verificationMethods: ["m.sas.v1"] }); // set globally if configured to do so From 4e93b7253e6b24fad637cd49dcea32db9e779686 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Fri, 18 Mar 2022 13:37:24 -0600 Subject: [PATCH 20/21] Revert examples --- examples/README.md | 20 ---- examples/request-device-verification.json | 92 ------------------ examples/request-device-verification.png | Bin 17055 -> 0 bytes .../start-accept-verification-from-user.json | 86 ---------------- .../start-accept-verification-from-user.png | Bin 15513 -> 0 bytes 5 files changed, 198 deletions(-) delete mode 100644 examples/request-device-verification.json delete mode 100644 examples/request-device-verification.png delete mode 100644 examples/start-accept-verification-from-user.json delete mode 100644 examples/start-accept-verification-from-user.png diff --git a/examples/README.md b/examples/README.md index 845ab78..95de200 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,8 +10,6 @@ Build something cool with these nodes? Feel free to submit a pull request to sha - [Create User with Shared Secret Registration](#create-user-with-shared-secret-registration) - [Create/Edit Synapse User](#createedit-synapse-user) - [Use function node to run any command](#use-function-node-to-run-any-command) -- [Start and accept device verification from specific user](#start-and-accept-device-verification-from-specific-user) -- [Request device verification & immediately accept](#request-device-verification--immediately-accept) - [Respond to "ping" with "pong"](#respond-to-ping-with-pong) - [Respond to "html" with an HTML message](#respond-to-html-with-an-html-message) - [Respond to "image" with an uploaded image](#respond-to-image-with-an-uploaded-image) @@ -64,24 +62,6 @@ To view what sort of functions you have access to check out the `client.ts` file ![custom-redact-function-node.png](custom-redact-function-node.png) -### Request device verification & immediately accept - -[View JSON](request-device-verification.json) - -Edit the inject node to match the details of a user & device you would like to request verification from. -After the end user starts verification the bot automatically accepts the result (note: you should be validating the result and not just blindly accepting them, this is just an example) - -![add-user-with-admin-user.png](request-device-verification.png) - - -### Start and accept device verification from specific user - -[View JSON](start-accept-verification-from-user.json) - -Edit the switch node labeled "is from me" to match whatever user ID you would like to accept verification requests from. -After verification starts the bot automatically accepts the result (note: you should be validating the result and not just blindly accepting them, this is just an example) - -![add-user-with-admin-user.png](start-accept-verification-from-user.png) ### Respond to "ping" with "pong" diff --git a/examples/request-device-verification.json b/examples/request-device-verification.json deleted file mode 100644 index 1029338..0000000 --- a/examples/request-device-verification.json +++ /dev/null @@ -1,92 +0,0 @@ -[ - { - "id": "9345e8c42e327dba", - "type": "matrix-device-verification", - "z": "f025a8b9fbd1b054", - "name": "", - "server": null, - "mode": "request", - "inputs": 1, - "outputs": 2, - "x": 480, - "y": 1660, - "wires": [ - [ - "b676082d56430aec" - ], - [] - ] - }, - { - "id": "b676082d56430aec", - "type": "matrix-device-verification", - "z": "f025a8b9fbd1b054", - "name": "", - "server": null, - "mode": "start", - "inputs": 1, - "outputs": 1, - "x": 740, - "y": 1660, - "wires": [ - [ - "23a0225f2f2615a3" - ] - ] - }, - { - "id": "23a0225f2f2615a3", - "type": "matrix-device-verification", - "z": "f025a8b9fbd1b054", - "name": "", - "server": null, - "mode": "accept", - "inputs": 1, - "outputs": 1, - "x": 970, - "y": 1660, - "wires": [ - [] - ] - }, - { - "id": "3eced60b58c999eb", - "type": "inject", - "z": "f025a8b9fbd1b054", - "name": "", - "props": [ - { - "p": "userId", - "v": "@bot:example.com", - "vt": "str" - }, - { - "p": "devices", - "v": "[\"ZRRJKASJDUK\"]", - "vt": "json" - } - ], - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": 0.1, - "topic": "", - "x": 290, - "y": 1660, - "wires": [ - [ - "9345e8c42e327dba" - ] - ] - }, - { - "id": "f58ceba2a8819c09", - "type": "comment", - "z": "f025a8b9fbd1b054", - "name": "Request verification from a specific userId and device", - "info": "", - "x": 440, - "y": 1620, - "wires": [] - } -] \ No newline at end of file diff --git a/examples/request-device-verification.png b/examples/request-device-verification.png deleted file mode 100644 index 0c1cc114c16f46e972499a46f5338c0169b0405d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17055 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 Date: Fri, 18 Mar 2022 13:49:38 -0600 Subject: [PATCH 21/21] Version 0.5.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b450128..090bf77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red-contrib-matrix-chat", - "version": "0.5.6", + "version": "0.5.8", "description": "Matrix chat server client for Node-RED", "dependencies": { "fs-extra": "^10.0.1",