From d01733c6472c11d93623af82a2e5a63b80318898 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Thu, 17 Mar 2022 19:38:29 -0600 Subject: [PATCH] closes #48 - Device verification can now be requested or received --- package.json | 1 + src/matrix-device-verification.html | 232 +++++++++++++++++++++++++++ src/matrix-device-verification.js | 234 ++++++++++++++++++++++++++++ 3 files changed, 467 insertions(+) create mode 100644 src/matrix-device-verification.html create mode 100644 src/matrix-device-verification.js diff --git a/package.json b/package.json index b8d9665..e55ba57 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "matrix-crypt-file": "src/matrix-crypt-file.js", "matrix-room-kick": "src/matrix-room-kick.js", "matrix-room-ban": "src/matrix-room-ban.js", + "matrix-device-verification": "src/matrix-device-verification.js", "matrix-synapse-users": "src/matrix-synapse-users.js", "matrix-synapse-register": "src/matrix-synapse-register.js", "matrix-synapse-create-edit-user": "src/matrix-synapse-create-edit-user.js", diff --git a/src/matrix-device-verification.html b/src/matrix-device-verification.html new file mode 100644 index 0000000..e7fe732 --- /dev/null +++ b/src/matrix-device-verification.html @@ -0,0 +1,232 @@ + + + + + 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