diff --git a/src/matrix-device-verify.js b/src/matrix-device-verify.js index 673e9b8..bb7c4ed 100644 --- a/src/matrix-device-verify.js +++ b/src/matrix-device-verify.js @@ -39,45 +39,23 @@ module.exports = function(RED) { * rejected. */ node.server.matrixClient.on("crypto.verification.request", async function(data){ - console.log("[######### crypto.verification.request #########]", data.phase, data.methods); - if(data.isSelfVerification) { - 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, - type: 'crypto.verification.request', - }); - // data.on('change', async function() { - // console.log("VerificationRequest.change", this, this.phase); - // - // if(this.phase === 4) { - // data._verifier.on('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] - // // } - // console.log("show_sas event", e); - // }) - // await data._verifier.verify(); - // } - // }); - // - // await data.accept(); - } + console.log("[######### crypto.verification.request #########]", data.phase, data); + + if(data.phase === 5 || data.phase === 6) { + 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, + type: 'crypto.verification.request', + selfVerification: data.isSelfVerification + }); } }); } @@ -132,7 +110,7 @@ module.exports = function(RED) { var that = this; console.log("[##### VERIFICATION PHASE CHANGE #######]", this.phase); if(this.phase === 4) { - var verifierCancel = function(){ + let verifierCancel = function(){ let verifyRequestId = that.targetDevice.userId + ':' + that.targetDevice.deviceId; if(verificationRequests.has(verifyRequestId)) { verificationRequests.delete(verifyRequestId); @@ -279,6 +257,9 @@ module.exports = function(RED) { data._verifier.sasEvent.confirm() .then(function(e){ console.log("!!!!!!!! CONFIRMED VERIFY", e); + }) + .catch(function(e) { + console.log("!!!!!!!! CONFIRMED VERIFY FAILED", e); }); } else { console.log("Verification must be started", data); diff --git a/src/matrix-secret-storage.js b/src/matrix-secret-storage.js index 9e11a18..63a7c8d 100644 --- a/src/matrix-secret-storage.js +++ b/src/matrix-secret-storage.js @@ -59,11 +59,11 @@ module.exports = function(RED) { // maybe we can skip this? // authUploadDeviceSigningKeys: this._doBootstrapUIAuth, }); - const backupState = await node.server.matrixClient.getKeyBackupVersion(); + const backupInfo = await node.server.matrixClient.getKeyBackupVersion(); await node.server.matrixClient.bootstrapSecretStorage({ createSecretStorageKey: async () => this._recoveryKey, - keyBackupInfo: backupState.backupInfo, - setupNewKeyBackup: !backupState.backupInfo, + keyBackupInfo: backupInfo, + setupNewKeyBackup: !backupInfo, getKeyBackupPassphrase: () => { // We may already have the backup key if we earlier went // through the restore backup path, so pass it along diff --git a/src/matrix-server-config.html b/src/matrix-server-config.html index a36d4b6..9bb4194 100644 --- a/src/matrix-server-config.html +++ b/src/matrix-server-config.html @@ -29,7 +29,8 @@ userId: { type: "text", required: true }, deviceLabel: { type: "text", required: false }, accessToken: { type: "password", required: true }, - deviceId: { type: "text", required: false }, + deviceId: { type: "text", required: true }, + secureStoragePassphrase: { type: "text", required: false }, url: { type: "text", required: true } }, defaults: { @@ -95,6 +96,14 @@ WARNING: If you change this after the client has already initialized you will break encryption. Your Device ID is tied to your encryption keys. +
+ + +
+
+ If set secure storage will be setup with this passphrase. If secure storage already exists on the account it will attempt to decrypt it. Leave blank to skip. +
+
diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index 90fdad0..6cf654e 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -4,6 +4,7 @@ const sdk = require("matrix-js-sdk"); const { resolve } = require('path'); const { LocalStorage } = require('node-localstorage'); const { LocalStorageCryptoStore } = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store'); +const {deriveKey} = require("matrix-js-sdk/lib/crypto/key_passphrase"); module.exports = function(RED) { function MatrixFolderNameFromUserId(name) { @@ -27,13 +28,14 @@ module.exports = function(RED) { this.userId = this.credentials.userId; this.deviceLabel = this.credentials.deviceLabel || null; this.deviceId = this.credentials.deviceId || null; + this.secureStoragePassphrase = this.credentials.secureStoragePassphrase || null; this.url = this.credentials.url; this.autoAcceptRoomInvites = n.autoAcceptRoomInvites; this.enableE2ee = n.enableE2ee || false; this.e2ee = (this.enableE2ee && this.deviceId); this.globalAccess = n.global; this.initializedAt = new Date(); - + if(!this.userId) { node.log("Matrix connection failed: missing user ID in configuration."); return; @@ -50,7 +52,7 @@ module.exports = function(RED) { } else if(!this.url) { node.error("Matrix connection failed: missing server URL in configuration."); } else { - node.setConnected = function(connected, cb) { + node.setConnected = async function(connected, cb) { if (node.connected !== connected) { node.connected = connected; if(typeof cb === 'function') { @@ -92,6 +94,13 @@ module.exports = function(RED) { ); } + try { + await accessSecretStorage(function(){}); + } catch(e) { + node.error("secret storage bootstrap failure: " + e); + console.log("secret storage bootstrap failure: ", e); + } + initialSetup = true; } } else { @@ -107,14 +116,70 @@ module.exports = function(RED) { fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist upgradeDirectoryIfNecessary(node, storageDir); + node.matrixClient = sdk.createClient({ baseUrl: this.url, accessToken: this.credentials.accessToken, sessionStore: new sdk.WebStorageSessionStore(localStorage), cryptoStore: new LocalStorageCryptoStore(localStorage), userId: this.userId, - deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined - verificationMethods: ["m.sas.v1"] + deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined, + verificationMethods: ["m.sas.v1"], + // cryptoCallbacks: { + // getSecretStorageKey: async function( + // { keys: keyInfos }, + // ssssItemName, + // ){ + // const cli = node.matrixClient; + // let keyId = await cli.getDefaultSecretStorageKeyId(); + // // console.log("DEFAULT SECRET STORAGE KEY ID: " + keyId, keyInfos); + // // + // // let decodeBase64 = function(base64) { + // // return Buffer.from(base64, "base64"); + // // } + // // return await this.crypto.getSecretStorageKey(keyId); + // let keyInfo; + // if (keyId) { + // // use the default SSSS key if set + // keyInfo = keyInfos[keyId]; + // if (!keyInfo) { + // // if the default key is not available, pretend the default key + // // isn't set + // keyId = undefined; + // } + // } + // if (!keyId) { + // // if no default SSSS key is set, fall back to a heuristic of using the + // // only available key, if only one key is set + // const keyInfoEntries = Object.entries(keyInfos); + // if (keyInfoEntries.length > 1) { + // throw new Error("Multiple storage key requests not implemented"); + // } + // [keyId, keyInfo] = keyInfoEntries[0]; + // } + // + // // Check the in-memory cache + // // if (isCachingAllowed() && secretStorageKeys[keyId]) { + // // return [keyId, secretStorageKeys[keyId]]; + // // } + // + // // if (dehydrationCache.key) { + // // if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) { + // // cacheSecretStorageKey(keyId, keyInfo, dehydrationCache.key); + // // return [keyId, dehydrationCache.key]; + // // } + // // } + // + // // const backupInfo = await node.matrixClient.getKeyBackupVersion(); + // const backupInfo = await node.matrixClient.getAccountDataFromServer( + // "m.secret_storage.key." + keyId + // ); + // + // // if(await cli.checkSecretStorageKey(key, keyInfo)) { + // // } + // return [keyId, await node.matrixClient.keyBackupKeyFromPassword(node.secureStoragePassphrase, backupInfo)] ; + // } + // } }); // set globally if configured to do so @@ -304,13 +369,88 @@ module.exports = function(RED) { stopClient(); }); + + /** + * This helper should be used whenever you need to access secret storage. It + * ensures that secret storage (and also cross-signing since they each depend on + * each other in a cycle of sorts) have been bootstrapped before running the + * provided function. + * + * Bootstrapping secret storage may take one of these paths: + * 1. Create secret storage from a passphrase and store cross-signing keys + * in secret storage. + * 2. Access existing secret storage by requesting passphrase and accessing + * cross-signing keys as needed. + * 3. All keys are loaded and there's nothing to do. + * + * @param {Function} [func] An operation to perform once secret storage has been + * bootstrapped. Optional. + * @param {boolean} [forceReset] Reset secret storage even if it's already set up + */ + let accessSecretStorage = async function(func = async () => { }, forceReset = false) { + // only do this if we have a secure storage password + if(!node.secureStoragePassphrase) { + return; + } + + const recoveryKey = await node.matrixClient.createRecoveryKeyFromPassphrase(node.secureStoragePassphrase); + const cli = node.matrixClient; + try { + if (!(await cli.hasSecretStorageKey()) || forceReset) { + // For password authentication users after 2020-09, this cross-signing + // step will be a no-op since it is now setup during registration or login + // when needed. We should keep this here to cover other cases such as: + // * Users with existing sessions prior to 2020-09 changes + // * SSO authentication users which require interactive auth to upload + // keys (and also happen to skip all post-authentication flows at the + // moment via token login) + if(!await node.matrixClient.isCrossSigningReady()) { + await node.matrixClient.bootstrapCrossSigning({ + // maybe we can skip this? + authUploadDeviceSigningKeys: () => { + return true; + } + }); + } + + const backupInfo = await node.matrixClient.getKeyBackupVersion(); + await node.matrixClient.bootstrapSecretStorage({ + createSecretStorageKey: async () => recoveryKey, + keyBackupInfo: backupInfo, + setupNewKeyBackup: !backupInfo, + getKeyBackupPassphrase: () => { + return recoveryKey; + }, + }); + } else { + await node.matrixClient.bootstrapSecretStorage({ + getKeyBackupPassphrase: async () => recoveryKey, + }); + } + + // `return await` needed here to ensure `finally` block runs after the + // inner operation completes. + return await func(); + } catch (e) { + node.error("Secret storage init failure: " + e); + } + }; + async function run() { try { - if(node.e2ee){ + if(node.e2ee && node.matrixClient.initCrypto){ node.log("Initializing crypto..."); - await node.matrixClient.initCrypto(); - node.matrixClient.setGlobalErrorOnUnknownDevices(false); + try { + await node.matrixClient.initCrypto(); + node.matrixClient.setGlobalErrorOnUnknownDevices(false); + node.matrixClient.setCryptoTrustCrossSignedDevices(true); // false = manually verify sessions + // await tryToUnlockSecretStorageWithDehydrationKey(this.matrixClient); + } catch (e) { + node.error("Failed to initialize crypto: " + e); + console.log(e); + } } + node.log("Connecting to Matrix server..."); await node.matrixClient.startClient({ initialSyncLimit: 8 @@ -350,6 +490,7 @@ module.exports = function(RED) { userId: { type: "text", required: true }, accessToken: { type: "text", required: true }, deviceId: { type: "text", required: false }, + secureStoragePassphrase: { type: "text", required: false }, url: { type: "text", required: true } } });