From ab67a28c74db9fea79b8d03af39df95ec6fdb2d3 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 10 Feb 2023 17:35:45 +0100 Subject: [PATCH 1/7] add feature flag for cross-signing --- src/domain/session/settings/FeaturesViewModel.ts | 7 ++++++- src/features.ts | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/domain/session/settings/FeaturesViewModel.ts b/src/domain/session/settings/FeaturesViewModel.ts index 6017cc6a..552e27d4 100644 --- a/src/domain/session/settings/FeaturesViewModel.ts +++ b/src/domain/session/settings/FeaturesViewModel.ts @@ -29,7 +29,12 @@ export class FeaturesViewModel extends ViewModel { name: this.i18n`Audio/video calls`, description: this.i18n`Allows starting and participating in A/V calls compatible with Element Call (MSC3401). Look for the start call option in the room menu ((...) in the right corner) to start a call.`, feature: FeatureFlag.Calls - })), + })), + new FeatureViewModel(this.childOptions({ + name: this.i18n`Cross-Signing`, + description: this.i18n`Allows.verifying the identity of people you chat with`, + feature: FeatureFlag.CrossSigning + })), ]; } } diff --git a/src/features.ts b/src/features.ts index e503f0e0..6fa5dd43 100644 --- a/src/features.ts +++ b/src/features.ts @@ -18,6 +18,7 @@ import type {SettingsStorage} from "./platform/web/dom/SettingsStorage"; export enum FeatureFlag { Calls = 1 << 0, + CrossSigning = 1 << 1 } export class FeatureSet { @@ -39,6 +40,10 @@ export class FeatureSet { return this.isFeatureEnabled(FeatureFlag.Calls); } + get crossSigning(): boolean { + return this.isFeatureEnabled(FeatureFlag.CrossSigning); + } + static async load(settingsStorage: SettingsStorage): Promise { const flags = await settingsStorage.getInt("enabled_features") || 0; return new FeatureSet(flags); From 2043541f568a6c45efab8ca7429c4ca99a5c225e Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 10 Feb 2023 17:36:14 +0100 Subject: [PATCH 2/7] fix missing free in key backup --- src/matrix/e2ee/megolm/keybackup/Curve25519.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts index 7d2ebac7..45cacb3f 100644 --- a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts +++ b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts @@ -57,6 +57,7 @@ export class BackupEncryption { encryption.set_recipient_key(pubKey); } catch(err) { decryption.free(); + encryption.free(); throw err; } return new BackupEncryption(encryption, decryption); From ce5b27f4b8c0d1a01fb0d145bbeb8f178fe6b25d Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 14 Feb 2023 12:11:59 +0100 Subject: [PATCH 3/7] support fetching the master signing key for a user in the device tracker --- src/matrix/e2ee/DeviceTracker.js | 53 ++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index b256065a..c36d2ec6 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -25,6 +25,7 @@ function createUserIdentity(userId, initialRoomId = undefined) { return { userId: userId, roomIds: initialRoomId ? [initialRoomId] : [], + masterKey: undefined, deviceTrackingStatus: TRACKING_STATUS_OUTDATED, }; } @@ -152,6 +153,26 @@ export class DeviceTracker { } } + async getMasterKeyForUser(userId, hsApi, log) { + return await log.wrap("DeviceTracker.getMasterKeyForUser", async log => { + let txn = await this._storage.readTxn([ + this._storage.storeNames.userIdentities + ]); + let userIdentity = await txn.userIdentities.get(userId); + if (userIdentity && userIdentity.deviceTrackingStatus !== TRACKING_STATUS_OUTDATED) { + return userIdentity.masterKey; + } + // fetch from hs + await this._queryKeys([userId], hsApi, log); + // Retreive from storage now + txn = await this._storage.readTxn([ + this._storage.storeNames.userIdentities + ]); + userIdentity = await txn.userIdentities.get(userId); + return userIdentity?.masterKey; + }); + } + async writeHistoryVisibility(room, historyVisibility, syncTxn, log) { const added = []; const removed = []; @@ -224,6 +245,7 @@ export class DeviceTracker { "token": this._getSyncToken() }, {log}).response(); + const masterKeys = log.wrap("master keys", log => this._filterValidMasterKeys(deviceKeyResponse, log)); const verifiedKeysPerUser = log.wrap("verify", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log)); const txn = await this._storage.readWriteTxn([ this._storage.storeNames.userIdentities, @@ -233,7 +255,7 @@ export class DeviceTracker { try { const devicesIdentitiesPerUser = await Promise.all(verifiedKeysPerUser.map(async ({userId, verifiedKeys}) => { const deviceIdentities = verifiedKeys.map(deviceKeysAsDeviceIdentity); - return await this._storeQueriedDevicesForUserId(userId, deviceIdentities, txn); + return await this._storeQueriedDevicesForUserId(userId, masterKeys.get(userId), deviceIdentities, txn); })); deviceIdentities = devicesIdentitiesPerUser.reduce((all, devices) => all.concat(devices), []); log.set("devices", deviceIdentities.length); @@ -245,7 +267,7 @@ export class DeviceTracker { return deviceIdentities; } - async _storeQueriedDevicesForUserId(userId, deviceIdentities, txn) { + async _storeQueriedDevicesForUserId(userId, masterKey, deviceIdentities, txn) { const knownDeviceIds = await txn.deviceIdentities.getAllDeviceIds(userId); // delete any devices that we know off but are not in the response anymore. // important this happens before checking if the ed25519 key changed, @@ -286,11 +308,38 @@ export class DeviceTracker { identity = createUserIdentity(userId); } identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE; + identity.masterKey = masterKey; txn.userIdentities.set(identity); return allDeviceIdentities; } + _filterValidMasterKeys(keyQueryResponse, parentLog) { + const masterKeysResponse = keyQueryResponse["master_keys"]; + if (!masterKeysResponse) { + return []; + } + const validMasterKeyResponses = Object.entries(masterKeysResponse).filter(([userId, keyInfo]) => { + if (keyInfo["user_id"] !== userId) { + return false; + } + if (!Array.isArray(keyInfo.usage) || !keyInfo.usage.includes("master")) { + return false; + } + return true; + }); + const masterKeys = validMasterKeyResponses.reduce((msks, [userId, keyInfo]) => { + const keyIds = Object.keys(keyInfo.keys); + if (keyIds.length !== 1) { + return false; + } + const masterKey = keyInfo.keys[keyIds[0]]; + msks.set(userId, masterKey); + return msks; + }, new Map()); + return masterKeys; + } + /** * @return {Array<{userId, verifiedKeys: Array>} */ From fdce098245c24b409b4f617962d54cf7f7774322 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 14 Feb 2023 12:12:20 +0100 Subject: [PATCH 4/7] create cross-signing class, support deriving msk from 4s stored privkey and check if they match the publicized one and then trust it --- src/matrix/Session.js | 23 +++++++++ src/matrix/verification/CrossSigning.ts | 68 +++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/matrix/verification/CrossSigning.ts diff --git a/src/matrix/Session.js b/src/matrix/Session.js index d4c68a8d..222c8ef2 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -31,6 +31,7 @@ import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption"; import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption"; import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader"; import {KeyBackup} from "./e2ee/megolm/keybackup/KeyBackup"; +import {CrossSigning} from "./verification/CrossSigning"; import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js"; import {MEGOLM_ALGORITHM} from "./e2ee/common.js"; import {RoomEncryption} from "./e2ee/RoomEncryption.js"; @@ -59,6 +60,7 @@ export class Session { this._storage = storage; this._hsApi = hsApi; this._mediaRepository = mediaRepository; + this._features = features; this._syncInfo = null; this._sessionInfo = sessionInfo; this._rooms = new ObservableMap(); @@ -88,6 +90,7 @@ export class Session { this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; this._keyBackup = new ObservableValue(undefined); + this._crossSigning = undefined; this._observedRoomStatus = new Map(); if (olm) { @@ -330,6 +333,20 @@ export class Session { txn ); if (keyBackup) { + if (this._features.crossSigning) { + this._crossSigning = new CrossSigning({ + storage: this._storage, + secretStorage, + platform: this._platform, + olm: this._olm, + deviceTracker: this._deviceTracker, + hsApi: this._hsApi, + ownUserId: this.userId + }); + await log.wrap("enable cross-signing", log => { + return this._crossSigning.init(log); + }); + } for (const room of this._rooms.values()) { if (room.isEncrypted) { room.enableKeyBackup(keyBackup); @@ -337,6 +354,8 @@ export class Session { } this._keyBackup.set(keyBackup); return true; + } else { + log.set("no_backup", true); } } catch (err) { log.catch(err); @@ -354,6 +373,10 @@ export class Session { return this._keyBackup; } + get crossSigning() { + return this._crossSigning; + } + get hasIdentity() { return !!this._e2eeAccount; } diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts new file mode 100644 index 00000000..8c3b6b61 --- /dev/null +++ b/src/matrix/verification/CrossSigning.ts @@ -0,0 +1,68 @@ +/* +Copyright 2023 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 type {SecretStorage} from "../ssss/SecretStorage"; +import type {Storage} from "../storage/idb/Storage"; +import type {Platform} from "../../platform/web/Platform"; +import type {DeviceTracker} from "../e2ee/DeviceTracker"; +import type * as OlmNamespace from "@matrix-org/olm"; +import type {HomeServerApi} from "../net/HomeServerApi"; +type Olm = typeof OlmNamespace; + +export class CrossSigning { + private readonly storage: Storage; + private readonly secretStorage: SecretStorage; + private readonly platform: Platform; + private readonly deviceTracker: DeviceTracker; + private readonly olm: Olm; + private readonly hsApi: HomeServerApi; + private readonly ownUserId: string; + private _isMasterKeyTrusted: boolean = false; + + constructor(options: {storage: Storage, secretStorage: SecretStorage, deviceTracker: DeviceTracker, platform: Platform, olm: Olm, ownUserId: string, hsApi: HomeServerApi}) { + this.storage = options.storage; + this.secretStorage = options.secretStorage; + this.platform = options.platform; + this.deviceTracker = options.deviceTracker; + this.olm = options.olm; + this.hsApi = options.hsApi; + this.ownUserId = options.ownUserId; + } + + async init(log) { + // use errorboundary here + const txn = await this.storage.readTxn([this.storage.storeNames.accountData]); + + const mskSeed = await this.secretStorage.readSecret("m.cross_signing.master", txn); + const signing = new this.olm.PkSigning(); + let derivedPublicKey; + try { + const seed = new Uint8Array(this.platform.encoding.base64.decode(mskSeed)); + derivedPublicKey = signing.init_with_seed(seed); + } finally { + signing.free(); + } + const publishedMasterKey = await this.deviceTracker.getMasterKeyForUser(this.ownUserId, this.hsApi, log); + log.set({publishedMasterKey, derivedPublicKey}); + this._isMasterKeyTrusted = publishedMasterKey === derivedPublicKey; + log.set("isMasterKeyTrusted", this.isMasterKeyTrusted); + } + + get isMasterKeyTrusted(): boolean { + return this._isMasterKeyTrusted; + } +} + From 45d45cb690efc4baac95771620a46cf0fb6dc003 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 14 Feb 2023 12:13:07 +0100 Subject: [PATCH 5/7] show MSK trust status in settings after enabling key backup --- src/domain/session/settings/KeyBackupViewModel.js | 4 ++++ src/platform/web/ui/session/settings/KeyBackupSettingsView.js | 3 +++ src/platform/web/ui/session/settings/SettingsView.js | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index 40dbfd73..56339a4e 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -88,6 +88,10 @@ export class KeyBackupViewModel extends ViewModel { return this._session.keyBackup.get()?.version; } + get isMasterKeyTrusted() { + return this._session.crossSigning?.isMasterKeyTrusted ?? false; + } + get backupWriteStatus() { const keyBackup = this._session.keyBackup.get(); if (!keyBackup) { diff --git a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js index 3f8812c9..bd1eb4e8 100644 --- a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js @@ -53,6 +53,9 @@ export class KeyBackupSettingsView extends TemplateView { default: return null; } + }), + t.if(vm => vm.isMasterKeyTrusted, t => { + return t.p("Cross-signing master key found and trusted.") }) ]); } diff --git a/src/platform/web/ui/session/settings/SettingsView.js b/src/platform/web/ui/session/settings/SettingsView.js index 0d0d6941..aea1108a 100644 --- a/src/platform/web/ui/session/settings/SettingsView.js +++ b/src/platform/web/ui/session/settings/SettingsView.js @@ -49,7 +49,7 @@ export class SettingsView extends TemplateView { }, vm.i18n`Log out`)), ); settingNodes.push( - t.h3("Key backup"), + t.h3("Key backup & security"), t.view(new KeyBackupSettingsView(vm.keyBackupViewModel)) ); From 103ae1e789dea032161c09b4e90a1645999b3edc Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 14 Feb 2023 13:24:26 +0100 Subject: [PATCH 6/7] fix unit tests --- src/matrix/e2ee/DeviceTracker.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index c36d2ec6..810dfd95 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -315,9 +315,10 @@ export class DeviceTracker { } _filterValidMasterKeys(keyQueryResponse, parentLog) { + const masterKeys = new Map(); const masterKeysResponse = keyQueryResponse["master_keys"]; if (!masterKeysResponse) { - return []; + return masterKeys; } const validMasterKeyResponses = Object.entries(masterKeysResponse).filter(([userId, keyInfo]) => { if (keyInfo["user_id"] !== userId) { @@ -328,7 +329,7 @@ export class DeviceTracker { } return true; }); - const masterKeys = validMasterKeyResponses.reduce((msks, [userId, keyInfo]) => { + validMasterKeyResponses.reduce((msks, [userId, keyInfo]) => { const keyIds = Object.keys(keyInfo.keys); if (keyIds.length !== 1) { return false; @@ -336,7 +337,7 @@ export class DeviceTracker { const masterKey = keyInfo.keys[keyIds[0]]; msks.set(userId, masterKey); return msks; - }, new Map()); + }, masterKeys); return masterKeys; } @@ -680,12 +681,14 @@ export function tests() { const txn = await storage.readTxn([storage.storeNames.userIdentities]); assert.deepEqual(await txn.userIdentities.get("@alice:hs.tld"), { userId: "@alice:hs.tld", + masterKey: undefined, roomIds: [roomId], deviceTrackingStatus: TRACKING_STATUS_OUTDATED }); assert.deepEqual(await txn.userIdentities.get("@bob:hs.tld"), { userId: "@bob:hs.tld", roomIds: [roomId], + masterKey: undefined, deviceTrackingStatus: TRACKING_STATUS_OUTDATED }); assert.equal(await txn.userIdentities.get("@charly:hs.tld"), undefined); From 751987826441f26107ec05a9ce86dd2ced83de34 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 14 Feb 2023 16:30:25 +0100 Subject: [PATCH 7/7] add stronger warning to enable cross-signing --- src/domain/session/settings/FeaturesViewModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/settings/FeaturesViewModel.ts b/src/domain/session/settings/FeaturesViewModel.ts index 552e27d4..b0ee3598 100644 --- a/src/domain/session/settings/FeaturesViewModel.ts +++ b/src/domain/session/settings/FeaturesViewModel.ts @@ -32,7 +32,7 @@ export class FeaturesViewModel extends ViewModel { })), new FeatureViewModel(this.childOptions({ name: this.i18n`Cross-Signing`, - description: this.i18n`Allows.verifying the identity of people you chat with`, + description: this.i18n`Allows verifying the identity of people you chat with. This feature is still evolving constantly, expect things to break.`, feature: FeatureFlag.CrossSigning })), ];