From e079a4d76cc52c75c1c02b1abf9e61ef00546551 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 5 Jun 2023 11:43:07 +0530 Subject: [PATCH 01/44] Check if the device is up to date If not, fetch from hs --- src/matrix/e2ee/DeviceTracker.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index dc3e4008..374d99f3 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -528,6 +528,21 @@ export class DeviceTracker { /** Gets a single device */ async deviceForId(userId: string, deviceId: string, hsApi: HomeServerApi, log: ILogItem) { + /** + * 1. If the device keys are outdated, we will fetch all the keys and update them. + */ + const userIdentityTxn = await this._storage.readTxn([this._storage.storeNames.userIdentities]); + const userIdentity = await userIdentityTxn.userIdentities.get(userId); + if (userIdentity?.keysTrackingStatus !== KeysTrackingStatus.UpToDate) { + const {deviceKeys} = await this._queryKeys([userId], hsApi, log); + const keyList = deviceKeys.get(userId); + const device = keyList!.find(device => device.device_id === deviceId); + return device; + } + + /** + * 2. If keys are up to date, return from storage. + */ const txn = await this._storage.readTxn([ this._storage.storeNames.deviceKeys, ]); @@ -554,6 +569,9 @@ export class DeviceTracker { const txn = await this._storage.readWriteTxn([ this._storage.storeNames.deviceKeys, ]); + // todo: the following comment states what the code does + // but it fails to explain why it does what it does... + // check again we don't have the device already. // when updating all keys for a user we allow updating the // device when the key hasn't changed so the device display name From ed3772d67b638e040ee79c830eda5999cb9dff7c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 5 Jun 2023 11:44:03 +0530 Subject: [PATCH 02/44] Add method to fetch device by curve key --- src/matrix/e2ee/DeviceTracker.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 374d99f3..914c60f5 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -595,6 +595,22 @@ export class DeviceTracker { return deviceKey; } + async deviceForCurveKey(userId: string, key: string, hsApi: HomeServerApi, log: ILogItem) { + const txn = await this._storage.readTxn([ + this._storage.storeNames.deviceKeys, + this._storage.storeNames.userIdentities, + ]); + const userIdentity = await txn.userIdentities.get(userId); + if (userIdentity?.keysTrackingStatus !== KeysTrackingStatus.UpToDate) { + const {deviceKeys} = await this._queryKeys([userId], hsApi, log); + const keyList = deviceKeys.get(userId); + const device = keyList!.find(device => device.keys.curve25519 === key); + return device; + } + const device = await txn.deviceKeys.getByCurve25519Key(key); + return device; + } + /** * Gets all the device identities with which keys should be shared for a set of users in a tracked room. * If any userIdentities are outdated, it will fetch them from the homeserver. From 3a1ffe8544c9cbdef3252b1f68e15558d5026a20 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 5 Jun 2023 11:44:30 +0530 Subject: [PATCH 03/44] Add getter for device-id --- src/matrix/e2ee/DeviceTracker.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 914c60f5..995b8ade 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -677,6 +677,10 @@ export class DeviceTracker { async getDeviceByCurve25519Key(curve25519Key, txn: Transaction): Promise { return await txn.deviceKeys.getByCurve25519Key(curve25519Key); } + + get ownDeviceId(): string { + return this._ownDeviceId; + } } import {createMockStorage} from "../../mocks/Storage"; From 83236a63ac29e7b42a6841432cdf59ad4a0c8aeb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 5 Jun 2023 11:45:00 +0530 Subject: [PATCH 04/44] Fix typo --- src/matrix/e2ee/DeviceTracker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 995b8ade..0cde59cf 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -645,7 +645,7 @@ export class DeviceTracker { /** Gets the device identites for a set of user identities that * are known to be up to date, and a set of userIds that are known - * to be absent from our store our outdated. The outdated user ids + * to be absent from our store or are outdated. The outdated user ids * will have their keys fetched from the homeserver. */ async _devicesForUserIdentities(upToDateIdentities: UserIdentity[], outdatedUserIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { log.set("uptodate", upToDateIdentities.length); From e16d69143774f51f01072bf87b0da200d6fbe741 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 5 Jun 2023 11:46:01 +0530 Subject: [PATCH 05/44] Check key and emit event for encrypted message --- src/matrix/DeviceMessageHandler.js | 33 +++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/matrix/DeviceMessageHandler.js b/src/matrix/DeviceMessageHandler.js index 1af139b0..68da1661 100644 --- a/src/matrix/DeviceMessageHandler.js +++ b/src/matrix/DeviceMessageHandler.js @@ -77,7 +77,32 @@ export class DeviceMessageHandler extends EventEmitter{ } async afterSyncCompleted(decryptionResults, deviceTracker, hsApi, log) { - this._emitEncryptedEvents(decryptionResults); + // this._emitEncryptedEvents(decryptionResults); + await log.wrap("Verifying fingerprint of encrypted toDevice messages", async (log) => { + for (const result of decryptionResults) { + const sender = result.event.sender; + const device = await deviceTracker.deviceForCurveKey( + sender, + result.senderCurve25519Key, + hsApi, + log + ); + result.setDevice(device); + if (result.isVerified) { + this.emit("message", { encrypted: result }); + } + else { + log.log({ + l: "could not verify olm fingerprint key matches, ignoring", + ed25519Key: result.device.ed25519Key, + claimedEd25519Key: result.claimedEd25519Key, + deviceId: device.deviceId, + userId: device.userId, + }); + } + } + }); + // todo: Refactor the following to use to device messages if (this._callHandler) { // if we don't have a device, we need to fetch the device keys the message claims // and check the keys, and we should only do network requests during @@ -113,12 +138,6 @@ export class DeviceMessageHandler extends EventEmitter{ } } - _emitEncryptedEvents(decryptionResults) { - // We don't emit for now as we're not verifying the identity of the sender - // for (const result of decryptionResults) { - // this.emit("message", { encrypted: result }); - // } - } } class SyncPreparation { From c0928c75f833f11b57b4a5b5f329b751d28d5b79 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 5 Jun 2023 11:47:55 +0530 Subject: [PATCH 06/44] Export all classes from index.ts --- src/matrix/ssss/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/matrix/ssss/index.ts b/src/matrix/ssss/index.ts index 02f3290e..d6875542 100644 --- a/src/matrix/ssss/index.ts +++ b/src/matrix/ssss/index.ts @@ -24,6 +24,12 @@ import type {KeyDescriptionData} from "./common"; import type {Platform} from "../../platform/web/Platform.js"; import type * as OlmNamespace from "@matrix-org/olm" +// Add exports for other classes +export {SecretFetcher} from "./SecretFetcher"; +export {SharedSecret} from "./SharedSecret"; +export {SecretStorage} from "./SecretStorage"; + + type Olm = typeof OlmNamespace; const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`; From e6a9e39c7d19a37426b9761f001214199c727076 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 5 Jun 2023 11:51:07 +0530 Subject: [PATCH 07/44] Implement store for shared secret --- src/matrix/storage/common.ts | 3 +- src/matrix/storage/idb/Transaction.ts | 5 +++ src/matrix/storage/idb/schema.ts | 8 +++- .../storage/idb/stores/SharedSecretStore.ts | 39 +++++++++++++++++++ .../verification/SAS/channel/Channel.ts | 8 +++- 5 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 src/matrix/storage/idb/stores/SharedSecretStore.ts diff --git a/src/matrix/storage/common.ts b/src/matrix/storage/common.ts index bf9ce39b..ce85056d 100644 --- a/src/matrix/storage/common.ts +++ b/src/matrix/storage/common.ts @@ -34,7 +34,8 @@ export enum StoreNames { operations = "operations", accountData = "accountData", calls = "calls", - crossSigningKeys = "crossSigningKeys" + crossSigningKeys = "crossSigningKeys", + sharedSecrets = "sharedSecrets", } export const STORE_NAMES: Readonly = Object.values(StoreNames); diff --git a/src/matrix/storage/idb/Transaction.ts b/src/matrix/storage/idb/Transaction.ts index 4c76608c..2070eaae 100644 --- a/src/matrix/storage/idb/Transaction.ts +++ b/src/matrix/storage/idb/Transaction.ts @@ -38,6 +38,7 @@ import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore" import {OperationStore} from "./stores/OperationStore"; import {AccountDataStore} from "./stores/AccountDataStore"; import {CallStore} from "./stores/CallStore"; +import {SharedSecretStore} from "./stores/SharedSecretStore"; import type {ILogger, ILogItem} from "../../../logging/types"; export type IDBKey = IDBValidKey | IDBKeyRange; @@ -178,6 +179,10 @@ export class Transaction { return this._store(StoreNames.calls, idbStore => new CallStore(idbStore)); } + get sharedSecrets(): SharedSecretStore { + return this._store(StoreNames.sharedSecrets, idbStore => new SharedSecretStore(idbStore)); + } + async complete(log?: ILogItem): Promise { try { await txnAsPromise(this._txn); diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 9b4d5547..ce3c89c4 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -37,7 +37,8 @@ export const schema: MigrationFunc[] = [ addInboundSessionBackupIndex, migrateBackupStatus, createCallStore, - applyCrossSigningChanges + applyCrossSigningChanges, + createSharedSecretStore, ]; // TODO: how to deal with git merge conflicts of this array? @@ -299,3 +300,8 @@ async function applyCrossSigningChanges(db: IDBDatabase, txn: IDBTransaction, lo }); log.set("marked_outdated", counter); } + +//v19 create shared secrets store +function createSharedSecretStore(db: IDBDatabase) : void { + db.createObjectStore("sharedSecrets", {keyPath: "key"}); +} diff --git a/src/matrix/storage/idb/stores/SharedSecretStore.ts b/src/matrix/storage/idb/stores/SharedSecretStore.ts new file mode 100644 index 00000000..79f9f625 --- /dev/null +++ b/src/matrix/storage/idb/stores/SharedSecretStore.ts @@ -0,0 +1,39 @@ +/* +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 {Store} from "../Store"; + +type SharedSecret = any; + +export class SharedSecretStore { + private _store: Store; + + constructor(store: Store) { + this._store = store; + } + + get(name: string): Promise { + return this._store.get(name); + } + + set(name: string, secret: SharedSecret): void { + secret.key = name; + this._store.put(secret); + } + + remove(name: string): void { + this._store.delete(name); + } +} diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 10adbd7f..2860ea48 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -98,8 +98,12 @@ export class ToDeviceChannel extends Disposables implements IChannel { this.track( this.deviceMessageHandler.disposableOn( "message", - async ({ unencrypted }) => - await this.handleDeviceMessage(unencrypted) + async ({ unencrypted }) => { + if (!unencrypted) { + return; + } + await this.handleDeviceMessage(unencrypted); + } ) ); this.track(() => { From 631b2f059fb03897bead785b6e2133ee9f8b6d27 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 5 Jun 2023 11:52:54 +0530 Subject: [PATCH 08/44] Implement secret sharing --- src/matrix/Session.js | 84 ++++--- src/matrix/ssss/SecretFetcher.ts | 46 ++++ src/matrix/ssss/SharedSecret.ts | 277 ++++++++++++++++++++++++ src/matrix/verification/CrossSigning.ts | 25 ++- 4 files changed, 396 insertions(+), 36 deletions(-) create mode 100644 src/matrix/ssss/SecretFetcher.ts create mode 100644 src/matrix/ssss/SharedSecret.ts diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 2f4e0f1d..f3e85540 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -43,9 +43,11 @@ import { readKey as ssssReadKey, writeKey as ssssWriteKey, removeKey as ssssRemoveKey, - keyFromDehydratedDeviceKey as createSSSSKeyFromDehydratedDeviceKey + keyFromDehydratedDeviceKey as createSSSSKeyFromDehydratedDeviceKey, + SecretStorage, + SharedSecret, + SecretFetcher } from "./ssss/index"; -import {SecretStorage} from "./ssss/SecretStorage"; import {ObservableValue, RetainedObservableValue} from "../observable/value"; import {CallHandler} from "./calls/CallHandler"; import {RoomStateHandlerSet} from "./room/state/RoomStateHandlerSet"; @@ -109,6 +111,7 @@ export class Session { this._createRoomEncryption = this._createRoomEncryption.bind(this); this._forgetArchivedRoom = this._forgetArchivedRoom.bind(this); this.needsKeyBackup = new ObservableValue(false); + this.secretFetcher = new SecretFetcher(); } get fingerprintKey() { @@ -198,6 +201,20 @@ export class Session { }); this._megolmDecryption = new MegOlmDecryption(this._keyLoader, this._olmWorker); this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption: this._megolmDecryption}); + this._sharedSecret = new SharedSecret({ + hsApi: this._hsApi, + storage: this._storage, + deviceMessageHandler: this._deviceMessageHandler, + deviceTracker: this._deviceTracker, + ourUserId: this.userId, + olmEncryption: this._olmEncryption, + crypto: this._platform.crypto, + encoding: this._platform.encoding, + crossSigning: this._crossSigning, + logger: this._platform.logger, + }); + this.secretFetcher.setSecretSharing(this._sharedSecret); + } _createRoomEncryption(room, encryptionParams) { @@ -251,11 +268,11 @@ export class Session { this._keyBackup.get().dispose(); this._keyBackup.set(undefined); } - const crossSigning = this._crossSigning.get(); - if (crossSigning) { - crossSigning.dispose(); - this._crossSigning.set(undefined); - } + // const crossSigning = this._crossSigning.get(); + // if (crossSigning) { + // crossSigning.dispose(); + // this._crossSigning.set(undefined); + // } const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); if (await this._tryLoadSecretStorage(key, log)) { // only after having read a secret, write the key @@ -331,7 +348,9 @@ export class Session { const isValid = await secretStorage.hasValidKeyForAnyAccountData(); log.set("isValid", isValid); if (isValid) { - await this._loadSecretStorageServices(secretStorage, log); + this._secretStorage = secretStorage; + await this._loadSecretStorageService(log); + this.secretFetcher.setSecretStorage(secretStorage); } return isValid; }); @@ -359,29 +378,6 @@ export class Session { log.set("no_backup", true); } }); - if (this._features.crossSigning) { - await log.wrap("enable cross-signing", async log => { - const crossSigning = new CrossSigning({ - storage: this._storage, - secretStorage, - platform: this._platform, - olm: this._olm, - olmUtil: this._olmUtil, - deviceTracker: this._deviceTracker, - deviceMessageHandler: this._deviceMessageHandler, - hsApi: this._hsApi, - ownUserId: this.userId, - e2eeAccount: this._e2eeAccount, - deviceId: this.deviceId, - }); - if (await crossSigning.load(log)) { - this._crossSigning.set(crossSigning); - } - else { - crossSigning.dispose(); - } - }); - } } catch (err) { log.catch(err); } @@ -588,6 +584,32 @@ export class Session { } }); } + + if (this._features.crossSigning) { + this._platform.logger.run("enable cross-signing", async log => { + const crossSigning = new CrossSigning({ + storage: this._storage, + // secretStorage: this._secretStorage, + secretFetcher: this.secretFetcher, + platform: this._platform, + olm: this._olm, + olmUtil: this._olmUtil, + deviceTracker: this._deviceTracker, + deviceMessageHandler: this._deviceMessageHandler, + hsApi: this._hsApi, + ownUserId: this.userId, + e2eeAccount: this._e2eeAccount, + deviceId: this.deviceId, + }); + this._crossSigning.set(crossSigning); + // if (await crossSigning.load(log)) { + // this._crossSigning.set(crossSigning); + // } + // else { + // crossSigning.dispose(); + // } + }); + } await this._keyBackup.get()?.start(log); await this._crossSigning.get()?.start(log); diff --git a/src/matrix/ssss/SecretFetcher.ts b/src/matrix/ssss/SecretFetcher.ts new file mode 100644 index 00000000..9da0cc4a --- /dev/null +++ b/src/matrix/ssss/SecretFetcher.ts @@ -0,0 +1,46 @@ +/* +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 "./SecretStorage"; +import type {SharedSecret} from "./SharedSecret"; + +/** + * This is a wrapper around SecretStorage and SecretSharing so that + * you don't need to always check both sources for something. + */ +export class SecretFetcher { + public secretStorage: SecretStorage; + public secretSharing: SharedSecret; + + async getSecret(name: string): Promise { + ; + return await this.secretStorage?.readSecret(name) ?? + await this.secretSharing?.getLocallyStoredSecret(name); + // note that we don't ask another device for secret here + // that should be done explicitly since it can take arbitrary + // amounts of time to be fulfilled as the other devices may + // be offline etc... + } + + setSecretStorage(storage: SecretStorage) { + this.secretStorage = storage; + } + + setSecretSharing(sharing: SharedSecret) { + this.secretSharing = sharing; + this.secretSharing.setSecretFetcher(this); + } +} diff --git a/src/matrix/ssss/SharedSecret.ts b/src/matrix/ssss/SharedSecret.ts new file mode 100644 index 00000000..d6736521 --- /dev/null +++ b/src/matrix/ssss/SharedSecret.ts @@ -0,0 +1,277 @@ +/* +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 {HomeServerApi} from "../net/HomeServerApi"; +import type {Storage} from "../storage/idb/Storage"; +import type {DeviceMessageHandler} from "../DeviceMessageHandler.js" +import type {DeviceTracker} from "../e2ee/DeviceTracker"; +import type {ILogger, ILogItem} from "../../logging/types"; +import type {Encryption as OlmEncryption} from "../e2ee/olm/Encryption"; +import type {Crypto} from "../../platform/web/dom/Crypto.js"; +import type {Encoding} from "../../platform/web/utils/Encoding.js"; +import type {CrossSigning} from "../verification/CrossSigning"; +import type {SecretFetcher} from "./SecretFetcher"; +import type {ObservableValue} from "../../observable/value"; +import {makeTxnId, formatToDeviceMessagesPayload} from "../common.js"; +import {Deferred} from "../../utils/Deferred"; +import {StoreNames} from "../storage/common"; +import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common"; + +type Options = { + hsApi: HomeServerApi; + storage: Storage; + deviceMessageHandler: DeviceMessageHandler; + deviceTracker: DeviceTracker; + ourUserId: string; + olmEncryption: OlmEncryption; + crypto: Crypto; + encoding: Encoding; + crossSigning: ObservableValue; + logger: ILogger; +}; + +const enum EVENT_TYPE { + REQUEST = "m.secret.request", + SEND = "m.secret.send", +} + +export class SharedSecret { + private readonly hsApi: HomeServerApi; + private readonly storage: Storage; + private readonly deviceMessageHandler: DeviceMessageHandler; + private readonly deviceTracker: DeviceTracker; + private readonly ourUserId: string; + private readonly olmEncryption: OlmEncryption; + private readonly waitMap: Map> = new Map(); + private readonly crypto: Crypto; + private readonly encoding: Encoding; + private readonly aesEncryption: AESEncryption; + private readonly crossSigning: ObservableValue; + private readonly logger: ILogger; + private secretFetcher: SecretFetcher; + + constructor(options: Options) { + this.hsApi = options.hsApi; + this.storage = options.storage; + this.deviceMessageHandler = options.deviceMessageHandler; + this.deviceTracker = options.deviceTracker; + this.ourUserId = options.ourUserId; + this.olmEncryption = options.olmEncryption; + this.crypto = options.crypto; + this.encoding = options.encoding; + this.crossSigning = options.crossSigning; + this.logger = options.logger; + this.aesEncryption = new AESEncryption(this.storage, this.crypto, this.encoding); + (window as any).foo = this; + this.init(); + } + + private async init() { + this.deviceMessageHandler.on("message", ({ encrypted }) => { + const type: EVENT_TYPE = encrypted?.event.type; + switch (type) { + case EVENT_TYPE.REQUEST: { + this._respondToRequest(encrypted); + } + case EVENT_TYPE.SEND: { + const { request_id } = encrypted.event.content; + const deffered = this.waitMap.get(request_id); + deffered?.resolve(encrypted); + this.waitMap.delete(request_id); + break; + } + } + }); + await this.aesEncryption.load(); + } + + private async _respondToRequest(request) { + await this.logger.run("SharedSecret.respondToRequest", async (log) => { + if (!this.shouldRespondToRequest(request, log)) { + return; + } + const requestContent = request.event.content; + const id = requestContent.request_id; + const deviceId = requestContent.requesting_device_id; + const name = requestContent.name; + + const secret = await this.secretFetcher.getSecret(name); + if (!secret) { + // Can't share a secret that we don't know about. + log.log({ l: "Secret not available to share" }); + return; + } + + const content = { secret, request_id: id }; + const device = await this.deviceTracker.deviceForId(this.ourUserId, deviceId, this.hsApi, log); + if (!device) { + log.log({ l: "Cannot find device", deviceId }); + return; + } + const messages = await log.wrap("olm encrypt", log => this.olmEncryption.encrypt( + EVENT_TYPE.SEND, content, [device], this.hsApi, log)); + console.log("messages", messages); + const payload = formatToDeviceMessagesPayload(messages); + console.log("payload", payload); + await this.hsApi.sendToDevice("m.room.encrypted", payload, makeTxnId(), {log}).response(); + }); + } + + private async shouldRespondToRequest(request: any, log: ILogItem): Promise { + return log.wrap("SecretSharing.shouldRespondToRequest", async () => { + const crossSigning = this.crossSigning.get(); + if (!crossSigning) { + // We're not in a position to respond to this request + log.log({ crossSigningNotAvailable: true }); + return false; + } + + const content = request.event.content; + if ( + request.event.sender !== this.ourUserId || + !( + content.name && + content.action && + content.requesting_device_id && + content.request_id + ) || + content.action === "request_cancellation" + ) { + // 1. Ensure that the message came from the same user as us + // 2. Validate message format + // 3. Check if this is a cancellation + return false; + } + + // 3. Check that the device is verified + const deviceId = content.requesting_device_id; + const device = await this.deviceTracker.deviceForId(this.ourUserId, deviceId, this.hsApi, log); + if (!device) { + log.log({ l: "Device could not be acquired", deviceId }); + return false; + } + if (!await crossSigning.isOurUserDeviceTrusted(device, log)) { + log.log({ l: "Device not trusted, returning" }); + return false; + } + return true; + }) + + } + + async getLocallyStoredSecret(name: string): Promise { + const txn = await this.storage.readTxn([ + this.storage.storeNames.sharedSecrets, + ]); + const storedSecret = await txn.sharedSecrets.get(name); + if (storedSecret) { + const secret = await this.aesEncryption.decrypt(storedSecret.encrypted); + return secret; + } + } + + // todo: this will break if two different pieces of code call this method + requestSecret(name: string, log: ILogItem): Promise { + return log.wrap("SharedSecret.requestSecret", async (_log) => { + const request_id = makeTxnId(); + const promise = this.trackSecretRequest(request_id); + await this.sendRequestForSecret(name, request_id, _log); + const result = await promise; + const secret = result.event.content.secret; + await this.writeToStorage(name, secret); + return secret; + }); + } + + private async writeToStorage(name:string, secret: any) { + const encrypted = await this.aesEncryption.encrypt(secret); + const txn = await this.storage.readWriteTxn([StoreNames.sharedSecrets]); + txn.sharedSecrets.set(name, { encrypted }); + } + + private trackSecretRequest(request_id: string): Promise { + const deferred = new Deferred(); + this.waitMap.set(request_id, deferred); + return deferred.promise; + } + + private async sendRequestForSecret(name: string, request_id: string, log: ILogItem) { + const content = { + action: "request", + name, + request_id, + requesting_device_id: this.deviceTracker.ownDeviceId, + } + let devices = await this.deviceTracker.devicesForUsers([this.ourUserId], this.hsApi, log); + devices = devices.filter(d => d.device_id !== this.deviceTracker.ownDeviceId); + const messages = await log.wrap("olm encrypt", log => this.olmEncryption.encrypt( + EVENT_TYPE.REQUEST, content, devices, this.hsApi, log)); + console.log("messages", messages); + const payload = formatToDeviceMessagesPayload(messages); + console.log("payload", payload); + await this.hsApi.sendToDevice("m.room.encrypted", payload, makeTxnId(), {log}).response(); + } + + setSecretFetcher(secretFetcher: SecretFetcher): void { + this.secretFetcher = secretFetcher; + } +} + +class AESEncryption { + private key: JsonWebKey; + private iv: Uint8Array; + + constructor(private storage: Storage, private crypto: Crypto, private encoding: Encoding) { }; + + async load() { + const storageKey = `${SESSION_E2EE_KEY_PREFIX}localAESKey`; + // 1. Check if we're already storing the AES key + const txn = await this.storage.readTxn([StoreNames.session]); + let { key, iv } = await txn.session.get(storageKey) ?? {}; + + // 2. If no key, create it and store in session store + if (!key) { + key = await this.crypto.aes.generateKey("jwk"); + iv = await this.crypto.aes.generateIV(); + const txn = await this.storage.readWriteTxn([StoreNames.session]); + txn.session.set(storageKey, { key, iv }); + } + + // 3. Set props + this.key = key; + this.iv = iv; + } + + async encrypt(secret: string): Promise { + const data = this.encoding.utf8.encode(secret); + const encrypted = await this.crypto.aes.encryptCTR({ + jwkKey: this.key, + iv: this.iv, + data, + }); + return encrypted; + } + + async decrypt(ciphertext: Uint8Array): Promise { + const buffer = await this.crypto.aes.decryptCTR({ + jwkKey: this.key, + iv: this.iv, + data: ciphertext, + }); + const secret = this.encoding.utf8.decode(buffer); + return secret; + } +} diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index c23c2f54..a685a898 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -22,7 +22,7 @@ import {ToDeviceChannel} from "./SAS/channel/Channel"; import {VerificationEventType} from "./SAS/channel/types"; import {ObservableMap} from "../../observable/map"; import {SASRequest} from "./SAS/SASRequest"; -import type {SecretStorage} from "../ssss/SecretStorage"; +import {SecretFetcher} from "../ssss"; import type {Storage} from "../storage/idb/Storage"; import type {Platform} from "../../platform/web/Platform"; import type {DeviceTracker} from "../e2ee/DeviceTracker"; @@ -80,7 +80,7 @@ enum MSKVerification { export class CrossSigning { private readonly storage: Storage; - private readonly secretStorage: SecretStorage; + private readonly secretFetcher: SecretFetcher; private readonly platform: Platform; private readonly deviceTracker: DeviceTracker; private readonly olm: Olm; @@ -97,7 +97,7 @@ export class CrossSigning { constructor(options: { storage: Storage, - secretStorage: SecretStorage, + secretFetcher: SecretFetcher, deviceTracker: DeviceTracker, platform: Platform, olm: Olm, @@ -109,7 +109,7 @@ export class CrossSigning { deviceMessageHandler: DeviceMessageHandler, }) { this.storage = options.storage; - this.secretStorage = options.secretStorage; + this.secretFetcher = options.secretFetcher; this.platform = options.platform; this.deviceTracker = options.deviceTracker; this.olm = options.olm; @@ -208,6 +208,7 @@ export class CrossSigning { } private handleSASDeviceMessage({ unencrypted: event }) { + if (!event) { return; } const txnId = event.content.transaction_id; /** * If we receive an event for the current/previously finished @@ -304,6 +305,20 @@ export class CrossSigning { }); } + async isOurUserDeviceTrusted(device: DeviceKey, log: ILogItem): Promise { + return await log.wrap("CrossSigning.getDeviceTrust", async () => { + const ourSSK = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.SelfSigning, this.hsApi, log); + if (!ourSSK) { + return false; + } + const verification = this.hasValidSignatureFrom(device, ourSSK, log); + if (verification === SignatureVerification.Valid) { + return true; + } + return false; + }); + } + getUserTrust(userId: string, log: ILogItem): Promise { return log.wrap("CrossSigning.getUserTrust", async log => { log.set("id", userId); @@ -421,7 +436,7 @@ export class CrossSigning { } private async getSigningKey(usage: KeyUsage): Promise { - const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`); + const seedStr = await this.secretFetcher.getSecret(`m.cross_signing.${usage}`); if (seedStr) { return new Uint8Array(this.platform.encoding.base64.decode(seedStr)); } From 8b36392b90dfd67d7dbe1b538a2eb68f138cb887 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 5 Jun 2023 11:58:55 +0530 Subject: [PATCH 09/44] Rename "SharedSecret" -> "SecretSharing" --- src/matrix/Session.js | 6 +++--- src/matrix/ssss/SecretFetcher.ts | 6 +++--- src/matrix/ssss/{SharedSecret.ts => SecretSharing.ts} | 2 +- src/matrix/ssss/index.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename src/matrix/ssss/{SharedSecret.ts => SecretSharing.ts} (99%) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index f3e85540..e5c26ca2 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -45,7 +45,7 @@ import { removeKey as ssssRemoveKey, keyFromDehydratedDeviceKey as createSSSSKeyFromDehydratedDeviceKey, SecretStorage, - SharedSecret, + SecretSharing, SecretFetcher } from "./ssss/index"; import {ObservableValue, RetainedObservableValue} from "../observable/value"; @@ -201,7 +201,7 @@ export class Session { }); this._megolmDecryption = new MegOlmDecryption(this._keyLoader, this._olmWorker); this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption: this._megolmDecryption}); - this._sharedSecret = new SharedSecret({ + this._secretSharing = new SecretSharing({ hsApi: this._hsApi, storage: this._storage, deviceMessageHandler: this._deviceMessageHandler, @@ -213,7 +213,7 @@ export class Session { crossSigning: this._crossSigning, logger: this._platform.logger, }); - this.secretFetcher.setSecretSharing(this._sharedSecret); + this.secretFetcher.setSecretSharing(this._secretSharing); } diff --git a/src/matrix/ssss/SecretFetcher.ts b/src/matrix/ssss/SecretFetcher.ts index 9da0cc4a..46caf705 100644 --- a/src/matrix/ssss/SecretFetcher.ts +++ b/src/matrix/ssss/SecretFetcher.ts @@ -15,7 +15,7 @@ limitations under the License. */ import type {SecretStorage} from "./SecretStorage"; -import type {SharedSecret} from "./SharedSecret"; +import type {SecretSharing} from "./SecretSharing"; /** * This is a wrapper around SecretStorage and SecretSharing so that @@ -23,7 +23,7 @@ import type {SharedSecret} from "./SharedSecret"; */ export class SecretFetcher { public secretStorage: SecretStorage; - public secretSharing: SharedSecret; + public secretSharing: SecretSharing; async getSecret(name: string): Promise { ; @@ -39,7 +39,7 @@ export class SecretFetcher { this.secretStorage = storage; } - setSecretSharing(sharing: SharedSecret) { + setSecretSharing(sharing: SecretSharing) { this.secretSharing = sharing; this.secretSharing.setSecretFetcher(this); } diff --git a/src/matrix/ssss/SharedSecret.ts b/src/matrix/ssss/SecretSharing.ts similarity index 99% rename from src/matrix/ssss/SharedSecret.ts rename to src/matrix/ssss/SecretSharing.ts index d6736521..153cdb4c 100644 --- a/src/matrix/ssss/SharedSecret.ts +++ b/src/matrix/ssss/SecretSharing.ts @@ -48,7 +48,7 @@ const enum EVENT_TYPE { SEND = "m.secret.send", } -export class SharedSecret { +export class SecretSharing { private readonly hsApi: HomeServerApi; private readonly storage: Storage; private readonly deviceMessageHandler: DeviceMessageHandler; diff --git a/src/matrix/ssss/index.ts b/src/matrix/ssss/index.ts index d6875542..c782a6ef 100644 --- a/src/matrix/ssss/index.ts +++ b/src/matrix/ssss/index.ts @@ -26,7 +26,7 @@ import type * as OlmNamespace from "@matrix-org/olm" // Add exports for other classes export {SecretFetcher} from "./SecretFetcher"; -export {SharedSecret} from "./SharedSecret"; +export {SecretSharing} from "./SecretSharing"; export {SecretStorage} from "./SecretStorage"; From 6c7cb4d678be1c93a15864dc80c4357703b08172 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 7 Jun 2023 14:50:57 +0530 Subject: [PATCH 10/44] Ask for secret after device verification --- .../DeviceVerificationViewModel.ts | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 3257c784..21f2badf 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -25,6 +25,7 @@ import {VerificationCompleteViewModel} from "./stages/VerificationCompleteViewMo import type {Session} from "../../../matrix/Session.js"; import type {SASVerification} from "../../../matrix/verification/SAS/SASVerification"; import type {SASRequest} from "../../../matrix/verification/SAS/SASRequest"; +import type {CrossSigning} from "../../../matrix/verification/CrossSigning"; type Options = BaseOptions & { session: Session; @@ -34,6 +35,7 @@ type Options = BaseOptions & { export class DeviceVerificationViewModel extends ErrorReportViewModel { private sas: SASVerification; private _currentStageViewModel: any; + private _needsToRequestSecret: boolean; constructor(options: Readonly) { super(options); @@ -48,13 +50,14 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { - const crossSigning = this.getOption("session").crossSigning.get(); - this.sas = crossSigning.startVerification(requestOrUserId, log); + await this.logAndCatch("DeviceVerificationViewModel.start", async (log) => { + const crossSigning = this.getOption("session").crossSigning.get() as CrossSigning; + this.sas = crossSigning.startVerification(requestOrUserId, log)!; this.addEventListeners(); if (typeof requestOrUserId === "string") { this.updateCurrentStageViewModel(new WaitingForOtherUserViewModel(this.childOptions({ sas: this.sas }))); } + this._needsToRequestSecret = !await crossSigning.areWeVerified(log); return this.sas.start(); }); } @@ -81,9 +84,25 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { + if (this._needsToRequestSecret) { + const neededSecrets = [ + "m.cross_signing.master", + "m.cross_signing.self_signing", + "m.cross_signing.user_signing", + ]; + const secretSharing = this.getOption("session").secretSharing; + const promises = neededSecrets.map((secret) => secretSharing.requestSecret(secret, log)); + await Promise.all(promises); + } + }); + } + private updateCurrentStageViewModel(vm) { this._currentStageViewModel = this.disposeTracked(this._currentStageViewModel); this._currentStageViewModel = this.track(vm); From b4805ab862dce981c04126b58438947f1186b01d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 7 Jun 2023 14:51:16 +0530 Subject: [PATCH 11/44] Show key backup vm when cross-signing is available --- src/domain/session/settings/KeyBackupViewModel.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/domain/session/settings/KeyBackupViewModel.ts b/src/domain/session/settings/KeyBackupViewModel.ts index 43681a29..7648e4de 100644 --- a/src/domain/session/settings/KeyBackupViewModel.ts +++ b/src/domain/session/settings/KeyBackupViewModel.ts @@ -66,6 +66,9 @@ export class KeyBackupViewModel extends ViewModel { this._onKeyBackupChange(); // update status }; this.track(this._session.keyBackup.subscribe(onKeyBackupSet)); + this.track(this._session.crossSigning.subscribe(() => { + this.emitChange("crossSigning"); + })); onKeyBackupSet(this._keyBackup); } From f8c5878a01d9a5b00e8322deb2983822ddd9cbb9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 7 Jun 2023 14:52:32 +0530 Subject: [PATCH 12/44] Fix key backup setup failing --- src/matrix/Session.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index e5c26ca2..997b96af 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -111,7 +111,8 @@ export class Session { this._createRoomEncryption = this._createRoomEncryption.bind(this); this._forgetArchivedRoom = this._forgetArchivedRoom.bind(this); this.needsKeyBackup = new ObservableValue(false); - this.secretFetcher = new SecretFetcher(); + this._secretFetcher = new SecretFetcher(); + this._secretSharing = null; } get fingerprintKey() { @@ -213,7 +214,7 @@ export class Session { crossSigning: this._crossSigning, logger: this._platform.logger, }); - this.secretFetcher.setSecretSharing(this._secretSharing); + this._secretFetcher.setSecretSharing(this._secretSharing); } @@ -349,8 +350,8 @@ export class Session { log.set("isValid", isValid); if (isValid) { this._secretStorage = secretStorage; - await this._loadSecretStorageService(log); - this.secretFetcher.setSecretStorage(secretStorage); + await this._loadSecretStorageServices(secretStorage, log); + this._secretFetcher.setSecretStorage(secretStorage); } return isValid; }); @@ -396,6 +397,10 @@ export class Session { return this._crossSigning; } + get secretSharing() { + return this._secretSharing; + } + get hasIdentity() { return !!this._e2eeAccount; } @@ -590,7 +595,7 @@ export class Session { const crossSigning = new CrossSigning({ storage: this._storage, // secretStorage: this._secretStorage, - secretFetcher: this.secretFetcher, + secretFetcher: this._secretFetcher, platform: this._platform, olm: this._olm, olmUtil: this._olmUtil, From 60966c482ab67be57e7545954889bdb7f8b18ba8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 7 Jun 2023 14:53:09 +0530 Subject: [PATCH 13/44] Ignore our own requests --- src/matrix/ssss/SecretSharing.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/matrix/ssss/SecretSharing.ts b/src/matrix/ssss/SecretSharing.ts index 153cdb4c..030fe714 100644 --- a/src/matrix/ssss/SecretSharing.ts +++ b/src/matrix/ssss/SecretSharing.ts @@ -132,6 +132,10 @@ export class SecretSharing { private async shouldRespondToRequest(request: any, log: ILogItem): Promise { return log.wrap("SecretSharing.shouldRespondToRequest", async () => { + if (request.event.content.requesting_device_id === this.deviceTracker.ownDeviceId) { + // This is the request that we sent, so ignore + return false; + } const crossSigning = this.crossSigning.get(); if (!crossSigning) { // We're not in a position to respond to this request From 5edf5e02c3370e4f2301f14492715489ed51240f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 7 Jun 2023 14:53:46 +0530 Subject: [PATCH 14/44] No need to overload this method --- src/matrix/verification/CrossSigning.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index a685a898..a1eb160b 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -172,8 +172,6 @@ export class CrossSigning { return this._isMasterKeyTrusted; } - startVerification(requestOrUserId: SASRequest, log: ILogItem): SASVerification | undefined; - startVerification(requestOrUserId: string, log: ILogItem): SASVerification | undefined; startVerification(requestOrUserId: string | SASRequest, log: ILogItem): SASVerification | undefined { if (this.sasVerificationInProgress && !this.sasVerificationInProgress.finished) { return; @@ -319,6 +317,13 @@ export class CrossSigning { }); } + areWeVerified(log: ILogItem): Promise { + return log.wrap("CrossSigning.areWeVerified", async () => { + const device = await this.deviceTracker.deviceForId(this.ownUserId, this.deviceId, this.hsApi, log); + return this.isOurUserDeviceTrusted(device!, log); + }); + } + getUserTrust(userId: string, log: ILogItem): Promise { return log.wrap("CrossSigning.getUserTrust", async log => { log.set("id", userId); From 2784500cbd4a93823f20e15f4aa90c1595404a2f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 7 Jun 2023 23:27:27 +0530 Subject: [PATCH 15/44] Fix curve key not being read correctly --- src/matrix/e2ee/DeviceTracker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 0cde59cf..82f9ce02 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -604,7 +604,7 @@ export class DeviceTracker { if (userIdentity?.keysTrackingStatus !== KeysTrackingStatus.UpToDate) { const {deviceKeys} = await this._queryKeys([userId], hsApi, log); const keyList = deviceKeys.get(userId); - const device = keyList!.find(device => device.keys.curve25519 === key); + const device = keyList!.find(device => getDeviceCurve25519Key(device) === key); return device; } const device = await txn.deviceKeys.getByCurve25519Key(key); From af2dde7a1f4a2cec29145614d2b6e343def7e24c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 7 Jun 2023 23:28:29 +0530 Subject: [PATCH 16/44] Improve secret-sharing API --- src/matrix/ssss/SecretSharing.ts | 58 +++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/matrix/ssss/SecretSharing.ts b/src/matrix/ssss/SecretSharing.ts index 030fe714..344351ab 100644 --- a/src/matrix/ssss/SecretSharing.ts +++ b/src/matrix/ssss/SecretSharing.ts @@ -55,7 +55,7 @@ export class SecretSharing { private readonly deviceTracker: DeviceTracker; private readonly ourUserId: string; private readonly olmEncryption: OlmEncryption; - private readonly waitMap: Map> = new Map(); + private readonly waitMap: Map, name: string }> = new Map(); private readonly crypto: Crypto; private readonly encoding: Encoding; private readonly aesEncryption: AESEncryption; @@ -87,10 +87,14 @@ export class SecretSharing { this._respondToRequest(encrypted); } case EVENT_TYPE.SEND: { - const { request_id } = encrypted.event.content; - const deffered = this.waitMap.get(request_id); - deffered?.resolve(encrypted); - this.waitMap.delete(request_id); + const { request_id, secret } = encrypted.event.content; + const obj = this.waitMap.get(request_id); + if (obj) { + const { deferred, name } = obj; + deferred.resolve(encrypted); + this.waitMap.delete(request_id); + this.writeToStorage(name, secret); + } break; } } @@ -188,15 +192,13 @@ export class SecretSharing { } // todo: this will break if two different pieces of code call this method - requestSecret(name: string, log: ILogItem): Promise { + requestSecret(name: string, log: ILogItem): Promise { return log.wrap("SharedSecret.requestSecret", async (_log) => { const request_id = makeTxnId(); - const promise = this.trackSecretRequest(request_id); + const promise = this.trackSecretRequest(request_id, name); await this.sendRequestForSecret(name, request_id, _log); - const result = await promise; - const secret = result.event.content.secret; - await this.writeToStorage(name, secret); - return secret; + const request = new SecretRequest(promise); + return request; }); } @@ -206,9 +208,9 @@ export class SecretSharing { txn.sharedSecrets.set(name, { encrypted }); } - private trackSecretRequest(request_id: string): Promise { + private trackSecretRequest(request_id: string, name: string): Promise { const deferred = new Deferred(); - this.waitMap.set(request_id, deferred); + this.waitMap.set(request_id, { deferred, name }); return deferred.promise; } @@ -234,6 +236,31 @@ export class SecretSharing { } } +class SecretRequest { + constructor(private receivedSecretPromise: Promise) { + } + + /** + * Wait for any of your device to respond to this secret request. + * If you're going to await this method, make sure you do that within a try catch block. + * @param timeout The max time (in seconds) that we will wait, after which the promise rejects + */ + async waitForResponse(timeout: number = 30): Promise { + const timeoutPromise: Promise = new Promise((_, reject) => { + setTimeout(reject, timeout * 1000); + }); + const response = await Promise.race([this.receivedSecretPromise, timeoutPromise]); + return response.event.content.secret; + } +} + + +/** + * The idea is to encrypt the secret with AES before persisting to storage. + * The AES key is also in storage so this isn't really that much more secure. + * But it's a tiny bit better than storing the secret in plaintext. + */ +// todo: We could also encrypt the access-token using AES like element does class AESEncryption { private key: JsonWebKey; private iv: Uint8Array; @@ -248,6 +275,11 @@ class AESEncryption { // 2. If no key, create it and store in session store if (!key) { + /** + * Element creates the key as "non-extractable", meaning that it cannot + * be exported through the crypto API. But since it is going + * to end up in local-storage anyway, I don't see a reason to do that. + */ key = await this.crypto.aes.generateKey("jwk"); iv = await this.crypto.aes.generateIV(); const txn = await this.storage.readWriteTxn([StoreNames.session]); From f8bfc384d31ad3908e7c388014ebffe14169eb55 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 8 Jun 2023 17:50:31 +0530 Subject: [PATCH 17/44] Persist request-id in storage --- src/matrix/ssss/SecretSharing.ts | 70 ++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/src/matrix/ssss/SecretSharing.ts b/src/matrix/ssss/SecretSharing.ts index 344351ab..5f3c2a5e 100644 --- a/src/matrix/ssss/SecretSharing.ts +++ b/src/matrix/ssss/SecretSharing.ts @@ -25,6 +25,7 @@ import type {Encoding} from "../../platform/web/utils/Encoding.js"; import type {CrossSigning} from "../verification/CrossSigning"; import type {SecretFetcher} from "./SecretFetcher"; import type {ObservableValue} from "../../observable/value"; +import type {DecryptionResult} from "../e2ee/DecryptionResult"; import {makeTxnId, formatToDeviceMessagesPayload} from "../common.js"; import {Deferred} from "../../utils/Deferred"; import {StoreNames} from "../storage/common"; @@ -48,6 +49,9 @@ const enum EVENT_TYPE { SEND = "m.secret.send", } + +const STORAGE_KEY = "secretRequestIds"; + export class SecretSharing { private readonly hsApi: HomeServerApi; private readonly storage: Storage; @@ -80,20 +84,18 @@ export class SecretSharing { } private async init() { - this.deviceMessageHandler.on("message", ({ encrypted }) => { + this.deviceMessageHandler.on("message", async ({ encrypted }) => { const type: EVENT_TYPE = encrypted?.event.type; switch (type) { case EVENT_TYPE.REQUEST: { this._respondToRequest(encrypted); + break; } case EVENT_TYPE.SEND: { - const { request_id, secret } = encrypted.event.content; - const obj = this.waitMap.get(request_id); - if (obj) { - const { deferred, name } = obj; - deferred.resolve(encrypted); - this.waitMap.delete(request_id); - this.writeToStorage(name, secret); + const {secret} = encrypted.event.content; + const name = await this.shouldAcceptSecret(encrypted); + if (name) { + this.writeSecretToStorage(name, secret); } break; } @@ -180,6 +182,40 @@ export class SecretSharing { } + /** + * Returns name of the secret if we can accept the response. + * Returns undefined otherwise. + * @param decryptionResult Encrypted to-device event that contains the secret + */ + async shouldAcceptSecret(decryptionResult: DecryptionResult): Promise { + const content = decryptionResult.event.content!; + const requestId = content.request_id; + const obj = this.waitMap.get(requestId); + if (obj) { + const { name, deferred } = obj; + deferred.resolve(decryptionResult); + this.waitMap.delete(requestId); + await this.removeStoredRequestId(requestId); + return name; + } + const txn = await this.storage.readTxn([this.storage.storeNames.session]); + const storedIds = await txn.session.get(STORAGE_KEY); + const name = storedIds?.[requestId]; + if (name) { + await this.removeStoredRequestId(requestId); + return name; + } + } + + async removeStoredRequestId(requestId: string): Promise { + const txn = await this.storage.readWriteTxn([this.storage.storeNames.session]); + const storedIds = await txn.session.get(STORAGE_KEY); + if (storedIds) { + delete storedIds[requestId]; + txn.session.set(STORAGE_KEY, storedIds); + } + } + async getLocallyStoredSecret(name: string): Promise { const txn = await this.storage.readTxn([ this.storage.storeNames.sharedSecrets, @@ -197,12 +233,28 @@ export class SecretSharing { const request_id = makeTxnId(); const promise = this.trackSecretRequest(request_id, name); await this.sendRequestForSecret(name, request_id, _log); + await this.writeRequestIdToStorage(request_id, name); const request = new SecretRequest(promise); return request; }); } - private async writeToStorage(name:string, secret: any) { + /** + * We will store the request-id of every secret request that we send. + * If a device responds to our secret request when we're offline and we receive + * it via sync when we come online at some later time, we can use this persisted + * request-id to determine if we should accept the secret. + */ + private async writeRequestIdToStorage(requestId: string, name: string): Promise { + const txn = await this.storage.readWriteTxn([ + this.storage.storeNames.session, + ]); + const txnIds = await txn.session.get(STORAGE_KEY) ?? {}; + txnIds[requestId] = name; + txn.session.set(STORAGE_KEY, txnIds) + } + + private async writeSecretToStorage(name:string, secret: any): Promise { const encrypted = await this.aesEncryption.encrypt(secret); const txn = await this.storage.readWriteTxn([StoreNames.sharedSecrets]); txn.sharedSecrets.set(name, { encrypted }); From 00479df71e5cf0993712963b805be632c4178e14 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 8 Jun 2023 21:01:11 +0530 Subject: [PATCH 18/44] Check verification for response as well --- src/matrix/e2ee/DecryptionResult.ts | 2 +- src/matrix/ssss/SecretSharing.ts | 16 ++++++++++++++++ src/matrix/verification/CrossSigning.ts | 8 ++++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/matrix/e2ee/DecryptionResult.ts b/src/matrix/e2ee/DecryptionResult.ts index 146a1ad3..1360811c 100644 --- a/src/matrix/e2ee/DecryptionResult.ts +++ b/src/matrix/e2ee/DecryptionResult.ts @@ -36,7 +36,7 @@ type DecryptedEvent = { } export class DecryptionResult { - private device?: DeviceKey; + public device?: DeviceKey; constructor( public readonly event: DecryptedEvent, diff --git a/src/matrix/ssss/SecretSharing.ts b/src/matrix/ssss/SecretSharing.ts index 5f3c2a5e..6be978bb 100644 --- a/src/matrix/ssss/SecretSharing.ts +++ b/src/matrix/ssss/SecretSharing.ts @@ -188,8 +188,23 @@ export class SecretSharing { * @param decryptionResult Encrypted to-device event that contains the secret */ async shouldAcceptSecret(decryptionResult: DecryptionResult): Promise { + // 1. Check if we can trust this response + const crossSigning = this.crossSigning.get(); + if (!crossSigning) { + return; + } + const device = decryptionResult.device; + if (!device) { + return; + } + if (!await crossSigning.isOurUserDeviceTrusted(device)) { + // We don't want to accept secrets from an untrusted device + console.log("received secret, but ignoring because not verified"); + return; + } const content = decryptionResult.event.content!; const requestId = content.request_id; + // 2. Check if this request is in waitMap const obj = this.waitMap.get(requestId); if (obj) { const { name, deferred } = obj; @@ -198,6 +213,7 @@ export class SecretSharing { await this.removeStoredRequestId(requestId); return name; } + // 3. Check if we've persisted the request to storage const txn = await this.storage.readTxn([this.storage.storeNames.session]); const storedIds = await txn.session.get(STORAGE_KEY); const name = storedIds?.[requestId]; diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index a1eb160b..34046e70 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -303,13 +303,13 @@ export class CrossSigning { }); } - async isOurUserDeviceTrusted(device: DeviceKey, log: ILogItem): Promise { - return await log.wrap("CrossSigning.getDeviceTrust", async () => { - const ourSSK = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.SelfSigning, this.hsApi, log); + async isOurUserDeviceTrusted(device: DeviceKey, log?: ILogItem): Promise { + return await this.platform.logger.wrapOrRun(log, "CrossSigning.getDeviceTrust", async (_log) => { + const ourSSK = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.SelfSigning, this.hsApi, _log); if (!ourSSK) { return false; } - const verification = this.hasValidSignatureFrom(device, ourSSK, log); + const verification = this.hasValidSignatureFrom(device, ourSSK, _log); if (verification === SignatureVerification.Valid) { return true; } From 4f42e1b34cd25aa121b5fb23fa49ce31b6596a1b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 9 Jun 2023 13:33:36 +0530 Subject: [PATCH 19/44] Show a view when keys are missing --- .../DeviceVerificationViewModel.ts | 50 ++++++++++++++++--- .../stages/MissingKeysViewModel.ts | 28 +++++++++++ src/matrix/Session.js | 4 ++ .../web/ui/css/themes/element/theme.css | 3 ++ .../verification/DeviceVerificationView.ts | 2 + .../verification/stages/MissingKeysView.ts | 47 +++++++++++++++++ 6 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 src/domain/session/verification/stages/MissingKeysViewModel.ts create mode 100644 src/platform/web/ui/session/verification/stages/MissingKeysView.ts diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 21f2badf..7ff9e1bc 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -22,16 +22,24 @@ import {VerificationCancelledViewModel} from "./stages/VerificationCancelledView import {SelectMethodViewModel} from "./stages/SelectMethodViewModel"; import {VerifyEmojisViewModel} from "./stages/VerifyEmojisViewModel"; import {VerificationCompleteViewModel} from "./stages/VerificationCompleteViewModel"; +import {MissingKeysViewModel} from "./stages/MissingKeysViewModel"; import type {Session} from "../../../matrix/Session.js"; import type {SASVerification} from "../../../matrix/verification/SAS/SASVerification"; import type {SASRequest} from "../../../matrix/verification/SAS/SASRequest"; import type {CrossSigning} from "../../../matrix/verification/CrossSigning"; +import type {ILogItem} from "../../../logging/types"; type Options = BaseOptions & { session: Session; request: SASRequest; }; +const neededSecrets = [ + "m.cross_signing.master", + "m.cross_signing.self_signing", + "m.cross_signing.user_signing", +]; + export class DeviceVerificationViewModel extends ErrorReportViewModel { private sas: SASVerification; private _currentStageViewModel: any; @@ -53,14 +61,47 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { const crossSigning = this.getOption("session").crossSigning.get() as CrossSigning; this.sas = crossSigning.startVerification(requestOrUserId, log)!; + if (!await this.performPreVerificationChecks(crossSigning, log)) { + return; + } this.addEventListeners(); if (typeof requestOrUserId === "string") { this.updateCurrentStageViewModel(new WaitingForOtherUserViewModel(this.childOptions({ sas: this.sas }))); } - this._needsToRequestSecret = !await crossSigning.areWeVerified(log); return this.sas.start(); }); } + + private async performPreVerificationChecks(crossSigning: CrossSigning, log: ILogItem): Promise { + return await log.wrap("DeviceVerificationViewModel.performPreVerificationChecks", async (_log) => { + const areWeVerified = await crossSigning.areWeVerified(log); + // If we're not verified, we'll need to ask the other device for secrets later + this._needsToRequestSecret = !areWeVerified; + if (this._needsToRequestSecret) { + return true; + } + /** + * It's possible that we are verified but don't have access + * to the private cross-signing keys. In this case we really + * can't verify the other device because we need these keys + * to sign their device. + * + * If this happens, we'll simply ask the user to enable key-backup + * (and secret storage) and try again later. + */ + const session = this.getOption("session"); + const promises = neededSecrets.map(s => session.secretFetcher.getSecret(s)); + const secrets = await Promise.all(promises) + for (const secret of secrets) { + if (!secret) { + // We really can't proceed! + this.updateCurrentStageViewModel(new MissingKeysViewModel(this.childOptions({}))); + return false; + } + } + return true; + }); + } private addEventListeners() { this.track(this.sas.disposableOn("SelectVerificationStage", (stage) => { @@ -91,11 +132,6 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { if (this._needsToRequestSecret) { - const neededSecrets = [ - "m.cross_signing.master", - "m.cross_signing.self_signing", - "m.cross_signing.user_signing", - ]; const secretSharing = this.getOption("session").secretSharing; const promises = neededSecrets.map((secret) => secretSharing.requestSecret(secret, log)); await Promise.all(promises); @@ -110,7 +146,7 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel {/** ignore */}); } super.dispose(); diff --git a/src/domain/session/verification/stages/MissingKeysViewModel.ts b/src/domain/session/verification/stages/MissingKeysViewModel.ts new file mode 100644 index 00000000..3a542e9a --- /dev/null +++ b/src/domain/session/verification/stages/MissingKeysViewModel.ts @@ -0,0 +1,28 @@ +/* +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 {ViewModel, Options} from "../../../ViewModel"; +import {SegmentType} from "../../../navigation/index"; + +export class MissingKeysViewModel extends ViewModel { + gotoSettings() { + this.navigation.push("settings", true); + } + + get kind(): string { + return "keys-missing"; + } +} diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 997b96af..01ae8db8 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -401,6 +401,10 @@ export class Session { return this._secretSharing; } + get secretFetcher() { + return this._secretFetcher; + } + get hasIdentity() { return !!this._e2eeAccount; } diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 5f13bb7c..3c4c9b61 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1411,6 +1411,7 @@ button.RoomDetailsView_row::after { .VerificationCompleteView__heading, .VerifyEmojisView__heading, .SelectMethodView__heading, +.MissingKeysView__heading, .WaitingForOtherUserView__heading { display: flex; align-items: center; @@ -1432,6 +1433,7 @@ button.RoomDetailsView_row::after { .SelectMethodView__title, .WaitingForOtherUserView__title, .VerificationCancelledView__description, +.MissingKeysView__description, .VerificationCompleteView__description, .VerifyEmojisView__description, .SelectMethodView__description, @@ -1441,6 +1443,7 @@ button.RoomDetailsView_row::after { } .VerificationCancelledView__actions, +.MissingKeysView__actions, .SelectMethodView__actions, .VerifyEmojisView__actions, .WaitingForOtherUserView__actions { diff --git a/src/platform/web/ui/session/verification/DeviceVerificationView.ts b/src/platform/web/ui/session/verification/DeviceVerificationView.ts index d107ca13..50911213 100644 --- a/src/platform/web/ui/session/verification/DeviceVerificationView.ts +++ b/src/platform/web/ui/session/verification/DeviceVerificationView.ts @@ -21,6 +21,7 @@ import {VerificationCancelledView} from "./stages/VerificationCancelledView"; import {SelectMethodView} from "./stages/SelectMethodView"; import {VerifyEmojisView} from "./stages/VerifyEmojisView"; import {VerificationCompleteView} from "./stages/VerificationCompleteView"; +import {MissingKeysView} from "./stages/MissingKeysView"; export class DeviceVerificationView extends TemplateView { render(t: Builder) { @@ -37,6 +38,7 @@ export class DeviceVerificationView extends TemplateView { + render(t: Builder, vm: MissingKeysViewModel) { + return t.div( + { + className: "MissingKeysView", + }, + [ + t.h2( + { className: "MissingKeysView__heading" }, + vm.i18n`Verification is currently not possible!` + ), + t.p( + { className: "MissingKeysView__description" }, + vm.i18n`Some keys needed for verification is missing. You can fix this by enabling key backup in settings.` + ), + t.div({ className: "MissingKeysView__actions" }, [ + t.button({ + className: { + "button-action": true, + "primary": true, + }, + onclick: () => vm.gotoSettings(), + }, "Open Settings") + ]), + ] + ); + } +} From fbc31e6fbe8f79bc2199447c2c7897683760f7f2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 11 Jun 2023 20:32:39 +0530 Subject: [PATCH 20/44] Wait for cross-signing to become available --- .../session/verification/DeviceVerificationViewModel.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index bb81fa9c..2f051745 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -64,8 +64,9 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { - const crossSigning = this.getOption("session").crossSigning.get(); + await this.logAndCatch("DeviceVerificationViewModel.startVerification", async (log) => { + const crossSigningObservable = this.getOption("session").crossSigning; + const crossSigning = await crossSigningObservable.waitFor(c => !!c).promise; this.sas = crossSigning.startVerification(requestOrUserId, room, log); if (!this.sas) { throw new Error("CrossSigning.startVerification did not return a sas object!"); From 696b4a243f8015a8bb1fcd694fd51c61abdd44c7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 11 Jun 2023 20:35:39 +0530 Subject: [PATCH 21/44] Don't return if MSK is not trusted --- src/matrix/Session.js | 7 +------ src/matrix/verification/CrossSigning.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index a3b7d06c..9db0bd7f 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -614,13 +614,8 @@ export class Session { e2eeAccount: this._e2eeAccount, deviceId: this.deviceId, }); + await crossSigning.load(log); this._crossSigning.set(crossSigning); - // if (await crossSigning.load(log)) { - // this._crossSigning.set(crossSigning); - // } - // else { - // crossSigning.dispose(); - // } }); } await this._keyBackup.get()?.start(log); diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 47677e94..9987db31 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -277,10 +277,17 @@ export class CrossSigning { async signDevice(verification: IVerificationMethod, log: ILogItem): Promise { return log.wrap("CrossSigning.signDevice", async log => { if (!this._isMasterKeyTrusted) { + /** + * If we're the unverified device that is participating in + * the verification process, it is expected that we do not + * have access to the private part of MSK and thus + * cannot determine if the MSK is trusted. In this case, we + * do not need to sign anything because the other (verified) + * device will sign our device key with the SSK. + */ log.set("mskNotTrusted", true); - return; } - const shouldSign = await verification.verify(); + const shouldSign = await verification.verify() && this._isMasterKeyTrusted; log.set("shouldSign", shouldSign); if (!shouldSign) { return; From f8f20852700661b49ae4a854e291ca38199e2bde Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jun 2023 13:26:11 +0530 Subject: [PATCH 22/44] Use type import here --- src/domain/session/verification/stages/MissingKeysViewModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/verification/stages/MissingKeysViewModel.ts b/src/domain/session/verification/stages/MissingKeysViewModel.ts index 3a542e9a..a3740c2a 100644 --- a/src/domain/session/verification/stages/MissingKeysViewModel.ts +++ b/src/domain/session/verification/stages/MissingKeysViewModel.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {ViewModel, Options} from "../../../ViewModel"; -import {SegmentType} from "../../../navigation/index"; +import type {SegmentType} from "../../../navigation/index"; export class MissingKeysViewModel extends ViewModel { gotoSettings() { From c0e29fe21b57530aa3d6b96ff2139e82307965c1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jun 2023 13:26:39 +0530 Subject: [PATCH 23/44] Add return type --- src/matrix/e2ee/DeviceTracker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 82f9ce02..47902dd5 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -527,7 +527,7 @@ export class DeviceTracker { } /** Gets a single device */ - async deviceForId(userId: string, deviceId: string, hsApi: HomeServerApi, log: ILogItem) { + async deviceForId(userId: string, deviceId: string, hsApi: HomeServerApi, log: ILogItem): Promise { /** * 1. If the device keys are outdated, we will fetch all the keys and update them. */ @@ -595,7 +595,7 @@ export class DeviceTracker { return deviceKey; } - async deviceForCurveKey(userId: string, key: string, hsApi: HomeServerApi, log: ILogItem) { + async deviceForCurveKey(userId: string, key: string, hsApi: HomeServerApi, log: ILogItem): Promise { const txn = await this._storage.readTxn([ this._storage.storeNames.deviceKeys, this._storage.storeNames.userIdentities, From d5d3e766599522923a18253c29de831e3e9518f9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jun 2023 13:26:50 +0530 Subject: [PATCH 24/44] Change comment style --- src/matrix/ssss/SecretFetcher.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/matrix/ssss/SecretFetcher.ts b/src/matrix/ssss/SecretFetcher.ts index 46caf705..2ee425a6 100644 --- a/src/matrix/ssss/SecretFetcher.ts +++ b/src/matrix/ssss/SecretFetcher.ts @@ -26,13 +26,14 @@ export class SecretFetcher { public secretSharing: SecretSharing; async getSecret(name: string): Promise { - ; return await this.secretStorage?.readSecret(name) ?? await this.secretSharing?.getLocallyStoredSecret(name); - // note that we don't ask another device for secret here - // that should be done explicitly since it can take arbitrary - // amounts of time to be fulfilled as the other devices may - // be offline etc... + /** + * Note that we don't ask another device for secret here; + * that should be done explicitly since it can take arbitrary + * amounts of time to be fulfilled as the other devices may + * be offline etc... + */ } setSecretStorage(storage: SecretStorage) { From 9f46a127e048ee49a927aa8206e2c587a362265b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jun 2023 13:27:57 +0530 Subject: [PATCH 25/44] Improve comment --- src/matrix/ssss/SecretFetcher.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/matrix/ssss/SecretFetcher.ts b/src/matrix/ssss/SecretFetcher.ts index 2ee425a6..498de13e 100644 --- a/src/matrix/ssss/SecretFetcher.ts +++ b/src/matrix/ssss/SecretFetcher.ts @@ -19,21 +19,21 @@ import type {SecretSharing} from "./SecretSharing"; /** * This is a wrapper around SecretStorage and SecretSharing so that - * you don't need to always check both sources for something. + * you don't need to check both sources for a secret. */ export class SecretFetcher { public secretStorage: SecretStorage; public secretSharing: SecretSharing; async getSecret(name: string): Promise { - return await this.secretStorage?.readSecret(name) ?? - await this.secretSharing?.getLocallyStoredSecret(name); /** * Note that we don't ask another device for secret here; * that should be done explicitly since it can take arbitrary * amounts of time to be fulfilled as the other devices may * be offline etc... */ + return await this.secretStorage?.readSecret(name) ?? + await this.secretSharing?.getLocallyStoredSecret(name); } setSecretStorage(storage: SecretStorage) { @@ -42,6 +42,11 @@ export class SecretFetcher { setSecretSharing(sharing: SecretSharing) { this.secretSharing = sharing; + /** + * SecretSharing also needs to respond to secret requests + * from other devices, so it needs the secret fetcher as + * well + */ this.secretSharing.setSecretFetcher(this); } } From d8987ee54b647d5892554594c11b99fdcb3bf81c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jun 2023 13:38:30 +0530 Subject: [PATCH 26/44] Crypto does not need to be a member --- src/matrix/ssss/SecretSharing.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/matrix/ssss/SecretSharing.ts b/src/matrix/ssss/SecretSharing.ts index 6be978bb..cedd8d3a 100644 --- a/src/matrix/ssss/SecretSharing.ts +++ b/src/matrix/ssss/SecretSharing.ts @@ -60,7 +60,6 @@ export class SecretSharing { private readonly ourUserId: string; private readonly olmEncryption: OlmEncryption; private readonly waitMap: Map, name: string }> = new Map(); - private readonly crypto: Crypto; private readonly encoding: Encoding; private readonly aesEncryption: AESEncryption; private readonly crossSigning: ObservableValue; @@ -74,11 +73,10 @@ export class SecretSharing { this.deviceTracker = options.deviceTracker; this.ourUserId = options.ourUserId; this.olmEncryption = options.olmEncryption; - this.crypto = options.crypto; this.encoding = options.encoding; this.crossSigning = options.crossSigning; this.logger = options.logger; - this.aesEncryption = new AESEncryption(this.storage, this.crypto, this.encoding); + this.aesEncryption = new AESEncryption(this.storage, options.crypto, this.encoding); (window as any).foo = this; this.init(); } From ba0b0300b427c19cc720f2c19ee7bb8d29fdf777 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jun 2023 15:37:47 +0530 Subject: [PATCH 27/44] await for secret sharing to load --- src/matrix/Session.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 9db0bd7f..c83e4946 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -172,7 +172,7 @@ export class Session { } // called once this._e2eeAccount is assigned - _setupEncryption() { + async _setupEncryption() { // TODO: this should all go in a wrapper in e2ee/ that is bootstrapped by passing in the account // and can create RoomEncryption objects and handle encrypted to_device messages and device list changes. const senderKeyLock = new LockMap(); @@ -218,6 +218,7 @@ export class Session { crossSigning: this._crossSigning, logger: this._platform.logger, }); + await this._secretSharing.load(); this._secretFetcher.setSecretSharing(this._secretSharing); } @@ -419,7 +420,7 @@ export class Session { if (!this._e2eeAccount) { this._e2eeAccount = await this._createNewAccount(this._sessionInfo.deviceId, this._storage); log.set("keys", this._e2eeAccount.identityKeys); - this._setupEncryption(); + await this._setupEncryption(); } await this._e2eeAccount.generateOTKsIfNeeded(this._storage, log); await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log)); From 2922c1691ab4499af6a07034323cc78d95c9d710 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jun 2023 15:38:33 +0530 Subject: [PATCH 28/44] Add return type to methods --- src/matrix/ssss/SecretSharing.ts | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/matrix/ssss/SecretSharing.ts b/src/matrix/ssss/SecretSharing.ts index cedd8d3a..43c2057d 100644 --- a/src/matrix/ssss/SecretSharing.ts +++ b/src/matrix/ssss/SecretSharing.ts @@ -77,16 +77,14 @@ export class SecretSharing { this.crossSigning = options.crossSigning; this.logger = options.logger; this.aesEncryption = new AESEncryption(this.storage, options.crypto, this.encoding); - (window as any).foo = this; - this.init(); } - private async init() { + async load(): Promise { this.deviceMessageHandler.on("message", async ({ encrypted }) => { const type: EVENT_TYPE = encrypted?.event.type; switch (type) { case EVENT_TYPE.REQUEST: { - this._respondToRequest(encrypted); + await this._respondToRequest(encrypted); break; } case EVENT_TYPE.SEND: { @@ -102,9 +100,9 @@ export class SecretSharing { await this.aesEncryption.load(); } - private async _respondToRequest(request) { + private async _respondToRequest(request): Promise { await this.logger.run("SharedSecret.respondToRequest", async (log) => { - if (!this.shouldRespondToRequest(request, log)) { + if (!await this.shouldRespondToRequest(request, log)) { return; } const requestContent = request.event.content; @@ -127,9 +125,7 @@ export class SecretSharing { } const messages = await log.wrap("olm encrypt", log => this.olmEncryption.encrypt( EVENT_TYPE.SEND, content, [device], this.hsApi, log)); - console.log("messages", messages); const payload = formatToDeviceMessagesPayload(messages); - console.log("payload", payload); await this.hsApi.sendToDevice("m.room.encrypted", payload, makeTxnId(), {log}).response(); }); } @@ -162,7 +158,7 @@ export class SecretSharing { // 2. Validate message format // 3. Check if this is a cancellation return false; - } + } // 3. Check that the device is verified const deviceId = content.requesting_device_id; @@ -176,8 +172,7 @@ export class SecretSharing { return false; } return true; - }) - + }); } /** @@ -185,7 +180,7 @@ export class SecretSharing { * Returns undefined otherwise. * @param decryptionResult Encrypted to-device event that contains the secret */ - async shouldAcceptSecret(decryptionResult: DecryptionResult): Promise { + private async shouldAcceptSecret(decryptionResult: DecryptionResult): Promise { // 1. Check if we can trust this response const crossSigning = this.crossSigning.get(); if (!crossSigning) { @@ -221,7 +216,7 @@ export class SecretSharing { } } - async removeStoredRequestId(requestId: string): Promise { + private async removeStoredRequestId(requestId: string): Promise { const txn = await this.storage.readWriteTxn([this.storage.storeNames.session]); const storedIds = await txn.session.get(STORAGE_KEY); if (storedIds) { @@ -280,7 +275,7 @@ export class SecretSharing { return deferred.promise; } - private async sendRequestForSecret(name: string, request_id: string, log: ILogItem) { + private async sendRequestForSecret(name: string, request_id: string, log: ILogItem): Promise { const content = { action: "request", name, @@ -291,9 +286,7 @@ export class SecretSharing { devices = devices.filter(d => d.device_id !== this.deviceTracker.ownDeviceId); const messages = await log.wrap("olm encrypt", log => this.olmEncryption.encrypt( EVENT_TYPE.REQUEST, content, devices, this.hsApi, log)); - console.log("messages", messages); const payload = formatToDeviceMessagesPayload(messages); - console.log("payload", payload); await this.hsApi.sendToDevice("m.room.encrypted", payload, makeTxnId(), {log}).response(); } @@ -333,7 +326,7 @@ class AESEncryption { constructor(private storage: Storage, private crypto: Crypto, private encoding: Encoding) { }; - async load() { + async load(): Promise { const storageKey = `${SESSION_E2EE_KEY_PREFIX}localAESKey`; // 1. Check if we're already storing the AES key const txn = await this.storage.readTxn([StoreNames.session]); @@ -343,8 +336,8 @@ class AESEncryption { if (!key) { /** * Element creates the key as "non-extractable", meaning that it cannot - * be exported through the crypto API. But since it is going - * to end up in local-storage anyway, I don't see a reason to do that. + * be exported through the crypto DOM API. But since it's going + * to end up in indexeddb anyway, it really doesn't matter. */ key = await this.crypto.aes.generateKey("jwk"); iv = await this.crypto.aes.generateIV(); From 3308a7e3746312d76a2695a8224ae1386e976003 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jun 2023 15:39:06 +0530 Subject: [PATCH 29/44] Remove newline --- src/matrix/ssss/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/matrix/ssss/index.ts b/src/matrix/ssss/index.ts index c782a6ef..7977991e 100644 --- a/src/matrix/ssss/index.ts +++ b/src/matrix/ssss/index.ts @@ -29,7 +29,6 @@ export {SecretFetcher} from "./SecretFetcher"; export {SecretSharing} from "./SecretSharing"; export {SecretStorage} from "./SecretStorage"; - type Olm = typeof OlmNamespace; const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`; From 17471868437da1fc79d9c1d1e390a931789214b1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jun 2023 16:16:36 +0530 Subject: [PATCH 30/44] Fix typos and comments --- src/matrix/DeviceMessageHandler.js | 1 - src/matrix/verification/CrossSigning.ts | 2 +- .../web/ui/session/verification/stages/MissingKeysView.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/matrix/DeviceMessageHandler.js b/src/matrix/DeviceMessageHandler.js index 68da1661..97423374 100644 --- a/src/matrix/DeviceMessageHandler.js +++ b/src/matrix/DeviceMessageHandler.js @@ -77,7 +77,6 @@ export class DeviceMessageHandler extends EventEmitter{ } async afterSyncCompleted(decryptionResults, deviceTracker, hsApi, log) { - // this._emitEncryptedEvents(decryptionResults); await log.wrap("Verifying fingerprint of encrypted toDevice messages", async (log) => { for (const result of decryptionResults) { const sender = result.event.sender; diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 9987db31..77238951 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -349,7 +349,7 @@ export class CrossSigning { } async isOurUserDeviceTrusted(device: DeviceKey, log?: ILogItem): Promise { - return await this.platform.logger.wrapOrRun(log, "CrossSigning.getDeviceTrust", async (_log) => { + return await this.platform.logger.wrapOrRun(log, "CrossSigning.isOurUserDeviceTrusted", async (_log) => { const ourSSK = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.SelfSigning, this.hsApi, _log); if (!ourSSK) { return false; diff --git a/src/platform/web/ui/session/verification/stages/MissingKeysView.ts b/src/platform/web/ui/session/verification/stages/MissingKeysView.ts index 2bf5d609..4ee7fc64 100644 --- a/src/platform/web/ui/session/verification/stages/MissingKeysView.ts +++ b/src/platform/web/ui/session/verification/stages/MissingKeysView.ts @@ -30,7 +30,7 @@ export class MissingKeysView extends TemplateView { ), t.p( { className: "MissingKeysView__description" }, - vm.i18n`Some keys needed for verification is missing. You can fix this by enabling key backup in settings.` + vm.i18n`Some keys needed for verification are missing. You can fix this by enabling key backup in settings.` ), t.div({ className: "MissingKeysView__actions" }, [ t.button({ From 816bdbba7035ad262821733e02d3abfac9973af2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jun 2023 16:18:09 +0530 Subject: [PATCH 31/44] Create cross-signing in load --- src/matrix/Session.js | 45 +++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index c83e4946..deed3081 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -113,6 +113,7 @@ export class Session { this.needsKeyBackup = new ObservableValue(false); this._secretFetcher = new SecretFetcher(); this._secretSharing = null; + this._secretStorage = null; } get fingerprintKey() { @@ -274,11 +275,6 @@ export class Session { this._keyBackup.get().dispose(); this._keyBackup.set(undefined); } - // const crossSigning = this._crossSigning.get(); - // if (crossSigning) { - // crossSigning.dispose(); - // this._crossSigning.set(undefined); - // } const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); if (await this._tryLoadSecretStorage(key, log)) { // only after having read a secret, write the key @@ -550,6 +546,25 @@ export class Session { await this._tryLoadSecretStorage(ssssKey, log); } } + if (this._features.crossSigning) { + this._platform.logger.run("enable cross-signing", async log => { + const crossSigning = new CrossSigning({ + storage: this._storage, + secretFetcher: this._secretFetcher, + platform: this._platform, + olm: this._olm, + olmUtil: this._olmUtil, + deviceTracker: this._deviceTracker, + deviceMessageHandler: this._deviceMessageHandler, + hsApi: this._hsApi, + ownUserId: this.userId, + e2eeAccount: this._e2eeAccount, + deviceId: this.deviceId, + }); + await crossSigning.load(log); + this._crossSigning.set(crossSigning); + }); + } } dispose() { @@ -599,26 +614,6 @@ export class Session { }); } - if (this._features.crossSigning) { - this._platform.logger.run("enable cross-signing", async log => { - const crossSigning = new CrossSigning({ - storage: this._storage, - // secretStorage: this._secretStorage, - secretFetcher: this._secretFetcher, - platform: this._platform, - olm: this._olm, - olmUtil: this._olmUtil, - deviceTracker: this._deviceTracker, - deviceMessageHandler: this._deviceMessageHandler, - hsApi: this._hsApi, - ownUserId: this.userId, - e2eeAccount: this._e2eeAccount, - deviceId: this.deviceId, - }); - await crossSigning.load(log); - this._crossSigning.set(crossSigning); - }); - } await this._keyBackup.get()?.start(log); await this._crossSigning.get()?.start(log); From c27d1b68bea4bc1df5100e4e85577e2295bc94bf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jun 2023 16:19:29 +0530 Subject: [PATCH 32/44] Remove newlines --- src/matrix/Session.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index deed3081..7323dc61 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -221,7 +221,6 @@ export class Session { }); await this._secretSharing.load(); this._secretFetcher.setSecretSharing(this._secretSharing); - } _createRoomEncryption(room, encryptionParams) { @@ -613,7 +612,6 @@ export class Session { } }); } - await this._keyBackup.get()?.start(log); await this._crossSigning.get()?.start(log); From 1da93493f6c4f428036e6e38edf5b08dac908676 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jun 2023 18:23:34 +0530 Subject: [PATCH 33/44] Prevent verification between unverified devices --- src/matrix/verification/CrossSigning.ts | 86 ++++++++++++++++--------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 77238951..64845c32 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -231,34 +231,60 @@ export class CrossSigning { return this.sasVerificationInProgress; } - private handleSASDeviceMessage({ unencrypted: event }) { - if (!event) { return; } - const txnId = event.content.transaction_id; - /** - * If we receive an event for the current/previously finished - * SAS verification, we should ignore it because the device channel - * object (who also listens for to_device messages) will take care of it (if needed). - */ - const shouldIgnoreEvent = this.sasVerificationInProgress?.channel.id === txnId; - if (shouldIgnoreEvent) { return; } - /** - * 1. If we receive the cancel message, we need to update the requests map. - * 2. If we receive an starting message (viz request/start), we need to create the SASRequest from it. - */ - switch (event.type) { - case VerificationEventType.Cancel: - this.receivedSASVerifications.remove(txnId); - return; - case VerificationEventType.Request: - case VerificationEventType.Start: - this.platform.logger.run("Create SASRequest", () => { - this.receivedSASVerifications.set(txnId, new SASRequest(event)); - }); - return; - default: - // we don't care about this event! - return; + private async handleSASDeviceMessage({ unencrypted: event }) { + if (!event || + (event.type !== VerificationEventType.Request && event.type !== VerificationEventType.Start) + ) { + return; } + await this.platform.logger.run("CrossSigning.handleSASDeviceMessage", async log => { + const txnId = event.content.transaction_id; + const fromDevice = event.content.from_device; + const fromUser = event.sender; + if (!fromDevice || fromUser !== this.ownUserId) { + /** + * SAS verification may be started with a request or a start message but + * both should contain a from_device. + */ + return; + } + if (!await this.areWeVerified(log)) { + /** + * If we're not verified, then the other device MUST be verified. + * We check this so that verification between two unverified devices + * never happen! + */ + const device = await this.deviceTracker.deviceForId(this.ownUserId, fromDevice, this.hsApi, log); + if (!device || !await this.isOurUserDeviceTrusted(device!, log)) { + return; + } + } + /** + * If we receive an event for the current/previously finished + * SAS verification, we should ignore it because the device channel + * object (who also listens for to_device messages) will take care of it (if needed). + */ + const shouldIgnoreEvent = this.sasVerificationInProgress?.channel.id === txnId; + if (shouldIgnoreEvent) { return; } + /** + * 1. If we receive the cancel message, we need to update the requests map. + * 2. If we receive an starting message (viz request/start), we need to create the SASRequest from it. + */ + switch (event.type) { + case VerificationEventType.Cancel: + this.receivedSASVerifications.remove(txnId); + return; + case VerificationEventType.Request: + case VerificationEventType.Start: + this.platform.logger.run("Create SASRequest", () => { + this.receivedSASVerifications.set(txnId, new SASRequest(event)); + }); + return; + default: + // we don't care about this event! + return; + } + }); } /** returns our own device key signed by our self-signing key. Other signatures will be missing. */ @@ -362,9 +388,9 @@ export class CrossSigning { }); } - areWeVerified(log: ILogItem): Promise { - return log.wrap("CrossSigning.areWeVerified", async () => { - const device = await this.deviceTracker.deviceForId(this.ownUserId, this.deviceId, this.hsApi, log); + areWeVerified(log?: ILogItem): Promise { + return this.platform.logger.wrapOrRun(log, "CrossSigning.areWeVerified", async (_log) => { + const device = await this.deviceTracker.deviceForId(this.ownUserId, this.deviceId, this.hsApi, _log); return this.isOurUserDeviceTrusted(device!, log); }); } From fea5bc9c48398c7b24b5cc6eb6506d48af079bc0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jun 2023 14:07:18 +0530 Subject: [PATCH 34/44] Fix resolveStartMessages not working --- .../SAS/stages/SelectVerificationMethodStage.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index aa2302fb..743c8ba0 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -18,10 +18,11 @@ import {CancelReason, VerificationEventType} from "../channel/types"; import {KEY_AGREEMENT_LIST, HASHES_LIST, MAC_LIST, SAS_LIST} from "./constants"; import {SendAcceptVerificationStage} from "./SendAcceptVerificationStage"; import {SendKeyStage} from "./SendKeyStage"; +import {Deferred} from "../../../../utils/Deferred"; import type {ILogItem} from "../../../../logging/types"; export class SelectVerificationMethodStage extends BaseSASVerificationStage { - private hasSentStartMessage = false; + private hasSentStartMessage?: Promise; private allowSelection = true; public otherDeviceName: string; @@ -36,6 +37,7 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { // We received the start message this.allowSelection = false; if (this.hasSentStartMessage) { + await this.hasSentStartMessage; await this.resolveStartConflict(log); } else { @@ -96,6 +98,8 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { async selectEmojiMethod(log: ILogItem) { if (!this.allowSelection) { return; } + const deferred = new Deferred(); + this.hasSentStartMessage = deferred.promise; const content = { method: "m.sas.v1", from_device: this.ourUserDeviceId, @@ -110,6 +114,6 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { * to the next stage (where we will send the key). */ await this.channel.send(VerificationEventType.Start, content, log); - this.hasSentStartMessage = true; + deferred.resolve(); } } From 69141af4c47e2de665265007c58ab8893a4787f7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jun 2023 15:36:08 +0530 Subject: [PATCH 35/44] Sign own device after enabling key backup --- src/domain/session/settings/KeyBackupViewModel.ts | 3 ++- .../web/ui/session/settings/KeyBackupSettingsView.ts | 8 -------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.ts b/src/domain/session/settings/KeyBackupViewModel.ts index 7648e4de..dc1a15d9 100644 --- a/src/domain/session/settings/KeyBackupViewModel.ts +++ b/src/domain/session/settings/KeyBackupViewModel.ts @@ -151,7 +151,7 @@ export class KeyBackupViewModel extends ViewModel { return !!this._crossSigning; } - async signOwnDevice(): Promise { + private async _signOwnDevice(): Promise { const crossSigning = this._crossSigning; if (crossSigning) { await this.logger.run("KeyBackupViewModel.signOwnDevice", async log => { @@ -208,6 +208,7 @@ export class KeyBackupViewModel extends ViewModel { if (setupDehydratedDevice) { this._dehydratedDeviceId = await this._session.setupDehydratedDevice(key); } + await this._signOwnDevice(); } catch (err) { console.error(err); this._error = err; diff --git a/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts b/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts index 7c3d6491..4b60bfc6 100644 --- a/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts @@ -63,14 +63,6 @@ export class KeyBackupSettingsView extends TemplateView { }), t.if(vm => vm.canSignOwnDevice, t => { return t.div([ - t.button( - { - onClick: disableTargetCallback(async (evt) => { - await vm.signOwnDevice(); - }), - }, - "Sign own device" - ), t.button( { onClick: disableTargetCallback(async () => { From f64cfd67f6751007e6d7b30016203fafcee7a84b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jun 2023 14:27:33 +0530 Subject: [PATCH 36/44] Show missing key view --- .../session/verification/DeviceVerificationViewModel.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 2f051745..da96c869 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -71,7 +71,7 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { + private async performPreVerificationChecks(crossSigning: CrossSigning, requestOrUserId: SASRequest | string, log: ILogItem): Promise { return await log.wrap("DeviceVerificationViewModel.performPreVerificationChecks", async (_log) => { const areWeVerified = await crossSigning.areWeVerified(log); // If we're not verified, we'll need to ask the other device for secrets later - this._needsToRequestSecret = !areWeVerified; + const otherUserId = typeof requestOrUserId === "string" ? requestOrUserId : requestOrUserId.sender; + const isDeviceVerification = otherUserId === this.getOption("session").userId; + this._needsToRequestSecret = isDeviceVerification && !areWeVerified; if (this._needsToRequestSecret) { return true; } From 93f415864565263acd7e37f7e2ee93144592fb50 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jun 2023 14:28:09 +0530 Subject: [PATCH 37/44] Make view messages applicable to users --- .../stages/VerificationCancelledViewModel.ts | 72 ++++++++++++++++++- .../stages/WaitingForOtherUserViewModel.ts | 14 ++++ .../stages/VerificationCancelledView.ts | 35 +-------- .../stages/WaitingForOtherUserView.ts | 4 +- 4 files changed, 89 insertions(+), 36 deletions(-) diff --git a/src/domain/session/verification/stages/VerificationCancelledViewModel.ts b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts index 935c39c3..e732a2b5 100644 --- a/src/domain/session/verification/stages/VerificationCancelledViewModel.ts +++ b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts @@ -16,7 +16,7 @@ limitations under the License. import {Options as BaseOptions} from "../../../ViewModel"; import {DismissibleVerificationViewModel} from "./DismissibleVerificationViewModel"; -import type {CancelReason} from "../../../../matrix/verification/SAS/channel/types"; +import {CancelReason} from "../../../../matrix/verification/SAS/channel/types"; import type {Session} from "../../../../matrix/Session.js"; import type {IChannel} from "../../../../matrix/verification/SAS/channel/IChannel"; import type {SASVerification} from "../../../../matrix/verification/SAS/SASVerification"; @@ -39,4 +39,74 @@ export class VerificationCancelledViewModel extends DismissibleVerificationViewM get kind(): string { return "verification-cancelled"; } + + get title(): string { + if (this.isCancelledByUs) { + return this.i18n`You cancelled the verification!`; + } + if (this.getOption("sas").isCrossSigningAnotherUser) { + return this.i18n`The other user cancelled the verification!`; + } + else { + return this.i18n`The other device cancelled the verification!`; + } + } + + get description(): string { + const descriptionsWhenWeCancelledForDeviceVerification = { + [CancelReason.InvalidMessage]: "Your other device sent an invalid message.", + [CancelReason.KeyMismatch]: "The key could not be verified.", + [CancelReason.TimedOut]: "The verification process timed out.", + [CancelReason.UnexpectedMessage]: "Your other device sent an unexpected message.", + [CancelReason.UnknownMethod]: "Your other device is using an unknown method for verification.", + [CancelReason.UnknownTransaction]: "Your other device sent a message with an unknown transaction id.", + [CancelReason.UserMismatch]: "The expected user did not match the user verified.", + [CancelReason.MismatchedCommitment]: "The hash commitment does not match.", + [CancelReason.MismatchedSAS]: "The emoji/decimal did not match.", + } + const descriptionsWhenTheyCancelledForDeviceVerification = { + [CancelReason.UserCancelled]: "Your other device cancelled the verification!", + [CancelReason.InvalidMessage]: "Invalid message sent to the other device.", + [CancelReason.KeyMismatch]: "The other device could not verify our keys", + [CancelReason.TimedOut]: "The verification process timed out.", + [CancelReason.UnexpectedMessage]: "Unexpected message sent to the other device.", + [CancelReason.UnknownMethod]: "Your other device does not understand the method you chose", + [CancelReason.UnknownTransaction]: "Your other device rejected our message.", + [CancelReason.UserMismatch]: "The expected user did not match the user verified.", + [CancelReason.MismatchedCommitment]: "Your other device was not able to verify the hash commitment", + [CancelReason.MismatchedSAS]: "The emoji/decimal did not match.", + } + const descriptionsWhenWeCancelledForCrossSigning = { + [CancelReason.InvalidMessage]: "The other user sent an invalid message.", + [CancelReason.KeyMismatch]: "The key could not be verified.", + [CancelReason.TimedOut]: "The verification process timed out.", + [CancelReason.UnexpectedMessage]: "The other user sent an unexpected message.", + [CancelReason.UnknownMethod]: "The other user is using an unknown method for verification.", + [CancelReason.UnknownTransaction]: "The other user sent a message with an unknown transaction id.", + [CancelReason.UserMismatch]: "The expected user did not match the user verified.", + [CancelReason.MismatchedCommitment]: "The hash commitment does not match.", + [CancelReason.MismatchedSAS]: "The emoji/decimal did not match.", + } + const descriptionsWhenTheyCancelledForCrossSigning = { + [CancelReason.UserCancelled]: "The other user cancelled the verification!", + [CancelReason.InvalidMessage]: "Invalid message sent to the other user.", + [CancelReason.KeyMismatch]: "The other user could not verify our keys", + [CancelReason.TimedOut]: "The verification process timed out.", + [CancelReason.UnexpectedMessage]: "Unexpected message sent to the other user.", + [CancelReason.UnknownMethod]: "The other user does not understand the method you chose", + [CancelReason.UnknownTransaction]: "The other user rejected our message.", + [CancelReason.UserMismatch]: "The expected user did not match the user verified.", + [CancelReason.MismatchedCommitment]: "The other user was not able to verify the hash commitment", + [CancelReason.MismatchedSAS]: "The emoji/decimal did not match.", + } + let map; + if (this.getOption("sas").isCrossSigningAnotherUser) { + map = this.isCancelledByUs ? descriptionsWhenWeCancelledForCrossSigning : descriptionsWhenTheyCancelledForCrossSigning; + } else { + map = this.isCancelledByUs ? descriptionsWhenWeCancelledForDeviceVerification : descriptionsWhenTheyCancelledForDeviceVerification; + } + const description = map[this.cancelCode] ?? "" + return this.i18n`${description}`; + + } } diff --git a/src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts b/src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts index ca68c941..512c8edd 100644 --- a/src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts +++ b/src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts @@ -27,6 +27,20 @@ export class WaitingForOtherUserViewModel extends ViewModel { render(t: Builder, vm: VerificationCancelledViewModel) { - const headerTextStart = vm.isCancelledByUs ? "You" : "The other device"; - return t.div( { className: "VerificationCancelledView", @@ -29,11 +26,11 @@ export class VerificationCancelledView extends TemplateView Date: Mon, 19 Jun 2023 13:55:17 +0530 Subject: [PATCH 38/44] Improve layout on mobile --- src/platform/web/ui/css/layout.css | 8 ++++++++ src/platform/web/ui/css/themes/element/theme.css | 9 +++++---- .../web/ui/session/room/timeline/VerificationTileView.ts | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/platform/web/ui/css/layout.css b/src/platform/web/ui/css/layout.css index a0f42b01..f7fbe732 100644 --- a/src/platform/web/ui/css/layout.css +++ b/src/platform/web/ui/css/layout.css @@ -101,6 +101,14 @@ the layout viewport up without resizing it when the keyboard shows */ .middle .close-middle { display: block !important; } /* hide grid button */ .LeftPanel .grid { display: none !important; } + + .VerificationReadyTileView { + flex-direction: column; + } + + .VerificationTileView__actions { + justify-content: center; + } } .LeftPanel { diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index efd1bc08..728afcc4 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1535,6 +1535,7 @@ button.RoomDetailsView_row::after { font-size: 1.4rem; color: var(--text-color); gap: 4px; + text-align: center; } .VerificationInProgressTileView, @@ -1542,7 +1543,7 @@ button.RoomDetailsView_row::after { .VerificationCancelledTileView, .VerificationReadyTileView { background: var(--background-color-primary--darker-5); - padding: 12px; + padding: 8px; box-sizing: border-box; border-radius: 8px; } @@ -1550,18 +1551,18 @@ button.RoomDetailsView_row::after { .VerificationTileView { display: flex; justify-content: center; - padding: 16px; + padding: 5px 10%; box-sizing: border-box; } .VerificationInProgressTileView .VerificationTileView__shield, .VerificationReadyTileView .VerificationTileView__shield { - background: url("./icons/e2ee-normal.svg?primary=background-color-secondary--darker-40"); + background: url("./icons/e2ee-normal.svg?primary=background-color-secondary--darker-40") no-repeat; } .VerificationCompletedTileView .VerificationTileView__shield { - background: url("./icons/e2ee-normal.svg?primary=accent-color"); + background: url("./icons/e2ee-normal.svg?primary=accent-color") no-repeat; } .VerificationTileView__shield { diff --git a/src/platform/web/ui/session/room/timeline/VerificationTileView.ts b/src/platform/web/ui/session/room/timeline/VerificationTileView.ts index 56e50211..7138de55 100644 --- a/src/platform/web/ui/session/room/timeline/VerificationTileView.ts +++ b/src/platform/web/ui/session/room/timeline/VerificationTileView.ts @@ -52,8 +52,8 @@ export class VerificationTileView extends TemplateView { class VerificationReadyTileView extends TemplateView { render(t: Builder, vm: VerificationTile) { return t.div({ className: "VerificationReadyTileView" }, [ + t.div({ className: "VerificationTileView__shield" }), t.div({ className: "VerificationTileView__description" }, [ - t.div({ className: "VerificationTileView__shield" }), t.div(vm.description) ]), t.div({ className: "VerificationTileView__actions" }, [ From eeceb9e6a0d9ba727e10263a48a440295ca2c1a5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 19 Jun 2023 13:58:38 +0530 Subject: [PATCH 39/44] VerifyMSK after receiving secret --- .../session/verification/DeviceVerificationViewModel.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index da96c869..866b47c4 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -150,8 +150,12 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { if (this._needsToRequestSecret) { const secretSharing = this.getOption("session").secretSharing; - const promises = neededSecrets.map((secret) => secretSharing.requestSecret(secret, log)); - await Promise.all(promises); + const requestPromises = neededSecrets.map((secret) => secretSharing.requestSecret(secret, log)); + const secretRequests = await Promise.all(requestPromises); + const receivedSecretPromises = secretRequests.map(r => r.waitForResponse()); + await Promise.all(receivedSecretPromises); + const crossSigning = this.getOption("session").crossSigning.get(); + crossSigning.start(log); } }); } From ae7f13c7e3b64141dbe601f206430c48250a9b89 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 19 Jun 2023 14:21:23 +0530 Subject: [PATCH 40/44] Don't add middile class if in room --- .../session/verification/DeviceVerificationViewModel.ts | 4 ++++ .../web/ui/session/verification/DeviceVerificationView.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 866b47c4..cbb0063d 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -180,4 +180,8 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { - render(t: Builder) { + render(t: Builder, vm: DeviceVerificationViewModel) { return t.div({ className: { - "middle": true, + "middle": !vm.isHappeningInRoom, "DeviceVerificationView": true, } }, [ From 071aa2c2a34c3f1c11b36d3598b6ed6725fca647 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 20 Jun 2023 14:47:42 +0530 Subject: [PATCH 41/44] Fix more bugs 1. Create cross-signing from createIdentity as well 2. Don't try sending anything if this.id is undefined --- .../verification/DeviceVerificationViewModel.ts | 2 +- src/matrix/Session.js | 7 +++++++ src/matrix/verification/SAS/channel/RoomChannel.ts | 11 ++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index cbb0063d..884119cb 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -168,7 +168,7 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel {/** ignore */}); + this.sas.abort().catch((e) => { console.error(e); }); } super.dispose(); } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 7323dc61..94783509 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -419,6 +419,7 @@ export class Session { } await this._e2eeAccount.generateOTKsIfNeeded(this._storage, log); await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log)); + await this._createCrossSigning(); } } @@ -545,6 +546,12 @@ export class Session { await this._tryLoadSecretStorage(ssssKey, log); } } + if (this._e2eeAccount) { + await this._createCrossSigning(); + } + } + + async _createCrossSigning() { if (this._features.crossSigning) { this._platform.logger.run("enable cross-signing", async log => { const crossSigning = new CrossSigning({ diff --git a/src/matrix/verification/SAS/channel/RoomChannel.ts b/src/matrix/verification/SAS/channel/RoomChannel.ts index c22a8cb1..5df15805 100644 --- a/src/matrix/verification/SAS/channel/RoomChannel.ts +++ b/src/matrix/verification/SAS/channel/RoomChannel.ts @@ -103,7 +103,7 @@ export class RoomChannel extends Disposables implements IChannel { } async send(eventType: VerificationEventType, content: any, log: ILogItem): Promise { - await log.wrap("RoomChannel.send", async () => { + await log.wrap("RoomChannel.send", async (_log) => { if (this.isCancelled) { throw new VerificationCancelledError(); } @@ -112,6 +112,15 @@ export class RoomChannel extends Disposables implements IChannel { await this.handleRequestEventSpecially(eventType, content, log); return; } + if (!this.id) { + /** + * This might happen if the user cancelled the verification from the UI, + * but no verification messages were yet sent (maybe because the keys are + * missing etc..). + */ + return; + } + await this.room.ensureMessageKeyIsShared(_log); Object.assign(content, createReference(this.id)); await this.room.sendEvent(eventType, content, undefined, log); this.sentMessages.set(eventType, {content}); From d00f140309e8cf62ce4a3b6d43652f4cb2b1f010 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 20 Jun 2023 17:45:19 +0530 Subject: [PATCH 42/44] Delete secrets when cross-signing is reset --- src/matrix/Session.js | 5 +++++ src/matrix/ssss/SecretSharing.ts | 11 +++++++++++ src/matrix/storage/idb/Store.ts | 15 +++++++++++++++ .../storage/idb/stores/SharedSecretStore.ts | 4 ++++ 4 files changed, 35 insertions(+) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 94783509..2d9ad66e 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -772,6 +772,7 @@ export class Session { e2eeAccountChanges: null, hasNewRoomKeys: false, deviceMessageDecryptionResults: null, + changedDevices: null, }; const syncToken = syncResponse.next_batch; if (syncToken !== this.syncToken) { @@ -789,6 +790,7 @@ export class Session { const deviceLists = syncResponse.device_lists; if (this._deviceTracker && Array.isArray(deviceLists?.changed) && deviceLists.changed.length) { await log.wrap("deviceLists", log => this._deviceTracker.writeDeviceChanges(deviceLists.changed, txn, log)); + changes.changedDevices = deviceLists.changed; } if (preparation) { @@ -838,6 +840,9 @@ export class Session { if (changes.deviceMessageDecryptionResults) { await this._deviceMessageHandler.afterSyncCompleted(changes.deviceMessageDecryptionResults, this._deviceTracker, this._hsApi, log); } + if (changes.changedDevices?.includes(this.userId)) { + this._secretSharing?.checkSecretValidity(); + } } _tryReplaceRoomBeingCreated(roomId, log) { diff --git a/src/matrix/ssss/SecretSharing.ts b/src/matrix/ssss/SecretSharing.ts index 43c2057d..2ac96d9a 100644 --- a/src/matrix/ssss/SecretSharing.ts +++ b/src/matrix/ssss/SecretSharing.ts @@ -225,6 +225,17 @@ export class SecretSharing { } } + async checkSecretValidity(log: ILogItem): Promise { + const crossSigning = this.crossSigning.get(); + const needsDeleting = !await crossSigning?.areWeVerified(log); + if (needsDeleting) { + // User probably reset their cross-signing keys + // Can't trust the secrets anymore! + const txn = await this.storage.readWriteTxn([this.storage.storeNames.sharedSecrets]); + txn.sharedSecrets.deleteAllSecrets(); + } + } + async getLocallyStoredSecret(name: string): Promise { const txn = await this.storage.readTxn([ this.storage.storeNames.sharedSecrets, diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index c9df33b2..de7a2733 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -118,6 +118,16 @@ export class QueryTargetWrapper { } } + clear(): IDBRequest { + try { + LOG_REQUESTS && logRequest("clear", [], this._qt); + return this._qtStore.clear(); + } + catch (err) { + throw new IDBRequestAttemptError("delete", this._qt, err, []); + } + } + count(keyRange?: IDBKeyRange): IDBRequest { try { return this._qt.count(keyRange); @@ -195,6 +205,11 @@ export class Store extends QueryTarget { this._prepareErrorLog(request, log, "delete", keyOrKeyRange, undefined); } + clear(log?: ILogItem): void { + const request = this._idbStore.clear(); + this._prepareErrorLog(request, log, "delete", undefined, undefined); + } + private _prepareErrorLog(request: IDBRequest, log: ILogItem | undefined, operationName: string, key: IDBKey | undefined, value: T | undefined) { if (log) { log.ensureRefId(); diff --git a/src/matrix/storage/idb/stores/SharedSecretStore.ts b/src/matrix/storage/idb/stores/SharedSecretStore.ts index 79f9f625..97f1ee74 100644 --- a/src/matrix/storage/idb/stores/SharedSecretStore.ts +++ b/src/matrix/storage/idb/stores/SharedSecretStore.ts @@ -36,4 +36,8 @@ export class SharedSecretStore { remove(name: string): void { this._store.delete(name); } + + deleteAllSecrets(): void { + this._store.clear(); + } } From 57d3ce96456de948ebde05ca5d4182348ca6f118 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 20 Jun 2023 18:32:41 +0530 Subject: [PATCH 43/44] Show a spinner by default --- .../web/ui/session/verification/DeviceVerificationView.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/session/verification/DeviceVerificationView.ts b/src/platform/web/ui/session/verification/DeviceVerificationView.ts index d74f73b3..a9222575 100644 --- a/src/platform/web/ui/session/verification/DeviceVerificationView.ts +++ b/src/platform/web/ui/session/verification/DeviceVerificationView.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Builder, TemplateView} from "../../general/TemplateView"; +import {Builder, InlineTemplateView, TemplateView} from "../../general/TemplateView"; import {DeviceVerificationViewModel} from "../../../../../domain/session/verification/DeviceVerificationViewModel"; import {WaitingForOtherUserView} from "./stages/WaitingForOtherUserView"; import {VerificationCancelledView} from "./stages/VerificationCancelledView"; @@ -22,6 +22,7 @@ import {SelectMethodView} from "./stages/SelectMethodView"; import {VerifyEmojisView} from "./stages/VerifyEmojisView"; import {VerificationCompleteView} from "./stages/VerificationCompleteView"; import {MissingKeysView} from "./stages/MissingKeysView"; +import {spinner} from "../../common.js"; export class DeviceVerificationView extends TemplateView { render(t: Builder, vm: DeviceVerificationViewModel) { @@ -39,7 +40,7 @@ export class DeviceVerificationView extends TemplateView spinner(t)); } }) ]) From 541369f291552a59835da5f0cf2cf83ba7762899 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 20 Jun 2023 18:59:24 +0530 Subject: [PATCH 44/44] Center heading --- src/platform/web/ui/css/themes/element/theme.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 728afcc4..1f7f2f13 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1433,6 +1433,10 @@ button.RoomDetailsView_row::after { padding: 8px; } +.MissingKeysView__heading { + text-align: center; +} + .VerificationCompleteView>*, .SelectMethodView>*, .VerifyEmojisView>*,