From 8a4440c314f42b0d5676bd0262948efff66e26b9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 4 Aug 2020 16:06:19 -0400 Subject: [PATCH 01/16] initial work on dehydration --- src/client.js | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/src/client.js b/src/client.js index b505498f4f3..c41a0fa6ba3 100644 --- a/src/client.js +++ b/src/client.js @@ -55,6 +55,7 @@ import {PushProcessor} from "./pushprocessor"; import {encodeBase64, decodeBase64} from "./crypto/olmlib"; import { User } from "./models/user"; import {AutoDiscovery} from "./autodiscovery"; +import anotherjson from "another-json"; const SCROLLBACK_DELAY_MS = 3000; export const CRYPTO_ENABLED = isCryptoAvailable(); @@ -441,6 +442,139 @@ export function MatrixClient(opts) { utils.inherits(MatrixClient, EventEmitter); utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); +/** + * @param {string} loginType + * @param {Object} data + * @param {string} key The key to decrypt with. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.loginWithRehydration = async function(loginType, data, key) { + const loginData = { + "org.matrix.msc2697.restore_device": true, + }; + Object.assign(loginData, data); + + const loginResult = await this.login(loginType, loginData); + + if (!loginResult.device_data) { + console.info("no dehydrated device found"); + return loginResult; + } + + const account = new global.Olm.Account(); + try { + console.log("unpickling device"); + account.unpickle(key, loginResult.device_data); + + const rehydrateResult = await this._http.request( + undefined, + "POST", + "/restore_device", + undefined, + { + rehydrate: true, + dehydration_token: loginResult.dehydration_token, + }, + { + prefix: "/_matrix/client/unstable/org.matrix.msc2697", + }, + ); + + if (rehydrateResult.device_id === loginResult.device_id) { + console.info("using dehydrated device"); + rehydrateResult._olm_account = account; + } + return rehydrateResult; + } catch { + account.free(); + console.warn("could not unpickle"); + return await this._http.request( + undefined, + "POST", + "/restore_device", + undefined, + { + rehydrate: false, + dehydration_token: loginResult.dehydration_token, + }, + { + prefix: "/_matrix/client/unstable/org.matrix.msc2697", + }, + ); + } +}; + +MatrixClient.prototype.dehydrateDevice = async function(key) { + // FIXME: move to crypto/index.js? + const account = new global.Olm.Account(); + account.create(); + const e2eKeys = JSON.parse(account.identity_keys()); + const maxKeys = account.max_number_of_one_time_keys(); + // FIXME: generate in small batches? + account.generate_one_time_keys(maxKeys / 2); + const otks = JSON.parse(account.one_time_keys()); + account.mark_keys_as_published(); + const pickledAccount = account.pickle(key); + + const dehydrateResult = await this._http.authedRequest( + undefined, + "POST", + "/device/dehydrate", + undefined, + { + device_data: pickledAccount, + // FIXME: initial device name? + }, + { + prefix: "/_matrix/client/unstable/org.matrix.msc2697", + }, + ); + + const deviceId = dehydrateResult.device_id; + const deviceKeys = { + algorithms: this._crypto._supportedAlgorithms, + device_id: deviceId, + user_id: this.credentials.userId, + keys: { + [`ed25519:${deviceId}`]: e2eKeys.ed25519, + [`curve25519:${deviceId}`]: e2eKeys.curve25519, + }, + }; + const deviceSignature = account.sign(anotherjson.stringify(deviceKeys)); + deviceKeys.signatures = { + [this.credentials.userId]: { + [`ed25519:${deviceId}`]: deviceSignature, + }, + }; + await this._crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); + + const oneTimeKeys = {}; + for (const [keyId, key] of Object.entries(otks.curve25519)) { + const k = { + key: key, + }; + const otkSignature = account.sign(anotherjson.stringify(k)); + k.signatures = { + [this.credentials.userId]: { + [`ed25519:${deviceId}`]: otkSignature, + }, + }; + oneTimeKeys[`signed_curve25519:${keyId}`] = k; + } + + await this._http.authedRequest( + undefined, + "POST", + "/keys/upload/" + encodeURI(deviceId), + undefined, + { + device_keys: deviceKeys, + one_time_keys: oneTimeKeys, + }, + ); +}; + MatrixClient.prototype.exportDevice = async function() { if (!(this._crypto)) { logger.warn('not exporting device if crypto is not enabled'); From 6e8e3e4150b287548f31d3d3afffc62f4ace69c1 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 17 Aug 2020 18:09:23 -0400 Subject: [PATCH 02/16] use newer version of dehydration proposal and add some comments --- src/client.js | 52 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/src/client.js b/src/client.js index c41a0fa6ba3..681f3722a8d 100644 --- a/src/client.js +++ b/src/client.js @@ -442,14 +442,18 @@ export function MatrixClient(opts) { utils.inherits(MatrixClient, EventEmitter); utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); +const DEHYDRATION_ALGORITHM = "m.dehydration.v1.olm"; + /** - * @param {string} loginType + * Log in, trying to rehydrate a device if available. The client must have + * been initialized with a `cryptoCallback.getDehydrationKey` option. + * + * @param {string} loginType The login type. * @param {Object} data - * @param {string} key The key to decrypt with. - * @return {Promise} Resolves: TODO + * @return {Promise} Resolves: to a login request result. * @return {module:http-api.MatrixError} Rejects: with an error response. */ -MatrixClient.prototype.loginWithRehydration = async function(loginType, data, key) { +MatrixClient.prototype.loginWithRehydration = async function(loginType, data) { const loginData = { "org.matrix.msc2697.restore_device": true, }; @@ -464,8 +468,14 @@ MatrixClient.prototype.loginWithRehydration = async function(loginType, data, ke const account = new global.Olm.Account(); try { - console.log("unpickling device"); - account.unpickle(key, loginResult.device_data); + const deviceData = loginResult.device_data; + if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) { + throw new Error("Wrong algorithm"); + } + const key = await this._cryptoCallbacks.getDehydrationKey(deviceData.passphrase); + console.log("unpickling dehydrated device"); + account.unpickle(key, deviceData.account); + // FIXME: retry asking for key if unpickle fails? const rehydrateResult = await this._http.request( undefined, @@ -505,17 +515,36 @@ MatrixClient.prototype.loginWithRehydration = async function(loginType, data, ke } }; -MatrixClient.prototype.dehydrateDevice = async function(key) { +/** + * Store a new dehydrated device on the server. The client must have been + * initialized with a `cryptoCallbacks.generateDehydrationKey` option. + * + * @return {Promise} A promise that resolves when the dehydrated device is stored. + */ +MatrixClient.prototype.dehydrateDevice = async function() { // FIXME: move to crypto/index.js? + const keyInfo = await this._cryptoCallbacks.generateDehydrationKey(); + // create the account and all the necessary keys const account = new global.Olm.Account(); account.create(); const e2eKeys = JSON.parse(account.identity_keys()); + const maxKeys = account.max_number_of_one_time_keys(); // FIXME: generate in small batches? account.generate_one_time_keys(maxKeys / 2); const otks = JSON.parse(account.one_time_keys()); account.mark_keys_as_published(); - const pickledAccount = account.pickle(key); + + // dehydrate the account and store it on the server + const pickledAccount = account.pickle(keyInfo.key); + + const deviceData = { + algorithm: DEHYDRATION_ALGORITHM, + account: pickledAccount, + }; + if (keyInfo.passphrase) { + deviceData.passphrase = keyInfo.passphrase; + } const dehydrateResult = await this._http.authedRequest( undefined, @@ -523,7 +552,7 @@ MatrixClient.prototype.dehydrateDevice = async function(key) { "/device/dehydrate", undefined, { - device_data: pickledAccount, + device_data: deviceData, // FIXME: initial device name? }, { @@ -531,6 +560,7 @@ MatrixClient.prototype.dehydrateDevice = async function(key) { }, ); + // send the keys to the server const deviceId = dehydrateResult.device_id; const deviceKeys = { algorithms: this._crypto._supportedAlgorithms, @@ -554,10 +584,10 @@ MatrixClient.prototype.dehydrateDevice = async function(key) { const k = { key: key, }; - const otkSignature = account.sign(anotherjson.stringify(k)); + const signature = account.sign(anotherjson.stringify(k)); k.signatures = { [this.credentials.userId]: { - [`ed25519:${deviceId}`]: otkSignature, + [`ed25519:${deviceId}`]: signature, }, }; oneTimeKeys[`signed_curve25519:${keyId}`] = k; From ad85740ae20a4aaa8f128a959ab0ecde47d1ade5 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 19 Aug 2020 18:01:31 -0400 Subject: [PATCH 03/16] allow other login functions to be used --- src/client.js | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/client.js b/src/client.js index 681f3722a8d..20cb617618c 100644 --- a/src/client.js +++ b/src/client.js @@ -448,18 +448,33 @@ const DEHYDRATION_ALGORITHM = "m.dehydration.v1.olm"; * Log in, trying to rehydrate a device if available. The client must have * been initialized with a `cryptoCallback.getDehydrationKey` option. * - * @param {string} loginType The login type. - * @param {Object} data - * @return {Promise} Resolves: to a login request result. + * @param {string} loginFunc The login function to use. e.g. "loginWithPassword" + * @param {...*} args arguments to pass to the login function. + * @return {Promise} Resolves: to a login request result. If a dehydrated + * device was found, it will include an `_olm_account` property, which will be + * an Olm Account to use. * @return {module:http-api.MatrixError} Rejects: with an error response. */ -MatrixClient.prototype.loginWithRehydration = async function(loginType, data) { - const loginData = { - "org.matrix.msc2697.restore_device": true, - }; - Object.assign(loginData, data); +MatrixClient.prototype.loginWithRehydration = async function(loginFunc, ...args) { + const origLogin = this.login; + + function rehydrationLoginWrapper(loginType, data, callback) { + const loginData = { + "org.matrix.msc2697.restore_device": true, + }; + Object.assign(loginData, data); + + return origLogin.call(this, loginType, loginData, callback); // eslint-disable-line babel/no-invalid-this + } + + this.login = rehydrationLoginWrapper; - const loginResult = await this.login(loginType, loginData); + let loginResult; + try { + loginResult = await this[loginFunc](...args); + } finally { + this.login = origLogin; + } if (!loginResult.device_data) { console.info("no dehydrated device found"); From 438861ae5e51000960a2e5dd924f94440339fe4b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 31 Aug 2020 15:44:29 -0400 Subject: [PATCH 04/16] separate dehydration functions to its own file and cache the key --- src/client.js | 117 +++++++--------------------- src/crypto/dehydration.ts | 158 ++++++++++++++++++++++++++++++++++++++ src/crypto/index.js | 3 + 3 files changed, 188 insertions(+), 90 deletions(-) create mode 100644 src/crypto/dehydration.ts diff --git a/src/client.js b/src/client.js index 20cb617618c..7f04160a07a 100644 --- a/src/client.js +++ b/src/client.js @@ -55,7 +55,7 @@ import {PushProcessor} from "./pushprocessor"; import {encodeBase64, decodeBase64} from "./crypto/olmlib"; import { User } from "./models/user"; import {AutoDiscovery} from "./autodiscovery"; -import anotherjson from "another-json"; +import {DEHYDRATION_ALGORITHM} from "./crypto/dehydration"; const SCROLLBACK_DELAY_MS = 3000; export const CRYPTO_ENABLED = isCryptoAvailable(); @@ -442,13 +442,12 @@ export function MatrixClient(opts) { utils.inherits(MatrixClient, EventEmitter); utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); -const DEHYDRATION_ALGORITHM = "m.dehydration.v1.olm"; - /** * Log in, trying to rehydrate a device if available. The client must have * been initialized with a `cryptoCallback.getDehydrationKey` option. * * @param {string} loginFunc The login function to use. e.g. "loginWithPassword" + * or `null` if using base_apis.login * @param {...*} args arguments to pass to the login function. * @return {Promise} Resolves: to a login request result. If a dehydrated * device was found, it will include an `_olm_account` property, which will be @@ -467,13 +466,17 @@ MatrixClient.prototype.loginWithRehydration = async function(loginFunc, ...args) return origLogin.call(this, loginType, loginData, callback); // eslint-disable-line babel/no-invalid-this } - this.login = rehydrationLoginWrapper; - let loginResult; - try { - loginResult = await this[loginFunc](...args); - } finally { - this.login = origLogin; + if (loginFunc === null) { + loginResult = await rehydrationLoginWrapper.call(this, ...args); + } else { + this.login = rehydrationLoginWrapper; + + try { + loginResult = await this[loginFunc](...args); + } finally { + this.login = origLogin; + } } if (!loginResult.device_data) { @@ -487,10 +490,11 @@ MatrixClient.prototype.loginWithRehydration = async function(loginFunc, ...args) if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) { throw new Error("Wrong algorithm"); } - const key = await this._cryptoCallbacks.getDehydrationKey(deviceData.passphrase); + const key = await this._cryptoCallbacks.getDehydrationKey(deviceData); console.log("unpickling dehydrated device"); account.unpickle(key, deviceData.account); // FIXME: retry asking for key if unpickle fails? + console.log("unpickled device"); const rehydrateResult = await this._http.request( undefined, @@ -509,11 +513,13 @@ MatrixClient.prototype.loginWithRehydration = async function(loginFunc, ...args) if (rehydrateResult.device_id === loginResult.device_id) { console.info("using dehydrated device"); rehydrateResult._olm_account = account; + } else { + console.info("not using dehydrated device"); } return rehydrateResult; - } catch { + } catch (e) { account.free(); - console.warn("could not unpickle"); + console.warn("could not unpickle", e); return await this._http.request( undefined, "POST", @@ -537,87 +543,18 @@ MatrixClient.prototype.loginWithRehydration = async function(loginFunc, ...args) * @return {Promise} A promise that resolves when the dehydrated device is stored. */ MatrixClient.prototype.dehydrateDevice = async function() { - // FIXME: move to crypto/index.js? - const keyInfo = await this._cryptoCallbacks.generateDehydrationKey(); - // create the account and all the necessary keys - const account = new global.Olm.Account(); - account.create(); - const e2eKeys = JSON.parse(account.identity_keys()); - - const maxKeys = account.max_number_of_one_time_keys(); - // FIXME: generate in small batches? - account.generate_one_time_keys(maxKeys / 2); - const otks = JSON.parse(account.one_time_keys()); - account.mark_keys_as_published(); - - // dehydrate the account and store it on the server - const pickledAccount = account.pickle(keyInfo.key); - - const deviceData = { - algorithm: DEHYDRATION_ALGORITHM, - account: pickledAccount, - }; - if (keyInfo.passphrase) { - deviceData.passphrase = keyInfo.passphrase; + if (!this._crypto) { + throw new Error("End-to-end encryption disabled"); } + return await this._crypto._dehydrationManager.dehydrateDevice(); +}; - const dehydrateResult = await this._http.authedRequest( - undefined, - "POST", - "/device/dehydrate", - undefined, - { - device_data: deviceData, - // FIXME: initial device name? - }, - { - prefix: "/_matrix/client/unstable/org.matrix.msc2697", - }, - ); - - // send the keys to the server - const deviceId = dehydrateResult.device_id; - const deviceKeys = { - algorithms: this._crypto._supportedAlgorithms, - device_id: deviceId, - user_id: this.credentials.userId, - keys: { - [`ed25519:${deviceId}`]: e2eKeys.ed25519, - [`curve25519:${deviceId}`]: e2eKeys.curve25519, - }, - }; - const deviceSignature = account.sign(anotherjson.stringify(deviceKeys)); - deviceKeys.signatures = { - [this.credentials.userId]: { - [`ed25519:${deviceId}`]: deviceSignature, - }, - }; - await this._crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); - - const oneTimeKeys = {}; - for (const [keyId, key] of Object.entries(otks.curve25519)) { - const k = { - key: key, - }; - const signature = account.sign(anotherjson.stringify(k)); - k.signatures = { - [this.credentials.userId]: { - [`ed25519:${deviceId}`]: signature, - }, - }; - oneTimeKeys[`signed_curve25519:${keyId}`] = k; +MatrixClient.prototype.cacheDehydrationKey = async function(key, keyInfo) { + if (!(this._crypto)) { + logger.warn('not exporting device if crypto is not enabled'); + return; } - - await this._http.authedRequest( - undefined, - "POST", - "/keys/upload/" + encodeURI(deviceId), - undefined, - { - device_keys: deviceKeys, - one_time_keys: oneTimeKeys, - }, - ); + return await this._crypto._dehydrationManager.cacheDehydrationKey(key, keyInfo); }; MatrixClient.prototype.exportDevice = async function() { diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts new file mode 100644 index 00000000000..f75c6bc2051 --- /dev/null +++ b/src/crypto/dehydration.ts @@ -0,0 +1,158 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {decodeBase64, encodeBase64} from './olmlib'; +import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store'; +import {decryptAES, encryptAES} from './aes'; +import anotherjson from "another-json"; + +export const DEHYDRATION_ALGORITHM = "m.dehydration.v1.olm"; + +export class DehydrationManager { + constructor(private client) {} + async cacheDehydrationKey(key, keyInfo = {}): Promise { + const pickleKey = Buffer.from(this.client._crypto._olmDevice._pickleKey); + key = await encryptAES(encodeBase64(key), pickleKey, DEHYDRATION_ALGORITHM); + this.client._crypto._cryptoStore.doTxn( + 'readwrite', + [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this.client._crypto._cryptoStore.storeSecretStorePrivateKey( + txn, DEHYDRATION_ALGORITHM, {keyInfo, key}, + ); + }, + ); + } + async dehydrateDevice(): Promise { + console.log("Attempting to dehydrate device"); + const {keyInfo, key} = await new Promise((resolve) => { + return this.client._crypto._cryptoStore.doTxn( + 'readonly', + [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this.client._crypto._cryptoStore.getSecretStorePrivateKey( + txn, resolve, DEHYDRATION_ALGORITHM, + ); + }, + ); + }); + // FIXME: abort nicely if key not found + const pickleKey = Buffer.from(this.client._crypto._olmDevice._pickleKey); + const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM); + const decryptedKey = decodeBase64(decrypted); + + console.log("Creating account"); + // create the account and all the necessary keys + const account = new global.Olm.Account(); + account.create(); + const e2eKeys = JSON.parse(account.identity_keys()); + + const maxKeys = account.max_number_of_one_time_keys(); + // FIXME: generate in small batches? + account.generate_one_time_keys(maxKeys / 2); + account.generate_fallback_key(); + const otks = JSON.parse(account.one_time_keys()); + const fallbacks = JSON.parse(account.fallback_key()); + account.mark_keys_as_published(); + + // dehydrate the account and store it on the server + const pickledAccount = account.pickle(decryptedKey); + + const deviceData: {[props: string]: any} = { + algorithm: DEHYDRATION_ALGORITHM, + account: pickledAccount, + }; + if (keyInfo.passphrase) { + deviceData.passphrase = keyInfo.passphrase; + } + + console.log("Uploading account to server"); + const dehydrateResult = await this.client._http.authedRequest( + undefined, + "POST", + "/device/dehydrate", + undefined, + { + device_data: deviceData, + // FIXME: initial device name? + }, + { + prefix: "/_matrix/client/unstable/org.matrix.msc2697", + }, + ); + + // send the keys to the server + const deviceId = dehydrateResult.device_id; + console.log("Preparing device keys", deviceId); + const deviceKeys = { + algorithms: this.client._crypto._supportedAlgorithms, + device_id: deviceId, + user_id: this.client.credentials.userId, + keys: { + [`ed25519:${deviceId}`]: e2eKeys.ed25519, + [`curve25519:${deviceId}`]: e2eKeys.curve25519, + }, + signatures: {}, + }; + const deviceSignature = account.sign(anotherjson.stringify(deviceKeys)); + deviceKeys.signatures = { + [this.client.credentials.userId]: { + [`ed25519:${deviceId}`]: deviceSignature, + }, + }; + await this.client._crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); + + console.log("Preparing one-time keys"); + const oneTimeKeys = {}; + for (const [keyId, key] of Object.entries(otks.curve25519)) { + const k = {key, signatures: {}}; + const signature = account.sign(anotherjson.stringify(k)); + k.signatures = { + [this.client.credentials.userId]: { + [`ed25519:${deviceId}`]: signature, + }, + }; + oneTimeKeys[`signed_curve25519:${keyId}`] = k; + } + + console.log("Preparing fallback keys"); + const fallbackKeys = {}; + for (const [keyId, key] of Object.entries(fallbacks.curve25519)) { + const k = {key, signatures: {}}; + const signature = account.sign(anotherjson.stringify(k)); + k.signatures = { + [this.client.credentials.userId]: { + [`ed25519:${deviceId}`]: signature, + }, + }; + fallbackKeys[`signed_curve25519:${keyId}`] = k; + } + + console.log("Uploading keys to server"); + await this.client._http.authedRequest( + undefined, + "POST", + "/keys/upload/" + encodeURI(deviceId), + undefined, + { + device_keys: deviceKeys, + one_time_keys: oneTimeKeys, + fallback_keys: fallbackKeys, + }, + ); + console.log("Done"); + } +} diff --git a/src/crypto/index.js b/src/crypto/index.js index caaaba5e481..ad0e9727bb2 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -56,6 +56,7 @@ import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDevice import {IllegalMethod} from "./verification/IllegalMethod"; import {KeySignatureUploadError} from "../errors"; import {decryptAES, encryptAES} from './aes'; +import {DehydrationManager} from './dehydration'; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -243,6 +244,8 @@ export function Crypto(baseApis, sessionStore, userId, deviceId, baseApis, cryptoCallbacks, ); + this._dehydrationManager = new DehydrationManager(baseApis); + // Assuming no app-supplied callback, default to getting from SSSS. if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) { cryptoCallbacks.getCrossSigningKey = async (type) => { From 25c2cc17686440ec7d1e39ec11cdcae93e25b1db Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 3 Sep 2020 11:45:37 -0400 Subject: [PATCH 05/16] various fixes --- src/client.js | 24 +-- src/crypto/dehydration.ts | 301 ++++++++++++++++++++++++-------------- src/crypto/index.js | 2 +- 3 files changed, 201 insertions(+), 126 deletions(-) diff --git a/src/client.js b/src/client.js index 7f04160a07a..a735ae3a7a4 100644 --- a/src/client.js +++ b/src/client.js @@ -455,6 +455,10 @@ utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.loginWithRehydration = async function(loginFunc, ...args) { + if (!global.Olm || !this._cryptoCallbacks + || !this._cryptoCallbacks.getDehydrationKey) { + return (loginFunc === null ? this.login : this[loginFunc]).call(this, ...args); + } const origLogin = this.login; function rehydrationLoginWrapper(loginType, data, callback) { @@ -537,24 +541,20 @@ MatrixClient.prototype.loginWithRehydration = async function(loginFunc, ...args) }; /** - * Store a new dehydrated device on the server. The client must have been - * initialized with a `cryptoCallbacks.generateDehydrationKey` option. + * Set the dehydration key. This will also periodically dehydrate devices to + * the server. * + * @param {Uint8Array} key the dehydration key + * @param {object} [keyInfo] Information about the key. Primarily for + * information about how to generate the key from a passphrase. * @return {Promise} A promise that resolves when the dehydrated device is stored. */ -MatrixClient.prototype.dehydrateDevice = async function() { - if (!this._crypto) { - throw new Error("End-to-end encryption disabled"); - } - return await this._crypto._dehydrationManager.dehydrateDevice(); -}; - -MatrixClient.prototype.cacheDehydrationKey = async function(key, keyInfo) { +MatrixClient.prototype.setDehydrationKey = async function(key, keyInfo = {}) { if (!(this._crypto)) { - logger.warn('not exporting device if crypto is not enabled'); + logger.warn('not dehydrating device if crypto is not enabled'); return; } - return await this._crypto._dehydrationManager.cacheDehydrationKey(key, keyInfo); + return await this._crypto._dehydrationManager.setDehydrationKey(key, keyInfo); }; MatrixClient.prototype.exportDevice = async function() { diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index f75c6bc2051..1ce6d13ad1e 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -19,140 +19,215 @@ import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store'; import {decryptAES, encryptAES} from './aes'; import anotherjson from "another-json"; -export const DEHYDRATION_ALGORITHM = "m.dehydration.v1.olm"; +export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle"; + +const oneweek = 7 * 24 * 60 * 60 * 1000; export class DehydrationManager { - constructor(private client) {} - async cacheDehydrationKey(key, keyInfo = {}): Promise { - const pickleKey = Buffer.from(this.client._crypto._olmDevice._pickleKey); - key = await encryptAES(encodeBase64(key), pickleKey, DEHYDRATION_ALGORITHM); - this.client._crypto._cryptoStore.doTxn( - 'readwrite', + private inProgress: boolean = false; + private timeoutId: any; + private key: Uint8Array; + private keyInfo: {[props: string]: any}; + constructor(private crypto) { + this.getDehydrationKeyFromCache(); + } + async getDehydrationKeyFromCache(): Promise { + return this.crypto._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this.client._crypto._cryptoStore.storeSecretStorePrivateKey( - txn, DEHYDRATION_ALGORITHM, {keyInfo, key}, + this.crypto._cryptoStore.getSecretStorePrivateKey( + txn, + async (result) => { + if (result) { + const {key, keyInfo, time} = result; + const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey); + const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM); + this.key = decodeBase64(decrypted); + this.keyInfo = keyInfo; + const now = Date.now(); + const delay = Math.max(1, time + oneweek - now); + this.timeoutId = global.setTimeout( + this.dehydrateDevice.bind(this), delay, + ); + } + }, + DEHYDRATION_ALGORITHM, ); }, ); } - async dehydrateDevice(): Promise { - console.log("Attempting to dehydrate device"); - const {keyInfo, key} = await new Promise((resolve) => { - return this.client._crypto._cryptoStore.doTxn( - 'readonly', + async setDehydrationKey(key: Uint8Array, keyInfo: {[props: string]: any} = {}): Promise { + if (!key) { + // unsetting the key -- cancel any pending dehydration task + if (this.timeoutId) { + global.clearTimeout(this.timeoutId); + this.timeoutId = undefined; + } + // clear storage + this.crypto._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this.client._crypto._cryptoStore.getSecretStorePrivateKey( - txn, resolve, DEHYDRATION_ALGORITHM, + this.crypto._cryptoStore.storeSecretStorePrivateKey( + txn, DEHYDRATION_ALGORITHM, null, ); }, ); - }); - // FIXME: abort nicely if key not found - const pickleKey = Buffer.from(this.client._crypto._olmDevice._pickleKey); - const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM); - const decryptedKey = decodeBase64(decrypted); - - console.log("Creating account"); - // create the account and all the necessary keys - const account = new global.Olm.Account(); - account.create(); - const e2eKeys = JSON.parse(account.identity_keys()); - - const maxKeys = account.max_number_of_one_time_keys(); - // FIXME: generate in small batches? - account.generate_one_time_keys(maxKeys / 2); - account.generate_fallback_key(); - const otks = JSON.parse(account.one_time_keys()); - const fallbacks = JSON.parse(account.fallback_key()); - account.mark_keys_as_published(); - - // dehydrate the account and store it on the server - const pickledAccount = account.pickle(decryptedKey); - - const deviceData: {[props: string]: any} = { - algorithm: DEHYDRATION_ALGORITHM, - account: pickledAccount, - }; - if (keyInfo.passphrase) { - deviceData.passphrase = keyInfo.passphrase; + this.key = undefined; + this.keyInfo = undefined; + return; } - console.log("Uploading account to server"); - const dehydrateResult = await this.client._http.authedRequest( - undefined, - "POST", - "/device/dehydrate", - undefined, - { - device_data: deviceData, - // FIXME: initial device name? - }, - { - prefix: "/_matrix/client/unstable/org.matrix.msc2697", - }, - ); + // Check to see if it's the same key as before. If it's different, + // dehydrate a new device. If it's the same, we can keep the same + // device. (Assume that keyInfo will be the same if the key is the same.) + let matches: boolean = this.key && key.length == this.key.length; + for (let i = 0; matches && i < key.length; i++) { + if (key[i] != this.key[i]) { + matches = false; + } + } + if (!matches) { + this.key = key; + this.keyInfo = keyInfo; + // start dehydration in the background + this.dehydrateDevice(); + } + } + private async dehydrateDevice(): Promise { + if (this.inProgress) { + console.log("Dehydration already in progress -- not starting new dehydration"); + return; + } + this.inProgress = true; + if (this.timeoutId) { + global.clearTimeout(this.timeoutId); + this.timeoutId = undefined; + } + try { + const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey); - // send the keys to the server - const deviceId = dehydrateResult.device_id; - console.log("Preparing device keys", deviceId); - const deviceKeys = { - algorithms: this.client._crypto._supportedAlgorithms, - device_id: deviceId, - user_id: this.client.credentials.userId, - keys: { - [`ed25519:${deviceId}`]: e2eKeys.ed25519, - [`curve25519:${deviceId}`]: e2eKeys.curve25519, - }, - signatures: {}, - }; - const deviceSignature = account.sign(anotherjson.stringify(deviceKeys)); - deviceKeys.signatures = { - [this.client.credentials.userId]: { - [`ed25519:${deviceId}`]: deviceSignature, - }, - }; - await this.client._crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); - - console.log("Preparing one-time keys"); - const oneTimeKeys = {}; - for (const [keyId, key] of Object.entries(otks.curve25519)) { - const k = {key, signatures: {}}; - const signature = account.sign(anotherjson.stringify(k)); - k.signatures = { - [this.client.credentials.userId]: { - [`ed25519:${deviceId}`]: signature, + // update the crypto store with the timestamp + const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM); + this.crypto._cryptoStore.doTxn( + 'readwrite', + [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this.crypto._cryptoStore.storeSecretStorePrivateKey( + txn, DEHYDRATION_ALGORITHM, {keyInfo: this.keyInfo, key, time: Date.now()}, + ); }, + ); + console.log("Attempting to dehydrate device"); + + console.log("Creating account"); + // create the account and all the necessary keys + const account = new global.Olm.Account(); + account.create(); + const e2eKeys = JSON.parse(account.identity_keys()); + + const maxKeys = account.max_number_of_one_time_keys(); + // FIXME: generate in small batches? + account.generate_one_time_keys(maxKeys / 2); + account.generate_fallback_key(); + const otks = JSON.parse(account.one_time_keys()); + const fallbacks = JSON.parse(account.fallback_key()); + account.mark_keys_as_published(); + + // dehydrate the account and store it on the server + const pickledAccount = account.pickle(new Uint8Array(this.key)); + + const deviceData: {[props: string]: any} = { + algorithm: DEHYDRATION_ALGORITHM, + account: pickledAccount, }; - oneTimeKeys[`signed_curve25519:${keyId}`] = k; - } + if (this.keyInfo.passphrase) { + deviceData.passphrase = this.keyInfo.passphrase; + } - console.log("Preparing fallback keys"); - const fallbackKeys = {}; - for (const [keyId, key] of Object.entries(fallbacks.curve25519)) { - const k = {key, signatures: {}}; - const signature = account.sign(anotherjson.stringify(k)); - k.signatures = { - [this.client.credentials.userId]: { - [`ed25519:${deviceId}`]: signature, + console.log("Uploading account to server"); + const dehydrateResult = await this.crypto._baseApis._http.authedRequest( + undefined, + "POST", + "/device/dehydrate", + undefined, + { + device_data: deviceData, + // FIXME: initial device name? + }, + { + prefix: "/_matrix/client/unstable/org.matrix.msc2697", }, + ); + + // send the keys to the server + const deviceId = dehydrateResult.device_id; + console.log("Preparing device keys", deviceId); + const deviceKeys = { + algorithms: this.crypto._supportedAlgorithms, + device_id: deviceId, + user_id: this.crypto._userId, + keys: { + [`ed25519:${deviceId}`]: e2eKeys.ed25519, + [`curve25519:${deviceId}`]: e2eKeys.curve25519, + }, + signatures: {}, }; - fallbackKeys[`signed_curve25519:${keyId}`] = k; - } + const deviceSignature = account.sign(anotherjson.stringify(deviceKeys)); + deviceKeys.signatures = { + [this.crypto._userId]: { + [`ed25519:${deviceId}`]: deviceSignature, + }, + }; + await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); - console.log("Uploading keys to server"); - await this.client._http.authedRequest( - undefined, - "POST", - "/keys/upload/" + encodeURI(deviceId), - undefined, - { - device_keys: deviceKeys, - one_time_keys: oneTimeKeys, - fallback_keys: fallbackKeys, - }, - ); - console.log("Done"); + console.log("Preparing one-time keys"); + const oneTimeKeys = {}; + for (const [keyId, key] of Object.entries(otks.curve25519)) { + const k = {key, signatures: {}}; + const signature = account.sign(anotherjson.stringify(k)); + k.signatures = { + [this.crypto._userId]: { + [`ed25519:${deviceId}`]: signature, + }, + }; + oneTimeKeys[`signed_curve25519:${keyId}`] = k; + } + + console.log("Preparing fallback keys"); + const fallbackKeys = {}; + for (const [keyId, key] of Object.entries(fallbacks.curve25519)) { + const k = {key, signatures: {}}; + const signature = account.sign(anotherjson.stringify(k)); + k.signatures = { + [this.crypto._userId]: { + [`ed25519:${deviceId}`]: signature, + }, + }; + fallbackKeys[`signed_curve25519:${keyId}`] = k; + } + + console.log("Uploading keys to server"); + await this.crypto._baseApis._http.authedRequest( + undefined, + "POST", + "/keys/upload/" + encodeURI(deviceId), + undefined, + { + device_keys: deviceKeys, + one_time_keys: oneTimeKeys, + fallback_keys: fallbackKeys, + }, + ); + console.log("Done"); + + // dehydrate again in a week + this.timeoutId = global.setTimeout( + this.dehydrateDevice.bind(this), oneweek, + ); + } finally { + this.inProgress = false; + } } } diff --git a/src/crypto/index.js b/src/crypto/index.js index ad0e9727bb2..ad3898351d4 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -244,7 +244,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId, baseApis, cryptoCallbacks, ); - this._dehydrationManager = new DehydrationManager(baseApis); + this._dehydrationManager = new DehydrationManager(this); // Assuming no app-supplied callback, default to getting from SSSS. if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) { From 19f3996e09185aad7280ecb08b803c23f244331d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 3 Sep 2020 16:06:06 -0400 Subject: [PATCH 06/16] only cross-sign dehydrated device if cross-signing is available --- src/crypto/dehydration.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 1ce6d13ad1e..58b7b8c040e 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -180,7 +180,9 @@ export class DehydrationManager { [`ed25519:${deviceId}`]: deviceSignature, }, }; - await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); + if (this.crypto._crossSigningInfo.getId(self_signing)) { + await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); + } console.log("Preparing one-time keys"); const oneTimeKeys = {}; From 16dccd75c17fbaaa5190b62097082a35b6c55eee Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 3 Sep 2020 16:31:34 -0400 Subject: [PATCH 07/16] fix string --- src/crypto/dehydration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 58b7b8c040e..06d3c588e04 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -180,7 +180,7 @@ export class DehydrationManager { [`ed25519:${deviceId}`]: deviceSignature, }, }; - if (this.crypto._crossSigningInfo.getId(self_signing)) { + if (this.crypto._crossSigningInfo.getId("self_signing")) { await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); } From 60b9ef959d35415cefe65e4c56f1fb207b96bc2c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 24 Sep 2020 18:46:17 -0400 Subject: [PATCH 08/16] don't include an empty signatures field when calculating the signature --- src/crypto/dehydration.ts | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 06d3c588e04..bc4f55f1cd0 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -19,6 +19,23 @@ import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store'; import {decryptAES, encryptAES} from './aes'; import anotherjson from "another-json"; +// FIXME: these types should eventually go in a different file +type Signatures = Record>; + +interface DeviceKeys { + algorithms: Array; + device_id: string; + user_id: string; + keys: Record; + signatures?: Signatures; +} + +interface OneTimeKey { + key: string; + fallback?: boolean; + signatures?: Signatures; +} + export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle"; const oneweek = 7 * 24 * 60 * 60 * 1000; @@ -131,8 +148,8 @@ export class DehydrationManager { // FIXME: generate in small batches? account.generate_one_time_keys(maxKeys / 2); account.generate_fallback_key(); - const otks = JSON.parse(account.one_time_keys()); - const fallbacks = JSON.parse(account.fallback_key()); + const otks: Record = JSON.parse(account.one_time_keys()); + const fallbacks: Record = JSON.parse(account.fallback_key()); account.mark_keys_as_published(); // dehydrate the account and store it on the server @@ -164,7 +181,7 @@ export class DehydrationManager { // send the keys to the server const deviceId = dehydrateResult.device_id; console.log("Preparing device keys", deviceId); - const deviceKeys = { + const deviceKeys: DeviceKeys = { algorithms: this.crypto._supportedAlgorithms, device_id: deviceId, user_id: this.crypto._userId, @@ -172,7 +189,6 @@ export class DehydrationManager { [`ed25519:${deviceId}`]: e2eKeys.ed25519, [`curve25519:${deviceId}`]: e2eKeys.curve25519, }, - signatures: {}, }; const deviceSignature = account.sign(anotherjson.stringify(deviceKeys)); deviceKeys.signatures = { @@ -187,7 +203,7 @@ export class DehydrationManager { console.log("Preparing one-time keys"); const oneTimeKeys = {}; for (const [keyId, key] of Object.entries(otks.curve25519)) { - const k = {key, signatures: {}}; + const k: OneTimeKey = {key}; const signature = account.sign(anotherjson.stringify(k)); k.signatures = { [this.crypto._userId]: { @@ -200,7 +216,7 @@ export class DehydrationManager { console.log("Preparing fallback keys"); const fallbackKeys = {}; for (const [keyId, key] of Object.entries(fallbacks.curve25519)) { - const k = {key, signatures: {}}; + const k: OneTimeKey = {key, fallback: true}; const signature = account.sign(anotherjson.stringify(k)); k.signatures = { [this.crypto._userId]: { From aa9b807b82f37c1409077a81ad73a4fdc723d030 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Sep 2020 00:48:07 -0400 Subject: [PATCH 09/16] update to latest dehydration endpoints, and simplify API --- src/client.js | 112 ++++++++++++++++++-------------------- src/crypto/dehydration.ts | 8 +-- 2 files changed, 57 insertions(+), 63 deletions(-) diff --git a/src/client.js b/src/client.js index f44642306ab..25bb611d899 100644 --- a/src/client.js +++ b/src/client.js @@ -461,100 +461,94 @@ utils.inherits(MatrixClient, EventEmitter); utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); /** - * Log in, trying to rehydrate a device if available. The client must have - * been initialized with a `cryptoCallback.getDehydrationKey` option. - * - * @param {string} loginFunc The login function to use. e.g. "loginWithPassword" - * or `null` if using base_apis.login - * @param {...*} args arguments to pass to the login function. - * @return {Promise} Resolves: to a login request result. If a dehydrated - * device was found, it will include an `_olm_account` property, which will be - * an Olm Account to use. + * Try to rehydrate a device if available. The client must have been + * initialized with a `cryptoCallback.getDehydrationKey` option, and this + * function must be called before initCrypto and startClient are called. + * + * @return {Promise} Resolves to undefined if a device could not be dehydrated, or + * to the new device ID if the dehydration was successful. * @return {module:http-api.MatrixError} Rejects: with an error response. */ -MatrixClient.prototype.loginWithRehydration = async function(loginFunc, ...args) { - if (!global.Olm || !this._cryptoCallbacks - || !this._cryptoCallbacks.getDehydrationKey) { - return (loginFunc === null ? this.login : this[loginFunc]).call(this, ...args); +MatrixClient.prototype.rehydrateDevice = async function() { + if (this._crypto) { + throw new Error("Cannot rehydrate device after crypto is initialized"); } - const origLogin = this.login; - - function rehydrationLoginWrapper(loginType, data, callback) { - const loginData = { - "org.matrix.msc2697.restore_device": true, - }; - Object.assign(loginData, data); - return origLogin.call(this, loginType, loginData, callback); // eslint-disable-line babel/no-invalid-this + if (!this._cryptoCallbacks.getDehydrationKey) { + return; } - let loginResult; - if (loginFunc === null) { - loginResult = await rehydrationLoginWrapper.call(this, ...args); - } else { - this.login = rehydrationLoginWrapper; - - try { - loginResult = await this[loginFunc](...args); - } finally { - this.login = origLogin; - } + let getDeviceResult; + try { + getDeviceResult = await this._http.authedRequest( + undefined, + "GET", + "/dehydrated_device", + undefined, undefined, + { + prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2", + }, + ); + } catch (e) { + console.info("could not get dehydrated device", e); + return; } - if (!loginResult.device_data) { + if (!getDeviceResult.device_data || !getDeviceResult.device_id) { console.info("no dehydrated device found"); - return loginResult; + return; } const account = new global.Olm.Account(); try { - const deviceData = loginResult.device_data; + const deviceData = getDeviceResult.device_data; if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) { - throw new Error("Wrong algorithm"); + console.warn("Wrong algorithm for dehydrated device"); + return; } - const key = await this._cryptoCallbacks.getDehydrationKey(deviceData); console.log("unpickling dehydrated device"); + const key = await this._cryptoCallbacks.getDehydrationKey( + deviceData, + (k) => { + // copy the key so that it doesn't get clobbered + account.unpickle(new Uint8Array(k), deviceData.account); + } + ); account.unpickle(key, deviceData.account); - // FIXME: retry asking for key if unpickle fails? console.log("unpickled device"); - const rehydrateResult = await this._http.request( + const rehydrateResult = await this._http.authedRequest( undefined, "POST", - "/restore_device", + "/dehydrated_device/claim", undefined, { - rehydrate: true, - dehydration_token: loginResult.dehydration_token, + device_id: getDeviceResult.device_id, }, { - prefix: "/_matrix/client/unstable/org.matrix.msc2697", + prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2", }, ); - if (rehydrateResult.device_id === loginResult.device_id) { + if (rehydrateResult.success === true) { + this.deviceId = getDeviceResult.device_id; console.info("using dehydrated device"); - rehydrateResult._olm_account = account; + const pickleKey = this.pickleKey || "DEFAULT_KEY"; + this._exportedOlmDeviceToImport = { + pickledAccount: account.pickle(pickleKey), + sessions: [], + pickleKey: pickleKey, + }; + account.free(); + return this.deviceId; } else { + account.free(); console.info("not using dehydrated device"); + return; } - return rehydrateResult; } catch (e) { account.free(); console.warn("could not unpickle", e); - return await this._http.request( - undefined, - "POST", - "/restore_device", - undefined, - { - rehydrate: false, - dehydration_token: loginResult.dehydration_token, - }, - { - prefix: "/_matrix/client/unstable/org.matrix.msc2697", - }, - ); } }; diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index bc4f55f1cd0..3b0f933b055 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -166,15 +166,15 @@ export class DehydrationManager { console.log("Uploading account to server"); const dehydrateResult = await this.crypto._baseApis._http.authedRequest( undefined, - "POST", - "/device/dehydrate", + "PUT", + "/dehydrated_device", undefined, { device_data: deviceData, // FIXME: initial device name? }, { - prefix: "/_matrix/client/unstable/org.matrix.msc2697", + prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2", }, ); @@ -238,7 +238,7 @@ export class DehydrationManager { fallback_keys: fallbackKeys, }, ); - console.log("Done"); + console.log("Done dehydrating"); // dehydrate again in a week this.timeoutId = global.setTimeout( From f03f7c0acba3b66bf1c0a6921676bc5244eeea60 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Sep 2020 00:58:37 -0400 Subject: [PATCH 10/16] fix lint --- src/crypto/dehydration.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 3b0f933b055..068639c96ff 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -24,8 +24,8 @@ type Signatures = Record>; interface DeviceKeys { algorithms: Array; - device_id: string; - user_id: string; + device_id: string; // eslint-disable-line camel-case + user_id: string; // eslint-disable-line camel-case keys: Record; signatures?: Signatures; } @@ -41,7 +41,7 @@ export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle"; const oneweek = 7 * 24 * 60 * 60 * 1000; export class DehydrationManager { - private inProgress: boolean = false; + private inProgress = false; private timeoutId: any; private key: Uint8Array; private keyInfo: {[props: string]: any}; @@ -148,8 +148,8 @@ export class DehydrationManager { // FIXME: generate in small batches? account.generate_one_time_keys(maxKeys / 2); account.generate_fallback_key(); - const otks: Record = JSON.parse(account.one_time_keys()); - const fallbacks: Record = JSON.parse(account.fallback_key()); + const otks: Record = JSON.parse(account.one_time_keys()); + const fallbacks: Record = JSON.parse(account.fallback_key()); account.mark_keys_as_published(); // dehydrate the account and store it on the server From 6e5230f9f9ed75f6a9787ce0b08328f4a4489501 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Sep 2020 01:07:35 -0400 Subject: [PATCH 11/16] fix lint again --- src/crypto/dehydration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 068639c96ff..df63e82ee2c 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -24,8 +24,8 @@ type Signatures = Record>; interface DeviceKeys { algorithms: Array; - device_id: string; // eslint-disable-line camel-case - user_id: string; // eslint-disable-line camel-case + device_id: string; // eslint-disable-line camelcase + user_id: string; // eslint-disable-line camelcase keys: Record; signatures?: Signatures; } From cf7c84c4ba1d2bc8a7378aade020d393a2169615 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Sep 2020 01:10:33 -0400 Subject: [PATCH 12/16] more linting, because GitHub is showing me outdated CI info --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 25bb611d899..8bf3e2961cd 100644 --- a/src/client.js +++ b/src/client.js @@ -512,7 +512,7 @@ MatrixClient.prototype.rehydrateDevice = async function() { (k) => { // copy the key so that it doesn't get clobbered account.unpickle(new Uint8Array(k), deviceData.account); - } + }, ); account.unpickle(key, deviceData.account); console.log("unpickled device"); From 1dc89f642d261007e432322b93fc7bfcbaa81605 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Sep 2020 17:46:18 -0400 Subject: [PATCH 13/16] use logger instead of console --- src/client.js | 16 ++++++++-------- src/crypto/dehydration.ts | 25 +++++++++++++------------ 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/client.js b/src/client.js index 8bf3e2961cd..ae2a49679c4 100644 --- a/src/client.js +++ b/src/client.js @@ -490,12 +490,12 @@ MatrixClient.prototype.rehydrateDevice = async function() { }, ); } catch (e) { - console.info("could not get dehydrated device", e); + logger.info("could not get dehydrated device", e); return; } if (!getDeviceResult.device_data || !getDeviceResult.device_id) { - console.info("no dehydrated device found"); + logger.info("no dehydrated device found"); return; } @@ -503,10 +503,10 @@ MatrixClient.prototype.rehydrateDevice = async function() { try { const deviceData = getDeviceResult.device_data; if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) { - console.warn("Wrong algorithm for dehydrated device"); + logger.warn("Wrong algorithm for dehydrated device"); return; } - console.log("unpickling dehydrated device"); + logger.log("unpickling dehydrated device"); const key = await this._cryptoCallbacks.getDehydrationKey( deviceData, (k) => { @@ -515,7 +515,7 @@ MatrixClient.prototype.rehydrateDevice = async function() { }, ); account.unpickle(key, deviceData.account); - console.log("unpickled device"); + logger.log("unpickled device"); const rehydrateResult = await this._http.authedRequest( undefined, @@ -532,7 +532,7 @@ MatrixClient.prototype.rehydrateDevice = async function() { if (rehydrateResult.success === true) { this.deviceId = getDeviceResult.device_id; - console.info("using dehydrated device"); + logger.info("using dehydrated device"); const pickleKey = this.pickleKey || "DEFAULT_KEY"; this._exportedOlmDeviceToImport = { pickledAccount: account.pickle(pickleKey), @@ -543,12 +543,12 @@ MatrixClient.prototype.rehydrateDevice = async function() { return this.deviceId; } else { account.free(); - console.info("not using dehydrated device"); + logger.info("not using dehydrated device"); return; } } catch (e) { account.free(); - console.warn("could not unpickle", e); + logger.warn("could not unpickle", e); } }; diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index df63e82ee2c..d92129069c1 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -18,6 +18,7 @@ import {decodeBase64, encodeBase64} from './olmlib'; import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store'; import {decryptAES, encryptAES} from './aes'; import anotherjson from "another-json"; +import {logger} from '../logger'; // FIXME: these types should eventually go in a different file type Signatures = Record>; @@ -114,7 +115,7 @@ export class DehydrationManager { } private async dehydrateDevice(): Promise { if (this.inProgress) { - console.log("Dehydration already in progress -- not starting new dehydration"); + logger.log("Dehydration already in progress -- not starting new dehydration"); return; } this.inProgress = true; @@ -136,9 +137,9 @@ export class DehydrationManager { ); }, ); - console.log("Attempting to dehydrate device"); + logger.log("Attempting to dehydrate device"); - console.log("Creating account"); + logger.log("Creating account"); // create the account and all the necessary keys const account = new global.Olm.Account(); account.create(); @@ -163,7 +164,7 @@ export class DehydrationManager { deviceData.passphrase = this.keyInfo.passphrase; } - console.log("Uploading account to server"); + logger.log("Uploading account to server"); const dehydrateResult = await this.crypto._baseApis._http.authedRequest( undefined, "PUT", @@ -180,7 +181,7 @@ export class DehydrationManager { // send the keys to the server const deviceId = dehydrateResult.device_id; - console.log("Preparing device keys", deviceId); + logger.log("Preparing device keys", deviceId); const deviceKeys: DeviceKeys = { algorithms: this.crypto._supportedAlgorithms, device_id: deviceId, @@ -200,7 +201,7 @@ export class DehydrationManager { await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing"); } - console.log("Preparing one-time keys"); + logger.log("Preparing one-time keys"); const oneTimeKeys = {}; for (const [keyId, key] of Object.entries(otks.curve25519)) { const k: OneTimeKey = {key}; @@ -213,7 +214,7 @@ export class DehydrationManager { oneTimeKeys[`signed_curve25519:${keyId}`] = k; } - console.log("Preparing fallback keys"); + logger.log("Preparing fallback keys"); const fallbackKeys = {}; for (const [keyId, key] of Object.entries(fallbacks.curve25519)) { const k: OneTimeKey = {key, fallback: true}; @@ -226,19 +227,19 @@ export class DehydrationManager { fallbackKeys[`signed_curve25519:${keyId}`] = k; } - console.log("Uploading keys to server"); + logger.log("Uploading keys to server"); await this.crypto._baseApis._http.authedRequest( undefined, "POST", "/keys/upload/" + encodeURI(deviceId), undefined, { - device_keys: deviceKeys, - one_time_keys: oneTimeKeys, - fallback_keys: fallbackKeys, + "device_keys": deviceKeys, + "one_time_keys": oneTimeKeys, + "org.matrix.msc2732.fallback_keys": fallbackKeys, }, ); - console.log("Done dehydrating"); + logger.log("Done dehydrating"); // dehydrate again in a week this.timeoutId = global.setTimeout( From 7b9f73709df1687e6f63fbcb81e08a5f96b1de66 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Sep 2020 17:50:37 -0400 Subject: [PATCH 14/16] use "dehydration" as the cache name --- src/crypto/dehydration.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index d92129069c1..bd5535a2bce 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -70,7 +70,7 @@ export class DehydrationManager { ); } }, - DEHYDRATION_ALGORITHM, + "dehydration", ); }, ); @@ -88,7 +88,7 @@ export class DehydrationManager { [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { this.crypto._cryptoStore.storeSecretStorePrivateKey( - txn, DEHYDRATION_ALGORITHM, null, + txn, "dehydration", null, ); }, ); @@ -133,7 +133,7 @@ export class DehydrationManager { [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { this.crypto._cryptoStore.storeSecretStorePrivateKey( - txn, DEHYDRATION_ALGORITHM, {keyInfo: this.keyInfo, key, time: Date.now()}, + txn, "dehydration", {keyInfo: this.keyInfo, key, time: Date.now()}, ); }, ); From 2d2a73bf5226b159dc7ef8a07496341bb8be12ab Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Sep 2020 18:05:52 -0400 Subject: [PATCH 15/16] make dehydrated device name configurable --- src/client.js | 10 ++++++++-- src/crypto/dehydration.ts | 23 ++++++++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/client.js b/src/client.js index ae2a49679c4..f1f997416c6 100644 --- a/src/client.js +++ b/src/client.js @@ -559,14 +559,20 @@ MatrixClient.prototype.rehydrateDevice = async function() { * @param {Uint8Array} key the dehydration key * @param {object} [keyInfo] Information about the key. Primarily for * information about how to generate the key from a passphrase. + * @param {string} [deviceDisplayName] The device display name for the + * dehydrated device. * @return {Promise} A promise that resolves when the dehydrated device is stored. */ -MatrixClient.prototype.setDehydrationKey = async function(key, keyInfo = {}) { +MatrixClient.prototype.setDehydrationKey = async function( + key, keyInfo = {}, deviceDisplayName = undefined, +) { if (!(this._crypto)) { logger.warn('not dehydrating device if crypto is not enabled'); return; } - return await this._crypto._dehydrationManager.setDehydrationKey(key, keyInfo); + return await this._crypto._dehydrationManager.setDehydrationKey( + key, keyInfo, deviceDisplayName, + ); }; MatrixClient.prototype.exportDevice = async function() { diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index bd5535a2bce..9792d313eea 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -46,6 +46,7 @@ export class DehydrationManager { private timeoutId: any; private key: Uint8Array; private keyInfo: {[props: string]: any}; + private deviceDisplayName: string; constructor(private crypto) { this.getDehydrationKeyFromCache(); } @@ -58,11 +59,12 @@ export class DehydrationManager { txn, async (result) => { if (result) { - const {key, keyInfo, time} = result; + const {key, keyInfo, deviceDisplayName, time} = result; const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey); const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM); this.key = decodeBase64(decrypted); this.keyInfo = keyInfo; + this.deviceDisplayName = deviceDisplayName; const now = Date.now(); const delay = Math.max(1, time + oneweek - now); this.timeoutId = global.setTimeout( @@ -75,7 +77,10 @@ export class DehydrationManager { }, ); } - async setDehydrationKey(key: Uint8Array, keyInfo: {[props: string]: any} = {}): Promise { + async setDehydrationKey( + key: Uint8Array, keyInfo: {[props: string]: any} = {}, + deviceDisplayName: string = undefined + ): Promise { if (!key) { // unsetting the key -- cancel any pending dehydration task if (this.timeoutId) { @@ -99,7 +104,8 @@ export class DehydrationManager { // Check to see if it's the same key as before. If it's different, // dehydrate a new device. If it's the same, we can keep the same - // device. (Assume that keyInfo will be the same if the key is the same.) + // device. (Assume that keyInfo and deviceDisplayNamme will be the + // same if the key is the same.) let matches: boolean = this.key && key.length == this.key.length; for (let i = 0; matches && i < key.length; i++) { if (key[i] != this.key[i]) { @@ -109,6 +115,7 @@ export class DehydrationManager { if (!matches) { this.key = key; this.keyInfo = keyInfo; + this.deviceDisplayName = deviceDisplayName; // start dehydration in the background this.dehydrateDevice(); } @@ -133,7 +140,13 @@ export class DehydrationManager { [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { this.crypto._cryptoStore.storeSecretStorePrivateKey( - txn, "dehydration", {keyInfo: this.keyInfo, key, time: Date.now()}, + txn, "dehydration", + { + keyInfo: this.keyInfo, + key, + deviceDisplayName: this.deviceDisplayName, + time: Date.now() + }, ); }, ); @@ -172,7 +185,7 @@ export class DehydrationManager { undefined, { device_data: deviceData, - // FIXME: initial device name? + initial_device_display_name: this.deviceDisplayName, }, { prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2", From 60c863f829e30862ae464e305d613e771ad5f6c2 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 30 Sep 2020 18:33:27 -0400 Subject: [PATCH 16/16] lint --- src/crypto/dehydration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index 9792d313eea..637fa5ce154 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -79,7 +79,7 @@ export class DehydrationManager { } async setDehydrationKey( key: Uint8Array, keyInfo: {[props: string]: any} = {}, - deviceDisplayName: string = undefined + deviceDisplayName: string = undefined, ): Promise { if (!key) { // unsetting the key -- cancel any pending dehydration task @@ -145,7 +145,7 @@ export class DehydrationManager { keyInfo: this.keyInfo, key, deviceDisplayName: this.deviceDisplayName, - time: Date.now() + time: Date.now(), }, ); },