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).
diff --git a/examples/README.md b/examples/README.md
index 5456690..845ab78 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)
@@ -53,7 +55,7 @@ Allows an administrator to create or modify a user account with a specified `msg
[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.
+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.
@@ -61,6 +63,26 @@ 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"
[View JSON](respond-ping-pong.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 0000000..0c1cc11
Binary files /dev/null and b/examples/request-device-verification.png differ
diff --git a/examples/start-accept-verification-from-user.json b/examples/start-accept-verification-from-user.json
new file mode 100644
index 0000000..af1255d
--- /dev/null
+++ b/examples/start-accept-verification-from-user.json
@@ -0,0 +1,86 @@
+[
+ {
+ "id": "5073ca88b21abfb4",
+ "type": "matrix-device-verification",
+ "z": "f025a8b9fbd1b054",
+ "name": "",
+ "server": null,
+ "mode": "receive",
+ "inputs": 0,
+ "outputs": 1,
+ "x": 350,
+ "y": 1540,
+ "wires": [
+ [
+ "b76c1d185c2793a0"
+ ]
+ ]
+ },
+ {
+ "id": "05947740ced04e2c",
+ "type": "matrix-device-verification",
+ "z": "f025a8b9fbd1b054",
+ "name": "",
+ "server": null,
+ "mode": "start",
+ "inputs": 1,
+ "outputs": 1,
+ "x": 740,
+ "y": 1540,
+ "wires": [
+ [
+ "b3158c0779b72b41"
+ ]
+ ]
+ },
+ {
+ "id": "b76c1d185c2793a0",
+ "type": "switch",
+ "z": "f025a8b9fbd1b054",
+ "name": "is from me",
+ "property": "userId",
+ "propertyType": "msg",
+ "rules": [
+ {
+ "t": "eq",
+ "v": "@skylord123:skylar.tech",
+ "vt": "str"
+ }
+ ],
+ "checkall": "true",
+ "repair": false,
+ "outputs": 1,
+ "x": 550,
+ "y": 1540,
+ "wires": [
+ [
+ "05947740ced04e2c"
+ ]
+ ]
+ },
+ {
+ "id": "b3158c0779b72b41",
+ "type": "matrix-device-verification",
+ "z": "f025a8b9fbd1b054",
+ "name": "",
+ "server": null,
+ "mode": "accept",
+ "inputs": 1,
+ "outputs": 1,
+ "x": 970,
+ "y": 1540,
+ "wires": [
+ []
+ ]
+ },
+ {
+ "id": "eb8ba0741df1b365",
+ "type": "comment",
+ "z": "f025a8b9fbd1b054",
+ "name": "Accept all device validation from a user",
+ "info": "",
+ "x": 390,
+ "y": 1500,
+ "wires": []
+ }
+]
\ No newline at end of file
diff --git a/examples/start-accept-verification-from-user.png b/examples/start-accept-verification-from-user.png
new file mode 100644
index 0000000..3e10abb
Binary files /dev/null and b/examples/start-accept-verification-from-user.png differ
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..a4ca23b
--- /dev/null
+++ b/src/matrix-device-verification.html
@@ -0,0 +1,240 @@
+
+
+
+
+
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