From 2b54f442d1574e36a8da466cffa5f739317000ae Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 30 Jan 2019 18:10:40 +0000 Subject: [PATCH 01/97] Add cross signing key creation into key backup Start of cross-signing impl --- src/base-apis.js | 6 ++++ src/client.js | 66 +++++++++++++++++++++++++++++++++++++---- src/crypto/PkSigning.js | 40 +++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 src/crypto/PkSigning.js diff --git a/src/base-apis.js b/src/base-apis.js index 4e9fed77b19..4926700a4b0 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -1685,6 +1685,12 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) { ); }; +MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(keys) { + return this._http.authedRequestWithPrefix( + undefined, "POST", "/keys/device_signing/upload", undefined, keys, + httpApi.PREFIX_UNSTABLE, + ); +}; // Identity Server Operations // ========================== diff --git a/src/client.js b/src/client.js index 9efdf6d5380..4ed0c6af139 100644 --- a/src/client.js +++ b/src/client.js @@ -51,6 +51,7 @@ import { isCryptoAvailable } from './crypto'; import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password'; import { randomString } from './randomstring'; +import { pkSign } from './crypto/PkSigning'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -955,8 +956,14 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { throw new Error("End-to-end encryption disabled"); } - const decryption = new global.Olm.PkDecryption(); + let decryption, encryption, signing; try { + decryption = new global.Olm.PkDecryption(); + encryption = new global.Olm.PkEncryption(); + if (global.Olm.PkSigning) { + signing = new global.Olm.PkSigning(); + } + let publicKey; const authData = {}; if (password) { @@ -969,14 +976,37 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { } authData.public_key = publicKey; + encryption.set_recipient_key(publicKey); - return { + const returnInfo = { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, auth_data: authData, recovery_key: encodeRecoveryKey(decryption.get_private_key()), }; + + if (signing) { + const ssk_seed = signing.generate_seed(); + // put the encrypted version of the seed in the auth data to upload + // XXX: our encryption really should support encrypting binary data. + authData.self_signing_key = encryption.encrypt(Buffer.from(ssk_seed).toString('base64')); + // and the unencrypted one in the returndata so we can use it later + returnInfo.ssk_seed = ssk_seed + // also keep the public part there + returnInfo.ssk_public = signing.init_with_seed(ssk_seed); + signing.free(); + + const usk_seed = signing.generate_seed(); + authData.user_signing_key = encryption.encrypt(Buffer.from(usk_seed).toString('base64')); + returnInfo.usk_seed = usk_seed; + returnInfo.usk_public = signing.init_with_seed(usk_seed); + signing.free(); + } + + return returnInfo; } finally { - decryption.free(); + if (decryption) decryption.free(); + if (encryption) encryption.free(); + if (signing) signing.free(); } }; @@ -987,7 +1017,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { * @param {object} info Info object from prepareKeyBackupVersion * @returns {Promise} Object with 'version' param indicating the version created */ -MatrixClient.prototype.createKeyBackupVersion = function(info) { +MatrixClient.prototype.createKeyBackupVersion = function(info, auth, replacesSsk) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } @@ -996,7 +1026,33 @@ MatrixClient.prototype.createKeyBackupVersion = function(info) { algorithm: info.algorithm, auth_data: info.auth_data, }; - return this._crypto._signObject(data.auth_data).then(() => { + + const uskInfo = { + user_id: this.credentials.userId, + usage: ['user_signing'], + keys: { + ['ed25519:' + info.usk_public]: info.usk_public, + }, + }; + + pkSign(uskInfo, info.ssk_seed, this.credentials.userId); + + const keys = { + self_signing_key: { + user_id: this.credentials.userId, + usage: ['self_signing'], + keys: { + ['ed25519:' + info.ssk_public]: info.ssk_public, + }, + replaces: replacesSsk, + }, + user_signing_key: uskInfo, + auth, + }; + + return this.uploadDeviceSigningKeys(keys).then(() => { + return this._crypto._signObject(data.auth_data); + }).then(() => { return this._http.authedRequest( undefined, "POST", "/room_keys/version", undefined, data, ); diff --git a/src/crypto/PkSigning.js b/src/crypto/PkSigning.js new file mode 100644 index 00000000000..4de1c2f3e8b --- /dev/null +++ b/src/crypto/PkSigning.js @@ -0,0 +1,40 @@ +/* +Copyright 2019 New Vector Ltd + +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. +*/ + +const anotherjson = require('another-json'); + +/** + * Higher level wrapper around olm.PkSigning that signs JSON objects + */ +export function pkSign(obj, seed, userId) { + const signing = new global.Olm.PkSigning(); + try { + const pubkey = signing.init_with_seed(seed); + const sigs = obj.signatures || {}; + const mysigs = sigs[userId] || {}; + sigs[userId] = mysigs; + + delete obj.signatures; + const unsigned = obj.unsigned; + if (obj.unsigned) delete obj.unsigned; + + mysigs['ed25519:' + pubkey] = signing.sign(anotherjson.stringify(obj)); + obj.signatures = sigs; + if (unsigned) obj.unsigned = unsigned; + } finally { + signing.free(); + } +} From 02d4dcb1282ddd7651fc1cfc926e14961efef1d0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 31 Jan 2019 15:48:05 +0000 Subject: [PATCH 02/97] Store SSK & USK in crypto store and restore them from the key backup. NB. This has an interface change to restoreKeyBackup where I've changed it to take a backupInfo rather than a version (this also saves us re-fetching the backup metadata in the case of a passphrase restore). --- src/client.js | 89 ++++++++++++++----- .../store/indexeddb-crypto-store-backend.js | 17 ++++ src/crypto/store/indexeddb-crypto-store.js | 24 ++++- src/crypto/store/localStorage-crypto-store.js | 12 +++ src/crypto/store/memory-crypto-store.js | 9 ++ 5 files changed, 130 insertions(+), 21 deletions(-) diff --git a/src/client.js b/src/client.js index 4ed0c6af139..4ccb9325a7c 100644 --- a/src/client.js +++ b/src/client.js @@ -53,6 +53,8 @@ import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password' import { randomString } from './randomstring'; import { pkSign } from './crypto/PkSigning'; +import IndexedDBCryptoStore from './crypto/store/indexeddb-crypto-store'; + // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. Promise.config({warnings: false}); @@ -982,24 +984,40 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, auth_data: authData, recovery_key: encodeRecoveryKey(decryption.get_private_key()), + accountKeys: null, }; if (signing) { - const ssk_seed = signing.generate_seed(); + await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.getAccountKeys(txn, keys => { + returnInfo.accountKeys = keys; + }); + }); + + if (!returnInfo.accountKeys) { + const ssk_seed = signing.generate_seed(); + const usk_seed = signing.generate_seed(); + + returnInfo.accountKeys = { + self_signing_key_seed: Buffer.from(ssk_seed).toString('base64'), + user_signing_key_seed: Buffer.from(usk_seed).toString('base64'), + } + } + // put the encrypted version of the seed in the auth data to upload // XXX: our encryption really should support encrypting binary data. - authData.self_signing_key = encryption.encrypt(Buffer.from(ssk_seed).toString('base64')); - // and the unencrypted one in the returndata so we can use it later - returnInfo.ssk_seed = ssk_seed + authData.self_signing_key_seed = encryption.encrypt(returnInfo.accountKeys.self_signing_key_seed); // also keep the public part there - returnInfo.ssk_public = signing.init_with_seed(ssk_seed); + returnInfo.ssk_public = signing.init_with_seed(Buffer.from(returnInfo.accountKeys.self_signing_key_seed, 'base64')); signing.free(); - const usk_seed = signing.generate_seed(); - authData.user_signing_key = encryption.encrypt(Buffer.from(usk_seed).toString('base64')); - returnInfo.usk_seed = usk_seed; - returnInfo.usk_public = signing.init_with_seed(usk_seed); + // same for the USK + authData.user_signing_key_seed = encryption.encrypt(returnInfo.accountKeys.user_signing_key_seed); + returnInfo.usk_public = signing.init_with_seed(Buffer.from(returnInfo.accountKeys.user_signing_key_seed, 'base64')); signing.free(); + + // we don't save these keys back to the store yet: we'll do that when (if) we + // actually create the backup } return returnInfo; @@ -1035,7 +1053,8 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, auth, replacesSsk }, }; - pkSign(uskInfo, info.ssk_seed, this.credentials.userId); + // sign the USK with the SSK + pkSign(uskInfo, Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'), this.credentials.userId); const keys = { self_signing_key: { @@ -1050,7 +1069,11 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, auth, replacesSsk auth, }; - return this.uploadDeviceSigningKeys(keys).then(() => { + return this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.storeAccountKeys(txn, info.accountKeys); + }).then(() => { + return this.uploadDeviceSigningKeys(keys); + }).then(() => { return this._crypto._signObject(data.auth_data); }).then(() => { return this._http.authedRequest( @@ -1150,27 +1173,25 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { }; MatrixClient.prototype.restoreKeyBackupWithPassword = async function( - password, targetRoomId, targetSessionId, version, + password, targetRoomId, targetSessionId, backupInfo, ) { - const backupInfo = await this.getKeyBackupVersion(); - const privKey = await keyForExistingBackup(backupInfo, password); return this._restoreKeyBackup( - privKey, targetRoomId, targetSessionId, version, + privKey, targetRoomId, targetSessionId, backupInfo, ); }; MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function( - recoveryKey, targetRoomId, targetSessionId, version, + recoveryKey, targetRoomId, targetSessionId, backupInfo, ) { const privKey = decodeRecoveryKey(recoveryKey); return this._restoreKeyBackup( - privKey, targetRoomId, targetSessionId, version, + privKey, targetRoomId, targetSessionId, backupInfo, ); }; -MatrixClient.prototype._restoreKeyBackup = function( - privKey, targetRoomId, targetSessionId, version, +MatrixClient.prototype._restoreKeyBackup = async function( + privKey, targetRoomId, targetSessionId, backupInfo, ) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); @@ -1178,11 +1199,39 @@ MatrixClient.prototype._restoreKeyBackup = function( let totalKeyCount = 0; let keys = []; - const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); + const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version); const decryption = new global.Olm.PkDecryption(); try { decryption.init_with_private_key(privKey); + + // decrypt the account keys from the backup info if there are any + // fetch the old ones first so we don't lose info if only one of them is in the backup + let accountKeys; + await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.getAccountKeys(txn, keys => { + accountKeys = keys || {}; + }); + }); + + if (backupInfo.auth_data.self_signing_key_seed) { + accountKeys.self_signing_key_seed = decryption.decrypt( + backupInfo.auth_data.self_signing_key_seed.ephemeral, + backupInfo.auth_data.self_signing_key_seed.mac, + backupInfo.auth_data.self_signing_key_seed.ciphertext, + ); + } + if (backupInfo.auth_data.user_signing_key_seed) { + accountKeys.user_signing_key_seed = decryption.decrypt( + backupInfo.auth_data.user_signing_key_seed.ephemeral, + backupInfo.auth_data.user_signing_key_seed.mac, + backupInfo.auth_data.user_signing_key_seed.ciphertext, + ); + } + + await this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.storeAccountKeys(txn, accountKeys); + }); } catch(e) { decryption.free(); throw e; diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index f4b07ed5a13..eac5049b963 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -332,6 +332,23 @@ export class Backend { objectStore.put(newData, "-"); } + getAccountKeys(txn, func) { + const objectStore = txn.objectStore("account"); + const getReq = objectStore.get("keys"); + getReq.onsuccess = function() { + try { + func(getReq.result || null); + } catch (e) { + abortWithException(txn, e); + } + }; + } + + storeAccountKeys(txn, keys) { + const objectStore = txn.objectStore("account"); + objectStore.put(keys, "keys"); + } + // Olm Sessions countEndToEndSessions(txn, func) { diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 5f9defd020a..be6950e11d7 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -283,7 +283,7 @@ export default class IndexedDBCryptoStore { this._backendPromise.value().getAccount(txn, func); } - /* + /** * Write the account pickle to the store. * This requires an active transaction. See doTxn(). * @@ -294,6 +294,28 @@ export default class IndexedDBCryptoStore { this._backendPromise.value().storeAccount(txn, newData); } + /** + * Get the account keys fort cross-signing (eg. self-signing key, + * user signing key). + * + * @param {*} txn An active transaction. See doTxn(). + * @param {function(string)} func Called with the account keys object: + * { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed + */ + getAccountKeys(txn, func) { + this._backendPromise.value().getAccountKeys(txn, func); + } + + /** + * Write the account keys back to the store + * + * @param {*} txn An active transaction. See doTxn(). + * @param {string} keys Account keys object as getAccountKeys() + */ + storeAccountKeys(txn, keys) { + this._backendPromise.value().storeAccountKeys(txn, keys); + } + // Olm sessions /** diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 4c30b61997c..d5316132fd8 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -31,6 +31,7 @@ import MemoryCryptoStore from './memory-crypto-store.js'; const E2E_PREFIX = "crypto."; const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; +const KEY_END_TO_END_ACCOUNT_KEYS = E2E_PREFIX + "account_keys"; const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; @@ -274,6 +275,17 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { ); } + getAccountKeys(txn, func) { + const keys = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT_KEYS); + func(keys); + } + + storeAccountKeys(txn, keys) { + setJsonItem( + this.store, KEY_END_TO_END_ACCOUNT_KEYS, keys, + ); + } + doTxn(mode, stores, func) { return Promise.resolve(func(null)); } diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 49df5f238e1..fd2205c9fa1 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -33,6 +33,7 @@ export default class MemoryCryptoStore { constructor() { this._outgoingRoomKeyRequests = []; this._account = null; + this._accountKeys = null; // Map of {devicekey -> {sessionId -> session pickle}} this._sessions = {}; @@ -234,6 +235,14 @@ export default class MemoryCryptoStore { this._account = newData; } + getAccountKeys(txn, func) { + func(this._accountKeys); + } + + storeAccountKeys(txn, keys) { + this._accountKeys = keys; + } + // Olm Sessions countEndToEndSessions(txn, func) { From 1f77cc6d1a26fd421dde19ace2e8762622873bef Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 31 Jan 2019 21:13:01 +0000 Subject: [PATCH 03/97] Cross sign the current device with the SSK whenever we get the SSK, ie. when creating or restoring a backup --- src/base-apis.js | 6 +++++ src/client.js | 18 ++++++++++----- src/crypto/index.js | 55 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/base-apis.js b/src/base-apis.js index 4926700a4b0..9186c3f255d 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -1594,6 +1594,12 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) { ); }; +MatrixBaseApis.prototype.uploadKeySignatures = function(content) { + return this._http.authedRequestWithPrefix( + undefined, "POST", '/keys/signatures/upload', undefined, content, httpApi.PREFIX_UNSTABLE, + ); +}; + /** * Download device keys * diff --git a/src/client.js b/src/client.js index 4ccb9325a7c..54dd4924f34 100644 --- a/src/client.js +++ b/src/client.js @@ -1070,8 +1070,10 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, auth, replacesSsk }; return this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + // store the newly generated account keys this._cryptoStore.storeAccountKeys(txn, info.accountKeys); }).then(() => { + // upload the public part of the account keys return this.uploadDeviceSigningKeys(keys); }).then(() => { return this._crypto._signObject(data.auth_data); @@ -1086,6 +1088,9 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, auth, replacesSsk version: res.version, }); return res; + }).then(() => { + // upload signatures between the SSK & this device + return this._crypto.uploadDeviceKeySignatures(); }); }; @@ -1199,8 +1204,6 @@ MatrixClient.prototype._restoreKeyBackup = async function( let totalKeyCount = 0; let keys = []; - const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version); - const decryption = new global.Olm.PkDecryption(); try { decryption.init_with_private_key(privKey); @@ -1237,9 +1240,14 @@ MatrixClient.prototype._restoreKeyBackup = async function( throw e; } - return this._http.authedRequest( - undefined, "GET", path.path, path.queryData, - ).then((res) => { + // start by signing this device from the SSK now we have it + return this._crypto.uploadDeviceKeySignatures().then(() => { + // Now fetch the encrypted keys + const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version); + return this._http.authedRequest( + undefined, "GET", path.path, path.queryData, + ); + }).then((res) => { if (res.rooms) { for (const [roomId, roomData] of Object.entries(res.rooms)) { if (!roomData.sessions) continue; diff --git a/src/crypto/index.js b/src/crypto/index.js index 0621226d1d1..c4d6ba8c04e 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -46,6 +46,8 @@ import { newUnknownMethodError, } from './verification/Error'; +import { pkSign } from './PkSigning'; + const defaultVerificationMethods = { [ScanQRCode.NAME]: ScanQRCode, [ShowQRCode.NAME]: ShowQRCode, @@ -457,8 +459,20 @@ Crypto.prototype.uploadDeviceKeys = function() { user_id: userId, }; + let accountKeys; return crypto._signObject(deviceKeys).then(() => { - crypto._baseApis.uploadKeysRequest({ + return this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.getAccountKeys(txn, keys => { + accountKeys = keys; + }); + }); + }).then(() => { + if (accountKeys && accountKeys.self_signing_key_seed) { + // if we have an SSK, sign the key with the SSK too + pkSign(deviceKeys, Buffer.from(accountKeys.self_signing_key_seed, 'base64'), userId); + } + + return crypto._baseApis.uploadKeysRequest({ device_keys: deviceKeys, }, { // for now, we set the device id explicitly, as we may not be using the @@ -468,6 +482,45 @@ Crypto.prototype.uploadDeviceKeys = function() { }); }; +/** + * If a self-signing key is available, uploads the signature of this device from + * the self-signing key + * + * @return {bool} Promise: True if signatures were uploaded or otherwise false + * (eg. if no account keys were available) + */ +Crypto.prototype.uploadDeviceKeySignatures = async function() { + const crypto = this; + const userId = crypto._userId; + const deviceId = crypto._deviceId; + + const thisDeviceKey = { + algorithms: crypto._supportedAlgorithms, + device_id: deviceId, + keys: crypto._deviceKeys, + user_id: userId, + }; + let accountKeys; + await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.getAccountKeys(txn, keys => { + accountKeys = keys; + }); + }); + if (!accountKeys || !accountKeys.self_signing_key_seed) return false; + + // Sign this device with the SSK + pkSign(thisDeviceKey, Buffer.from(accountKeys.self_signing_key_seed, 'base64'), userId); + + const content = { + [userId]: { + [deviceId]: thisDeviceKey, + }, + }; + + await crypto._baseApis.uploadKeySignatures(content); + return true +}; + /** * Stores the current one_time_key count which will be handled later (in a call of * onSyncCompleted). The count is e.g. coming from a /sync response. From 1d58a64ee1361547ca9de2a7223d846a86166571 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 Feb 2019 13:04:21 +0000 Subject: [PATCH 04/97] Track SSKs for users and verify our own against our locally stored private part --- .babelrc | 3 +- package.json | 2 + src/crypto/DeviceList.js | 133 +++++++++++++++++++++++++++++++++------ src/crypto/index.js | 69 ++++++++++++++++++++ src/crypto/sskinfo.js | 76 ++++++++++++++++++++++ 5 files changed, 262 insertions(+), 21 deletions(-) create mode 100644 src/crypto/sskinfo.js diff --git a/.babelrc b/.babelrc index 22a8c2bbf99..a3c9597bbb7 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,7 @@ { - "presets": ["es2015"], + "presets": ["es2015", "es2016"], "plugins": [ + "transform-class-properties", // this transforms async functions into generator functions, which // are then made to use the regenerator module by babel's // transform-regnerator plugin (which is enabled by es2015). diff --git a/package.json b/package.json index f64544e76ee..bf77a0c3aa8 100644 --- a/package.json +++ b/package.json @@ -67,8 +67,10 @@ "devDependencies": { "babel-cli": "^6.18.0", "babel-plugin-transform-async-to-bluebird": "^1.1.1", + "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-es2015": "^6.18.0", + "babel-preset-es2016": "^6.24.1", "browserify": "^16.2.3", "browserify-shim": "^3.8.13", "eslint": "^5.12.0", diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 751982b4237..6ff51789c53 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -23,9 +23,11 @@ limitations under the License. */ import Promise from 'bluebird'; +import {EventEmitter} from 'events'; import logger from '../logger'; import DeviceInfo from './deviceinfo'; +import SskInfo from './sskinfo'; import olmlib from './olmlib'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; @@ -60,8 +62,10 @@ const TRACKING_STATUS_UP_TO_DATE = 3; /** * @alias module:crypto/DeviceList */ -export default class DeviceList { +export default class DeviceList extends EventEmitter { constructor(baseApis, cryptoStore, sessionStore, olmDevice) { + super(); + this._cryptoStore = cryptoStore; this._sessionStore = sessionStore; @@ -72,6 +76,11 @@ export default class DeviceList { // } this._devices = {}; + // userId -> { + // [key info] + // } + this._ssks = {}; + // map of identity keys to the user who owns it this._userByIdentityKey = {}; @@ -122,12 +131,14 @@ export default class DeviceList { this._syncToken = this._sessionStore.getEndToEndDeviceSyncToken(); this._cryptoStore.storeEndToEndDeviceData({ devices: this._devices, + self_signing_keys: this._ssks, trackingStatus: this._deviceTrackingStatus, syncToken: this._syncToken, }, txn); shouldDeleteSessionStore = true; } else { this._devices = deviceData ? deviceData.devices : {}, + this._ssks = deviceData ? deviceData.self_signing_keys || {} : {}; this._deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {}; this._syncToken = deviceData ? deviceData.syncToken : null; @@ -224,6 +235,7 @@ export default class DeviceList { 'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { this._cryptoStore.storeEndToEndDeviceData({ devices: this._devices, + self_signing_keys: this._ssks, trackingStatus: this._deviceTrackingStatus, syncToken: this._syncToken, }, txn); @@ -357,6 +369,21 @@ export default class DeviceList { return this._devices[userId]; } + getRawStoredSskForUser(userId) { + return this._ssks[userId]; + } + + getStoredSskForUser(userId) { + if (!this._ssks[userId]) return null; + + return SskInfo.fromStorage(this._ssks[userId]); + } + + storeSskForUser(userId, ssk) { + this._ssks[userId] = ssk; + this._dirty = true; + } + /** * Get the stored keys for a single device * @@ -584,6 +611,10 @@ export default class DeviceList { } } + setRawStoredSskForUser(userId, ssk) { + this._ssks[userId] = ssk; + } + /** * Fire off download update requests for the given users, and update the * device list tracking status for them, and the @@ -747,6 +778,7 @@ class DeviceListUpdateSerialiser { downloadUsers, opts, ).then((res) => { const dk = res.device_keys || {}; + const ssks = res.self_signing_keys || {}; // do each user in a separate promise, to avoid wedging the CPU // (https://github.com/vector-im/riot-web/issues/3158) @@ -756,7 +788,7 @@ class DeviceListUpdateSerialiser { let prom = Promise.resolve(); for (const userId of downloadUsers) { prom = prom.delay(5).then(() => { - return this._processQueryResponseForUser(userId, dk[userId]); + return this._processQueryResponseForUser(userId, dk[userId], ssks[userId]); }); } @@ -780,30 +812,48 @@ class DeviceListUpdateSerialiser { return deferred.promise; } - async _processQueryResponseForUser(userId, response) { - logger.log('got keys for ' + userId + ':', response); + async _processQueryResponseForUser(userId, dk_response, ssk_response) { + logger.log('got device keys for ' + userId + ':', dk_response); + logger.log('got self-signing keys for ' + userId + ':', ssk_response); + + { + // map from deviceid -> deviceinfo for this user + const userStore = {}; + const devs = this._deviceList.getRawStoredDevicesForUser(userId); + if (devs) { + Object.keys(devs).forEach((deviceId) => { + const d = DeviceInfo.fromStorage(devs[deviceId], deviceId); + userStore[deviceId] = d; + }); + } + + await _updateStoredDeviceKeysForUser( + this._olmDevice, userId, userStore, dk_response || {}, + ); - // map from deviceid -> deviceinfo for this user - const userStore = {}; - const devs = this._deviceList.getRawStoredDevicesForUser(userId); - if (devs) { - Object.keys(devs).forEach((deviceId) => { - const d = DeviceInfo.fromStorage(devs[deviceId], deviceId); - userStore[deviceId] = d; + // put the updates into the object that will be returned as our results + const storage = {}; + Object.keys(userStore).forEach((deviceId) => { + storage[deviceId] = userStore[deviceId].toStorage(); }); + + this._deviceList._setRawStoredDevicesForUser(userId, storage); } - await _updateStoredDeviceKeysForUser( - this._olmDevice, userId, userStore, response || {}, - ); + // now do the same for the self-signing key + { + const ssk = this._deviceList.getRawStoredSskForUser(userId) || {}; - // put the updates into thr object that will be returned as our results - const storage = {}; - Object.keys(userStore).forEach((deviceId) => { - storage[deviceId] = userStore[deviceId].toStorage(); - }); + const updated = await _updateStoredSelfSigningKeyForUser( + this._olmDevice, userId, ssk, ssk_response || {}, + ); + + this._deviceList.setRawStoredSskForUser(userId, ssk); - this._deviceList._setRawStoredDevicesForUser(userId, storage); + // NB. Unlike most events in the js-sdk, this one is internal to the + // js-sdk and is not re-emitted + if (updated) this._deviceList.emit('userSskUpdated', userId); + } } } @@ -854,6 +904,49 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore, return updated; } +async function _updateStoredSelfSigningKeyForUser( + _olmDevice, userId, userStore, userResult, +) { + let updated = false; + + if (userResult.user_id !== userId) { + logger.warn("Mismatched user_id " + userResult.user_id + + " in self-signing key from " + userId); + return; + } + if (!userResult || !userResult.usage.includes('self_signing')) { + logger.warn("Self-signing key for " + userId + " does not include 'self_signing' usage: ignoring"); + return; + } + const keyCount = Object.keys(userResult.keys).length; + if (keyCount !== 1) { + logger.warn( + "Self-signing key block for " + userId + " has " + keyCount + " keys: expected exactly 1. Ignoring.", + ); + return; + } + let oldKeyId = null; + let oldKey = null; + if (userStore.keys && Object.keys(userStore.keys).length > 0) { + oldKeyId = Object.keys(userStore.keys)[0]; + oldKey = userStore.keys[oldKeyId]; + } + const newKeyId = Object.keys(userResult.keys)[0]; + const newKey = userResult.keys[newKeyId]; + if (oldKeyId !== newKeyId || oldKey !== newKey) { + updated = true; + logger.info("New self-signing key detected for " + userId + ": " + newKeyId + ", was previously " + oldKeyId); + + userStore.user_id = userResult.user_id; + userStore.usage = userResult.usage; + userStore.keys = userResult.keys; + // reset verification status since its now a new key + userStore.verified = SskInfo.SskVerification.UNVERIFIED; + } + + return updated; +} + /* * Process a device in a /query response, and add it to the userStore * diff --git a/src/crypto/index.js b/src/crypto/index.js index c4d6ba8c04e..f80e8edd1e0 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -31,6 +31,7 @@ const OlmDevice = require("./OlmDevice"); const olmlib = require("./olmlib"); const algorithms = require("./algorithms"); const DeviceInfo = require("./deviceinfo"); +import SskInfo from './sskinfo'; const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceList = require('./DeviceList').default; import { randomString } from '../randomstring'; @@ -102,6 +103,8 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200; */ export default function Crypto(baseApis, sessionStore, userId, deviceId, clientStore, cryptoStore, roomList, verificationMethods) { + this._onDeviceListUserSskUpdated = this._onDeviceListUserSskUpdated.bind(this); + this._baseApis = baseApis; this._sessionStore = sessionStore; this._userId = userId; @@ -140,6 +143,9 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, this._deviceList = new DeviceList( baseApis, cryptoStore, sessionStore, this._olmDevice, ); + // XXX: This isn't removed at any point, but then none of the event listeners + // this class sets seem to be removed at any point... :/ + this._deviceList.on('userSskUpdated', this._onDeviceListUserSskUpdated); // the last time we did a check for the number of one-time-keys on the // server. @@ -255,6 +261,59 @@ Crypto.prototype.init = async function() { this._checkAndStartKeyBackup(); }; +/* + * Event handler for DeviceList's userNewDevices event + */ +Crypto.prototype._onDeviceListUserSskUpdated = async function(userId) { + if (userId === this._userId) { + // If we see an update to our own SSK, check it against the SSK we have and, + // if it matches, mark it as verified + + // First, get the pubkey of the one we can see + const seenSsk = this._deviceList.getStoredSskForUser(userId); + if (!seenSsk) { + logger.error("Got SSK update event for user " + userId + " but no new SSK found!"); + return; + } + const seenPubkey = seenSsk.getFingerprint(); + + // Now dig out the account keys and get the pubkey of the one in there + let accountKeys = null; + await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.getAccountKeys(txn, keys => { + accountKeys = keys; + }); + }); + if (!accountKeys || !accountKeys.self_signing_key_seed) { + logger.info("Ignoring new self-signing key for us because we have no private part stored"); + return; + } + let signing; + let localPubkey; + try { + signing = new global.Olm.PkSigning(); + localPubkey = signing.init_with_seed(Buffer.from(accountKeys.self_signing_key_seed, 'base64')) + } finally { + if (signing) signing.free(); + signing = null; + } + if (!localPubkey) { + logger.error("Unable to compute public key for stored SSK seed"); + } + + // Finally, are they the same? + if (seenPubkey === localPubkey) { + logger.info("Published self-signing key matches local copy: marking as verified"); + this.setSskVerification(userId, SskInfo.SskVerification.VERIFIED); + } else { + logger.info( + "Published self-signing key DOES NOT match local copy! Local: " + + localPubkey + ", published: " + seenPubkey, + ); + } + } +}; + /** * Check the server for an active key backup and * if one is present and has a valid signature from @@ -719,6 +778,16 @@ Crypto.prototype.saveDeviceList = function(delay) { return this._deviceList.saveIfDirty(delay); }; +Crypto.prototype.setSskVerification = async function(userId, verified) { + const ssk = this._deviceList.getRawStoredSskForUser(userId); + if (!ssk) { + throw new Error("No self-signing key found for user " + userId); + } + ssk.verified = verified; + this._deviceList.storeSskForUser(userId, ssk) + this._deviceList.saveIfDirty(); +}; + /** * Update the blocked/verified state of the given device * diff --git a/src/crypto/sskinfo.js b/src/crypto/sskinfo.js new file mode 100644 index 00000000000..660426deab0 --- /dev/null +++ b/src/crypto/sskinfo.js @@ -0,0 +1,76 @@ +/* +Copyright 2019 New Vector Ltd + +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. +*/ + + +/** + * @module crypto/sskinfo + */ + +/** + * Information about a user's self-signing key + * + * @constructor + * @alias module:crypto/sskinfo + * + * @property {Object.} keys a map from + * <key type>:<id> -> <base64-encoded key>> + * + * @property {module:crypto/sskinfo.SskVerification} verified + * whether the device has been verified/blocked by the user + * + * @property {boolean} known + * whether the user knows of this device's existence (useful when warning + * the user that a user has added new devices) + * + * @property {Object} unsigned additional data from the homeserver + */ +export default class SskInfo { + constructor() { + this.keys = {}; + this.verified = SskInfo.SskVerification.UNVERIFIED; + //this.known = false; // is this useful? + this.unsigned = {}; + } + + /** + * @enum + */ + static SskVerification = { + VERIFIED: 1, + UNVERIFIED: 0, + BLOCKED: -1, + }; + + static fromStorage(obj) { + const res = new SskInfo(); + for (const [prop, val] of Object.entries(obj)) { + res[prop] = val; + } + return res; + } + + getFingerprint() { + return Object.values(this.keys)[0]; + } + + isVerified() { + return this.verified == SskInfo.SskVerification.VERIFIED; + }; + + isUnverified() { + return this.verified == SskInfo.SskVerification.UNVERIFIED; + }; +} From 910d0ec9c161d23072cb8c4c352c13e8257f02b6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 Feb 2019 15:49:20 +0000 Subject: [PATCH 05/97] Sign & trust the key backup from the SSK --- src/client.js | 17 ++++- src/crypto/index.js | 141 ++++++++++++++++++++++++++++-------------- src/crypto/olmlib.js | 7 ++- src/crypto/sskinfo.js | 4 ++ 4 files changed, 118 insertions(+), 51 deletions(-) diff --git a/src/client.js b/src/client.js index 54dd4924f34..c3e81aa11cf 100644 --- a/src/client.js +++ b/src/client.js @@ -1035,7 +1035,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { * @param {object} info Info object from prepareKeyBackupVersion * @returns {Promise} Object with 'version' param indicating the version created */ -MatrixClient.prototype.createKeyBackupVersion = function(info, auth, replacesSsk) { +MatrixClient.prototype.createKeyBackupVersion = async function(info, auth, replacesSsk) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } @@ -1056,6 +1056,14 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, auth, replacesSsk // sign the USK with the SSK pkSign(uskInfo, Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'), this.credentials.userId); + // Now sig the backup auth data. Do it as this device first because crypto._signObject + // is dumb and bluntly replaces the whole signatures block... + // this can probably go away very soon in favour of just signing with the SSK. + await this._crypto._signObject(data.auth_data); + + // now also sign the auth data with the SSK + pkSign(data.auth_data, Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'), this.credentials.userId); + const keys = { self_signing_key: { user_id: this.credentials.userId, @@ -1072,11 +1080,12 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, auth, replacesSsk return this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { // store the newly generated account keys this._cryptoStore.storeAccountKeys(txn, info.accountKeys); + }).then(() => { + // re-check the SSK in the device store if necessary + return this._crypto.checkOwnSskTrust(); }).then(() => { // upload the public part of the account keys return this.uploadDeviceSigningKeys(keys); - }).then(() => { - return this._crypto._signObject(data.auth_data); }).then(() => { return this._http.authedRequest( undefined, "POST", "/room_keys/version", undefined, data, @@ -1235,6 +1244,8 @@ MatrixClient.prototype._restoreKeyBackup = async function( await this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { this._cryptoStore.storeAccountKeys(txn, accountKeys); }); + + await this._crypto.checkOwnSskTrust(); } catch(e) { decryption.free(); throw e; diff --git a/src/crypto/index.js b/src/crypto/index.js index f80e8edd1e0..5162a892c15 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -266,51 +266,63 @@ Crypto.prototype.init = async function() { */ Crypto.prototype._onDeviceListUserSskUpdated = async function(userId) { if (userId === this._userId) { - // If we see an update to our own SSK, check it against the SSK we have and, - // if it matches, mark it as verified + this.checkOwnSskTrust(); + } +} - // First, get the pubkey of the one we can see - const seenSsk = this._deviceList.getStoredSskForUser(userId); - if (!seenSsk) { - logger.error("Got SSK update event for user " + userId + " but no new SSK found!"); - return; - } - const seenPubkey = seenSsk.getFingerprint(); +/* + * Check the copy of our SSK that we have in the device list and see if it + * matches our private part. If it does, mark it as trusted. + */ +Crypto.prototype.checkOwnSskTrust = async function() { + const userId = this._userId; - // Now dig out the account keys and get the pubkey of the one in there - let accountKeys = null; - await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this._cryptoStore.getAccountKeys(txn, keys => { - accountKeys = keys; - }); + // If we see an update to our own SSK, check it against the SSK we have and, + // if it matches, mark it as verified + + // First, get the pubkey of the one we can see + const seenSsk = this._deviceList.getStoredSskForUser(userId); + if (!seenSsk) { + logger.error("Got SSK update event for user " + userId + " but no new SSK found!"); + return; + } + const seenPubkey = seenSsk.getFingerprint(); + + // Now dig out the account keys and get the pubkey of the one in there + let accountKeys = null; + await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + this._cryptoStore.getAccountKeys(txn, keys => { + accountKeys = keys; }); - if (!accountKeys || !accountKeys.self_signing_key_seed) { - logger.info("Ignoring new self-signing key for us because we have no private part stored"); - return; - } - let signing; - let localPubkey; - try { - signing = new global.Olm.PkSigning(); - localPubkey = signing.init_with_seed(Buffer.from(accountKeys.self_signing_key_seed, 'base64')) - } finally { - if (signing) signing.free(); - signing = null; - } - if (!localPubkey) { - logger.error("Unable to compute public key for stored SSK seed"); - } + }); + if (!accountKeys || !accountKeys.self_signing_key_seed) { + logger.info("Ignoring new self-signing key for us because we have no private part stored"); + return; + } + let signing; + let localPubkey; + try { + signing = new global.Olm.PkSigning(); + localPubkey = signing.init_with_seed(Buffer.from(accountKeys.self_signing_key_seed, 'base64')) + } finally { + if (signing) signing.free(); + signing = null; + } + if (!localPubkey) { + logger.error("Unable to compute public key for stored SSK seed"); + } - // Finally, are they the same? - if (seenPubkey === localPubkey) { - logger.info("Published self-signing key matches local copy: marking as verified"); - this.setSskVerification(userId, SskInfo.SskVerification.VERIFIED); - } else { - logger.info( - "Published self-signing key DOES NOT match local copy! Local: " + - localPubkey + ", published: " + seenPubkey, - ); - } + // Finally, are they the same? + if (seenPubkey === localPubkey) { + logger.info("Published self-signing key matches local copy: marking as verified"); + this.setSskVerification(userId, SskInfo.SskVerification.VERIFIED); + // Now we may be able to trust our key backup + await this.checkKeyBackup(); + } else { + logger.info( + "Published self-signing key DOES NOT match local copy! Local: " + + localPubkey + ", published: " + seenPubkey, + ); } }; @@ -397,7 +409,34 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { } for (const keyId of Object.keys(mySigs)) { - const sigInfo = { deviceId: keyId.split(':')[1] }; // XXX: is this how we're supposed to get the device ID? + const sigInfo = {}; + // Could be an SSK but just say this is the device ID for backwards compat + sigInfo.deviceId = keyId.split(':')[1]; + + // first check to see if it's from our SSK + const ssk = this._deviceList.getStoredSskForUser(this._userId); + if (ssk && ssk.getKeyId() === keyId) { + sigInfo.self_signing_key = ssk; + try { + await olmlib.verifySignature( + this._olmDevice, + backupInfo.auth_data, + this._userId, + sigInfo.deviceId, + ssk.getFingerprint(), + ); + sigInfo.valid = true; + } catch (e) { + console.log("Bad signature from ssk " + ssk.getKeyId(), e); + sigInfo.valid = false; + } + ret.sigs.push(sigInfo); + continue; + } + + // Now look for a sig from a device + // At some point this can probably go away and we'll just support + // it being signed by the SSK const device = this._deviceList.getStoredDevice( this._userId, sigInfo.deviceId, ); @@ -423,7 +462,13 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { ret.sigs.push(sigInfo); } - ret.usable = ret.sigs.some((s) => s.valid && s.device.isVerified()); + ret.usable = ret.sigs.some((s) => { + return ( + s.valid && ( + (s.device && s.device.isVerified()) || (s.self_signing_key && s.self_signing_key.isVerified()) + ) + ) + }); return ret; }; @@ -2340,11 +2385,17 @@ Crypto.prototype._getRoomDecryptor = function(roomId, algorithm) { * @param {Object} obj Object to which we will add a 'signatures' property */ Crypto.prototype._signObject = async function(obj) { - const sigs = {}; - sigs[this._userId] = {}; + const sigs = obj.signatures || {}; + const unsigned = obj.unsigned; + + delete obj.signatures; + delete obj.unsigned; + + sigs[this._userId] = sigs[this._userId] || {}; sigs[this._userId]["ed25519:" + this._deviceId] = await this._olmDevice.sign(anotherjson.stringify(obj)); obj.signatures = sigs; + if (unsigned !== undefined) obj.unsigned = unsigned; }; diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index 4ee89bf8d3a..0a7546c934b 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -283,9 +283,10 @@ const _verifySignature = module.exports.verifySignature = async function( // prepare the canonical json: remove unsigned and signatures, and stringify with // anotherjson - delete obj.unsigned; - delete obj.signatures; - const json = anotherjson.stringify(obj); + const mangledObj = Object.assign({}, obj); + delete mangledObj.unsigned; + delete mangledObj.signatures; + const json = anotherjson.stringify(mangledObj); olmDevice.verifySignature( signingKey, json, signature, diff --git a/src/crypto/sskinfo.js b/src/crypto/sskinfo.js index 660426deab0..4ed63341d3d 100644 --- a/src/crypto/sskinfo.js +++ b/src/crypto/sskinfo.js @@ -62,6 +62,10 @@ export default class SskInfo { return res; } + getKeyId() { + return Object.keys(this.keys)[0]; + } + getFingerprint() { return Object.values(this.keys)[0]; } From 719536518853c486791099e63d9febdf22d39aa4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 Feb 2019 15:59:53 +0000 Subject: [PATCH 06/97] Update package-lock.json because Travis and npm now have a thing where they combust if your package-lock is out of sync --- package-lock.json | 68 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index ca269928670..b924079c57a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.14.2", + "version": "0.14.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -369,6 +369,17 @@ "trim-right": "^1.0.1" } }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, "babel-helper-call-delegate": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", @@ -393,6 +404,17 @@ "lodash": "^4.17.4" } }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, "babel-helper-function-name": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", @@ -495,6 +517,18 @@ "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", "dev": true }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", + "dev": true + }, "babel-plugin-transform-async-to-bluebird": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-bluebird/-/babel-plugin-transform-async-to-bluebird-1.1.1.tgz", @@ -507,6 +541,18 @@ "babel-traverse": "^6.10.4" } }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-plugin-syntax-class-properties": "^6.8.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, "babel-plugin-transform-es2015-arrow-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", @@ -741,6 +787,17 @@ "regexpu-core": "^2.0.0" } }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", + "babel-plugin-syntax-exponentiation-operator": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, "babel-plugin-transform-regenerator": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", @@ -820,6 +877,15 @@ "babel-plugin-transform-regenerator": "^6.24.1" } }, + "babel-preset-es2016": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2016/-/babel-preset-es2016-6.24.1.tgz", + "integrity": "sha1-+QC/k+LrwNJ235uKtZck6/2Vn4s=", + "dev": true, + "requires": { + "babel-plugin-transform-exponentiation-operator": "^6.24.1" + } + }, "babel-register": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", From 7dedcb82b23d19ccd3b2ddf44c1501f4e9716fe9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 Feb 2019 18:12:27 +0000 Subject: [PATCH 07/97] Lint or at least the rules that are consistent with the rest of our codebase --- src/base-apis.js | 3 +- src/client.js | 94 +++++++++++++++++++++++++++------------- src/crypto/DeviceList.js | 14 +++--- src/crypto/PkSigning.js | 3 ++ src/crypto/index.js | 10 ++--- 5 files changed, 82 insertions(+), 42 deletions(-) diff --git a/src/base-apis.js b/src/base-apis.js index 9186c3f255d..8018881fcbc 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -1596,7 +1596,8 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) { MatrixBaseApis.prototype.uploadKeySignatures = function(content) { return this._http.authedRequestWithPrefix( - undefined, "POST", '/keys/signatures/upload', undefined, content, httpApi.PREFIX_UNSTABLE, + undefined, "POST", '/keys/signatures/upload', undefined, + content, httpApi.PREFIX_UNSTABLE, ); }; diff --git a/src/client.js b/src/client.js index c3e81aa11cf..cb24c38948a 100644 --- a/src/client.js +++ b/src/client.js @@ -958,7 +958,9 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { throw new Error("End-to-end encryption disabled"); } - let decryption, encryption, signing; + let decryption; + let encryption; + let signing; try { decryption = new global.Olm.PkDecryption(); encryption = new global.Olm.PkEncryption(); @@ -988,32 +990,43 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { }; if (signing) { - await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this._cryptoStore.getAccountKeys(txn, keys => { - returnInfo.accountKeys = keys; - }); - }); + await this._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.getAccountKeys(txn, (keys) => { + returnInfo.accountKeys = keys; + }); + }, + ); if (!returnInfo.accountKeys) { - const ssk_seed = signing.generate_seed(); - const usk_seed = signing.generate_seed(); + const sskSeed = signing.generate_seed(); + const uskSeed = signing.generate_seed(); returnInfo.accountKeys = { - self_signing_key_seed: Buffer.from(ssk_seed).toString('base64'), - user_signing_key_seed: Buffer.from(usk_seed).toString('base64'), - } + self_signing_key_seed: Buffer.from(sskSeed).toString('base64'), + user_signing_key_seed: Buffer.from(uskSeed).toString('base64'), + }; } // put the encrypted version of the seed in the auth data to upload // XXX: our encryption really should support encrypting binary data. - authData.self_signing_key_seed = encryption.encrypt(returnInfo.accountKeys.self_signing_key_seed); + authData.self_signing_key_seed = encryption.encrypt( + returnInfo.accountKeys.self_signing_key_seed, + ); // also keep the public part there - returnInfo.ssk_public = signing.init_with_seed(Buffer.from(returnInfo.accountKeys.self_signing_key_seed, 'base64')); + returnInfo.ssk_public = signing.init_with_seed( + Buffer.from(returnInfo.accountKeys.self_signing_key_seed, 'base64'), + ); signing.free(); // same for the USK - authData.user_signing_key_seed = encryption.encrypt(returnInfo.accountKeys.user_signing_key_seed); - returnInfo.usk_public = signing.init_with_seed(Buffer.from(returnInfo.accountKeys.user_signing_key_seed, 'base64')); + authData.user_signing_key_seed = encryption.encrypt( + returnInfo.accountKeys.user_signing_key_seed, + ); + returnInfo.usk_public = signing.init_with_seed( + Buffer.from(returnInfo.accountKeys.user_signing_key_seed, 'base64'), + ); signing.free(); // we don't save these keys back to the store yet: we'll do that when (if) we @@ -1033,6 +1046,8 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { * from prepareKeyBackupVersion. * * @param {object} info Info object from prepareKeyBackupVersion + * @param {object} auth Auth object for UI auth + * @param {string} replacesSsk If the SSK is being replaced, the ID of the old key * @returns {Promise} Object with 'version' param indicating the version created */ MatrixClient.prototype.createKeyBackupVersion = async function(info, auth, replacesSsk) { @@ -1054,7 +1069,11 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info, auth, repla }; // sign the USK with the SSK - pkSign(uskInfo, Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'), this.credentials.userId); + pkSign( + uskInfo, + Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'), + this.credentials.userId, + ); // Now sig the backup auth data. Do it as this device first because crypto._signObject // is dumb and bluntly replaces the whole signatures block... @@ -1062,7 +1081,11 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info, auth, repla await this._crypto._signObject(data.auth_data); // now also sign the auth data with the SSK - pkSign(data.auth_data, Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'), this.credentials.userId); + pkSign( + data.auth_data, + Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'), + this.credentials.userId, + ); const keys = { self_signing_key: { @@ -1077,10 +1100,13 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info, auth, repla auth, }; - return this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - // store the newly generated account keys - this._cryptoStore.storeAccountKeys(txn, info.accountKeys); - }).then(() => { + return this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + // store the newly generated account keys + this._cryptoStore.storeAccountKeys(txn, info.accountKeys); + }, + ).then(() => { // re-check the SSK in the device store if necessary return this._crypto.checkOwnSskTrust(); }).then(() => { @@ -1220,11 +1246,14 @@ MatrixClient.prototype._restoreKeyBackup = async function( // decrypt the account keys from the backup info if there are any // fetch the old ones first so we don't lose info if only one of them is in the backup let accountKeys; - await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this._cryptoStore.getAccountKeys(txn, keys => { - accountKeys = keys || {}; - }); - }); + await this._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.getAccountKeys(txn, (keys) => { + accountKeys = keys || {}; + }); + }, + ); if (backupInfo.auth_data.self_signing_key_seed) { accountKeys.self_signing_key_seed = decryption.decrypt( @@ -1241,9 +1270,12 @@ MatrixClient.prototype._restoreKeyBackup = async function( ); } - await this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this._cryptoStore.storeAccountKeys(txn, accountKeys); - }); + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.storeAccountKeys(txn, accountKeys); + }, + ); await this._crypto.checkOwnSskTrust(); } catch(e) { @@ -1254,7 +1286,9 @@ MatrixClient.prototype._restoreKeyBackup = async function( // start by signing this device from the SSK now we have it return this._crypto.uploadDeviceKeySignatures().then(() => { // Now fetch the encrypted keys - const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version); + const path = this._makeKeyBackupPath( + targetRoomId, targetSessionId, backupInfo.version, + ); return this._http.authedRequest( undefined, "GET", path.path, path.queryData, ); diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 6ff51789c53..16b59bd1c60 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -788,7 +788,9 @@ class DeviceListUpdateSerialiser { let prom = Promise.resolve(); for (const userId of downloadUsers) { prom = prom.delay(5).then(() => { - return this._processQueryResponseForUser(userId, dk[userId], ssks[userId]); + return this._processQueryResponseForUser( + userId, dk[userId], ssks[userId], + ); }); } @@ -812,9 +814,9 @@ class DeviceListUpdateSerialiser { return deferred.promise; } - async _processQueryResponseForUser(userId, dk_response, ssk_response) { - logger.log('got device keys for ' + userId + ':', dk_response); - logger.log('got self-signing keys for ' + userId + ':', ssk_response); + async _processQueryResponseForUser(userId, dkResponse, sskResponse) { + logger.log('got device keys for ' + userId + ':', dkResponse); + logger.log('got self-signing keys for ' + userId + ':', sskResponse); { // map from deviceid -> deviceinfo for this user @@ -828,7 +830,7 @@ class DeviceListUpdateSerialiser { } await _updateStoredDeviceKeysForUser( - this._olmDevice, userId, userStore, dk_response || {}, + this._olmDevice, userId, userStore, dkResponse || {}, ); // put the updates into the object that will be returned as our results @@ -845,7 +847,7 @@ class DeviceListUpdateSerialiser { const ssk = this._deviceList.getRawStoredSskForUser(userId) || {}; const updated = await _updateStoredSelfSigningKeyForUser( - this._olmDevice, userId, ssk, ssk_response || {}, + this._olmDevice, userId, ssk, sskResponse || {}, ); this._deviceList.setRawStoredSskForUser(userId, ssk); diff --git a/src/crypto/PkSigning.js b/src/crypto/PkSigning.js index 4de1c2f3e8b..483b13437d6 100644 --- a/src/crypto/PkSigning.js +++ b/src/crypto/PkSigning.js @@ -18,6 +18,9 @@ const anotherjson = require('another-json'); /** * Higher level wrapper around olm.PkSigning that signs JSON objects + * @param obj {Object} Object to sign + * @param seed {Uint8Array} The private key seed (32 bytes) + * @param userId {string} The user ID who owns the signing key */ export function pkSign(obj, seed, userId) { const signing = new global.Olm.PkSigning(); diff --git a/src/crypto/index.js b/src/crypto/index.js index 5162a892c15..e60374846e8 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -268,7 +268,7 @@ Crypto.prototype._onDeviceListUserSskUpdated = async function(userId) { if (userId === this._userId) { this.checkOwnSskTrust(); } -} +}; /* * Check the copy of our SSK that we have in the device list and see if it @@ -303,7 +303,7 @@ Crypto.prototype.checkOwnSskTrust = async function() { let localPubkey; try { signing = new global.Olm.PkSigning(); - localPubkey = signing.init_with_seed(Buffer.from(accountKeys.self_signing_key_seed, 'base64')) + localPubkey = signing.init_with_seed(Buffer.from(accountKeys.self_signing_key_seed, 'base64')); } finally { if (signing) signing.free(); signing = null; @@ -467,7 +467,7 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { s.valid && ( (s.device && s.device.isVerified()) || (s.self_signing_key && s.self_signing_key.isVerified()) ) - ) + ); }); return ret; }; @@ -622,7 +622,7 @@ Crypto.prototype.uploadDeviceKeySignatures = async function() { }; await crypto._baseApis.uploadKeySignatures(content); - return true + return true; }; /** @@ -829,7 +829,7 @@ Crypto.prototype.setSskVerification = async function(userId, verified) { throw new Error("No self-signing key found for user " + userId); } ssk.verified = verified; - this._deviceList.storeSskForUser(userId, ssk) + this._deviceList.storeSskForUser(userId, ssk); this._deviceList.saveIfDirty(); }; From c8082535de1cdfe0ff1c81609f9b7f1e9860a074 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 Feb 2019 19:19:00 +0000 Subject: [PATCH 08/97] Always track your own devices This was causing all the cross-signing stuff to fail and was almost certainly the cause of https://github.com/vector-im/riot-web/issues/8213 --- src/crypto/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/crypto/index.js b/src/crypto/index.js index e60374846e8..79d216388b2 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -257,6 +257,9 @@ Crypto.prototype.init = async function() { ); this._deviceList.saveIfDirty(); } + // make sure we are keeping track of our own devices + // (this is important for key backups & things) + this._deviceList.startTrackingDeviceList(this._userId); this._checkAndStartKeyBackup(); }; From 5500f0d794a9d4e7b55634d0ae0eebe106187883 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 Feb 2019 22:39:12 +0000 Subject: [PATCH 09/97] Re-track own device list Sp we don't stop tracking our own --- src/crypto/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/crypto/index.js b/src/crypto/index.js index 79d216388b2..eac52a1ba75 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1618,6 +1618,8 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) { // at which point we'll start tracking all the users of that room. logger.log("Initial sync performed - resetting device tracking state"); this._deviceList.stopTrackingAllDeviceLists(); + // we always track our own device list (for key backups etc) + this._deviceList.startTrackingDeviceList(this._userId); this._roomDeviceTrackingState = {}; } }; From b3513dc8f8c42239a1b2cc72f12488888f1285c9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 5 Feb 2019 11:56:08 +0000 Subject: [PATCH 10/97] Make linting rules more consistent * Put back babel-eslint for class-properties * Allow arrow functions without params This makes the style more consistent with react-sdk. NB. The line lengths are still inconsistent but it's not clear which way to go on that yet. --- .eslintrc.js | 3 +- package-lock.json | 221 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 224 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index ef7ad036c93..4606ca6ae3f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,6 @@ module.exports = { + parser: "babel-eslint", // now needed for class properties parserOptions: { - ecmaVersion: 2017, sourceType: "module", ecmaFeatures: { } @@ -72,5 +72,6 @@ module.exports = { "named": "never", "asyncArrow": "always", }], + "arrow-parens": "off", } } diff --git a/package-lock.json b/package-lock.json index b924079c57a..9b94ba48349 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,195 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/generator": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.2.tgz", + "integrity": "sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ==", + "dev": true, + "requires": { + "@babel/types": "^7.3.2", + "jsesc": "^2.5.1", + "lodash": "^4.17.10", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", + "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.2.tgz", + "integrity": "sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ==", + "dev": true + }, + "@babel/template": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz", + "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.2.2", + "@babel/types": "^7.2.2" + } + }, + "@babel/traverse": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz", + "integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.2.2", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.0.0", + "@babel/parser": "^7.2.3", + "@babel/types": "^7.2.2", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.10" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "globals": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.10.0.tgz", + "integrity": "sha512-0GZF1RiPKU97IHUO5TORo9w1PwrH/NBPl+fS7oMLdaTRiYmYbwK4NWoZWrAdd0/abG9R2BU+OiwyQpTpE6pdfQ==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.2.tgz", + "integrity": "sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.10", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + } + } + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -353,6 +542,20 @@ "source-map": "^0.5.7" } }, + "babel-eslint": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.1.tgz", + "integrity": "sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "^1.0.0" + } + }, "babel-generator": { "version": "6.26.1", "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", @@ -2184,6 +2387,24 @@ "integrity": "sha1-VZj4SY6eB4Qg80uASVuNlZ9lH7I=", "dev": true }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, "eslint-utils": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", diff --git a/package.json b/package.json index bf77a0c3aa8..58f22c970cb 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ }, "devDependencies": { "babel-cli": "^6.18.0", + "babel-eslint": "^10.0.1", "babel-plugin-transform-async-to-bluebird": "^1.1.1", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-runtime": "^6.23.0", From 7f5584e4f594fd459f295f105c955ab2d269b5b4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 5 Feb 2019 13:03:27 +0000 Subject: [PATCH 11/97] All the linting --- src/crypto/DeviceList.js | 13 ++++++-- src/crypto/PkSigning.js | 6 ++-- src/crypto/index.js | 65 +++++++++++++++++++++++++++------------- src/crypto/sskinfo.js | 4 +-- 4 files changed, 60 insertions(+), 28 deletions(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 16b59bd1c60..6cd83d3a334 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -917,13 +917,17 @@ async function _updateStoredSelfSigningKeyForUser( return; } if (!userResult || !userResult.usage.includes('self_signing')) { - logger.warn("Self-signing key for " + userId + " does not include 'self_signing' usage: ignoring"); + logger.warn( + "Self-signing key for " + userId + + " does not include 'self_signing' usage: ignoring", + ); return; } const keyCount = Object.keys(userResult.keys).length; if (keyCount !== 1) { logger.warn( - "Self-signing key block for " + userId + " has " + keyCount + " keys: expected exactly 1. Ignoring.", + "Self-signing key block for " + userId + " has " + + keyCount + " keys: expected exactly 1. Ignoring.", ); return; } @@ -937,7 +941,10 @@ async function _updateStoredSelfSigningKeyForUser( const newKey = userResult.keys[newKeyId]; if (oldKeyId !== newKeyId || oldKey !== newKey) { updated = true; - logger.info("New self-signing key detected for " + userId + ": " + newKeyId + ", was previously " + oldKeyId); + logger.info( + "New self-signing key detected for " + userId + + ": " + newKeyId + ", was previously " + oldKeyId, + ); userStore.user_id = userResult.user_id; userStore.usage = userResult.usage; diff --git a/src/crypto/PkSigning.js b/src/crypto/PkSigning.js index 483b13437d6..35703935781 100644 --- a/src/crypto/PkSigning.js +++ b/src/crypto/PkSigning.js @@ -18,9 +18,9 @@ const anotherjson = require('another-json'); /** * Higher level wrapper around olm.PkSigning that signs JSON objects - * @param obj {Object} Object to sign - * @param seed {Uint8Array} The private key seed (32 bytes) - * @param userId {string} The user ID who owns the signing key + * @param {Object} obj Object to sign + * @param {Uint8Array} seed The private key seed (32 bytes) + * @param {string} userId The user ID who owns the signing key */ export function pkSign(obj, seed, userId) { const signing = new global.Olm.PkSigning(); diff --git a/src/crypto/index.js b/src/crypto/index.js index eac52a1ba75..7e7837eca61 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -286,27 +286,37 @@ Crypto.prototype.checkOwnSskTrust = async function() { // First, get the pubkey of the one we can see const seenSsk = this._deviceList.getStoredSskForUser(userId); if (!seenSsk) { - logger.error("Got SSK update event for user " + userId + " but no new SSK found!"); + logger.error( + "Got SSK update event for user " + userId + + " but no new SSK found!", + ); return; } const seenPubkey = seenSsk.getFingerprint(); // Now dig out the account keys and get the pubkey of the one in there let accountKeys = null; - await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this._cryptoStore.getAccountKeys(txn, keys => { - accountKeys = keys; - }); - }); + await this._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.getAccountKeys(txn, keys => { + accountKeys = keys; + }); + }, + ); if (!accountKeys || !accountKeys.self_signing_key_seed) { - logger.info("Ignoring new self-signing key for us because we have no private part stored"); + logger.info( + "Ignoring new self-signing key for us because we have no private part stored", + ); return; } let signing; let localPubkey; try { signing = new global.Olm.PkSigning(); - localPubkey = signing.init_with_seed(Buffer.from(accountKeys.self_signing_key_seed, 'base64')); + localPubkey = signing.init_with_seed( + Buffer.from(accountKeys.self_signing_key_seed, 'base64'), + ); } finally { if (signing) signing.free(); signing = null; @@ -468,7 +478,8 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { ret.usable = ret.sigs.some((s) => { return ( s.valid && ( - (s.device && s.device.isVerified()) || (s.self_signing_key && s.self_signing_key.isVerified()) + (s.device && s.device.isVerified()) || + (s.self_signing_key && s.self_signing_key.isVerified()) ) ); }); @@ -568,15 +579,22 @@ Crypto.prototype.uploadDeviceKeys = function() { let accountKeys; return crypto._signObject(deviceKeys).then(() => { - return this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this._cryptoStore.getAccountKeys(txn, keys => { - accountKeys = keys; - }); - }); + return this._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.getAccountKeys(txn, keys => { + accountKeys = keys; + }); + }, + ); }).then(() => { if (accountKeys && accountKeys.self_signing_key_seed) { // if we have an SSK, sign the key with the SSK too - pkSign(deviceKeys, Buffer.from(accountKeys.self_signing_key_seed, 'base64'), userId); + pkSign( + deviceKeys, + Buffer.from(accountKeys.self_signing_key_seed, 'base64'), + userId, + ); } return crypto._baseApis.uploadKeysRequest({ @@ -608,15 +626,22 @@ Crypto.prototype.uploadDeviceKeySignatures = async function() { user_id: userId, }; let accountKeys; - await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - this._cryptoStore.getAccountKeys(txn, keys => { - accountKeys = keys; - }); + await this._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.getAccountKeys(txn, keys => { + accountKeys = keys; + }, + ); }); if (!accountKeys || !accountKeys.self_signing_key_seed) return false; // Sign this device with the SSK - pkSign(thisDeviceKey, Buffer.from(accountKeys.self_signing_key_seed, 'base64'), userId); + pkSign( + thisDeviceKey, + Buffer.from(accountKeys.self_signing_key_seed, 'base64'), + userId, + ); const content = { [userId]: { diff --git a/src/crypto/sskinfo.js b/src/crypto/sskinfo.js index 4ed63341d3d..1f578247f0f 100644 --- a/src/crypto/sskinfo.js +++ b/src/crypto/sskinfo.js @@ -72,9 +72,9 @@ export default class SskInfo { isVerified() { return this.verified == SskInfo.SskVerification.VERIFIED; - }; + } isUnverified() { return this.verified == SskInfo.SskVerification.UNVERIFIED; - }; + } } From e54f71718fb34971d11156cd6a24b8ee98d52fc9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 5 Feb 2019 13:41:14 +0000 Subject: [PATCH 12/97] Olm pre2 for cross-signing --- travis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/travis.sh b/travis.sh index 02b2f0bbbde..4c8b6658384 100755 --- a/travis.sh +++ b/travis.sh @@ -5,7 +5,7 @@ set -ex npm run lint # install Olm so that we can run the crypto tests. -npm install https://matrix.org/packages/npm/olm/olm-3.1.0-pre1.tgz +npm install https://matrix.org/packages/npm/olm/olm-3.1.0-pre2.tgz npm run test From ec2f07e1aa20808fd3258f1913b649b523de3bc2 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 3 May 2019 18:05:36 -0400 Subject: [PATCH 13/97] add methods for signing and checking users and devices with cross-signing --- src/client.js | 29 ++++++ src/crypto/DeviceList.js | 26 ++--- src/crypto/index.js | 107 +++++++++++++++++++++ src/crypto/olmlib.js | 62 ++++++++++++ src/crypto/store/indexeddb-crypto-store.js | 2 +- 5 files changed, 212 insertions(+), 14 deletions(-) diff --git a/src/client.js b/src/client.js index 4e1b33f4ec6..ec1dcf6c724 100644 --- a/src/client.js +++ b/src/client.js @@ -762,6 +762,29 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() { return this._crypto.getGlobalBlacklistUnverifiedDevices(); }; +/** + * returns a function that just calls the corresponding function from this._crypto. + * + * @param {string} name the function to call + * + * @return {Function} a wrapper function + */ +function wrapCryptoFunc(name) { + return function(...args) { + if (!this._crypto) { // eslint-disable-line no-invalid-this + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto[name](...args); // eslint-disable-line no-invalid-this + }; +} + +MatrixClient.prototype.checkUserTrust + = wrapCryptoFunc("checkUserTrust"); + +MatrixClient.prototype.checkDeviceTrust + = wrapCryptoFunc("checkDeviceTrust"); + /** * Get e2e information on the device that sent an event * @@ -793,6 +816,12 @@ MatrixClient.prototype.isEventSenderVerified = async function(event) { return device.isVerified(); }; +MatrixClient.prototype.resetCrossSigningKeys + = wrapCryptoFunc("resetCrossSigningKeys"); + +MatrixClient.prototype.setCrossSigningKeys + = wrapCryptoFunc("setCrossSigningKeys"); + /** * Cancel a room key request for this event if one is ongoing and resend the * request. diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 6a5ec635f8d..0c715cf0779 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -27,7 +27,7 @@ import {EventEmitter} from 'events'; import logger from '../logger'; import DeviceInfo from './deviceinfo'; -import SskInfo from './sskinfo'; +import {CrossSigningInfo, CrossSigningVerification} from './CrossSigning'; import olmlib from './olmlib'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; @@ -78,7 +78,7 @@ export default class DeviceList extends EventEmitter { // userId -> { // [key info] // } - this._ssks = {}; + this._crossSigningInfo = {}; // map of identity keys to the user who owns it this._userByIdentityKey = {}; @@ -345,18 +345,18 @@ export default class DeviceList extends EventEmitter { return this._devices[userId]; } - getRawStoredSskForUser(userId) { - return this._ssks[userId]; + getRawStoredCrossSigningForUser(userId) { + return this._crossSigningInfo[userId]; } - getStoredSskForUser(userId) { - if (!this._ssks[userId]) return null; + getStoredCrossSigningForUser(userId) { + if (!this._crossSigningInfo[userId]) return null; - return SskInfo.fromStorage(this._ssks[userId]); + return CrossSigningInfo.fromStorage(this._crossSigningInfo[userId], userId); } - storeSskForUser(userId, ssk) { - this._ssks[userId] = ssk; + storeCrossSigningForUser(userId, info) { + this._crossSigningInfo[userId] = info; this._dirty = true; } @@ -587,8 +587,8 @@ export default class DeviceList extends EventEmitter { } } - setRawStoredSskForUser(userId, ssk) { - this._ssks[userId] = ssk; + setRawStoredCrossSigningForUser(userId, info) { + this._crossSigningInfo[userId] = info; } /** @@ -838,6 +838,7 @@ class DeviceListUpdateSerialiser { async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore, userResult) { + // FIXME: this isn't correct any more let updated = false; // remove any devices in the store which aren't in the response @@ -885,6 +886,7 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore, async function _updateStoredSelfSigningKeyForUser( _olmDevice, userId, userStore, userResult, ) { + // FIXME: this function may need modifying let updated = false; if (userResult.user_id !== userId) { @@ -925,8 +927,6 @@ async function _updateStoredSelfSigningKeyForUser( userStore.user_id = userResult.user_id; userStore.usage = userResult.usage; userStore.keys = userResult.keys; - // reset verification status since its now a new key - userStore.verified = SskInfo.SskVerification.UNVERIFIED; } return updated; diff --git a/src/crypto/index.js b/src/crypto/index.js index 955a52b894b..cbcfab78b8e 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -35,6 +35,7 @@ import SskInfo from './sskinfo'; const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceList = require('./DeviceList').default; import { randomString } from '../randomstring'; +import { CrossSigningInfo, CrossSigningLevel, CrossSigningVerification } from './CrossSigning'; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; @@ -196,6 +197,14 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, this._lastNewSessionForced = {}; this._verificationTransactions = new Map(); + + this._crossSigningInfo = new CrossSigningInfo(userId); + this._crossSigningInfo.on("cross-signing:savePrivateKeys", (...args) => { + this._baseApis.emit("cross-signing:savePrivateKeys", ...args); + }); + this._crossSigningInfo.on("cross-signing:getKey", (...args) => { + this._baseApis.emit("cross-signing:getKey", ...args); + }); } utils.inherits(Crypto, EventEmitter); @@ -251,6 +260,74 @@ Crypto.prototype.init = async function() { this._checkAndStartKeyBackup(); }; +/** + * Generate new cross-signing keys. + * + * @param {CrossSigningLevel} level the level of cross-signing to reset. New + * keys will be created for the given level and below. Defaults to + * regenerating all keys. + */ +Crypto.prototype.resetCrossSigningKeys = async function(level) { + await this._crossSigningInfo.resetKeys(level); +}; + +/** + * Set the user's cross-signing keys to use. + * + * @param {object} keys A mapping of key type to key data. + */ +Crypto.prototype.setCrossSigningKeys = function(keys) { + this._crossSigningInfo.setKeys(keys); +}; + +/** + * Check whether a given user is trusted. + * + * @param {string} userId The ID of the user to check. + * + * @returns {integer} a bit mask indicating how the user is trusted (if at all) + * - returnValue & 1: unused + * - returnValue & 2: trust-on-first-use cross-signing key + * - returnValue & 4: user's cross-signing key is verified + * + * TODO: is this a good way of representing it? Or we could return an object + * with different keys, or a set? The advantage of doing it this way is that + * you can define which methods you want to use, "&" with the appopriate mask, + * then test for truthiness. Or if you want to just trust everything, then use + * the value alone. However, I wonder if bit masks are too obscure... + */ +Crypto.prototype.checkUserTrust = function(userId) { + const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + if (!userCrossSigning) { + return 0; + } + return this._crossSigningInfo.checkUserTrust(userCrossSigning) << 1; +}; + +/** + * Check whether a given device is trusted. + * + * @param {string} userId The ID of the user whose devices is to be checked. + * @param {string} deviceId The ID of the device to check + * + * @returns {integer} a bit mask indicating how the user is trusted (if at all) + * - returnValue & 1: device marked as verified + * - returnValue & 2: trust-on-first-use cross-signing key + * - returnValue & 4: user's cross-signing key is verified and device is signed + * + * TODO: see checkUserTrust + */ +Crypto.prototype.checkDeviceTrust = function(userId, deviceId) { + const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + const device = this._deviceList.getStoredDevice(userId, deviceId); + let rv = 0; + if (device.isVerified()) { + rv |= 1; + } + rv |= this._crossSigningInfo.checkDeviceTrust(userCrossSigning, device) << 1; + return rv; +}; + /* * Event handler for DeviceList's userNewDevices event */ @@ -906,6 +983,24 @@ Crypto.prototype.setSskVerification = async function(userId, verified) { Crypto.prototype.setDeviceVerification = async function( userId, deviceId, verified, blocked, known, ) { + const xsk = this._deviceList.getStoredCrossSigningForUser(userId); + if (xsk.getId() === deviceId) { + if (verified) { + xsk.verified = CrossSigningVerification.VERIFIED; + const device = await this._crossSigningInfo.signUser(xsk); + // FIXME: mark xsk as dirty in device list + this._baseApis.uploadKeySignatures({ + [userId]: { + [deviceId]: device, + }, + }); + return device; + } else { + // FIXME: ??? + } + return; + } + const devices = this._deviceList.getRawStoredDevicesForUser(userId); if (!devices || !devices[deviceId]) { throw new Error("Unknown device " + userId + ":" + deviceId); @@ -937,6 +1032,18 @@ Crypto.prototype.setDeviceVerification = async function( this._deviceList.storeDevicesForUser(userId, devices); this._deviceList.saveIfDirty(); } + + // do cross-signing + if (verified && userId === this._userId) { + const device = await this._crossSigningInfo.signDevice(userId, dev); + // FIXME: mark device as dirty in device list + this._baseApis.uploadKeySignatures({ + [userId]: { + [deviceId]: device, + }, + }); + } + return DeviceInfo.fromStorage(dev, deviceId); }; diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index f403308d7f4..db4d21b18d2 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -337,3 +337,65 @@ const _verifySignature = module.exports.verifySignature = async function( signingKey, json, signature, ); }; + +/** + * Sign a JSON object using public key cryptography + * @param {Object} obj Object to sign. The object will be modified to include + * the new signature + * @param {Olm.PkSigning|Uint8Array} key the signing object or the private key + * seed + * @param {string} userId The user ID who owns the signing key + * @param {string} pubkey The public key (ignored if key is a seed) + * @returns {string} the signature for the object + */ +module.exports.pkSign = function(obj, key, userId, pubkey) { + let createdKey = false; + if (key instanceof Uint8Array) { + const keyObj = new global.Olm.PkSigning(); + pubkey = keyObj.init_with_seed(key); + key = keyObj; + createdKey = true; + } + const sigs = obj.signatures || {}; + delete obj.signatures; + const unsigned = obj.unsigned; + if (obj.unsigned) delete obj.unsigned; + try { + const mysigs = sigs[userId] || {}; + sigs[userId] = mysigs; + + return mysigs['ed25519:' + pubkey] = key.sign(anotherjson.stringify(obj)); + } finally { + obj.signatures = sigs; + if (unsigned) obj.unsigned = unsigned; + if (createdKey) { + key.free(); + } + } +}; + +/** + * Verify a signed JSON object + * @param {Object} obj Object to verify + * @param {string} pubkey The public key to use to verify + * @param {string} userId The user ID who signed the object + */ +module.exports.pkVerify = function(obj, pubkey, userId) { + const keyId = "ed25519:" + pubkey; + if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) { + throw new Error("No signature"); + } + const signature = obj.signatures[userId][keyId]; + const util = new global.Olm.Utility(); + const sigs = obj.signatures; + delete obj.signatures; + const unsigned = obj.unsigned; + if (obj.unsigned) delete obj.unsigned; + try { + util.ed25519_verify(pubkey, anotherjson.stringify(obj), signature); + } finally { + obj.signatures = sigs; + if (unsigned) obj.unsigned = unsigned; + util.free(); + } +}; diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 1b2aa076ae6..89195f34589 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -302,7 +302,7 @@ export default class IndexedDBCryptoStore { } /** - * Get the account keys fort cross-signing (eg. self-signing key, + * Get the account keys for cross-signing (eg. self-signing key, * user signing key). * * @param {*} txn An active transaction. See doTxn(). From ae71f411384ed8404f5ddbaf36054fbd8c98d011 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 3 May 2019 18:12:17 -0400 Subject: [PATCH 14/97] add missing files --- spec/unit/crypto/cross-signing.spec.js | 192 ++++++++++++++++ src/crypto/CrossSigning.js | 294 +++++++++++++++++++++++++ 2 files changed, 486 insertions(+) create mode 100644 spec/unit/crypto/cross-signing.spec.js create mode 100644 src/crypto/CrossSigning.js diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js new file mode 100644 index 00000000000..16174652e3f --- /dev/null +++ b/spec/unit/crypto/cross-signing.spec.js @@ -0,0 +1,192 @@ +/* +Copyright 2019 New Vector Ltd + +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 '../../olm-loader'; + +import expect from 'expect'; +import anotherjson from 'another-json'; + +import olmlib from '../../../lib/crypto/olmlib'; + +import TestClient from '../../TestClient'; + +async function makeTestClient(userInfo, options) { + const client = (new TestClient( + userInfo.userId, userInfo.deviceId, undefined, undefined, options, + )).client; + + await client.initCrypto(); + + return client; +} + +describe("Cross Signing", function() { + if (!global.Olm) { + console.warn('Not running megolm backup unit tests: libolm not present'); + return; + } + + beforeEach(async function() { + await global.Olm.init(); + }); + + it("should upload a signature when a user is verified", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + // set Alices' cross-signing key + let privateKeys; + alice.on("cross-signing:savePrivateKeys", function(e) { + privateKeys = e; + }); + alice.resetCrossSigningKeys(); + // Alice downloads Bob's device key + alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + keys: { + selfSigning: { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + "ed25519:bobs+ssk+pubkey": "bobs+ssk+pubkey", + }, + }, + }, + verified: 0, + unsigned: {}, + }); + // Alice verifies Bob's key + alice.on("cross-signing:getKey", function(e) { + console.log(e); + expect(e.type).toBe("user_signing"); + e.done(privateKeys.userSigning); + }); + const promise = new Promise((resolve, reject) => { + alice.uploadKeySignatures = (...args) => { + resolve(...args); + }; + }); + alice.setDeviceVerified("@bob:example.com", "bobs+ssk+pubkey", true); + // Alice should send a signature of Bob's key to the server + await promise; + }); + + it("should get ssk and usk from sync", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + alice.on("cross-signing:newKey", function(e) { + // FIXME: ??? + }); + // feed sync result that includes ssk, usk, device key + // client should emit event asking about ssk + // once ssk is confirmed, device key should be trusted + }); + + it("should use trust chain to determine device verification", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + // set Alices' cross-signing key + let privateKeys; + alice.on("cross-signing:savePrivateKeys", function(e) { + console.log("save private keys"); + privateKeys = e; + }); + console.log("reset cross signing keys"); + alice.resetCrossSigningKeys(); + // Alice downloads Bob's ssk and device key + const bobSigning = new global.Olm.PkSigning(); + const bobPrivkey = bobSigning.generate_seed(); + const bobPubkey = bobSigning.init_with_seed(bobPrivkey); + alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + keys: { + selfSigning: { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey]: bobPubkey, + }, + }, + }, + fu: 1, + unsigned: {}, + }); + const bobDevice = { + user_id: "@bob:example.com", + device_id: "Dynabook", + algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + keys: { + "curve25519:Dynabook": "somePubkey", + "ed25519:Dynabook": "someOtherPubkey", + }, + }; + const sig = bobSigning.sign(anotherjson.stringify(bobDevice)); + bobDevice.signatures = {}; + bobDevice.signatures["@bob:example.com"] = {}; + bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig; + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: bobDevice, + }); + // Bob's device key should be TOFU + expect(alice.checkUserTrust("@bob:example.com")).toBe(2); + expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(2); + // Alice verifies Bob's SSK + alice.on("cross-signing:getKey", function(e) { + expect(e.type).toBe("user_signing"); + e.done(privateKeys.userSigning); + }); + alice.uploadKeySignatures = () => {}; + await alice.setDeviceVerified("@bob:example.com", bobPubkey, true); + // Bob's device key should be trusted + expect(alice.checkUserTrust("@bob:example.com")).toBe(6); + expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(6); + }); + + it("should trust signatures received from other devices", async function() { + // Alice downloads Bob's keys + // - device key + // - ssk signed by her usk + // Bob's device key should be trusted + }); + + it("should dis-trust an unsigned device", async function() { + // Alice downloads Bob's device key (without signature) + // Bob's device key should be untrusted + }); + + it("should dis-trust a user when their ssk changes", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + // Alice downloads Bob's keys + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: { + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "curve25519:Dynabook": "blablabla", + "ed25519:Dynabook": "blablabla", + }, + verified: 0, + known: false, + unsigned: {}, + }, + }); + // Alice verifies Bob's SSK + // Bob's devices should be trusted + // Alice downloads new SSK for Bob + // Bob's devices should be untrusted + }); +}); diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js new file mode 100644 index 00000000000..355a999d831 --- /dev/null +++ b/src/crypto/CrossSigning.js @@ -0,0 +1,294 @@ +/* +Copyright 2019 New Vector Ltd + +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. +*/ + +/** + * Cross signing methods + * @module crypto/CrossSigning + */ + +import {pkSign, pkVerify} from './olmlib'; +import {EventEmitter} from 'events'; + +function getPublicKey(keyInfo) { + return Object.entries(keyInfo.keys)[0]; +} + +function getPrivateKey(self, type, check) { + return new Promise((resolve, reject) => { + const askForKey = (error) => { + self.emit("cross-signing:getKey", { + type: type, + error, + done: (key) => { + // FIXME: the key needs to be interpreted? + const signing = new global.Olm.PkSigning(); + const pubkey = signing.init_with_seed(key); + const error = check(pubkey, signing); + if (error) { + return askForKey(error); + } + resolve([pubkey, signing]); + }, + cancel: (error) => { + reject(error || new Error("Cancelled")); + }, + }); + }; + askForKey(); + }); +} + +export class CrossSigningInfo extends EventEmitter { + /** + * Information about a user's cross-signing keys + * + * @class + * + * @param {string} userId the user that the information is about + */ + constructor(userId) { + super(); + + // you can't change the userId + Object.defineProperty(this, 'userId', { + enumerabel: true, + value: userId, + }); + this.keys = {}; + this.fu = true; + // FIXME: add chain of ssks? + } + + static fromStorage(obj, userId) { + const res = new CrossSigningInfo(userId); + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + res[prop] = obj[prop]; + } + } + return res; + } + + toStorage() { + return { + keys: this.keys, + verified: this.verified, + }; + } + + /** Get the ID used to identify the user + * + * @return {string} the ID + */ + getId() { + return getPublicKey(this.keys.selfSigning)[1]; + } + + async resetKeys(level) { + if (level === undefined) { + level = CrossSigningLevel.SELF_SIGNING; + } + + const privateKeys = {}; + const keys = {}; + let sskSigning; + let sskPub; + switch (level) { + case CrossSigningLevel.SELF_SIGNING: { + sskSigning = new global.Olm.PkSigning(); + privateKeys.selfSigning = sskSigning.generate_seed(); + sskPub = sskSigning.init_with_seed(privateKeys.selfSigning); + keys.selfSigning = { + user_id: this.userId, + usage: ['self_signing'], + keys: { + ['ed25519:' + sskPub]: sskPub, + }, + }; + if (this.keys.selfSigning) { + keys.selfSigning.replaces = getPublicKey(this.keys.selfSigning)[1]; + + // try to get ssk private key + const [oldPubkey, oldSskSigning] + = await getPrivateKey(this, "self_signing", (pubkey) => { + // make sure it agrees with the pubkey that we have + if (pubkey !== keys.selfSigning.replaces) { + return "Key does not match"; + } + return; + }); + if (oldSskSigning) { + pkSign(keys.selfSigning, oldSskSigning, this.userId, oldPubkey); + } + } + } + // fall through + case CrossSigningLevel.USER_SIGNING: { + if (!sskSigning) { + // if we didn't generate a new SSK above, then we need to ask + // the client to provide the private key so that we can sign + // the new USK + [sskPub, sskSigning] = await getPrivateKey(this, "self_signing", (pubkey) => { + // make sure it agrees with the pubkey that we have + if (pubkey !== getPublicKey(this.keys.selfSigning)[1]) { + return "Key does not match"; + } + return; + }); + } + const uskSigning = new global.Olm.PkSigning(); + privateKeys.userSigning = uskSigning.generate_seed(); + const uskPub = uskSigning.init_with_seed(privateKeys.userSigning); + keys.userSigning = { + user_id: this.userId, + usage: ['user_signing'], + keys: { + ['ed25519:' + uskPub]: uskPub, + }, + }; + pkSign(keys.userSigning, sskSigning, this.userId, sskPub); + break; + } + default: + // FIXME: + } + Object.assign(this.keys, keys); + this.emit("cross-signing:savePrivateKeys", privateKeys); + } + + setKeys(keys) { + const signingKeys = {}; + if (keys.selfSigning) { + if (this.keys.selfSigning) { + const [oldKeyId, oldKey] = getPublicKey(this.keys.selfSigning); + // check if ssk is signed by previous key + // if the signature checks out, then keep the same First-Use status + // otherwise First-Use is false + if (keys.selfSigning.signatures + && keys.selfSigning.signatures[this.userId] + && keys.selfSigning.signatures[this.userId][oldKeyId]) { + try { + pkVerify(keys.selfSigning, oldKey, this.userId); + } catch (e) { + this.fu = false; + } + } else { + this.fu = false; + } + } else { + // this is the first key that we're setting, so First-Use is true + this.fu = true; + } + signingKeys.selfSigning = keys.selfSigning; + } else { + signingKeys.selfSigning = this.keys.selfSigning; + } + // FIXME: if self-signing key is set, then a new user-signing key must + // be set as well + if (keys.userSigning) { + const usk = getPublicKey(signingKeys.selfSigning)[1]; + try { + pkVerify(keys.userSigning, usk, this.userId); + } catch (e) { + // FIXME: what do we want to do here? + throw e; + } + } + + // if everything checks out, then save the keys + if (keys.selfSigning) { + this.keys.selfSigning = keys.selfSigning; + } + if (keys.userSigning) { + this.keys.userSigning = keys.userSigning; + } + } + + async signUser(key) { + const [pubkey, usk] = await getPrivateKey(this, "user_signing", (key) => { + return; + }); + const otherSsk = key.keys.selfSigning; + pkSign(otherSsk, usk, this.userId, pubkey); + return otherSsk; + } + + async signDevice(userId, device) { + if (userId !== this.userId) { + throw new Error("Urgh!"); + } + const [pubkey, ssk] = await getPrivateKey(this, "self_signing", (key) => { + return; + }); + const keyObj = { + algorithms: device.algorithms, + keys: device.keys, + device_id: device.deviceId, + user_id: userId, + }; + pkSign(keyObj, ssk, this.userId, pubkey); + return keyObj; + } + + checkUserTrust(userCrossSigning) { + let userTrusted; + const userSSK = userCrossSigning.keys.selfSigning; + const uskId = getPublicKey(this.keys.userSigning)[1]; + try { + pkVerify(userSSK, uskId, this.userId); + userTrusted = true; + } catch (e) { + userTrusted = false; + } + return (userTrusted ? CrossSigningVerification.VERIFIED + : CrossSigningVerification.UNVERIFIED) + | (userCrossSigning.fu ? CrossSigningVerification.TOFU + : CrossSigningVerification.UNVERIFIED); + } + + checkDeviceTrust(userCrossSigning, device) { + const userTrust = this.checkUserTrust(userCrossSigning); + + const deviceObj = deviceToObject(device, userCrossSigning.userId); + try { + pkVerify(deviceObj, userCrossSigning.getId(), userCrossSigning.userId); + return userTrust; + } catch (e) { + return 0; + } + } +} + +function deviceToObject(device, userId) { + return { + algorithms: device.algorithms, + keys: device.keys, + device_id: device.deviceId, + user_id: userId, + signatures: device.signatures, + }; +} + +export const CrossSigningLevel = { + SELF_SIGNING: 1, + USER_SIGNING: 2, +}; + +export const CrossSigningVerification = { + UNVERIFIED: 0, + TOFU: 1, + VERIFIED: 2, +}; From b0275afac2331844259f3dbe80575f6876c2a62b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 3 May 2019 23:22:51 -0400 Subject: [PATCH 15/97] remove some debugging lines --- spec/unit/crypto/cross-signing.spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 16174652e3f..e1cdcb546cc 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -69,7 +69,6 @@ describe("Cross Signing", function() { }); // Alice verifies Bob's key alice.on("cross-signing:getKey", function(e) { - console.log(e); expect(e.type).toBe("user_signing"); e.done(privateKeys.userSigning); }); @@ -102,10 +101,8 @@ describe("Cross Signing", function() { // set Alices' cross-signing key let privateKeys; alice.on("cross-signing:savePrivateKeys", function(e) { - console.log("save private keys"); privateKeys = e; }); - console.log("reset cross signing keys"); alice.resetCrossSigningKeys(); // Alice downloads Bob's ssk and device key const bobSigning = new global.Olm.PkSigning(); From 405451d78374a1f26a4b81f7e09774a68cb3610e Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 3 May 2019 23:23:08 -0400 Subject: [PATCH 16/97] complete some more unit tests --- spec/unit/crypto/cross-signing.spec.js | 142 ++++++++++++++++++++++--- 1 file changed, 130 insertions(+), 12 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index e1cdcb546cc..d6af2a176a6 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -160,30 +160,148 @@ describe("Cross Signing", function() { }); it("should dis-trust an unsigned device", async function() { - // Alice downloads Bob's device key (without signature) + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + // set Alices' cross-signing key + let privateKeys; + alice.on("cross-signing:savePrivateKeys", function(e) { + privateKeys = e; + }); + alice.resetCrossSigningKeys(); + // Alice downloads Bob's ssk and device key + // (NOTE: device key is not signed by ssk) + const bobSigning = new global.Olm.PkSigning(); + const bobPrivkey = bobSigning.generate_seed(); + const bobPubkey = bobSigning.init_with_seed(bobPrivkey); + alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + keys: { + selfSigning: { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey]: bobPubkey, + }, + }, + }, + fu: 1, + unsigned: {}, + }); + const bobDevice = { + user_id: "@bob:example.com", + device_id: "Dynabook", + algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + keys: { + "curve25519:Dynabook": "somePubkey", + "ed25519:Dynabook": "someOtherPubkey", + }, + }; + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: bobDevice, + }); // Bob's device key should be untrusted + expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); + // Alice verifies Bob's SSK + alice.on("cross-signing:getKey", function(e) { + expect(e.type).toBe("user_signing"); + e.done(privateKeys.userSigning); + }); + alice.uploadKeySignatures = () => {}; + await alice.setDeviceVerified("@bob:example.com", bobPubkey, true); + // Bob's device key should be untrusted + expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); }); it("should dis-trust a user when their ssk changes", async function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); + let privateKeys; + alice.on("cross-signing:savePrivateKeys", function(e) { + privateKeys = e; + }); + alice.resetCrossSigningKeys(); // Alice downloads Bob's keys - alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { - Dynabook: { - algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], - keys: { - "curve25519:Dynabook": "blablabla", - "ed25519:Dynabook": "blablabla", + const bobSigning = new global.Olm.PkSigning(); + const bobPrivkey = bobSigning.generate_seed(); + const bobPubkey = bobSigning.init_with_seed(bobPrivkey); + alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + keys: { + selfSigning: { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey]: bobPubkey, + }, }, - verified: 0, - known: false, - unsigned: {}, }, + fu: 1, + unsigned: {}, + }); + const bobDevice = { + user_id: "@bob:example.com", + device_id: "Dynabook", + algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + keys: { + "curve25519:Dynabook": "somePubkey", + "ed25519:Dynabook": "someOtherPubkey", + }, + }; + const bobDeviceString = anotherjson.stringify(bobDevice); + const sig = bobSigning.sign(bobDeviceString); + bobDevice.signatures = {}; + bobDevice.signatures["@bob:example.com"] = {}; + bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig; + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: bobDevice, }); // Alice verifies Bob's SSK - // Bob's devices should be trusted + alice.on("cross-signing:getKey", function(e) { + expect(e.type).toBe("user_signing"); + e.done(privateKeys.userSigning); + }); + alice.uploadKeySignatures = () => {}; + await alice.setDeviceVerified("@bob:example.com", bobPubkey, true); + // Bob's device key should be trusted + expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(6); // Alice downloads new SSK for Bob - // Bob's devices should be untrusted + const bobSigning2 = new global.Olm.PkSigning(); + const bobPrivkey2 = bobSigning2.generate_seed(); + const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2); + alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + keys: { + selfSigning: { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey2]: bobPubkey2, + }, + }, + }, + fu: 0, + unsigned: {}, + }); + // Bob's and his device should be untrusted + expect(alice.checkUserTrust("@bob:example.com")).toBe(0); + expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); + // Alice verifies Bob's SSK + alice.on("cross-signing:getKey", function(e) { + expect(e.type).toBe("user_signing"); + e.done(privateKeys.userSigning); + }); + alice.uploadKeySignatures = () => {}; + await alice.setDeviceVerified("@bob:example.com", bobPubkey2, true); + // Bob should be trusted but not his device + expect(alice.checkUserTrust("@bob:example.com")).toBe(4); + expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); + // Alice gets new signature for device + const sig2 = bobSigning2.sign(bobDeviceString); + bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2; + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: bobDevice, + }); + // Bob's device should be trusted again (but not TOFU) + expect(alice.checkUserTrust("@bob:example.com")).toBe(4); + expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(4); }); }); From 193ad9e09d5cf292aa5cfc4f652d02914481f948 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 May 2019 18:18:21 -0400 Subject: [PATCH 17/97] use 3 keys for cross-signing --- spec/unit/crypto/cross-signing.spec.js | 126 ++++++++++++++----- src/crypto/CrossSigning.js | 167 ++++++++++++------------- 2 files changed, 178 insertions(+), 115 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index d6af2a176a6..e6788e42df1 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -56,11 +56,11 @@ describe("Cross Signing", function() { // Alice downloads Bob's device key alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { - selfSigning: { + master: { user_id: "@bob:example.com", - usage: ["self_signing"], + usage: ["master"], keys: { - "ed25519:bobs+ssk+pubkey": "bobs+ssk+pubkey", + "ed25519:bobs+master+pubkey": "bobs+master+pubkey", }, }, }, @@ -70,14 +70,14 @@ describe("Cross Signing", function() { // Alice verifies Bob's key alice.on("cross-signing:getKey", function(e) { expect(e.type).toBe("user_signing"); - e.done(privateKeys.userSigning); + e.done(privateKeys.user_signing); }); const promise = new Promise((resolve, reject) => { alice.uploadKeySignatures = (...args) => { resolve(...args); }; }); - alice.setDeviceVerified("@bob:example.com", "bobs+ssk+pubkey", true); + await alice.setDeviceVerified("@bob:example.com", "bobs+master+pubkey", true); // Alice should send a signature of Bob's key to the server await promise; }); @@ -105,18 +105,35 @@ describe("Cross Signing", function() { }); alice.resetCrossSigningKeys(); // Alice downloads Bob's ssk and device key + const bobMasterSigning = new global.Olm.PkSigning(); + const bobMasterPrivkey = bobMasterSigning.generate_seed(); + const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey); const bobSigning = new global.Olm.PkSigning(); const bobPrivkey = bobSigning.generate_seed(); const bobPubkey = bobSigning.init_with_seed(bobPrivkey); + const bobSSK = { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey]: bobPubkey, + }, + }; + const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK)); + bobSSK.signatures = { + "@bob:example.com": { + ["ed25519:" + bobMasterPubkey]: sskSig, + }, + }; alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { - selfSigning: { + master: { user_id: "@bob:example.com", - usage: ["self_signing"], + usage: ["master"], keys: { - ["ed25519:" + bobPubkey]: bobPubkey, + ["ed25519:" + bobMasterPubkey]: bobMasterPubkey, }, }, + self_signing: bobSSK, }, fu: 1, unsigned: {}, @@ -131,9 +148,11 @@ describe("Cross Signing", function() { }, }; const sig = bobSigning.sign(anotherjson.stringify(bobDevice)); - bobDevice.signatures = {}; - bobDevice.signatures["@bob:example.com"] = {}; - bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig; + bobDevice.signatures = { + "@bob:example.com": { + ["ed25519:" + bobPubkey]: sig, + }, + }; alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); @@ -143,10 +162,10 @@ describe("Cross Signing", function() { // Alice verifies Bob's SSK alice.on("cross-signing:getKey", function(e) { expect(e.type).toBe("user_signing"); - e.done(privateKeys.userSigning); + e.done(privateKeys.user_signing); }); alice.uploadKeySignatures = () => {}; - await alice.setDeviceVerified("@bob:example.com", bobPubkey, true); + await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); // Bob's device key should be trusted expect(alice.checkUserTrust("@bob:example.com")).toBe(6); expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(6); @@ -171,18 +190,35 @@ describe("Cross Signing", function() { alice.resetCrossSigningKeys(); // Alice downloads Bob's ssk and device key // (NOTE: device key is not signed by ssk) + const bobMasterSigning = new global.Olm.PkSigning(); + const bobMasterPrivkey = bobMasterSigning.generate_seed(); + const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey); const bobSigning = new global.Olm.PkSigning(); const bobPrivkey = bobSigning.generate_seed(); const bobPubkey = bobSigning.init_with_seed(bobPrivkey); + const bobSSK = { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey]: bobPubkey, + }, + }; + const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK)); + bobSSK.signatures = { + "@bob:example.com": { + ["ed25519:" + bobMasterPubkey]: sskSig, + }, + }; alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { - selfSigning: { + master: { user_id: "@bob:example.com", - usage: ["self_signing"], + usage: ["master"], keys: { - ["ed25519:" + bobPubkey]: bobPubkey, + ["ed25519:" + bobMasterPubkey]: bobMasterPubkey, }, }, + self_signing: bobSSK, }, fu: 1, unsigned: {}, @@ -204,10 +240,10 @@ describe("Cross Signing", function() { // Alice verifies Bob's SSK alice.on("cross-signing:getKey", function(e) { expect(e.type).toBe("user_signing"); - e.done(privateKeys.userSigning); + e.done(privateKeys.user_signing); }); alice.uploadKeySignatures = () => {}; - await alice.setDeviceVerified("@bob:example.com", bobPubkey, true); + await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); // Bob's device key should be untrusted expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); }); @@ -222,18 +258,35 @@ describe("Cross Signing", function() { }); alice.resetCrossSigningKeys(); // Alice downloads Bob's keys + const bobMasterSigning = new global.Olm.PkSigning(); + const bobMasterPrivkey = bobMasterSigning.generate_seed(); + const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey); const bobSigning = new global.Olm.PkSigning(); const bobPrivkey = bobSigning.generate_seed(); const bobPubkey = bobSigning.init_with_seed(bobPrivkey); + const bobSSK = { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey]: bobPubkey, + }, + }; + const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK)); + bobSSK.signatures = { + "@bob:example.com": { + ["ed25519:" + bobMasterPubkey]: sskSig, + }, + }; alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { - selfSigning: { + master: { user_id: "@bob:example.com", - usage: ["self_signing"], + usage: ["master"], keys: { - ["ed25519:" + bobPubkey]: bobPubkey, + ["ed25519:" + bobMasterPubkey]: bobMasterPubkey, }, }, + self_signing: bobSSK, }, fu: 1, unsigned: {}, @@ -258,25 +311,42 @@ describe("Cross Signing", function() { // Alice verifies Bob's SSK alice.on("cross-signing:getKey", function(e) { expect(e.type).toBe("user_signing"); - e.done(privateKeys.userSigning); + e.done(privateKeys.user_signing); }); alice.uploadKeySignatures = () => {}; - await alice.setDeviceVerified("@bob:example.com", bobPubkey, true); + await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); // Bob's device key should be trusted expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(6); // Alice downloads new SSK for Bob + const bobMasterSigning2 = new global.Olm.PkSigning(); + const bobMasterPrivkey2 = bobMasterSigning2.generate_seed(); + const bobMasterPubkey2 = bobMasterSigning2.init_with_seed(bobMasterPrivkey2); const bobSigning2 = new global.Olm.PkSigning(); const bobPrivkey2 = bobSigning2.generate_seed(); const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2); + const bobSSK2 = { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey2]: bobPubkey2, + }, + }; + const sskSig2 = bobMasterSigning2.sign(anotherjson.stringify(bobSSK2)); + bobSSK2.signatures = { + "@bob:example.com": { + ["ed25519:" + bobMasterPubkey2]: sskSig2, + }, + }; alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { - selfSigning: { + master: { user_id: "@bob:example.com", - usage: ["self_signing"], + usage: ["master"], keys: { - ["ed25519:" + bobPubkey2]: bobPubkey2, + ["ed25519:" + bobMasterPubkey2]: bobMasterPubkey2, }, }, + self_signing: bobSSK2, }, fu: 0, unsigned: {}, @@ -287,10 +357,10 @@ describe("Cross Signing", function() { // Alice verifies Bob's SSK alice.on("cross-signing:getKey", function(e) { expect(e.type).toBe("user_signing"); - e.done(privateKeys.userSigning); + e.done(privateKeys.user_signing); }); alice.uploadKeySignatures = () => {}; - await alice.setDeviceVerified("@bob:example.com", bobPubkey2, true); + await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey2, true); // Bob should be trusted but not his device expect(alice.checkUserTrust("@bob:example.com")).toBe(4); expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 355a999d831..7e7d1312e7a 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -94,114 +94,101 @@ export class CrossSigningInfo extends EventEmitter { * @return {string} the ID */ getId() { - return getPublicKey(this.keys.selfSigning)[1]; + return getPublicKey(this.keys.master)[1]; } async resetKeys(level) { - if (level === undefined) { - level = CrossSigningLevel.SELF_SIGNING; + if (level === undefined || level & 4) { + level = CrossSigningLevel.MASTER; + } else if (level === 0) { + return; } const privateKeys = {}; const keys = {}; - let sskSigning; - let sskPub; - switch (level) { - case CrossSigningLevel.SELF_SIGNING: { - sskSigning = new global.Olm.PkSigning(); - privateKeys.selfSigning = sskSigning.generate_seed(); - sskPub = sskSigning.init_with_seed(privateKeys.selfSigning); - keys.selfSigning = { + let masterSigning; + let masterPub; + + if (level & 4) { + masterSigning = new global.Olm.PkSigning(); + privateKeys.master = masterSigning.generate_seed(); + masterPub = masterSigning.init_with_seed(privateKeys.master); + keys.master = { + user_id: this.userId, + usage: ['master'], + keys: { + ['ed25519:' + masterPub]: masterPub, + }, + }; + } else { + [masterPub, masterSigning] = await getPrivateKey(this, "master", (pubkey) => { + // make sure it agrees with the pubkey that we have + if (pubkey !== getPublicKey(this.keys.master)[1]) { + return "Key does not match"; + } + return; + }); + } + + if (level & CrossSigningLevel.SELF_SIGNING) { + const sskSigning = new global.Olm.PkSigning(); + privateKeys.self_signing = sskSigning.generate_seed(); + const sskPub = sskSigning.init_with_seed(privateKeys.self_signing); + keys.self_signing = { user_id: this.userId, usage: ['self_signing'], keys: { ['ed25519:' + sskPub]: sskPub, }, }; - if (this.keys.selfSigning) { - keys.selfSigning.replaces = getPublicKey(this.keys.selfSigning)[1]; - - // try to get ssk private key - const [oldPubkey, oldSskSigning] - = await getPrivateKey(this, "self_signing", (pubkey) => { - // make sure it agrees with the pubkey that we have - if (pubkey !== keys.selfSigning.replaces) { - return "Key does not match"; - } - return; - }); - if (oldSskSigning) { - pkSign(keys.selfSigning, oldSskSigning, this.userId, oldPubkey); - } - } + pkSign(keys.self_signing, masterSigning, this.userId, masterPub); } - // fall through - case CrossSigningLevel.USER_SIGNING: { - if (!sskSigning) { - // if we didn't generate a new SSK above, then we need to ask - // the client to provide the private key so that we can sign - // the new USK - [sskPub, sskSigning] = await getPrivateKey(this, "self_signing", (pubkey) => { - // make sure it agrees with the pubkey that we have - if (pubkey !== getPublicKey(this.keys.selfSigning)[1]) { - return "Key does not match"; - } - return; - }); - } + + if (level & CrossSigningLevel.USER_SIGNING) { const uskSigning = new global.Olm.PkSigning(); - privateKeys.userSigning = uskSigning.generate_seed(); - const uskPub = uskSigning.init_with_seed(privateKeys.userSigning); - keys.userSigning = { + privateKeys.user_signing = uskSigning.generate_seed(); + const uskPub = uskSigning.init_with_seed(privateKeys.user_signing); + keys.user_signing = { user_id: this.userId, usage: ['user_signing'], keys: { ['ed25519:' + uskPub]: uskPub, }, }; - pkSign(keys.userSigning, sskSigning, this.userId, sskPub); - break; - } - default: - // FIXME: + pkSign(keys.user_signing, masterSigning, this.userId, masterPub); } + Object.assign(this.keys, keys); this.emit("cross-signing:savePrivateKeys", privateKeys); } setKeys(keys) { const signingKeys = {}; - if (keys.selfSigning) { - if (this.keys.selfSigning) { - const [oldKeyId, oldKey] = getPublicKey(this.keys.selfSigning); - // check if ssk is signed by previous key - // if the signature checks out, then keep the same First-Use status - // otherwise First-Use is false - if (keys.selfSigning.signatures - && keys.selfSigning.signatures[this.userId] - && keys.selfSigning.signatures[this.userId][oldKeyId]) { - try { - pkVerify(keys.selfSigning, oldKey, this.userId); - } catch (e) { - this.fu = false; - } - } else { - this.fu = false; - } - } else { - // this is the first key that we're setting, so First-Use is true - this.fu = true; + if (keys.master) { + // First-Use is true if and only if we had no previous key for the user + this.fu = !(this.keys.self_signing); + signingKeys.master = keys.master; + if (!keys.user_signing || !keys.self_signing) { + throw new Error("Must have new self-signing and user-signing" + + "keys when new master key is set"); } - signingKeys.selfSigning = keys.selfSigning; } else { - signingKeys.selfSigning = this.keys.selfSigning; + signingKeys.master = this.keys.master; + } + const masterKey = getPublicKey(signingKeys.master)[1]; + + // verify signatures + if (keys.user_signing) { + try { + pkVerify(keys.user_signing, masterKey, this.userId); + } catch (e) { + // FIXME: what do we want to do here? + throw e; + } } - // FIXME: if self-signing key is set, then a new user-signing key must - // be set as well - if (keys.userSigning) { - const usk = getPublicKey(signingKeys.selfSigning)[1]; + if (keys.self_signing) { try { - pkVerify(keys.userSigning, usk, this.userId); + pkVerify(keys.self_signing, masterKey, this.userId); } catch (e) { // FIXME: what do we want to do here? throw e; @@ -209,11 +196,14 @@ export class CrossSigningInfo extends EventEmitter { } // if everything checks out, then save the keys - if (keys.selfSigning) { - this.keys.selfSigning = keys.selfSigning; + if (keys.master) { + this.keys.master = keys.master; + } + if (keys.self_signing) { + this.keys.self_signing = keys.self_signing; } - if (keys.userSigning) { - this.keys.userSigning = keys.userSigning; + if (keys.user_signing) { + this.keys.user_signing = keys.user_signing; } } @@ -221,9 +211,9 @@ export class CrossSigningInfo extends EventEmitter { const [pubkey, usk] = await getPrivateKey(this, "user_signing", (key) => { return; }); - const otherSsk = key.keys.selfSigning; - pkSign(otherSsk, usk, this.userId, pubkey); - return otherSsk; + const otherMaster = key.keys.master; + pkSign(otherMaster, usk, this.userId, pubkey); + return otherMaster; } async signDevice(userId, device) { @@ -245,10 +235,10 @@ export class CrossSigningInfo extends EventEmitter { checkUserTrust(userCrossSigning) { let userTrusted; - const userSSK = userCrossSigning.keys.selfSigning; - const uskId = getPublicKey(this.keys.userSigning)[1]; + const userMaster = userCrossSigning.keys.master; + const uskId = getPublicKey(this.keys.user_signing)[1]; try { - pkVerify(userSSK, uskId, this.userId); + pkVerify(userMaster, uskId, this.userId); userTrusted = true; } catch (e) { userTrusted = false; @@ -262,9 +252,11 @@ export class CrossSigningInfo extends EventEmitter { checkDeviceTrust(userCrossSigning, device) { const userTrust = this.checkUserTrust(userCrossSigning); + const userSSK = userCrossSigning.keys.self_signing; const deviceObj = deviceToObject(device, userCrossSigning.userId); try { - pkVerify(deviceObj, userCrossSigning.getId(), userCrossSigning.userId); + pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); + pkVerify(deviceObj, getPublicKey(userSSK)[1], userCrossSigning.userId); return userTrust; } catch (e) { return 0; @@ -283,6 +275,7 @@ function deviceToObject(device, userId) { } export const CrossSigningLevel = { + MASTER: 7, SELF_SIGNING: 1, USER_SIGNING: 2, }; From 53804cac5cff7f4d5848f46b3b71809284accd40 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 28 May 2019 22:28:54 -0400 Subject: [PATCH 18/97] save cross-signing keys from sync and verify new keys for user --- spec/test-utils.js | 131 +++++++++++++++ spec/unit/crypto/cross-signing.spec.js | 123 +++++++++++++- src/crypto/CrossSigning.js | 224 ++++++++++++++++--------- src/crypto/DeviceList.js | 90 ++++------ src/crypto/deviceinfo.js | 2 + src/crypto/index.js | 97 +++++++++-- 6 files changed, 507 insertions(+), 160 deletions(-) diff --git a/spec/test-utils.js b/spec/test-utils.js index d0b673568a9..780de5ac809 100644 --- a/spec/test-utils.js +++ b/spec/test-utils.js @@ -1,6 +1,7 @@ "use strict"; import expect from 'expect'; import Promise from 'bluebird'; +const logger = require("../logger"); // load olm before the sdk if possible import './olm-loader'; @@ -241,3 +242,133 @@ module.exports.awaitDecryption = function(event) { }); }); }; + + +const HttpResponse = module.exports.HttpResponse = function( + httpLookups, acceptKeepalives, +) { + this.httpLookups = httpLookups; + this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives; + this.pendingLookup = null; +}; + +HttpResponse.prototype.request = function HttpResponse( + cb, method, path, qp, data, prefix, +) { + if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) { + return Promise.resolve(); + } + const next = this.httpLookups.shift(); + const logLine = ( + "MatrixClient[UT] RECV " + method + " " + path + " " + + "EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next) + ); + logger.log(logLine); + + if (!next) { // no more things to return + if (this.pendingLookup) { + if (this.pendingLookup.method === method + && this.pendingLookup.path === path) { + return this.pendingLookup.promise; + } + // >1 pending thing, and they are different, whine. + expect(false).toBe( + true, ">1 pending request. You should probably handle them. " + + "PENDING: " + JSON.stringify(this.pendingLookup) + " JUST GOT: " + + method + " " + path, + ); + } + this.pendingLookup = { + promise: Promise.defer().promise, + method: method, + path: path, + }; + return this.pendingLookup.promise; + } + if (next.path === path && next.method === method) { + logger.log( + "MatrixClient[UT] Matched. Returning " + + (next.error ? "BAD" : "GOOD") + " response", + ); + if (next.expectBody) { + expect(next.expectBody).toEqual(data); + } + if (next.expectQueryParams) { + Object.keys(next.expectQueryParams).forEach(function(k) { + expect(qp[k]).toEqual(next.expectQueryParams[k]); + }); + } + + if (next.thenCall) { + process.nextTick(next.thenCall, 0); // next tick so we return first. + } + + if (next.error) { + return Promise.reject({ + errcode: next.error.errcode, + httpStatus: next.error.httpStatus, + name: next.error.errcode, + message: "Expected testing error", + data: next.error, + }); + } + return Promise.resolve(next.data); + } + expect(true).toBe(false, "Expected different request. " + logLine); + return Promise.defer().promise; +}; + +HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions"; + +HttpResponse.PUSH_RULES_RESPONSE = { + method: "GET", + path: "/pushrules/", + data: {}, +}; + +HttpResponse.USER_ID = "@alice:bar"; + +HttpResponse.filterResponse = function(userId) { + const filterPath = "/user/" + encodeURIComponent(userId) + "/filter"; + return { + method: "POST", + path: filterPath, + data: { filter_id: "f1lt3r" }, + }; +}; + +HttpResponse.SYNC_DATA = { + next_batch: "s_5_3", + presence: { events: [] }, + rooms: {}, +}; + +HttpResponse.SYNC_RESPONSE = { + method: "GET", + path: "/sync", + data: HttpResponse.SYNC_DATA, +}; + +HttpResponse.defaultResponses = function(userId) { + return [ + HttpResponse.PUSH_RULES_RESPONSE, + HttpResponse.filterResponse(userId), + HttpResponse.SYNC_RESPONSE, + ]; +}; + +module.exports.setHttpResponses = function setHttpResponses( + client, responses, acceptKeepalives, +) { + const httpResponseObj = new HttpResponse(responses, acceptKeepalives); + + const httpReq = httpResponseObj.request.bind(httpResponseObj); + client._http = [ + "authedRequest", "authedRequestWithPrefix", "getContentUri", + "request", "requestWithPrefix", "uploadContent", + ].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {}); + client._http.authedRequest.andCall(httpReq); + client._http.authedRequestWithPrefix.andCall(httpReq); + client._http.requestWithPrefix.andCall(httpReq); + client._http.request.andCall(httpReq); +}; diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index e6788e42df1..5929207130d 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 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. @@ -23,6 +24,8 @@ import olmlib from '../../../lib/crypto/olmlib'; import TestClient from '../../TestClient'; +import {HttpResponse, setHttpResponses} from '../../test-utils'; + async function makeTestClient(userInfo, options) { const client = (new TestClient( userInfo.userId, userInfo.deviceId, undefined, undefined, options, @@ -86,12 +89,126 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); - alice.on("cross-signing:newKey", function(e) { - // FIXME: ??? + + const masterKey = new Uint8Array([ + 0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, + 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, + 0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6, + 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d, + ]); + const selfSigningKey = new Uint8Array([ + 0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, + 0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, + 0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49, + 0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f, + ]); + + const keyChangePromise = new Promise((resolve, reject) => { + alice.once("cross-signing:keysChanged", (e) => { + resolve(e); + }); }); + + alice.once("cross-signing:newKey", (e) => { + e.done(masterKey); + }); + + const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"] + .Osborne2; + const aliceDevice = { + user_id: "@alice:example.com", + device_id: "Osborne2", + }; + aliceDevice.keys = deviceInfo.keys; + aliceDevice.algorithms = deviceInfo.algorithms; + await alice._crypto._signObject(aliceDevice); + olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com"); + // feed sync result that includes ssk, usk, device key - // client should emit event asking about ssk + const responses = [ + HttpResponse.PUSH_RULES_RESPONSE, + { + method: "POST", + path: "/keys/upload/Osborne2", + data: { + one_time_key_counts: { + curve25519: 100, + signed_curve25519: 100, + }, + }, + }, + HttpResponse.filterResponse("@alice:example.com"), + { + method: "GET", + path: "/sync", + data: { + next_batch: "abcdefg", + device_lists: { + changed: [ + "@alice:example.com", + "@bob:example.com", + ], + }, + }, + }, + { + method: "POST", + path: "/keys/query", + data: { + "failures": {}, + "device_keys": { + "@alice:example.com": { + "Osborne2": aliceDevice, + }, + }, + "master_keys": { + "@alice:example.com": { + user_id: "@alice:example.com", + usage: ["master"], + keys: { + "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": + "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", + }, + }, + }, + "self_signing_keys": { + "@alice:example.com": { + user_id: "@alice:example.com", + usage: ["self-signing"], + keys: { + "ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ": + "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", + }, + signatures: { + "@alice:example.com": { + "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": + "Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs" + + "Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA", + }, + }, + }, + }, + }, + }, + { + method: "POST", + path: "/keys/upload/Osborne2", + data: { + one_time_key_counts: { + curve25519: 100, + signed_curve25519: 100, + }, + }, + }, + ]; + setHttpResponses(alice, responses); + + await alice.startClient(); + // once ssk is confirmed, device key should be trusted + await keyChangePromise; + expect(alice.checkUserTrust("@alice:example.com")).toBe(6); + expect(alice.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(7); }); it("should use trust chain to determine device verification", async function() { diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 7e7d1312e7a..197d371fd0a 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -21,14 +21,18 @@ limitations under the License. import {pkSign, pkVerify} from './olmlib'; import {EventEmitter} from 'events'; +import logger from '../logger'; function getPublicKey(keyInfo) { return Object.entries(keyInfo.keys)[0]; } -function getPrivateKey(self, type, check) { - return new Promise((resolve, reject) => { - const askForKey = (error) => { +async function getPrivateKey(self, type, check) { + let error; + let pubkey; + let signing; + do { + [pubkey, signing] = await new Promise((resolve, reject) => { self.emit("cross-signing:getKey", { type: type, error, @@ -36,9 +40,11 @@ function getPrivateKey(self, type, check) { // FIXME: the key needs to be interpreted? const signing = new global.Olm.PkSigning(); const pubkey = signing.init_with_seed(key); - const error = check(pubkey, signing); + error = check(pubkey, signing); if (error) { - return askForKey(error); + logger.error(error); + signing.free(); + resolve([null, null]); } resolve([pubkey, signing]); }, @@ -46,9 +52,9 @@ function getPrivateKey(self, type, check) { reject(error || new Error("Cancelled")); }, }); - }; - askForKey(); - }); + }); + } while (!pubkey); + return [pubkey, signing]; } export class CrossSigningInfo extends EventEmitter { @@ -64,12 +70,11 @@ export class CrossSigningInfo extends EventEmitter { // you can't change the userId Object.defineProperty(this, 'userId', { - enumerabel: true, + enumerable: true, value: userId, }); this.keys = {}; this.fu = true; - // FIXME: add chain of ssks? } static fromStorage(obj, userId) { @@ -85,20 +90,24 @@ export class CrossSigningInfo extends EventEmitter { toStorage() { return { keys: this.keys, - verified: this.verified, + fu: this.fu, }; } /** Get the ID used to identify the user + * + * @param {string} type The type of key to get the ID of. One of "master", + * "self_signing", or "user_signing". Defaults to "master". * * @return {string} the ID */ - getId() { - return getPublicKey(this.keys.master)[1]; + getId(type) { + type = type || "master"; + return this.keys[type] && getPublicKey(this.keys[type])[1]; } async resetKeys(level) { - if (level === undefined || level & 4) { + if (level === undefined || level & 4 || !this.keys.master) { level = CrossSigningLevel.MASTER; } else if (level === 0) { return; @@ -109,87 +118,120 @@ export class CrossSigningInfo extends EventEmitter { let masterSigning; let masterPub; - if (level & 4) { - masterSigning = new global.Olm.PkSigning(); - privateKeys.master = masterSigning.generate_seed(); - masterPub = masterSigning.init_with_seed(privateKeys.master); - keys.master = { - user_id: this.userId, - usage: ['master'], - keys: { - ['ed25519:' + masterPub]: masterPub, - }, - }; - } else { - [masterPub, masterSigning] = await getPrivateKey(this, "master", (pubkey) => { - // make sure it agrees with the pubkey that we have - if (pubkey !== getPublicKey(this.keys.master)[1]) { - return "Key does not match"; + try { + if (level & 4) { + masterSigning = new global.Olm.PkSigning(); + privateKeys.master = masterSigning.generate_seed(); + masterPub = masterSigning.init_with_seed(privateKeys.master); + keys.master = { + user_id: this.userId, + usage: ['master'], + keys: { + ['ed25519:' + masterPub]: masterPub, + }, + }; + } else { + [masterPub, masterSigning] = await getPrivateKey( + this, "master", (pubkey) => { + // make sure it agrees with the pubkey that we have + if (pubkey !== getPublicKey(this.keys.master)[1]) { + return "Key does not match"; + } + return; + }); + } + + if (level & CrossSigningLevel.SELF_SIGNING) { + const sskSigning = new global.Olm.PkSigning(); + try { + privateKeys.self_signing = sskSigning.generate_seed(); + const sskPub = sskSigning.init_with_seed(privateKeys.self_signing); + keys.self_signing = { + user_id: this.userId, + usage: ['self_signing'], + keys: { + ['ed25519:' + sskPub]: sskPub, + }, + }; + pkSign(keys.self_signing, masterSigning, this.userId, masterPub); + } finally { + sskSigning.free(); } - return; - }); - } + } - if (level & CrossSigningLevel.SELF_SIGNING) { - const sskSigning = new global.Olm.PkSigning(); - privateKeys.self_signing = sskSigning.generate_seed(); - const sskPub = sskSigning.init_with_seed(privateKeys.self_signing); - keys.self_signing = { - user_id: this.userId, - usage: ['self_signing'], - keys: { - ['ed25519:' + sskPub]: sskPub, - }, - }; - pkSign(keys.self_signing, masterSigning, this.userId, masterPub); - } + if (level & CrossSigningLevel.USER_SIGNING) { + const uskSigning = new global.Olm.PkSigning(); + try { + privateKeys.user_signing = uskSigning.generate_seed(); + const uskPub = uskSigning.init_with_seed(privateKeys.user_signing); + keys.user_signing = { + user_id: this.userId, + usage: ['user_signing'], + keys: { + ['ed25519:' + uskPub]: uskPub, + }, + }; + pkSign(keys.user_signing, masterSigning, this.userId, masterPub); + } finally { + uskSigning.free(); + } + } - if (level & CrossSigningLevel.USER_SIGNING) { - const uskSigning = new global.Olm.PkSigning(); - privateKeys.user_signing = uskSigning.generate_seed(); - const uskPub = uskSigning.init_with_seed(privateKeys.user_signing); - keys.user_signing = { - user_id: this.userId, - usage: ['user_signing'], - keys: { - ['ed25519:' + uskPub]: uskPub, - }, - }; - pkSign(keys.user_signing, masterSigning, this.userId, masterPub); + Object.assign(this.keys, keys); + this.emit("cross-signing:savePrivateKeys", privateKeys); + } finally { + if (masterSigning) { + masterSigning.free(); + } } - - Object.assign(this.keys, keys); - this.emit("cross-signing:savePrivateKeys", privateKeys); } setKeys(keys) { const signingKeys = {}; if (keys.master) { + if (keys.master.user_id !== this.userId) { + const error = "Mismatched user ID " + keys.master.user_id + + " in master key from " + this.userId; + logger.error(error); + throw new Error(error); + } // First-Use is true if and only if we had no previous key for the user this.fu = !(this.keys.self_signing); signingKeys.master = keys.master; - if (!keys.user_signing || !keys.self_signing) { - throw new Error("Must have new self-signing and user-signing" - + "keys when new master key is set"); - } - } else { + } else if (this.keys.master) { signingKeys.master = this.keys.master; + } else { + throw new Error("Tried to set cross-signing keys without a master key"); } const masterKey = getPublicKey(signingKeys.master)[1]; // verify signatures if (keys.user_signing) { + if (keys.user_signing.user_id !== this.userId) { + const error = "Mismatched user ID " + keys.master.user_id + + " in user_signing key from " + this.userId; + logger.error(error); + throw new Error(error); + } try { pkVerify(keys.user_signing, masterKey, this.userId); } catch (e) { + logger.error("invalid signature on user-signing key"); // FIXME: what do we want to do here? throw e; } } if (keys.self_signing) { + if (keys.self_signing.user_id !== this.userId) { + const error = "Mismatched user ID " + keys.master.user_id + + " in self_signing key from " + this.userId; + logger.error(error); + throw new Error(error); + } try { pkVerify(keys.self_signing, masterKey, this.userId); } catch (e) { + logger.error("invalid signature on self-signing key"); // FIXME: what do we want to do here? throw e; } @@ -198,6 +240,10 @@ export class CrossSigningInfo extends EventEmitter { // if everything checks out, then save the keys if (keys.master) { this.keys.master = keys.master; + // if the master key is set, then the old self-signing and + // user-signing keys are obsolete + delete this.keys.self_signing; + delete this.keys.user_signing; } if (keys.self_signing) { this.keys.self_signing = keys.self_signing; @@ -211,9 +257,13 @@ export class CrossSigningInfo extends EventEmitter { const [pubkey, usk] = await getPrivateKey(this, "user_signing", (key) => { return; }); - const otherMaster = key.keys.master; - pkSign(otherMaster, usk, this.userId, pubkey); - return otherMaster; + try { + const otherMaster = key.keys.master; + pkSign(otherMaster, usk, this.userId, pubkey); + return otherMaster; + } finally { + usk.free(); + } } async signDevice(userId, device) { @@ -223,17 +273,34 @@ export class CrossSigningInfo extends EventEmitter { const [pubkey, ssk] = await getPrivateKey(this, "self_signing", (key) => { return; }); - const keyObj = { - algorithms: device.algorithms, - keys: device.keys, - device_id: device.deviceId, - user_id: userId, - }; - pkSign(keyObj, ssk, this.userId, pubkey); - return keyObj; + try { + const keyObj = { + algorithms: device.algorithms, + keys: device.keys, + device_id: device.deviceId, + user_id: userId, + }; + pkSign(keyObj, ssk, this.userId, pubkey); + return keyObj; + } finally { + ssk.free(); + } } checkUserTrust(userCrossSigning) { + if (this.userId === userCrossSigning.userId + && this.getId() && this.getId() === userCrossSigning.getId() + && this.getId("self_signing") + && this.getId("self_signing") === userCrossSigning.getId("self_signing")) { + return CrossSigningVerification.VERIFIED + | (this.fu ? CrossSigningVerification.TOFU + : CrossSigningVerification.UNVERIFIED); + } + + if (!this.keys.user_signing) { + return 0; + } + let userTrusted; const userMaster = userCrossSigning.keys.master; const uskId = getPublicKey(this.keys.user_signing)[1]; @@ -253,6 +320,9 @@ export class CrossSigningInfo extends EventEmitter { const userTrust = this.checkUserTrust(userCrossSigning); const userSSK = userCrossSigning.keys.self_signing; + if (!userSSK) { + return 0; + } const deviceObj = deviceToObject(device, userCrossSigning.userId); try { pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 0c715cf0779..ec9cb9bcd43 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations Ltd Copyright 2018, 2019 New Vector Ltd +Copyright 2019 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. @@ -27,7 +28,7 @@ import {EventEmitter} from 'events'; import logger from '../logger'; import DeviceInfo from './deviceinfo'; -import {CrossSigningInfo, CrossSigningVerification} from './CrossSigning'; +import {CrossSigningInfo} from './CrossSigning'; import olmlib from './olmlib'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; @@ -754,7 +755,9 @@ class DeviceListUpdateSerialiser { downloadUsers, opts, ).then((res) => { const dk = res.device_keys || {}; + const master_keys = res.master_keys || {}; const ssks = res.self_signing_keys || {}; + const usks = res.user_signing_keys || {}; // do each user in a separate promise, to avoid wedging the CPU // (https://github.com/vector-im/riot-web/issues/3158) @@ -765,7 +768,11 @@ class DeviceListUpdateSerialiser { for (const userId of downloadUsers) { prom = prom.delay(5).then(() => { return this._processQueryResponseForUser( - userId, dk[userId], ssks[userId], + userId, dk[userId], { + master: master_keys[userId], + self_signing: ssks[userId], + user_signing: usks[userId], + }, ); }); } @@ -790,9 +797,11 @@ class DeviceListUpdateSerialiser { return deferred.promise; } - async _processQueryResponseForUser(userId, dkResponse, sskResponse) { + async _processQueryResponseForUser( + userId, dkResponse, crossSigningResponse, sskResponse, + ) { logger.log('got device keys for ' + userId + ':', dkResponse); - logger.log('got self-signing keys for ' + userId + ':', sskResponse); + logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse); { // map from deviceid -> deviceinfo for this user @@ -818,19 +827,23 @@ class DeviceListUpdateSerialiser { this._deviceList._setRawStoredDevicesForUser(userId, storage); } - // now do the same for the self-signing key + // now do the same for the cross-signing keys { - const ssk = this._deviceList.getRawStoredSskForUser(userId) || {}; + if (crossSigningResponse && Object.keys(crossSigningResponse).length) { + const crossSigning + = this._deviceList.getStoredCrossSigningForUser(userId) + || new CrossSigningInfo(userId); - const updated = await _updateStoredSelfSigningKeyForUser( - this._olmDevice, userId, ssk, sskResponse || {}, - ); + crossSigning.setKeys(crossSigningResponse); - this._deviceList.setRawStoredSskForUser(userId, ssk); + this._deviceList.setRawStoredCrossSigningForUser( + userId, crossSigning.toStorage(), + ); - // NB. Unlike most events in the js-sdk, this one is internal to the - // js-sdk and is not re-emitted - if (updated) this._deviceList.emit('userSskUpdated', userId); + // NB. Unlike most events in the js-sdk, this one is internal to the + // js-sdk and is not re-emitted + this._deviceList.emit('userCrossSigningUpdated', userId); + } } } } @@ -883,55 +896,6 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore, return updated; } -async function _updateStoredSelfSigningKeyForUser( - _olmDevice, userId, userStore, userResult, -) { - // FIXME: this function may need modifying - let updated = false; - - if (userResult.user_id !== userId) { - logger.warn("Mismatched user_id " + userResult.user_id + - " in self-signing key from " + userId); - return; - } - if (!userResult || !userResult.usage.includes('self_signing')) { - logger.warn( - "Self-signing key for " + userId + - " does not include 'self_signing' usage: ignoring", - ); - return; - } - const keyCount = Object.keys(userResult.keys).length; - if (keyCount !== 1) { - logger.warn( - "Self-signing key block for " + userId + " has " + - keyCount + " keys: expected exactly 1. Ignoring.", - ); - return; - } - let oldKeyId = null; - let oldKey = null; - if (userStore.keys && Object.keys(userStore.keys).length > 0) { - oldKeyId = Object.keys(userStore.keys)[0]; - oldKey = userStore.keys[oldKeyId]; - } - const newKeyId = Object.keys(userResult.keys)[0]; - const newKey = userResult.keys[newKeyId]; - if (oldKeyId !== newKeyId || oldKey !== newKey) { - updated = true; - logger.info( - "New self-signing key detected for " + userId + - ": " + newKeyId + ", was previously " + oldKeyId, - ); - - userStore.user_id = userResult.user_id; - userStore.usage = userResult.usage; - userStore.keys = userResult.keys; - } - - return updated; -} - /* * Process a device in a /query response, and add it to the userStore * @@ -955,6 +919,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { } const unsigned = deviceResult.unsigned || {}; + const signatures = deviceResult.signatures || {}; try { await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey); @@ -987,5 +952,6 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { deviceStore.keys = deviceResult.keys || {}; deviceStore.algorithms = deviceResult.algorithms || []; deviceStore.unsigned = unsigned; + deviceStore.signatures = signatures; return true; } diff --git a/src/crypto/deviceinfo.js b/src/crypto/deviceinfo.js index aa5c4afac38..c996e463580 100644 --- a/src/crypto/deviceinfo.js +++ b/src/crypto/deviceinfo.js @@ -56,6 +56,7 @@ function DeviceInfo(deviceId) { this.verified = DeviceVerification.UNVERIFIED; this.known = false; this.unsigned = {}; + this.signatures = {} } /** @@ -88,6 +89,7 @@ DeviceInfo.prototype.toStorage = function() { verified: this.verified, known: this.known, unsigned: this.unsigned, + signatures: this.signatures, }; }; diff --git a/src/crypto/index.js b/src/crypto/index.js index cbcfab78b8e..6570481986e 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -2,6 +2,7 @@ Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018-2019 New Vector Ltd +Copyright 2019 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. @@ -31,11 +32,10 @@ const OlmDevice = require("./OlmDevice"); const olmlib = require("./olmlib"); const algorithms = require("./algorithms"); const DeviceInfo = require("./deviceinfo"); -import SskInfo from './sskinfo'; const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceList = require('./DeviceList').default; import { randomString } from '../randomstring'; -import { CrossSigningInfo, CrossSigningLevel, CrossSigningVerification } from './CrossSigning'; +import { CrossSigningInfo } from './CrossSigning'; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; @@ -104,7 +104,7 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200; */ export default function Crypto(baseApis, sessionStore, userId, deviceId, clientStore, cryptoStore, roomList, verificationMethods) { - this._onDeviceListUserSskUpdated = this._onDeviceListUserSskUpdated.bind(this); + this._onDeviceListUserCrossSigningUpdated = this._onDeviceListUserCrossSigningUpdated.bind(this); this._baseApis = baseApis; this._sessionStore = sessionStore; @@ -146,7 +146,7 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, ); // XXX: This isn't removed at any point, but then none of the event listeners // this class sets seem to be removed at any point... :/ - this._deviceList.on('userSskUpdated', this._onDeviceListUserSskUpdated); + this._deviceList.on('userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated); // the last time we did a check for the number of one-time-keys on the // server. @@ -269,6 +269,7 @@ Crypto.prototype.init = async function() { */ Crypto.prototype.resetCrossSigningKeys = async function(level) { await this._crossSigningInfo.resetKeys(level); + this._baseApis.emit("cross-signing:keysChanged", {}); }; /** @@ -278,6 +279,7 @@ Crypto.prototype.resetCrossSigningKeys = async function(level) { */ Crypto.prototype.setCrossSigningKeys = function(keys) { this._crossSigningInfo.setKeys(keys); + this._baseApis.emit("cross-signing:keysChanged", {}); }; /** @@ -331,34 +333,93 @@ Crypto.prototype.checkDeviceTrust = function(userId, deviceId) { /* * Event handler for DeviceList's userNewDevices event */ -Crypto.prototype._onDeviceListUserSskUpdated = async function(userId) { +Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) { if (userId === this._userId) { - this.checkOwnSskTrust(); + this.checkOwnCrossSigningTrust(); } }; /* - * Check the copy of our SSK that we have in the device list and see if it - * matches our private part. If it does, mark it as trusted. + * Check the copy of our cross-signing key that we have in the device list and + * see if we can get the private key. If so, mark it as trusted. */ -Crypto.prototype.checkOwnSskTrust = async function() { +Crypto.prototype.checkOwnCrossSigningTrust = async function() { const userId = this._userId; - // If we see an update to our own SSK, check it against the SSK we have and, - // if it matches, mark it as verified + // If we see an update to our own master key, check it against the master + // key we have and, if it matches, mark it as verified - // First, get the pubkey of the one we can see - const seenSsk = this._deviceList.getStoredSskForUser(userId); - if (!seenSsk) { + // First, get the new cross-signing info + const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + if (!newCrossSigning) { logger.error( - "Got SSK update event for user " + userId + - " but no new SSK found!", + "Got cross-signing update event for user " + userId + + " but no new cross-signing information found!", ); return; } - const seenPubkey = seenSsk.getFingerprint(); + const seenPubkey = newCrossSigning.getId(); + const changed = this._crossSigningInfo.getId() !== seenPubkey; + let privkey; + if (changed) { + // try to get the private key if the master key changed + logger.info("Got new master key", seenPubkey); + + let error; + do { + privkey = await new Promise((resolve, reject) => { + this._baseApis.emit("cross-signing:newKey", { + publicKey: seenPubkey, + type: "master", + error, + done: (key) => { + // check key matches + const signing = new global.Olm.PkSigning(); + try { + const pubkey = signing.init_with_seed(key); + if (pubkey !== seenPubkey) { + error = "Key does not match"; + logger.info("Key does not match: got " + pubkey + + " expected " + seenPubkey); + return; + } + } finally { + signing.free(); + } + resolve(key); + }, + cancel: (error) => { + reject(error || new Error("Cancelled by user")); + }, + }); + }); + } while (!privkey); + this._baseApis.emit("cross-signing:savePrivateKeys", {master: privkey}); + + logger.info("Got private key"); + } + + // FIXME: fetch the private key? + if (this._crossSigningInfo.getId("self_signing") + !== newCrossSigning.getId("self_signing")) { + logger.info("Got new self-signing key", newCrossSigning.getId("self_signing")); + } + if (this._crossSigningInfo.getId("user_signing") + !== newCrossSigning.getId("user_signing")) { + logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); + } + + this._crossSigningInfo.setKeys(newCrossSigning.keys); + // FIXME: save it ... somewhere? + + if (changed) { + this._baseApis.emit("cross-signing:keysChanged", {}); + } + + // FIXME: // Now dig out the account keys and get the pubkey of the one in there + /* let accountKeys = null; await this._cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], @@ -401,6 +462,7 @@ Crypto.prototype.checkOwnSskTrust = async function() { localPubkey + ", published: " + seenPubkey, ); } + */ }; /** @@ -986,7 +1048,6 @@ Crypto.prototype.setDeviceVerification = async function( const xsk = this._deviceList.getStoredCrossSigningForUser(userId); if (xsk.getId() === deviceId) { if (verified) { - xsk.verified = CrossSigningVerification.VERIFIED; const device = await this._crossSigningInfo.signUser(xsk); // FIXME: mark xsk as dirty in device list this._baseApis.uploadKeySignatures({ From 609ee663fae925e6c9006414e17f67bc9dfdf2f7 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 29 May 2019 16:58:49 -0400 Subject: [PATCH 19/97] use the right path for logger --- spec/test-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/test-utils.js b/spec/test-utils.js index 780de5ac809..7ff34f77772 100644 --- a/spec/test-utils.js +++ b/spec/test-utils.js @@ -1,7 +1,7 @@ "use strict"; import expect from 'expect'; import Promise from 'bluebird'; -const logger = require("../logger"); +const logger = require("../lib/logger"); // load olm before the sdk if possible import './olm-loader'; From 941d871daf2b4645b7ca5d7f81101fd7ac918bda Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 29 May 2019 16:59:51 -0400 Subject: [PATCH 20/97] fix check for empty cross-signing repsonse --- src/crypto/DeviceList.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index ec9cb9bcd43..dee989538cb 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -829,7 +829,11 @@ class DeviceListUpdateSerialiser { // now do the same for the cross-signing keys { - if (crossSigningResponse && Object.keys(crossSigningResponse).length) { + // FIXME: should we be ignoring empty cross-signing responses, or + // should we be dropping the keys? + if (crossSigningResponse + && (crossSigningResponse.master || crossSigningResponse.self_signing + || crossSigningResponse.user_signing)) { const crossSigning = this._deviceList.getStoredCrossSigningForUser(userId) || new CrossSigningInfo(userId); From 936eef194a099643174b02a0ec750f5b9740bd91 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 29 May 2019 17:01:13 -0400 Subject: [PATCH 21/97] minor fixes to tests --- spec/unit/crypto/cross-signing.spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 5929207130d..df8f39d15a4 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -50,12 +50,12 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); - // set Alices' cross-signing key + // set Alice's cross-signing key let privateKeys; alice.on("cross-signing:savePrivateKeys", function(e) { privateKeys = e; }); - alice.resetCrossSigningKeys(); + await alice.resetCrossSigningKeys(); // Alice downloads Bob's device key alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { @@ -85,7 +85,7 @@ describe("Cross Signing", function() { await promise; }); - it("should get ssk and usk from sync", async function() { + it("should get cross-signing keys from sync", async function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); @@ -124,7 +124,7 @@ describe("Cross Signing", function() { await alice._crypto._signObject(aliceDevice); olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com"); - // feed sync result that includes ssk, usk, device key + // feed sync result that includes master key, ssk, device key const responses = [ HttpResponse.PUSH_RULES_RESPONSE, { @@ -215,12 +215,12 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); - // set Alices' cross-signing key + // set Alice's cross-signing key let privateKeys; alice.on("cross-signing:savePrivateKeys", function(e) { privateKeys = e; }); - alice.resetCrossSigningKeys(); + await alice.resetCrossSigningKeys(); // Alice downloads Bob's ssk and device key const bobMasterSigning = new global.Olm.PkSigning(); const bobMasterPrivkey = bobMasterSigning.generate_seed(); @@ -299,12 +299,12 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); - // set Alices' cross-signing key + // set Alice's cross-signing key let privateKeys; alice.on("cross-signing:savePrivateKeys", function(e) { privateKeys = e; }); - alice.resetCrossSigningKeys(); + await alice.resetCrossSigningKeys(); // Alice downloads Bob's ssk and device key // (NOTE: device key is not signed by ssk) const bobMasterSigning = new global.Olm.PkSigning(); @@ -373,7 +373,7 @@ describe("Cross Signing", function() { alice.on("cross-signing:savePrivateKeys", function(e) { privateKeys = e; }); - alice.resetCrossSigningKeys(); + await alice.resetCrossSigningKeys(); // Alice downloads Bob's keys const bobMasterSigning = new global.Olm.PkSigning(); const bobMasterPrivkey = bobMasterSigning.generate_seed(); From 95131c76580608d43925b281927d9c8d2bcd12b2 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 29 May 2019 17:01:25 -0400 Subject: [PATCH 22/97] add test for syncing trust on another user --- spec/unit/crypto/cross-signing.spec.js | 155 ++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index df8f39d15a4..3b78fd06add 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -289,10 +289,163 @@ describe("Cross Signing", function() { }); it("should trust signatures received from other devices", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com"); + alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {}; + + // set Alice's cross-signing key + let privateKeys; + alice.on("cross-signing:savePrivateKeys", function(e) { + privateKeys = e; + }); + await alice.resetCrossSigningKeys(); + + alice.once("cross-signing:getKey", (e) => { + e.done(privateKeys[e.type]); + }); + + const selfSigningKey = new Uint8Array([ + 0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, + 0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, + 0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49, + 0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f, + ]); + + const keyChangePromise = new Promise((resolve, reject) => { + alice._crypto._deviceList.once("userCrossSigningUpdated", (userId) => { + if (userId === "@bob:example.com") { + resolve(); + } + }); + }); + + const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"] + .Osborne2; + const aliceDevice = { + user_id: "@alice:example.com", + device_id: "Osborne2", + }; + aliceDevice.keys = deviceInfo.keys; + aliceDevice.algorithms = deviceInfo.algorithms; + await alice._crypto._signObject(aliceDevice); + + const bobOlmAccount = new global.Olm.Account(); + bobOlmAccount.create(); + const bobKeys = JSON.parse(bobOlmAccount.identity_keys()); + const bobDevice = { + user_id: "@bob:example.com", + device_id: "Dynabook", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:Dynabook": bobKeys.ed25519, + "curve25519:Dynabook": bobKeys.curve25519, + }, + }; + const deviceStr = anotherjson.stringify(bobDevice); + bobDevice.signatures = { + "@bob:example.com": { + "ed25519:Dynabook": bobOlmAccount.sign(deviceStr), + }, + }; + olmlib.pkSign(bobDevice, selfSigningKey, "@bob:example.com"); + + const bobMaster = { + user_id: "@bob:example.com", + usage: ["master"], + keys: { + "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": + "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", + }, + }; + olmlib.pkSign(bobMaster, privateKeys.user_signing, "@alice:example.com"); + // Alice downloads Bob's keys // - device key - // - ssk signed by her usk + // - ssk + // - master key signed by her usk (pretend that it was signed by another + // of Alice's devices) + const responses = [ + HttpResponse.PUSH_RULES_RESPONSE, + { + method: "POST", + path: "/keys/upload/Osborne2", + data: { + one_time_key_counts: { + curve25519: 100, + signed_curve25519: 100, + }, + }, + }, + HttpResponse.filterResponse("@alice:example.com"), + { + method: "GET", + path: "/sync", + data: { + next_batch: "abcdefg", + device_lists: { + changed: [ + "@bob:example.com", + ], + }, + }, + }, + { + method: "POST", + path: "/keys/query", + data: { + "failures": {}, + "device_keys": { + "@alice:example.com": { + "Osborne2": aliceDevice, + }, + "@bob:example.com": { + "Dynabook": bobDevice, + }, + }, + "master_keys": { + "@bob:example.com": bobMaster, + }, + "self_signing_keys": { + "@bob:example.com": { + user_id: "@bob:example.com", + usage: ["self-signing"], + keys: { + "ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ": + "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", + }, + signatures: { + "@bob:example.com": { + "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": + "2KLiufImvEbfJuAFvsaZD+PsL8ELWl7N1u9yr/9hZvwRghBfQMB" + + "LAI86b1kDV9+Cq1lt85ykReeCEzmTEPY2BQ", + }, + }, + }, + }, + }, + }, + { + method: "POST", + path: "/keys/upload/Osborne2", + data: { + one_time_key_counts: { + curve25519: 100, + signed_curve25519: 100, + }, + }, + }, + ]; + setHttpResponses(alice, responses); + + await alice.startClient(); + + await keyChangePromise; + // Bob's device key should be trusted + expect(alice.checkUserTrust("@bob:example.com")).toBe(6); + expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(6); }); it("should dis-trust an unsigned device", async function() { From dc971b9a5979f6a8a977f6516c3416393885603d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 4 Jun 2019 14:58:46 -0400 Subject: [PATCH 23/97] add missing semicolon --- src/crypto/deviceinfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/deviceinfo.js b/src/crypto/deviceinfo.js index c996e463580..2dbb2393d84 100644 --- a/src/crypto/deviceinfo.js +++ b/src/crypto/deviceinfo.js @@ -56,7 +56,7 @@ function DeviceInfo(deviceId) { this.verified = DeviceVerification.UNVERIFIED; this.known = false; this.unsigned = {}; - this.signatures = {} + this.signatures = {}; } /** From 4a9a1b40e9e919ab43e2a459eb3f9a4583482b57 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 4 Jun 2019 15:04:45 -0400 Subject: [PATCH 24/97] initial implementation of secret storage and sharing --- spec/unit/crypto/secrets.spec.js | 153 +++++++++++ spec/unit/crypto/verification/util.js | 15 +- src/crypto/Secrets.js | 362 ++++++++++++++++++++++++++ src/crypto/index.js | 29 ++- 4 files changed, 540 insertions(+), 19 deletions(-) create mode 100644 spec/unit/crypto/secrets.spec.js create mode 100644 src/crypto/Secrets.js diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js new file mode 100644 index 00000000000..8f33befdf35 --- /dev/null +++ b/spec/unit/crypto/secrets.spec.js @@ -0,0 +1,153 @@ +/* +Copyright 2019 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 '../../olm-loader'; + +import expect from 'expect'; +import anotherjson from 'another-json'; +import { MatrixEvent } from '../../../lib/models/event'; + +import olmlib from '../../../lib/crypto/olmlib'; + +import TestClient from '../../TestClient'; +import { makeTestClients } from './verification/util'; + +import {HttpResponse, setHttpResponses} from '../../test-utils'; + +async function makeTestClient(userInfo, options) { + const client = (new TestClient( + userInfo.userId, userInfo.deviceId, undefined, undefined, options, + )).client; + + await client.initCrypto(); + + return client; +} + +describe("Secrets", function() { + if (!global.Olm) { + console.warn('Not running megolm backup unit tests: libolm not present'); + return; + } + + beforeEach(async function() { + await global.Olm.init(); + }); + + it("should store and retrieve a secret", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + const secretStorage = alice._crypto._secretStorage; + + const decryption = new global.Olm.PkDecryption(); + const pubkey = decryption.generate_key(); + const privkey = decryption.get_private_key(); + + alice.setAccountData = async function(eventType, contents, callback) { + alice.store.storeAccountDataEvents([ + new MatrixEvent({ + type: eventType, + content: contents, + }), + ]); + if (callback) { + callback(); + } + }; + + alice.store.storeAccountDataEvents([ + new MatrixEvent({ + type: "m.secret_storage.key.abc", + content: { + algorithm: "m.secret_storage.v1.curve25519-aes-sha2", + pubkey: pubkey, + }, + }), + ]); + + expect(secretStorage.isStored("foo")).toBe(false); + + await secretStorage.store("foo", "bar", ["abc"]); + + expect(secretStorage.isStored("foo")).toBe(true); + + const getKey = expect.createSpy().andCall(function(e) { + expect(Object.keys(e.keys)).toEqual(["abc"]); + e.done("abc", privkey); + }); + alice.once("crypto.secrets.getKey", getKey); + + expect(await secretStorage.get("foo")).toBe("bar"); + + expect(getKey).toHaveBeenCalled(); + }); + + it("should request secrets from other clients", async function() { + const [osborne2, vax] = await makeTestClients( + [ + {userId: "@alice:example.com", deviceId: "Osborne2"}, + {userId: "@alice:example.com", deviceId: "VAX"}, + ], + ); + + const vaxDevice = vax._crypto._olmDevice; + const osborne2Device = osborne2._crypto._olmDevice; + const secretStorage = osborne2._crypto._secretStorage; + + osborne2._crypto._deviceList.storeDevicesForUser("@alice:example.com", { + "VAX": { + user_id: "@alice:example.com", + device_id: "VAX", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:VAX": vaxDevice.deviceEd25519Key, + "curve25519:VAX": vaxDevice.deviceCurve25519Key, + }, + }, + }); + vax._crypto._deviceList.storeDevicesForUser("@alice:example.com", { + "Osborne2": { + user_id: "@alice:example.com", + device_id: "Osborne2", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:Osborne2": osborne2Device.deviceEd25519Key, + "curve25519:Osborne2": osborne2Device.deviceCurve25519Key, + }, + }, + }); + + vax.once("crypto.secrets.request", function(e) { + expect(e.name).toBe("foo"); + e.send("bar"); + }); + + await osborne2Device.generateOneTimeKeys(1); + const otks = (await osborne2Device.getOneTimeKeys()).curve25519; + await osborne2Device.markKeysAsPublished(); + + await vax._crypto._olmDevice.createOutboundSession( + osborne2Device.deviceCurve25519Key, + Object.values(otks)[0], + ); + + const request = await secretStorage.request("foo", ["VAX"]); + const secret = await request.promise; + + expect(secret).toBe("bar"); + }); +}); diff --git a/spec/unit/crypto/verification/util.js b/spec/unit/crypto/verification/util.js index faa9c58f416..eef40fdb9a1 100644 --- a/spec/unit/crypto/verification/util.js +++ b/spec/unit/crypto/verification/util.js @@ -33,11 +33,16 @@ export async function makeTestClients(userInfos, options) { type: type, content: msg, }); - setTimeout( - () => clientMap[userId][deviceId] - .emit("toDeviceEvent", event), - 0, - ); + const client = clientMap[userId][deviceId]; + if (event.isEncrypted()) { + event.attemptDecryption(client._crypto) + .then(() => client.emit("toDeviceEvent", event)); + } else { + setTimeout( + () => client.emit("toDeviceEvent", event), + 0, + ); + } } } } diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js new file mode 100644 index 00000000000..1184820a1f0 --- /dev/null +++ b/src/crypto/Secrets.js @@ -0,0 +1,362 @@ +/* +Copyright 2019 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 {EventEmitter} from 'events'; +import logger from '../logger'; +import olmlib from './olmlib'; + +/** Implements MSC-1946 + */ +export default class SecretStorage extends EventEmitter { + constructor(baseApis) { + super(); + this._baseApis = baseApis; + this._requests = {}; + this._incomingRequests = {}; + } + + /** store an encrypted secret on the server + * + * @param {string} name The name of the secret + * @param {string} secret The secret contents. + * @param {Array} keys The IDs of the keys to use to encrypt the secret + */ + async store(name, secret, keys) { + const encrypted = {}; + + for (const keyName of keys) { + // get key information from key storage + const keyInfo = this._baseApis.getAccountData( + "m.secret_storage.key." + keyName, + ); + if (!keyInfo) { + continue; + } + const keyInfoContent = keyInfo.getContent(); + // FIXME: check signature of key info + // encrypt secret, based on the algorithm + switch (keyInfoContent.algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + { + const encryption = new global.Olm.PkEncryption(); + try { + encryption.set_recipient_key(keyInfoContent.pubkey); + encrypted[keyName] = encryption.encrypt(secret); + } finally { + encryption.free(); + } + break; + } + default: + logger.warn("unknown algorithm for secret storage key " + keyName + + ": " + keyInfoContent.algorithm); + // do nothing if we don't understand the encryption algorithm + } + } + + // save encrypted secret + await this._baseApis.setAccountData(name, {encrypted}); + } + + async get(name) { + const secretInfo = this._baseApis.getAccountData(name); + if (!secretInfo) { + return; + } + + const secretContent = secretInfo.getContent(); + + if (!secretContent.encrypted) { + return; + } + + // get possible keys to decrypt + const keys = {}; + for (const keyName of Object.keys(secretContent.encrypted)) { + // get key information from key storage + const keyInfo = this._baseApis.getAccountData( + "m.secret_storage.key." + keyName, + ).getContent(); + const encInfo = secretContent.encrypted[keyName]; + switch (keyInfo.algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac + && encInfo.ephemeral) { + keys[keyName] = keyInfo; + } + break; + default: + // do nothing if we don't understand the encryption algorithm + } + } + + // fetch private key from app + let decryption; + let keyName; + let cleanUp; + let error; + do { + [keyName, decryption, cleanUp] = await new Promise((resolve, reject) => { + this._baseApis.emit("crypto.secrets.getKey", { + keys, + error, + done: function(keyName, key) { + // FIXME: interpret key? + if (!keys[keyName]) { + error = "Unknown key (your app is broken)"; + resolve([]); + } + switch (keys[keyName].algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + { + const decryption = new global.Olm.PkDecryption(); + try { + const pubkey = decryption.init_with_private_key(key); + if (pubkey !== keys[keyName].pubkey) { + error = "Key does not match"; + resolve([]); + return; + } + } catch (e) { + decryption.free(); + error = "Invalid key"; + resolve([]); + return; + } + resolve([ + keyName, + decryption, + decryption.free.bind(decryption), + ]); + break; + } + default: + error = "The universe is broken"; + resolve([]); + } + }, + cancel: function(e) { + reject(e || new Error("Cancelled")); + }, + }); + }); + if (error) { + logger.error("Error getting private key:", error); + } + } while (!keyName); + + // decrypt secret + try { + const encInfo = secretContent.encrypted[keyName]; + switch (keys[keyName].algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + return decryption.decrypt( + encInfo.ephemeral, encInfo.mac, encInfo.ciphertext, + ); + } + } finally { + cleanUp(); + } + } + + isStored(name, checkKey) { + // check if secret exists + const secretInfo = this._baseApis.getAccountData(name); + if (!secretInfo) { + return false; + } + + const secretContent = secretInfo.getContent(); + + if (!secretContent.encrypted) { + return false; + } + + // check if secret is encrypted by a known/trusted secret and + // encryption looks sane + for (const keyName of Object.keys(secretContent.encrypted)) { + // get key information from key storage + const keyInfo = this._baseApis.getAccountData( + "m.secret_storage.key." + keyName, + ).getContent(); + const encInfo = secretContent.encrypted[keyName]; + if (checkKey) { + // FIXME: check signature on key + } + switch (keyInfo.algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac + && encInfo.ephemeral) { + return true; + } + break; + default: + // do nothing if we don't understand the encryption algorithm + } + } + return false; + } + + request(name, devices) { + const requestId = this._baseApis.makeTxnId(); + + const requestControl = this._requests[requestId] = { + devices, + }; + const promise = new Promise((resolve, reject) => { + requestControl.resolve = resolve; + requestControl.reject = reject; + }); + const cancel = (reason) => { + // send cancellation event + const cancelData = { + action: "cancel_request", + requesting_device_id: this._baseApis.deviceId, + request_id: requestId, + }; + const toDevice = {}; + for (const device of devices) { + toDevice[device] = cancelData; + } + this._baseApis.sendToDevice("m.secret.request", { + [this._baseApis.getUserId()]: toDevice, + }); + + // and reject the promise so that anyone waiting on it will be + // notified + requestControl.reject(new Error(reason ||"Cancelled")); + }; + + // send request to devices + const requestData = { + name, + action: "request", + requesting_device_id: this._baseApis.deviceId, + request_id: requestId, + }; + const toDevice = {}; + for (const device of devices) { + toDevice[device] = requestData; + } + this._baseApis.sendToDevice("m.secret.request", { + [this._baseApis.getUserId()]: toDevice, + }); + + return { + request_id: requestId, + promise, + cancel, + }; + } + + _onRequestReceived(event) { + const sender = event.getSender(); + const content = event.getContent(); + if (sender !== this._baseApis.getUserId() + || !(content.name && content.action + && content.requesting_device_id && content.request_id)) { + // ignore requests from anyone else, for now + return; + } + const deviceId = content.requesting_device_id; + // check if it's a cancel + if (content.action === "cancel_request") { + if (this._incomingRequests[deviceId] + && this._incomingRequests[deviceId][content.request_id]) { + logger.info("received request cancellation for secret (" + sender + + ", " + deviceId + ", " + content.request_id + ")"); + this.baseApis.emit("crypto.secrets.request_cancelled", { + user_id: sender, + device_id: deviceId, + request_id: content.request_id, + }); + } + } else if (content.action === "request") { + // if from us and device is trusted (or else check trust) + // check if we have the secret + logger.info("received request for secret (" + sender + + ", " + deviceId + ", " + content.request_id + ")"); + this._baseApis.emit("crypto.secrets.request", { + sender: sender, + device_id: deviceId, + request_id: content.request_id, + name: content.name, + device_trust: this._baseApis.checkDeviceTrust(sender, deviceId), + send: async (secret) => { + const payload = { + type: "m.secret.share", + content: { + request_id: content.request_id, + secret: secret, + }, + }; + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this._baseApis.getUserId(), + this._baseApis.deviceId, + this._baseApis._crypto._olmDevice, + sender, + this._baseApis._crypto.getStoredDevice(sender, deviceId), + payload, + ); + const contentMap = { + [sender]: { + [deviceId]: encryptedContent, + }, + }; + + this._baseApis.sendToDevice("m.room.encrypted", contentMap); + }, + }); + } + } + + _onSecretReceived(event) { + if (event.getSender() !== this._baseApis.getUserId()) { + // we shouldn't be receiving secrets from anyone else, so ignore + // because someone could be trying to send us bogus data + return; + } + const content = event.getContent(); + logger.log("got secret share for request ", content.request_id); + const requestControl = this._requests[content.request_id]; + if (requestControl) { + // make sure that the device that sent it is one of the devices that + // we requested from + const deviceInfo = this._baseApis._crypto._deviceList.getDeviceByIdentityKey( + olmlib.OLM_ALGORITHM, + event.getSenderKey(), + ); + if (!deviceInfo) { + logger.log( + "secret share from unknown device with key", event.getSenderKey(), + ); + return; + } + if (!requestControl.devices.includes(deviceInfo.deviceId)) { + logger.log("unsolicited secret share from device", deviceInfo.deviceId); + return; + } + + requestControl.resolve(content.secret); + } + } +} diff --git a/src/crypto/index.js b/src/crypto/index.js index 6570481986e..1fc8f8ed445 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -36,6 +36,7 @@ const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceList = require('./DeviceList').default; import { randomString } from '../randomstring'; import { CrossSigningInfo } from './CrossSigning'; +import SecretStorage from './Secrets'; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; @@ -205,6 +206,9 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, this._crossSigningInfo.on("cross-signing:getKey", (...args) => { this._baseApis.emit("cross-signing:getKey", ...args); }); + + this._secretStorage = new SecretStorage(baseApis); + // TODO: expose SecretStorage methods } utils.inherits(Crypto, EventEmitter); @@ -320,13 +324,16 @@ Crypto.prototype.checkUserTrust = function(userId) { * TODO: see checkUserTrust */ Crypto.prototype.checkDeviceTrust = function(userId, deviceId) { - const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); - const device = this._deviceList.getStoredDevice(userId, deviceId); let rv = 0; - if (device.isVerified()) { + + const device = this._deviceList.getStoredDevice(userId, deviceId); + if (device && device.isVerified()) { rv |= 1; } - rv |= this._crossSigningInfo.checkDeviceTrust(userCrossSigning, device) << 1; + const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + if (device && userCrossSigning) { + rv |= this._crossSigningInfo.checkDeviceTrust(userCrossSigning, device) << 1; + } return rv; }; @@ -1015,16 +1022,6 @@ Crypto.prototype.saveDeviceList = function(delay) { return this._deviceList.saveIfDirty(delay); }; -Crypto.prototype.setSskVerification = async function(userId, verified) { - const ssk = this._deviceList.getRawStoredSskForUser(userId); - if (!ssk) { - throw new Error("No self-signing key found for user " + userId); - } - ssk.verified = verified; - this._deviceList.storeSskForUser(userId, ssk); - this._deviceList.saveIfDirty(); -}; - /** * Update the blocked/verified state of the given device * @@ -1953,6 +1950,10 @@ Crypto.prototype._onToDeviceEvent = function(event) { this._onRoomKeyEvent(event); } else if (event.getType() == "m.room_key_request") { this._onRoomKeyRequestEvent(event); + } else if (event.getType() === "m.secret.request") { + this._secretStorage._onRequestReceived(event); + } else if (event.getType() === "m.secret.share") { + this._secretStorage._onSecretReceived(event); } else if (event.getType() === "m.key.verification.request") { this._onKeyVerificationRequest(event); } else if (event.getType() === "m.key.verification.start") { From 0c714ba4a1fcf11f1c32bfae4e76d6e0438bb553 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 5 Jun 2019 15:24:03 -0400 Subject: [PATCH 25/97] some cleanups --- spec/test-utils.js | 1 - spec/unit/crypto/cross-signing.spec.js | 2 -- spec/unit/crypto/secrets.spec.js | 3 --- src/crypto/index.js | 2 +- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/spec/test-utils.js b/spec/test-utils.js index fcb9eb0fb2f..57835536bcf 100644 --- a/spec/test-utils.js +++ b/spec/test-utils.js @@ -1,7 +1,6 @@ "use strict"; import expect from 'expect'; import Promise from 'bluebird'; -const logger = require("../lib/logger"); // load olm before the sdk if possible import './olm-loader'; diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 3b78fd06add..0f7e75c4e97 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -67,8 +67,6 @@ describe("Cross Signing", function() { }, }, }, - verified: 0, - unsigned: {}, }); // Alice verifies Bob's key alice.on("cross-signing:getKey", function(e) { diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index 8f33befdf35..c357c33aacf 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -17,7 +17,6 @@ limitations under the License. import '../../olm-loader'; import expect from 'expect'; -import anotherjson from 'another-json'; import { MatrixEvent } from '../../../lib/models/event'; import olmlib from '../../../lib/crypto/olmlib'; @@ -25,8 +24,6 @@ import olmlib from '../../../lib/crypto/olmlib'; import TestClient from '../../TestClient'; import { makeTestClients } from './verification/util'; -import {HttpResponse, setHttpResponses} from '../../test-utils'; - async function makeTestClient(userInfo, options) { const client = (new TestClient( userInfo.userId, userInfo.deviceId, undefined, undefined, options, diff --git a/src/crypto/index.js b/src/crypto/index.js index 1164892ea03..3be1be3c07b 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1047,7 +1047,7 @@ Crypto.prototype.setDeviceVerification = async function( userId, deviceId, verified, blocked, known, ) { const xsk = this._deviceList.getStoredCrossSigningForUser(userId); - if (xsk.getId() === deviceId) { + if (xsk && xsk.getId() === deviceId) { if (verified) { const device = await this._crossSigningInfo.signUser(xsk); // FIXME: mark xsk as dirty in device list From 6f6e7ea9211cb0b786d6b1676b669640186e2385 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 5 Jun 2019 15:27:31 -0400 Subject: [PATCH 26/97] verify cross-signing key with SAS --- spec/unit/crypto/verification/sas.spec.js | 98 +++++++++++++++-------- src/client.js | 3 + src/crypto/index.js | 12 +++ src/crypto/verification/Base.js | 20 ++++- src/crypto/verification/SAS.js | 21 ++++- 5 files changed, 113 insertions(+), 41 deletions(-) diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index 59df685b471..0fbea9a9627 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -22,6 +22,7 @@ try { } import expect from 'expect'; +import olmlib from '../../../../lib/crypto/olmlib'; import sdk from '../../../..'; @@ -78,38 +79,35 @@ describe("SAS verification", function() { }, ); - alice.setDeviceVerified = expect.createSpy(); - alice.getDeviceEd25519Key = () => { - return "alice+base64+ed25519+key"; - }; - alice.getStoredDevice = () => { - return DeviceInfo.fromStorage( - { - keys: { - "ed25519:Dynabook": "bob+base64+ed25519+key", - }, + const aliceDevice = alice._crypto._olmDevice; + const bobDevice = bob._crypto._olmDevice; + + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: { + user_id: "@bob:example.com", + device_id: "Dynabook", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:Dynabook": bobDevice.deviceEd25519Key, + "curve25519:Dynabook": bobDevice.deviceCurve25519Key, }, - "Dynabook", - ); - }; + }, + }); alice.downloadKeys = () => { return Promise.resolve(); }; - bob.setDeviceVerified = expect.createSpy(); - bob.getStoredDevice = () => { - return DeviceInfo.fromStorage( - { - keys: { - "ed25519:Osborne2": "alice+base64+ed25519+key", - }, + bob._crypto._deviceList.storeDevicesForUser("@alice:example.com", { + Osborne2: { + user_id: "@alice:example.com", + device_id: "Osborne2", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:Osborne2": aliceDevice.deviceEd25519Key, + "curve25519:Osborne2": aliceDevice.deviceCurve25519Key, }, - "Osborne2", - ); - }; - bob.getDeviceEd25519Key = () => { - return "bob+base64+ed25519+key"; - }; + }, + }); bob.downloadKeys = () => { return Promise.resolve(); }; @@ -180,10 +178,12 @@ describe("SAS verification", function() { expect(macMethod).toBe("hkdf-hmac-sha256"); // make sure Alice and Bob verified each other - expect(alice.setDeviceVerified) - .toHaveBeenCalledWith(bob.getUserId(), bob.deviceId); - expect(bob.setDeviceVerified) - .toHaveBeenCalledWith(alice.getUserId(), alice.deviceId); + const bobDevice + = await alice.getStoredDevice("@bob:example.com", "Dynabook"); + expect(bobDevice.isVerified()).toBeTruthy(); + const aliceDevice + = await bob.getStoredDevice("@alice:example.com", "Osborne2"); + expect(aliceDevice.isVerified()).toBeTruthy(); }); it("should be able to verify using the old MAC", async function() { @@ -218,10 +218,40 @@ describe("SAS verification", function() { expect(macMethod).toBe("hmac-sha256"); - expect(alice.setDeviceVerified) - .toHaveBeenCalledWith(bob.getUserId(), bob.deviceId); - expect(bob.setDeviceVerified) - .toHaveBeenCalledWith(alice.getUserId(), alice.deviceId); + const bobDevice + = await alice.getStoredDevice("@bob:example.com", "Dynabook"); + expect(bobDevice.isVerified()).toBeTruthy(); + const aliceDevice + = await bob.getStoredDevice("@alice:example.com", "Osborne2"); + expect(aliceDevice.isVerified()).toBeTruthy(); + }); + + it("should verify a cross-signing key", async function() { + const privateKeys = {}; + alice.on("cross-signing:savePrivateKeys", function(e) { + privateKeys.alice = e; + }); + await alice.resetCrossSigningKeys(); + bob.on("cross-signing:savePrivateKeys", function(e) { + privateKeys.bob = e; + }); + await bob.resetCrossSigningKeys(); + + bob.on("cross-signing:getKey", function(e) { + e.done(privateKeys.bob[e.type]); + }); + + bob._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + keys: alice._crypto._crossSigningInfo.keys, + }); + await Promise.all([ + aliceVerifier.verify(), + bobPromise.then((verifier) => verifier.verify()), + ]); + + expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(1); + expect(bob.checkUserTrust("@alice:example.com")).toBe(6); + expect(bob.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(1); }); }); diff --git a/src/client.js b/src/client.js index c4ddd6d8c7e..0f64fb2fa8d 100644 --- a/src/client.js +++ b/src/client.js @@ -854,6 +854,9 @@ MatrixClient.prototype.resetCrossSigningKeys MatrixClient.prototype.setCrossSigningKeys = wrapCryptoFunc("setCrossSigningKeys"); +MatrixClient.prototype.getCrossSigningId + = wrapCryptoFunc("getCrossSigningId"); + /** * Cancel a room key request for this event if one is ongoing and resend the * request. diff --git a/src/crypto/index.js b/src/crypto/index.js index 3be1be3c07b..6edf07787d6 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -286,6 +286,18 @@ Crypto.prototype.setCrossSigningKeys = function(keys) { this._baseApis.emit("cross-signing:keysChanged", {}); }; +/** + * Get the user's cross-signing key ID. + * + * @param {string} type The type of key to get the ID of. One of "master", + * "self_signing", or "user_signing". Defaults to "master". + * + * @returns {string} the key ID + */ +Crypto.prototype.getCrossSigningId = function(type) { + return this._crossSigningInfo.getId(type); +}; + /** * Check whether a given user is trusted. * diff --git a/src/crypto/verification/Base.js b/src/crypto/verification/Base.js index 5662fe348fc..cee051c8d24 100644 --- a/src/crypto/verification/Base.js +++ b/src/crypto/verification/Base.js @@ -22,6 +22,7 @@ limitations under the License. import {MatrixEvent} from '../../models/event'; import {EventEmitter} from 'events'; import logger from '../../logger'; +import DeviceInfo from '../deviceinfo'; export default class VerificationBase extends EventEmitter { /** @@ -192,11 +193,24 @@ export default class VerificationBase extends EventEmitter { for (const [keyId, keyInfo] of Object.entries(keys)) { const deviceId = keyId.split(':', 2)[1]; const device = await this._baseApis.getStoredDevice(userId, deviceId); - if (!device) { - logger.warn(`verification: Could not find device ${deviceId} to verify`); - } else { + if (device) { await verifier(keyId, device, keyInfo); verifiedDevices.push(deviceId); + } else { + const crossSigningInfo = this._baseApis._crypto._deviceList + .getStoredCrossSigningForUser(userId); + if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { + await verifier(keyId, DeviceInfo.fromStorage({ + keys: { + [keyId]: deviceId, + }, + }, deviceId), keyInfo); + verifiedDevices.push(deviceId); + } else { + logger.warn( + `verification: Could not find device ${deviceId} to verify`, + ); + } } } diff --git a/src/crypto/verification/SAS.js b/src/crypto/verification/SAS.js index 5889c56bef2..36359770454 100644 --- a/src/crypto/verification/SAS.js +++ b/src/crypto/verification/SAS.js @@ -354,19 +354,32 @@ export default class SAS extends Base { } _sendMAC(olmSAS, method) { - const keyId = `ed25519:${this._baseApis.deviceId}`; const mac = {}; + const keyList = []; const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this._baseApis.getUserId() + this._baseApis.deviceId + this.userId + this.deviceId + this.transactionId; - mac[keyId] = olmSAS[macMethods[method]]( + const deviceKeyId = `ed25519:${this._baseApis.deviceId}`; + mac[deviceKeyId] = olmSAS[macMethods[method]]( this._baseApis.getDeviceEd25519Key(), - baseInfo + keyId, + baseInfo + deviceKeyId, ); + keyList.push(deviceKeyId); + + const crossSigningId = this._baseApis.getCrossSigningId(); + if (crossSigningId) { + const crossSigningKeyId = `ed25519:${crossSigningId}`; + mac[crossSigningKeyId] = olmSAS[macMethods[method]]( + crossSigningId, + baseInfo + crossSigningKeyId, + ); + keyList.push(crossSigningKeyId); + } + const keys = olmSAS[macMethods[method]]( - keyId, + keyList.sort().join(","), baseInfo + "KEY_IDS", ); this._sendToDevice("m.key.verification.mac", { mac, keys }); From 98815ffdf61fc6eae6a018b750591d8ae088470a Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 12 Jun 2019 11:41:26 -0400 Subject: [PATCH 27/97] allow http request stub to ignore unhandled syncs --- spec/test-utils.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/spec/test-utils.js b/spec/test-utils.js index 57835536bcf..9a81906c343 100644 --- a/spec/test-utils.js +++ b/spec/test-utils.js @@ -245,10 +245,11 @@ module.exports.awaitDecryption = function(event) { const HttpResponse = module.exports.HttpResponse = function( - httpLookups, acceptKeepalives, + httpLookups, acceptKeepalives, ignoreUnhandledSync, ) { this.httpLookups = httpLookups; this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives; + this.ignoreUnhandledSync = ignoreUnhandledSync; this.pendingLookup = null; }; @@ -266,6 +267,10 @@ HttpResponse.prototype.request = function HttpResponse( logger.log(logLine); if (!next) { // no more things to return + if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) { + logger.log("MatrixClient[UT] Ignoring."); + return Promise.defer().promise; + } if (this.pendingLookup) { if (this.pendingLookup.method === method && this.pendingLookup.path === path) { @@ -313,6 +318,10 @@ HttpResponse.prototype.request = function HttpResponse( }); } return Promise.resolve(next.data); + } else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) { + logger.log("MatrixClient[UT] Ignoring."); + this.httpLookups.unshift(next); + return Promise.defer().promise; } expect(true).toBe(false, "Expected different request. " + logLine); return Promise.defer().promise; @@ -358,9 +367,11 @@ HttpResponse.defaultResponses = function(userId) { }; module.exports.setHttpResponses = function setHttpResponses( - client, responses, acceptKeepalives, + client, responses, acceptKeepalives, ignoreUnhandledSyncs, ) { - const httpResponseObj = new HttpResponse(responses, acceptKeepalives); + const httpResponseObj = new HttpResponse( + responses, acceptKeepalives, ignoreUnhandledSyncs, + ); const httpReq = httpResponseObj.request.bind(httpResponseObj); client._http = [ From 4c6fa890533798b759abe9c9bff203d5f158a131 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 12 Jun 2019 11:47:12 -0400 Subject: [PATCH 28/97] various cross-signing fixes and improvements --- spec/unit/crypto/cross-signing.spec.js | 60 ++++++++----- src/base-apis.js | 5 +- src/client.js | 55 ++++++------ src/crypto/CrossSigning.js | 26 +++++- src/crypto/DeviceList.js | 1 + src/crypto/Secrets.js | 68 ++++++++++++++- src/crypto/index.js | 115 +++++++++++++++++-------- 7 files changed, 239 insertions(+), 91 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 0f7e75c4e97..6251e6339be 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -50,11 +50,16 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); + alice.uploadDeviceSigningKeys = async function(e) {return;}; + alice.uploadKeySignatures = async function(e) {return;}; // set Alice's cross-signing key let privateKeys; alice.on("cross-signing:savePrivateKeys", function(e) { privateKeys = e; }); + alice.on("cross-signing:getKey", function(e) { + e.done(privateKeys[e.type]); + }); await alice.resetCrossSigningKeys(); // Alice downloads Bob's device key alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { @@ -69,10 +74,6 @@ describe("Cross Signing", function() { }, }); // Alice verifies Bob's key - alice.on("cross-signing:getKey", function(e) { - expect(e.type).toBe("user_signing"); - e.done(privateKeys.user_signing); - }); const promise = new Promise((resolve, reject) => { alice.uploadKeySignatures = (...args) => { resolve(...args); @@ -110,6 +111,10 @@ describe("Cross Signing", function() { alice.once("cross-signing:newKey", (e) => { e.done(masterKey); }); + alice.on("cross-signing:getKey", (e) => { + // will be called to sign our own device + e.done(selfSigningKey); + }); const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"] .Osborne2; @@ -198,8 +203,13 @@ describe("Cross Signing", function() { }, }, }, + { + method: "POST", + path: "/keys/signatures/upload", + data: {}, + }, ]; - setHttpResponses(alice, responses); + setHttpResponses(alice, responses, true, true); await alice.startClient(); @@ -213,11 +223,16 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); + alice.uploadDeviceSigningKeys = async function(e) {return;}; + alice.uploadKeySignatures = async function(e) {return;}; // set Alice's cross-signing key let privateKeys; alice.on("cross-signing:savePrivateKeys", function(e) { privateKeys = e; }); + alice.on("cross-signing:getKey", function(e) { + e.done(privateKeys[e.type]); + }); await alice.resetCrossSigningKeys(); // Alice downloads Bob's ssk and device key const bobMasterSigning = new global.Olm.PkSigning(); @@ -275,10 +290,6 @@ describe("Cross Signing", function() { expect(alice.checkUserTrust("@bob:example.com")).toBe(2); expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(2); // Alice verifies Bob's SSK - alice.on("cross-signing:getKey", function(e) { - expect(e.type).toBe("user_signing"); - e.done(privateKeys.user_signing); - }); alice.uploadKeySignatures = () => {}; await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); // Bob's device key should be trusted @@ -292,17 +303,18 @@ describe("Cross Signing", function() { ); alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com"); alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {}; + alice.uploadDeviceSigningKeys = async function(e) {return;}; + alice.uploadKeySignatures = async function(e) {return;}; // set Alice's cross-signing key let privateKeys; alice.on("cross-signing:savePrivateKeys", function(e) { privateKeys = e; }); - await alice.resetCrossSigningKeys(); - - alice.once("cross-signing:getKey", (e) => { + alice.on("cross-signing:getKey", (e) => { e.done(privateKeys[e.type]); }); + await alice.resetCrossSigningKeys(); const selfSigningKey = new Uint8Array([ 0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, @@ -450,11 +462,16 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); + alice.uploadDeviceSigningKeys = async function(e) {return;}; + alice.uploadKeySignatures = async function(e) {return;}; // set Alice's cross-signing key let privateKeys; alice.on("cross-signing:savePrivateKeys", function(e) { privateKeys = e; }); + alice.on("cross-signing:getKey", (e) => { + e.done(privateKeys[e.type]); + }); await alice.resetCrossSigningKeys(); // Alice downloads Bob's ssk and device key // (NOTE: device key is not signed by ssk) @@ -506,12 +523,9 @@ describe("Cross Signing", function() { // Bob's device key should be untrusted expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); // Alice verifies Bob's SSK - alice.on("cross-signing:getKey", function(e) { - expect(e.type).toBe("user_signing"); - e.done(privateKeys.user_signing); - }); - alice.uploadKeySignatures = () => {}; + console.log("verifying bob's device"); await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); + console.log("done"); // Bob's device key should be untrusted expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); }); @@ -520,10 +534,15 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); + alice.uploadDeviceSigningKeys = async function(e) {return;}; + alice.uploadKeySignatures = async function(e) {return;}; let privateKeys; alice.on("cross-signing:savePrivateKeys", function(e) { privateKeys = e; }); + alice.on("cross-signing:getKey", function(e) { + e.done(privateKeys[e.type]); + }); await alice.resetCrossSigningKeys(); // Alice downloads Bob's keys const bobMasterSigning = new global.Olm.PkSigning(); @@ -577,10 +596,6 @@ describe("Cross Signing", function() { Dynabook: bobDevice, }); // Alice verifies Bob's SSK - alice.on("cross-signing:getKey", function(e) { - expect(e.type).toBe("user_signing"); - e.done(privateKeys.user_signing); - }); alice.uploadKeySignatures = () => {}; await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); // Bob's device key should be trusted @@ -624,8 +639,7 @@ describe("Cross Signing", function() { expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); // Alice verifies Bob's SSK alice.on("cross-signing:getKey", function(e) { - expect(e.type).toBe("user_signing"); - e.done(privateKeys.user_signing); + e.done(privateKeys[e.type]); }); alice.uploadKeySignatures = () => {}; await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey2, true); diff --git a/src/base-apis.js b/src/base-apis.js index 602dd916480..646ad301ef8 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -1676,9 +1676,10 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) { ); }; -MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(keys) { +MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) { + const data = Object.assign({}, keys, {auth}); return this._http.authedRequestWithPrefix( - undefined, "POST", "/keys/device_signing/upload", undefined, keys, + undefined, "POST", "/keys/device_signing/upload", undefined, data, httpApi.PREFIX_UNSTABLE, ); }; diff --git a/src/client.js b/src/client.js index 0f64fb2fa8d..333e84fd490 100644 --- a/src/client.js +++ b/src/client.js @@ -559,6 +559,9 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.roomKeyRequest", "crypto.roomKeyRequestCancellation", "crypto.warning", + "crypto.devicesUpdated", + "cross-signing:savePrivateKeys", + "cross-signing:getKey", ]); logger.log("Crypto: initialising crypto object..."); @@ -795,27 +798,34 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() { }; /** - * returns a function that just calls the corresponding function from this._crypto. + * add methods that call the corresponding method in this._crypto * - * @param {string} name the function to call - * - * @return {Function} a wrapper function - */ -function wrapCryptoFunc(name) { - return function(...args) { - if (!this._crypto) { // eslint-disable-line no-invalid-this - throw new Error("End-to-end encryption disabled"); - } + * @param {class} MatrixClient the class to add the method to + * @param {string} names the names of the methods to call + */ +function wrapCryptoFuncs(MatrixClient, names) { + for (const name of names) { + MatrixClient.prototype[name] = function(...args) { + if (!this._crypto) { // eslint-disable-line no-invalid-this + throw new Error("End-to-end encryption disabled"); + } - return this._crypto[name](...args); // eslint-disable-line no-invalid-this - }; + return this._crypto[name](...args); // eslint-disable-line no-invalid-this + }; + } } -MatrixClient.prototype.checkUserTrust - = wrapCryptoFunc("checkUserTrust"); +wrapCryptoFuncs(MatrixClient, [ + "checkUserTrust", + "checkDeviceTrust", +]); -MatrixClient.prototype.checkDeviceTrust - = wrapCryptoFunc("checkDeviceTrust"); +wrapCryptoFuncs(MatrixClient, [ + "storeSecret", + "getSecret", + "isSecretStored", + "requestSecret", +]); /** * Get e2e information on the device that sent an event @@ -848,14 +858,11 @@ MatrixClient.prototype.isEventSenderVerified = async function(event) { return device.isVerified(); }; -MatrixClient.prototype.resetCrossSigningKeys - = wrapCryptoFunc("resetCrossSigningKeys"); - -MatrixClient.prototype.setCrossSigningKeys - = wrapCryptoFunc("setCrossSigningKeys"); - -MatrixClient.prototype.getCrossSigningId - = wrapCryptoFunc("getCrossSigningId"); +wrapCryptoFuncs(MatrixClient, [ + "resetCrossSigningKeys", + "getCrossSigningId", + "getStoredCrossSigningForUser", +]); /** * Cancel a room key request for this event if one is ongoing and resend the diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 197d371fd0a..02a87834a27 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -195,8 +195,13 @@ export class CrossSigningInfo extends EventEmitter { logger.error(error); throw new Error(error); } - // First-Use is true if and only if we had no previous key for the user - this.fu = !(this.keys.self_signing); + if (!this.keys.master) { + // this is the first key we've seen, so first-use is true + this.fu = true; + } else if (getPublicKey(keys.master)[1] !== this.getId()) { + // this is a different key, so first-use is false + this.fu = false; + } // otherwise, same key, so no change signingKeys.master = keys.master; } else if (this.keys.master) { signingKeys.master = this.keys.master; @@ -254,7 +259,11 @@ export class CrossSigningInfo extends EventEmitter { } async signUser(key) { + if (!this.keys.user_signing) { + return; + } const [pubkey, usk] = await getPrivateKey(this, "user_signing", (key) => { + // FIXME: return; }); try { @@ -268,9 +277,15 @@ export class CrossSigningInfo extends EventEmitter { async signDevice(userId, device) { if (userId !== this.userId) { - throw new Error("Urgh!"); + throw new Error( + `Trying to sign ${userId}'s device; can only sign our own device`, + ); + } + if (!this.keys.self_signing) { + return; } const [pubkey, ssk] = await getPrivateKey(this, "self_signing", (key) => { + // FIXME: return; }); try { @@ -288,6 +303,8 @@ export class CrossSigningInfo extends EventEmitter { } checkUserTrust(userCrossSigning) { + // if we're checking our own key, then it's trusted if the master key + // and self-signing key match if (this.userId === userCrossSigning.userId && this.getId() && this.getId() === userCrossSigning.getId() && this.getId("self_signing") @@ -298,7 +315,8 @@ export class CrossSigningInfo extends EventEmitter { } if (!this.keys.user_signing) { - return 0; + return (userCrossSigning.fu ? CrossSigningVerification.TOFU + : CrossSigningVerification.UNVERIFIED); } let userTrusted; diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index dee989538cb..a8c02c98acf 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -655,6 +655,7 @@ export default class DeviceList extends EventEmitter { } }); this.saveIfDirty(); + this.emit("crypto.devicesUpdated", users); }; return prom; diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 1184820a1f0..a17892c7581 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -17,6 +17,9 @@ limitations under the License. import {EventEmitter} from 'events'; import logger from '../logger'; import olmlib from './olmlib'; +import { randomString } from '../randomstring'; +import { keyForNewBackup } from './backup_password'; +import { encodeRecoveryKey, decodeRecoveryKey } from './recoverykey'; /** Implements MSC-1946 */ @@ -28,6 +31,56 @@ export default class SecretStorage extends EventEmitter { this._incomingRequests = {}; } + async addKey(type, opts) { + const keyData = { + algorithm: opts.algorithm, + }; + + switch (opts.algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + { + const decryption = new global.Olm.PkDecryption(); + try { + if (opts.passphrase) { + const key = await keyForNewBackup(opts.passphrase); + keyData.passphrase = { + algorithm: "m.pbkdf2", + iterations: key.iterations, + salt: key.salt, + }; + opts.encodedkey = encodeRecoveryKey(key.key); + keyData.pubkey = decryption.init_with_private_key(key.key); + } else if (opts.privkey) { + keyData.pubkey = decryption.init_with_private_key(opts.privkey); + opts.encodedkey = encodeRecoveryKey(opts.privkey); + } else { + keyData.pubkey = decryption.generate_key(); + opts.encodedkey = encodeRecoveryKey(decryption.get_private_key()); + } + } finally { + decryption.free(); + } + break; + } + default: + throw new Error(`Unknown key algorithm ${opts.algorithm}`); + } + + let keyName; + + do { + keyName = randomString(32); + } while (!this._baseApis.getAccountData(`m.secret_storage.key.${keyName}`)); + + // FIXME: sign keyData? + + await this._baseApis.setAccountData( + `m.secret_storage.key.${keyName}`, keyData, + ); + + return keyName; + } + /** store an encrypted secret on the server * * @param {string} name The name of the secret @@ -285,7 +338,11 @@ export default class SecretStorage extends EventEmitter { }); } } else if (content.action === "request") { - // if from us and device is trusted (or else check trust) + if (deviceId === this._baseApis.deviceId) { + // no point in trying to send ourself the secret + return; + } + // check if we have the secret logger.info("received request for secret (" + sender + ", " + deviceId + ", " + content.request_id + ")"); @@ -308,6 +365,15 @@ export default class SecretStorage extends EventEmitter { sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key, ciphertext: {}, }; + await olmlib.ensureOlmSessionsForDevices( + this._baseApis._crypto._olmDevice, + this._baseApis, + { + [sender]: [ + await this._baseApis.getStoredDevice(sender, deviceId), + ], + }, + ); await olmlib.encryptMessageForDevice( encryptedContent.ciphertext, this._baseApis.getUserId(), diff --git a/src/crypto/index.js b/src/crypto/index.js index 6edf07787d6..5bd8cec7119 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -25,6 +25,7 @@ limitations under the License. const anotherjson = require('another-json'); import Promise from 'bluebird'; import {EventEmitter} from 'events'; +import ReEmitter from '../ReEmitter'; import logger from '../logger'; const utils = require("../utils"); @@ -107,6 +108,7 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, clientStore, cryptoStore, roomList, verificationMethods) { this._onDeviceListUserCrossSigningUpdated = this._onDeviceListUserCrossSigningUpdated.bind(this); + this._reEmitter = new ReEmitter(this); this._baseApis = baseApis; this._sessionStore = sessionStore; this._userId = userId; @@ -148,6 +150,7 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, // XXX: This isn't removed at any point, but then none of the event listeners // this class sets seem to be removed at any point... :/ this._deviceList.on('userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated); + this._reEmitter.reEmit(this._deviceList, ["crypto.devicesUpdated"]); // the last time we did a check for the number of one-time-keys on the // server. @@ -200,18 +203,35 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, this._verificationTransactions = new Map(); this._crossSigningInfo = new CrossSigningInfo(userId); - this._crossSigningInfo.on("cross-signing:savePrivateKeys", (...args) => { - this._baseApis.emit("cross-signing:savePrivateKeys", ...args); - }); - this._crossSigningInfo.on("cross-signing:getKey", (...args) => { - this._baseApis.emit("cross-signing:getKey", ...args); - }); + this._reEmitter.reEmit(this._crossSigningInfo, [ + "cross-signing:savePrivateKeys", + "cross-signing:getKey", + ]); this._secretStorage = new SecretStorage(baseApis); // TODO: expose SecretStorage methods } utils.inherits(Crypto, EventEmitter); +Crypto.prototype.storeSecret = function(name, secret, keys) { + return this._secretStorage.store(name, secret, keys); +}; + +Crypto.prototype.getSecret = function(name) { + return this._secretStorage.get(name); +}; + +Crypto.prototype.isSecretStored = function(name, checkKey) { + return this._secretStorage.isStored(name, checkKey); +}; + +Crypto.prototype.requestSecret = function(name, devices) { + if (!devices) { + devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(this._userId)); + } + return this._secretStorage.request(name, devices); +}; + /** * Initialise the crypto module so that it is ready for use * @@ -271,19 +291,22 @@ Crypto.prototype.init = async function() { * keys will be created for the given level and below. Defaults to * regenerating all keys. */ -Crypto.prototype.resetCrossSigningKeys = async function(level) { +Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) { await this._crossSigningInfo.resetKeys(level); + const keys = {}; + for (const [name, key] of Object.entries(this._crossSigningInfo.keys)) { + keys[name + "_key"] = key; + } + await this._baseApis.uploadDeviceSigningKeys(authDict || {}, keys); this._baseApis.emit("cross-signing:keysChanged", {}); -}; -/** - * Set the user's cross-signing keys to use. - * - * @param {object} keys A mapping of key type to key data. - */ -Crypto.prototype.setCrossSigningKeys = function(keys) { - this._crossSigningInfo.setKeys(keys); - this._baseApis.emit("cross-signing:keysChanged", {}); + const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); + const signedDevice = await this._crossSigningInfo.signDevice(this._userId, device); + await this._baseApis.uploadKeySignatures({ + [this._userId]: { + [this._deviceId]: signedDevice, + }, + }); }; /** @@ -298,6 +321,10 @@ Crypto.prototype.getCrossSigningId = function(type) { return this._crossSigningInfo.getId(type); }; +Crypto.prototype.getStoredCrossSigningForUser = function(userId) { + return this._deviceList.getStoredCrossSigningForUser(userId); +}; + /** * Check whether a given user is trusted. * @@ -419,19 +446,29 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() { logger.info("Got private key"); } - // FIXME: fetch the private key? - if (this._crossSigningInfo.getId("self_signing") - !== newCrossSigning.getId("self_signing")) { + const oldSelfSigningId = this._crossSigningInfo.getId("self_signing"); + const oldUserSigningId = this._crossSigningInfo.getId("user_signing") + + this._crossSigningInfo.setKeys(newCrossSigning.keys); + // FIXME: save it ... somewhere? + + if (oldSelfSigningId !== newCrossSigning.getId("self_signing")) { logger.info("Got new self-signing key", newCrossSigning.getId("self_signing")); + + const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); + const signedDevice = await this._crossSigningInfo.signDevice( + this._userId, device, + ); + await this._baseApis.uploadKeySignatures({ + [this._userId]: { + [this._deviceId]: signedDevice, + }, + }); } - if (this._crossSigningInfo.getId("user_signing") - !== newCrossSigning.getId("user_signing")) { + if (oldUserSigningId !== newCrossSigning.getId("user_signing")) { logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); } - this._crossSigningInfo.setKeys(newCrossSigning.keys); - // FIXME: save it ... somewhere? - if (changed) { this._baseApis.emit("cross-signing:keysChanged", {}); } @@ -1062,12 +1099,13 @@ Crypto.prototype.setDeviceVerification = async function( if (xsk && xsk.getId() === deviceId) { if (verified) { const device = await this._crossSigningInfo.signUser(xsk); - // FIXME: mark xsk as dirty in device list - this._baseApis.uploadKeySignatures({ - [userId]: { - [deviceId]: device, - }, - }); + if (device) { + this._baseApis.uploadKeySignatures({ + [userId]: { + [deviceId]: device, + }, + }); + } return device; } else { // FIXME: ??? @@ -1109,13 +1147,16 @@ Crypto.prototype.setDeviceVerification = async function( // do cross-signing if (verified && userId === this._userId) { - const device = await this._crossSigningInfo.signDevice(userId, dev); - // FIXME: mark device as dirty in device list - this._baseApis.uploadKeySignatures({ - [userId]: { - [deviceId]: device, - }, - }); + const device = await this._crossSigningInfo.signDevice( + userId, DeviceInfo.fromStorage(dev, deviceId), + ); + if (device) { + this._baseApis.uploadKeySignatures({ + [userId]: { + [deviceId]: device, + }, + }); + } } return DeviceInfo.fromStorage(dev, deviceId); From 5bcbe76f2cb2660464ad99c2d97ba41d169d0076 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 14 Jun 2019 22:50:29 -0400 Subject: [PATCH 29/97] cleanups and a lot more docs --- src/client.js | 221 +++++++++++++++++++++++++++++++++++-- src/crypto/CrossSigning.js | 4 +- src/crypto/Secrets.js | 73 +++++++++--- src/crypto/index.js | 35 ++++-- 4 files changed, 298 insertions(+), 35 deletions(-) diff --git a/src/client.js b/src/client.js index 333e84fd490..8e64988d59a 100644 --- a/src/client.js +++ b/src/client.js @@ -560,8 +560,8 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.roomKeyRequestCancellation", "crypto.warning", "crypto.devicesUpdated", - "cross-signing:savePrivateKeys", - "cross-signing:getKey", + "cross-signing.savePrivateKeys", + "cross-signing.getKey", ]); logger.log("Crypto: initialising crypto object..."); @@ -815,12 +815,149 @@ function wrapCryptoFuncs(MatrixClient, names) { } } +/** + * Generate new cross-signing keys. + * + * @function module:client~MatrixClient#resetCrossSigningKeys + * @param {object} authDict Auth data to supply for User-Interactive auth. + * @param {CrossSigningLevel} [level] the level of cross-signing to reset. New + * keys will be created for the given level and below. Defaults to + * regenerating all keys. + */ + +/** + * Get the user's cross-signing key ID. + * + * @function module:client~MatrixClient#getCrossSigningId + * @param {string} [type=master] The type of key to get the ID of. One of + * "master", "self_signing", or "user_signing". Defaults to "master". + * + * @returns {string} the key ID + */ + +/** + * Get the cross signing information for a given user. + * + * @function module:client~MatrixClient#getStoredCrossSigningForUser + * @param {string} userId the user ID to get the cross-signing info for. + * + * @returns {CrossSigningInfo} the cross signing informmation for the user. + */ + +/** + * Check whether a given user is trusted. + * + * @function module:client~MatrixClient#checkUserTrust + * @param {string} userId The ID of the user to check. + * + * @returns {integer} a bit mask indicating how the user is trusted (if at all) + * - returnValue & 1: unused + * - returnValue & 2: trust-on-first-use cross-signing key + * - returnValue & 4: user's cross-signing key is verified + * + * TODO: is this a good way of representing it? Or we could return an object + * with different keys, or a set? The advantage of doing it this way is that + * you can define which methods you want to use, "&" with the appopriate mask, + * then test for truthiness. Or if you want to just trust everything, then use + * the value alone. However, I wonder if bit masks are too obscure... + */ + +/** + * Check whether a given device is trusted. + * + * @function module:client~MatrixClient#checkDeviceTrust + * @param {string} userId The ID of the user whose devices is to be checked. + * @param {string} deviceId The ID of the device to check + * + * @returns {integer} a bit mask indicating how the user is trusted (if at all) + * - returnValue & 1: device marked as verified + * - returnValue & 2: trust-on-first-use cross-signing key + * - returnValue & 4: user's cross-signing key is verified and device is signed + * + * TODO: see checkUserTrust + */ + wrapCryptoFuncs(MatrixClient, [ + "resetCrossSigningKeys", + "getCrossSigningId", + "getStoredCrossSigningForUser", "checkUserTrust", "checkDeviceTrust", ]); +/** + * Check if the sender of an event is verified + * + * @param {MatrixEvent} event event to be checked + * + * @returns {integer} a bit mask indicating how the user is trusted (if at all) + * - returnValue & 1: device marked as verified + * - returnValue & 2: trust-on-first-use cross-signing key + * - returnValue & 4: user's cross-signing key is verified + */ +MatrixClient.prototype.checkEventSenderTrust = async function(event) { + const device = await this.getEventSenderDeviceInfo(event); + if (!device) { + return 0; + } + return await this._crypto.checkDeviceTrust(event.getSender(), device.deviceId); +}; + +/** + * Add a key for encrypting secrets. + * + * @function module:client~MatrixClient#addSecretKey + * @param {string} algorithm the algorithm used by the key + * @param {object} opts the options for the algorithm. The properties used + * depend on the algorithm given. This object may be modified to pass + * information back about the key. + * @param {string} [keyName] the name of the key. If not given, a random + * name will be generated. + * + * @return {string} the name of the key + */ + +/** + * Store an encrypted secret on the server + * + * @function module:client~MatrixClient#storeSecret + * @param {string} name The name of the secret + * @param {string} secret The secret contents. + * @param {Array} keys The IDs of the keys to use to encrypt the secret + */ + +/** + * Get a secret from storage. + * + * @function module:client~MatrixClient#getSecret + * @param {string} name the name of the secret + * + * @return {string} the contents of the secret + */ + +/** + * Check if a secret is stored on the server. + * + * @function module:client~MatrixClient#isSecretStored + * @param {string} name the name of the secret + * @param {boolean} checkKey check if the secret is encrypted by a trusted + * key (currently unimplemented) + * + * @return {boolean} whether or not the secret is stored + */ + +/** + * Request a secret from another device. + * + * @function module:client~MatrixClient#requestSecret + * @param {string} name the name of the secret to request + * @param {string[]} devices the devices to request the secret from + * + * @return {string} the contents of the secret + */ + wrapCryptoFuncs(MatrixClient, [ + "addSecretKey", "storeSecret", "getSecret", "isSecretStored", @@ -858,12 +995,6 @@ MatrixClient.prototype.isEventSenderVerified = async function(event) { return device.isVerified(); }; -wrapCryptoFuncs(MatrixClient, [ - "resetCrossSigningKeys", - "getCrossSigningId", - "getStoredCrossSigningForUser", -]); - /** * Cancel a room key request for this event if one is ongoing and resend the * request. @@ -4712,6 +4843,80 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * perform the key verification */ +/** + * Fires when private keys for cross-signing need to be saved. + * @event module:client~MatrixClient#"cross-signing.savePrivateKeys" + * @param {object} keys the private keys to save. + * @param {UInt8Array} [keys.master] the private master key + * @param {UInt8Array} [keys.self_signing] the private user-signing key + * @param {UInt8Array} [keys.user_signing] the private self-signing key + */ + +/** + * Fires when a private key is needed. + * @event module:client~MatrixClient#"cross-signing.getKey" + * @param {object} data + * @param {string} data.type the type of key needed. Will be one of "master", + * "self_signing", or "user_signing" + * @param {Function} data.done a function to call with the private key as a + * `UInt8Array` + * @param {Function} data.cancel a function to call if the private key cannot + * be provided + * @param {string} [data.error] Error string to display to the user. Normally + * provided if a previously provided key was invalid, to re-prompt the + * user. + */ + +/** + * Fires when a new cross-signing key is provided from the server. The handler + * must verify the key by providing the private key for the given public key. + * @event module:client~MatrixClient#"cross-signing.newKey" + * @param {object} data + * @param {string} data.publicKey the public key received from the server + * @param {string} data.type the type of key that was received. Currently will + * only be "master". + * @param {Function} data.done a function to call with the private key + * corresponding to the given public key. + * @param {Function} data.cancel a function to call if the private key cannot be + * provided, indicating that the client does not accept the cross-signing key. + * @param {string} [data.error] Error string to display to the user. Normally + * provided if a previously provided key was invalid. + */ + +/** + * Fires when a secret has been requested by another client. Clients should + * ensure that the requesting device is allowed to have the secret. For + * example, if the device is not already trusted, a verification should be + * performed before sharing the secret. The client may also wish to prompt the + * user before sharing the secret. + * @event module:client~MatrixClient#"crypto.secrets.request" + * @param {object} data + * @param {string} data.name The name of the secret being requested. + * @param {string} data.user_id (string) The user ID of the client requesting + * the secret. In most cases, this shoud be the same as the client's user. + * @param {string} data.device_id The device ID of the client requesting the secret. + * @param {string} data.request_id The ID of the request. Used to match a + * corresponding `crypto.secrets.request_cancelled`. The request ID will be + * unique per sender, device pair. + * @param {int} data.device_trust: The trust status of the device requesting + * the secret. Will be a bit mask in the same form as returned by {@link + * module:client~MatrixClient#checkDeviceTrust}. + * @param {Function} data.send A function to call to send the secret to the + * requester + */ + +/** + * Fires when a secret request has been cancelled. If the client is prompting + * the user to ask whether they want to share a secret, the prompt can be + * dismissed. + * @event module:client~MatrixClient#"crypto.secrets.request_cancelled" + * @param {object} data + * @param {string} data.user_id The user ID of the client that had requested the secret. + * @param {string} data.device_id The device ID of the client that had requested the + * secret. + * @param {string} data.request_id The ID of the original request. + */ + // EventEmitter JSDocs /** diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 02a87834a27..bac3875469d 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -33,7 +33,7 @@ async function getPrivateKey(self, type, check) { let signing; do { [pubkey, signing] = await new Promise((resolve, reject) => { - self.emit("cross-signing:getKey", { + self.emit("cross-signing.getKey", { type: type, error, done: (key) => { @@ -178,7 +178,7 @@ export class CrossSigningInfo extends EventEmitter { } Object.assign(this.keys, keys); - this.emit("cross-signing:savePrivateKeys", privateKeys); + this.emit("cross-signing.savePrivateKeys", privateKeys); } finally { if (masterSigning) { masterSigning.free(); diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index a17892c7581..181048acbc4 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -21,7 +21,9 @@ import { randomString } from '../randomstring'; import { keyForNewBackup } from './backup_password'; import { encodeRecoveryKey, decodeRecoveryKey } from './recoverykey'; -/** Implements MSC-1946 +/** + * Implements secret storage and sharing (MSC-1946) + * @module crypto/Secrets */ export default class SecretStorage extends EventEmitter { constructor(baseApis) { @@ -31,12 +33,26 @@ export default class SecretStorage extends EventEmitter { this._incomingRequests = {}; } - async addKey(type, opts) { - const keyData = { - algorithm: opts.algorithm, - }; + /** + * Add a key for encrypting secrets. + * + * @param {string} algorithm the algorithm used by the key. + * @param {object} opts the options for the algorithm. The properties used + * depend on the algorithm given. This object may be modified to pass + * information back about the key. + * @param {string} [keyID] the ID of the key. If not given, a random + * ID will be generated. + * + * @return {string} the ID of the key + */ + async addKey(algorithm, opts, keyID) { + const keyData = {algorithm}; + + if (opts.name) { + keyData.name = opts.name; + } - switch (opts.algorithm) { + switch (algorithm) { case "m.secret_storage.v1.curve25519-aes-sha2": { const decryption = new global.Olm.PkDecryption(); @@ -66,22 +82,25 @@ export default class SecretStorage extends EventEmitter { throw new Error(`Unknown key algorithm ${opts.algorithm}`); } - let keyName; - - do { - keyName = randomString(32); - } while (!this._baseApis.getAccountData(`m.secret_storage.key.${keyName}`)); + if (!keyID) { + do { + keyID = randomString(32); + } while (!this._baseApis.getAccountData(`m.secret_storage.key.${keyID}`)); + } // FIXME: sign keyData? await this._baseApis.setAccountData( - `m.secret_storage.key.${keyName}`, keyData, + `m.secret_storage.key.${keyID}`, keyData, ); - return keyName; + return keyID; } - /** store an encrypted secret on the server + // TODO: need a function to get all the secret keys + + /** + * Store an encrypted secret on the server * * @param {string} name The name of the secret * @param {string} secret The secret contents. @@ -124,6 +143,13 @@ export default class SecretStorage extends EventEmitter { await this._baseApis.setAccountData(name, {encrypted}); } + /** + * Get a secret from storage. + * + * @param {string} name the name of the secret + * + * @return {string} the contents of the secret + */ async get(name) { const secretInfo = this._baseApis.getAccountData(name); if (!secretInfo) { @@ -225,6 +251,15 @@ export default class SecretStorage extends EventEmitter { } } + /** + * Check if a secret is stored on the server. + * + * @param {string} name the name of the secret + * @param {boolean} checkKey check if the secret is encrypted by a trusted + * key (currently unimplemented) + * + * @return {boolean} whether or not the secret is stored + */ isStored(name, checkKey) { // check if secret exists const secretInfo = this._baseApis.getAccountData(name); @@ -263,6 +298,14 @@ export default class SecretStorage extends EventEmitter { return false; } + /** + * Request a secret from another device + * + * @param {string} name the name of the secret to request + * @param {string[]} devices the devices to request the secret from + * + * @return {string} the contents of the secret + */ request(name, devices) { const requestId = this._baseApis.makeTxnId(); @@ -347,7 +390,7 @@ export default class SecretStorage extends EventEmitter { logger.info("received request for secret (" + sender + ", " + deviceId + ", " + content.request_id + ")"); this._baseApis.emit("crypto.secrets.request", { - sender: sender, + user_id: sender, device_id: deviceId, request_id: content.request_id, name: content.name, diff --git a/src/crypto/index.js b/src/crypto/index.js index 5bd8cec7119..cfcf142ee92 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -204,8 +204,8 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, this._crossSigningInfo = new CrossSigningInfo(userId); this._reEmitter.reEmit(this._crossSigningInfo, [ - "cross-signing:savePrivateKeys", - "cross-signing:getKey", + "cross-signing.savePrivateKeys", + "cross-signing.getKey", ]); this._secretStorage = new SecretStorage(baseApis); @@ -213,6 +213,10 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, } utils.inherits(Crypto, EventEmitter); +Crypto.prototype.addSecretKey = function(algorithm, opts, keyID) { + return this._secretStorage.store(algorithm, opts, keyID); +}; + Crypto.prototype.storeSecret = function(name, secret, keys) { return this._secretStorage.store(name, secret, keys); }; @@ -287,7 +291,8 @@ Crypto.prototype.init = async function() { /** * Generate new cross-signing keys. * - * @param {CrossSigningLevel} level the level of cross-signing to reset. New + * @param {object} authDict Auth data to supply for User-Interactive auth. + * @param {CrossSigningLevel} [level] the level of cross-signing to reset. New * keys will be created for the given level and below. Defaults to * regenerating all keys. */ @@ -298,7 +303,7 @@ Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) { keys[name + "_key"] = key; } await this._baseApis.uploadDeviceSigningKeys(authDict || {}, keys); - this._baseApis.emit("cross-signing:keysChanged", {}); + this._baseApis.emit("cross-signing.keysChanged", {}); const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); const signedDevice = await this._crossSigningInfo.signDevice(this._userId, device); @@ -312,8 +317,8 @@ Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) { /** * Get the user's cross-signing key ID. * - * @param {string} type The type of key to get the ID of. One of "master", - * "self_signing", or "user_signing". Defaults to "master". + * @param {string} [type=master] The type of key to get the ID of. One of + * "master", "self_signing", or "user_signing". Defaults to "master". * * @returns {string} the key ID */ @@ -321,6 +326,13 @@ Crypto.prototype.getCrossSigningId = function(type) { return this._crossSigningInfo.getId(type); }; +/** + * Get the cross signing information for a given user. + * + * @param {string} userId the user ID to get the cross-signing info for. + * + * @returns {CrossSigningInfo} the cross signing informmation for the user. + */ Crypto.prototype.getStoredCrossSigningForUser = function(userId) { return this._deviceList.getStoredCrossSigningForUser(userId); }; @@ -415,7 +427,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() { let error; do { privkey = await new Promise((resolve, reject) => { - this._baseApis.emit("cross-signing:newKey", { + this._baseApis.emit("cross-signing.newKey", { publicKey: seenPubkey, type: "master", error, @@ -436,18 +448,21 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() { resolve(key); }, cancel: (error) => { + // FIXME: should we forcibly push our copy of the key + // to the server if the client rejects the server's + // key? reject(error || new Error("Cancelled by user")); }, }); }); } while (!privkey); - this._baseApis.emit("cross-signing:savePrivateKeys", {master: privkey}); + this._baseApis.emit("cross-signing.savePrivateKeys", {master: privkey}); logger.info("Got private key"); } const oldSelfSigningId = this._crossSigningInfo.getId("self_signing"); - const oldUserSigningId = this._crossSigningInfo.getId("user_signing") + const oldUserSigningId = this._crossSigningInfo.getId("user_signing"); this._crossSigningInfo.setKeys(newCrossSigning.keys); // FIXME: save it ... somewhere? @@ -470,7 +485,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() { } if (changed) { - this._baseApis.emit("cross-signing:keysChanged", {}); + this._baseApis.emit("cross-signing.keysChanged", {}); } // FIXME: From 1cae5e8b978a85e06f6f830cf404c4d3bbf8f9af Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 27 Jun 2019 23:33:07 -0400 Subject: [PATCH 30/97] fix unit tests to match event name changes --- spec/unit/crypto/cross-signing.spec.js | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 6251e6339be..2d344615bda 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -54,10 +54,10 @@ describe("Cross Signing", function() { alice.uploadKeySignatures = async function(e) {return;}; // set Alice's cross-signing key let privateKeys; - alice.on("cross-signing:savePrivateKeys", function(e) { + alice.on("cross-signing.savePrivateKeys", function(e) { privateKeys = e; }); - alice.on("cross-signing:getKey", function(e) { + alice.on("cross-signing.getKey", function(e) { e.done(privateKeys[e.type]); }); await alice.resetCrossSigningKeys(); @@ -103,15 +103,15 @@ describe("Cross Signing", function() { ]); const keyChangePromise = new Promise((resolve, reject) => { - alice.once("cross-signing:keysChanged", (e) => { + alice.once("cross-signing.keysChanged", (e) => { resolve(e); }); }); - alice.once("cross-signing:newKey", (e) => { + alice.once("cross-signing.newKey", (e) => { e.done(masterKey); }); - alice.on("cross-signing:getKey", (e) => { + alice.on("cross-signing.getKey", (e) => { // will be called to sign our own device e.done(selfSigningKey); }); @@ -227,10 +227,10 @@ describe("Cross Signing", function() { alice.uploadKeySignatures = async function(e) {return;}; // set Alice's cross-signing key let privateKeys; - alice.on("cross-signing:savePrivateKeys", function(e) { + alice.on("cross-signing.savePrivateKeys", function(e) { privateKeys = e; }); - alice.on("cross-signing:getKey", function(e) { + alice.on("cross-signing.getKey", function(e) { e.done(privateKeys[e.type]); }); await alice.resetCrossSigningKeys(); @@ -308,10 +308,10 @@ describe("Cross Signing", function() { // set Alice's cross-signing key let privateKeys; - alice.on("cross-signing:savePrivateKeys", function(e) { + alice.on("cross-signing.savePrivateKeys", function(e) { privateKeys = e; }); - alice.on("cross-signing:getKey", (e) => { + alice.on("cross-signing.getKey", (e) => { e.done(privateKeys[e.type]); }); await alice.resetCrossSigningKeys(); @@ -466,10 +466,10 @@ describe("Cross Signing", function() { alice.uploadKeySignatures = async function(e) {return;}; // set Alice's cross-signing key let privateKeys; - alice.on("cross-signing:savePrivateKeys", function(e) { + alice.on("cross-signing.savePrivateKeys", function(e) { privateKeys = e; }); - alice.on("cross-signing:getKey", (e) => { + alice.on("cross-signing.getKey", (e) => { e.done(privateKeys[e.type]); }); await alice.resetCrossSigningKeys(); @@ -537,10 +537,10 @@ describe("Cross Signing", function() { alice.uploadDeviceSigningKeys = async function(e) {return;}; alice.uploadKeySignatures = async function(e) {return;}; let privateKeys; - alice.on("cross-signing:savePrivateKeys", function(e) { + alice.on("cross-signing.savePrivateKeys", function(e) { privateKeys = e; }); - alice.on("cross-signing:getKey", function(e) { + alice.on("cross-signing.getKey", function(e) { e.done(privateKeys[e.type]); }); await alice.resetCrossSigningKeys(); @@ -638,7 +638,7 @@ describe("Cross Signing", function() { expect(alice.checkUserTrust("@bob:example.com")).toBe(0); expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); // Alice verifies Bob's SSK - alice.on("cross-signing:getKey", function(e) { + alice.on("cross-signing.getKey", function(e) { e.done(privateKeys[e.type]); }); alice.uploadKeySignatures = () => {}; From 4356603665e16e9c01cce7d863fc234059bc9ed5 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 27 Jun 2019 23:37:57 -0400 Subject: [PATCH 31/97] save public part of cross-signing keys --- src/crypto/index.js | 45 +++++++++++-------- .../store/indexeddb-crypto-store-backend.js | 8 ++-- src/crypto/store/indexeddb-crypto-store.js | 14 +++--- src/crypto/store/localStorage-crypto-store.js | 10 ++--- src/crypto/store/memory-crypto-store.js | 10 ++--- 5 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index b29e7705316..845a7d1bce8 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -280,6 +280,17 @@ Crypto.prototype.init = async function() { ); this._deviceList.saveIfDirty(); } + + await this._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.getCrossSigningKeys(txn, (keys) => { + if (keys) { + this._crossSigningInfo.setKeys(keys); + } + }); + }, + ); // make sure we are keeping track of our own devices // (this is important for key backups & things) this._deviceList.startTrackingDeviceList(this._userId); @@ -298,6 +309,14 @@ Crypto.prototype.init = async function() { */ Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) { await this._crossSigningInfo.resetKeys(level); + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys); + }, + ); + + // send keys to server const keys = {}; for (const [name, key] of Object.entries(this._crossSigningInfo.keys)) { keys[name + "_key"] = key; @@ -305,6 +324,7 @@ Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) { await this._baseApis.uploadDeviceSigningKeys(authDict || {}, keys); this._baseApis.emit("cross-signing.keysChanged", {}); + // sign the current device with the new key, and upload to the server const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); const signedDevice = await this._crossSigningInfo.signDevice(this._userId, device); await this._baseApis.uploadKeySignatures({ @@ -465,7 +485,12 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() { const oldUserSigningId = this._crossSigningInfo.getId("user_signing"); this._crossSigningInfo.setKeys(newCrossSigning.keys); - // FIXME: save it ... somewhere? + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys); + }, + ); if (oldSelfSigningId !== newCrossSigning.getId("self_signing")) { logger.info("Got new self-signing key", newCrossSigning.getId("self_signing")); @@ -820,24 +845,6 @@ Crypto.prototype.uploadDeviceKeys = function() { let accountKeys; return crypto._signObject(deviceKeys).then(() => { - return this._cryptoStore.doTxn( - 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - this._cryptoStore.getAccountKeys(txn, keys => { - accountKeys = keys; - }); - }, - ); - }).then(() => { - if (accountKeys && accountKeys.self_signing_key_seed) { - // if we have an SSK, sign the key with the SSK too - pkSign( - deviceKeys, - Buffer.from(accountKeys.self_signing_key_seed, 'base64'), - userId, - ); - } - return crypto._baseApis.uploadKeysRequest({ device_keys: deviceKeys, }, { diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index f6b4774c847..76f06688795 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -332,9 +332,9 @@ export class Backend { objectStore.put(newData, "-"); } - getAccountKeys(txn, func) { + getCrossSigningKeys(txn, func) { const objectStore = txn.objectStore("account"); - const getReq = objectStore.get("keys"); + const getReq = objectStore.get("crossSigningKeys"); getReq.onsuccess = function() { try { func(getReq.result || null); @@ -344,9 +344,9 @@ export class Backend { }; } - storeAccountKeys(txn, keys) { + storeCrossSigningKeys(txn, keys) { const objectStore = txn.objectStore("account"); - objectStore.put(keys, "keys"); + objectStore.put(keys, "crossSigningKeys"); } // Olm Sessions diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 3a1c77912b1..15492204ff1 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -302,25 +302,25 @@ export default class IndexedDBCryptoStore { } /** - * Get the account keys for cross-signing (eg. self-signing key, + * Get the public part of the cross-signing keys (eg. self-signing key, * user signing key). * * @param {*} txn An active transaction. See doTxn(). * @param {function(string)} func Called with the account keys object: * { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed */ - getAccountKeys(txn, func) { - this._backendPromise.value().getAccountKeys(txn, func); + getCrossSigningKeys(txn, func) { + this._backendPromise.value().getCrossSigningKeys(txn, func); } /** - * Write the account keys back to the store + * Write the cross-siging keys back to the store * * @param {*} txn An active transaction. See doTxn(). - * @param {string} keys Account keys object as getAccountKeys() + * @param {string} keys keys object as getCrossSigningKeys() */ - storeAccountKeys(txn, keys) { - this._backendPromise.value().storeAccountKeys(txn, keys); + storeCrossSigningKeys(txn, keys) { + this._backendPromise.value().storeCrossSigningKeys(txn, keys); } // Olm sessions diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 0163c0c9581..fd7934bb119 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -31,7 +31,7 @@ import MemoryCryptoStore from './memory-crypto-store.js'; const E2E_PREFIX = "crypto."; const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; -const KEY_END_TO_END_ACCOUNT_KEYS = E2E_PREFIX + "account_keys"; +const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys"; const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; @@ -285,14 +285,14 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { ); } - getAccountKeys(txn, func) { - const keys = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT_KEYS); + getCrossSigningKeys(txn, func) { + const keys = getJsonItem(this.store, KEY_CROSS_SIGNING_KEYS); func(keys); } - storeAccountKeys(txn, keys) { + storeCrossSigningKeys(txn, keys) { setJsonItem( - this.store, KEY_END_TO_END_ACCOUNT_KEYS, keys, + this.store, KEY_CROSS_SIGNING_KEYS, keys, ); } diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index fd2205c9fa1..2897be81e7e 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -33,7 +33,7 @@ export default class MemoryCryptoStore { constructor() { this._outgoingRoomKeyRequests = []; this._account = null; - this._accountKeys = null; + this._crossSigningKeys = null; // Map of {devicekey -> {sessionId -> session pickle}} this._sessions = {}; @@ -235,12 +235,12 @@ export default class MemoryCryptoStore { this._account = newData; } - getAccountKeys(txn, func) { - func(this._accountKeys); + getCrossSigningKeys(txn, func) { + func(this._crossSigningKeys); } - storeAccountKeys(txn, keys) { - this._accountKeys = keys; + storeCrossSigningKeys(txn, keys) { + this._crossSigningKeys = keys; } // Olm Sessions From c5caf8f8f41ebde92cb0527ca32ae770ef09ecec Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 3 Jul 2019 15:15:41 -0400 Subject: [PATCH 32/97] sign backups with master key --- src/client.js | 194 +++++-------------------------------- src/crypto/CrossSigning.js | 55 ++++------- src/crypto/index.js | 113 +++------------------ src/crypto/sskinfo.js | 80 --------------- 4 files changed, 58 insertions(+), 384 deletions(-) delete mode 100644 src/crypto/sskinfo.js diff --git a/src/client.js b/src/client.js index fa3543a5210..8d9c8f4049c 100644 --- a/src/client.js +++ b/src/client.js @@ -1209,16 +1209,8 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { throw new Error("End-to-end encryption disabled"); } - let decryption; - let encryption; - let signing; + const decryption = new global.Olm.PkDecryption(); try { - decryption = new global.Olm.PkDecryption(); - encryption = new global.Olm.PkEncryption(); - if (global.Olm.PkSigning) { - signing = new global.Olm.PkSigning(); - } - let publicKey; const authData = {}; if (password) { @@ -1231,64 +1223,15 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { } authData.public_key = publicKey; - encryption.set_recipient_key(publicKey); - const returnInfo = { + return { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, auth_data: authData, recovery_key: encodeRecoveryKey(decryption.get_private_key()), accountKeys: null, }; - - if (signing) { - await this._cryptoStore.doTxn( - 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - this._cryptoStore.getAccountKeys(txn, (keys) => { - returnInfo.accountKeys = keys; - }); - }, - ); - - if (!returnInfo.accountKeys) { - const sskSeed = signing.generate_seed(); - const uskSeed = signing.generate_seed(); - - returnInfo.accountKeys = { - self_signing_key_seed: Buffer.from(sskSeed).toString('base64'), - user_signing_key_seed: Buffer.from(uskSeed).toString('base64'), - }; - } - - // put the encrypted version of the seed in the auth data to upload - // XXX: our encryption really should support encrypting binary data. - authData.self_signing_key_seed = encryption.encrypt( - returnInfo.accountKeys.self_signing_key_seed, - ); - // also keep the public part there - returnInfo.ssk_public = signing.init_with_seed( - Buffer.from(returnInfo.accountKeys.self_signing_key_seed, 'base64'), - ); - signing.free(); - - // same for the USK - authData.user_signing_key_seed = encryption.encrypt( - returnInfo.accountKeys.user_signing_key_seed, - ); - returnInfo.usk_public = signing.init_with_seed( - Buffer.from(returnInfo.accountKeys.user_signing_key_seed, 'base64'), - ); - signing.free(); - - // we don't save these keys back to the store yet: we'll do that when (if) we - // actually create the backup - } - - return returnInfo; } finally { - if (decryption) decryption.free(); - if (encryption) encryption.free(); - if (signing) signing.free(); + decryption.free(); } }; @@ -1297,11 +1240,9 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { * from prepareKeyBackupVersion. * * @param {object} info Info object from prepareKeyBackupVersion - * @param {object} auth Auth object for UI auth - * @param {string} replacesSsk If the SSK is being replaced, the ID of the old key * @returns {Promise} Object with 'version' param indicating the version created */ -MatrixClient.prototype.createKeyBackupVersion = async function(info, auth, replacesSsk) { +MatrixClient.prototype.createKeyBackupVersion = async function(info) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } @@ -1311,73 +1252,25 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info, auth, repla auth_data: info.auth_data, }; - const uskInfo = { - user_id: this.credentials.userId, - usage: ['user_signing'], - keys: { - ['ed25519:' + info.usk_public]: info.usk_public, - }, - }; - - // sign the USK with the SSK - pkSign( - uskInfo, - Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'), - this.credentials.userId, - ); - // Now sig the backup auth data. Do it as this device first because crypto._signObject // is dumb and bluntly replaces the whole signatures block... // this can probably go away very soon in favour of just signing with the SSK. await this._crypto._signObject(data.auth_data); - // now also sign the auth data with the SSK - pkSign( - data.auth_data, - Buffer.from(info.accountKeys.self_signing_key_seed, 'base64'), - this.credentials.userId, - ); - - const keys = { - self_signing_key: { - user_id: this.credentials.userId, - usage: ['self_signing'], - keys: { - ['ed25519:' + info.ssk_public]: info.ssk_public, - }, - replaces: replacesSsk, - }, - user_signing_key: uskInfo, - auth, - }; + if (this._crypto._crossSigningInfo.getId()) { + // now also sign the auth data with the SSK + await this._crypto._crossSigningInfo.signObject(data.auth_data, "master"); + } - return this._cryptoStore.doTxn( - 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - // store the newly generated account keys - this._cryptoStore.storeAccountKeys(txn, info.accountKeys); - }, - ).then(() => { - // re-check the SSK in the device store if necessary - return this._crypto.checkOwnSskTrust(); - }).then(() => { - // upload the public part of the account keys - return this.uploadDeviceSigningKeys(keys); - }).then(() => { - return this._http.authedRequest( - undefined, "POST", "/room_keys/version", undefined, data, - ); - }).then((res) => { - this.enableKeyBackup({ - algorithm: info.algorithm, - auth_data: info.auth_data, - version: res.version, - }); - return res; - }).then(() => { - // upload signatures between the SSK & this device - return this._crypto.uploadDeviceKeySignatures(); + const res = await this._http.authedRequest( + undefined, "POST", "/room_keys/version", undefined, data, + ); + this.enableKeyBackup({ + algorithm: info.algorithm, + auth_data: info.auth_data, + version: res.version, }); + return res; }; MatrixClient.prototype.deleteKeyBackupVersion = function(version) { @@ -1483,7 +1376,7 @@ MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function( ); }; -MatrixClient.prototype._restoreKeyBackup = async function( +MatrixClient.prototype._restoreKeyBackup = function( privKey, targetRoomId, targetSessionId, backupInfo, ) { if (this._crypto === null) { @@ -1492,46 +1385,14 @@ MatrixClient.prototype._restoreKeyBackup = async function( let totalKeyCount = 0; let keys = []; + const path = this._makeKeyBackupPath( + targetRoomId, targetSessionId, backupInfo.version, + ); + const decryption = new global.Olm.PkDecryption(); let backupPubKey; try { backupPubKey = decryption.init_with_private_key(privKey); - - // decrypt the account keys from the backup info if there are any - // fetch the old ones first so we don't lose info if only one of them is in the backup - let accountKeys; - await this._cryptoStore.doTxn( - 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - this._cryptoStore.getAccountKeys(txn, (keys) => { - accountKeys = keys || {}; - }); - }, - ); - - if (backupInfo.auth_data.self_signing_key_seed) { - accountKeys.self_signing_key_seed = decryption.decrypt( - backupInfo.auth_data.self_signing_key_seed.ephemeral, - backupInfo.auth_data.self_signing_key_seed.mac, - backupInfo.auth_data.self_signing_key_seed.ciphertext, - ); - } - if (backupInfo.auth_data.user_signing_key_seed) { - accountKeys.user_signing_key_seed = decryption.decrypt( - backupInfo.auth_data.user_signing_key_seed.ephemeral, - backupInfo.auth_data.user_signing_key_seed.mac, - backupInfo.auth_data.user_signing_key_seed.ciphertext, - ); - } - - await this._cryptoStore.doTxn( - 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - this._cryptoStore.storeAccountKeys(txn, accountKeys); - }, - ); - - await this._crypto.checkOwnSskTrust(); } catch(e) { decryption.free(); throw e; @@ -1544,16 +1405,9 @@ MatrixClient.prototype._restoreKeyBackup = async function( return Promise.reject({errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY}); } - // start by signing this device from the SSK now we have it - return this._crypto.uploadDeviceKeySignatures().then(() => { - // Now fetch the encrypted keys - const path = this._makeKeyBackupPath( - targetRoomId, targetSessionId, backupInfo.version, - ); - return this._http.authedRequest( - undefined, "GET", path.path, path.queryData, - ); - }).then((res) => { + return this._http.authedRequest( + undefined, "GET", path.path, path.queryData, + ).then((res) => { if (res.rooms) { for (const [roomId, roomData] of Object.entries(res.rooms)) { if (!roomData.sessions) continue; diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index bac3875469d..1652d5a79e4 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -40,13 +40,15 @@ async function getPrivateKey(self, type, check) { // FIXME: the key needs to be interpreted? const signing = new global.Olm.PkSigning(); const pubkey = signing.init_with_seed(key); - error = check(pubkey, signing); - if (error) { + // make sure it agrees with the pubkey that we have + if (pubkey !== getPublicKey(self.keys[type])[1]) { + error = "Key does not match"; logger.error(error); signing.free(); resolve([null, null]); + } else { + resolve([pubkey, signing]); } - resolve([pubkey, signing]); }, cancel: (error) => { reject(error || new Error("Cancelled")); @@ -131,14 +133,7 @@ export class CrossSigningInfo extends EventEmitter { }, }; } else { - [masterPub, masterSigning] = await getPrivateKey( - this, "master", (pubkey) => { - // make sure it agrees with the pubkey that we have - if (pubkey !== getPublicKey(this.keys.master)[1]) { - return "Key does not match"; - } - return; - }); + [masterPub, masterSigning] = await getPrivateKey(this, "master"); } if (level & CrossSigningLevel.SELF_SIGNING) { @@ -258,21 +253,21 @@ export class CrossSigningInfo extends EventEmitter { } } + async signObject(data, type) { + const [pubkey, signing] = await getPrivateKey(this, type); + try { + pkSign(data, signing, this.userId, pubkey); + return data; + } finally { + signing.free(); + } + } + async signUser(key) { if (!this.keys.user_signing) { return; } - const [pubkey, usk] = await getPrivateKey(this, "user_signing", (key) => { - // FIXME: - return; - }); - try { - const otherMaster = key.keys.master; - pkSign(otherMaster, usk, this.userId, pubkey); - return otherMaster; - } finally { - usk.free(); - } + return this.signObject(key.keys.master, "user_signing"); } async signDevice(userId, device) { @@ -284,22 +279,14 @@ export class CrossSigningInfo extends EventEmitter { if (!this.keys.self_signing) { return; } - const [pubkey, ssk] = await getPrivateKey(this, "self_signing", (key) => { - // FIXME: - return; - }); - try { - const keyObj = { + return this.signObject( + { algorithms: device.algorithms, keys: device.keys, device_id: device.deviceId, user_id: userId, - }; - pkSign(keyObj, ssk, this.userId, pubkey); - return keyObj; - } finally { - ssk.free(); - } + }, "self_signing", + ); } checkUserTrust(userCrossSigning) { diff --git a/src/crypto/index.js b/src/crypto/index.js index 845a7d1bce8..f07e1624291 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -513,52 +513,10 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() { this._baseApis.emit("cross-signing.keysChanged", {}); } - // FIXME: - // Now dig out the account keys and get the pubkey of the one in there - /* - let accountKeys = null; - await this._cryptoStore.doTxn( - 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - this._cryptoStore.getAccountKeys(txn, keys => { - accountKeys = keys; - }); - }, - ); - if (!accountKeys || !accountKeys.self_signing_key_seed) { - logger.info( - "Ignoring new self-signing key for us because we have no private part stored", - ); - return; - } - let signing; - let localPubkey; - try { - signing = new global.Olm.PkSigning(); - localPubkey = signing.init_with_seed( - Buffer.from(accountKeys.self_signing_key_seed, 'base64'), - ); - } finally { - if (signing) signing.free(); - signing = null; - } - if (!localPubkey) { - logger.error("Unable to compute public key for stored SSK seed"); - } - - // Finally, are they the same? - if (seenPubkey === localPubkey) { - logger.info("Published self-signing key matches local copy: marking as verified"); - this.setSskVerification(userId, SskInfo.SskVerification.VERIFIED); - // Now we may be able to trust our key backup - await this.checkKeyBackup(); - } else { - logger.info( - "Published self-signing key DOES NOT match local copy! Local: " + - localPubkey + ", published: " + seenPubkey, - ); - } - */ + // Now we may be able to trust our key backup + await this.checkKeyBackup(); + // FIXME: if we previously trusted the backup, should we automatically sign + // the backup with the new key (if not already signed)? }; /** @@ -686,21 +644,23 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { // Could be an SSK but just say this is the device ID for backwards compat const sigInfo = { deviceId: keyIdParts[1] }; // XXX: is this how we're supposed to get the device ID? - // first check to see if it's from our SSK - const ssk = this._deviceList.getStoredSskForUser(this._userId); - if (ssk && ssk.getKeyId() === keyId) { - sigInfo.self_signing_key = ssk; + // first check to see if it's from our cross-signing key + const crossSigningId = this._crossSigningInfo.getId(); + if (crossSigningId === keyId) { + sigInfo.cross_signing_key = crossSigningId; try { await olmlib.verifySignature( this._olmDevice, backupInfo.auth_data, this._userId, sigInfo.deviceId, - ssk.getFingerprint(), + crossSigningId, ); sigInfo.valid = true; } catch (e) { - console.log("Bad signature from ssk " + ssk.getKeyId(), e); + logger.warning( + "Bad signature from cross signing key " + crossSigningId, e, + ); sigInfo.valid = false; } ret.sigs.push(sigInfo); @@ -745,7 +705,7 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { return ( s.valid && ( (s.device && s.device.isVerified()) || - (s.self_signing_key && s.self_signing_key.isVerified()) + (s.cross_signing_key) ) ); }); @@ -843,7 +803,6 @@ Crypto.prototype.uploadDeviceKeys = function() { user_id: userId, }; - let accountKeys; return crypto._signObject(deviceKeys).then(() => { return crypto._baseApis.uploadKeysRequest({ device_keys: deviceKeys, @@ -855,52 +814,6 @@ Crypto.prototype.uploadDeviceKeys = function() { }); }; -/** - * If a self-signing key is available, uploads the signature of this device from - * the self-signing key - * - * @return {bool} Promise: True if signatures were uploaded or otherwise false - * (eg. if no account keys were available) - */ -Crypto.prototype.uploadDeviceKeySignatures = async function() { - const crypto = this; - const userId = crypto._userId; - const deviceId = crypto._deviceId; - - const thisDeviceKey = { - algorithms: crypto._supportedAlgorithms, - device_id: deviceId, - keys: crypto._deviceKeys, - user_id: userId, - }; - let accountKeys; - await this._cryptoStore.doTxn( - 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], - (txn) => { - this._cryptoStore.getAccountKeys(txn, keys => { - accountKeys = keys; - }, - ); - }); - if (!accountKeys || !accountKeys.self_signing_key_seed) return false; - - // Sign this device with the SSK - pkSign( - thisDeviceKey, - Buffer.from(accountKeys.self_signing_key_seed, 'base64'), - userId, - ); - - const content = { - [userId]: { - [deviceId]: thisDeviceKey, - }, - }; - - await crypto._baseApis.uploadKeySignatures(content); - return true; -}; - /** * Stores the current one_time_key count which will be handled later (in a call of * onSyncCompleted). The count is e.g. coming from a /sync response. diff --git a/src/crypto/sskinfo.js b/src/crypto/sskinfo.js deleted file mode 100644 index 1f578247f0f..00000000000 --- a/src/crypto/sskinfo.js +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2019 New Vector Ltd - -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. -*/ - - -/** - * @module crypto/sskinfo - */ - -/** - * Information about a user's self-signing key - * - * @constructor - * @alias module:crypto/sskinfo - * - * @property {Object.} keys a map from - * <key type>:<id> -> <base64-encoded key>> - * - * @property {module:crypto/sskinfo.SskVerification} verified - * whether the device has been verified/blocked by the user - * - * @property {boolean} known - * whether the user knows of this device's existence (useful when warning - * the user that a user has added new devices) - * - * @property {Object} unsigned additional data from the homeserver - */ -export default class SskInfo { - constructor() { - this.keys = {}; - this.verified = SskInfo.SskVerification.UNVERIFIED; - //this.known = false; // is this useful? - this.unsigned = {}; - } - - /** - * @enum - */ - static SskVerification = { - VERIFIED: 1, - UNVERIFIED: 0, - BLOCKED: -1, - }; - - static fromStorage(obj) { - const res = new SskInfo(); - for (const [prop, val] of Object.entries(obj)) { - res[prop] = val; - } - return res; - } - - getKeyId() { - return Object.keys(this.keys)[0]; - } - - getFingerprint() { - return Object.values(this.keys)[0]; - } - - isVerified() { - return this.verified == SskInfo.SskVerification.VERIFIED; - } - - isUnverified() { - return this.verified == SskInfo.SskVerification.UNVERIFIED; - } -} From 46a848624542f46c34c9744cfd37c3cc6d84f5f9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 3 Jul 2019 15:15:56 -0400 Subject: [PATCH 33/97] rename m.secrets.share to m.secrets.send to agree with latest MSC --- src/crypto/Secrets.js | 2 +- src/crypto/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 181048acbc4..9ea5048b8e7 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -397,7 +397,7 @@ export default class SecretStorage extends EventEmitter { device_trust: this._baseApis.checkDeviceTrust(sender, deviceId), send: async (secret) => { const payload = { - type: "m.secret.share", + type: "m.secret.send", content: { request_id: content.request_id, secret: secret, diff --git a/src/crypto/index.js b/src/crypto/index.js index f07e1624291..418034cbd1c 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1946,7 +1946,7 @@ Crypto.prototype._onToDeviceEvent = function(event) { this._onRoomKeyRequestEvent(event); } else if (event.getType() === "m.secret.request") { this._secretStorage._onRequestReceived(event); - } else if (event.getType() === "m.secret.share") { + } else if (event.getType() === "m.secret.send") { this._secretStorage._onSecretReceived(event); } else if (event.getType() === "m.key.verification.request") { this._onKeyVerificationRequest(event); From 6cd09c6af20054463503c4072ab3519c393db53f Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 3 Jul 2019 16:00:44 -0400 Subject: [PATCH 34/97] pksign was moved to olmlib --- src/client.js | 1 - src/crypto/PkSigning.js | 43 ----------------------------------------- src/crypto/index.js | 2 -- 3 files changed, 46 deletions(-) delete mode 100644 src/crypto/PkSigning.js diff --git a/src/client.js b/src/client.js index 8d9c8f4049c..f79b38bb21b 100644 --- a/src/client.js +++ b/src/client.js @@ -52,7 +52,6 @@ import { isCryptoAvailable } from './crypto'; import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password'; import { randomString } from './randomstring'; -import { pkSign } from './crypto/PkSigning'; import IndexedDBCryptoStore from './crypto/store/indexeddb-crypto-store'; diff --git a/src/crypto/PkSigning.js b/src/crypto/PkSigning.js deleted file mode 100644 index 35703935781..00000000000 --- a/src/crypto/PkSigning.js +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2019 New Vector Ltd - -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. -*/ - -const anotherjson = require('another-json'); - -/** - * Higher level wrapper around olm.PkSigning that signs JSON objects - * @param {Object} obj Object to sign - * @param {Uint8Array} seed The private key seed (32 bytes) - * @param {string} userId The user ID who owns the signing key - */ -export function pkSign(obj, seed, userId) { - const signing = new global.Olm.PkSigning(); - try { - const pubkey = signing.init_with_seed(seed); - const sigs = obj.signatures || {}; - const mysigs = sigs[userId] || {}; - sigs[userId] = mysigs; - - delete obj.signatures; - const unsigned = obj.unsigned; - if (obj.unsigned) delete obj.unsigned; - - mysigs['ed25519:' + pubkey] = signing.sign(anotherjson.stringify(obj)); - obj.signatures = sigs; - if (unsigned) obj.unsigned = unsigned; - } finally { - signing.free(); - } -} diff --git a/src/crypto/index.js b/src/crypto/index.js index 418034cbd1c..6456464d341 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -50,8 +50,6 @@ import { newUnknownMethodError, } from './verification/Error'; -import { pkSign } from './PkSigning'; - const defaultVerificationMethods = { [ScanQRCode.NAME]: ScanQRCode, [ShowQRCode.NAME]: ShowQRCode, From 8d1d657c444971cc4e88234c29d61bf119c22b49 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 3 Jul 2019 19:16:26 -0400 Subject: [PATCH 35/97] add unit test for backups signed by cross-signing key --- spec/unit/crypto/backup.spec.js | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 8a6fa4b8dd0..c6859175a63 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -29,6 +29,7 @@ import testUtils from '../../test-utils'; import OlmDevice from '../../../lib/crypto/OlmDevice'; import Crypto from '../../../lib/crypto'; import logger from '../../../src/logger'; +import olmlib from '../../../lib/crypto/olmlib'; const Olm = global.Olm; @@ -296,6 +297,71 @@ describe("MegolmBackup", function() { }); }); + it('signs backups with the cross-signing master key', async function() { + const groupSession = new Olm.OutboundGroupSession(); + groupSession.create(); + const ibGroupSession = new Olm.InboundGroupSession(); + ibGroupSession.create(groupSession.session_key()); + + const client = makeTestClient(sessionStore, cryptoStore); + + megolmDecryption = new MegolmDecryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: client, + roomId: ROOM_ID, + }); + + megolmDecryption.olmlib = mockOlmLib; + + await client.initCrypto(); + let privateKeys; + client.uploadDeviceSigningKeys = async function(e) {return;}; + client.uploadKeySignatures = async function(e) {return;}; + client.on("cross-signing.savePrivateKeys", function(e) { + privateKeys = e; + }); + client.on("cross-signing.getKey", function(e) { + e.done(privateKeys[e.type]); + }); + await client.resetCrossSigningKeys(); + let numCalls = 0; + await new Promise(async (resolve, reject) => { + client._http.authedRequest = function( + callback, method, path, queryParams, data, opts, + ) { + ++numCalls; + expect(numCalls).toBeLessThanOrEqualTo(1); + if (numCalls >= 2) { + // exit out of retry loop if there's something wrong + reject(new Error("authedRequest called too many timmes")); + return Promise.resolve({}); + } + expect(method).toBe("POST"); + expect(path).toBe("/room_keys/version"); + try { + // make sure auth_data is signed by the master key + olmlib.pkVerify( + data.auth_data, client.getCrossSigningId(), "@alice:bar", + ); + } catch (e) { + reject(e); + return Promise.resolve({}); + } + resolve(); + return Promise.resolve({}); + }; + await client.createKeyBackupVersion({ + algorithm: "m.megolm_backup.v1", + auth_data: { + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", + }, + }); + }); + expect(numCalls).toBe(1); + }); + it('retries when a backup fails', function() { const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); From b00804102d8bd10bc3a1a1f9f7bece288fe88e70 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 3 Jul 2019 21:37:18 -0400 Subject: [PATCH 36/97] obsolete todo --- src/crypto/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 6456464d341..01a8cbabdfc 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -207,7 +207,6 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, ]); this._secretStorage = new SecretStorage(baseApis); - // TODO: expose SecretStorage methods } utils.inherits(Crypto, EventEmitter); From 761f22b63d391ab1105b6f4657dbea44938f88cd Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 8 Jul 2019 12:25:28 -0400 Subject: [PATCH 37/97] minor cleanups --- spec/unit/crypto/cross-signing.spec.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 2d344615bda..adf239e92d7 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -50,8 +50,8 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); - alice.uploadDeviceSigningKeys = async function(e) {return;}; - alice.uploadKeySignatures = async function(e) {return;}; + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; // set Alice's cross-signing key let privateKeys; alice.on("cross-signing.savePrivateKeys", function(e) { @@ -223,8 +223,8 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); - alice.uploadDeviceSigningKeys = async function(e) {return;}; - alice.uploadKeySignatures = async function(e) {return;}; + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; // set Alice's cross-signing key let privateKeys; alice.on("cross-signing.savePrivateKeys", function(e) { @@ -303,8 +303,8 @@ describe("Cross Signing", function() { ); alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com"); alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {}; - alice.uploadDeviceSigningKeys = async function(e) {return;}; - alice.uploadKeySignatures = async function(e) {return;}; + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; // set Alice's cross-signing key let privateKeys; @@ -462,8 +462,8 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); - alice.uploadDeviceSigningKeys = async function(e) {return;}; - alice.uploadKeySignatures = async function(e) {return;}; + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; // set Alice's cross-signing key let privateKeys; alice.on("cross-signing.savePrivateKeys", function(e) { @@ -523,9 +523,7 @@ describe("Cross Signing", function() { // Bob's device key should be untrusted expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); // Alice verifies Bob's SSK - console.log("verifying bob's device"); await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); - console.log("done"); // Bob's device key should be untrusted expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); }); @@ -534,8 +532,8 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, ); - alice.uploadDeviceSigningKeys = async function(e) {return;}; - alice.uploadKeySignatures = async function(e) {return;}; + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; let privateKeys; alice.on("cross-signing.savePrivateKeys", function(e) { privateKeys = e; From 7f8b9de5607cf37050ddfce25ea163a05125df9b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 8 Jul 2019 12:26:00 -0400 Subject: [PATCH 38/97] offer to upgrade device verifications to cross-signing --- spec/unit/crypto/cross-signing.spec.js | 126 +++++++++++++++++++++- src/client.js | 17 +++ src/crypto/index.js | 138 +++++++++++++++++++++++-- 3 files changed, 269 insertions(+), 12 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index adf239e92d7..7d8ec512917 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -46,6 +46,30 @@ describe("Cross Signing", function() { await global.Olm.init(); }); + it("should sign the master key with the device key", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + alice.uploadDeviceSigningKeys = expect.createSpy() + .andCall(async (auth, keys) => { + await olmlib.verifySignature( + alice._crypto._olmDevice, keys.master_key, "@alice:example.com", + "Osborne2", alice._crypto._olmDevice.deviceEd25519Key, + ); + }); + alice.uploadKeySignatures = async () => {}; + // set Alice's cross-signing key + let privateKeys; + alice.on("cross-signing.savePrivateKeys", function(e) { + privateKeys = e; + }); + alice.on("cross-signing.getKey", function(e) { + e.done(privateKeys[e.type]); + }); + await alice.resetCrossSigningKeys(); + expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled(); + }); + it("should upload a signature when a user is verified", async function() { const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, @@ -116,6 +140,23 @@ describe("Cross Signing", function() { e.done(selfSigningKey); }); + const uploadSigsPromise = new Promise((resolve, reject) => { + alice.uploadKeySignatures = expect.createSpy().andCall(async (content) => { + await olmlib.verifySignature( + alice._crypto._olmDevice, + content["@alice:example.com"]["nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"], + "@alice:example.com", + "Osborne2", alice._crypto._olmDevice.deviceEd25519Key, + ); + olmlib.pkVerify( + content["@alice:example.com"]["Osborne2"], + "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", + "@alice:example.com", + ); + resolve(); + }); + }); + const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"] .Osborne2; const aliceDevice = { @@ -203,11 +244,6 @@ describe("Cross Signing", function() { }, }, }, - { - method: "POST", - path: "/keys/signatures/upload", - data: {}, - }, ]; setHttpResponses(alice, responses, true, true); @@ -215,6 +251,7 @@ describe("Cross Signing", function() { // once ssk is confirmed, device key should be trusted await keyChangePromise; + await uploadSigsPromise; expect(alice.checkUserTrust("@alice:example.com")).toBe(6); expect(alice.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(7); }); @@ -654,4 +691,83 @@ describe("Cross Signing", function() { expect(alice.checkUserTrust("@bob:example.com")).toBe(4); expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(4); }); + + it("should offer to upgrade device verifications to cross-signing", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"} + ); + const bob = await makeTestClient( + {userId: "@bob:example.com", deviceId: "Dynabook"}, + ); + const privateKeys = {}; + + bob.uploadDeviceSigningKeys = async () => {}; + bob.uploadKeySignatures = async () => {}; + // set Bob's cross-signing key + bob.on("cross-signing.savePrivateKeys", function(e) { + privateKeys.bob = e; + }); + bob.on("cross-signing.getKey", function(e) { + e.done(privateKeys.bob[e.type]); + }); + await bob.resetCrossSigningKeys(); + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: { + algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + keys: { + "curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key, + "ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key, + }, + verified: 1, + known: true, + } + }); + alice._crypto._deviceList.storeCrossSigningForUser( + "@bob:example.com", + bob._crypto._crossSigningInfo.toStorage(), + ); + + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; + // set Alice's cross-signing key + alice.on("cross-signing.savePrivateKeys", function(e) { + privateKeys.alice = e; + }); + alice.on("cross-signing.getKey", function(e) { + e.done(privateKeys.alice[e.type]); + }); + // when alice sets up cross-signing, she should notice that bob's + // cross-signing key is signed by his Dynabook, which alice has + // verified, and ask if the device verification should be upgraded to a + // cross-signing verification + let upgradePromise = new Promise((resolve, reject) => { + alice.once("cross-signing.upgradeDeviceVerifications", async (e) => { + expect(e.users["@bob:example.com"]).toExist(); + await e.accept(["@bob:example.com"]); + resolve(); + }); + }); + await alice.resetCrossSigningKeys(); + await upgradePromise; + + expect(alice.checkUserTrust("@bob:example.com")).toBe(6); + + // "forget" that Bob is trusted + delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"] + .keys.master.signatures["@alice:example.com"]; + + expect(alice.checkUserTrust("@bob:example.com")).toBe(2); + + upgradePromise = new Promise((resolve, reject) => { + alice.once("cross-signing.upgradeDeviceVerifications", async (e) => { + expect(e.users["@bob:example.com"]).toExist(); + await e.accept(["@bob:example.com"]); + resolve(); + }); + }); + alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com"); + await upgradePromise; + + expect(alice.checkUserTrust("@bob:example.com")).toBe(6); + }); }); diff --git a/src/client.js b/src/client.js index f79b38bb21b..feb834ffbf6 100644 --- a/src/client.js +++ b/src/client.js @@ -561,6 +561,7 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.devicesUpdated", "cross-signing.savePrivateKeys", "cross-signing.getKey", + "cross-signing.upgradeDeviceVerifications", ]); logger.log("Crypto: initialising crypto object..."); @@ -4751,6 +4752,22 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * provided if a previously provided key was invalid. */ +/** + * Fires when a device verification can be upgraded to a cross-signing + * verification. The handler should call the `accept` callback in order to + * perform the upgrade. + * @event module:client~MatrixClient#"cross-signing.upgradeDeviceVerifications" + * @param {object} data + * @param {object} data.users The users whose device verifications can be + * upgraded to cross-signing verifications. This will be a map of user IDs + * to objects with the properties `devices` (array of the user's devices + * that verified their cross-signing key), and `crossSigningInfo` (the + * user's cross-signing information) + * @param {object} data.accept a function to call to upgrade the device + * verifications. It should be called with an array of the user IDs who + * should be cross-signed. + */ + /** * Fires when a secret has been requested by another client. Clients should * ensure that the requesting device is allowed to have the secret. For diff --git a/src/crypto/index.js b/src/crypto/index.js index 01a8cbabdfc..5f538e9b78c 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -306,6 +306,7 @@ Crypto.prototype.init = async function() { */ Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) { await this._crossSigningInfo.resetKeys(level); + await this._signObject(this._crossSigningInfo.keys.master); await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { @@ -329,6 +330,91 @@ Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) { [this._deviceId]: signedDevice, }, }); + + // check all users for signatures + // FIXME: do this in batches + const users = {}; + for (const [userId, crossSigningInfo] + of Object.entries(this._deviceList._crossSigningInfo)) { + const upgradeInfo = await this._checkForDeviceVerificationUpgrade( + userId, CrossSigningInfo.fromStorage(crossSigningInfo, userId), + ); + if (upgradeInfo) { + users[userId] = upgradeInfo; + } + } + this.emit("cross-signing.upgradeDeviceVerifications", { + users, + accept: async (upgradeUsers) => { + for (const userId of upgradeUsers) { + if (userId in users) { + await this._baseApis.setDeviceVerified( + userId, users[userId].crossSigningInfo.getId(), + ); + } + } + }, + }); +}; + +/** + * Check if a user's cross-signing key is a candidate for upgrading from device + * verification. + * + * @param {string} userId the user whose cross-signing information is to be checked + * @param {object} crossSigningInfo the cross-signing information to check + */ +Crypto.prototype._checkForDeviceVerificationUpgrade = async function( + userId, crossSigningInfo, +) { + // only upgrade if this is the first cross-signing key that we've seen for + // them, and if their cross-signing key isn't already verified + if (crossSigningInfo.fu + && !(this._crossSigningInfo.checkUserTrust(crossSigningInfo) & 2)) { + const devices = this._deviceList.getRawStoredDevicesForUser(userId); + const deviceIds = await this._checkForValidDeviceSignature( + userId, crossSigningInfo.keys.master, devices, + ); + if (deviceIds.length) { + return { + devices: deviceIds.map( + deviceId => DeviceInfo.fromStorage(devices[deviceId], deviceId), + ), + crossSigningInfo, + }; + } + } +}; + +/** + * Check if the cross-signing key is signed by a verified device. + * + * @param {string} userId the user ID whose key is being checked + * @param {object} key the key that is being checked + * @param {object} devices the user's devices. Should be a map from device ID + * to device info + */ +Crypto.prototype._checkForValidDeviceSignature = async function(userId, key, devices) { + const deviceIds = []; + if (devices && key.signatures && key.signatures[userId]) { + for (const signame of Object.keys(key.signatures[userId])) { + const [, deviceId] = signame.split(':', 2); + if (deviceId in devices + && devices[deviceId].verified === DeviceVerification.VERIFIED) { + try { + await olmlib.verifySignature( + this._olmDevice, + key, + userId, + deviceId, + devices[deviceId].keys[signame], + ); + deviceIds.push(deviceId); + } catch (e) {} + } + } + } + return deviceIds; }; /** @@ -410,7 +496,9 @@ Crypto.prototype.checkDeviceTrust = function(userId, deviceId) { */ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) { if (userId === this._userId) { - this.checkOwnCrossSigningTrust(); + await this._checkOwnCrossSigningTrust(); + } else { + await this._checkDeviceVerifications(userId); } }; @@ -418,7 +506,7 @@ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) { * Check the copy of our cross-signing key that we have in the device list and * see if we can get the private key. If so, mark it as trusted. */ -Crypto.prototype.checkOwnCrossSigningTrust = async function() { +Crypto.prototype._checkOwnCrossSigningTrust = async function() { const userId = this._userId; // If we see an update to our own master key, check it against the master @@ -489,6 +577,8 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() { }, ); + const keySignatures = {}; + if (oldSelfSigningId !== newCrossSigning.getId("self_signing")) { logger.info("Got new self-signing key", newCrossSigning.getId("self_signing")); @@ -496,26 +586,60 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() { const signedDevice = await this._crossSigningInfo.signDevice( this._userId, device, ); - await this._baseApis.uploadKeySignatures({ - [this._userId]: { - [this._deviceId]: signedDevice, - }, - }); + keySignatures[this._deviceId] = signedDevice; } if (oldUserSigningId !== newCrossSigning.getId("user_signing")) { logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); } if (changed) { + await this._signObject(this._crossSigningInfo.keys.master); + keySignatures[this._crossSigningInfo.getId()] + = this._crossSigningInfo.keys.master; this._baseApis.emit("cross-signing.keysChanged", {}); } + if (Object.keys(keySignatures).length) { + await this._baseApis.uploadKeySignatures({[this._userId]: keySignatures}); + } + // Now we may be able to trust our key backup await this.checkKeyBackup(); // FIXME: if we previously trusted the backup, should we automatically sign // the backup with the new key (if not already signed)? }; +/** + * Check if the master key is signed by a verified device, and if so, prompt + * the application to mark it as verified. + * + * @param {string} userId the user ID whose key should be checked + */ +Crypto.prototype._checkDeviceVerifications = async function(userId) { + if (this._crossSigningInfo.keys.user_signing) { + const crossSigningInfo = this._deviceList.getStoredCrossSigningForUser(userId); + if (crossSigningInfo) { + const upgradeInfo = await this._checkForDeviceVerificationUpgrade( + userId, crossSigningInfo, + ); + if (upgradeInfo) { + this.emit("cross-signing.upgradeDeviceVerifications", { + users: { + [userId]: upgradeInfo, + }, + accept: async (users) => { + if (users.includes(userId)) { + await this._baseApis.setDeviceVerified( + userId, crossSigningInfo.getId(), + ); + } + }, + }); + } + } + } +}; + /** * Check the server for an active key backup and * if one is present and has a valid signature from From f3ec9768bcb55842f5d7407246d1021e3ffed049 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 27 Aug 2019 16:53:36 -0700 Subject: [PATCH 39/97] update to follow latest MSC --- src/crypto/Secrets.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 9ea5048b8e7..31c610feaca 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -319,7 +319,7 @@ export default class SecretStorage extends EventEmitter { const cancel = (reason) => { // send cancellation event const cancelData = { - action: "cancel_request", + action: "request_cancellation", requesting_device_id: this._baseApis.deviceId, request_id: requestId, }; @@ -369,7 +369,7 @@ export default class SecretStorage extends EventEmitter { } const deviceId = content.requesting_device_id; // check if it's a cancel - if (content.action === "cancel_request") { + if (content.action === "request_cancellation") { if (this._incomingRequests[deviceId] && this._incomingRequests[deviceId][content.request_id]) { logger.info("received request cancellation for secret (" + sender From 8cad116dd7474e1dacdcd46aa0366c84cdbab234 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 28 Oct 2019 14:56:35 +0000 Subject: [PATCH 40/97] Make tests pass * Pass the http backend out of makeTestClients so we can tell it to expect queries and flush requests out * Change colons to dots in the key events --- spec/unit/crypto/verification/request.spec.js | 10 +- spec/unit/crypto/verification/sas.spec.js | 161 ++++++++++++------ spec/unit/crypto/verification/util.js | 12 +- 3 files changed, 123 insertions(+), 60 deletions(-) diff --git a/spec/unit/crypto/verification/request.spec.js b/spec/unit/crypto/verification/request.spec.js index 558e2880dd9..a53d580026d 100644 --- a/spec/unit/crypto/verification/request.spec.js +++ b/spec/unit/crypto/verification/request.spec.js @@ -51,7 +51,7 @@ describe("verification request", function() { verificationMethods: [verificationMethods.SAS], }, ); - alice._crypto._deviceList.getRawStoredDevicesForUser = function() { + alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() { return { Dynabook: { keys: { @@ -60,17 +60,17 @@ describe("verification request", function() { }, }; }; - alice.downloadKeys = () => { + alice.client.downloadKeys = () => { return Promise.resolve(); }; - bob.downloadKeys = () => { + bob.client.downloadKeys = () => { return Promise.resolve(); }; - bob.on("crypto.verification.request", (request) => { + bob.client.on("crypto.verification.request", (request) => { const bobVerifier = request.beginKeyVerification(verificationMethods.SAS); bobVerifier.verify(); }); - const aliceVerifier = await alice.requestVerification("@bob:example.com"); + const aliceVerifier = await alice.client.requestVerification("@bob:example.com"); expect(aliceVerifier).toBeAn(SAS); }); }); diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index 0fbea9a9627..e4ec05c3388 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -37,6 +37,9 @@ const MatrixEvent = sdk.MatrixEvent; import {makeTestClients} from './util'; +let ALICE_DEVICES; +let BOB_DEVICES; + describe("SAS verification", function() { if (!global.Olm) { logger.warn('Not running device verification unit tests: libolm not present'); @@ -79,10 +82,22 @@ describe("SAS verification", function() { }, ); - const aliceDevice = alice._crypto._olmDevice; - const bobDevice = bob._crypto._olmDevice; + const aliceDevice = alice.client._crypto._olmDevice; + const bobDevice = bob.client._crypto._olmDevice; + + ALICE_DEVICES = { + Osborne2: { + user_id: "@alice:example.com", + device_id: "Osborne2", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:Osborne2": aliceDevice.deviceEd25519Key, + "curve25519:Osborne2": aliceDevice.deviceCurve25519Key, + }, + }, + }; - alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + BOB_DEVICES = { Dynabook: { user_id: "@bob:example.com", device_id: "Dynabook", @@ -92,22 +107,14 @@ describe("SAS verification", function() { "curve25519:Dynabook": bobDevice.deviceCurve25519Key, }, }, - }); + }; + + alice.client._crypto._deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES); alice.downloadKeys = () => { return Promise.resolve(); }; - bob._crypto._deviceList.storeDevicesForUser("@alice:example.com", { - Osborne2: { - user_id: "@alice:example.com", - device_id: "Osborne2", - algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], - keys: { - "ed25519:Osborne2": aliceDevice.deviceEd25519Key, - "curve25519:Osborne2": aliceDevice.deviceCurve25519Key, - }, - }, - }); + bob.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES); bob.downloadKeys = () => { return Promise.resolve(); }; @@ -116,7 +123,7 @@ describe("SAS verification", function() { bobSasEvent = null; bobPromise = new Promise((resolve, reject) => { - bob.on("crypto.verification.start", (verifier) => { + bob.client.on("crypto.verification.start", (verifier) => { verifier.on("show_sas", (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); @@ -137,8 +144,8 @@ describe("SAS verification", function() { }); }); - aliceVerifier = alice.beginKeyVerification( - verificationMethods.SAS, bob.getUserId(), bob.deviceId, + aliceVerifier = alice.client.beginKeyVerification( + verificationMethods.SAS, bob.client.getUserId(), bob.deviceId, ); aliceVerifier.on("show_sas", (e) => { if (!e.sas.emoji || !e.sas.decimal) { @@ -160,18 +167,33 @@ describe("SAS verification", function() { it("should verify a key", async function() { let macMethod; - const origSendToDevice = alice.sendToDevice; - bob.sendToDevice = function(type, map) { + const origSendToDevice = alice.client.sendToDevice; + bob.client.sendToDevice = function(type, map) { if (type === "m.key.verification.accept") { - macMethod = map[alice.getUserId()][alice.deviceId] + macMethod = map[alice.client.getUserId()][alice.client.deviceId] .message_authentication_code; } return origSendToDevice.call(this, type, map); }; + alice.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@bob:example.com": BOB_DEVICES, + }, + }); + bob.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@alice:example.com": ALICE_DEVICES, + }, + }); + await Promise.all([ aliceVerifier.verify(), bobPromise.then((verifier) => verifier.verify()), + alice.httpBackend.flush(), + bob.httpBackend.flush(), ]); // make sure that it uses the preferred method @@ -179,10 +201,10 @@ describe("SAS verification", function() { // make sure Alice and Bob verified each other const bobDevice - = await alice.getStoredDevice("@bob:example.com", "Dynabook"); + = await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); expect(bobDevice.isVerified()).toBeTruthy(); const aliceDevice - = await bob.getStoredDevice("@alice:example.com", "Osborne2"); + = await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); expect(aliceDevice.isVerified()).toBeTruthy(); }); @@ -190,68 +212,109 @@ describe("SAS verification", function() { // pretend that Alice can only understand the old (incorrect) MAC, // and make sure that she can still verify with Bob let macMethod; - const origSendToDevice = alice.sendToDevice; - alice.sendToDevice = function(type, map) { + const origSendToDevice = alice.client.sendToDevice; + alice.client.sendToDevice = function(type, map) { if (type === "m.key.verification.start") { // Note: this modifies not only the message that Bob // receives, but also the copy of the message that Alice // has, since it is the same object. If this does not // happen, the verification will fail due to a hash // commitment mismatch. - map[bob.getUserId()][bob.deviceId] + map[bob.client.getUserId()][bob.client.deviceId] .message_authentication_codes = ['hmac-sha256']; } return origSendToDevice.call(this, type, map); }; - bob.sendToDevice = function(type, map) { + bob.client.sendToDevice = function(type, map) { if (type === "m.key.verification.accept") { - macMethod = map[alice.getUserId()][alice.deviceId] + macMethod = map[alice.client.getUserId()][alice.client.deviceId] .message_authentication_code; } return origSendToDevice.call(this, type, map); }; + alice.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@bob:example.com": BOB_DEVICES, + }, + }); + bob.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@alice:example.com": ALICE_DEVICES, + }, + }); + await Promise.all([ aliceVerifier.verify(), bobPromise.then((verifier) => verifier.verify()), + alice.httpBackend.flush(), + bob.httpBackend.flush(), ]); expect(macMethod).toBe("hmac-sha256"); const bobDevice - = await alice.getStoredDevice("@bob:example.com", "Dynabook"); + = await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); expect(bobDevice.isVerified()).toBeTruthy(); const aliceDevice - = await bob.getStoredDevice("@alice:example.com", "Osborne2"); + = await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); expect(aliceDevice.isVerified()).toBeTruthy(); }); it("should verify a cross-signing key", async function() { const privateKeys = {}; - alice.on("cross-signing:savePrivateKeys", function(e) { + alice.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {}); + alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {}); + alice.client.on("cross-signing.savePrivateKeys", function(e) { privateKeys.alice = e; }); - await alice.resetCrossSigningKeys(); - bob.on("cross-signing:savePrivateKeys", function(e) { + alice.client.on("cross-signing.getKey", function(e) { + e.done(privateKeys.alice[e.type]); + }); + alice.httpBackend.flush(undefined, 2); + await alice.client.resetCrossSigningKeys(); + bob.client.on("cross-signing.savePrivateKeys", function(e) { privateKeys.bob = e; }); - await bob.resetCrossSigningKeys(); + bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {}); + bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {}); + bob.httpBackend.flush(undefined, 2); - bob.on("cross-signing:getKey", function(e) { + bob.client.on("cross-signing.getKey", function(e) { e.done(privateKeys.bob[e.type]); }); - bob._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { - keys: alice._crypto._crossSigningInfo.keys, + await bob.client.resetCrossSigningKeys(); + + bob.client._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { + keys: alice.client._crypto._crossSigningInfo.keys, + }); + + alice.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@bob:example.com": BOB_DEVICES, + }, }); + bob.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@alice:example.com": ALICE_DEVICES, + }, + }); + await Promise.all([ aliceVerifier.verify(), bobPromise.then((verifier) => verifier.verify()), + alice.httpBackend.flush(), + bob.httpBackend.flush(), ]); - expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(1); - expect(bob.checkUserTrust("@alice:example.com")).toBe(6); - expect(bob.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(1); + expect(alice.client.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(1); + expect(bob.client.checkUserTrust("@alice:example.com")).toBe(6); + expect(bob.client.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(1); }); }); @@ -265,17 +328,17 @@ describe("SAS verification", function() { verificationMethods: [verificationMethods.SAS], }, ); - alice.setDeviceVerified = expect.createSpy(); - alice.downloadKeys = () => { + alice.client.setDeviceVerified = expect.createSpy(); + alice.client.downloadKeys = () => { return Promise.resolve(); }; - bob.setDeviceVerified = expect.createSpy(); - bob.downloadKeys = () => { + bob.client.setDeviceVerified = expect.createSpy(); + bob.client.downloadKeys = () => { return Promise.resolve(); }; const bobPromise = new Promise((resolve, reject) => { - bob.on("crypto.verification.start", (verifier) => { + bob.client.on("crypto.verification.start", (verifier) => { verifier.on("show_sas", (e) => { e.mismatch(); }); @@ -283,8 +346,8 @@ describe("SAS verification", function() { }); }); - const aliceVerifier = alice.beginKeyVerification( - verificationMethods.SAS, bob.getUserId(), bob.deviceId, + const aliceVerifier = alice.client.beginKeyVerification( + verificationMethods.SAS, bob.client.getUserId(), bob.client.deviceId, ); const aliceSpy = expect.createSpy(); @@ -295,9 +358,9 @@ describe("SAS verification", function() { ]); expect(aliceSpy).toHaveBeenCalled(); expect(bobSpy).toHaveBeenCalled(); - expect(alice.setDeviceVerified) + expect(alice.client.setDeviceVerified) .toNotHaveBeenCalled(); - expect(bob.setDeviceVerified) + expect(bob.client.setDeviceVerified) .toNotHaveBeenCalled(); }); }); diff --git a/spec/unit/crypto/verification/util.js b/spec/unit/crypto/verification/util.js index 34a0408a8a5..79247dd1a0e 100644 --- a/spec/unit/crypto/verification/util.js +++ b/spec/unit/crypto/verification/util.js @@ -50,19 +50,19 @@ export async function makeTestClients(userInfos, options) { }; for (const userInfo of userInfos) { - const client = (new TestClient( + const testClient = new TestClient( userInfo.userId, userInfo.deviceId, undefined, undefined, options, - )).client; + ); if (!(userInfo.userId in clientMap)) { clientMap[userInfo.userId] = {}; } - clientMap[userInfo.userId][userInfo.deviceId] = client; - client.sendToDevice = sendToDevice; - clients.push(client); + clientMap[userInfo.userId][userInfo.deviceId] = testClient.client; + testClient.client.sendToDevice = sendToDevice; + clients.push(testClient); } - await Promise.all(clients.map((client) => client.initCrypto())); + await Promise.all(clients.map((testClient) => testClient.client.initCrypto())); return clients; } From 3bec28b2ff5cea17a712e4ca0c826c77feeacb84 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 28 Oct 2019 15:23:58 +0000 Subject: [PATCH 41/97] make other tests pass --- spec/unit/crypto/secrets.spec.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index c357c33aacf..4024fc52f29 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -101,11 +101,11 @@ describe("Secrets", function() { ], ); - const vaxDevice = vax._crypto._olmDevice; - const osborne2Device = osborne2._crypto._olmDevice; - const secretStorage = osborne2._crypto._secretStorage; + const vaxDevice = vax.client._crypto._olmDevice; + const osborne2Device = osborne2.client._crypto._olmDevice; + const secretStorage = osborne2.client._crypto._secretStorage; - osborne2._crypto._deviceList.storeDevicesForUser("@alice:example.com", { + osborne2.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", { "VAX": { user_id: "@alice:example.com", device_id: "VAX", @@ -116,7 +116,7 @@ describe("Secrets", function() { }, }, }); - vax._crypto._deviceList.storeDevicesForUser("@alice:example.com", { + vax.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", { "Osborne2": { user_id: "@alice:example.com", device_id: "Osborne2", @@ -128,7 +128,7 @@ describe("Secrets", function() { }, }); - vax.once("crypto.secrets.request", function(e) { + vax.client.once("crypto.secrets.request", function(e) { expect(e.name).toBe("foo"); e.send("bar"); }); @@ -137,7 +137,7 @@ describe("Secrets", function() { const otks = (await osborne2Device.getOneTimeKeys()).curve25519; await osborne2Device.markKeysAsPublished(); - await vax._crypto._olmDevice.createOutboundSession( + await vax.client._crypto._olmDevice.createOutboundSession( osborne2Device.deviceCurve25519Key, Object.values(otks)[0], ); From de1b545df14e1de8cf84e482367bf6f319275f12 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 28 Oct 2019 15:42:42 +0000 Subject: [PATCH 42/97] lint --- spec/unit/crypto/cross-signing.spec.js | 8 +++++--- spec/unit/crypto/verification/sas.spec.js | 21 ++++++++++++++------- src/client.js | 2 -- src/crypto/DeviceList.js | 4 ++-- src/crypto/Secrets.js | 2 +- src/crypto/index.js | 7 +++++-- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 7d8ec512917..b38aa8d376b 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -144,7 +144,9 @@ describe("Cross Signing", function() { alice.uploadKeySignatures = expect.createSpy().andCall(async (content) => { await olmlib.verifySignature( alice._crypto._olmDevice, - content["@alice:example.com"]["nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"], + content["@alice:example.com"][ + "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" + ], "@alice:example.com", "Osborne2", alice._crypto._olmDevice.deviceEd25519Key, ); @@ -694,7 +696,7 @@ describe("Cross Signing", function() { it("should offer to upgrade device verifications to cross-signing", async function() { const alice = await makeTestClient( - {userId: "@alice:example.com", deviceId: "Osborne2"} + {userId: "@alice:example.com", deviceId: "Osborne2"}, ); const bob = await makeTestClient( {userId: "@bob:example.com", deviceId: "Dynabook"}, @@ -720,7 +722,7 @@ describe("Cross Signing", function() { }, verified: 1, known: true, - } + }, }); alice._crypto._deviceList.storeCrossSigningForUser( "@bob:example.com", diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index e4ec05c3388..8f64304efd0 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -27,7 +27,6 @@ import olmlib from '../../../../lib/crypto/olmlib'; import sdk from '../../../..'; import {verificationMethods} from '../../../../lib/crypto'; -import DeviceInfo from '../../../../lib/crypto/deviceinfo'; import SAS from '../../../../lib/crypto/verification/SAS'; @@ -109,12 +108,16 @@ describe("SAS verification", function() { }, }; - alice.client._crypto._deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES); + alice.client._crypto._deviceList.storeDevicesForUser( + "@bob:example.com", BOB_DEVICES, + ); alice.downloadKeys = () => { return Promise.resolve(); }; - bob.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES); + bob.client._crypto._deviceList.storeDevicesForUser( + "@alice:example.com", ALICE_DEVICES, + ); bob.downloadKeys = () => { return Promise.resolve(); }; @@ -265,7 +268,9 @@ describe("SAS verification", function() { it("should verify a cross-signing key", async function() { const privateKeys = {}; - alice.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {}); + alice.httpBackend.when('POST', '/keys/device_signing/upload').respond( + 200, {}, + ); alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {}); alice.client.on("cross-signing.savePrivateKeys", function(e) { privateKeys.alice = e; @@ -288,9 +293,11 @@ describe("SAS verification", function() { await bob.client.resetCrossSigningKeys(); - bob.client._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", { - keys: alice.client._crypto._crossSigningInfo.keys, - }); + bob.client._crypto._deviceList.storeCrossSigningForUser( + "@alice:example.com", { + keys: alice.client._crypto._crossSigningInfo.keys, + }, + ); alice.httpBackend.when('POST', '/keys/query').respond(200, { failures: {}, diff --git a/src/client.js b/src/client.js index feb834ffbf6..8e89572219d 100644 --- a/src/client.js +++ b/src/client.js @@ -53,8 +53,6 @@ import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password'; import { randomString } from './randomstring'; -import IndexedDBCryptoStore from './crypto/store/indexeddb-crypto-store'; - // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. Promise.config({warnings: false}); diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index a8c02c98acf..55cae532b46 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -756,7 +756,7 @@ class DeviceListUpdateSerialiser { downloadUsers, opts, ).then((res) => { const dk = res.device_keys || {}; - const master_keys = res.master_keys || {}; + const masterKeys = res.masterKeys || {}; const ssks = res.self_signing_keys || {}; const usks = res.user_signing_keys || {}; @@ -770,7 +770,7 @@ class DeviceListUpdateSerialiser { prom = prom.delay(5).then(() => { return this._processQueryResponseForUser( userId, dk[userId], { - master: master_keys[userId], + master: masterKeys[userId], self_signing: ssks[userId], user_signing: usks[userId], }, diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 31c610feaca..e4aa36416b6 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -19,7 +19,7 @@ import logger from '../logger'; import olmlib from './olmlib'; import { randomString } from '../randomstring'; import { keyForNewBackup } from './backup_password'; -import { encodeRecoveryKey, decodeRecoveryKey } from './recoverykey'; +import { encodeRecoveryKey } from './recoverykey'; /** * Implements secret storage and sharing (MSC-1946) diff --git a/src/crypto/index.js b/src/crypto/index.js index 5f538e9b78c..9379026c911 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -104,7 +104,8 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200; */ export default function Crypto(baseApis, sessionStore, userId, deviceId, clientStore, cryptoStore, roomList, verificationMethods) { - this._onDeviceListUserCrossSigningUpdated = this._onDeviceListUserCrossSigningUpdated.bind(this); + this._onDeviceListUserCrossSigningUpdated = + this._onDeviceListUserCrossSigningUpdated.bind(this); this._reEmitter = new ReEmitter(this); this._baseApis = baseApis; @@ -147,7 +148,9 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, ); // XXX: This isn't removed at any point, but then none of the event listeners // this class sets seem to be removed at any point... :/ - this._deviceList.on('userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated); + this._deviceList.on( + 'userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated, + ); this._reEmitter.reEmit(this._deviceList, ["crypto.devicesUpdated"]); // the last time we did a check for the number of one-time-keys on the From e92d2bd70a1b5e3466a0ba6f68f88b1dc3788770 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 28 Oct 2019 16:07:26 +0000 Subject: [PATCH 43/97] Fix test again That one was part of the protocol - don't camelcase that --- src/crypto/DeviceList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 55cae532b46..31616a5eda9 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -756,7 +756,7 @@ class DeviceListUpdateSerialiser { downloadUsers, opts, ).then((res) => { const dk = res.device_keys || {}; - const masterKeys = res.masterKeys || {}; + const masterKeys = res.master_keys || {}; const ssks = res.self_signing_keys || {}; const usks = res.user_signing_keys || {}; From 49588da73d257b54764bbf3cf9eee6be7959afe3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 29 Oct 2019 19:39:31 +0000 Subject: [PATCH 44/97] Fix more tests --- spec/unit/crypto/verification/sas.spec.js | 35 ++++++++++++----------- spec/unit/crypto/verification/util.js | 4 +-- src/base-apis.js | 13 +++++---- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index d26ec3be200..89af121d4c9 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -27,6 +27,7 @@ import olmlib from '../../../../lib/crypto/olmlib'; import sdk from '../../../..'; import {verificationMethods} from '../../../../lib/crypto'; +import DeviceInfo from '../../../../lib/crypto/deviceinfo'; import SAS from '../../../../lib/crypto/verification/SAS'; @@ -393,11 +394,11 @@ describe("SAS verification", function() { }, ); - alice.setDeviceVerified = expect.createSpy(); - alice.getDeviceEd25519Key = () => { + alice.client.setDeviceVerified = expect.createSpy(); + alice.client.getDeviceEd25519Key = () => { return "alice+base64+ed25519+key"; }; - alice.getStoredDevice = () => { + alice.client.getStoredDevice = () => { return DeviceInfo.fromStorage( { keys: { @@ -407,12 +408,12 @@ describe("SAS verification", function() { "Dynabook", ); }; - alice.downloadKeys = () => { + alice.client.downloadKeys = () => { return Promise.resolve(); }; - bob.setDeviceVerified = expect.createSpy(); - bob.getStoredDevice = () => { + bob.client.setDeviceVerified = expect.createSpy(); + bob.client.getStoredDevice = () => { return DeviceInfo.fromStorage( { keys: { @@ -422,10 +423,10 @@ describe("SAS verification", function() { "Osborne2", ); }; - bob.getDeviceEd25519Key = () => { + bob.client.getDeviceEd25519Key = () => { return "bob+base64+ed25519+key"; }; - bob.downloadKeys = () => { + bob.client.downloadKeys = () => { return Promise.resolve(); }; @@ -433,13 +434,13 @@ describe("SAS verification", function() { bobSasEvent = null; bobPromise = new Promise((resolve, reject) => { - bob.on("event", async (event) => { + bob.client.on("event", async (event) => { const content = event.getContent(); if (event.getType() === "m.room.message" && content.msgtype === "m.key.verification.request") { expect(content.methods).toInclude(SAS.NAME); - expect(content.to).toBe(bob.getUserId()); - const verifier = bob.acceptVerificationDM(event, SAS.NAME); + expect(content.to).toBe(bob.client.getUserId()); + const verifier = bob.client.acceptVerificationDM(event, SAS.NAME); verifier.on("show_sas", (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); @@ -462,8 +463,8 @@ describe("SAS verification", function() { }); }); - aliceVerifier = await alice.requestVerificationDM( - bob.getUserId(), "!room_id", [verificationMethods.SAS], + aliceVerifier = await alice.client.requestVerificationDM( + bob.client.getUserId(), "!room_id", [verificationMethods.SAS], ); aliceVerifier.on("show_sas", (e) => { if (!e.sas.emoji || !e.sas.decimal) { @@ -490,10 +491,10 @@ describe("SAS verification", function() { ]); // make sure Alice and Bob verified each other - expect(alice.setDeviceVerified) - .toHaveBeenCalledWith(bob.getUserId(), bob.deviceId); - expect(bob.setDeviceVerified) - .toHaveBeenCalledWith(alice.getUserId(), alice.deviceId); + expect(alice.client.setDeviceVerified) + .toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId); + expect(bob.client.setDeviceVerified) + .toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId); }); }); }); diff --git a/spec/unit/crypto/verification/util.js b/spec/unit/crypto/verification/util.js index d12b0462af6..4ffa305537d 100644 --- a/spec/unit/crypto/verification/util.js +++ b/spec/unit/crypto/verification/util.js @@ -58,9 +58,9 @@ export async function makeTestClients(userInfos, options) { room_id: room, event_id: eventId, }); - for (const client of clients) { + for (const tc of clients) { setTimeout( - () => client.emit("event", event), + () => tc.client.emit("event", event), 0, ); } diff --git a/src/base-apis.js b/src/base-apis.js index 0e34d5e7003..24dd0ada857 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -1719,9 +1719,11 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) { }; MatrixBaseApis.prototype.uploadKeySignatures = function(content) { - return this._http.authedRequestWithPrefix( + return this._http.authedRequest( undefined, "POST", '/keys/signatures/upload', undefined, - content, httpApi.PREFIX_UNSTABLE, + content, { + prefix: httpApi.PREFIX_UNSTABLE, + }, ); }; @@ -1811,9 +1813,10 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) { MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) { const data = Object.assign({}, keys, {auth}); - return this._http.authedRequestWithPrefix( - undefined, "POST", "/keys/device_signing/upload", undefined, data, - httpApi.PREFIX_UNSTABLE, + return this._http.authedRequest( + undefined, "POST", "/keys/device_signing/upload", undefined, data, { + prefix: httpApi.PREFIX_UNSTABLE, + }, ); }; From 74b649c04cdfae78fa7c493c82b6414671f67456 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 Nov 2019 10:45:41 +0000 Subject: [PATCH 45/97] Typo --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 84ec107bce1..1a5ffcdc1f4 100644 --- a/src/client.js +++ b/src/client.js @@ -928,7 +928,7 @@ function wrapCryptoFuncs(MatrixClient, names) { * @function module:client~MatrixClient#getStoredCrossSigningForUser * @param {string} userId the user ID to get the cross-signing info for. * - * @returns {CrossSigningInfo} the cross signing informmation for the user. + * @returns {CrossSigningInfo} the cross signing information for the user. */ /** From a571624e1330e3cdfdf70393f70a2efbb37de476 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 Nov 2019 10:46:43 +0000 Subject: [PATCH 46/97] Typo --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 1a5ffcdc1f4..1f910586e6b 100644 --- a/src/client.js +++ b/src/client.js @@ -1340,7 +1340,7 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) { auth_data: info.auth_data, }; - // Now sig the backup auth data. Do it as this device first because crypto._signObject + // Now sign the backup auth data. Do it as this device first because crypto._signObject // is dumb and bluntly replaces the whole signatures block... // this can probably go away very soon in favour of just signing with the SSK. await this._crypto._signObject(data.auth_data); From f3073e120d10250bc867fc894cd89a82844f5a3d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 Nov 2019 10:51:49 +0000 Subject: [PATCH 47/97] Space --- src/crypto/Secrets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index e4aa36416b6..aebf869bfc0 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -333,7 +333,7 @@ export default class SecretStorage extends EventEmitter { // and reject the promise so that anyone waiting on it will be // notified - requestControl.reject(new Error(reason ||"Cancelled")); + requestControl.reject(new Error(reason || "Cancelled")); }; // send request to devices From a34758f938930977156ed94b94412ada99746666 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Nov 2019 12:31:44 +0000 Subject: [PATCH 48/97] Convert event interface to callbacks Use options.cryptoCallbacks for things that require information from the app rather than events, since events can have zero, one or many listeners and the emitter doesn't know how many, so if nobody's listening then we would have just waited forever for a response. Also a collection of other changes like renaming 'fu' to 'firstUse' --- spec/unit/crypto/backup.spec.js | 11 ++ spec/unit/crypto/cross-signing.spec.js | 145 +++++++++------------- src/client.js | 160 ++++++++++++------------ src/crypto/CrossSigning.js | 94 +++++++------- src/crypto/index.js | 165 +++++++++++++++---------- 5 files changed, 293 insertions(+), 282 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index c6859175a63..e829ef70e05 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -84,6 +84,16 @@ const BACKUP_INFO = { }, }; +const keys = {}; + +function getPrivateKey(type) { + return keys[type]; +} + +function savePrivateKeys(k) { + Object.assign(keys, k); +} + function makeTestClient(sessionStore, cryptoStore) { const scheduler = [ "getQueueForEvent", "queueEvent", "removeEventFromQueue", @@ -109,6 +119,7 @@ function makeTestClient(sessionStore, cryptoStore) { deviceId: "device", sessionStore: sessionStore, cryptoStore: cryptoStore, + cryptoCallbacks: { getPrivateKey, savePrivateKeys }, }); } diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index b38aa8d376b..6702b4f5a7b 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -26,7 +26,21 @@ import TestClient from '../../TestClient'; import {HttpResponse, setHttpResponses} from '../../test-utils'; -async function makeTestClient(userInfo, options) { +async function makeTestClient(userInfo, options, keys) { + if (!keys) keys = {}; + + function getPrivateKey(type) { + return keys[type]; + } + + function savePrivateKeys(k) { + Object.assign(keys, k); + } + + if (!options) options = {}; + options.cryptoCallbacks = Object.assign( + {}, { getPrivateKey, savePrivateKeys }, options.cryptoCallbacks || {}, + ); const client = (new TestClient( userInfo.userId, userInfo.deviceId, undefined, undefined, options, )).client; @@ -59,13 +73,6 @@ describe("Cross Signing", function() { }); alice.uploadKeySignatures = async () => {}; // set Alice's cross-signing key - let privateKeys; - alice.on("cross-signing.savePrivateKeys", function(e) { - privateKeys = e; - }); - alice.on("cross-signing.getKey", function(e) { - e.done(privateKeys[e.type]); - }); await alice.resetCrossSigningKeys(); expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled(); }); @@ -77,13 +84,6 @@ describe("Cross Signing", function() { alice.uploadDeviceSigningKeys = async () => {}; alice.uploadKeySignatures = async () => {}; // set Alice's cross-signing key - let privateKeys; - alice.on("cross-signing.savePrivateKeys", function(e) { - privateKeys = e; - }); - alice.on("cross-signing.getKey", function(e) { - e.done(privateKeys[e.type]); - }); await alice.resetCrossSigningKeys(); // Alice downloads Bob's device key alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { @@ -109,10 +109,6 @@ describe("Cross Signing", function() { }); it("should get cross-signing keys from sync", async function() { - const alice = await makeTestClient( - {userId: "@alice:example.com", deviceId: "Osborne2"}, - ); - const masterKey = new Uint8Array([ 0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, @@ -126,20 +122,29 @@ describe("Cross Signing", function() { 0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f, ]); + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + // will be called to sign our own device + getPrivateKey: type => { + if (type === 'master') { + return masterKey; + } else { + return selfSigningKey; + } + }, + }, + }, + ); + const keyChangePromise = new Promise((resolve, reject) => { - alice.once("cross-signing.keysChanged", (e) => { + alice.once("cross-signing.keysChanged", async (e) => { resolve(e); + await alice.checkOwnCrossSigningTrust(); }); }); - alice.once("cross-signing.newKey", (e) => { - e.done(masterKey); - }); - alice.on("cross-signing.getKey", (e) => { - // will be called to sign our own device - e.done(selfSigningKey); - }); - const uploadSigsPromise = new Promise((resolve, reject) => { alice.uploadKeySignatures = expect.createSpy().andCall(async (content) => { await olmlib.verifySignature( @@ -265,13 +270,6 @@ describe("Cross Signing", function() { alice.uploadDeviceSigningKeys = async () => {}; alice.uploadKeySignatures = async () => {}; // set Alice's cross-signing key - let privateKeys; - alice.on("cross-signing.savePrivateKeys", function(e) { - privateKeys = e; - }); - alice.on("cross-signing.getKey", function(e) { - e.done(privateKeys[e.type]); - }); await alice.resetCrossSigningKeys(); // Alice downloads Bob's ssk and device key const bobMasterSigning = new global.Olm.PkSigning(); @@ -304,7 +302,7 @@ describe("Cross Signing", function() { }, self_signing: bobSSK, }, - fu: 1, + firstUse: 1, unsigned: {}, }); const bobDevice = { @@ -337,8 +335,11 @@ describe("Cross Signing", function() { }); it("should trust signatures received from other devices", async function() { + const aliceKeys = {}; const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, + null, + aliceKeys, ); alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com"); alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {}; @@ -346,13 +347,6 @@ describe("Cross Signing", function() { alice.uploadKeySignatures = async () => {}; // set Alice's cross-signing key - let privateKeys; - alice.on("cross-signing.savePrivateKeys", function(e) { - privateKeys = e; - }); - alice.on("cross-signing.getKey", (e) => { - e.done(privateKeys[e.type]); - }); await alice.resetCrossSigningKeys(); const selfSigningKey = new Uint8Array([ @@ -408,7 +402,7 @@ describe("Cross Signing", function() { "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", }, }; - olmlib.pkSign(bobMaster, privateKeys.user_signing, "@alice:example.com"); + olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com"); // Alice downloads Bob's keys // - device key @@ -504,13 +498,6 @@ describe("Cross Signing", function() { alice.uploadDeviceSigningKeys = async () => {}; alice.uploadKeySignatures = async () => {}; // set Alice's cross-signing key - let privateKeys; - alice.on("cross-signing.savePrivateKeys", function(e) { - privateKeys = e; - }); - alice.on("cross-signing.getKey", (e) => { - e.done(privateKeys[e.type]); - }); await alice.resetCrossSigningKeys(); // Alice downloads Bob's ssk and device key // (NOTE: device key is not signed by ssk) @@ -544,7 +531,7 @@ describe("Cross Signing", function() { }, self_signing: bobSSK, }, - fu: 1, + firstUse: 1, unsigned: {}, }); const bobDevice = { @@ -573,13 +560,6 @@ describe("Cross Signing", function() { ); alice.uploadDeviceSigningKeys = async () => {}; alice.uploadKeySignatures = async () => {}; - let privateKeys; - alice.on("cross-signing.savePrivateKeys", function(e) { - privateKeys = e; - }); - alice.on("cross-signing.getKey", function(e) { - e.done(privateKeys[e.type]); - }); await alice.resetCrossSigningKeys(); // Alice downloads Bob's keys const bobMasterSigning = new global.Olm.PkSigning(); @@ -612,7 +592,7 @@ describe("Cross Signing", function() { }, self_signing: bobSSK, }, - fu: 1, + firstUse: 1, unsigned: {}, }); const bobDevice = { @@ -668,16 +648,13 @@ describe("Cross Signing", function() { }, self_signing: bobSSK2, }, - fu: 0, + firstUse: 0, unsigned: {}, }); // Bob's and his device should be untrusted expect(alice.checkUserTrust("@bob:example.com")).toBe(0); expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); // Alice verifies Bob's SSK - alice.on("cross-signing.getKey", function(e) { - e.done(privateKeys[e.type]); - }); alice.uploadKeySignatures = () => {}; await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey2, true); // Bob should be trusted but not his device @@ -695,8 +672,19 @@ describe("Cross Signing", function() { }); it("should offer to upgrade device verifications to cross-signing", async function() { + let upgradeResolveFunc; + const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + shouldUpgradeDeviceVerifications: (verifs) => { + expect(verifs.users["@bob:example.com"]).toExist(); + upgradeResolveFunc(); + return ["@bob:example.com"]; + }, + }, + }, ); const bob = await makeTestClient( {userId: "@bob:example.com", deviceId: "Dynabook"}, @@ -706,12 +694,6 @@ describe("Cross Signing", function() { bob.uploadDeviceSigningKeys = async () => {}; bob.uploadKeySignatures = async () => {}; // set Bob's cross-signing key - bob.on("cross-signing.savePrivateKeys", function(e) { - privateKeys.bob = e; - }); - bob.on("cross-signing.getKey", function(e) { - e.done(privateKeys.bob[e.type]); - }); await bob.resetCrossSigningKeys(); alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: { @@ -731,23 +713,12 @@ describe("Cross Signing", function() { alice.uploadDeviceSigningKeys = async () => {}; alice.uploadKeySignatures = async () => {}; - // set Alice's cross-signing key - alice.on("cross-signing.savePrivateKeys", function(e) { - privateKeys.alice = e; - }); - alice.on("cross-signing.getKey", function(e) { - e.done(privateKeys.alice[e.type]); - }); // when alice sets up cross-signing, she should notice that bob's // cross-signing key is signed by his Dynabook, which alice has // verified, and ask if the device verification should be upgraded to a // cross-signing verification - let upgradePromise = new Promise((resolve, reject) => { - alice.once("cross-signing.upgradeDeviceVerifications", async (e) => { - expect(e.users["@bob:example.com"]).toExist(); - await e.accept(["@bob:example.com"]); - resolve(); - }); + let upgradePromise = new Promise((resolve) => { + upgradeResolveFunc = resolve; }); await alice.resetCrossSigningKeys(); await upgradePromise; @@ -760,12 +731,8 @@ describe("Cross Signing", function() { expect(alice.checkUserTrust("@bob:example.com")).toBe(2); - upgradePromise = new Promise((resolve, reject) => { - alice.once("cross-signing.upgradeDeviceVerifications", async (e) => { - expect(e.users["@bob:example.com"]).toExist(); - await e.accept(["@bob:example.com"]); - resolve(); - }); + upgradePromise = new Promise((resolve) => { + upgradeResolveFunc = resolve; }); alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com"); await upgradePromise; diff --git a/src/client.js b/src/client.js index 1f910586e6b..4240d65936b 100644 --- a/src/client.js +++ b/src/client.js @@ -175,6 +175,54 @@ function keyFromRecoverySession(session, decryptionKey) { * @param {boolean} [opts.fallbackICEServerAllowed] * Optional. Whether to allow a fallback ICE server should be used for negotiating a * WebRTC connection if the homeserver doesn't provide any servers. Defaults to false. + * + * @param {object} opts.cryptoCallbacks Optional. Callbacks for crypto and cross-signing. + * + * @param {function} [opts.cryptoCallbacks.getPrivateKey] + * Optional (required for cross-signing). Function to call when a private key is needed. + * Args: + * {string} type The type of key needed. Will be one of "master", + * "self_signing", or "user_signing" + * {Uint8Array} publicKey The public key matching the expected private key. + * This can be passed to checkPrivateKey() along with the private key + * in order to check that a given private key matches what is being + * requested. + * Should return a promise that resolves with the private key as a + * UInt8Array or rejects with an error. + * + * @param {function} [opts.cryptoCallbacks.savePrivateKeys] + * Optional (required for cross-signing). Called when new private keys + * for cross-signing need to be saved. + * Args: + * {object} keys the private keys to save. Map of key name to private key + * as a UInt8Array. The getPrivateKey callback above will be called + * with the corresponding key name when the keys are required again. + * + * @param {function} [opts.cryptoCallbacks.shouldUpgradeDeviceVerifications] + * Optional. Called when there are device-to-device verifications that can be + * upgraded into cross-signing verifications. + * Args: + * {object} users The users whose device verifications can be + * upgraded to cross-signing verifications. This will be a map of user IDs + * to objects with the properties `devices` (array of the user's devices + * that verified their cross-signing key), and `crossSigningInfo` (the + * user's cross-signing information) + * Should return a promise which resolves with an array of the user IDs who + * should be cross-signed. + * + * @param {function} [opts.cryptoCallbacks.onSecretRequested] + * Optional. Function called when a request for a secret is received from another + * device. + * Args: + * {string} name The name of the secret being requested. + * {string} user_id (string) The user ID of the client requesting + * {string} device_id The device ID of the client requesting the secret. + * {string} request_id The ID of the request. Used to match a + * corresponding `crypto.secrets.request_cancelled`. The request ID will be + * unique per sender, device pair. + * {int} device_trust: The trust status of the device requesting + * the secret. Will be a bit mask in the same form as returned by {@link + * module:client~MatrixClient#checkDeviceTrust}. */ function MatrixClient(opts) { opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl); @@ -235,6 +283,7 @@ function MatrixClient(opts) { this._cryptoStore = opts.cryptoStore; this._sessionStore = opts.sessionStore; this._verificationMethods = opts.verificationMethods; + this._cryptoCallbacks = opts.cryptoCallbacks; this._forceTURN = opts.forceTURN || false; this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false; @@ -612,9 +661,9 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.roomKeyRequestCancellation", "crypto.warning", "crypto.devicesUpdated", - "cross-signing.savePrivateKeys", - "cross-signing.getKey", - "cross-signing.upgradeDeviceVerifications", + "deviceVerificationChanged", + "userVerificationChanged", + "cross-signing.keysChanged", ]); logger.log("Crypto: initialising crypto object..."); @@ -783,10 +832,9 @@ async function _setDeviceVerification( if (!client._crypto) { throw new Error("End-to-End encryption disabled"); } - const dev = await client._crypto.setDeviceVerification( + await client._crypto.setDeviceVerification( userId, deviceId, verified, blocked, known, ); - client.emit("deviceVerificationChanged", userId, deviceId, dev); } /** @@ -970,6 +1018,8 @@ wrapCryptoFuncs(MatrixClient, [ "getStoredCrossSigningForUser", "checkUserTrust", "checkDeviceTrust", + "checkOwnCrossSigningTrust", + "checkPrivateKey", ]); /** @@ -5040,6 +5090,28 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * @param {module:crypto/deviceinfo} deviceInfo updated device information */ +/** + * Fires when the trust status of a user changes + * If userId is the userId of the logged in user, this indicated a change + * in the trust status of the cross-signing data on the account. + * + * @event module:client~MatrixClient#"userTrustStatusChanged" + * @param {string} userId the userId of the user in question + * @param {integer} trustLevel The new trust level of the user + */ + +/** + * Fires when the user's cross-signing keys have changed or cross-signing + * has been enabled/disabled. The client can use getStoredCrossSigningForUser + * with the user ID of the logged in user to check if cross-signing is + * enabled on the account. If enabled, it can test whether the current key + * is trusted using with checkUserTrust with the user ID of the logged + * in user. The checkOwnCrossSigningTrust function may be used to reconcile + * the trust in the account key. + * + * @event module:client~MatrixClient#"cross-signing.keysChanged" + */ + /** * Fires whenever new user-scoped account_data is added. * @event module:client~MatrixClient#"accountData" @@ -5097,84 +5169,6 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * perform the key verification */ -/** - * Fires when private keys for cross-signing need to be saved. - * @event module:client~MatrixClient#"cross-signing.savePrivateKeys" - * @param {object} keys the private keys to save. - * @param {UInt8Array} [keys.master] the private master key - * @param {UInt8Array} [keys.self_signing] the private user-signing key - * @param {UInt8Array} [keys.user_signing] the private self-signing key - */ - -/** - * Fires when a private key is needed. - * @event module:client~MatrixClient#"cross-signing.getKey" - * @param {object} data - * @param {string} data.type the type of key needed. Will be one of "master", - * "self_signing", or "user_signing" - * @param {Function} data.done a function to call with the private key as a - * `UInt8Array` - * @param {Function} data.cancel a function to call if the private key cannot - * be provided - * @param {string} [data.error] Error string to display to the user. Normally - * provided if a previously provided key was invalid, to re-prompt the - * user. - */ - -/** - * Fires when a new cross-signing key is provided from the server. The handler - * must verify the key by providing the private key for the given public key. - * @event module:client~MatrixClient#"cross-signing.newKey" - * @param {object} data - * @param {string} data.publicKey the public key received from the server - * @param {string} data.type the type of key that was received. Currently will - * only be "master". - * @param {Function} data.done a function to call with the private key - * corresponding to the given public key. - * @param {Function} data.cancel a function to call if the private key cannot be - * provided, indicating that the client does not accept the cross-signing key. - * @param {string} [data.error] Error string to display to the user. Normally - * provided if a previously provided key was invalid. - */ - -/** - * Fires when a device verification can be upgraded to a cross-signing - * verification. The handler should call the `accept` callback in order to - * perform the upgrade. - * @event module:client~MatrixClient#"cross-signing.upgradeDeviceVerifications" - * @param {object} data - * @param {object} data.users The users whose device verifications can be - * upgraded to cross-signing verifications. This will be a map of user IDs - * to objects with the properties `devices` (array of the user's devices - * that verified their cross-signing key), and `crossSigningInfo` (the - * user's cross-signing information) - * @param {object} data.accept a function to call to upgrade the device - * verifications. It should be called with an array of the user IDs who - * should be cross-signed. - */ - -/** - * Fires when a secret has been requested by another client. Clients should - * ensure that the requesting device is allowed to have the secret. For - * example, if the device is not already trusted, a verification should be - * performed before sharing the secret. The client may also wish to prompt the - * user before sharing the secret. - * @event module:client~MatrixClient#"crypto.secrets.request" - * @param {object} data - * @param {string} data.name The name of the secret being requested. - * @param {string} data.user_id (string) The user ID of the client requesting - * the secret. In most cases, this shoud be the same as the client's user. - * @param {string} data.device_id The device ID of the client requesting the secret. - * @param {string} data.request_id The ID of the request. Used to match a - * corresponding `crypto.secrets.request_cancelled`. The request ID will be - * unique per sender, device pair. - * @param {int} data.device_trust: The trust status of the device requesting - * the secret. Will be a bit mask in the same form as returned by {@link - * module:client~MatrixClient#checkDeviceTrust}. - * @param {Function} data.send A function to call to send the secret to the - * requester - */ - /** * Fires when a secret request has been cancelled. If the client is prompting * the user to ask whether they want to share a secret, the prompt can be diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 1652d5a79e4..7bd4831dd55 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -27,38 +27,6 @@ function getPublicKey(keyInfo) { return Object.entries(keyInfo.keys)[0]; } -async function getPrivateKey(self, type, check) { - let error; - let pubkey; - let signing; - do { - [pubkey, signing] = await new Promise((resolve, reject) => { - self.emit("cross-signing.getKey", { - type: type, - error, - done: (key) => { - // FIXME: the key needs to be interpreted? - const signing = new global.Olm.PkSigning(); - const pubkey = signing.init_with_seed(key); - // make sure it agrees with the pubkey that we have - if (pubkey !== getPublicKey(self.keys[type])[1]) { - error = "Key does not match"; - logger.error(error); - signing.free(); - resolve([null, null]); - } else { - resolve([pubkey, signing]); - } - }, - cancel: (error) => { - reject(error || new Error("Cancelled")); - }, - }); - }); - } while (!pubkey); - return [pubkey, signing]; -} - export class CrossSigningInfo extends EventEmitter { /** * Information about a user's cross-signing keys @@ -66,8 +34,10 @@ export class CrossSigningInfo extends EventEmitter { * @class * * @param {string} userId the user that the information is about + * @param {object} callbacks Callbacks used to interact with the app + * Requires getPrivateKey and savePrivateKeys */ - constructor(userId) { + constructor(userId, callbacks) { super(); // you can't change the userId @@ -75,8 +45,42 @@ export class CrossSigningInfo extends EventEmitter { enumerable: true, value: userId, }); + this._callbacks = callbacks || {}; this.keys = {}; - this.fu = true; + this.firstUse = true; + } + + /** + * Calls the app callback to ask for a private key + * @param {string} type The key type ("master", "self_signing", or "user_signing") + * @param {Uint8Array} expectedPubkey The matching public key or undefined to use + * the stored public key for the given key type. + */ + async getPrivateKey(type, expectedPubkey) { + if (!this._callbacks.getPrivateKey) { + throw new Error("No getPrivateKey callback supplied"); + } + + if (expectedPubkey === undefined) { + expectedPubkey = getPublicKey(this.keys[type])[1]; + } + + const privkey = await this._callbacks.getPrivateKey(type, expectedPubkey); + if (!privkey) { + throw new Error( + "getPrivateKey callback for " + type + " returned falsey", + ); + } + const signing = new global.Olm.PkSigning(); + const gotPubkey = signing.init_with_seed(privkey); + if (gotPubkey !== expectedPubkey) { + signing.free(); + throw new Error( + "Key type " + type + " from getPrivateKey callback did not match", + ); + } else { + return [gotPubkey, signing]; + } } static fromStorage(obj, userId) { @@ -92,7 +96,7 @@ export class CrossSigningInfo extends EventEmitter { toStorage() { return { keys: this.keys, - fu: this.fu, + firstUse: this.firstUse, }; } @@ -109,6 +113,10 @@ export class CrossSigningInfo extends EventEmitter { } async resetKeys(level) { + if (!this._callbacks.savePrivateKeys) { + throw new Error("No savePrivateKeys callback supplied"); + } + if (level === undefined || level & 4 || !this.keys.master) { level = CrossSigningLevel.MASTER; } else if (level === 0) { @@ -133,7 +141,7 @@ export class CrossSigningInfo extends EventEmitter { }, }; } else { - [masterPub, masterSigning] = await getPrivateKey(this, "master"); + [masterPub, masterSigning] = await this.getPrivateKey("master"); } if (level & CrossSigningLevel.SELF_SIGNING) { @@ -173,7 +181,7 @@ export class CrossSigningInfo extends EventEmitter { } Object.assign(this.keys, keys); - this.emit("cross-signing.savePrivateKeys", privateKeys); + this._callbacks.savePrivateKeys(privateKeys); } finally { if (masterSigning) { masterSigning.free(); @@ -192,10 +200,10 @@ export class CrossSigningInfo extends EventEmitter { } if (!this.keys.master) { // this is the first key we've seen, so first-use is true - this.fu = true; + this.firstUse = true; } else if (getPublicKey(keys.master)[1] !== this.getId()) { // this is a different key, so first-use is false - this.fu = false; + this.firstUse = false; } // otherwise, same key, so no change signingKeys.master = keys.master; } else if (this.keys.master) { @@ -254,7 +262,7 @@ export class CrossSigningInfo extends EventEmitter { } async signObject(data, type) { - const [pubkey, signing] = await getPrivateKey(this, type); + const [pubkey, signing] = await this.getPrivateKey(type); try { pkSign(data, signing, this.userId, pubkey); return data; @@ -297,12 +305,12 @@ export class CrossSigningInfo extends EventEmitter { && this.getId("self_signing") && this.getId("self_signing") === userCrossSigning.getId("self_signing")) { return CrossSigningVerification.VERIFIED - | (this.fu ? CrossSigningVerification.TOFU + | (this.firstUse ? CrossSigningVerification.TOFU : CrossSigningVerification.UNVERIFIED); } if (!this.keys.user_signing) { - return (userCrossSigning.fu ? CrossSigningVerification.TOFU + return (userCrossSigning.firstUse ? CrossSigningVerification.TOFU : CrossSigningVerification.UNVERIFIED); } @@ -317,7 +325,7 @@ export class CrossSigningInfo extends EventEmitter { } return (userTrusted ? CrossSigningVerification.VERIFIED : CrossSigningVerification.UNVERIFIED) - | (userCrossSigning.fu ? CrossSigningVerification.TOFU + | (userCrossSigning.firstUse ? CrossSigningVerification.TOFU : CrossSigningVerification.UNVERIFIED); } diff --git a/src/crypto/index.js b/src/crypto/index.js index a96e224dc73..4f57f80088c 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -203,11 +203,9 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, this._verificationTransactions = new Map(); - this._crossSigningInfo = new CrossSigningInfo(userId); - this._reEmitter.reEmit(this._crossSigningInfo, [ - "cross-signing.savePrivateKeys", - "cross-signing.getKey", - ]); + this._crossSigningInfo = new CrossSigningInfo( + userId, this._baseApis._cryptoCallbacks, + ); this._secretStorage = new SecretStorage(baseApis); } @@ -236,6 +234,27 @@ Crypto.prototype.requestSecret = function(name, devices) { return this._secretStorage.request(name, devices); }; +/** + * Checks that a given private key matches a given public key + * This can be used by the getPrivateKey callback to verify that the + * private key it is about to supply is the one that was requested. + * + * @param {Uint8Array} privateKey The private key + * @param {Uint8Array} expectedPublicKey The public key supplied by the getPrivateKey callback + * @returns {boolean} true if the key matches, otherwise false + */ +Crypto.prototype.checkPrivateKey = function(privateKey, expectedPublicKey) { + let signing = null; + try { + signing = new global.Olm.PkSigning(); + const gotPubkey = signing.init_with_seed(privateKey); + // make sure it agrees with the given pubkey + return gotPubkey === expectedPublicKey; + } finally { + if (signing) signing.free(); + } +}; + /** * Initialise the crypto module so that it is ready for use * @@ -346,18 +365,28 @@ Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) { users[userId] = upgradeInfo; } } - this.emit("cross-signing.upgradeDeviceVerifications", { - users, - accept: async (upgradeUsers) => { - for (const userId of upgradeUsers) { - if (userId in users) { - await this._baseApis.setDeviceVerified( - userId, users[userId].crossSigningInfo.getId(), - ); + + const shouldUpgradeCb = ( + this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications + ); + if (Object.keys(users).length > 0 && shouldUpgradeCb) { + try { + const usersToUpgrade = await shouldUpgradeCb({users: users}); + if (usersToUpgrade) { + for (const userId of usersToUpgrade) { + if (userId in users) { + await this._baseApis.setDeviceVerified( + userId, users[userId].crossSigningInfo.getId(), + ); + } } } - }, - }); + } catch (e) { + logger.log( + "shouldUpgradeDeviceVerifications threw an error: not upgrading", e, + ); + } + } }; /** @@ -372,7 +401,7 @@ Crypto.prototype._checkForDeviceVerificationUpgrade = async function( ) { // only upgrade if this is the first cross-signing key that we've seen for // them, and if their cross-signing key isn't already verified - if (crossSigningInfo.fu + if (crossSigningInfo.firstUse && !(this._crossSigningInfo.checkUserTrust(crossSigningInfo) & 2)) { const devices = this._deviceList.getRawStoredDevicesForUser(userId); const deviceIds = await this._checkForValidDeviceSignature( @@ -499,9 +528,28 @@ Crypto.prototype.checkDeviceTrust = function(userId, deviceId) { */ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) { if (userId === this._userId) { - await this._checkOwnCrossSigningTrust(); + // An update to our own cross-signing key. + // Get the new key first: + const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + const seenPubkey = newCrossSigning ? newCrossSigning.getId() : null; + const currentPubkey = this._crossSigningInfo.getId(); + const changed = currentPubkey !== seenPubkey; + + if (currentPubkey && seenPubkey && !changed) { + // If it's not changed, just make sure everything is up to date + await this.checkOwnCrossSigningTrust(); + } else { + this.emit("cross-signing.keysChanged", {}); + // We'll now be in a state where cross-signing on the account is not trusted + // because our locally stored cross-signing keys will not match the ones + // on the server for our account. The app must call checkOwnCrossSigningTrust() + // to fix this. + // XXX: Do we need to do something to emit events saying every device has become + // untrusted? + } } else { await this._checkDeviceVerifications(userId); + this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); } }; @@ -509,7 +557,7 @@ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) { * Check the copy of our cross-signing key that we have in the device list and * see if we can get the private key. If so, mark it as trusted. */ -Crypto.prototype._checkOwnCrossSigningTrust = async function() { +Crypto.prototype.checkOwnCrossSigningTrust = async function() { const userId = this._userId; // If we see an update to our own master key, check it against the master @@ -527,51 +575,27 @@ Crypto.prototype._checkOwnCrossSigningTrust = async function() { const seenPubkey = newCrossSigning.getId(); const changed = this._crossSigningInfo.getId() !== seenPubkey; - let privkey; if (changed) { // try to get the private key if the master key changed logger.info("Got new master key", seenPubkey); - let error; - do { - privkey = await new Promise((resolve, reject) => { - this._baseApis.emit("cross-signing.newKey", { - publicKey: seenPubkey, - type: "master", - error, - done: (key) => { - // check key matches - const signing = new global.Olm.PkSigning(); - try { - const pubkey = signing.init_with_seed(key); - if (pubkey !== seenPubkey) { - error = "Key does not match"; - logger.info("Key does not match: got " + pubkey - + " expected " + seenPubkey); - return; - } - } finally { - signing.free(); - } - resolve(key); - }, - cancel: (error) => { - // FIXME: should we forcibly push our copy of the key - // to the server if the client rejects the server's - // key? - reject(error || new Error("Cancelled by user")); - }, - }); - }); - } while (!privkey); - this._baseApis.emit("cross-signing.savePrivateKeys", {master: privkey}); + let signing = null; + try { + const ret = await this._crossSigningInfo.getPrivateKey( + 'master', seenPubkey, + ); + signing = ret[1]; + } finally { + signing.free(); + } - logger.info("Got private key"); + logger.info("Got matching private key from callback for new public master key"); } const oldSelfSigningId = this._crossSigningInfo.getId("self_signing"); const oldUserSigningId = this._crossSigningInfo.getId("user_signing"); + // Update the version of our keys in our cross-siging object and the local store this._crossSigningInfo.setKeys(newCrossSigning.keys); await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], @@ -599,13 +623,14 @@ Crypto.prototype._checkOwnCrossSigningTrust = async function() { await this._signObject(this._crossSigningInfo.keys.master); keySignatures[this._crossSigningInfo.getId()] = this._crossSigningInfo.keys.master; - this._baseApis.emit("cross-signing.keysChanged", {}); } if (Object.keys(keySignatures).length) { await this._baseApis.uploadKeySignatures({[this._userId]: keySignatures}); } + this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); + // Now we may be able to trust our key backup await this.checkKeyBackup(); // FIXME: if we previously trusted the backup, should we automatically sign @@ -625,19 +650,20 @@ Crypto.prototype._checkDeviceVerifications = async function(userId) { const upgradeInfo = await this._checkForDeviceVerificationUpgrade( userId, crossSigningInfo, ); - if (upgradeInfo) { - this.emit("cross-signing.upgradeDeviceVerifications", { + const shouldUpgradeCb = ( + this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications + ); + if (upgradeInfo && shouldUpgradeCb) { + const usersToUpgrade = await shouldUpgradeCb({ users: { [userId]: upgradeInfo, - }, - accept: async (users) => { - if (users.includes(userId)) { - await this._baseApis.setDeviceVerified( - userId, crossSigningInfo.getId(), - ); - } - }, + } }); + if (usersToUpgrade.includes(userId)) { + await this._baseApis.setDeviceVerified( + userId, crossSigningInfo.getId(), + ); + } } } } @@ -1161,12 +1187,14 @@ Crypto.prototype.setDeviceVerification = async function( if (verified) { const device = await this._crossSigningInfo.signUser(xsk); if (device) { - this._baseApis.uploadKeySignatures({ + await this._baseApis.uploadKeySignatures({ [userId]: { [deviceId]: device, }, }); + // XXX: we'll need to wait for the device list to be updated } + this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); return device; } else { // FIXME: ??? @@ -1212,15 +1240,18 @@ Crypto.prototype.setDeviceVerification = async function( userId, DeviceInfo.fromStorage(dev, deviceId), ); if (device) { - this._baseApis.uploadKeySignatures({ + await this._baseApis.uploadKeySignatures({ [userId]: { [deviceId]: device, }, }); + // XXX: we'll need to wait for the device list to be updated } } - return DeviceInfo.fromStorage(dev, deviceId); + const deviceObj = DeviceInfo.fromStorage(dev, deviceId); + this.emit("deviceVerificationChanged", userId, deviceId, deviceObj); + return deviceObj; }; From fabfe16d4526520452f1e2a7f318b772bc5293fa Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Nov 2019 12:35:39 +0000 Subject: [PATCH 49/97] lint --- spec/unit/crypto/cross-signing.spec.js | 1 - src/crypto/index.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 6702b4f5a7b..219df41ca5e 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -689,7 +689,6 @@ describe("Cross Signing", function() { const bob = await makeTestClient( {userId: "@bob:example.com", deviceId: "Dynabook"}, ); - const privateKeys = {}; bob.uploadDeviceSigningKeys = async () => {}; bob.uploadKeySignatures = async () => {}; diff --git a/src/crypto/index.js b/src/crypto/index.js index 4f57f80088c..686788fb870 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -657,7 +657,7 @@ Crypto.prototype._checkDeviceVerifications = async function(userId) { const usersToUpgrade = await shouldUpgradeCb({ users: { [userId]: upgradeInfo, - } + }, }); if (usersToUpgrade.includes(userId)) { await this._baseApis.setDeviceVerified( From 12627022d1b4ada4c100340308b148e14a337e86 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Nov 2019 15:18:16 +0000 Subject: [PATCH 50/97] Convert sas verification test to callbacks --- spec/unit/crypto/verification/sas.spec.js | 29 +++++++++-------------- spec/unit/crypto/verification/util.js | 7 ++++++ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index 89af121d4c9..4175455cf72 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -271,30 +271,16 @@ describe("SAS verification", function() { }); it("should verify a cross-signing key", async function() { - const privateKeys = {}; alice.httpBackend.when('POST', '/keys/device_signing/upload').respond( 200, {}, ); alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {}); - alice.client.on("cross-signing.savePrivateKeys", function(e) { - privateKeys.alice = e; - }); - alice.client.on("cross-signing.getKey", function(e) { - e.done(privateKeys.alice[e.type]); - }); alice.httpBackend.flush(undefined, 2); await alice.client.resetCrossSigningKeys(); - bob.client.on("cross-signing.savePrivateKeys", function(e) { - privateKeys.bob = e; - }); bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {}); bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {}); bob.httpBackend.flush(undefined, 2); - bob.client.on("cross-signing.getKey", function(e) { - e.done(privateKeys.bob[e.type]); - }); - await bob.client.resetCrossSigningKeys(); bob.client._crypto._deviceList.storeCrossSigningForUser( @@ -316,13 +302,20 @@ describe("SAS verification", function() { }, }); - await Promise.all([ + const verifyProm = Promise.all([ aliceVerifier.verify(), - bobPromise.then((verifier) => verifier.verify()), - alice.httpBackend.flush(), - bob.httpBackend.flush(), + bobPromise.then((verifier) => { + bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {}); + bob.httpBackend.flush(undefined, 2); + return verifier.verify(); + }), ]); + await alice.httpBackend.flush(undefined, 1); + console.log("alice reqs flushed"); + + await verifyProm; + expect(alice.client.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(1); expect(bob.client.checkUserTrust("@alice:example.com")).toBe(6); expect(bob.client.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(1); diff --git a/spec/unit/crypto/verification/util.js b/spec/unit/crypto/verification/util.js index 4ffa305537d..2dbcb9cb5fa 100644 --- a/spec/unit/crypto/verification/util.js +++ b/spec/unit/crypto/verification/util.js @@ -69,6 +69,13 @@ export async function makeTestClients(userInfos, options) { }; for (const userInfo of userInfos) { + let keys = {}; + if (!options) options = {}; + if (!options.cryptoCallbacks) options.cryptoCallbacks = {}; + if (!options.cryptoCallbacks.savePrivateKeys) { + options.cryptoCallbacks.savePrivateKeys = k => { keys = k; }; + options.cryptoCallbacks.getPrivateKey = typ => keys[typ]; + } const testClient = new TestClient( userInfo.userId, userInfo.deviceId, undefined, undefined, options, From 03fe4afe32a2b4a846c55c2a6d79714beaf1bab5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Nov 2019 15:20:07 +0000 Subject: [PATCH 51/97] lint --- spec/unit/crypto/verification/sas.spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index 4175455cf72..8a5eaa51867 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -305,7 +305,9 @@ describe("SAS verification", function() { const verifyProm = Promise.all([ aliceVerifier.verify(), bobPromise.then((verifier) => { - bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {}); + bob.httpBackend.when( + 'POST', '/keys/signatures/upload', + ).respond(200, {}); bob.httpBackend.flush(undefined, 2); return verifier.verify(); }), From 3a983271d62b4d468ab4d3bf9420acbf77766e25 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Nov 2019 16:21:53 +0000 Subject: [PATCH 52/97] add comments --- src/crypto/CrossSigning.js | 2 ++ src/crypto/index.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 7bd4831dd55..4000104c17d 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -358,6 +358,8 @@ function deviceToObject(device, userId) { } export const CrossSigningLevel = { + // NB. The actual master key is 4 but you must, by definition, reset all + // keys if you reset the master key so this is essentially 'all keys' MASTER: 7, SELF_SIGNING: 1, USER_SIGNING: 2, diff --git a/src/crypto/index.js b/src/crypto/index.js index 686788fb870..48a5745c74f 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -493,6 +493,8 @@ Crypto.prototype.checkUserTrust = function(userId) { if (!userCrossSigning) { return 0; } + // We shift the result from CrossSigningInfo.checkUserTrust so this + // function's return is consistent with checkDeviceTrust return this._crossSigningInfo.checkUserTrust(userCrossSigning) << 1; }; From 6f8d9c46936c29df5d30a21dbce1ab20a4c6ef00 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Nov 2019 16:45:01 +0000 Subject: [PATCH 53/97] Rename getPrivateKeys to getCrossSigningKeys --- src/client.js | 6 +++--- src/crypto/CrossSigning.js | 24 ++++++++++++------------ src/crypto/index.js | 6 +++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/client.js b/src/client.js index 4240d65936b..54d4f038a76 100644 --- a/src/client.js +++ b/src/client.js @@ -178,8 +178,8 @@ function keyFromRecoverySession(session, decryptionKey) { * * @param {object} opts.cryptoCallbacks Optional. Callbacks for crypto and cross-signing. * - * @param {function} [opts.cryptoCallbacks.getPrivateKey] - * Optional (required for cross-signing). Function to call when a private key is needed. + * @param {function} [opts.cryptoCallbacks.getCrossSigningKey] + * Optional (required for cross-signing). Function to call when a cross-signing private key is needed. * Args: * {string} type The type of key needed. Will be one of "master", * "self_signing", or "user_signing" @@ -190,7 +190,7 @@ function keyFromRecoverySession(session, decryptionKey) { * Should return a promise that resolves with the private key as a * UInt8Array or rejects with an error. * - * @param {function} [opts.cryptoCallbacks.savePrivateKeys] + * @param {function} [opts.cryptoCallbacks.saveCrossSigningKeys] * Optional (required for cross-signing). Called when new private keys * for cross-signing need to be saved. * Args: diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 4000104c17d..880931d0046 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -35,7 +35,7 @@ export class CrossSigningInfo extends EventEmitter { * * @param {string} userId the user that the information is about * @param {object} callbacks Callbacks used to interact with the app - * Requires getPrivateKey and savePrivateKeys + * Requires getCrossSigningKey and saveCrossSigningKeys */ constructor(userId, callbacks) { super(); @@ -56,19 +56,19 @@ export class CrossSigningInfo extends EventEmitter { * @param {Uint8Array} expectedPubkey The matching public key or undefined to use * the stored public key for the given key type. */ - async getPrivateKey(type, expectedPubkey) { - if (!this._callbacks.getPrivateKey) { - throw new Error("No getPrivateKey callback supplied"); + async getCrossSigningKey(type, expectedPubkey) { + if (!this._callbacks.getCrossSigningKey) { + throw new Error("No getCrossSigningKey callback supplied"); } if (expectedPubkey === undefined) { expectedPubkey = getPublicKey(this.keys[type])[1]; } - const privkey = await this._callbacks.getPrivateKey(type, expectedPubkey); + const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey); if (!privkey) { throw new Error( - "getPrivateKey callback for " + type + " returned falsey", + "getCrossSigningKey callback for " + type + " returned falsey", ); } const signing = new global.Olm.PkSigning(); @@ -76,7 +76,7 @@ export class CrossSigningInfo extends EventEmitter { if (gotPubkey !== expectedPubkey) { signing.free(); throw new Error( - "Key type " + type + " from getPrivateKey callback did not match", + "Key type " + type + " from getCrossSigningKey callback did not match", ); } else { return [gotPubkey, signing]; @@ -113,8 +113,8 @@ export class CrossSigningInfo extends EventEmitter { } async resetKeys(level) { - if (!this._callbacks.savePrivateKeys) { - throw new Error("No savePrivateKeys callback supplied"); + if (!this._callbacks.saveCrossSigningKeys) { + throw new Error("No saveCrossSigningKeys callback supplied"); } if (level === undefined || level & 4 || !this.keys.master) { @@ -141,7 +141,7 @@ export class CrossSigningInfo extends EventEmitter { }, }; } else { - [masterPub, masterSigning] = await this.getPrivateKey("master"); + [masterPub, masterSigning] = await this.getCrossSigningyKey("master"); } if (level & CrossSigningLevel.SELF_SIGNING) { @@ -181,7 +181,7 @@ export class CrossSigningInfo extends EventEmitter { } Object.assign(this.keys, keys); - this._callbacks.savePrivateKeys(privateKeys); + this._callbacks.saveCrossSigningKeys(privateKeys); } finally { if (masterSigning) { masterSigning.free(); @@ -262,7 +262,7 @@ export class CrossSigningInfo extends EventEmitter { } async signObject(data, type) { - const [pubkey, signing] = await this.getPrivateKey(type); + const [pubkey, signing] = await this.getCrossSigningKey(type); try { pkSign(data, signing, this.userId, pubkey); return data; diff --git a/src/crypto/index.js b/src/crypto/index.js index 48a5745c74f..bbe7c8be581 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -236,11 +236,11 @@ Crypto.prototype.requestSecret = function(name, devices) { /** * Checks that a given private key matches a given public key - * This can be used by the getPrivateKey callback to verify that the + * This can be used by the getCrossSigningKey callback to verify that the * private key it is about to supply is the one that was requested. * * @param {Uint8Array} privateKey The private key - * @param {Uint8Array} expectedPublicKey The public key supplied by the getPrivateKey callback + * @param {Uint8Array} expectedPublicKey The public key supplied by the getCrossSigningKey callback * @returns {boolean} true if the key matches, otherwise false */ Crypto.prototype.checkPrivateKey = function(privateKey, expectedPublicKey) { @@ -583,7 +583,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() { let signing = null; try { - const ret = await this._crossSigningInfo.getPrivateKey( + const ret = await this._crossSigningInfo.getCrossSigningKey( 'master', seenPubkey, ); signing = ret[1]; From a98e6964efad725a74e745f39bc184035b8b0e2a Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Nov 2019 16:51:49 +0000 Subject: [PATCH 54/97] Missed bits of callback renaming --- spec/unit/crypto/backup.spec.js | 8 ++++---- spec/unit/crypto/cross-signing.spec.js | 8 ++++---- spec/unit/crypto/verification/util.js | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index e829ef70e05..6f4d6e69e3a 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -86,11 +86,11 @@ const BACKUP_INFO = { const keys = {}; -function getPrivateKey(type) { +function getCrossSigningKey(type) { return keys[type]; } -function savePrivateKeys(k) { +function saveCrossSigningKeys(k) { Object.assign(keys, k); } @@ -119,7 +119,7 @@ function makeTestClient(sessionStore, cryptoStore) { deviceId: "device", sessionStore: sessionStore, cryptoStore: cryptoStore, - cryptoCallbacks: { getPrivateKey, savePrivateKeys }, + cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys }, }); } @@ -330,7 +330,7 @@ describe("MegolmBackup", function() { let privateKeys; client.uploadDeviceSigningKeys = async function(e) {return;}; client.uploadKeySignatures = async function(e) {return;}; - client.on("cross-signing.savePrivateKeys", function(e) { + client.on("cross-signing.saveCrossSigningKeys", function(e) { privateKeys = e; }); client.on("cross-signing.getKey", function(e) { diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 219df41ca5e..efc7cea2ffb 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -29,17 +29,17 @@ import {HttpResponse, setHttpResponses} from '../../test-utils'; async function makeTestClient(userInfo, options, keys) { if (!keys) keys = {}; - function getPrivateKey(type) { + function getCrossSigningKey(type) { return keys[type]; } - function savePrivateKeys(k) { + function saveCrossSigningKeys(k) { Object.assign(keys, k); } if (!options) options = {}; options.cryptoCallbacks = Object.assign( - {}, { getPrivateKey, savePrivateKeys }, options.cryptoCallbacks || {}, + {}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {}, ); const client = (new TestClient( userInfo.userId, userInfo.deviceId, undefined, undefined, options, @@ -127,7 +127,7 @@ describe("Cross Signing", function() { { cryptoCallbacks: { // will be called to sign our own device - getPrivateKey: type => { + getCrossSigningKey: type => { if (type === 'master') { return masterKey; } else { diff --git a/spec/unit/crypto/verification/util.js b/spec/unit/crypto/verification/util.js index 2dbcb9cb5fa..2c6ccca7677 100644 --- a/spec/unit/crypto/verification/util.js +++ b/spec/unit/crypto/verification/util.js @@ -72,9 +72,9 @@ export async function makeTestClients(userInfos, options) { let keys = {}; if (!options) options = {}; if (!options.cryptoCallbacks) options.cryptoCallbacks = {}; - if (!options.cryptoCallbacks.savePrivateKeys) { - options.cryptoCallbacks.savePrivateKeys = k => { keys = k; }; - options.cryptoCallbacks.getPrivateKey = typ => keys[typ]; + if (!options.cryptoCallbacks.saveCrossSigningKeys) { + options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; }; + options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ]; } const testClient = new TestClient( userInfo.userId, userInfo.deviceId, undefined, undefined, From 4c651c15ea28657de55db85e3843dcb7de0928e0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Nov 2019 20:01:11 +0000 Subject: [PATCH 55/97] Convert secrets events to callbacks too --- spec/unit/crypto/secrets.spec.js | 38 +++--- src/client.js | 14 +++ src/crypto/Secrets.js | 197 +++++++++++++++---------------- src/crypto/index.js | 4 +- 4 files changed, 137 insertions(+), 116 deletions(-) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index 4024fc52f29..be23c6d6182 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -45,15 +45,25 @@ describe("Secrets", function() { }); it("should store and retrieve a secret", async function() { + const decryption = new global.Olm.PkDecryption(); + const pubkey = decryption.generate_key(); + const privkey = decryption.get_private_key(); + + const getKey = expect.createSpy().andCall(e => { + expect(Object.keys(e.keys)).toEqual(["abc"]); + return ['abc', privkey]; + }); + const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + getSecretStorageKey: getKey, + }, + }, ); const secretStorage = alice._crypto._secretStorage; - const decryption = new global.Olm.PkDecryption(); - const pubkey = decryption.generate_key(); - const privkey = decryption.get_private_key(); - alice.setAccountData = async function(eventType, contents, callback) { alice.store.storeAccountDataEvents([ new MatrixEvent({ @@ -81,13 +91,6 @@ describe("Secrets", function() { await secretStorage.store("foo", "bar", ["abc"]); expect(secretStorage.isStored("foo")).toBe(true); - - const getKey = expect.createSpy().andCall(function(e) { - expect(Object.keys(e.keys)).toEqual(["abc"]); - e.done("abc", privkey); - }); - alice.once("crypto.secrets.getKey", getKey); - expect(await secretStorage.get("foo")).toBe("bar"); expect(getKey).toHaveBeenCalled(); @@ -99,6 +102,14 @@ describe("Secrets", function() { {userId: "@alice:example.com", deviceId: "Osborne2"}, {userId: "@alice:example.com", deviceId: "VAX"}, ], + { + cryptoCallbacks: { + onSecretRequested: e => { + expect(e.name).toBe("foo"); + return "bar"; + }, + }, + }, ); const vaxDevice = vax.client._crypto._olmDevice; @@ -128,11 +139,6 @@ describe("Secrets", function() { }, }); - vax.client.once("crypto.secrets.request", function(e) { - expect(e.name).toBe("foo"); - e.send("bar"); - }); - await osborne2Device.generateOneTimeKeys(1); const otks = (await osborne2Device.getOneTimeKeys()).curve25519; await osborne2Device.markKeysAsPublished(); diff --git a/src/client.js b/src/client.js index 54d4f038a76..8b23a9a7545 100644 --- a/src/client.js +++ b/src/client.js @@ -210,6 +210,20 @@ function keyFromRecoverySession(session, decryptionKey) { * Should return a promise which resolves with an array of the user IDs who * should be cross-signed. * + * @param {function} [opts.cryptoCallbacks.getSecretStorageKey] + * Optional. Function called when an encryption key for secret storage + * is required. One or more keys will be described in the keys object. + * The callback function should return with an array of: + * [, ] or null if it cannot provide + * any of the keys. + * Args: + * {object} keys Information about the keys: + * { + * : { + * pubkey: {UInt8Array} + * } + * } + * * @param {function} [opts.cryptoCallbacks.onSecretRequested] * Optional. Function called when a request for a secret is received from another * device. diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index aebf869bfc0..738e9b0248a 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -26,11 +26,12 @@ import { encodeRecoveryKey } from './recoverykey'; * @module crypto/Secrets */ export default class SecretStorage extends EventEmitter { - constructor(baseApis) { + constructor(baseApis, cryptoCallbacks) { super(); this._baseApis = baseApis; this._requests = {}; this._incomingRequests = {}; + this._cryptoCallbacks = cryptoCallbacks; } /** @@ -159,7 +160,7 @@ export default class SecretStorage extends EventEmitter { const secretContent = secretInfo.getContent(); if (!secretContent.encrypted) { - return; + throw new Error("Content is not encrypted!"); } // get possible keys to decrypt @@ -182,63 +183,13 @@ export default class SecretStorage extends EventEmitter { } } - // fetch private key from app - let decryption; let keyName; - let cleanUp; - let error; - do { - [keyName, decryption, cleanUp] = await new Promise((resolve, reject) => { - this._baseApis.emit("crypto.secrets.getKey", { - keys, - error, - done: function(keyName, key) { - // FIXME: interpret key? - if (!keys[keyName]) { - error = "Unknown key (your app is broken)"; - resolve([]); - } - switch (keys[keyName].algorithm) { - case "m.secret_storage.v1.curve25519-aes-sha2": - { - const decryption = new global.Olm.PkDecryption(); - try { - const pubkey = decryption.init_with_private_key(key); - if (pubkey !== keys[keyName].pubkey) { - error = "Key does not match"; - resolve([]); - return; - } - } catch (e) { - decryption.free(); - error = "Invalid key"; - resolve([]); - return; - } - resolve([ - keyName, - decryption, - decryption.free.bind(decryption), - ]); - break; - } - default: - error = "The universe is broken"; - resolve([]); - } - }, - cancel: function(e) { - reject(e || new Error("Cancelled")); - }, - }); - }); - if (error) { - logger.error("Error getting private key:", error); - } - } while (!keyName); - - // decrypt secret + let decryption; try { + // fetch private key from app + [keyName, decryption] = await this._getSecretStorageKey(keys); + + // decrypt secret const encInfo = secretContent.encrypted[keyName]; switch (keys[keyName].algorithm) { case "m.secret_storage.v1.curve25519-aes-sha2": @@ -247,7 +198,7 @@ export default class SecretStorage extends EventEmitter { ); } } finally { - cleanUp(); + if (decryption) decryption.free(); } } @@ -358,7 +309,7 @@ export default class SecretStorage extends EventEmitter { }; } - _onRequestReceived(event) { + async _onRequestReceived(event) { const sender = event.getSender(); const content = event.getContent(); if (sender !== this._baseApis.getUserId() @@ -389,52 +340,55 @@ export default class SecretStorage extends EventEmitter { // check if we have the secret logger.info("received request for secret (" + sender + ", " + deviceId + ", " + content.request_id + ")"); - this._baseApis.emit("crypto.secrets.request", { + if (!this._cryptoCallbacks.onSecretRequested) { + return; + } + const secret = await this._cryptoCallbacks.onSecretRequested({ user_id: sender, device_id: deviceId, request_id: content.request_id, name: content.name, device_trust: this._baseApis.checkDeviceTrust(sender, deviceId), - send: async (secret) => { - const payload = { - type: "m.secret.send", - content: { - request_id: content.request_id, - secret: secret, - }, - }; - const encryptedContent = { - algorithm: olmlib.OLM_ALGORITHM, - sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key, - ciphertext: {}, - }; - await olmlib.ensureOlmSessionsForDevices( - this._baseApis._crypto._olmDevice, - this._baseApis, - { - [sender]: [ - await this._baseApis.getStoredDevice(sender, deviceId), - ], - }, - ); - await olmlib.encryptMessageForDevice( - encryptedContent.ciphertext, - this._baseApis.getUserId(), - this._baseApis.deviceId, - this._baseApis._crypto._olmDevice, - sender, - this._baseApis._crypto.getStoredDevice(sender, deviceId), - payload, - ); - const contentMap = { - [sender]: { - [deviceId]: encryptedContent, - }, - }; - - this._baseApis.sendToDevice("m.room.encrypted", contentMap); - }, }); + if (secret) { + const payload = { + type: "m.secret.send", + content: { + request_id: content.request_id, + secret: secret, + }, + }; + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + await olmlib.ensureOlmSessionsForDevices( + this._baseApis._crypto._olmDevice, + this._baseApis, + { + [sender]: [ + await this._baseApis.getStoredDevice(sender, deviceId), + ], + }, + ); + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this._baseApis.getUserId(), + this._baseApis.deviceId, + this._baseApis._crypto._olmDevice, + sender, + this._baseApis._crypto.getStoredDevice(sender, deviceId), + payload, + ); + const contentMap = { + [sender]: { + [deviceId]: encryptedContent, + }, + }; + + this._baseApis.sendToDevice("m.room.encrypted", contentMap); + } } } @@ -468,4 +422,49 @@ export default class SecretStorage extends EventEmitter { requestControl.resolve(content.secret); } } + + async _getSecretStorageKey(keys) { + if (!this._cryptoCallbacks.getSecretStorageKey) { + throw new Error("No getSecretStorageKey callback supplied"); + } + + const returned = await Promise.resolve( + this._cryptoCallbacks.getSecretStorageKey({keys}), + ); + + if (!returned) { + throw new Error("getSecretStorageKey callback returned falsey"); + } + if (returned.length < 2) { + throw new Error("getSecretStorageKey callback returned invalid data"); + } + + const [keyName, privateKey] = returned; + if (!keys[keyName]) { + throw new Error("App returned unknown key from getSecretStorageKey!"); + } + + switch (keys[keyName].algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + { + const decryption = new global.Olm.PkDecryption(); + let pubkey; + try { + pubkey = decryption.init_with_private_key(privateKey); + } catch (e) { + decryption.free(); + throw new Error("getSecretStorageKey callback returned invalid key"); + } + if (pubkey !== keys[keyName].pubkey) { + decryption.free(); + throw new Error( + "getSecretStorageKey callback returned incorrect key", + ); + } + return [keyName, decryption]; + } + default: + throw new Error("Unknown key type: " + keys[keyName].algorithm); + } + } } diff --git a/src/crypto/index.js b/src/crypto/index.js index bbe7c8be581..651f9df4d98 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -207,7 +207,9 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, userId, this._baseApis._cryptoCallbacks, ); - this._secretStorage = new SecretStorage(baseApis); + this._secretStorage = new SecretStorage( + baseApis, this._baseApis._cryptoCallbacks, + ); } utils.inherits(Crypto, EventEmitter); From 9bc185d459ce5d599c9187d3827f39da7e3a0ea9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 12 Nov 2019 13:21:37 +0000 Subject: [PATCH 56/97] Fix what was probablyt a c+p fail --- src/crypto/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 651f9df4d98..9ba5d94391d 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -214,7 +214,7 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, utils.inherits(Crypto, EventEmitter); Crypto.prototype.addSecretKey = function(algorithm, opts, keyID) { - return this._secretStorage.store(algorithm, opts, keyID); + return this._secretStorage.addKey(algorithm, opts, keyID); }; Crypto.prototype.storeSecret = function(name, secret, keys) { From c97a87d1f6bc3ea7affe0a073aa0c1f7f4f3c442 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 12 Nov 2019 14:07:05 +0000 Subject: [PATCH 57/97] Throw if an unknown key is specified It's probably important that the app knows if a secret isn't going to be stored under one or more of the keys it thought it was going to be stored under. Also add a test to assert it. --- spec/unit/crypto/secrets.spec.js | 14 ++++++++++++++ src/crypto/Secrets.js | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index be23c6d6182..f650d38fb61 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -96,6 +96,20 @@ describe("Secrets", function() { expect(getKey).toHaveBeenCalled(); }); + it("should throw if given a key that doesn't exist", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + + try { + await alice.storeSecret("foo", "bar", ["this secret does not exist"]); + // should be able to use expect(...).toThrow() but mocha still fails + // the test even when it throws for reasons I have no inclination to debug + expect(true).toBeFalsy(); + } catch (e) { + } + }); + it("should request secrets from other clients", async function() { const [osborne2, vax] = await makeTestClients( [ diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 738e9b0248a..abbfe02a504 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -116,7 +116,7 @@ export default class SecretStorage extends EventEmitter { "m.secret_storage.key." + keyName, ); if (!keyInfo) { - continue; + throw new Error("Unknown key: " +keyName); } const keyInfoContent = keyInfo.getContent(); // FIXME: check signature of key info From 26aa3d3ce732634e8a7c69ebf8f9df99efed64fb Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Nov 2019 14:09:40 +0000 Subject: [PATCH 58/97] Support default keys --- spec/unit/crypto/secrets.spec.js | 47 ++++++++++++++++++++++++++++++++ src/client.js | 2 ++ src/crypto/Secrets.js | 30 +++++++++++++++++++- src/crypto/index.js | 8 ++++++ 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index f650d38fb61..1ac1430851f 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -110,6 +110,53 @@ describe("Secrets", function() { } }); + it("should refuse to encrypt with zero keys", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + + try { + await alice.storeSecret("foo", "bar", []); + expect(true).toBeFalsy(); + } catch (e) { + } + }); + + it("should encrypt with default key if keys is null", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + alice.setAccountData = async function(eventType, contents, callback) { + alice.store.storeAccountDataEvents([ + new MatrixEvent({ + type: eventType, + content: contents, + }), + ]); + }; + + const newKeyId = await alice.addSecretKey( + 'm.secret_storage.v1.curve25519-aes-sha2', + ); + await alice.setDefaultKeyId(newKeyId); + await alice.storeSecret("foo", "bar"); + + const accountData = alice.getAccountData('foo'); + expect(accountData.getContent().encrypted).toBeTruthy(); + }); + + it("should refuse to encrypt if no keys given and no default key", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + + try { + await alice.storeSecret("foo", "bar"); + expect(true).toBeFalsy(); + } catch (e) { + } + }); + it("should request secrets from other clients", async function() { const [osborne2, vax] = await makeTestClients( [ diff --git a/src/client.js b/src/client.js index 8b23a9a7545..60d66cede62 100644 --- a/src/client.js +++ b/src/client.js @@ -1113,6 +1113,8 @@ wrapCryptoFuncs(MatrixClient, [ "getSecret", "isSecretStored", "requestSecret", + "getDefaultKeyId", + "setDefaultKeyId", ]); /** diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index abbfe02a504..8d45caaa338 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -34,6 +34,19 @@ export default class SecretStorage extends EventEmitter { this._cryptoCallbacks = cryptoCallbacks; } + getDefaultKeyId() { + const defaultKeyEvent = this._baseApis.getAccountData('m.secret_storage.default_key'); + if (!defaultKeyEvent) return null; + return defaultKeyEvent.getContent().key; + } + + setDefaultKeyId(keyId) { + return this._baseApis.setAccountData( + 'm.secret_storage.default_key', + { key: keyId }, + ); + } + /** * Add a key for encrypting secrets. * @@ -49,6 +62,8 @@ export default class SecretStorage extends EventEmitter { async addKey(algorithm, opts, keyID) { const keyData = {algorithm}; + if (!opts) opts = {}; + if (opts.name) { keyData.name = opts.name; } @@ -86,7 +101,7 @@ export default class SecretStorage extends EventEmitter { if (!keyID) { do { keyID = randomString(32); - } while (!this._baseApis.getAccountData(`m.secret_storage.key.${keyID}`)); + } while (this._baseApis.getAccountData(`m.secret_storage.key.${keyID}`)); } // FIXME: sign keyData? @@ -106,10 +121,23 @@ export default class SecretStorage extends EventEmitter { * @param {string} name The name of the secret * @param {string} secret The secret contents. * @param {Array} keys The IDs of the keys to use to encrypt the secret + * or null/undefined to use the default key. */ async store(name, secret, keys) { const encrypted = {}; + if (!keys) { + const defaultKeyId = this.getDefaultKeyId(); + if (!defaultKeyId) { + throw new Error("No keys specified and no default key present"); + } + keys = [defaultKeyId]; + } + + if (keys.length === 0) { + throw new Error("Zero keys given to encrypt with!"); + } + for (const keyName of keys) { // get key information from key storage const keyInfo = this._baseApis.getAccountData( diff --git a/src/crypto/index.js b/src/crypto/index.js index 9ba5d94391d..d90a5b9cb63 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -236,6 +236,14 @@ Crypto.prototype.requestSecret = function(name, devices) { return this._secretStorage.request(name, devices); }; +Crypto.prototype.getDefaultKeyId = function() { + return this._secretStorage.getDefaultKeyId(); +}; + +Crypto.prototype.setDefaultKeyId = function(k) { + return this._secretStorage.setDefaultKeyId(k); +}; + /** * Checks that a given private key matches a given public key * This can be used by the getCrossSigningKey callback to verify that the From d12c56a623337ea2847da379da29b9906e03f6f4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Nov 2019 14:11:50 +0000 Subject: [PATCH 59/97] lint --- src/crypto/Secrets.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 8d45caaa338..41aace51049 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -35,7 +35,9 @@ export default class SecretStorage extends EventEmitter { } getDefaultKeyId() { - const defaultKeyEvent = this._baseApis.getAccountData('m.secret_storage.default_key'); + const defaultKeyEvent = this._baseApis.getAccountData( + 'm.secret_storage.default_key', + ); if (!defaultKeyEvent) return null; return defaultKeyEvent.getContent().key; } From 1798f3921f376874d45e734511d849d00fc33da8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Nov 2019 14:42:08 +0000 Subject: [PATCH 60/97] Make setDeafultKeyId wait for event --- spec/unit/crypto/secrets.spec.js | 4 +++- src/crypto/Secrets.js | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index 1ac1430851f..fcf8f605e94 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -138,7 +138,9 @@ describe("Secrets", function() { const newKeyId = await alice.addSecretKey( 'm.secret_storage.v1.curve25519-aes-sha2', ); - await alice.setDefaultKeyId(newKeyId); + // we don't await on this because it waits for the event to come down the sync + // which won't happen in the test setup + alice.setDefaultKeyId(newKeyId); await alice.storeSecret("foo", "bar"); const accountData = alice.getAccountData('foo'); diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 41aace51049..cadaa3c85ec 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -43,10 +43,23 @@ export default class SecretStorage extends EventEmitter { } setDefaultKeyId(keyId) { - return this._baseApis.setAccountData( - 'm.secret_storage.default_key', - { key: keyId }, - ); + return new Promise((resolve) => { + const listener = (ev) => { + if ( + ev.getType() === 'm.secret_storage.default_key' && + ev.getContent().key === keyId + ) { + this._baseApis.removeListener('accountData', listener); + resolve(); + } + }; + this._baseApis.on('accountData', listener); + + this._baseApis.setAccountData( + 'm.secret_storage.default_key', + { key: keyId }, + ); + }); } /** From 7218e31a9c252dafc5a8812ab66d45608fa754e7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Nov 2019 17:52:24 +0000 Subject: [PATCH 61/97] Sign & verify SSSS keys --- spec/unit/crypto/secrets.spec.js | 36 ++++++++++++++++++++++++++++---- src/crypto/CrossSigning.js | 27 +++++++++++++++++------- src/crypto/Secrets.js | 25 +++++++++++++++++----- src/crypto/index.js | 2 +- 4 files changed, 73 insertions(+), 17 deletions(-) diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js index fcf8f605e94..0fa1aa56e09 100644 --- a/spec/unit/crypto/secrets.spec.js +++ b/spec/unit/crypto/secrets.spec.js @@ -49,6 +49,18 @@ describe("Secrets", function() { const pubkey = decryption.generate_key(); const privkey = decryption.get_private_key(); + const signing = new global.Olm.PkSigning(); + const signingKey = signing.generate_seed(); + const signingPubKey = signing.init_with_seed(signingKey); + + const signingkeyInfo = { + user_id: "@alice:example.com", + usage: ['master'], + keys: { + ['ed25519:' + signingPubKey]: signingPubKey, + }, + }; + const getKey = expect.createSpy().andCall(e => { expect(Object.keys(e.keys)).toEqual(["abc"]); return ['abc', privkey]; @@ -58,10 +70,15 @@ describe("Secrets", function() { {userId: "@alice:example.com", deviceId: "Osborne2"}, { cryptoCallbacks: { + getCrossSigningKey: t => signingKey, getSecretStorageKey: getKey, }, }, ); + alice._crypto._crossSigningInfo.setKeys({ + master: signingkeyInfo, + }); + const secretStorage = alice._crypto._secretStorage; alice.setAccountData = async function(eventType, contents, callback) { @@ -76,13 +93,16 @@ describe("Secrets", function() { } }; + const keyAccountData = { + algorithm: "m.secret_storage.v1.curve25519-aes-sha2", + pubkey: pubkey, + }; + await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master'); + alice.store.storeAccountDataEvents([ new MatrixEvent({ type: "m.secret_storage.key.abc", - content: { - algorithm: "m.secret_storage.v1.curve25519-aes-sha2", - pubkey: pubkey, - }, + content: keyAccountData, }), ]); @@ -123,8 +143,15 @@ describe("Secrets", function() { }); it("should encrypt with default key if keys is null", async function() { + let keys = {}; const alice = await makeTestClient( {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + getCrossSigningKey: t => keys[t], + saveCrossSigningKeys: k => keys = k, + }, + }, ); alice.setAccountData = async function(eventType, contents, callback) { alice.store.storeAccountDataEvents([ @@ -134,6 +161,7 @@ describe("Secrets", function() { }), ]); }; + alice.resetCrossSigningKeys(); const newKeyId = await alice.addSecretKey( 'm.secret_storage.v1.curve25519-aes-sha2', diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 880931d0046..cd0d2930ffc 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -23,7 +23,7 @@ import {pkSign, pkVerify} from './olmlib'; import {EventEmitter} from 'events'; import logger from '../logger'; -function getPublicKey(keyInfo) { +function publicKeyFromKeyInfo(keyInfo) { return Object.entries(keyInfo.keys)[0]; } @@ -50,6 +50,14 @@ export class CrossSigningInfo extends EventEmitter { this.firstUse = true; } + getPublicKey(type) { + if (!this.keys[type]) { + throw new Error("No " + type + " key present"); + } + const keyInfo = this.keys[type]; + return publicKeyFromKeyInfo(keyInfo)[1]; + } + /** * Calls the app callback to ask for a private key * @param {string} type The key type ("master", "self_signing", or "user_signing") @@ -62,7 +70,7 @@ export class CrossSigningInfo extends EventEmitter { } if (expectedPubkey === undefined) { - expectedPubkey = getPublicKey(this.keys[type])[1]; + expectedPubkey = this.getPublicKey(type); } const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey); @@ -109,7 +117,7 @@ export class CrossSigningInfo extends EventEmitter { */ getId(type) { type = type || "master"; - return this.keys[type] && getPublicKey(this.keys[type])[1]; + return this.keys[type] && this.getPublicKey(type); } async resetKeys(level) { @@ -201,7 +209,7 @@ export class CrossSigningInfo extends EventEmitter { if (!this.keys.master) { // this is the first key we've seen, so first-use is true this.firstUse = true; - } else if (getPublicKey(keys.master)[1] !== this.getId()) { + } else if (publicKeyFromKeyInfo(keys.master)[1] !== this.getId()) { // this is a different key, so first-use is false this.firstUse = false; } // otherwise, same key, so no change @@ -211,7 +219,7 @@ export class CrossSigningInfo extends EventEmitter { } else { throw new Error("Tried to set cross-signing keys without a master key"); } - const masterKey = getPublicKey(signingKeys.master)[1]; + const masterKey = publicKeyFromKeyInfo(signingKeys.master)[1]; // verify signatures if (keys.user_signing) { @@ -262,6 +270,11 @@ export class CrossSigningInfo extends EventEmitter { } async signObject(data, type) { + if (!this.keys[type]) { + throw new Error( + "Attempted to sign with " + type + " key but no such key present", + ); + } const [pubkey, signing] = await this.getCrossSigningKey(type); try { pkSign(data, signing, this.userId, pubkey); @@ -316,7 +329,7 @@ export class CrossSigningInfo extends EventEmitter { let userTrusted; const userMaster = userCrossSigning.keys.master; - const uskId = getPublicKey(this.keys.user_signing)[1]; + const uskId = this.getPublicKey('user_signing'); try { pkVerify(userMaster, uskId, this.userId); userTrusted = true; @@ -339,7 +352,7 @@ export class CrossSigningInfo extends EventEmitter { const deviceObj = deviceToObject(device, userCrossSigning.userId); try { pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); - pkVerify(deviceObj, getPublicKey(userSSK)[1], userCrossSigning.userId); + pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK)[1], userCrossSigning.userId); return userTrust; } catch (e) { return 0; diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index cadaa3c85ec..e6fafa1d52b 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -20,18 +20,20 @@ import olmlib from './olmlib'; import { randomString } from '../randomstring'; import { keyForNewBackup } from './backup_password'; import { encodeRecoveryKey } from './recoverykey'; +import { pkVerify } from './olmlib'; /** * Implements secret storage and sharing (MSC-1946) * @module crypto/Secrets */ export default class SecretStorage extends EventEmitter { - constructor(baseApis, cryptoCallbacks) { + constructor(baseApis, cryptoCallbacks, crossSigningInfo) { super(); this._baseApis = baseApis; + this._cryptoCallbacks = cryptoCallbacks; + this._crossSigningInfo = crossSigningInfo; this._requests = {}; this._incomingRequests = {}; - this._cryptoCallbacks = cryptoCallbacks; } getDefaultKeyId() { @@ -119,7 +121,7 @@ export default class SecretStorage extends EventEmitter { } while (this._baseApis.getAccountData(`m.secret_storage.key.${keyID}`)); } - // FIXME: sign keyData? + await this._crossSigningInfo.signObject(keyData, 'master'); await this._baseApis.setAccountData( `m.secret_storage.key.${keyID}`, keyData, @@ -162,7 +164,14 @@ export default class SecretStorage extends EventEmitter { throw new Error("Unknown key: " +keyName); } const keyInfoContent = keyInfo.getContent(); - // FIXME: check signature of key info + + // check signature of key info + pkVerify( + keyInfoContent, + this._crossSigningInfo.getPublicKey('master'), + this._crossSigningInfo.userId, + ); + // encrypt secret, based on the algorithm switch (keyInfoContent.algorithm) { case "m.secret_storage.v1.curve25519-aes-sha2": @@ -261,6 +270,8 @@ export default class SecretStorage extends EventEmitter { return false; } + if (checkKey === undefined) checkKey = true; + const secretContent = secretInfo.getContent(); if (!secretContent.encrypted) { @@ -276,7 +287,11 @@ export default class SecretStorage extends EventEmitter { ).getContent(); const encInfo = secretContent.encrypted[keyName]; if (checkKey) { - // FIXME: check signature on key + pkVerify( + keyInfo, + this._crossSigningInfo.getPublicKey('master'), + this._crossSigningInfo.userId, + ); } switch (keyInfo.algorithm) { case "m.secret_storage.v1.curve25519-aes-sha2": diff --git a/src/crypto/index.js b/src/crypto/index.js index d90a5b9cb63..03848553fe1 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -208,7 +208,7 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, ); this._secretStorage = new SecretStorage( - baseApis, this._baseApis._cryptoCallbacks, + baseApis, this._baseApis._cryptoCallbacks, this._crossSigningInfo, ); } utils.inherits(Crypto, EventEmitter); From 693c749da069fd3b337113f8d773b16d57ed7b78 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Nov 2019 17:59:25 +0000 Subject: [PATCH 62/97] lint --- src/crypto/CrossSigning.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index cd0d2930ffc..d5d6a053574 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -352,7 +352,9 @@ export class CrossSigningInfo extends EventEmitter { const deviceObj = deviceToObject(device, userCrossSigning.userId); try { pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); - pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK)[1], userCrossSigning.userId); + pkVerify( + deviceObj, publicKeyFromKeyInfo(userSSK)[1], userCrossSigning.userId, + ); return userTrust; } catch (e) { return 0; From d5d8032b5ba58aa3557eccbb9020109ddf6d129b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 11:04:37 +0000 Subject: [PATCH 63/97] Camelcase event names Co-Authored-By: J. Ryan Stinnett --- src/client.js | 6 +++--- src/crypto/Secrets.js | 2 +- src/crypto/index.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client.js b/src/client.js index 60d66cede62..9198b200138 100644 --- a/src/client.js +++ b/src/client.js @@ -677,7 +677,7 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.devicesUpdated", "deviceVerificationChanged", "userVerificationChanged", - "cross-signing.keysChanged", + "crossSigning.keysChanged", ]); logger.log("Crypto: initialising crypto object..."); @@ -5125,7 +5125,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * in user. The checkOwnCrossSigningTrust function may be used to reconcile * the trust in the account key. * - * @event module:client~MatrixClient#"cross-signing.keysChanged" + * @event module:client~MatrixClient#"crossSigning.keysChanged" */ /** @@ -5189,7 +5189,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * Fires when a secret request has been cancelled. If the client is prompting * the user to ask whether they want to share a secret, the prompt can be * dismissed. - * @event module:client~MatrixClient#"crypto.secrets.request_cancelled" + * @event module:client~MatrixClient#"crypto.secrets.requestCancelled" * @param {object} data * @param {string} data.user_id The user ID of the client that had requested the secret. * @param {string} data.device_id The device ID of the client that had requested the diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index e6fafa1d52b..779b11a3d5d 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -383,7 +383,7 @@ export default class SecretStorage extends EventEmitter { && this._incomingRequests[deviceId][content.request_id]) { logger.info("received request cancellation for secret (" + sender + ", " + deviceId + ", " + content.request_id + ")"); - this.baseApis.emit("crypto.secrets.request_cancelled", { + this.baseApis.emit("crypto.secrets.requestCancelled", { user_id: sender, device_id: deviceId, request_id: content.request_id, diff --git a/src/crypto/index.js b/src/crypto/index.js index 03848553fe1..a94868324da 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -352,7 +352,7 @@ Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) { keys[name + "_key"] = key; } await this._baseApis.uploadDeviceSigningKeys(authDict || {}, keys); - this._baseApis.emit("cross-signing.keysChanged", {}); + this._baseApis.emit("crossSigning.keysChanged", {}); // sign the current device with the new key, and upload to the server const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); @@ -551,7 +551,7 @@ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) { // If it's not changed, just make sure everything is up to date await this.checkOwnCrossSigningTrust(); } else { - this.emit("cross-signing.keysChanged", {}); + this.emit("crossSigning.keysChanged", {}); // We'll now be in a state where cross-signing on the account is not trusted // because our locally stored cross-signing keys will not match the ones // on the server for our account. The app must call checkOwnCrossSigningTrust() From d9d65309b3b91534bc6be755854fce99052a8329 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 11:29:08 +0000 Subject: [PATCH 64/97] More s/cross-signing/crossSigning/ --- spec/unit/crypto/backup.spec.js | 4 ++-- spec/unit/crypto/cross-signing.spec.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 6f4d6e69e3a..236a7ed28a2 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -330,10 +330,10 @@ describe("MegolmBackup", function() { let privateKeys; client.uploadDeviceSigningKeys = async function(e) {return;}; client.uploadKeySignatures = async function(e) {return;}; - client.on("cross-signing.saveCrossSigningKeys", function(e) { + client.on("crossSigning.saveCrossSigningKeys", function(e) { privateKeys = e; }); - client.on("cross-signing.getKey", function(e) { + client.on("crossSigning.getKey", function(e) { e.done(privateKeys[e.type]); }); await client.resetCrossSigningKeys(); diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index efc7cea2ffb..15b8480174d 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -139,7 +139,7 @@ describe("Cross Signing", function() { ); const keyChangePromise = new Promise((resolve, reject) => { - alice.once("cross-signing.keysChanged", async (e) => { + alice.once("crossSigning.keysChanged", async (e) => { resolve(e); await alice.checkOwnCrossSigningTrust(); }); From 0048cbef08ca082a11f2910d060eff38df919b62 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 11:51:44 +0000 Subject: [PATCH 65/97] Mark cross siging / SSSS APIs as unstable also add missing jsdoc --- src/client.js | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 9198b200138..6a7e116eb51 100644 --- a/src/client.js +++ b/src/client.js @@ -177,6 +177,7 @@ function keyFromRecoverySession(session, decryptionKey) { * WebRTC connection if the homeserver doesn't provide any servers. Defaults to false. * * @param {object} opts.cryptoCallbacks Optional. Callbacks for crypto and cross-signing. + * The cross-signing API is currently UNSTABLE and may change without notice. * * @param {function} [opts.cryptoCallbacks.getCrossSigningKey] * Optional (required for cross-signing). Function to call when a cross-signing private key is needed. @@ -966,6 +967,7 @@ function wrapCryptoFuncs(MatrixClient, names) { /** * Generate new cross-signing keys. + * The cross-signing API is currently UNSTABLE and may change without notice. * * @function module:client~MatrixClient#resetCrossSigningKeys * @param {object} authDict Auth data to supply for User-Interactive auth. @@ -976,6 +978,7 @@ function wrapCryptoFuncs(MatrixClient, names) { /** * Get the user's cross-signing key ID. + * The cross-signing API is currently UNSTABLE and may change without notice. * * @function module:client~MatrixClient#getCrossSigningId * @param {string} [type=master] The type of key to get the ID of. One of @@ -986,6 +989,7 @@ function wrapCryptoFuncs(MatrixClient, names) { /** * Get the cross signing information for a given user. + * The cross-signing API is currently UNSTABLE and may change without notice. * * @function module:client~MatrixClient#getStoredCrossSigningForUser * @param {string} userId the user ID to get the cross-signing info for. @@ -995,6 +999,7 @@ function wrapCryptoFuncs(MatrixClient, names) { /** * Check whether a given user is trusted. + * The cross-signing API is currently UNSTABLE and may change without notice. * * @function module:client~MatrixClient#checkUserTrust * @param {string} userId The ID of the user to check. @@ -1013,6 +1018,7 @@ function wrapCryptoFuncs(MatrixClient, names) { /** * Check whether a given device is trusted. + * The cross-signing API is currently UNSTABLE and may change without notice. * * @function module:client~MatrixClient#checkDeviceTrust * @param {string} userId The ID of the user whose devices is to be checked. @@ -1038,6 +1044,7 @@ wrapCryptoFuncs(MatrixClient, [ /** * Check if the sender of an event is verified + * The cross-signing API is currently UNSTABLE and may change without notice. * * @param {MatrixEvent} event event to be checked * @@ -1056,6 +1063,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) { /** * Add a key for encrypting secrets. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * * @function module:client~MatrixClient#addSecretKey * @param {string} algorithm the algorithm used by the key @@ -1070,15 +1078,18 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) { /** * Store an encrypted secret on the server + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * * @function module:client~MatrixClient#storeSecret * @param {string} name The name of the secret * @param {string} secret The secret contents. - * @param {Array} keys The IDs of the keys to use to encrypt the secret + * @param {Array} keys The IDs of the keys to use to encrypt the secret or null/undefined + * to use the default (will throw if no default key is set). */ /** * Get a secret from storage. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * * @function module:client~MatrixClient#getSecret * @param {string} name the name of the secret @@ -1088,6 +1099,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) { /** * Check if a secret is stored on the server. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * * @function module:client~MatrixClient#isSecretStored * @param {string} name the name of the secret @@ -1099,6 +1111,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) { /** * Request a secret from another device. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * * @function module:client~MatrixClient#requestSecret * @param {string} name the name of the secret to request @@ -1107,6 +1120,23 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) { * @return {string} the contents of the secret */ +/** + * Get the current default key ID for encrypting secrets. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#getDefaultKeyId + * + * @return {string} The default key ID or null if no default key ID is set + */ + +/** + * Set the current default key ID for encrypting secrets. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#setDefaultKeyId + * @param {string} keyId The new default key ID + */ + wrapCryptoFuncs(MatrixClient, [ "addSecretKey", "storeSecret", @@ -5111,6 +5141,8 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * If userId is the userId of the logged in user, this indicated a change * in the trust status of the cross-signing data on the account. * + * The cross-signing API is currently UNSTABLE and may change without notice. + * * @event module:client~MatrixClient#"userTrustStatusChanged" * @param {string} userId the userId of the user in question * @param {integer} trustLevel The new trust level of the user @@ -5125,6 +5157,8 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * in user. The checkOwnCrossSigningTrust function may be used to reconcile * the trust in the account key. * + * The cross-signing API is currently UNSTABLE and may change without notice. + * * @event module:client~MatrixClient#"crossSigning.keysChanged" */ @@ -5189,6 +5223,9 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * Fires when a secret request has been cancelled. If the client is prompting * the user to ask whether they want to share a secret, the prompt can be * dismissed. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * * @event module:client~MatrixClient#"crypto.secrets.requestCancelled" * @param {object} data * @param {string} data.user_id The user ID of the client that had requested the secret. From e10c17c8662ddefb82416223a534054aec37882d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 12:10:56 +0000 Subject: [PATCH 66/97] Use official name for SSSS Co-Authored-By: J. Ryan Stinnett --- src/crypto/Secrets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 779b11a3d5d..1d830b74c29 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -23,7 +23,7 @@ import { encodeRecoveryKey } from './recoverykey'; import { pkVerify } from './olmlib'; /** - * Implements secret storage and sharing (MSC-1946) + * Implements Secure Secret Storage and Sharing (MSC1946) * @module crypto/Secrets */ export default class SecretStorage extends EventEmitter { From 291133beb9ef959b3734667f48dbc61efe3f24ef Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 11:56:35 +0000 Subject: [PATCH 67/97] Fix comment --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 6a7e116eb51..b9ab20037f6 100644 --- a/src/client.js +++ b/src/client.js @@ -1442,7 +1442,7 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) { await this._crypto._signObject(data.auth_data); if (this._crypto._crossSigningInfo.getId()) { - // now also sign the auth data with the SSK + // now also sign the auth data with the master key await this._crypto._crossSigningInfo.signObject(data.auth_data, "master"); } From 2cd748b50cad2f88a7d5fcd0884888ab75d3f782 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 11:57:47 +0000 Subject: [PATCH 68/97] Add matrix foundation copyright The creation of this file just predates matrix.org foundation so it should have both --- src/crypto/CrossSigning.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index d5d6a053574..1e2397ebccd 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 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. From 69ecf3b145327212d4739aa8ac1f6e3a4e1c4979 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 12:00:31 +0000 Subject: [PATCH 69/97] jsdoc formatting --- src/crypto/CrossSigning.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 1e2397ebccd..8da0c0418c5 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -109,7 +109,8 @@ export class CrossSigningInfo extends EventEmitter { }; } - /** Get the ID used to identify the user + /** + * Get the ID used to identify the user * * @param {string} type The type of key to get the ID of. One of "master", * "self_signing", or "user_signing". Defaults to "master". From 2a7b2835b6e22361d686712d30808e0c629f70dd Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 12:09:12 +0000 Subject: [PATCH 70/97] remove unused function --- src/crypto/DeviceList.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 31616a5eda9..539fdb3a127 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -346,10 +346,6 @@ export default class DeviceList extends EventEmitter { return this._devices[userId]; } - getRawStoredCrossSigningForUser(userId) { - return this._crossSigningInfo[userId]; - } - getStoredCrossSigningForUser(userId) { if (!this._crossSigningInfo[userId]) return null; From 686a7a40f99f39ea8eb68762e18ff942f5ee7c54 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 12:10:10 +0000 Subject: [PATCH 71/97] Remove outdated comment uhoreg says this is fine now... --- src/crypto/DeviceList.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 539fdb3a127..94b3140b0ed 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -852,7 +852,6 @@ class DeviceListUpdateSerialiser { async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore, userResult) { - // FIXME: this isn't correct any more let updated = false; // remove any devices in the store which aren't in the response From 7ca09ad749afbdfb4d5c90796c47b789d6c23b03 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 12:18:07 +0000 Subject: [PATCH 72/97] tariling space --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index b9ab20037f6..c3726301eeb 100644 --- a/src/client.js +++ b/src/client.js @@ -5223,7 +5223,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * Fires when a secret request has been cancelled. If the client is prompting * the user to ask whether they want to share a secret, the prompt can be * dismissed. - * + * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * * @event module:client~MatrixClient#"crypto.secrets.requestCancelled" From be9b7a0d24457939219dd21e3afc94bf4b5bc572 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 14:10:13 +0000 Subject: [PATCH 73/97] Remove getPublicKey which was the same as getId --- src/crypto/CrossSigning.js | 17 ++++++----------- src/crypto/Secrets.js | 4 ++-- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 8da0c0418c5..353aec1d6b5 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -51,14 +51,6 @@ export class CrossSigningInfo extends EventEmitter { this.firstUse = true; } - getPublicKey(type) { - if (!this.keys[type]) { - throw new Error("No " + type + " key present"); - } - const keyInfo = this.keys[type]; - return publicKeyFromKeyInfo(keyInfo)[1]; - } - /** * Calls the app callback to ask for a private key * @param {string} type The key type ("master", "self_signing", or "user_signing") @@ -71,7 +63,7 @@ export class CrossSigningInfo extends EventEmitter { } if (expectedPubkey === undefined) { - expectedPubkey = this.getPublicKey(type); + expectedPubkey = this.getId(type); } const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey); @@ -119,9 +111,12 @@ export class CrossSigningInfo extends EventEmitter { */ getId(type) { type = type || "master"; - return this.keys[type] && this.getPublicKey(type); + if (!this.keys[type]) return null; + const keyInfo = this.keys[type]; + return publicKeyFromKeyInfo(keyInfo)[1]; } + async resetKeys(level) { if (!this._callbacks.saveCrossSigningKeys) { throw new Error("No saveCrossSigningKeys callback supplied"); @@ -331,7 +326,7 @@ export class CrossSigningInfo extends EventEmitter { let userTrusted; const userMaster = userCrossSigning.keys.master; - const uskId = this.getPublicKey('user_signing'); + const uskId = this.getId('user_signing'); try { pkVerify(userMaster, uskId, this.userId); userTrusted = true; diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 1d830b74c29..51f3c1bebef 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -168,7 +168,7 @@ export default class SecretStorage extends EventEmitter { // check signature of key info pkVerify( keyInfoContent, - this._crossSigningInfo.getPublicKey('master'), + this._crossSigningInfo.getId('master'), this._crossSigningInfo.userId, ); @@ -289,7 +289,7 @@ export default class SecretStorage extends EventEmitter { if (checkKey) { pkVerify( keyInfo, - this._crossSigningInfo.getPublicKey('master'), + this._crossSigningInfo.getId('master'), this._crossSigningInfo.userId, ); } From 5937185ce9791cfe7bcde3d642e0cee800b85f93 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 14:24:41 +0000 Subject: [PATCH 74/97] Assert usage of setDeviceVerification for cross-signing keys We can't mark a cross-signing key as blocked/unblocked, known/unknown or unverified, so throw an exception instead of doing nothing. Also comment what's going on in this function. --- src/crypto/index.js | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index a94868324da..f2a5522ff14 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1194,24 +1194,34 @@ Crypto.prototype.saveDeviceList = function(delay) { Crypto.prototype.setDeviceVerification = async function( userId, deviceId, verified, blocked, known, ) { + // get rid of any undefined's here so we can just check + // for null rather than null or undefined + if (verified === undefined) verified = null; + if (blocked === undefined) blocked = null; + if (known === undefined) known = null; + + // Check if the 'device' is actually a cross signing key + // The js-sdk's verification treats cross-signing keys as devices + // and so uses this method to mark them verified. const xsk = this._deviceList.getStoredCrossSigningForUser(userId); if (xsk && xsk.getId() === deviceId) { - if (verified) { - const device = await this._crossSigningInfo.signUser(xsk); - if (device) { - await this._baseApis.uploadKeySignatures({ - [userId]: { - [deviceId]: device, - }, - }); - // XXX: we'll need to wait for the device list to be updated - } - this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); - return device; - } else { - // FIXME: ??? + if (blocked !== null || known !== null) { + throw new Error("Cannot set blocked or known for a cross-signing key"); } - return; + if (!verified) { + throw new Error("Cannot set a cross-signing key as unverified"); + } + const device = await this._crossSigningInfo.signUser(xsk); + if (device) { + await this._baseApis.uploadKeySignatures({ + [userId]: { + [deviceId]: device, + }, + }); + // XXX: we'll need to wait for the device list to be updated + } + this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); + return device; } const devices = this._deviceList.getRawStoredDevicesForUser(userId); @@ -1235,7 +1245,7 @@ Crypto.prototype.setDeviceVerification = async function( } let knownStatus = dev.known; - if (known !== null && known !== undefined) { + if (known !== null) { knownStatus = known; } From ce2d1d6e2bbf93990070f3f65b372144d692b824 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Nov 2019 17:41:58 +0000 Subject: [PATCH 75/97] Don't emit event here, as per comment --- src/crypto/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index f2a5522ff14..8c177d84dcb 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1218,9 +1218,9 @@ Crypto.prototype.setDeviceVerification = async function( [deviceId]: device, }, }); - // XXX: we'll need to wait for the device list to be updated + // This will emit events when it comes back down the sync + // (we could do local echo to speed things up) } - this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); return device; } From e541b96a71b57a1ff217299f509a0c2101c3cb4b Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 12:15:13 +0000 Subject: [PATCH 76/97] Change check{User|Device}Trust interfaces ...to return objects with functions rather than a bitmask --- spec/unit/crypto/cross-signing.spec.js | 99 +++++++++++++++---- spec/unit/crypto/verification/sas.spec.js | 14 ++- src/client.js | 23 +---- src/crypto/CrossSigning.js | 111 ++++++++++++++++++---- src/crypto/index.js | 44 +++------ 5 files changed, 201 insertions(+), 90 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 15b8480174d..0eabcf0859f 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -259,8 +259,17 @@ describe("Cross Signing", function() { // once ssk is confirmed, device key should be trusted await keyChangePromise; await uploadSigsPromise; - expect(alice.checkUserTrust("@alice:example.com")).toBe(6); - expect(alice.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(7); + + const aliceTrust = alice.checkUserTrust("@alice:example.com"); + expect(aliceTrust.isCrossSigningVerified()).toBeTruthy(); + expect(aliceTrust.isTofu()).toBeTruthy(); + expect(aliceTrust.isVerified()).toBeTruthy(); + + const aliceDeviceTrust = alice.checkDeviceTrust("@alice:example.com", "Osborne2"); + expect(aliceDeviceTrust.isCrossSigningVerified()).toBeTruthy(); + expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy(); + expect(aliceDeviceTrust.isTofu()).toBeTruthy(); + expect(aliceDeviceTrust.isVerified()).toBeTruthy(); }); it("should use trust chain to determine device verification", async function() { @@ -324,14 +333,27 @@ describe("Cross Signing", function() { Dynabook: bobDevice, }); // Bob's device key should be TOFU - expect(alice.checkUserTrust("@bob:example.com")).toBe(2); - expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(2); + const bobTrust = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust.isVerified()).toBeFalsy(); + expect(bobTrust.isTofu()).toBeTruthy(); + + const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust.isVerified()).toBeFalsy(); + expect(bobDeviceTrust.isTofu()).toBeTruthy(); + // Alice verifies Bob's SSK alice.uploadKeySignatures = () => {}; await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); + // Bob's device key should be trusted - expect(alice.checkUserTrust("@bob:example.com")).toBe(6); - expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(6); + const bobTrust2 = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust2.isCrossSigningVerified()).toBeTruthy(); + expect(bobTrust2.isTofu()).toBeTruthy(); + + const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust2.isCrossSigningVerified()).toBeTruthy(); + expect(bobDeviceTrust2.isLocallyVerified()).toBeFalsy(); + expect(bobDeviceTrust2.isTofu()).toBeTruthy(); }); it("should trust signatures received from other devices", async function() { @@ -487,8 +509,14 @@ describe("Cross Signing", function() { await keyChangePromise; // Bob's device key should be trusted - expect(alice.checkUserTrust("@bob:example.com")).toBe(6); - expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(6); + const bobTrust = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust.isCrossSigningVerified()).toBeTruthy(); + expect(bobTrust.isTofu()).toBeTruthy(); + + const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust.isCrossSigningVerified()).toBeTruthy(); + expect(bobDeviceTrust.isLocallyVerified()).toBeFalsy(); + expect(bobDeviceTrust.isTofu()).toBeTruthy(); }); it("should dis-trust an unsigned device", async function() { @@ -547,11 +575,17 @@ describe("Cross Signing", function() { Dynabook: bobDevice, }); // Bob's device key should be untrusted - expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); + const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust.isVerified()).toBeFalsy(); + expect(bobDeviceTrust.isTofu()).toBeFalsy(); + // Alice verifies Bob's SSK await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); + // Bob's device key should be untrusted - expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); + const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust2.isVerified()).toBeFalsy(); + expect(bobDeviceTrust2.isTofu()).toBeFalsy(); }); it("should dis-trust a user when their ssk changes", async function() { @@ -615,8 +649,12 @@ describe("Cross Signing", function() { // Alice verifies Bob's SSK alice.uploadKeySignatures = () => {}; await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); + // Bob's device key should be trusted - expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(6); + const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust.isVerified()).toBeTruthy(); + expect(bobDeviceTrust.isTofu()).toBeTruthy(); + // Alice downloads new SSK for Bob const bobMasterSigning2 = new global.Olm.PkSigning(); const bobMasterPrivkey2 = bobMasterSigning2.generate_seed(); @@ -652,23 +690,38 @@ describe("Cross Signing", function() { unsigned: {}, }); // Bob's and his device should be untrusted - expect(alice.checkUserTrust("@bob:example.com")).toBe(0); - expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); + const bobTrust = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust.isVerified()).toBeFalsy(); + expect(bobTrust.isTofu()).toBeFalsy(); + + const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust2.isVerified()).toBeFalsy(); + expect(bobDeviceTrust2.isTofu()).toBeFalsy(); + // Alice verifies Bob's SSK alice.uploadKeySignatures = () => {}; await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey2, true); + // Bob should be trusted but not his device - expect(alice.checkUserTrust("@bob:example.com")).toBe(4); - expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0); + const bobTrust2 = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust2.isVerified()).toBeTruthy(); + + const bobDeviceTrust3 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust3.isVerified()).toBeFalsy(); + // Alice gets new signature for device const sig2 = bobSigning2.sign(bobDeviceString); bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2; alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); + // Bob's device should be trusted again (but not TOFU) - expect(alice.checkUserTrust("@bob:example.com")).toBe(4); - expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(4); + const bobTrust3 = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust3.isVerified()).toBeTruthy(); + + const bobDeviceTrust4 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust4.isCrossSigningVerified()).toBeTruthy(); }); it("should offer to upgrade device verifications to cross-signing", async function() { @@ -722,13 +775,17 @@ describe("Cross Signing", function() { await alice.resetCrossSigningKeys(); await upgradePromise; - expect(alice.checkUserTrust("@bob:example.com")).toBe(6); + const bobTrust = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust.isCrossSigningVerified()).toBeTruthy(); + expect(bobTrust.isTofu()).toBeTruthy(); // "forget" that Bob is trusted delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"] .keys.master.signatures["@alice:example.com"]; - expect(alice.checkUserTrust("@bob:example.com")).toBe(2); + const bobTrust2 = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust2.isCrossSigningVerified()).toBeFalsy(); + expect(bobTrust2.isTofu()).toBeTruthy(); upgradePromise = new Promise((resolve) => { upgradeResolveFunc = resolve; @@ -736,6 +793,8 @@ describe("Cross Signing", function() { alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com"); await upgradePromise; - expect(alice.checkUserTrust("@bob:example.com")).toBe(6); + const bobTrust3 = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust.isCrossSigningVerified()).toBeTruthy(); + expect(bobTrust.isTofu()).toBeTruthy(); }); }); diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index 8a5eaa51867..7f7ab2c9d1e 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -318,9 +318,17 @@ describe("SAS verification", function() { await verifyProm; - expect(alice.client.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(1); - expect(bob.client.checkUserTrust("@alice:example.com")).toBe(6); - expect(bob.client.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(1); + const bobDeviceTrust = alice.client.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy(); + expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy(); + + const aliceTrust = bob.client.checkUserTrust("@alice:example.com"); + expect(aliceTrust.isCrossSigningVerified()).toBeTruthy(); + expect(aliceTrust.isTofu()).toBeTruthy(); + + const aliceDeviceTrust = bob.client.checkDeviceTrust("@alice:example.com", "Osborne2"); + expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy(); + expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy(); }); }); diff --git a/src/client.js b/src/client.js index c3726301eeb..17a58e1e29e 100644 --- a/src/client.js +++ b/src/client.js @@ -235,9 +235,8 @@ function keyFromRecoverySession(session, decryptionKey) { * {string} request_id The ID of the request. Used to match a * corresponding `crypto.secrets.request_cancelled`. The request ID will be * unique per sender, device pair. - * {int} device_trust: The trust status of the device requesting - * the secret. Will be a bit mask in the same form as returned by {@link - * module:client~MatrixClient#checkDeviceTrust}. + * {DeviceTrustLevel} device_trust: The trust status of the device requesting + * the secret as returned by {@link module:client~MatrixClient#checkDeviceTrust}. */ function MatrixClient(opts) { opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl); @@ -1004,16 +1003,7 @@ function wrapCryptoFuncs(MatrixClient, names) { * @function module:client~MatrixClient#checkUserTrust * @param {string} userId The ID of the user to check. * - * @returns {integer} a bit mask indicating how the user is trusted (if at all) - * - returnValue & 1: unused - * - returnValue & 2: trust-on-first-use cross-signing key - * - returnValue & 4: user's cross-signing key is verified - * - * TODO: is this a good way of representing it? Or we could return an object - * with different keys, or a set? The advantage of doing it this way is that - * you can define which methods you want to use, "&" with the appopriate mask, - * then test for truthiness. Or if you want to just trust everything, then use - * the value alone. However, I wonder if bit masks are too obscure... + * @returns {UserTrustLevel} */ /** @@ -1024,12 +1014,7 @@ function wrapCryptoFuncs(MatrixClient, names) { * @param {string} userId The ID of the user whose devices is to be checked. * @param {string} deviceId The ID of the device to check * - * @returns {integer} a bit mask indicating how the user is trusted (if at all) - * - returnValue & 1: device marked as verified - * - returnValue & 2: trust-on-first-use cross-signing key - * - returnValue & 4: user's cross-signing key is verified and device is signed - * - * TODO: see checkUserTrust + * @returns {DeviuceTrustLevel} */ wrapCryptoFuncs(MatrixClient, [ diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 353aec1d6b5..3d290a2ccb9 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -313,15 +313,15 @@ export class CrossSigningInfo extends EventEmitter { if (this.userId === userCrossSigning.userId && this.getId() && this.getId() === userCrossSigning.getId() && this.getId("self_signing") - && this.getId("self_signing") === userCrossSigning.getId("self_signing")) { - return CrossSigningVerification.VERIFIED - | (this.firstUse ? CrossSigningVerification.TOFU - : CrossSigningVerification.UNVERIFIED); + && this.getId("self_signing") === userCrossSigning.getId("self_signing") + ) { + return new UserTrustLevel(true, this.firstUse); } if (!this.keys.user_signing) { - return (userCrossSigning.firstUse ? CrossSigningVerification.TOFU - : CrossSigningVerification.UNVERIFIED); + // If there's no user signing key, they can't possibly be verified. + // They may be TOFU trusted though. + return new UserTrustLevel(false, userCrossSigning.firstUse); } let userTrusted; @@ -333,28 +333,31 @@ export class CrossSigningInfo extends EventEmitter { } catch (e) { userTrusted = false; } - return (userTrusted ? CrossSigningVerification.VERIFIED - : CrossSigningVerification.UNVERIFIED) - | (userCrossSigning.firstUse ? CrossSigningVerification.TOFU - : CrossSigningVerification.UNVERIFIED); + return new UserTrustLevel(userTrusted, userCrossSigning.firstUse); } - checkDeviceTrust(userCrossSigning, device) { + checkDeviceTrust(userCrossSigning, device, localTrust) { const userTrust = this.checkUserTrust(userCrossSigning); const userSSK = userCrossSigning.keys.self_signing; if (!userSSK) { - return 0; + // if the user has no self-signing key then we cannot make any + // trust assertions about this device from cross-signing + return new DeviceTrustLevel(false, false, localTrust); } + const deviceObj = deviceToObject(device, userCrossSigning.userId); try { + // if we can verify the user's SSK from their master key... pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); + // ...and this device's key from their SSK... pkVerify( deviceObj, publicKeyFromKeyInfo(userSSK)[1], userCrossSigning.userId, ); - return userTrust; + // ...then we trust this device as much as far as we trust the user + return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust); } catch (e) { - return 0; + return new DeviceTrustLevel(false, false, localTrust); } } } @@ -377,8 +380,80 @@ export const CrossSigningLevel = { USER_SIGNING: 2, }; -export const CrossSigningVerification = { - UNVERIFIED: 0, - TOFU: 1, - VERIFIED: 2, +/** + * Represents the ways in which we trust a user + */ +export class UserTrustLevel { + constructor(crossSigningVerified, tofu) { + this._crossSigningVerified = crossSigningVerified; + this._tofu = tofu; + } + + /** + * @returns {bool} true if this user is verified via any means + */ + isVerified() { + return this.isCrossSigningVerified(); + } + + /** + * @returns {bool} true if this user is verified via cross signing + */ + isCrossSigningVerified() { + return this._crossSigningVerified; + } + + /** + * @returns {bool} true if this user's key is trusted on first use + */ + isTofu() { + return this._tofu; + } +}; + +/** + * Represents the ways in which we trust a device + */ +export class DeviceTrustLevel { + constructor(crossSigningVerified, tofu, localVerified) { + this._crossSigningVerified = crossSigningVerified; + this._tofu = tofu; + this._localVerified = localVerified; + } + + static fromUserTrustLevel(userTrustLevel, localVerified) { + return new DeviceTrustLevel( + userTrustLevel._crossSigningVerified, + userTrustLevel._tofu, + localVerified, + ); + } + + /** + * @returns {bool} true if this user is verified via any means + */ + isVerified() { + return this.isCrossSigningVerified() || this.isLocallyVerified(); + } + + /** + * @returns {bool} true if this user is verified via cross signing + */ + isCrossSigningVerified() { + return this._crossSigningVerified; + } + + /** + * @returns {bool} true if this user is verified via cross signing + */ + isLocallyVerified() { + return this._localVerified; + } + + /** + * @returns {bool} true if this user's key is trusted on first use + */ + isTofu() { + return this._tofu; + } }; diff --git a/src/crypto/index.js b/src/crypto/index.js index 8c177d84dcb..caf7ab6b932 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -36,7 +36,7 @@ const DeviceInfo = require("./deviceinfo"); const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceList = require('./DeviceList').default; import { randomString } from '../randomstring'; -import { CrossSigningInfo } from './CrossSigning'; +import { CrossSigningInfo, UserTrustLevel, DeviceTrustLevel } from './CrossSigning'; import SecretStorage from './Secrets'; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; @@ -411,8 +411,8 @@ Crypto.prototype._checkForDeviceVerificationUpgrade = async function( ) { // only upgrade if this is the first cross-signing key that we've seen for // them, and if their cross-signing key isn't already verified - if (crossSigningInfo.firstUse - && !(this._crossSigningInfo.checkUserTrust(crossSigningInfo) & 2)) { + const trustLevel = this._crossSigningInfo.checkUserTrust(crossSigningInfo); + if (crossSigningInfo.firstUse && !trustLevel.verified) { const devices = this._deviceList.getRawStoredDevicesForUser(userId); const deviceIds = await this._checkForValidDeviceSignature( userId, crossSigningInfo.keys.master, devices, @@ -487,25 +487,14 @@ Crypto.prototype.getStoredCrossSigningForUser = function(userId) { * * @param {string} userId The ID of the user to check. * - * @returns {integer} a bit mask indicating how the user is trusted (if at all) - * - returnValue & 1: unused - * - returnValue & 2: trust-on-first-use cross-signing key - * - returnValue & 4: user's cross-signing key is verified - * - * TODO: is this a good way of representing it? Or we could return an object - * with different keys, or a set? The advantage of doing it this way is that - * you can define which methods you want to use, "&" with the appopriate mask, - * then test for truthiness. Or if you want to just trust everything, then use - * the value alone. However, I wonder if bit masks are too obscure... + * @returns {UserTrustLevel} */ Crypto.prototype.checkUserTrust = function(userId) { const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); if (!userCrossSigning) { - return 0; + return new UserTrustLevel(false, false); } - // We shift the result from CrossSigningInfo.checkUserTrust so this - // function's return is consistent with checkDeviceTrust - return this._crossSigningInfo.checkUserTrust(userCrossSigning) << 1; + return this._crossSigningInfo.checkUserTrust(userCrossSigning); }; /** @@ -514,25 +503,20 @@ Crypto.prototype.checkUserTrust = function(userId) { * @param {string} userId The ID of the user whose devices is to be checked. * @param {string} deviceId The ID of the device to check * - * @returns {integer} a bit mask indicating how the user is trusted (if at all) - * - returnValue & 1: device marked as verified - * - returnValue & 2: trust-on-first-use cross-signing key - * - returnValue & 4: user's cross-signing key is verified and device is signed - * - * TODO: see checkUserTrust + * @returns {DeviceTrustLevel} */ Crypto.prototype.checkDeviceTrust = function(userId, deviceId) { - let rv = 0; - const device = this._deviceList.getStoredDevice(userId, deviceId); - if (device && device.isVerified()) { - rv |= 1; - } + const trustedLocally = device && device.isVerified(); + const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); if (device && userCrossSigning) { - rv |= this._crossSigningInfo.checkDeviceTrust(userCrossSigning, device) << 1; + return this._crossSigningInfo.checkDeviceTrust( + userCrossSigning, device, trustedLocally, + ); + } else { + return new DeviceTrustLevel(false, false, trustedLocally); } - return rv; }; /* From c3215d51bda848ce86327fffeadad2340c43df39 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 12:23:37 +0000 Subject: [PATCH 77/97] Switch the CroosSigningLevel constants we check in resetKeys and set all if it's & 4 anyway, so may as well make the constants a normal bitmask and then we can use the MASTER constant below. --- src/crypto/CrossSigning.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 3d290a2ccb9..40a113ca53f 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -122,8 +122,13 @@ export class CrossSigningInfo extends EventEmitter { throw new Error("No saveCrossSigningKeys callback supplied"); } - if (level === undefined || level & 4 || !this.keys.master) { - level = CrossSigningLevel.MASTER; + // If we're resetting the master key, we reset all keys + if (level === undefined || level & CrossSigningLevel.MASTER || !this.keys.master) { + level = ( + CrossSigningLevel.MASTER | + CrossSigningLevel.USER_SIGNING | + CrossSigningLevel.SELF_SIGNING + ); } else if (level === 0) { return; } @@ -134,7 +139,7 @@ export class CrossSigningInfo extends EventEmitter { let masterPub; try { - if (level & 4) { + if (level & CrossSigningLevel.MASTER) { masterSigning = new global.Olm.PkSigning(); privateKeys.master = masterSigning.generate_seed(); masterPub = masterSigning.init_with_seed(privateKeys.master); @@ -373,11 +378,9 @@ function deviceToObject(device, userId) { } export const CrossSigningLevel = { - // NB. The actual master key is 4 but you must, by definition, reset all - // keys if you reset the master key so this is essentially 'all keys' - MASTER: 7, - SELF_SIGNING: 1, + MASTER: 4, USER_SIGNING: 2, + SELF_SIGNING: 1, }; /** From 6f42824c35e0114efaa38f116ebf81b5c7c8b62f Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 12:26:24 +0000 Subject: [PATCH 78/97] Typo Co-Authored-By: J. Ryan Stinnett --- src/crypto/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index caf7ab6b932..f19ee913ebb 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -591,7 +591,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() { const oldSelfSigningId = this._crossSigningInfo.getId("self_signing"); const oldUserSigningId = this._crossSigningInfo.getId("user_signing"); - // Update the version of our keys in our cross-siging object and the local store + // Update the version of our keys in our cross-signing object and the local store this._crossSigningInfo.setKeys(newCrossSigning.keys); await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], From 408934932a5b39f74afd619181ce72a0479d8b4d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 12:27:14 +0000 Subject: [PATCH 79/97] copy jsdoc to internal methods --- src/crypto/CrossSigning.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 40a113ca53f..10f0a677452 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -312,6 +312,13 @@ export class CrossSigningInfo extends EventEmitter { ); } + /** + * Check whether a given user is trusted. + * + * @param {string} userId The ID of the user to check. + * + * @returns {UserTrustLevel} + */ checkUserTrust(userCrossSigning) { // if we're checking our own key, then it's trusted if the master key // and self-signing key match @@ -341,6 +348,14 @@ export class CrossSigningInfo extends EventEmitter { return new UserTrustLevel(userTrusted, userCrossSigning.firstUse); } + /** + * Check whether a given device is trusted. + * + * @param {string} userId The ID of the user whose devices is to be checked. + * @param {string} deviceId The ID of the device to check + * + * @returns {DeviceTrustLevel} + */ checkDeviceTrust(userCrossSigning, device, localTrust) { const userTrust = this.checkUserTrust(userCrossSigning); From 545ebf81bf54b3a9813fdc1f8fd817f4245d1b9f Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 12:29:03 +0000 Subject: [PATCH 80/97] Move Crypto.prototype.init back to its rightful place --- src/crypto/index.js | 104 ++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index f19ee913ebb..179ab684d25 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -213,58 +213,6 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, } utils.inherits(Crypto, EventEmitter); -Crypto.prototype.addSecretKey = function(algorithm, opts, keyID) { - return this._secretStorage.addKey(algorithm, opts, keyID); -}; - -Crypto.prototype.storeSecret = function(name, secret, keys) { - return this._secretStorage.store(name, secret, keys); -}; - -Crypto.prototype.getSecret = function(name) { - return this._secretStorage.get(name); -}; - -Crypto.prototype.isSecretStored = function(name, checkKey) { - return this._secretStorage.isStored(name, checkKey); -}; - -Crypto.prototype.requestSecret = function(name, devices) { - if (!devices) { - devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(this._userId)); - } - return this._secretStorage.request(name, devices); -}; - -Crypto.prototype.getDefaultKeyId = function() { - return this._secretStorage.getDefaultKeyId(); -}; - -Crypto.prototype.setDefaultKeyId = function(k) { - return this._secretStorage.setDefaultKeyId(k); -}; - -/** - * Checks that a given private key matches a given public key - * This can be used by the getCrossSigningKey callback to verify that the - * private key it is about to supply is the one that was requested. - * - * @param {Uint8Array} privateKey The private key - * @param {Uint8Array} expectedPublicKey The public key supplied by the getCrossSigningKey callback - * @returns {boolean} true if the key matches, otherwise false - */ -Crypto.prototype.checkPrivateKey = function(privateKey, expectedPublicKey) { - let signing = null; - try { - signing = new global.Olm.PkSigning(); - const gotPubkey = signing.init_with_seed(privateKey); - // make sure it agrees with the given pubkey - return gotPubkey === expectedPublicKey; - } finally { - if (signing) signing.free(); - } -}; - /** * Initialise the crypto module so that it is ready for use * @@ -328,6 +276,58 @@ Crypto.prototype.init = async function() { this._checkAndStartKeyBackup(); }; +Crypto.prototype.addSecretKey = function(algorithm, opts, keyID) { + return this._secretStorage.addKey(algorithm, opts, keyID); +}; + +Crypto.prototype.storeSecret = function(name, secret, keys) { + return this._secretStorage.store(name, secret, keys); +}; + +Crypto.prototype.getSecret = function(name) { + return this._secretStorage.get(name); +}; + +Crypto.prototype.isSecretStored = function(name, checkKey) { + return this._secretStorage.isStored(name, checkKey); +}; + +Crypto.prototype.requestSecret = function(name, devices) { + if (!devices) { + devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(this._userId)); + } + return this._secretStorage.request(name, devices); +}; + +Crypto.prototype.getDefaultKeyId = function() { + return this._secretStorage.getDefaultKeyId(); +}; + +Crypto.prototype.setDefaultKeyId = function(k) { + return this._secretStorage.setDefaultKeyId(k); +}; + +/** + * Checks that a given private key matches a given public key + * This can be used by the getCrossSigningKey callback to verify that the + * private key it is about to supply is the one that was requested. + * + * @param {Uint8Array} privateKey The private key + * @param {Uint8Array} expectedPublicKey The public key supplied by the getCrossSigningKey callback + * @returns {boolean} true if the key matches, otherwise false + */ +Crypto.prototype.checkPrivateKey = function(privateKey, expectedPublicKey) { + let signing = null; + try { + signing = new global.Olm.PkSigning(); + const gotPubkey = signing.init_with_seed(privateKey); + // make sure it agrees with the given pubkey + return gotPubkey === expectedPublicKey; + } finally { + if (signing) signing.free(); + } +}; + /** * Generate new cross-signing keys. * From fe010242d95ad30c520362a38fb4c7193243ccc7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 12:30:05 +0000 Subject: [PATCH 81/97] Why is 'cross-signing' so hard to type? Co-Authored-By: J. Ryan Stinnett --- src/crypto/store/indexeddb-crypto-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 15492204ff1..b2c2fe4d058 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -314,7 +314,7 @@ export default class IndexedDBCryptoStore { } /** - * Write the cross-siging keys back to the store + * Write the cross-signing keys back to the store * * @param {*} txn An active transaction. See doTxn(). * @param {string} keys keys object as getCrossSigningKeys() From f5a5f5e51a2ab5c14f3d16b3a22014da812ec445 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 12:31:22 +0000 Subject: [PATCH 82/97] Update yarn.lock --- yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yarn.lock b/yarn.lock index 4e8e5a5580d..b3335052676 100644 --- a/yarn.lock +++ b/yarn.lock @@ -557,6 +557,11 @@ babel-plugin-syntax-class-properties@^6.8.0: resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" integrity sha1-1+sjt5oxf4VDlixQW4J8fWysJ94= +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + integrity sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4= + babel-plugin-transform-async-to-bluebird@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-bluebird/-/babel-plugin-transform-async-to-bluebird-1.1.1.tgz#46ea3e7c5af629782ac9f1ed1b7cd38f8425afd4" From d37ed9ff6fb86dde9dd3ee0ce5dea6c95b20bb21 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 12:39:14 +0000 Subject: [PATCH 83/97] lint --- spec/unit/crypto/cross-signing.spec.js | 4 ++-- spec/unit/crypto/verification/sas.spec.js | 8 ++++++-- src/crypto/CrossSigning.js | 17 +++++++++++------ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index 0eabcf0859f..c26356d98e1 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -794,7 +794,7 @@ describe("Cross Signing", function() { await upgradePromise; const bobTrust3 = alice.checkUserTrust("@bob:example.com"); - expect(bobTrust.isCrossSigningVerified()).toBeTruthy(); - expect(bobTrust.isTofu()).toBeTruthy(); + expect(bobTrust3.isCrossSigningVerified()).toBeTruthy(); + expect(bobTrust3.isTofu()).toBeTruthy(); }); }); diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index 7f7ab2c9d1e..cee4d14491e 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -318,7 +318,9 @@ describe("SAS verification", function() { await verifyProm; - const bobDeviceTrust = alice.client.checkDeviceTrust("@bob:example.com", "Dynabook"); + const bobDeviceTrust = alice.client.checkDeviceTrust( + "@bob:example.com", "Dynabook", + ); expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy(); expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy(); @@ -326,7 +328,9 @@ describe("SAS verification", function() { expect(aliceTrust.isCrossSigningVerified()).toBeTruthy(); expect(aliceTrust.isTofu()).toBeTruthy(); - const aliceDeviceTrust = bob.client.checkDeviceTrust("@alice:example.com", "Osborne2"); + const aliceDeviceTrust = bob.client.checkDeviceTrust( + "@alice:example.com", "Osborne2", + ); expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy(); expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy(); }); diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 10f0a677452..026500d95c1 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -123,7 +123,11 @@ export class CrossSigningInfo extends EventEmitter { } // If we're resetting the master key, we reset all keys - if (level === undefined || level & CrossSigningLevel.MASTER || !this.keys.master) { + if ( + level === undefined || + level & CrossSigningLevel.MASTER || + !this.keys.master + ) { level = ( CrossSigningLevel.MASTER | CrossSigningLevel.USER_SIGNING | @@ -315,7 +319,7 @@ export class CrossSigningInfo extends EventEmitter { /** * Check whether a given user is trusted. * - * @param {string} userId The ID of the user to check. + * @param {CrossSigningInfo} userCrossSigning Cross signing info for user * * @returns {UserTrustLevel} */ @@ -351,8 +355,9 @@ export class CrossSigningInfo extends EventEmitter { /** * Check whether a given device is trusted. * - * @param {string} userId The ID of the user whose devices is to be checked. - * @param {string} deviceId The ID of the device to check + * @param {CrossSigningInfo} userCrossSigning Cross signing info for user + * @param {module:crypto/deviceinfo} device The device to check + * @param {bool} localTrust Whether the device is trusted locally * * @returns {DeviceTrustLevel} */ @@ -427,7 +432,7 @@ export class UserTrustLevel { isTofu() { return this._tofu; } -}; +} /** * Represents the ways in which we trust a device @@ -474,4 +479,4 @@ export class DeviceTrustLevel { isTofu() { return this._tofu; } -}; +} From f84ec090cbef1434705a89566ca123f73f94cbc7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 14:38:27 +0000 Subject: [PATCH 84/97] backticks in jsdoc Co-Authored-By: J. Ryan Stinnett --- src/crypto/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 179ab684d25..4b670d5a36f 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1178,7 +1178,7 @@ Crypto.prototype.saveDeviceList = function(delay) { Crypto.prototype.setDeviceVerification = async function( userId, deviceId, verified, blocked, known, ) { - // get rid of any undefined's here so we can just check + // get rid of any `undefined`s here so we can just check // for null rather than null or undefined if (verified === undefined) verified = null; if (blocked === undefined) blocked = null; From f2f205f9bdf1d692dbe376955e83e8a24ccca377 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 14:38:44 +0000 Subject: [PATCH 85/97] Typo Co-Authored-By: J. Ryan Stinnett --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 17a58e1e29e..c24d25f9929 100644 --- a/src/client.js +++ b/src/client.js @@ -1014,7 +1014,7 @@ function wrapCryptoFuncs(MatrixClient, names) { * @param {string} userId The ID of the user whose devices is to be checked. * @param {string} deviceId The ID of the device to check * - * @returns {DeviuceTrustLevel} + * @returns {DeviceTrustLevel} */ wrapCryptoFuncs(MatrixClient, [ From 86e0f492310a16eca1c3180806fd877d9aa33a95 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 14:40:16 +0000 Subject: [PATCH 86/97] c+p fail Co-Authored-By: J. Ryan Stinnett --- src/crypto/CrossSigning.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 026500d95c1..5c9aafd9da8 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -453,7 +453,7 @@ export class DeviceTrustLevel { } /** - * @returns {bool} true if this user is verified via any means + * @returns {bool} true if this device is verified via any means */ isVerified() { return this.isCrossSigningVerified() || this.isLocallyVerified(); From 00b571a42962d1ded88d4b999706c398a3d6ea4b Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 14:40:57 +0000 Subject: [PATCH 87/97] c+p fail Co-Authored-By: J. Ryan Stinnett --- src/crypto/CrossSigning.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 5c9aafd9da8..cbb3709449a 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -467,7 +467,7 @@ export class DeviceTrustLevel { } /** - * @returns {bool} true if this user is verified via cross signing + * @returns {bool} true if this device is verified locally */ isLocallyVerified() { return this._localVerified; From 97dff4640adea7a601f4eb6501a7b4c30274dbfe Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 14:41:12 +0000 Subject: [PATCH 88/97] Capitalise jsdoc Co-Authored-By: J. Ryan Stinnett --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index c24d25f9929..5a5f09025c7 100644 --- a/src/client.js +++ b/src/client.js @@ -947,7 +947,7 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() { }; /** - * add methods that call the corresponding method in this._crypto + * Add methods that call the corresponding method in this._crypto * * @param {class} MatrixClient the class to add the method to * @param {string} names the names of the methods to call From 6d0237ec713a46d16b156813dc788c7d61879621 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 14:42:06 +0000 Subject: [PATCH 89/97] This now returns DeviceTrustLevel too --- src/client.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/client.js b/src/client.js index 5a5f09025c7..3bf1c05ab49 100644 --- a/src/client.js +++ b/src/client.js @@ -1033,10 +1033,7 @@ wrapCryptoFuncs(MatrixClient, [ * * @param {MatrixEvent} event event to be checked * - * @returns {integer} a bit mask indicating how the user is trusted (if at all) - * - returnValue & 1: device marked as verified - * - returnValue & 2: trust-on-first-use cross-signing key - * - returnValue & 4: user's cross-signing key is verified + * @returns {DeviceTrustLevel} */ MatrixClient.prototype.checkEventSenderTrust = async function(event) { const device = await this.getEventSenderDeviceInfo(event); From f0ba1f2ac0d3498267cd17db6642d40261102edf Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 14:43:41 +0000 Subject: [PATCH 90/97] c+p fail Co-Authored-By: J. Ryan Stinnett --- src/crypto/CrossSigning.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index cbb3709449a..e07426af20b 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -460,7 +460,7 @@ export class DeviceTrustLevel { } /** - * @returns {bool} true if this user is verified via cross signing + * @returns {bool} true if this device is verified via cross signing */ isCrossSigningVerified() { return this._crossSigningVerified; From fa2e669eda2686dc0b91dc5aceec48d345835228 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 14:44:08 +0000 Subject: [PATCH 91/97] More jsdoc updates Co-Authored-By: J. Ryan Stinnett --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 3bf1c05ab49..8cfc93ad674 100644 --- a/src/client.js +++ b/src/client.js @@ -5127,7 +5127,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * * @event module:client~MatrixClient#"userTrustStatusChanged" * @param {string} userId the userId of the user in question - * @param {integer} trustLevel The new trust level of the user + * @param {UserTrustLevel} trustLevel The new trust level of the user */ /** From 2ab033e76e1a76e52a17cb9d97694205377eb5a8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 14:45:43 +0000 Subject: [PATCH 92/97] is now implemented --- src/crypto/Secrets.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 51f3c1bebef..44a6a070b73 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -258,8 +258,7 @@ export default class SecretStorage extends EventEmitter { * Check if a secret is stored on the server. * * @param {string} name the name of the secret - * @param {boolean} checkKey check if the secret is encrypted by a trusted - * key (currently unimplemented) + * @param {boolean} checkKey check if the secret is encrypted by a trusted key * * @return {boolean} whether or not the secret is stored */ From 5224ef4b1ffb5d3212bfa4964688f0d5563a38f0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 14:56:05 +0000 Subject: [PATCH 93/97] This is now implemented Co-Authored-By: J. Ryan Stinnett --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 8cfc93ad674..387e48f7f41 100644 --- a/src/client.js +++ b/src/client.js @@ -1086,7 +1086,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) { * @function module:client~MatrixClient#isSecretStored * @param {string} name the name of the secret * @param {boolean} checkKey check if the secret is encrypted by a trusted - * key (currently unimplemented) + * key * * @return {boolean} whether or not the secret is stored */ From c550f83a04fb82e706fd529ac7005f05f21db8fc Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 14:57:29 +0000 Subject: [PATCH 94/97] update jsdoc --- src/crypto/CrossSigning.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index e07426af20b..90d95014aef 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -474,7 +474,8 @@ export class DeviceTrustLevel { } /** - * @returns {bool} true if this user's key is trusted on first use + * @returns {bool} true if this device is trusted from a user's key + * that is trusted on first use */ isTofu() { return this._tofu; From 04b57bbe9dffb3bf4e2e528b3bc8efc36e95b739 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 15:27:02 +0000 Subject: [PATCH 95/97] Remove ghost of some old code --- src/client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client.js b/src/client.js index 387e48f7f41..79efb60f3fd 100644 --- a/src/client.js +++ b/src/client.js @@ -1394,7 +1394,6 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, auth_data: authData, recovery_key: encodeRecoveryKey(decryption.get_private_key()), - accountKeys: null, }; } finally { decryption.free(); From 56261263f5f5823ab189445c48d01a90ec81c7a1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 15:54:43 +0000 Subject: [PATCH 96/97] Rename backup_password & functions Not Just For Backups Anymore --- src/client.js | 6 +++--- src/crypto/Secrets.js | 4 ++-- src/crypto/{backup_password.js => key_passphrase.js} | 11 +++++------ 3 files changed, 10 insertions(+), 11 deletions(-) rename src/crypto/{backup_password.js => key_passphrase.js} (88%) diff --git a/src/client.js b/src/client.js index 79efb60f3fd..f78c842f59b 100644 --- a/src/client.js +++ b/src/client.js @@ -51,7 +51,7 @@ import logger from './logger'; import Crypto from './crypto'; import { isCryptoAvailable } from './crypto'; import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; -import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password'; +import { keyFromPassphrase, keyFromAuthData } from './crypto/key_passphrase'; import { randomString } from './randomstring'; // Disable warnings for now: we use deprecated bluebird functions @@ -1380,7 +1380,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { let publicKey; const authData = {}; if (password) { - const keyInfo = await keyForNewBackup(password); + const keyInfo = await keyFromPassphrase(password); publicKey = decryption.init_with_private_key(keyInfo.key); authData.private_key_salt = keyInfo.salt; authData.private_key_iterations = keyInfo.iterations; @@ -1542,7 +1542,7 @@ MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; MatrixClient.prototype.restoreKeyBackupWithPassword = async function( password, targetRoomId, targetSessionId, backupInfo, ) { - const privKey = await keyForExistingBackup(backupInfo, password); + const privKey = await keyFromAuthData(backupInfo.auth_data, password); return this._restoreKeyBackup( privKey, targetRoomId, targetSessionId, backupInfo, ); diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 44a6a070b73..5160a079aa1 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -18,7 +18,7 @@ import {EventEmitter} from 'events'; import logger from '../logger'; import olmlib from './olmlib'; import { randomString } from '../randomstring'; -import { keyForNewBackup } from './backup_password'; +import { keyFromPassphrase } from './backup_password'; import { encodeRecoveryKey } from './recoverykey'; import { pkVerify } from './olmlib'; @@ -91,7 +91,7 @@ export default class SecretStorage extends EventEmitter { const decryption = new global.Olm.PkDecryption(); try { if (opts.passphrase) { - const key = await keyForNewBackup(opts.passphrase); + const key = await keyFromPassphrase(opts.passphrase); keyData.passphrase = { algorithm: "m.pbkdf2", iterations: key.iterations, diff --git a/src/crypto/backup_password.js b/src/crypto/key_passphrase.js similarity index 88% rename from src/crypto/backup_password.js rename to src/crypto/key_passphrase.js index 1a6d1f28426..057bae84cbc 100644 --- a/src/crypto/backup_password.js +++ b/src/crypto/key_passphrase.js @@ -1,5 +1,6 @@ /* Copyright 2018 New Vector Ltd +Copyright 2019 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. @@ -18,13 +19,11 @@ import { randomString } from '../randomstring'; const DEFAULT_ITERATIONS = 500000; -export async function keyForExistingBackup(backupData, password) { +export async function keyFromAuthData(authData, password) { if (!global.Olm) { throw new Error("Olm is not available"); } - const authData = backupData.auth_data; - if (!authData.private_key_salt || !authData.private_key_iterations) { throw new Error( "Salt and/or iterations not found: " + @@ -33,12 +32,12 @@ export async function keyForExistingBackup(backupData, password) { } return await deriveKey( - password, backupData.auth_data.private_key_salt, - backupData.auth_data.private_key_iterations, + password, authData.private_key_salt, + authData.private_key_iterations, ); } -export async function keyForNewBackup(password) { +export async function keyFromPassphrase(password) { if (!global.Olm) { throw new Error("Olm is not available"); } From 2a63cc474c08414a7dd9bdf5c49ea7c5ec594201 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Nov 2019 15:57:25 +0000 Subject: [PATCH 97/97] Update import --- src/crypto/Secrets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/Secrets.js b/src/crypto/Secrets.js index 5160a079aa1..508b541ab4d 100644 --- a/src/crypto/Secrets.js +++ b/src/crypto/Secrets.js @@ -18,7 +18,7 @@ import {EventEmitter} from 'events'; import logger from '../logger'; import olmlib from './olmlib'; import { randomString } from '../randomstring'; -import { keyFromPassphrase } from './backup_password'; +import { keyFromPassphrase } from './key_passphrase'; import { encodeRecoveryKey } from './recoverykey'; import { pkVerify } from './olmlib';