From 087a4ad7cec1d33e5426a91ac934037fe35de259 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Nov 2022 20:52:55 -0600 Subject: [PATCH 001/168] Add copy permalink action --- .../room/timeline/tiles/BaseMessageTile.js | 6 ++ src/matrix/room/PowerLevels.js | 3 +- src/platform/web/dom/ImageHandle.js | 2 +- src/platform/web/dom/utils.js | 35 -------- src/platform/web/dom/utils.ts | 79 +++++++++++++++++++ .../session/room/timeline/BaseMessageView.js | 2 + .../web/ui/session/room/timeline/VideoView.js | 2 +- 7 files changed, 91 insertions(+), 38 deletions(-) delete mode 100644 src/platform/web/dom/utils.js create mode 100644 src/platform/web/dom/utils.ts diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index cfa27a9446..2cc3e572e2 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -17,6 +17,8 @@ limitations under the License. import {SimpleTile} from "./SimpleTile.js"; import {ReactionsViewModel} from "../ReactionsViewModel.js"; import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar"; +import {copyPlaintext} from "../../../../../platform/web/dom/utils"; + export class BaseMessageTile extends SimpleTile { constructor(entry, options) { @@ -45,6 +47,10 @@ export class BaseMessageTile extends SimpleTile { return `https://matrix.to/#/${encodeURIComponent(this._room.id)}/${encodeURIComponent(this._entry.id)}`; } + copyPermalink() { + copyPlaintext(this.permaLink); + } + get senderProfileLink() { return `https://matrix.to/#/${encodeURIComponent(this.sender)}`; } diff --git a/src/matrix/room/PowerLevels.js b/src/matrix/room/PowerLevels.js index 76e062ef37..63a5b0b0a1 100644 --- a/src/matrix/room/PowerLevels.js +++ b/src/matrix/room/PowerLevels.js @@ -66,10 +66,11 @@ export class PowerLevels { /** @param {string} action either "invite", "kick", "ban" or "redact". */ _getActionLevel(action) { - const level = this._plEvent?.content[action]; + const level = this._plEvent?.content?.[action]; if (typeof level === "number") { return level; } else { + // TODO: Why does this default to 50? return 50; } } diff --git a/src/platform/web/dom/ImageHandle.js b/src/platform/web/dom/ImageHandle.js index 4ac3a6cd2e..e41486f983 100644 --- a/src/platform/web/dom/ImageHandle.js +++ b/src/platform/web/dom/ImageHandle.js @@ -15,7 +15,7 @@ limitations under the License. */ import {BlobHandle} from "./BlobHandle.js"; -import {domEventAsPromise} from "./utils.js"; +import {domEventAsPromise} from "./utils"; export class ImageHandle { static async fromBlob(blob) { diff --git a/src/platform/web/dom/utils.js b/src/platform/web/dom/utils.js deleted file mode 100644 index 43a2664033..0000000000 --- a/src/platform/web/dom/utils.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -export function domEventAsPromise(element, successEvent) { - return new Promise((resolve, reject) => { - let detach; - const handleError = evt => { - detach(); - reject(evt.target.error); - }; - const handleSuccess = () => { - detach(); - resolve(); - }; - detach = () => { - element.removeEventListener(successEvent, handleSuccess); - element.removeEventListener("error", handleError); - }; - element.addEventListener(successEvent, handleSuccess); - element.addEventListener("error", handleError); - }); -} diff --git a/src/platform/web/dom/utils.ts b/src/platform/web/dom/utils.ts new file mode 100644 index 0000000000..4c17f1af95 --- /dev/null +++ b/src/platform/web/dom/utils.ts @@ -0,0 +1,79 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export function domEventAsPromise(element, successEvent): Promise { + return new Promise((resolve, reject) => { + let detach; + const handleError = evt => { + detach(); + reject(evt.target.error); + }; + const handleSuccess = () => { + detach(); + resolve(); + }; + detach = () => { + element.removeEventListener(successEvent, handleSuccess); + element.removeEventListener("error", handleError); + }; + element.addEventListener(successEvent, handleSuccess); + element.addEventListener("error", handleError); + }); +} + +// Copies the given text to clipboard and returns a boolean of whether the action was +// successful +export async function copyPlaintext(text: string): Promise { + try { + if (navigator?.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return true; + } else { + const textArea = document.createElement("textarea"); + textArea.value = text; + + // Avoid scrolling to bottom + textArea.style.top = "0"; + textArea.style.left = "0"; + textArea.style.position = "fixed"; + + document.body.appendChild(textArea); + + const selection = document.getSelection(); + if (!selection) { + console.error('copyPlaintext: Unable to copy text to clipboard in fallback mode because `selection` was null/undefined'); + return false; + } + + const range = document.createRange(); + // range.selectNodeContents(textArea); + range.selectNode(textArea); + selection.removeAllRanges(); + selection.addRange(range); + + const successful = document.execCommand("copy"); + selection.removeAllRanges(); + document.body.removeChild(textArea); + if(!successful) { + console.error('copyPlaintext: Unable to copy text to clipboard in fallback mode because the `copy` command is unsupported or disabled'); + } + return successful; + } + } catch (err) { + console.error("copyPlaintext: Ran into an error", err); + } + return false; +} diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index ee0a37db58..845c4a4bfd 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -120,6 +120,8 @@ export class BaseMessageView extends TemplateView { } else if (vm.canRedact) { options.push(Menu.option(vm.i18n`Delete`, () => vm.redact()).setDestructive()); } + + options.push(Menu.option(vm.i18n`Copy permalink`, () => vm.copyPermalink())); return options; } diff --git a/src/platform/web/ui/session/room/timeline/VideoView.js b/src/platform/web/ui/session/room/timeline/VideoView.js index 340cae6d24..9b092ed091 100644 --- a/src/platform/web/ui/session/room/timeline/VideoView.js +++ b/src/platform/web/ui/session/room/timeline/VideoView.js @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseMediaView} from "./BaseMediaView.js"; -import {domEventAsPromise} from "../../../../dom/utils.js"; +import {domEventAsPromise} from "../../../../dom/utils"; export class VideoView extends BaseMediaView { renderMedia(t) { From f0d53fe40fcfbce03fb2e47de5299225d6a5db24 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Nov 2022 20:56:27 -0600 Subject: [PATCH 002/168] Remove newline --- src/platform/web/ui/session/room/timeline/BaseMessageView.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index 845c4a4bfd..acd093e2cb 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -120,7 +120,6 @@ export class BaseMessageView extends TemplateView { } else if (vm.canRedact) { options.push(Menu.option(vm.i18n`Delete`, () => vm.redact()).setDestructive()); } - options.push(Menu.option(vm.i18n`Copy permalink`, () => vm.copyPermalink())); return options; } From 29aac096415bdf193138885ecff7d144e479d859 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Nov 2022 21:11:38 -0600 Subject: [PATCH 003/168] Add types to function parameters --- src/platform/web/dom/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/dom/utils.ts b/src/platform/web/dom/utils.ts index 4c17f1af95..8013b49161 100644 --- a/src/platform/web/dom/utils.ts +++ b/src/platform/web/dom/utils.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function domEventAsPromise(element, successEvent): Promise { +export function domEventAsPromise(element: HTMLElement, successEvent: string): Promise { return new Promise((resolve, reject) => { let detach; const handleError = evt => { From 35a08e3b051baaec5e2da6653a7ad9a349d22866 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 18 Nov 2022 12:13:47 -0600 Subject: [PATCH 004/168] Update language to "Copy matrix.to permalink" --- src/platform/web/ui/session/room/timeline/BaseMessageView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index acd093e2cb..d35e8c5a01 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -120,7 +120,7 @@ export class BaseMessageView extends TemplateView { } else if (vm.canRedact) { options.push(Menu.option(vm.i18n`Delete`, () => vm.redact()).setDestructive()); } - options.push(Menu.option(vm.i18n`Copy permalink`, () => vm.copyPermalink())); + options.push(Menu.option(vm.i18n`Copy matrix.to permalink`, () => vm.copyPermalink())); return options; } From 772d91f9241cf4da81f6ea9e55190b885ebdeb53 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 16 Feb 2023 11:27:43 +0530 Subject: [PATCH 005/168] WIP --- src/domain/session/room/RoomViewModel.js | 10 ++++ .../verification/SAS/SASVerification.ts | 33 +++++++++++ .../SAS/stages/BaseSASVerificationStage.ts | 35 ++++++++++++ .../SAS/stages/StartVerificationStage.ts | 55 +++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 src/matrix/verification/SAS/SASVerification.ts create mode 100644 src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts create mode 100644 src/matrix/verification/SAS/stages/StartVerificationStage.ts diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 31608a62e7..2130bc7f77 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -28,6 +28,7 @@ import {LocalMedia} from "../../../matrix/calls/LocalMedia"; // this is a breaking SDK change though to make this option mandatory import {tileClassForEntry as defaultTileClassForEntry} from "./timeline/tiles/index"; import {joinRoom} from "../../../matrix/room/joinRoom"; +import {SASVerification} from "../../../matrix/verification/SAS/SASVerification"; export class RoomViewModel extends ErrorReportViewModel { constructor(options) { @@ -49,6 +50,15 @@ export class RoomViewModel extends ErrorReportViewModel { this._setupCallViewModel(); } + async _startCrossSigning(otherUserId) { + await this.logAndCatch("startCrossSigning", async log => { + const session = this.getOption("session"); + const { userId, deviceId } = session; + const sas = new SASVerification(this.room, { userId, deviceId }, otherUserId, log); + await sas.start(); + }); + } + _setupCallViewModel() { if (!this.features.calls) { return; diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts new file mode 100644 index 0000000000..e2882fd45e --- /dev/null +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -0,0 +1,33 @@ +/* +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 {StartVerificationStage} from "./stages/StartVerificationStage"; +import type {ILogItem} from "../../../logging/types"; +import type {Room} from "../../room/Room.js"; +import type {BaseSASVerificationStage, UserData} from "./stages/BaseSASVerificationStage"; + +export class SASVerification { + private stages: BaseSASVerificationStage[] = []; + + constructor(private room: Room, private ourUser: UserData, otherUserId: string, log: ILogItem) { + this.stages.push(new StartVerificationStage(room, ourUser, otherUserId, log)); + } + + async start() { + for (const stage of this.stages) { + await stage.completeStage(); + } + } +} diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts new file mode 100644 index 0000000000..9d86d08b2b --- /dev/null +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -0,0 +1,35 @@ +/* +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 {ILogItem} from "../../../../lib.js"; +import type {Room} from "../../../room/Room.js"; + +export type UserData = { + userId: string; + deviceId: string; +} + +export abstract class BaseSASVerificationStage { + constructor(protected room: Room, + protected ourUser: UserData, + protected otherUserId: string, + protected log: ILogItem) { + + } + + abstract get type(): string; + abstract completeStage(): boolean | Promise; + abstract get nextStage(): BaseSASVerificationStage; +} diff --git a/src/matrix/verification/SAS/stages/StartVerificationStage.ts b/src/matrix/verification/SAS/stages/StartVerificationStage.ts new file mode 100644 index 0000000000..1f92afc5f5 --- /dev/null +++ b/src/matrix/verification/SAS/stages/StartVerificationStage.ts @@ -0,0 +1,55 @@ +/* +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 {BaseSASVerificationStage} from "./BaseSASVerificationStage"; + +// From element-web +// type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; +// type MacMethod = "hkdf-hmac-sha256.v2" | "org.matrix.msc3783.hkdf-hmac-sha256" | "hkdf-hmac-sha256" | "hmac-sha256"; + +// const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; +// const HASHES_LIST = ["sha256"]; +// const MAC_LIST: MacMethod[] = [ +// "hkdf-hmac-sha256.v2", +// "org.matrix.msc3783.hkdf-hmac-sha256", +// "hkdf-hmac-sha256", +// "hmac-sha256", +// ]; + +// const SAS_LIST = Object.keys(sasGenerators); +export class StartVerificationStage extends BaseSASVerificationStage { + + async completeStage() { + await this.log.wrap("StartVerificationStage.completeStage", async (log) => { + const content = { + "body": `${this.ourUser.userId} is requesting to verify your device, but your client does not support verification, so you may need to use a different verification method.`, + "from_device": this.ourUser.deviceId, + "methods": ["m.sas.v1"], + "msgtype": "m.key.verification.request", + "to": this.otherUserId, + }; + await this.room.sendEvent("m.room.message", content, null, log); + }); + return true; + } + + get type() { + return "m.key.verification.request"; + } + + get nextStage(): BaseSASVerificationStage { + return this; + } +} From d81864e90187227f98e1660c0b70ce2f6fb8e073 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 16 Feb 2023 21:41:33 +0530 Subject: [PATCH 006/168] WIP --- src/matrix/room/timeline/Timeline.js | 1 + .../SAS/stages/BaseSASVerificationStage.ts | 38 ++++++++-- .../SAS/stages/StartVerificationStage.ts | 33 ++++++++- .../SAS/stages/WaitForIncomingMessageStage.ts | 74 +++++++++++++++++++ 4 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index a721092e43..1850a762af 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -254,6 +254,7 @@ export class Timeline { /** @package */ addEntries(newEntries) { + console.log("addEntries", newEntries); this._addLocalRelationsToNewRemoteEntries(newEntries); this._updateEntriesFetchedFromHomeserver(newEntries); this._moveEntryToRemoteEntries(newEntries); diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index 9d86d08b2b..57a42e724c 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -15,21 +15,47 @@ limitations under the License. */ import type {ILogItem} from "../../../../lib.js"; import type {Room} from "../../../room/Room.js"; +import {Disposables} from "../../../../utils/Disposables"; export type UserData = { userId: string; deviceId: string; } -export abstract class BaseSASVerificationStage { - constructor(protected room: Room, - protected ourUser: UserData, - protected otherUserId: string, - protected log: ILogItem) { +export type Options = { + room: Room; + ourUser: UserData; + otherUserId: string; + log: ILogItem; +} + +export abstract class BaseSASVerificationStage extends Disposables { + protected room: Room; + protected ourUser: UserData; + protected otherUserId: string; + protected log: ILogItem; + protected requestEventId: string; + protected previousResult: undefined | any; + + constructor(options: Options) { + super(); + this.room = options.room; + this.ourUser = options.ourUser; + this.otherUserId = options.otherUserId; + this.log = options.log; + } + + setRequestEventId(id: string) { + this.requestEventId = id; + // todo: can this race with incoming message? + this.nextStage?.setRequestEventId(id); + } + setResultFromPreviousStage(result?: any) { + this.previousResult = result; } abstract get type(): string; - abstract completeStage(): boolean | Promise; + abstract completeStage(): undefined | Record; abstract get nextStage(): BaseSASVerificationStage; } diff --git a/src/matrix/verification/SAS/stages/StartVerificationStage.ts b/src/matrix/verification/SAS/stages/StartVerificationStage.ts index 1f92afc5f5..1ef6fd0588 100644 --- a/src/matrix/verification/SAS/stages/StartVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/StartVerificationStage.ts @@ -30,7 +30,10 @@ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; // const SAS_LIST = Object.keys(sasGenerators); export class StartVerificationStage extends BaseSASVerificationStage { - + + private readyMessagePromise: Promise; + private startMessagePromise: Promise; + async completeStage() { await this.log.wrap("StartVerificationStage.completeStage", async (log) => { const content = { @@ -41,10 +44,38 @@ export class StartVerificationStage extends BaseSASVerificationStage { "to": this.otherUserId, }; await this.room.sendEvent("m.room.message", content, null, log); + const [readyContent, startContent] = await this.fetchMessageEventsFromTimeline(); + console.log("readyContent", readyContent, "startContent", startContent); + this.dispose(); }); return true; } + private fetchMessageEventsFromTimeline() { + let readyResolve, startResolve; + this.readyMessagePromise = new Promise(r => { readyResolve = r; }); + this.startMessagePromise = new Promise(r => { startResolve = r; }); + this.track( + this.room._timeline.entries.subscribe({ + onAdd: (_, entry) => { + if (entry.eventType === "m.key.verification.ready") { + readyResolve(entry.content); + } + else if (entry.eventType === "m.key.verification.start") { + startResolve(entry.content); + } + }, + onRemove: () => { + + }, + onUpdate: () => { + + }, + }) + ); + return Promise.all([this.readyMessagePromise, this.startMessagePromise]); + } + get type() { return "m.key.verification.request"; } diff --git a/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts b/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts new file mode 100644 index 0000000000..1f8e1fd8a7 --- /dev/null +++ b/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts @@ -0,0 +1,74 @@ +/* +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 {BaseSASVerificationStage, Options} from "./BaseSASVerificationStage"; + +export class WaitForIncomingMessageStage extends BaseSASVerificationStage { + constructor(private messageType: string, options: Options) { + super(options); + } + + async completeStage() { + await this.log.wrap("WaitForIncomingMessageStage.completeStage", async (log) => { + const content = await this.fetchMessageEventsFromTimeline(); + console.log("content found", content); + this.nextStage.setResultFromPreviousStage({ + ...this.previousResult, + [this.messageType]: content + }); + this.dispose(); + }); + return true; + } + + private fetchMessageEventsFromTimeline() { + // todo: add timeout after 10 mins + return new Promise(resolve => { + this.track( + this.room._timeline.entries.subscribe({ + onAdd: (_, entry) => { + if (entry.sender === this.ourUser.userId) { + // We only care about incoming / remote message events + return; + } + if (entry.eventType === this.messageType && + entry.content["m.relates_to"]["event_id"] === this.requestEventId) { + resolve(entry.content); + } + }, + onRemove: () => { }, + onUpdate: () => { }, + }) + ); + const remoteEntries = this.room._timeline.remoteEntries; + // In case we were slow and the event is already added to the timeline, + for (const entry of remoteEntries) { + if (entry.eventType === this.messageType && + entry.content["m.relates_to"]["event_id"] === this.requestEventId) { + resolve(entry.content); + } + } + }); + } + + get type() { + return this.messageType; + } + + get nextStage(): BaseSASVerificationStage { + return this; + } +} + From e6ea003beffbca0fe0005dcf50d48963edf7629e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 17 Feb 2023 17:18:17 +0530 Subject: [PATCH 007/168] WIP +1 --- .../verification/SAS/SASVerification.ts | 21 +++++-- .../SAS/stages/BaseSASVerificationStage.ts | 14 ++++- .../SAS/stages/StartVerificationStage.ts | 58 ++++++++----------- .../SAS/stages/WaitForIncomingMessageStage.ts | 13 ++--- 4 files changed, 58 insertions(+), 48 deletions(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index e2882fd45e..82c5f856e6 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -17,17 +17,28 @@ import {StartVerificationStage} from "./stages/StartVerificationStage"; import type {ILogItem} from "../../../logging/types"; import type {Room} from "../../room/Room.js"; import type {BaseSASVerificationStage, UserData} from "./stages/BaseSASVerificationStage"; +import {WaitForIncomingMessageStage} from "./stages/WaitForIncomingMessageStage"; export class SASVerification { - private stages: BaseSASVerificationStage[] = []; + private startStage: BaseSASVerificationStage; constructor(private room: Room, private ourUser: UserData, otherUserId: string, log: ILogItem) { - this.stages.push(new StartVerificationStage(room, ourUser, otherUserId, log)); + const options = { room, ourUser, otherUserId, log }; + let stage: BaseSASVerificationStage = new StartVerificationStage(options); + this.startStage = stage; + + stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.ready", options)); + stage = stage.nextStage; + + stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.start", options)); + stage = stage.nextStage; } async start() { - for (const stage of this.stages) { - await stage.completeStage(); - } + let stage = this.startStage; + do { + await stage.completeStage(); + stage = stage.nextStage; + } while (stage); } } diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index 57a42e724c..0fcd249bcc 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -36,13 +36,14 @@ export abstract class BaseSASVerificationStage extends Disposables { protected log: ILogItem; protected requestEventId: string; protected previousResult: undefined | any; + protected _nextStage: BaseSASVerificationStage; constructor(options: Options) { super(); this.room = options.room; this.ourUser = options.ourUser; this.otherUserId = options.otherUserId; - this.log = options.log; + this.log = options.log; } setRequestEventId(id: string) { @@ -55,7 +56,14 @@ export abstract class BaseSASVerificationStage extends Disposables { this.previousResult = result; } + setNextStage(stage: BaseSASVerificationStage) { + this._nextStage = stage; + } + + get nextStage(): BaseSASVerificationStage { + return this._nextStage; + } + abstract get type(): string; - abstract completeStage(): undefined | Record; - abstract get nextStage(): BaseSASVerificationStage; + abstract completeStage(): Promise; } diff --git a/src/matrix/verification/SAS/stages/StartVerificationStage.ts b/src/matrix/verification/SAS/stages/StartVerificationStage.ts index 1ef6fd0588..081c6d6717 100644 --- a/src/matrix/verification/SAS/stages/StartVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/StartVerificationStage.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; +import {FragmentBoundaryEntry} from "../../../room/timeline/entries/FragmentBoundaryEntry.js"; // From element-web // type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; @@ -31,9 +32,6 @@ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; // const SAS_LIST = Object.keys(sasGenerators); export class StartVerificationStage extends BaseSASVerificationStage { - private readyMessagePromise: Promise; - private startMessagePromise: Promise; - async completeStage() { await this.log.wrap("StartVerificationStage.completeStage", async (log) => { const content = { @@ -43,44 +41,38 @@ export class StartVerificationStage extends BaseSASVerificationStage { "msgtype": "m.key.verification.request", "to": this.otherUserId, }; + const promise = this.trackEventId(); await this.room.sendEvent("m.room.message", content, null, log); - const [readyContent, startContent] = await this.fetchMessageEventsFromTimeline(); - console.log("readyContent", readyContent, "startContent", startContent); + const eventId = await promise; + console.log("eventId", eventId); + this.setRequestEventId(eventId); this.dispose(); }); - return true; } - private fetchMessageEventsFromTimeline() { - let readyResolve, startResolve; - this.readyMessagePromise = new Promise(r => { readyResolve = r; }); - this.startMessagePromise = new Promise(r => { startResolve = r; }); - this.track( - this.room._timeline.entries.subscribe({ - onAdd: (_, entry) => { - if (entry.eventType === "m.key.verification.ready") { - readyResolve(entry.content); - } - else if (entry.eventType === "m.key.verification.start") { - startResolve(entry.content); - } - }, - onRemove: () => { - - }, - onUpdate: () => { - - }, - }) - ); - return Promise.all([this.readyMessagePromise, this.startMessagePromise]); + private trackEventId(): Promise { + return new Promise(resolve => { + this.track( + this.room._timeline.entries.subscribe({ + onAdd: (_, entry) => { + if (entry instanceof FragmentBoundaryEntry) { + return; + } + if (!entry.isPending && + entry.content["msgtype"] === "m.key.verification.request" && + entry.content["from_device"] === this.ourUser.deviceId) { + console.log("found event", entry); + resolve(entry.id); + } + }, + onRemove: () => { /**noop*/ }, + onUpdate: () => { /**noop*/ }, + }) + ); + }); } get type() { return "m.key.verification.request"; } - - get nextStage(): BaseSASVerificationStage { - return this; - } } diff --git a/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts b/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts index 1f8e1fd8a7..b2d288f144 100644 --- a/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts +++ b/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage, Options} from "./BaseSASVerificationStage"; +import {FragmentBoundaryEntry} from "../../../room/timeline/entries/FragmentBoundaryEntry.js"; export class WaitForIncomingMessageStage extends BaseSASVerificationStage { constructor(private messageType: string, options: Options) { @@ -30,7 +31,6 @@ export class WaitForIncomingMessageStage extends BaseSASVerificationStage { }); this.dispose(); }); - return true; } private fetchMessageEventsFromTimeline() { @@ -43,7 +43,7 @@ export class WaitForIncomingMessageStage extends BaseSASVerificationStage { // We only care about incoming / remote message events return; } - if (entry.eventType === this.messageType && + if (entry.type === this.messageType && entry.content["m.relates_to"]["event_id"] === this.requestEventId) { resolve(entry.content); } @@ -55,7 +55,10 @@ export class WaitForIncomingMessageStage extends BaseSASVerificationStage { const remoteEntries = this.room._timeline.remoteEntries; // In case we were slow and the event is already added to the timeline, for (const entry of remoteEntries) { - if (entry.eventType === this.messageType && + if (entry instanceof FragmentBoundaryEntry) { + return; + } + if (entry.type === this.messageType && entry.content["m.relates_to"]["event_id"] === this.requestEventId) { resolve(entry.content); } @@ -66,9 +69,5 @@ export class WaitForIncomingMessageStage extends BaseSASVerificationStage { get type() { return this.messageType; } - - get nextStage(): BaseSASVerificationStage { - return this; - } } From 3321859ae6bfb177311a0fa6ec7f5f5505e1cb55 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 22 Feb 2023 12:03:03 +0530 Subject: [PATCH 008/168] Add more stages --- src/domain/session/room/RoomViewModel.js | 3 +- src/matrix/Session.js | 2 + src/matrix/verification/CrossSigning.ts | 13 ++ .../verification/SAS/SASVerification.ts | 39 +++- .../SAS/stages/AcceptVerificationStage.ts | 84 +++++++ .../SAS/stages/BaseSASVerificationStage.ts | 9 + .../verification/SAS/stages/SendKeyStage.ts | 213 ++++++++++++++++++ .../SAS/stages/WaitForIncomingMessageStage.ts | 24 +- 8 files changed, 362 insertions(+), 25 deletions(-) create mode 100644 src/matrix/verification/SAS/stages/AcceptVerificationStage.ts create mode 100644 src/matrix/verification/SAS/stages/SendKeyStage.ts diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 2130bc7f77..9af9aa47e6 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -53,8 +53,7 @@ export class RoomViewModel extends ErrorReportViewModel { async _startCrossSigning(otherUserId) { await this.logAndCatch("startCrossSigning", async log => { const session = this.getOption("session"); - const { userId, deviceId } = session; - const sas = new SASVerification(this.room, { userId, deviceId }, otherUserId, log); + const sas = session.crossSigning?.startVerification(this._room, otherUserId, log); await sas.start(); }); } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 35f713f658..22dd0b56e6 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -339,9 +339,11 @@ export class Session { secretStorage, platform: this._platform, olm: this._olm, + olmUtil: this._olmUtil, deviceTracker: this._deviceTracker, hsApi: this._hsApi, ownUserId: this.userId, + deviceId: this.deviceId, e2eeAccount: this._e2eeAccount }); await log.wrap("enable cross-signing", log => { diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index db480dd08a..8445a38e1c 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -21,9 +21,11 @@ import type {DeviceTracker} from "../e2ee/DeviceTracker"; import type * as OlmNamespace from "@matrix-org/olm"; import type {HomeServerApi} from "../net/HomeServerApi"; import type {Account} from "../e2ee/Account"; +import type {Room} from "../room/Room.js"; import { ILogItem } from "../../lib"; import {pkSign} from "./common"; import type {ISignatures} from "./common"; +import {SASVerification} from "./SAS/SASVerification"; type Olm = typeof OlmNamespace; @@ -33,10 +35,12 @@ export class CrossSigning { private readonly platform: Platform; private readonly deviceTracker: DeviceTracker; private readonly olm: Olm; + private readonly olmUtil: Olm.Utility; private readonly hsApi: HomeServerApi; private readonly ownUserId: string; private readonly e2eeAccount: Account; private _isMasterKeyTrusted: boolean = false; + private readonly deviceId: string; constructor(options: { storage: Storage, @@ -44,7 +48,9 @@ export class CrossSigning { deviceTracker: DeviceTracker, platform: Platform, olm: Olm, + olmUtil: Olm.Utility, ownUserId: string, + deviceId: string, hsApi: HomeServerApi, e2eeAccount: Account }) { @@ -53,8 +59,10 @@ export class CrossSigning { this.platform = options.platform; this.deviceTracker = options.deviceTracker; this.olm = options.olm; + this.olmUtil = options.olmUtil; this.hsApi = options.hsApi; this.ownUserId = options.ownUserId; + this.deviceId = options.deviceId; this.e2eeAccount = options.e2eeAccount } @@ -108,5 +116,10 @@ export class CrossSigning { get isMasterKeyTrusted(): boolean { return this._isMasterKeyTrusted; } + + startVerification(room: Room, userId: string, log: ILogItem): SASVerification { + return new SASVerification(room, this.olm, this.olmUtil, { userId: this.ownUserId, deviceId: this.deviceId }, userId, log); + } } + diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 82c5f856e6..7c38b7cede 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -14,24 +14,45 @@ See the License for the specific language governing permissions and limitations under the License. */ import {StartVerificationStage} from "./stages/StartVerificationStage"; +import {WaitForIncomingMessageStage} from "./stages/WaitForIncomingMessageStage"; +import {AcceptVerificationStage} from "./stages/AcceptVerificationStage"; +import {SendKeyStage} from "./stages/SendKeyStage"; import type {ILogItem} from "../../../logging/types"; import type {Room} from "../../room/Room.js"; import type {BaseSASVerificationStage, UserData} from "./stages/BaseSASVerificationStage"; -import {WaitForIncomingMessageStage} from "./stages/WaitForIncomingMessageStage"; +import type * as OlmNamespace from "@matrix-org/olm"; + +type Olm = typeof OlmNamespace; export class SASVerification { private startStage: BaseSASVerificationStage; - constructor(private room: Room, private ourUser: UserData, otherUserId: string, log: ILogItem) { - const options = { room, ourUser, otherUserId, log }; - let stage: BaseSASVerificationStage = new StartVerificationStage(options); - this.startStage = stage; + constructor(private room: Room, private olm: Olm, private olmUtil: Olm.Utility, private ourUser: UserData, otherUserId: string, log: ILogItem) { + const olmSas = new olm.SAS(); + try { + const options = { room, ourUser, otherUserId, log, olmSas, olmUtil }; + let stage: BaseSASVerificationStage = new StartVerificationStage(options); + this.startStage = stage; - stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.ready", options)); - stage = stage.nextStage; + stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.ready", options)); + stage = stage.nextStage; + + stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.start", options)); + stage = stage.nextStage; - stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.start", options)); - stage = stage.nextStage; + stage.setNextStage(new AcceptVerificationStage(options)); + stage = stage.nextStage; + + stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.key", options)); + stage = stage.nextStage; + + stage.setNextStage(new SendKeyStage(options)); + stage = stage.nextStage; + console.log("startStage", this.startStage); + } + finally { + olmSas.free(); + } } async start() { diff --git a/src/matrix/verification/SAS/stages/AcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/AcceptVerificationStage.ts new file mode 100644 index 0000000000..a8d320467c --- /dev/null +++ b/src/matrix/verification/SAS/stages/AcceptVerificationStage.ts @@ -0,0 +1,84 @@ +/* +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 {BaseSASVerificationStage} from "./BaseSASVerificationStage"; +import anotherjson from "another-json"; + +// From element-web +type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; +type MacMethod = "hkdf-hmac-sha256.v2" | "org.matrix.msc3783.hkdf-hmac-sha256" | "hkdf-hmac-sha256" | "hmac-sha256"; + +const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; +const HASHES_LIST = ["sha256"]; +const MAC_LIST: MacMethod[] = [ + "hkdf-hmac-sha256.v2", + "org.matrix.msc3783.hkdf-hmac-sha256", + "hkdf-hmac-sha256", + "hmac-sha256", +]; +const SAS_LIST = ["decimal", "emoji"]; +const SAS_SET = new Set(SAS_LIST); + +export class AcceptVerificationStage extends BaseSASVerificationStage { + + async completeStage() { + await this.log.wrap("AcceptVerificationStage.completeStage", async (log) => { + const event = this.previousResult["m.key.verification.start"]; + const content = { + ...event.content, + "m.relates_to": event.relation, + }; + console.log("content from event", content); + const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0]; + const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0]; + const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0]; + const sasMethods = intersection(content.short_authentication_string, SAS_SET); + if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) { + // todo: ensure this cancels the verification + throw new Error("Descriptive error here!"); + } + const ourPubKey = this.olmSAS.get_pubkey(); + const commitmentStr = ourPubKey + anotherjson.stringify(content); + const contentToSend = { + key_agreement_protocol: keyAgreement, + hash: hashMethod, + message_authentication_code: macMethod, + short_authentication_string: sasMethods, + // TODO: use selected hash function (when we support multiple) + commitment: this.olmUtil.sha256(commitmentStr), + "m.relates_to": { + event_id: this.requestEventId, + rel_type: "m.reference", + } + }; + await this.room.sendEvent("m.key.verification.accept", contentToSend, null, log); + this.nextStage?.setResultFromPreviousStage({ + ...this.previousResult, + [this.type]: contentToSend, + "our_pub_key": ourPubKey, + }); + this.dispose(); + }); + } + + + get type() { + return "m.key.verification.accept"; + } +} + +function intersection(anArray: T[], aSet: Set): T[] { + return Array.isArray(anArray) ? anArray.filter((x) => aSet.has(x)) : []; +} diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index 0fcd249bcc..9c5be61158 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -15,8 +15,11 @@ limitations under the License. */ import type {ILogItem} from "../../../../lib.js"; import type {Room} from "../../../room/Room.js"; +import type * as OlmNamespace from "@matrix-org/olm"; import {Disposables} from "../../../../utils/Disposables"; +type Olm = typeof OlmNamespace; + export type UserData = { userId: string; deviceId: string; @@ -27,6 +30,8 @@ export type Options = { ourUser: UserData; otherUserId: string; log: ILogItem; + olmSas: Olm.SAS; + olmUtil: Olm.Utility; } export abstract class BaseSASVerificationStage extends Disposables { @@ -34,6 +39,8 @@ export abstract class BaseSASVerificationStage extends Disposables { protected ourUser: UserData; protected otherUserId: string; protected log: ILogItem; + protected olmSAS: Olm.SAS; + protected olmUtil: Olm.Utility; protected requestEventId: string; protected previousResult: undefined | any; protected _nextStage: BaseSASVerificationStage; @@ -44,6 +51,8 @@ export abstract class BaseSASVerificationStage extends Disposables { this.ourUser = options.ourUser; this.otherUserId = options.otherUserId; this.log = options.log; + this.olmSAS = options.olmSas; + this.olmUtil = options.olmUtil; } setRequestEventId(id: string) { diff --git a/src/matrix/verification/SAS/stages/SendKeyStage.ts b/src/matrix/verification/SAS/stages/SendKeyStage.ts new file mode 100644 index 0000000000..dbbe449082 --- /dev/null +++ b/src/matrix/verification/SAS/stages/SendKeyStage.ts @@ -0,0 +1,213 @@ +/* +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 {BaseSASVerificationStage} from "./BaseSASVerificationStage"; +import anotherjson from "another-json"; + +// From element-web +type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; +type MacMethod = "hkdf-hmac-sha256.v2" | "org.matrix.msc3783.hkdf-hmac-sha256" | "hkdf-hmac-sha256" | "hmac-sha256"; + +const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; +const HASHES_LIST = ["sha256"]; +const MAC_LIST: MacMethod[] = [ + "hkdf-hmac-sha256.v2", + "org.matrix.msc3783.hkdf-hmac-sha256", + "hkdf-hmac-sha256", + "hmac-sha256", +]; +const SAS_LIST = ["decimal", "emoji"]; +const SAS_SET = new Set(SAS_LIST); + + +type SASUserInfo = { + userId: string; + deviceId: string; + publicKey: string; +} +type SASUserInfoCollection = { + our: SASUserInfo; + their: SASUserInfo; + requestId: string; +}; + +const calculateKeyAgreement = { + // eslint-disable-next-line @typescript-eslint/naming-convention + "curve25519-hkdf-sha256": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array { + console.log("sas.requestId", sas.requestId); + const ourInfo = `${sas.our.userId}|${sas.our.deviceId}|` + `${sas.our.publicKey}|`; + const theirInfo = `${sas.their.userId}|${sas.their.deviceId}|${sas.their.publicKey}|`; + console.log("ourInfo", ourInfo); + console.log("theirInfo", theirInfo); + const initiatedByMe = false; + const sasInfo = + "MATRIX_KEY_VERIFICATION_SAS|" + + (initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.requestId; + console.log("sasInfo", sasInfo); + return olmSAS.generate_bytes(sasInfo, bytes); + }, + "curve25519": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array { + const ourInfo = `${sas.our.userId}${sas.our.deviceId}`; + const theirInfo = `${sas.their.userId}${sas.their.deviceId}`; + const initiatedByMe = false; + const sasInfo = + "MATRIX_KEY_VERIFICATION_SAS" + + (initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.requestId; + return olmSAS.generate_bytes(sasInfo, bytes); + }, +} as const; + +export class SendKeyStage extends BaseSASVerificationStage { + + async completeStage() { + await this.log.wrap("SendKeyStage.completeStage", async (log) => { + const event = this.previousResult["m.key.verification.key"]; + const content = event.content; + const theirKey = content.key; + const ourSasKey = this.previousResult["our_pub_key"]; + console.log("ourSasKey", ourSasKey); + const contentToSend = { + key: ourSasKey, + "m.relates_to": { + event_id: this.requestEventId, + rel_type: "m.reference", + }, + }; + await this.room.sendEvent("m.key.verification.key", contentToSend, null, log); + const keyAgreement = this.previousResult["m.key.verification.accept"].key_agreement_protocol; + const otherUserDeviceId = this.previousResult["m.key.verification.start"].content.from_device; + this.olmSAS.set_their_key(theirKey); + const sasBytes = calculateKeyAgreement[keyAgreement]({ + our: { + userId: this.ourUser.userId, + deviceId: this.ourUser.deviceId, + publicKey: ourSasKey, + }, + their: { + userId: this.otherUserId, + deviceId: otherUserDeviceId, + publicKey: theirKey, + }, + requestId: this.requestEventId, + }, this.olmSAS, 6); + const emoji = generateEmojiSas(Array.from(sasBytes)); + console.log("emoji", emoji); + this.dispose(); + }); + } + + + get type() { + return "m.key.verification.accept"; + } +} + +function intersection(anArray: T[], aSet: Set): T[] { + return Array.isArray(anArray) ? anArray.filter((x) => aSet.has(x)) : []; +} + +// function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas { +// const sas: IGeneratedSas = {}; +// for (const method of methods) { +// if (method in sasGenerators) { +// // @ts-ignore - ts doesn't like us mixing types like this +// sas[method] = sasGenerators[method](Array.from(sasBytes)); +// } +// } +// return sas; +// } + +type EmojiMapping = [emoji: string, name: string]; + +const emojiMapping: EmojiMapping[] = [ + ["๐Ÿถ", "dog"], // 0 + ["๐Ÿฑ", "cat"], // 1 + ["๐Ÿฆ", "lion"], // 2 + ["๐ŸŽ", "horse"], // 3 + ["๐Ÿฆ„", "unicorn"], // 4 + ["๐Ÿท", "pig"], // 5 + ["๐Ÿ˜", "elephant"], // 6 + ["๐Ÿฐ", "rabbit"], // 7 + ["๐Ÿผ", "panda"], // 8 + ["๐Ÿ“", "rooster"], // 9 + ["๐Ÿง", "penguin"], // 10 + ["๐Ÿข", "turtle"], // 11 + ["๐ŸŸ", "fish"], // 12 + ["๐Ÿ™", "octopus"], // 13 + ["๐Ÿฆ‹", "butterfly"], // 14 + ["๐ŸŒท", "flower"], // 15 + ["๐ŸŒณ", "tree"], // 16 + ["๐ŸŒต", "cactus"], // 17 + ["๐Ÿ„", "mushroom"], // 18 + ["๐ŸŒ", "globe"], // 19 + ["๐ŸŒ™", "moon"], // 20 + ["โ˜๏ธ", "cloud"], // 21 + ["๐Ÿ”ฅ", "fire"], // 22 + ["๐ŸŒ", "banana"], // 23 + ["๐ŸŽ", "apple"], // 24 + ["๐Ÿ“", "strawberry"], // 25 + ["๐ŸŒฝ", "corn"], // 26 + ["๐Ÿ•", "pizza"], // 27 + ["๐ŸŽ‚", "cake"], // 28 + ["โค๏ธ", "heart"], // 29 + ["๐Ÿ™‚", "smiley"], // 30 + ["๐Ÿค–", "robot"], // 31 + ["๐ŸŽฉ", "hat"], // 32 + ["๐Ÿ‘“", "glasses"], // 33 + ["๐Ÿ”ง", "spanner"], // 34 + ["๐ŸŽ…", "santa"], // 35 + ["๐Ÿ‘", "thumbs up"], // 36 + ["โ˜‚๏ธ", "umbrella"], // 37 + ["โŒ›", "hourglass"], // 38 + ["โฐ", "clock"], // 39 + ["๐ŸŽ", "gift"], // 40 + ["๐Ÿ’ก", "light bulb"], // 41 + ["๐Ÿ“•", "book"], // 42 + ["โœ๏ธ", "pencil"], // 43 + ["๐Ÿ“Ž", "paperclip"], // 44 + ["โœ‚๏ธ", "scissors"], // 45 + ["๐Ÿ”’", "lock"], // 46 + ["๐Ÿ”‘", "key"], // 47 + ["๐Ÿ”จ", "hammer"], // 48 + ["โ˜Ž๏ธ", "telephone"], // 49 + ["๐Ÿ", "flag"], // 50 + ["๐Ÿš‚", "train"], // 51 + ["๐Ÿšฒ", "bicycle"], // 52 + ["โœˆ๏ธ", "aeroplane"], // 53 + ["๐Ÿš€", "rocket"], // 54 + ["๐Ÿ†", "trophy"], // 55 + ["โšฝ", "ball"], // 56 + ["๐ŸŽธ", "guitar"], // 57 + ["๐ŸŽบ", "trumpet"], // 58 + ["๐Ÿ””", "bell"], // 59 + ["โš“๏ธ", "anchor"], // 60 + ["๐ŸŽง", "headphones"], // 61 + ["๐Ÿ“", "folder"], // 62 + ["๐Ÿ“Œ", "pin"], // 63 +]; + +function generateEmojiSas(sasBytes: number[]): EmojiMapping[] { + const emojis = [ + // just like base64 encoding + sasBytes[0] >> 2, + ((sasBytes[0] & 0x3) << 4) | (sasBytes[1] >> 4), + ((sasBytes[1] & 0xf) << 2) | (sasBytes[2] >> 6), + sasBytes[2] & 0x3f, + sasBytes[3] >> 2, + ((sasBytes[3] & 0x3) << 4) | (sasBytes[4] >> 4), + ((sasBytes[4] & 0xf) << 2) | (sasBytes[5] >> 6), + ]; + return emojis.map((num) => emojiMapping[num]); +} diff --git a/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts b/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts index b2d288f144..087883acbe 100644 --- a/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts +++ b/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts @@ -23,11 +23,11 @@ export class WaitForIncomingMessageStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("WaitForIncomingMessageStage.completeStage", async (log) => { - const content = await this.fetchMessageEventsFromTimeline(); - console.log("content found", content); - this.nextStage.setResultFromPreviousStage({ + const entry = await this.fetchMessageEventsFromTimeline(); + console.log("content", entry); + this.nextStage?.setResultFromPreviousStage({ ...this.previousResult, - [this.messageType]: content + [this.messageType]: entry }); this.dispose(); }); @@ -39,13 +39,9 @@ export class WaitForIncomingMessageStage extends BaseSASVerificationStage { this.track( this.room._timeline.entries.subscribe({ onAdd: (_, entry) => { - if (entry.sender === this.ourUser.userId) { - // We only care about incoming / remote message events - return; - } - if (entry.type === this.messageType && - entry.content["m.relates_to"]["event_id"] === this.requestEventId) { - resolve(entry.content); + if (entry.eventType === this.messageType && + entry.relatedEventId === this.requestEventId) { + resolve(entry); } }, onRemove: () => { }, @@ -58,9 +54,9 @@ export class WaitForIncomingMessageStage extends BaseSASVerificationStage { if (entry instanceof FragmentBoundaryEntry) { return; } - if (entry.type === this.messageType && - entry.content["m.relates_to"]["event_id"] === this.requestEventId) { - resolve(entry.content); + if (entry.eventType === this.messageType && + entry.relatedEventId === this.requestEventId) { + resolve(entry); } } }); From 5e1dca946b589d3d4c6e1d531a6329b92ffddda1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 22 Feb 2023 13:00:36 +0530 Subject: [PATCH 009/168] Free olmSas after all stages have completed --- src/matrix/verification/SAS/SASVerification.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 7c38b7cede..8d1d6ba6d2 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -26,9 +26,11 @@ type Olm = typeof OlmNamespace; export class SASVerification { private startStage: BaseSASVerificationStage; + private olmSas: Olm.SAS; constructor(private room: Room, private olm: Olm, private olmUtil: Olm.Utility, private ourUser: UserData, otherUserId: string, log: ILogItem) { const olmSas = new olm.SAS(); + this.olmSas = olmSas; try { const options = { room, ourUser, otherUserId, log, olmSas, olmUtil }; let stage: BaseSASVerificationStage = new StartVerificationStage(options); @@ -51,15 +53,19 @@ export class SASVerification { console.log("startStage", this.startStage); } finally { - olmSas.free(); } } async start() { - let stage = this.startStage; - do { - await stage.completeStage(); - stage = stage.nextStage; - } while (stage); + try { + let stage = this.startStage; + do { + await stage.completeStage(); + stage = stage.nextStage; + } while (stage); + } + finally { + this.olmSas.free(); + } } } From af918e3df0f07b3eb6143a5e8c66d4be89f17b1f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 22 Feb 2023 13:01:34 +0530 Subject: [PATCH 010/168] Remove comment --- .../SAS/stages/StartVerificationStage.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/matrix/verification/SAS/stages/StartVerificationStage.ts b/src/matrix/verification/SAS/stages/StartVerificationStage.ts index 081c6d6717..175d00454f 100644 --- a/src/matrix/verification/SAS/stages/StartVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/StartVerificationStage.ts @@ -16,20 +16,6 @@ limitations under the License. import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {FragmentBoundaryEntry} from "../../../room/timeline/entries/FragmentBoundaryEntry.js"; -// From element-web -// type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; -// type MacMethod = "hkdf-hmac-sha256.v2" | "org.matrix.msc3783.hkdf-hmac-sha256" | "hkdf-hmac-sha256" | "hmac-sha256"; - -// const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; -// const HASHES_LIST = ["sha256"]; -// const MAC_LIST: MacMethod[] = [ -// "hkdf-hmac-sha256.v2", -// "org.matrix.msc3783.hkdf-hmac-sha256", -// "hkdf-hmac-sha256", -// "hmac-sha256", -// ]; - -// const SAS_LIST = Object.keys(sasGenerators); export class StartVerificationStage extends BaseSASVerificationStage { async completeStage() { From 75688cf6f3f8f49d6e8522c5c7af3f724208882f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 22 Feb 2023 13:01:58 +0530 Subject: [PATCH 011/168] REFACTOR: Extract methods and functions --- src/matrix/verification/SAS/generator.ts | 122 ++++++++++++++ .../verification/SAS/stages/SendKeyStage.ts | 152 +++++------------- 2 files changed, 161 insertions(+), 113 deletions(-) create mode 100644 src/matrix/verification/SAS/generator.ts diff --git a/src/matrix/verification/SAS/generator.ts b/src/matrix/verification/SAS/generator.ts new file mode 100644 index 0000000000..cff46f6f24 --- /dev/null +++ b/src/matrix/verification/SAS/generator.ts @@ -0,0 +1,122 @@ +/* +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. +*/ + +// Copied from element-web + +type EmojiMapping = [emoji: string, name: string]; + +const emojiMapping: EmojiMapping[] = [ + ["๐Ÿถ", "dog"], // 0 + ["๐Ÿฑ", "cat"], // 1 + ["๐Ÿฆ", "lion"], // 2 + ["๐ŸŽ", "horse"], // 3 + ["๐Ÿฆ„", "unicorn"], // 4 + ["๐Ÿท", "pig"], // 5 + ["๐Ÿ˜", "elephant"], // 6 + ["๐Ÿฐ", "rabbit"], // 7 + ["๐Ÿผ", "panda"], // 8 + ["๐Ÿ“", "rooster"], // 9 + ["๐Ÿง", "penguin"], // 10 + ["๐Ÿข", "turtle"], // 11 + ["๐ŸŸ", "fish"], // 12 + ["๐Ÿ™", "octopus"], // 13 + ["๐Ÿฆ‹", "butterfly"], // 14 + ["๐ŸŒท", "flower"], // 15 + ["๐ŸŒณ", "tree"], // 16 + ["๐ŸŒต", "cactus"], // 17 + ["๐Ÿ„", "mushroom"], // 18 + ["๐ŸŒ", "globe"], // 19 + ["๐ŸŒ™", "moon"], // 20 + ["โ˜๏ธ", "cloud"], // 21 + ["๐Ÿ”ฅ", "fire"], // 22 + ["๐ŸŒ", "banana"], // 23 + ["๐ŸŽ", "apple"], // 24 + ["๐Ÿ“", "strawberry"], // 25 + ["๐ŸŒฝ", "corn"], // 26 + ["๐Ÿ•", "pizza"], // 27 + ["๐ŸŽ‚", "cake"], // 28 + ["โค๏ธ", "heart"], // 29 + ["๐Ÿ™‚", "smiley"], // 30 + ["๐Ÿค–", "robot"], // 31 + ["๐ŸŽฉ", "hat"], // 32 + ["๐Ÿ‘“", "glasses"], // 33 + ["๐Ÿ”ง", "spanner"], // 34 + ["๐ŸŽ…", "santa"], // 35 + ["๐Ÿ‘", "thumbs up"], // 36 + ["โ˜‚๏ธ", "umbrella"], // 37 + ["โŒ›", "hourglass"], // 38 + ["โฐ", "clock"], // 39 + ["๐ŸŽ", "gift"], // 40 + ["๐Ÿ’ก", "light bulb"], // 41 + ["๐Ÿ“•", "book"], // 42 + ["โœ๏ธ", "pencil"], // 43 + ["๐Ÿ“Ž", "paperclip"], // 44 + ["โœ‚๏ธ", "scissors"], // 45 + ["๐Ÿ”’", "lock"], // 46 + ["๐Ÿ”‘", "key"], // 47 + ["๐Ÿ”จ", "hammer"], // 48 + ["โ˜Ž๏ธ", "telephone"], // 49 + ["๐Ÿ", "flag"], // 50 + ["๐Ÿš‚", "train"], // 51 + ["๐Ÿšฒ", "bicycle"], // 52 + ["โœˆ๏ธ", "aeroplane"], // 53 + ["๐Ÿš€", "rocket"], // 54 + ["๐Ÿ†", "trophy"], // 55 + ["โšฝ", "ball"], // 56 + ["๐ŸŽธ", "guitar"], // 57 + ["๐ŸŽบ", "trumpet"], // 58 + ["๐Ÿ””", "bell"], // 59 + ["โš“๏ธ", "anchor"], // 60 + ["๐ŸŽง", "headphones"], // 61 + ["๐Ÿ“", "folder"], // 62 + ["๐Ÿ“Œ", "pin"], // 63 +]; + +export function generateEmojiSas(sasBytes: number[]): EmojiMapping[] { + const emojis = [ + // just like base64 encoding + sasBytes[0] >> 2, + ((sasBytes[0] & 0x3) << 4) | (sasBytes[1] >> 4), + ((sasBytes[1] & 0xf) << 2) | (sasBytes[2] >> 6), + sasBytes[2] & 0x3f, + sasBytes[3] >> 2, + ((sasBytes[3] & 0x3) << 4) | (sasBytes[4] >> 4), + ((sasBytes[4] & 0xf) << 2) | (sasBytes[5] >> 6), + ]; + return emojis.map((num) => emojiMapping[num]); +} + +/** + * Implementation of decimal encoding of SAS as per: + * https://spec.matrix.org/v1.4/client-server-api/#sas-method-decimal + * @param sasBytes - the five bytes generated by HKDF + * @returns the derived three numbers between 1000 and 9191 inclusive + */ +export function generateDecimalSas(sasBytes: number[]): [number, number, number] { + /* + * +--------+--------+--------+--------+--------+ + * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | + * +--------+--------+--------+--------+--------+ + * bits: 87654321 87654321 87654321 87654321 87654321 + * \____________/\_____________/\____________/ + * 1st number 2nd number 3rd number + */ + return [ + ((sasBytes[0] << 5) | (sasBytes[1] >> 3)) + 1000, + (((sasBytes[1] & 0x7) << 10) | (sasBytes[2] << 2) | (sasBytes[3] >> 6)) + 1000, + (((sasBytes[3] & 0x3f) << 7) | (sasBytes[4] >> 1)) + 1000, + ]; +} diff --git a/src/matrix/verification/SAS/stages/SendKeyStage.ts b/src/matrix/verification/SAS/stages/SendKeyStage.ts index dbbe449082..f4e6d74337 100644 --- a/src/matrix/verification/SAS/stages/SendKeyStage.ts +++ b/src/matrix/verification/SAS/stages/SendKeyStage.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import anotherjson from "another-json"; +import {generateEmojiSas} from "../generator"; +import {ILogItem} from "../../../../lib"; // From element-web type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; @@ -73,45 +74,53 @@ export class SendKeyStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SendKeyStage.completeStage", async (log) => { - const event = this.previousResult["m.key.verification.key"]; - const content = event.content; - const theirKey = content.key; - const ourSasKey = this.previousResult["our_pub_key"]; - console.log("ourSasKey", ourSasKey); - const contentToSend = { - key: ourSasKey, - "m.relates_to": { - event_id: this.requestEventId, - rel_type: "m.reference", - }, - }; - await this.room.sendEvent("m.key.verification.key", contentToSend, null, log); - const keyAgreement = this.previousResult["m.key.verification.accept"].key_agreement_protocol; - const otherUserDeviceId = this.previousResult["m.key.verification.start"].content.from_device; - this.olmSAS.set_their_key(theirKey); - const sasBytes = calculateKeyAgreement[keyAgreement]({ - our: { - userId: this.ourUser.userId, - deviceId: this.ourUser.deviceId, - publicKey: ourSasKey, - }, - their: { - userId: this.otherUserId, - deviceId: otherUserDeviceId, - publicKey: theirKey, - }, - requestId: this.requestEventId, - }, this.olmSAS, 6); + this.olmSAS.set_their_key(this.theirKey); + const ourSasKey = this.olmSAS.get_pubkey(); + await this.sendKey(ourSasKey, log); + const sasBytes = this.generateSASBytes(); const emoji = generateEmojiSas(Array.from(sasBytes)); console.log("emoji", emoji); this.dispose(); }); } + private async sendKey(key: string, log: ILogItem): Promise { + const contentToSend = { + key, + "m.relates_to": { + event_id: this.requestEventId, + rel_type: "m.reference", + }, + }; + await this.room.sendEvent("m.key.verification.key", contentToSend, null, log); + } + + private generateSASBytes(): Uint8Array { + const keyAgreement = this.previousResult["m.key.verification.accept"].key_agreement_protocol; + const otherUserDeviceId = this.previousResult["m.key.verification.start"].content.from_device; + const sasBytes = calculateKeyAgreement[keyAgreement]({ + our: { + userId: this.ourUser.userId, + deviceId: this.ourUser.deviceId, + publicKey: this.olmSAS.get_pubkey(), + }, + their: { + userId: this.otherUserId, + deviceId: otherUserDeviceId, + publicKey: this.theirKey, + }, + requestId: this.requestEventId, + }, this.olmSAS, 6); + return sasBytes; + } get type() { return "m.key.verification.accept"; } + + get theirKey(): string { + return this.previousResult["m.key.verification.key"].content.key; + } } function intersection(anArray: T[], aSet: Set): T[] { @@ -128,86 +137,3 @@ function intersection(anArray: T[], aSet: Set): T[] { // } // return sas; // } - -type EmojiMapping = [emoji: string, name: string]; - -const emojiMapping: EmojiMapping[] = [ - ["๐Ÿถ", "dog"], // 0 - ["๐Ÿฑ", "cat"], // 1 - ["๐Ÿฆ", "lion"], // 2 - ["๐ŸŽ", "horse"], // 3 - ["๐Ÿฆ„", "unicorn"], // 4 - ["๐Ÿท", "pig"], // 5 - ["๐Ÿ˜", "elephant"], // 6 - ["๐Ÿฐ", "rabbit"], // 7 - ["๐Ÿผ", "panda"], // 8 - ["๐Ÿ“", "rooster"], // 9 - ["๐Ÿง", "penguin"], // 10 - ["๐Ÿข", "turtle"], // 11 - ["๐ŸŸ", "fish"], // 12 - ["๐Ÿ™", "octopus"], // 13 - ["๐Ÿฆ‹", "butterfly"], // 14 - ["๐ŸŒท", "flower"], // 15 - ["๐ŸŒณ", "tree"], // 16 - ["๐ŸŒต", "cactus"], // 17 - ["๐Ÿ„", "mushroom"], // 18 - ["๐ŸŒ", "globe"], // 19 - ["๐ŸŒ™", "moon"], // 20 - ["โ˜๏ธ", "cloud"], // 21 - ["๐Ÿ”ฅ", "fire"], // 22 - ["๐ŸŒ", "banana"], // 23 - ["๐ŸŽ", "apple"], // 24 - ["๐Ÿ“", "strawberry"], // 25 - ["๐ŸŒฝ", "corn"], // 26 - ["๐Ÿ•", "pizza"], // 27 - ["๐ŸŽ‚", "cake"], // 28 - ["โค๏ธ", "heart"], // 29 - ["๐Ÿ™‚", "smiley"], // 30 - ["๐Ÿค–", "robot"], // 31 - ["๐ŸŽฉ", "hat"], // 32 - ["๐Ÿ‘“", "glasses"], // 33 - ["๐Ÿ”ง", "spanner"], // 34 - ["๐ŸŽ…", "santa"], // 35 - ["๐Ÿ‘", "thumbs up"], // 36 - ["โ˜‚๏ธ", "umbrella"], // 37 - ["โŒ›", "hourglass"], // 38 - ["โฐ", "clock"], // 39 - ["๐ŸŽ", "gift"], // 40 - ["๐Ÿ’ก", "light bulb"], // 41 - ["๐Ÿ“•", "book"], // 42 - ["โœ๏ธ", "pencil"], // 43 - ["๐Ÿ“Ž", "paperclip"], // 44 - ["โœ‚๏ธ", "scissors"], // 45 - ["๐Ÿ”’", "lock"], // 46 - ["๐Ÿ”‘", "key"], // 47 - ["๐Ÿ”จ", "hammer"], // 48 - ["โ˜Ž๏ธ", "telephone"], // 49 - ["๐Ÿ", "flag"], // 50 - ["๐Ÿš‚", "train"], // 51 - ["๐Ÿšฒ", "bicycle"], // 52 - ["โœˆ๏ธ", "aeroplane"], // 53 - ["๐Ÿš€", "rocket"], // 54 - ["๐Ÿ†", "trophy"], // 55 - ["โšฝ", "ball"], // 56 - ["๐ŸŽธ", "guitar"], // 57 - ["๐ŸŽบ", "trumpet"], // 58 - ["๐Ÿ””", "bell"], // 59 - ["โš“๏ธ", "anchor"], // 60 - ["๐ŸŽง", "headphones"], // 61 - ["๐Ÿ“", "folder"], // 62 - ["๐Ÿ“Œ", "pin"], // 63 -]; - -function generateEmojiSas(sasBytes: number[]): EmojiMapping[] { - const emojis = [ - // just like base64 encoding - sasBytes[0] >> 2, - ((sasBytes[0] & 0x3) << 4) | (sasBytes[1] >> 4), - ((sasBytes[1] & 0xf) << 2) | (sasBytes[2] >> 6), - sasBytes[2] & 0x3f, - sasBytes[3] >> 2, - ((sasBytes[3] & 0x3) << 4) | (sasBytes[4] >> 4), - ((sasBytes[4] & 0xf) << 2) | (sasBytes[5] >> 6), - ]; - return emojis.map((num) => emojiMapping[num]); -} From ed4eb9bde0da10b01cca84da233115cee98b412a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Feb 2023 23:31:30 +0530 Subject: [PATCH 012/168] Emit event from DeviceMessageHandler --- src/matrix/DeviceMessageHandler.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/matrix/DeviceMessageHandler.js b/src/matrix/DeviceMessageHandler.js index f6e7cad7f1..af2966d7e4 100644 --- a/src/matrix/DeviceMessageHandler.js +++ b/src/matrix/DeviceMessageHandler.js @@ -17,9 +17,11 @@ limitations under the License. import {OLM_ALGORITHM} from "./e2ee/common.js"; import {countBy, groupBy} from "../utils/groupBy"; import {LRUCache} from "../utils/LRUCache"; +import {EventEmitter} from "../utils/EventEmitter"; -export class DeviceMessageHandler { +export class DeviceMessageHandler extends EventEmitter{ constructor({storage, callHandler}) { + super(); this._storage = storage; this._olmDecryption = null; this._megolmDecryption = null; @@ -39,6 +41,7 @@ export class DeviceMessageHandler { async prepareSync(toDeviceEvents, lock, txn, log) { log.set("messageTypes", countBy(toDeviceEvents, e => e.type)); const encryptedEvents = toDeviceEvents.filter(e => e.type === "m.room.encrypted"); + this._emitUnencryptedEvents(toDeviceEvents); if (!this._olmDecryption) { log.log("can't decrypt, encryption not enabled", log.level.Warn); return; @@ -74,6 +77,7 @@ export class DeviceMessageHandler { } async afterSyncCompleted(decryptionResults, deviceTracker, hsApi, log) { + this._emitEncryptedEvents(decryptionResults); 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 @@ -101,6 +105,19 @@ export class DeviceMessageHandler { } } } + + _emitUnencryptedEvents(toDeviceEvents) { + const unencryptedEvents = toDeviceEvents.filter(e => e.type !== "m.room.encrypted"); + for (const event of unencryptedEvents) { + this.emit("message", { unencrypted: event }); + } + } + + _emitEncryptedEvents(decryptionResults) { + for (const result of decryptionResults) { + this.emit("message", { encrypted: result }); + } + } } class SyncPreparation { From e46b760fb73b3a4916cc26f3374540b73dd71ece Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Feb 2023 23:32:05 +0530 Subject: [PATCH 013/168] Remove log --- src/matrix/room/timeline/Timeline.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 1850a762af..a721092e43 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -254,7 +254,6 @@ export class Timeline { /** @package */ addEntries(newEntries) { - console.log("addEntries", newEntries); this._addLocalRelationsToNewRemoteEntries(newEntries); this._updateEntriesFetchedFromHomeserver(newEntries); this._moveEntryToRemoteEntries(newEntries); From b6041cd20cfce890444da2471df0432312a43464 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Feb 2023 23:33:05 +0530 Subject: [PATCH 014/168] Channel WIP --- src/matrix/Session.js | 3 +- src/matrix/verification/CrossSigning.ts | 25 +++- .../verification/SAS/SASVerification.ts | 19 ++- .../verification/SAS/channel/Channel.ts | 127 ++++++++++++++++++ .../SAS/stages/BaseSASVerificationStage.ts | 4 + .../SAS/stages/StartVerificationStage.ts | 10 +- 6 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 src/matrix/verification/SAS/channel/Channel.ts diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 22dd0b56e6..92abb4e3fd 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -344,7 +344,8 @@ export class Session { hsApi: this._hsApi, ownUserId: this.userId, deviceId: this.deviceId, - e2eeAccount: this._e2eeAccount + e2eeAccount: this._e2eeAccount, + deviceMessageHandler: this._deviceMessageHandler, }); await log.wrap("enable cross-signing", log => { return this._crossSigning.init(log); diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 8445a38e1c..c5f227865f 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -26,6 +26,8 @@ import { ILogItem } from "../../lib"; import {pkSign} from "./common"; import type {ISignatures} from "./common"; import {SASVerification} from "./SAS/SASVerification"; +import {ToDeviceChannel} from "./SAS/channel/Channel"; +import type {DeviceMessageHandler} from "../DeviceMessageHandler.js"; type Olm = typeof OlmNamespace; @@ -39,6 +41,7 @@ export class CrossSigning { private readonly hsApi: HomeServerApi; private readonly ownUserId: string; private readonly e2eeAccount: Account; + private readonly deviceMessageHandler: DeviceMessageHandler; private _isMasterKeyTrusted: boolean = false; private readonly deviceId: string; @@ -52,7 +55,8 @@ export class CrossSigning { ownUserId: string, deviceId: string, hsApi: HomeServerApi, - e2eeAccount: Account + e2eeAccount: Account, + deviceMessageHandler: DeviceMessageHandler, }) { this.storage = options.storage; this.secretStorage = options.secretStorage; @@ -64,6 +68,7 @@ export class CrossSigning { this.ownUserId = options.ownUserId; this.deviceId = options.deviceId; this.e2eeAccount = options.e2eeAccount + this.deviceMessageHandler = options.deviceMessageHandler; } async init(log: ILogItem) { @@ -118,7 +123,23 @@ export class CrossSigning { } startVerification(room: Room, userId: string, log: ILogItem): SASVerification { - return new SASVerification(room, this.olm, this.olmUtil, { userId: this.ownUserId, deviceId: this.deviceId }, userId, log); + const channel = new ToDeviceChannel({ + deviceTracker: this.deviceTracker, + hsApi: this.hsApi, + otherUserId: userId, + platform: this.platform, + deviceMessageHandler: this.deviceMessageHandler, + }); + return new SASVerification({ + room, + platform: this.platform, + olm: this.olm, + olmUtil: this.olmUtil, + ourUser: { userId: this.ownUserId, deviceId: this.deviceId }, + otherUserId: userId, + log, + channel + }); } } diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 8d1d6ba6d2..960e63a63e 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -19,20 +19,35 @@ import {AcceptVerificationStage} from "./stages/AcceptVerificationStage"; import {SendKeyStage} from "./stages/SendKeyStage"; import type {ILogItem} from "../../../logging/types"; import type {Room} from "../../room/Room.js"; +import type {Platform} from "../../../platform/web/Platform.js"; import type {BaseSASVerificationStage, UserData} from "./stages/BaseSASVerificationStage"; import type * as OlmNamespace from "@matrix-org/olm"; +import {IChannel} from "./channel/Channel"; type Olm = typeof OlmNamespace; +type Options = { + room: Room; + platform: Platform; + olm: Olm; + olmUtil: Olm.Utility; + ourUser: UserData; + otherUserId: string; + channel: IChannel; + log: ILogItem; +} + export class SASVerification { private startStage: BaseSASVerificationStage; private olmSas: Olm.SAS; - constructor(private room: Room, private olm: Olm, private olmUtil: Olm.Utility, private ourUser: UserData, otherUserId: string, log: ILogItem) { + constructor(options: Options) { + const { room, ourUser, otherUserId, log, olmUtil, olm, channel } = options; const olmSas = new olm.SAS(); this.olmSas = olmSas; + // channel.send("m.key.verification.request", {}, log); try { - const options = { room, ourUser, otherUserId, log, olmSas, olmUtil }; + const options = { room, ourUser, otherUserId, log, olmSas, olmUtil, channel }; let stage: BaseSASVerificationStage = new StartVerificationStage(options); this.startStage = stage; diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts new file mode 100644 index 0000000000..e2de538a49 --- /dev/null +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -0,0 +1,127 @@ +/* +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 {DeviceTracker} from "../../../e2ee/DeviceTracker.js"; +import type {ILogItem} from "../../../../logging/types"; +import type {Platform} from "../../../../platform/web/Platform.js"; +import type {DeviceMessageHandler} from "../../../DeviceMessageHandler.js"; +import {makeTxnId} from "../../../common.js"; + +const enum ChannelType { + MessageEvent, + ToDeviceMessage, +} + +const enum VerificationEventTypes { + Request = "m.key.verification.request", + Ready = "m.key.verification.ready", +} + +export interface IChannel { + send(eventType: string, content: any, log: ILogItem): Promise; + waitForEvent(eventType: string): any; + type: ChannelType; +} + +type Options = { + hsApi: HomeServerApi; + deviceTracker: DeviceTracker; + otherUserId: string; + platform: Platform; + deviceMessageHandler: DeviceMessageHandler; +} + +export class ToDeviceChannel implements IChannel { + private readonly hsApi: HomeServerApi; + private readonly deviceTracker: DeviceTracker; + private readonly otherUserId: string; + private readonly platform: Platform; + private readonly deviceMessageHandler: DeviceMessageHandler; + private readonly sentMessages: Map = new Map(); + private readonly receivedMessages: Map = new Map(); + private readonly waitMap: Map}> = new Map(); + + constructor(options: Options) { + this.hsApi = options.hsApi; + this.deviceTracker = options.deviceTracker; + this.otherUserId = options.otherUserId; + this.platform = options.platform; + this.deviceMessageHandler = options.deviceMessageHandler; + // todo: find a way to dispose this subscription + this.deviceMessageHandler.on("message", ({unencrypted}) => this.handleDeviceMessage(unencrypted)) + } + + get type() { + return ChannelType.ToDeviceMessage; + } + + async send(eventType: string, content: any, log: ILogItem): Promise { + await log.wrap("ToDeviceChannel.send", async () => { + if (eventType === VerificationEventTypes.Request) { + // Handle this case specially + await this.handleRequestEventSpecially(eventType, content, log); + return; + } + }); + } + + async handleRequestEventSpecially(eventType: string, content: any, log: ILogItem) { + await log.wrap("ToDeviceChannel.handleRequestEventSpecially", async () => { + const devices = await this.deviceTracker.devicesForUsers([this.otherUserId], this.hsApi, log); + console.log("devices", devices); + const timestamp = this.platform.clock.now(); + const txnId = makeTxnId(); + Object.assign(content, { timestamp, transaction_id: txnId }); + const payload = { + messages: { + [this.otherUserId]: { + "*": content + } + } + } + this.hsApi.sendToDevice(eventType, payload, txnId, { log }); + }); + } + + handleDeviceMessage(event) { + console.log("event", event); + this.resolveAnyWaits(event); + this.receivedMessages.set(event.type, event); + } + + private resolveAnyWaits(event) { + const { type } = event; + const wait = this.waitMap.get(type); + if (wait) { + wait.resolve(event); + this.waitMap.delete(type); + } + } + + waitForEvent(eventType: string): Promise { + const existingWait = this.waitMap.get(eventType); + if (existingWait) { + return existingWait.promise; + } + let resolve; + const promise = new Promise(r => { + resolve = r; + }); + this.waitMap.set(eventType, { resolve, promise }); + return promise; + } +} diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index 9c5be61158..8d275f4cd4 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -17,6 +17,7 @@ import type {ILogItem} from "../../../../lib.js"; import type {Room} from "../../../room/Room.js"; import type * as OlmNamespace from "@matrix-org/olm"; import {Disposables} from "../../../../utils/Disposables"; +import {IChannel} from "../channel/Channel.js"; type Olm = typeof OlmNamespace; @@ -32,6 +33,7 @@ export type Options = { log: ILogItem; olmSas: Olm.SAS; olmUtil: Olm.Utility; + channel: IChannel; } export abstract class BaseSASVerificationStage extends Disposables { @@ -44,6 +46,7 @@ export abstract class BaseSASVerificationStage extends Disposables { protected requestEventId: string; protected previousResult: undefined | any; protected _nextStage: BaseSASVerificationStage; + protected channel: IChannel; constructor(options: Options) { super(); @@ -53,6 +56,7 @@ export abstract class BaseSASVerificationStage extends Disposables { this.log = options.log; this.olmSAS = options.olmSas; this.olmUtil = options.olmUtil; + this.channel = options.channel; } setRequestEventId(id: string) { diff --git a/src/matrix/verification/SAS/stages/StartVerificationStage.ts b/src/matrix/verification/SAS/stages/StartVerificationStage.ts index 175d00454f..c31346a6e6 100644 --- a/src/matrix/verification/SAS/stages/StartVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/StartVerificationStage.ts @@ -21,14 +21,16 @@ export class StartVerificationStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("StartVerificationStage.completeStage", async (log) => { const content = { - "body": `${this.ourUser.userId} is requesting to verify your device, but your client does not support verification, so you may need to use a different verification method.`, + // "body": `${this.ourUser.userId} is requesting to verify your device, but your client does not support verification, so you may need to use a different verification method.`, "from_device": this.ourUser.deviceId, "methods": ["m.sas.v1"], - "msgtype": "m.key.verification.request", - "to": this.otherUserId, + // "msgtype": "m.key.verification.request", + // "to": this.otherUserId, }; const promise = this.trackEventId(); - await this.room.sendEvent("m.room.message", content, null, log); + // await this.room.sendEvent("m.room.message", content, null, log); + await this.channel.send("m.key.verification.request", content, log); + const c = await this.channel.waitForEvent("m.key.verification.ready"); const eventId = await promise; console.log("eventId", eventId); this.setRequestEventId(eventId); From 151090527b909f18620faddf6ded729038dd8872 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 24 Feb 2023 17:45:56 +0100 Subject: [PATCH 015/168] Store cross-signing keys in format as returned from server, in separate store This will make it easier to sign and verify signatures with these keys, as the signed value needs to have the same layout when signing and for every verification. --- src/matrix/e2ee/DeviceTracker.js | 149 ++++++++++-------- src/matrix/e2ee/common.js | 13 +- src/matrix/storage/common.ts | 3 +- src/matrix/storage/idb/Transaction.ts | 5 + src/matrix/storage/idb/schema.ts | 8 +- .../idb/stores/CrossSigningKeyStore.ts | 70 ++++++++ src/matrix/verification/CrossSigning.ts | 51 +++++- 7 files changed, 220 insertions(+), 79 deletions(-) create mode 100644 src/matrix/storage/idb/stores/CrossSigningKeyStore.ts diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index b669629ea5..5d991fcb32 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {verifyEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js"; +import {verifyEd25519Signature, getEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js"; import {HistoryVisibility, shouldShareKey} from "./common.js"; import {RoomMember} from "../room/members/RoomMember.js"; +import {getKeyUsage, getKeyEd25519Key, getKeyUserId, KeyUsage} from "../verification/CrossSigning"; const TRACKING_STATUS_OUTDATED = 0; const TRACKING_STATUS_UPTODATE = 1; @@ -153,7 +154,7 @@ export class DeviceTracker { } } - async getCrossSigningKeysForUser(userId, hsApi, log) { + async getCrossSigningKeyForUser(userId, usage, hsApi, log) { return await log.wrap("DeviceTracker.getMasterKeyForUser", async log => { let txn = await this._storage.readTxn([ this._storage.storeNames.userIdentities @@ -163,13 +164,16 @@ export class DeviceTracker { return userIdentity.crossSigningKeys; } // fetch from hs - await this._queryKeys([userId], hsApi, log); - // Retreive from storage now - txn = await this._storage.readTxn([ - this._storage.storeNames.userIdentities - ]); - userIdentity = await txn.userIdentities.get(userId); - return userIdentity?.crossSigningKeys; + const keys = await this._queryKeys([userId], hsApi, log); + switch (usage) { + case KeyUsage.Master: + return keys.masterKeys.get(userId); + case KeyUsage.SelfSigning: + return keys.selfSigningKeys.get(userId); + case KeyUsage.UserSigning: + return keys.userSigningKeys.get(userId); + + } }); } @@ -245,22 +249,29 @@ export class DeviceTracker { "token": this._getSyncToken() }, {log}).response(); - const masterKeys = log.wrap("master keys", log => this._filterValidMasterKeys(deviceKeyResponse, log)); - const selfSigningKeys = log.wrap("self-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["self_signing_keys"], "self_signing", masterKeys, log)) - const verifiedKeysPerUser = log.wrap("verify", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log)); + const masterKeys = log.wrap("master keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["master_keys"], KeyUsage.Master, undefined, log)); + const selfSigningKeys = log.wrap("self-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["self_signing_keys"], KeyUsage.SelfSigning, masterKeys, log)); + const userSigningKeys = log.wrap("user-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["user_signing_keys"], KeyUsage.UserSigning, masterKeys, log)); + const verifiedKeysPerUser = log.wrap("device keys", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log)); const txn = await this._storage.readWriteTxn([ this._storage.storeNames.userIdentities, this._storage.storeNames.deviceIdentities, + this._storage.storeNames.crossSigningKeys, ]); let deviceIdentities; try { + for (const key of masterKeys.values()) { + txn.crossSigningKeys.set(key); + } + for (const key of selfSigningKeys.values()) { + txn.crossSigningKeys.set(key); + } + for (const key of userSigningKeys.values()) { + txn.crossSigningKeys.set(key); + } const devicesIdentitiesPerUser = await Promise.all(verifiedKeysPerUser.map(async ({userId, verifiedKeys}) => { const deviceIdentities = verifiedKeys.map(deviceKeysAsDeviceIdentity); - const crossSigningKeys = { - masterKey: masterKeys.get(userId), - selfSigningKey: selfSigningKeys.get(userId), - }; - return await this._storeQueriedDevicesForUserId(userId, crossSigningKeys, deviceIdentities, txn); + return await this._storeQueriedDevicesForUserId(userId, deviceIdentities, txn); })); deviceIdentities = devicesIdentitiesPerUser.reduce((all, devices) => all.concat(devices), []); log.set("devices", deviceIdentities.length); @@ -269,10 +280,15 @@ export class DeviceTracker { throw err; } await txn.complete(); - return deviceIdentities; + return { + deviceIdentities, + masterKeys, + selfSigningKeys, + userSigningKeys + }; } - async _storeQueriedDevicesForUserId(userId, crossSigningKeys, deviceIdentities, txn) { + async _storeQueriedDevicesForUserId(userId, deviceIdentities, txn) { const knownDeviceIds = await txn.deviceIdentities.getAllDeviceIds(userId); // delete any devices that we know off but are not in the response anymore. // important this happens before checking if the ed25519 key changed, @@ -313,65 +329,61 @@ export class DeviceTracker { identity = createUserIdentity(userId); } identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE; - identity.crossSigningKeys = crossSigningKeys; txn.userIdentities.set(identity); return allDeviceIdentities; } - _filterValidMasterKeys(keyQueryResponse, log) { - const masterKeys = new Map(); - const masterKeysResponse = keyQueryResponse["master_keys"]; - if (!masterKeysResponse) { - return masterKeys; - } - const validMasterKeyResponses = Object.entries(masterKeysResponse).filter(([userId, keyInfo]) => { - if (keyInfo["user_id"] !== userId) { - return false; - } - if (!Array.isArray(keyInfo.usage) || !keyInfo.usage.includes("master")) { - return false; - } - return true; - }); - validMasterKeyResponses.reduce((msks, [userId, keyInfo]) => { - const keyIds = Object.keys(keyInfo.keys); - if (keyIds.length !== 1) { - return false; - } - const masterKey = keyInfo.keys[keyIds[0]]; - msks.set(userId, masterKey); - return msks; - }, masterKeys); - return masterKeys; - } - - _filterVerifiedCrossSigningKeys(crossSigningKeysResponse, usage, masterKeys, log) { + _filterVerifiedCrossSigningKeys(crossSigningKeysResponse, usage, parentKeys, log) { const keys = new Map(); if (!crossSigningKeysResponse) { return keys; } - const validKeysResponses = Object.entries(crossSigningKeysResponse).filter(([userId, keyInfo]) => { - if (keyInfo["user_id"] !== userId) { - return false; - } - if (!Array.isArray(keyInfo.usage) || !keyInfo.usage.includes(usage)) { - return false; - } - // verify with master key - const masterKey = masterKeys.get(userId); - return verifyEd25519Signature(this._olmUtil, userId, masterKey, masterKey, keyInfo, log); - }); - validKeysResponses.reduce((keys, [userId, keyInfo]) => { - const keyIds = Object.keys(keyInfo.keys); - if (keyIds.length !== 1) { + for (const [userId, keyInfo] of Object.entries(crossSigningKeysResponse)) { + log.wrap({l: userId}, log => { + const parentKeyInfo = parentKeys?.get(userId); + const parentKey = parentKeyInfo && getKeyEd25519Key(parentKeyInfo); + if (this._validateCrossSigningKey(userId, keyInfo, usage, parentKey, log)) { + keys.set(getKeyUserId(keyInfo), keyInfo); + } + }); + } + return keys; + } + + _validateCrossSigningKey(userId, keyInfo, usage, parentKey, log) { + if (getKeyUserId(keyInfo) !== userId) { + log.log({l: "user_id mismatch", userId: keyInfo["user_id"]}); + return false; + } + if (getKeyUsage(keyInfo) !== usage) { + log.log({l: "usage mismatch", usage: keyInfo.usage}); + return false; + } + const publicKey = getKeyEd25519Key(keyInfo); + if (!publicKey) { + log.log({l: "no ed25519 key", keys: keyInfo.keys}); + return false; + } + const isSelfSigned = usage === "master"; + const keyToVerifyWith = isSelfSigned ? publicKey : parentKey; + if (!keyToVerifyWith) { + log.log("signing_key not found"); + return false; + } + const hasSignature = !!getEd25519Signature(keyInfo, userId, keyToVerifyWith); + // self-signature is optional for now, not all keys seem to have it + if (!hasSignature && keyToVerifyWith !== publicKey) { + log.log({l: "signature not found", key: keyToVerifyWith}); + return false; + } + if (hasSignature) { + if(!verifyEd25519Signature(this._olmUtil, userId, keyToVerifyWith, keyToVerifyWith, keyInfo, log)) { + log.log("signature mismatch"); return false; } - const key = keyInfo.keys[keyIds[0]]; - keys.set(userId, key); - return keys; - }, keys); - return keys; + } + return true; } /** @@ -580,7 +592,8 @@ export class DeviceTracker { // TODO: ignore the race between /sync and /keys/query for now, // where users could get marked as outdated or added/removed from the room while // querying keys - queriedDevices = await this._queryKeys(outdatedUserIds, hsApi, log); + const {deviceIdentities} = await this._queryKeys(outdatedUserIds, hsApi, log); + queriedDevices = deviceIdentities; } const deviceTxn = await this._storage.readTxn([ diff --git a/src/matrix/e2ee/common.js b/src/matrix/e2ee/common.js index cc3bfff5f9..9c5fe66c95 100644 --- a/src/matrix/e2ee/common.js +++ b/src/matrix/e2ee/common.js @@ -35,16 +35,21 @@ export class DecryptionError extends Error { export const SIGNATURE_ALGORITHM = "ed25519"; +export function getEd25519Signature(signedValue, userId, deviceOrKeyId) { + return signedValue?.signatures?.[userId]?.[`${SIGNATURE_ALGORITHM}:${deviceOrKeyId}`]; +} + export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Key, value, log = undefined) { + const signature = getEd25519Signature(value, userId, deviceOrKeyId); + if (!signature) { + log?.set("no_signature", true); + return false; + } const clone = Object.assign({}, value); delete clone.unsigned; delete clone.signatures; const canonicalJson = anotherjson.stringify(clone); - const signature = value?.signatures?.[userId]?.[`${SIGNATURE_ALGORITHM}:${deviceOrKeyId}`]; try { - if (!signature) { - throw new Error("no signature"); - } // throws when signature is invalid olmUtil.ed25519_verify(ed25519Key, canonicalJson, signature); return true; diff --git a/src/matrix/storage/common.ts b/src/matrix/storage/common.ts index e1e3491725..adebcdd65f 100644 --- a/src/matrix/storage/common.ts +++ b/src/matrix/storage/common.ts @@ -33,7 +33,8 @@ export enum StoreNames { groupSessionDecryptions = "groupSessionDecryptions", operations = "operations", accountData = "accountData", - calls = "calls" + calls = "calls", + crossSigningKeys = "crossSigningKeys" } 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 7a8de420be..532ffd1d27 100644 --- a/src/matrix/storage/idb/Transaction.ts +++ b/src/matrix/storage/idb/Transaction.ts @@ -30,6 +30,7 @@ import {TimelineFragmentStore} from "./stores/TimelineFragmentStore"; import {PendingEventStore} from "./stores/PendingEventStore"; import {UserIdentityStore} from "./stores/UserIdentityStore"; import {DeviceIdentityStore} from "./stores/DeviceIdentityStore"; +import {CrossSigningKeyStore} from "./stores/CrossSigningKeyStore"; import {OlmSessionStore} from "./stores/OlmSessionStore"; import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore"; import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore"; @@ -145,6 +146,10 @@ export class Transaction { return this._store(StoreNames.deviceIdentities, idbStore => new DeviceIdentityStore(idbStore)); } + get crossSigningKeys(): CrossSigningKeyStore { + return this._store(StoreNames.crossSigningKeys, idbStore => new CrossSigningKeyStore(idbStore)); + } + get olmSessions(): OlmSessionStore { return this._store(StoreNames.olmSessions, idbStore => new OlmSessionStore(idbStore)); } diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index d88f535e98..3d1e714f33 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -34,7 +34,8 @@ export const schema: MigrationFunc[] = [ clearAllStores, addInboundSessionBackupIndex, migrateBackupStatus, - createCallStore + createCallStore, + createCrossSigningKeyStore ]; // TODO: how to deal with git merge conflicts of this array? @@ -275,3 +276,8 @@ async function migrateBackupStatus(db: IDBDatabase, txn: IDBTransaction, localSt function createCallStore(db: IDBDatabase) : void { db.createObjectStore("calls", {keyPath: "key"}); } + +//v18 create calls store +function createCrossSigningKeyStore(db: IDBDatabase) : void { + db.createObjectStore("crossSigningKeys", {keyPath: "key"}); +} diff --git a/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts b/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts new file mode 100644 index 0000000000..a2fa9ecbd1 --- /dev/null +++ b/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts @@ -0,0 +1,70 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {MAX_UNICODE, MIN_UNICODE} from "./common"; +import {Store} from "../Store"; + +// we store cross-signing keys in the format we get them from the server +// as that is what the signature is calculated on, so to verify, we need +// it in this format anyway. +export type CrossSigningKey = { + readonly user_id: string; + readonly usage: ReadonlyArray; + readonly keys: {[keyId: string]: string}; + readonly signatures: {[userId: string]: {[keyId: string]: string}} +} + +type CrossSigningKeyEntry = CrossSigningKey & { + key: string; // key in storage, not a crypto key +} + +function encodeKey(userId: string, usage: string): string { + return `${userId}|${usage}`; +} + +function decodeKey(key: string): { userId: string, usage: string } { + const [userId, usage] = key.split("|"); + return {userId, usage}; +} + +export class CrossSigningKeyStore { + private _store: Store; + + constructor(store: Store) { + this._store = store; + } + + get(userId: string, deviceId: string): Promise { + return this._store.get(encodeKey(userId, deviceId)); + } + + set(crossSigningKey: CrossSigningKey): void { + const deviceIdentityEntry = crossSigningKey as CrossSigningKeyEntry; + deviceIdentityEntry.key = encodeKey(crossSigningKey["user_id"], crossSigningKey.usage[0]); + this._store.put(deviceIdentityEntry); + } + + remove(userId: string, usage: string): void { + this._store.delete(encodeKey(userId, usage)); + } + + removeAllForUser(userId: string): void { + // exclude both keys as they are theoretical min and max, + // but we should't have a match for just the room id, or room id with max + const range = this._store.IDBKeyRange.bound(encodeKey(userId, MIN_UNICODE), encodeKey(userId, MAX_UNICODE), true, true); + this._store.delete(range); + } +} diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index db480dd08a..d3b6bc904a 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -27,6 +27,12 @@ import type {ISignatures} from "./common"; type Olm = typeof OlmNamespace; +export enum KeyUsage { + Master = "master", + SelfSigning = "self_signing", + UserSigning = "user_signing" +}; + export class CrossSigning { private readonly storage: Storage; private readonly secretStorage: SecretStorage; @@ -72,9 +78,9 @@ export class CrossSigning { } finally { signing.free(); } - const publishedKeys = await this.deviceTracker.getCrossSigningKeysForUser(this.ownUserId, this.hsApi, log); - log.set({publishedMasterKey: publishedKeys.masterKey, derivedPublicKey}); - this._isMasterKeyTrusted = publishedKeys.masterKey === derivedPublicKey; + const masterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log); + log.set({publishedMasterKey: masterKey, derivedPublicKey}); + this._isMasterKeyTrusted = masterKey === derivedPublicKey; log.set("isMasterKeyTrusted", this.isMasterKeyTrusted); }); } @@ -86,7 +92,7 @@ export class CrossSigning { return; } const deviceKey = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning(); - const signedDeviceKey = await this.signDevice(deviceKey); + const signedDeviceKey = await this.signDeviceData(deviceKey); const payload = { [signedDeviceKey["user_id"]]: { [signedDeviceKey["device_id"]]: signedDeviceKey @@ -97,7 +103,15 @@ export class CrossSigning { }); } - private async signDevice(data: T): Promise { + signDevice(deviceId: string) { + // need to get the device key for the device + } + + signUser(userId: string) { + // need to be able to get the msk for the user + } + + private async signDeviceData(data: T): Promise { const txn = await this.storage.readTxn([this.storage.storeNames.accountData]); const seedStr = await this.secretStorage.readSecret(`m.cross_signing.self_signing`, txn); const seed = new Uint8Array(this.platform.encoding.base64.decode(seedStr)); @@ -110,3 +124,30 @@ export class CrossSigning { } } +export function getKeyUsage(keyInfo): KeyUsage | undefined { + if (!Array.isArray(keyInfo.usage) || keyInfo.usage.length !== 1) { + return undefined; + } + const usage = keyInfo.usage[0]; + if (usage !== KeyUsage.Master && usage !== KeyUsage.SelfSigning && usage !== KeyUsage.UserSigning) { + return undefined; + } + return usage; +} + +const algorithm = "ed25519"; +const prefix = `${algorithm}:`; + +export function getKeyEd25519Key(keyInfo): string | undefined { + const ed25519KeyIds = Object.keys(keyInfo.keys).filter(keyId => keyId.startsWith(prefix)); + if (ed25519KeyIds.length !== 1) { + return undefined; + } + const keyId = ed25519KeyIds[0]; + const publicKey = keyInfo.keys[keyId]; + return publicKey; +} + +export function getKeyUserId(keyInfo): string | undefined { + return keyInfo["user_id"]; +} From b8fb2b6df10d5eb5898bd7375f9442d9a7ed9222 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 27 Feb 2023 18:13:53 +0100 Subject: [PATCH 016/168] Store device keys in format needed to sign/verify, convert to TS In order to sign and verify signatures of design keys, we need to have them in the format as they are uploaded and downloaded from the homeserver. So, like the cross-signing keys, we store them in locally in the same format to avoid constant convertions. I also renamed deviceIdentities to deviceKeys, analogue to crossSigningKeys. In order to prevent mistakes in this refactor, I also converted DeviceTracker to typescript. --- src/matrix/Sync.js | 4 +- src/matrix/e2ee/Account.js | 2 +- src/matrix/e2ee/DecryptionResult.ts | 13 +- .../{DeviceTracker.js => DeviceTracker.ts} | 423 ++++++++++-------- src/matrix/e2ee/RoomEncryption.js | 2 +- src/matrix/e2ee/{common.js => common.ts} | 69 ++- src/matrix/e2ee/megolm/keybackup/types.ts | 2 +- src/matrix/e2ee/olm/Encryption.ts | 54 +-- src/matrix/e2ee/olm/types.ts | 4 +- src/matrix/room/BaseRoom.js | 2 +- src/matrix/room/RoomBeingCreated.ts | 4 +- src/matrix/room/common.ts | 2 + src/matrix/storage/common.ts | 2 +- src/matrix/storage/idb/Transaction.ts | 6 +- src/matrix/storage/idb/schema.ts | 9 +- .../idb/stores/CrossSigningKeyStore.ts | 8 +- ...viceIdentityStore.ts => DeviceKeyStore.ts} | 47 +- 17 files changed, 360 insertions(+), 293 deletions(-) rename src/matrix/e2ee/{DeviceTracker.js => DeviceTracker.ts} (71%) rename src/matrix/e2ee/{common.js => common.ts} (57%) rename src/matrix/storage/idb/stores/{DeviceIdentityStore.ts => DeviceKeyStore.ts} (63%) diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index d335336d29..4fb48713c6 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -218,7 +218,7 @@ export class Sync { _openPrepareSyncTxn() { const storeNames = this._storage.storeNames; return this._storage.readTxn([ - storeNames.deviceIdentities, // to read device from olm messages + storeNames.deviceKeys, // to read device from olm messages storeNames.olmSessions, storeNames.inboundGroupSessions, // to read fragments when loading sync writer when rejoining archived room @@ -329,7 +329,7 @@ export class Sync { storeNames.pendingEvents, storeNames.userIdentities, storeNames.groupSessionDecryptions, - storeNames.deviceIdentities, + storeNames.deviceKeys, // to discard outbound session when somebody leaves a room // and to create room key messages when somebody joins storeNames.outboundGroupSessions, diff --git a/src/matrix/e2ee/Account.js b/src/matrix/e2ee/Account.js index 0238f0cf91..b0dd15461e 100644 --- a/src/matrix/e2ee/Account.js +++ b/src/matrix/e2ee/Account.js @@ -15,7 +15,7 @@ limitations under the License. */ import anotherjson from "another-json"; -import {SESSION_E2EE_KEY_PREFIX, OLM_ALGORITHM, MEGOLM_ALGORITHM} from "./common.js"; +import {SESSION_E2EE_KEY_PREFIX, OLM_ALGORITHM, MEGOLM_ALGORITHM} from "./common"; // use common prefix so it's easy to clear properties that are not e2ee related during session clear const ACCOUNT_SESSION_KEY = SESSION_E2EE_KEY_PREFIX + "olmAccount"; diff --git a/src/matrix/e2ee/DecryptionResult.ts b/src/matrix/e2ee/DecryptionResult.ts index 83ad7a1efb..146a1ad3be 100644 --- a/src/matrix/e2ee/DecryptionResult.ts +++ b/src/matrix/e2ee/DecryptionResult.ts @@ -26,7 +26,8 @@ limitations under the License. * see DeviceTracker */ -import type {DeviceIdentity} from "../storage/idb/stores/DeviceIdentityStore"; +import {getDeviceEd25519Key} from "./common"; +import type {DeviceKey} from "./common"; import type {TimelineEvent} from "../storage/types"; type DecryptedEvent = { @@ -35,7 +36,7 @@ type DecryptedEvent = { } export class DecryptionResult { - private device?: DeviceIdentity; + private device?: DeviceKey; constructor( public readonly event: DecryptedEvent, @@ -44,13 +45,13 @@ export class DecryptionResult { public readonly encryptedEvent?: TimelineEvent ) {} - setDevice(device: DeviceIdentity): void { + setDevice(device: DeviceKey): void { this.device = device; } get isVerified(): boolean { if (this.device) { - const comesFromDevice = this.device.ed25519Key === this.claimedEd25519Key; + const comesFromDevice = getDeviceEd25519Key(this.device) === this.claimedEd25519Key; return comesFromDevice; } return false; @@ -65,11 +66,11 @@ export class DecryptionResult { } get userId(): string | undefined { - return this.device?.userId; + return this.device?.user_id; } get deviceId(): string | undefined { - return this.device?.deviceId; + return this.device?.device_id; } get isVerificationUnknown(): boolean { diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.ts similarity index 71% rename from src/matrix/e2ee/DeviceTracker.js rename to src/matrix/e2ee/DeviceTracker.ts index 5d991fcb32..c8e9df096f 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -15,23 +15,38 @@ limitations under the License. */ import {verifyEd25519Signature, getEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js"; -import {HistoryVisibility, shouldShareKey} from "./common.js"; +import {HistoryVisibility, shouldShareKey, DeviceKey, getDeviceEd25519Key, getDeviceCurve25519Key} from "./common"; import {RoomMember} from "../room/members/RoomMember.js"; import {getKeyUsage, getKeyEd25519Key, getKeyUserId, KeyUsage} from "../verification/CrossSigning"; +import {MemberChange} from "../room/members/RoomMember"; +import type {CrossSigningKey} from "../storage/idb/stores/CrossSigningKeyStore"; +import type {HomeServerApi} from "../net/HomeServerApi"; +import type {ObservableMap} from "../../observable/map"; +import type {Room} from "../room/Room"; +import type {ILogItem} from "../../logging/types"; +import type {Storage} from "../storage/idb/Storage"; +import type {Transaction} from "../storage/idb/Transaction"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; const TRACKING_STATUS_OUTDATED = 0; const TRACKING_STATUS_UPTODATE = 1; -function createUserIdentity(userId, initialRoomId = undefined) { +export type UserIdentity = { + userId: string, + roomIds: string[], + deviceTrackingStatus: number, +} + +function createUserIdentity(userId: string, initialRoomId?: string): UserIdentity { return { userId: userId, roomIds: initialRoomId ? [initialRoomId] : [], - crossSigningKeys: undefined, deviceTrackingStatus: TRACKING_STATUS_OUTDATED, }; } -function addRoomToIdentity(identity, userId, roomId) { +function addRoomToIdentity(identity: UserIdentity | undefined, userId: string, roomId: string): UserIdentity | undefined { if (!identity) { identity = createUserIdentity(userId, roomId); return identity; @@ -43,31 +58,22 @@ function addRoomToIdentity(identity, userId, roomId) { } } -// map 1 device from /keys/query response to DeviceIdentity -function deviceKeysAsDeviceIdentity(deviceSection) { - const deviceId = deviceSection["device_id"]; - const userId = deviceSection["user_id"]; - return { - userId, - deviceId, - ed25519Key: deviceSection.keys[`ed25519:${deviceId}`], - curve25519Key: deviceSection.keys[`curve25519:${deviceId}`], - algorithms: deviceSection.algorithms, - displayName: deviceSection.unsigned?.device_display_name, - }; -} - export class DeviceTracker { - constructor({storage, getSyncToken, olmUtil, ownUserId, ownDeviceId}) { - this._storage = storage; - this._getSyncToken = getSyncToken; - this._identityChangedForRoom = null; - this._olmUtil = olmUtil; - this._ownUserId = ownUserId; - this._ownDeviceId = ownDeviceId; + private readonly _storage: Storage; + private readonly _getSyncToken: () => string; + private readonly _olmUtil: Olm.Utility; + private readonly _ownUserId: string; + private readonly _ownDeviceId: string; + + constructor(options: {storage: Storage, getSyncToken: () => string, olmUtil: Olm.Utility, ownUserId: string, ownDeviceId: string}) { + this._storage = options.storage; + this._getSyncToken = options.getSyncToken; + this._olmUtil = options.olmUtil; + this._ownUserId = options.ownUserId; + this._ownDeviceId = options.ownDeviceId; } - async writeDeviceChanges(changed, txn, log) { + async writeDeviceChanges(changedUserIds: ReadonlyArray, txn: Transaction, log: ILogItem): Promise { const {userIdentities} = txn; // TODO: should we also look at left here to handle this?: // the usual problem here is that you share a room with a user, @@ -76,8 +82,8 @@ export class DeviceTracker { // At which point you come online, all of this happens in the gap, // and you don't notice that they ever left, // and so the client doesn't invalidate their device cache for the user - log.set("changed", changed.length); - await Promise.all(changed.map(async userId => { + log.set("changed", changedUserIds.length); + await Promise.all(changedUserIds.map(async userId => { const user = await userIdentities.get(userId); if (user) { log.log({l: "outdated", id: userId}); @@ -90,9 +96,9 @@ export class DeviceTracker { /** @return Promise<{added: string[], removed: string[]}> the user ids for who the room was added or removed to the userIdentity, * and with who a key should be now be shared **/ - async writeMemberChanges(room, memberChanges, historyVisibility, txn) { - const added = []; - const removed = []; + async writeMemberChanges(room: Room, memberChanges: Map, historyVisibility: HistoryVisibility, txn: Transaction): Promise<{added: string[], removed: string[]}> { + const added: string[] = []; + const removed: string[] = []; await Promise.all(Array.from(memberChanges.values()).map(async memberChange => { // keys should now be shared with this member? // add the room to the userIdentity if so @@ -118,7 +124,7 @@ export class DeviceTracker { return {added, removed}; } - async trackRoom(room, historyVisibility, log) { + async trackRoom(room: Room, historyVisibility: HistoryVisibility, log: ILogItem): Promise { if (room.isTrackingMembers || !room.isEncrypted) { return; } @@ -126,13 +132,13 @@ export class DeviceTracker { const txn = await this._storage.readWriteTxn([ this._storage.storeNames.roomSummary, this._storage.storeNames.userIdentities, - this._storage.storeNames.deviceIdentities, // to remove all devices in _removeRoomFromUserIdentity + this._storage.storeNames.deviceKeys, // to remove all devices in _removeRoomFromUserIdentity ]); try { let isTrackingChanges; try { isTrackingChanges = room.writeIsTrackingMembers(true, txn); - const members = Array.from(memberList.members.values()); + const members = Array.from((memberList.members as ObservableMap).values()); log.set("members", members.length); // TODO: should we remove any userIdentities we should not share the key with?? // e.g. as an extra security measure if we had a mistake in other code? @@ -154,14 +160,15 @@ export class DeviceTracker { } } - async getCrossSigningKeyForUser(userId, usage, hsApi, log) { - return await log.wrap("DeviceTracker.getMasterKeyForUser", async log => { + async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi, log: ILogItem) { + return await log.wrap({l: "DeviceTracker.getCrossSigningKeyForUser", id: userId, usage}, async log => { let txn = await this._storage.readTxn([ - this._storage.storeNames.userIdentities + this._storage.storeNames.userIdentities, + this._storage.storeNames.crossSigningKeys, ]); let userIdentity = await txn.userIdentities.get(userId); if (userIdentity && userIdentity.deviceTrackingStatus !== TRACKING_STATUS_OUTDATED) { - return userIdentity.crossSigningKeys; + return await txn.crossSigningKeys.get(userId, usage); } // fetch from hs const keys = await this._queryKeys([userId], hsApi, log); @@ -172,19 +179,19 @@ export class DeviceTracker { return keys.selfSigningKeys.get(userId); case KeyUsage.UserSigning: return keys.userSigningKeys.get(userId); - } }); } - async writeHistoryVisibility(room, historyVisibility, syncTxn, log) { - const added = []; - const removed = []; + async writeHistoryVisibility(room: Room, historyVisibility: HistoryVisibility, syncTxn: Transaction, log: ILogItem): Promise<{added: string[], removed: string[]}> { + const added: string[] = []; + const removed: string[] = []; if (room.isTrackingMembers && room.isEncrypted) { await log.wrap("rewriting userIdentities", async log => { + // TODO: how do we know that we won't fetch the members from the server here and hence close the syncTxn? const memberList = await room.loadMemberList(syncTxn, log); try { - const members = Array.from(memberList.members.values()); + const members = Array.from((memberList.members as ObservableMap).values()); log.set("members", members.length); await Promise.all(members.map(async member => { if (shouldShareKey(member.membership, historyVisibility)) { @@ -205,7 +212,7 @@ export class DeviceTracker { return {added, removed}; } - async _addRoomToUserIdentity(roomId, userId, txn) { + async _addRoomToUserIdentity(roomId: string, userId: string, txn: Transaction): Promise { const {userIdentities} = txn; const identity = await userIdentities.get(userId); const updatedIdentity = addRoomToIdentity(identity, userId, roomId); @@ -216,15 +223,15 @@ export class DeviceTracker { return false; } - async _removeRoomFromUserIdentity(roomId, userId, txn) { - const {userIdentities, deviceIdentities} = txn; + async _removeRoomFromUserIdentity(roomId: string, userId: string, txn: Transaction): Promise { + const {userIdentities, deviceKeys} = txn; const identity = await userIdentities.get(userId); if (identity) { identity.roomIds = identity.roomIds.filter(id => id !== roomId); // no more encrypted rooms with this user, remove if (identity.roomIds.length === 0) { userIdentities.remove(userId); - deviceIdentities.removeAllForUser(userId); + deviceKeys.removeAllForUser(userId); } else { userIdentities.set(identity); } @@ -233,7 +240,12 @@ export class DeviceTracker { return false; } - async _queryKeys(userIds, hsApi, log) { + async _queryKeys(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise<{ + deviceKeys: Map, + masterKeys: Map, + selfSigningKeys: Map, + userSigningKeys: Map + }> { // TODO: we need to handle the race here between /sync and /keys/query just like we need to do for the member list ... // there are multiple requests going out for /keys/query though and only one for /members // So, while doing /keys/query, writeDeviceChanges should add userIds marked as outdated to a list @@ -252,10 +264,10 @@ export class DeviceTracker { const masterKeys = log.wrap("master keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["master_keys"], KeyUsage.Master, undefined, log)); const selfSigningKeys = log.wrap("self-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["self_signing_keys"], KeyUsage.SelfSigning, masterKeys, log)); const userSigningKeys = log.wrap("user-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["user_signing_keys"], KeyUsage.UserSigning, masterKeys, log)); - const verifiedKeysPerUser = log.wrap("device keys", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log)); + const deviceKeys = log.wrap("device keys", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log)); const txn = await this._storage.readWriteTxn([ this._storage.storeNames.userIdentities, - this._storage.storeNames.deviceIdentities, + this._storage.storeNames.deviceKeys, this._storage.storeNames.crossSigningKeys, ]); let deviceIdentities; @@ -269,54 +281,59 @@ export class DeviceTracker { for (const key of userSigningKeys.values()) { txn.crossSigningKeys.set(key); } - const devicesIdentitiesPerUser = await Promise.all(verifiedKeysPerUser.map(async ({userId, verifiedKeys}) => { - const deviceIdentities = verifiedKeys.map(deviceKeysAsDeviceIdentity); - return await this._storeQueriedDevicesForUserId(userId, deviceIdentities, txn); + let totalCount = 0; + await Promise.all(Array.from(deviceKeys.keys()).map(async (userId) => { + let deviceKeysForUser = deviceKeys.get(userId)!; + totalCount += deviceKeysForUser.length; + // check for devices that changed their keys and keep the old key + deviceKeysForUser = await this._storeQueriedDevicesForUserId(userId, deviceKeysForUser, txn); + deviceKeys.set(userId, deviceKeysForUser); })); - deviceIdentities = devicesIdentitiesPerUser.reduce((all, devices) => all.concat(devices), []); - log.set("devices", deviceIdentities.length); + log.set("devices", totalCount); } catch (err) { txn.abort(); throw err; } await txn.complete(); return { - deviceIdentities, + deviceKeys, masterKeys, selfSigningKeys, userSigningKeys }; } - async _storeQueriedDevicesForUserId(userId, deviceIdentities, txn) { - const knownDeviceIds = await txn.deviceIdentities.getAllDeviceIds(userId); + async _storeQueriedDevicesForUserId(userId: string, deviceKeys: DeviceKey[], txn: Transaction): Promise { + // TODO: we should obsolete (flag) the device keys that have been removed, + // but keep them to verify messages encrypted with it? + const knownDeviceIds = await txn.deviceKeys.getAllDeviceIds(userId); // delete any devices that we know off but are not in the response anymore. // important this happens before checking if the ed25519 key changed, // otherwise we would end up deleting existing devices with changed keys. for (const deviceId of knownDeviceIds) { - if (deviceIdentities.every(di => di.deviceId !== deviceId)) { - txn.deviceIdentities.remove(userId, deviceId); + if (deviceKeys.every(di => di.device_id !== deviceId)) { + txn.deviceKeys.remove(userId, deviceId); } } // all the device identities as we will have them in storage - const allDeviceIdentities = []; - const deviceIdentitiesToStore = []; + const allDeviceKeys: DeviceKey[] = []; + const deviceKeysToStore: DeviceKey[] = []; // filter out devices that have changed their ed25519 key since last time we queried them - await Promise.all(deviceIdentities.map(async deviceIdentity => { - if (knownDeviceIds.includes(deviceIdentity.deviceId)) { - const existingDevice = await txn.deviceIdentities.get(deviceIdentity.userId, deviceIdentity.deviceId); - if (existingDevice.ed25519Key !== deviceIdentity.ed25519Key) { - allDeviceIdentities.push(existingDevice); + await Promise.all(deviceKeys.map(async deviceKey => { + if (knownDeviceIds.includes(deviceKey.device_id)) { + const existingDevice = await txn.deviceKeys.get(deviceKey.user_id, deviceKey.device_id); + if (existingDevice && getDeviceEd25519Key(existingDevice) !== getDeviceEd25519Key(deviceKey)) { + allDeviceKeys.push(existingDevice); return; } } - allDeviceIdentities.push(deviceIdentity); - deviceIdentitiesToStore.push(deviceIdentity); + allDeviceKeys.push(deviceKey); + deviceKeysToStore.push(deviceKey); })); // store devices - for (const deviceIdentity of deviceIdentitiesToStore) { - txn.deviceIdentities.set(deviceIdentity); + for (const deviceKey of deviceKeysToStore) { + txn.deviceKeys.set(deviceKey); } // mark user identities as up to date let identity = await txn.userIdentities.get(userId); @@ -331,11 +348,11 @@ export class DeviceTracker { identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE; txn.userIdentities.set(identity); - return allDeviceIdentities; + return allDeviceKeys; } - _filterVerifiedCrossSigningKeys(crossSigningKeysResponse, usage, parentKeys, log) { - const keys = new Map(); + _filterVerifiedCrossSigningKeys(crossSigningKeysResponse: {[userId: string]: CrossSigningKey}, usage, parentKeys: Map | undefined, log): Map { + const keys: Map = new Map(); if (!crossSigningKeysResponse) { return keys; } @@ -344,14 +361,14 @@ export class DeviceTracker { const parentKeyInfo = parentKeys?.get(userId); const parentKey = parentKeyInfo && getKeyEd25519Key(parentKeyInfo); if (this._validateCrossSigningKey(userId, keyInfo, usage, parentKey, log)) { - keys.set(getKeyUserId(keyInfo), keyInfo); + keys.set(getKeyUserId(keyInfo)!, keyInfo); } }); } return keys; } - _validateCrossSigningKey(userId, keyInfo, usage, parentKey, log) { + _validateCrossSigningKey(userId: string, keyInfo: CrossSigningKey, usage: KeyUsage, parentKey: string | undefined, log: ILogItem): boolean { if (getKeyUserId(keyInfo) !== userId) { log.log({l: "user_id mismatch", userId: keyInfo["user_id"]}); return false; @@ -389,51 +406,67 @@ export class DeviceTracker { /** * @return {Array<{userId, verifiedKeys: Array>} */ - _filterVerifiedDeviceKeys(keyQueryDeviceKeysResponse, parentLog) { - const curve25519Keys = new Set(); - const verifiedKeys = Object.entries(keyQueryDeviceKeysResponse).map(([userId, keysByDevice]) => { - const verifiedEntries = Object.entries(keysByDevice).filter(([deviceId, deviceKeys]) => { - const deviceIdOnKeys = deviceKeys["device_id"]; - const userIdOnKeys = deviceKeys["user_id"]; - if (userIdOnKeys !== userId) { - return false; - } - if (deviceIdOnKeys !== deviceId) { - return false; - } - const ed25519Key = deviceKeys.keys?.[`ed25519:${deviceId}`]; - const curve25519Key = deviceKeys.keys?.[`curve25519:${deviceId}`]; - if (typeof ed25519Key !== "string" || typeof curve25519Key !== "string") { - return false; - } - if (curve25519Keys.has(curve25519Key)) { - parentLog.log({ - l: "ignore device with duplicate curve25519 key", - keys: deviceKeys - }, parentLog.level.Warn); - return false; - } - curve25519Keys.add(curve25519Key); - const isValid = this._hasValidSignature(deviceKeys, parentLog); - if (!isValid) { - parentLog.log({ - l: "ignore device with invalid signature", - keys: deviceKeys - }, parentLog.level.Warn); - } - return isValid; + _filterVerifiedDeviceKeys( + keyQueryDeviceKeysResponse: {[userId: string]: {[deviceId: string]: DeviceKey}}, + parentLog: ILogItem + ): Map { + const curve25519Keys: Set = new Set(); + const keys: Map = new Map(); + if (!keyQueryDeviceKeysResponse) { + return keys; + } + for (const [userId, keysByDevice] of Object.entries(keyQueryDeviceKeysResponse)) { + parentLog.wrap(userId, log => { + const verifiedEntries = Object.entries(keysByDevice).filter(([deviceId, deviceKey]) => { + return log.wrap(deviceId, log => { + if (this._validateDeviceKey(userId, deviceId, deviceKey, log)) { + const curve25519Key = getDeviceCurve25519Key(deviceKey); + if (curve25519Keys.has(curve25519Key)) { + parentLog.log({ + l: "ignore device with duplicate curve25519 key", + keys: deviceKey + }, parentLog.level.Warn); + return false; + } + curve25519Keys.add(curve25519Key); + return true; + } else { + return false; + } + }); + }); + const verifiedKeys = verifiedEntries.map(([, deviceKeys]) => deviceKeys); + keys.set(userId, verifiedKeys); }); - const verifiedKeys = verifiedEntries.map(([, deviceKeys]) => deviceKeys); - return {userId, verifiedKeys}; - }); - return verifiedKeys; + } + return keys; } - _hasValidSignature(deviceSection, parentLog) { - const deviceId = deviceSection["device_id"]; - const userId = deviceSection["user_id"]; - const ed25519Key = deviceSection?.keys?.[`${SIGNATURE_ALGORITHM}:${deviceId}`]; - return verifyEd25519Signature(this._olmUtil, userId, deviceId, ed25519Key, deviceSection, parentLog); + _validateDeviceKey(userIdFromServer: string, deviceIdFromServer: string, deviceKey: DeviceKey, log: ILogItem): boolean { + const deviceId = deviceKey["device_id"]; + const userId = deviceKey["user_id"]; + if (userId !== userIdFromServer) { + log.log("user_id mismatch"); + return false; + } + if (deviceId !== deviceIdFromServer) { + log.log("device_id mismatch"); + return false; + } + const ed25519Key = getDeviceEd25519Key(deviceKey); + const curve25519Key = getDeviceCurve25519Key(deviceKey); + if (typeof ed25519Key !== "string" || typeof curve25519Key !== "string") { + log.log("ed25519 and/or curve25519 key invalid").set({deviceKey}); + return false; + } + const isValid = verifyEd25519Signature(this._olmUtil, userId, deviceId, ed25519Key, deviceKey, log); + if (!isValid) { + log.log({ + l: "ignore device with invalid signature", + keys: deviceKey + }, log.level.Warn); + } + return isValid; } /** @@ -443,7 +476,7 @@ export class DeviceTracker { * @param {String} roomId [description] * @return {[type]} [description] */ - async devicesForTrackedRoom(roomId, hsApi, log) { + async devicesForTrackedRoom(roomId: string, hsApi: HomeServerApi, log: ILogItem): Promise { const txn = await this._storage.readTxn([ this._storage.storeNames.roomMembers, this._storage.storeNames.userIdentities, @@ -463,7 +496,7 @@ export class DeviceTracker { * Can be used to decide which users to share keys with. * Assumes room is already tracked. Call `trackRoom` first if unsure. */ - async devicesForRoomMembers(roomId, userIds, hsApi, log) { + async devicesForRoomMembers(roomId: string, userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { const txn = await this._storage.readTxn([ this._storage.storeNames.userIdentities, ]); @@ -474,13 +507,13 @@ export class DeviceTracker { * Cannot be used to decide which users to share keys with. * Does not assume membership to any room or whether any room is tracked. */ - async devicesForUsers(userIds, hsApi, log) { + async devicesForUsers(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { const txn = await this._storage.readTxn([ this._storage.storeNames.userIdentities, ]); - const upToDateIdentities = []; - const outdatedUserIds = []; + const upToDateIdentities: UserIdentity[] = []; + const outdatedUserIds: string[] = []; await Promise.all(userIds.map(async userId => { const i = await txn.userIdentities.get(userId); if (i && i.deviceTrackingStatus === TRACKING_STATUS_UPTODATE) { @@ -495,12 +528,12 @@ export class DeviceTracker { } /** gets a single device */ - async deviceForId(userId, deviceId, hsApi, log) { + async deviceForId(userId: string, deviceId: string, hsApi: HomeServerApi, log: ILogItem) { const txn = await this._storage.readTxn([ - this._storage.storeNames.deviceIdentities, + this._storage.storeNames.deviceKeys, ]); - let device = await txn.deviceIdentities.get(userId, deviceId); - if (device) { + let deviceKey = await txn.deviceKeys.get(userId, deviceId); + if (deviceKey) { log.set("existingDevice", true); } else { //// BEGIN EXTRACT (deviceKeysMap) @@ -514,29 +547,26 @@ export class DeviceTracker { // verify signature const verifiedKeysPerUser = log.wrap("verify", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log)); //// END EXTRACT - // TODO: what if verifiedKeysPerUser is empty or does not contain userId? - const verifiedKeys = verifiedKeysPerUser - .find(vkpu => vkpu.userId === userId).verifiedKeys - .find(vk => vk["device_id"] === deviceId); + const verifiedKey = verifiedKeysPerUser.get(userId)?.find(d => d.device_id === deviceId); // user hasn't uploaded keys for device? - if (!verifiedKeys) { + if (!verifiedKey) { return undefined; } - device = deviceKeysAsDeviceIdentity(verifiedKeys); const txn = await this._storage.readWriteTxn([ - this._storage.storeNames.deviceIdentities, + this._storage.storeNames.deviceKeys, ]); // 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 // can be updated, but here we don't. - const existingDevice = await txn.deviceIdentities.get(userId, deviceId); + const existingDevice = await txn.deviceKeys.get(userId, deviceId); if (existingDevice) { - device = existingDevice; + deviceKey = existingDevice; log.set("existingDeviceAfterFetch", true); } else { try { - txn.deviceIdentities.set(device); + txn.deviceKeys.set(verifiedKey); + deviceKey = verifiedKey; log.set("newDevice", true); } catch (err) { txn.abort(); @@ -545,7 +575,7 @@ export class DeviceTracker { await txn.complete(); } } - return device; + return deviceKey; } /** @@ -555,9 +585,9 @@ export class DeviceTracker { * @param {Array} userIds a set of user ids to try and find the identity for. * @param {Transaction} userIdentityTxn to read the user identities * @param {HomeServerApi} hsApi - * @return {Array} all devices identities for the given users we should share keys with. + * @return {Array} all devices identities for the given users we should share keys with. */ - async _devicesForUserIdsInTrackedRoom(roomId, userIds, userIdentityTxn, hsApi, log) { + async _devicesForUserIdsInTrackedRoom(roomId: string, userIds: string[], userIdentityTxn: Transaction, hsApi: HomeServerApi, log: ILogItem): Promise { const allMemberIdentities = await Promise.all(userIds.map(userId => userIdentityTxn.userIdentities.get(userId))); const identities = allMemberIdentities.filter(identity => { // we use roomIds to decide with whom we should share keys for a given room, @@ -566,7 +596,7 @@ export class DeviceTracker { // Given we assume the room is tracked, // also exclude any userId which doesn't have a userIdentity yet. return identity && identity.roomIds.includes(roomId); - }); + }) as UserIdentity[]; // undefined has been filter out const upToDateIdentities = identities.filter(i => i.deviceTrackingStatus === TRACKING_STATUS_UPTODATE); const outdatedUserIds = identities .filter(i => i.deviceTrackingStatus === TRACKING_STATUS_OUTDATED) @@ -574,7 +604,7 @@ export class DeviceTracker { let devices = await this._devicesForUserIdentities(upToDateIdentities, outdatedUserIds, hsApi, log); // filter out our own device as we should never share keys with it. devices = devices.filter(device => { - const isOwnDevice = device.userId === this._ownUserId && device.deviceId === this._ownDeviceId; + const isOwnDevice = device.user_id === this._ownUserId && device.device_id === this._ownDeviceId; return !isOwnDevice; }); return devices; @@ -584,43 +614,44 @@ export class DeviceTracker { * 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 * will have their keys fetched from the homeserver. */ - async _devicesForUserIdentities(upToDateIdentities, outdatedUserIds, hsApi, log) { + async _devicesForUserIdentities(upToDateIdentities: UserIdentity[], outdatedUserIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { log.set("uptodate", upToDateIdentities.length); log.set("outdated", outdatedUserIds.length); - let queriedDevices; + let queriedDeviceKeys: Map | undefined; if (outdatedUserIds.length) { // TODO: ignore the race between /sync and /keys/query for now, // where users could get marked as outdated or added/removed from the room while // querying keys - const {deviceIdentities} = await this._queryKeys(outdatedUserIds, hsApi, log); - queriedDevices = deviceIdentities; + const {deviceKeys} = await this._queryKeys(outdatedUserIds, hsApi, log); + queriedDeviceKeys = deviceKeys; } const deviceTxn = await this._storage.readTxn([ - this._storage.storeNames.deviceIdentities, + this._storage.storeNames.deviceKeys, ]); const devicesPerUser = await Promise.all(upToDateIdentities.map(identity => { - return deviceTxn.deviceIdentities.getAllForUserId(identity.userId); + return deviceTxn.deviceKeys.getAllForUserId(identity.userId); })); let flattenedDevices = devicesPerUser.reduce((all, devicesForUser) => all.concat(devicesForUser), []); - if (queriedDevices && queriedDevices.length) { - flattenedDevices = flattenedDevices.concat(queriedDevices); + if (queriedDeviceKeys && queriedDeviceKeys.size) { + for (const deviceKeysForUser of queriedDeviceKeys.values()) { + flattenedDevices = flattenedDevices.concat(deviceKeysForUser); + } } return flattenedDevices; } - async getDeviceByCurve25519Key(curve25519Key, txn) { - return await txn.deviceIdentities.getByCurve25519Key(curve25519Key); + async getDeviceByCurve25519Key(curve25519Key, txn: Transaction): Promise { + return await txn.deviceKeys.getByCurve25519Key(curve25519Key); } } import {createMockStorage} from "../../mocks/Storage"; import {Instance as NullLoggerInstance} from "../../logging/NullLogger"; -import {MemberChange} from "../room/members/RoomMember"; export function tests() { - function createUntrackedRoomMock(roomId, joinedUserIds, invitedUserIds = []) { + function createUntrackedRoomMock(roomId: string, joinedUserIds: string[], invitedUserIds: string[] = []) { return { id: roomId, isTrackingMembers: false, @@ -649,11 +680,11 @@ export function tests() { } } - function createQueryKeysHSApiMock(createKey = (algorithm, userId, deviceId) => `${algorithm}:${userId}:${deviceId}:key`) { + function createQueryKeysHSApiMock(createKey = (algorithm, userId, deviceId) => `${algorithm}:${userId}:${deviceId}:key`): HomeServerApi { return { queryKeys(payload) { const {device_keys: deviceKeys} = payload; - const userKeys = Object.entries(deviceKeys).reduce((userKeys, [userId, deviceIds]) => { + const userKeys = Object.entries(deviceKeys as {[userId: string]: string[]}).reduce((userKeys, [userId, deviceIds]) => { if (deviceIds.length === 0) { deviceIds = ["device1"]; } @@ -689,7 +720,7 @@ export function tests() { } }; } - }; + } as unknown as HomeServerApi; } async function writeMemberListToStorage(room, storage) { @@ -718,7 +749,7 @@ export function tests() { const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); @@ -727,14 +758,12 @@ export function tests() { const txn = await storage.readTxn([storage.storeNames.userIdentities]); assert.deepEqual(await txn.userIdentities.get("@alice:hs.tld"), { userId: "@alice:hs.tld", - crossSigningKeys: undefined, roomIds: [roomId], deviceTrackingStatus: TRACKING_STATUS_OUTDATED }); assert.deepEqual(await txn.userIdentities.get("@bob:hs.tld"), { userId: "@bob:hs.tld", roomIds: [roomId], - crossSigningKeys: undefined, deviceTrackingStatus: TRACKING_STATUS_OUTDATED }); assert.equal(await txn.userIdentities.get("@charly:hs.tld"), undefined); @@ -744,7 +773,7 @@ export function tests() { const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); @@ -753,15 +782,15 @@ export function tests() { const hsApi = createQueryKeysHSApiMock(); const devices = await tracker.devicesForRoomMembers(roomId, ["@alice:hs.tld", "@bob:hs.tld"], hsApi, NullLoggerInstance.item); assert.equal(devices.length, 2); - assert.equal(devices.find(d => d.userId === "@alice:hs.tld").ed25519Key, "ed25519:@alice:hs.tld:device1:key"); - assert.equal(devices.find(d => d.userId === "@bob:hs.tld").ed25519Key, "ed25519:@bob:hs.tld:device1:key"); + assert.equal(getDeviceEd25519Key(devices.find(d => d.user_id === "@alice:hs.tld")!), "ed25519:@alice:hs.tld:device1:key"); + assert.equal(getDeviceEd25519Key(devices.find(d => d.user_id === "@bob:hs.tld")!), "ed25519:@bob:hs.tld:device1:key"); }, "device with changed key is ignored": async assert => { const storage = await createMockStorage(); const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); @@ -779,18 +808,18 @@ export function tests() { }); const devices = await tracker.devicesForRoomMembers(roomId, ["@alice:hs.tld", "@bob:hs.tld"], hsApiWithChangedAliceKey, NullLoggerInstance.item); assert.equal(devices.length, 2); - assert.equal(devices.find(d => d.userId === "@alice:hs.tld").ed25519Key, "ed25519:@alice:hs.tld:device1:key"); - assert.equal(devices.find(d => d.userId === "@bob:hs.tld").ed25519Key, "ed25519:@bob:hs.tld:device1:key"); - const txn2 = await storage.readTxn([storage.storeNames.deviceIdentities]); + assert.equal(getDeviceEd25519Key(devices.find(d => d.user_id === "@alice:hs.tld")!), "ed25519:@alice:hs.tld:device1:key"); + assert.equal(getDeviceEd25519Key(devices.find(d => d.user_id === "@bob:hs.tld")!), "ed25519:@bob:hs.tld:device1:key"); + const txn2 = await storage.readTxn([storage.storeNames.deviceKeys]); // also check the modified key was not stored - assert.equal((await txn2.deviceIdentities.get("@alice:hs.tld", "device1")).ed25519Key, "ed25519:@alice:hs.tld:device1:key"); + assert.equal(getDeviceEd25519Key((await txn2.deviceKeys.get("@alice:hs.tld", "device1"))!), "ed25519:@alice:hs.tld:device1:key"); }, "change history visibility from joined to invited adds invitees": async assert => { const storage = await createMockStorage(); const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); @@ -798,10 +827,10 @@ export function tests() { const room = await createUntrackedRoomMock(roomId, ["@alice:hs.tld"], ["@bob:hs.tld"]); await tracker.trackRoom(room, HistoryVisibility.Joined, NullLoggerInstance.item); - const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); + const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceKeys]); assert.equal(await txn.userIdentities.get("@bob:hs.tld"), undefined); const {added, removed} = await tracker.writeHistoryVisibility(room, HistoryVisibility.Invited, txn, NullLoggerInstance.item); - assert.equal((await txn.userIdentities.get("@bob:hs.tld")).userId, "@bob:hs.tld"); + assert.equal((await txn.userIdentities.get("@bob:hs.tld"))!.userId, "@bob:hs.tld"); assert.deepEqual(added, ["@bob:hs.tld"]); assert.deepEqual(removed, []); }, @@ -810,7 +839,7 @@ export function tests() { const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); @@ -818,8 +847,8 @@ export function tests() { const room = await createUntrackedRoomMock(roomId, ["@alice:hs.tld"], ["@bob:hs.tld"]); await tracker.trackRoom(room, HistoryVisibility.Invited, NullLoggerInstance.item); - const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); - assert.equal((await txn.userIdentities.get("@bob:hs.tld")).userId, "@bob:hs.tld"); + const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceKeys]); + assert.equal((await txn.userIdentities.get("@bob:hs.tld"))!.userId, "@bob:hs.tld"); const {added, removed} = await tracker.writeHistoryVisibility(room, HistoryVisibility.Joined, txn, NullLoggerInstance.item); assert.equal(await txn.userIdentities.get("@bob:hs.tld"), undefined); assert.deepEqual(added, []); @@ -830,32 +859,32 @@ export function tests() { const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); const room = await createUntrackedRoomMock(roomId, ["@alice:hs.tld"]); await tracker.trackRoom(room, HistoryVisibility.Invited, NullLoggerInstance.item); - const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); + const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceKeys]); // inviting a new member const inviteChange = new MemberChange(RoomMember.fromUserId(roomId, "@bob:hs.tld", "invite")); - const {added, removed} = await tracker.writeMemberChanges(room, [inviteChange], HistoryVisibility.Invited, txn); + const {added, removed} = await tracker.writeMemberChanges(room, new Map([[inviteChange.userId, inviteChange]]), HistoryVisibility.Invited, txn); assert.deepEqual(added, ["@bob:hs.tld"]); assert.deepEqual(removed, []); - assert.equal((await txn.userIdentities.get("@bob:hs.tld")).userId, "@bob:hs.tld"); + assert.equal((await txn.userIdentities.get("@bob:hs.tld"))!.userId, "@bob:hs.tld"); }, "adding invitee with history visibility of joined doesn't add room": async assert => { const storage = await createMockStorage(); const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); const room = await createUntrackedRoomMock(roomId, ["@alice:hs.tld"]); await tracker.trackRoom(room, HistoryVisibility.Joined, NullLoggerInstance.item); - const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); + const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceKeys]); // inviting a new member const inviteChange = new MemberChange(RoomMember.fromUserId(roomId, "@bob:hs.tld", "invite")); const memberChanges = new Map([[inviteChange.userId, inviteChange]]); @@ -869,7 +898,7 @@ export function tests() { const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); @@ -881,22 +910,22 @@ export function tests() { await writeMemberListToStorage(room, storage); const devices = await tracker.devicesForTrackedRoom(roomId, hsApi, NullLoggerInstance.item); assert.equal(devices.length, 2); - assert.equal(devices.find(d => d.userId === "@alice:hs.tld").ed25519Key, "ed25519:@alice:hs.tld:device1:key"); - assert.equal(devices.find(d => d.userId === "@bob:hs.tld").ed25519Key, "ed25519:@bob:hs.tld:device1:key"); + assert.equal(getDeviceEd25519Key(devices.find(d => d.user_id === "@alice:hs.tld")!), "ed25519:@alice:hs.tld:device1:key"); + assert.equal(getDeviceEd25519Key(devices.find(d => d.user_id === "@bob:hs.tld")!), "ed25519:@bob:hs.tld:device1:key"); }, "rejecting invite with history visibility of invited removes room from user identity": async assert => { const storage = await createMockStorage(); const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); // alice is joined, bob is invited const room = await createUntrackedRoomMock(roomId, ["@alice:hs.tld"], ["@bob:hs.tld"]); await tracker.trackRoom(room, HistoryVisibility.Invited, NullLoggerInstance.item); - const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); + const txn = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceKeys]); // reject invite const inviteChange = new MemberChange(RoomMember.fromUserId(roomId, "@bob:hs.tld", "leave"), "invite"); const memberChanges = new Map([[inviteChange.userId, inviteChange]]); @@ -910,7 +939,7 @@ export function tests() { const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); @@ -920,21 +949,21 @@ export function tests() { await tracker.trackRoom(room1, HistoryVisibility.Joined, NullLoggerInstance.item); await tracker.trackRoom(room2, HistoryVisibility.Joined, NullLoggerInstance.item); const txn1 = await storage.readTxn([storage.storeNames.userIdentities]); - assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld")).roomIds, ["!abc:hs.tld", "!def:hs.tld"]); + assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld"))!.roomIds, ["!abc:hs.tld", "!def:hs.tld"]); const leaveChange = new MemberChange(RoomMember.fromUserId(room2.id, "@bob:hs.tld", "leave"), "join"); const memberChanges = new Map([[leaveChange.userId, leaveChange]]); - const txn2 = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceIdentities]); + const txn2 = await storage.readWriteTxn([storage.storeNames.userIdentities, storage.storeNames.deviceKeys]); await tracker.writeMemberChanges(room2, memberChanges, HistoryVisibility.Joined, txn2); await txn2.complete(); const txn3 = await storage.readTxn([storage.storeNames.userIdentities]); - assert.deepEqual((await txn3.userIdentities.get("@bob:hs.tld")).roomIds, ["!abc:hs.tld"]); + assert.deepEqual((await txn3.userIdentities.get("@bob:hs.tld"))!.roomIds, ["!abc:hs.tld"]); }, "add room to user identity sharing multiple rooms with us preserves other room": async assert => { const storage = await createMockStorage(); const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); @@ -943,40 +972,40 @@ export function tests() { const room2 = await createUntrackedRoomMock("!def:hs.tld", ["@alice:hs.tld", "@bob:hs.tld"]); await tracker.trackRoom(room1, HistoryVisibility.Joined, NullLoggerInstance.item); const txn1 = await storage.readTxn([storage.storeNames.userIdentities]); - assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld")).roomIds, ["!abc:hs.tld"]); + assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld"))!.roomIds, ["!abc:hs.tld"]); await tracker.trackRoom(room2, HistoryVisibility.Joined, NullLoggerInstance.item); const txn2 = await storage.readTxn([storage.storeNames.userIdentities]); - assert.deepEqual((await txn2.userIdentities.get("@bob:hs.tld")).roomIds, ["!abc:hs.tld", "!def:hs.tld"]); + assert.deepEqual((await txn2.userIdentities.get("@bob:hs.tld"))!.roomIds, ["!abc:hs.tld", "!def:hs.tld"]); }, "devicesForUsers fetches users even though they aren't in any tracked room": async assert => { const storage = await createMockStorage(); const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); const hsApi = createQueryKeysHSApiMock(); const devices = await tracker.devicesForUsers(["@bob:hs.tld"], hsApi, NullLoggerInstance.item); assert.equal(devices.length, 1); - assert.equal(devices[0].curve25519Key, "curve25519:@bob:hs.tld:device1:key"); + assert.equal(getDeviceCurve25519Key(devices[0]), "curve25519:@bob:hs.tld:device1:key"); const txn1 = await storage.readTxn([storage.storeNames.userIdentities]); - assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld")).roomIds, []); + assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld"))!.roomIds, []); }, "devicesForUsers doesn't add any roomId when creating userIdentity": async assert => { const storage = await createMockStorage(); const tracker = new DeviceTracker({ storage, getSyncToken: () => "token", - olmUtil: {ed25519_verify: () => {}}, // valid if it does not throw + olmUtil: {ed25519_verify: () => {}} as unknown as Olm.Utility, // valid if it does not throw ownUserId: "@alice:hs.tld", ownDeviceId: "ABCD", }); const hsApi = createQueryKeysHSApiMock(); await tracker.devicesForUsers(["@bob:hs.tld"], hsApi, NullLoggerInstance.item); const txn1 = await storage.readTxn([storage.storeNames.userIdentities]); - assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld")).roomIds, []); + assert.deepEqual((await txn1.userIdentities.get("@bob:hs.tld"))!.roomIds, []); } } } diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index b74dc710f1..4711289230 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -235,7 +235,7 @@ export class RoomEncryption { // Use devicesForUsers rather than devicesForRoomMembers as the room might not be tracked yet await this._deviceTracker.devicesForUsers(sendersWithoutDevice, hsApi, log); // now that we've fetched the missing devices, try verifying the results again - const txn = await this._storage.readTxn([this._storage.storeNames.deviceIdentities]); + const txn = await this._storage.readTxn([this._storage.storeNames.deviceKeys]); await this._verifyDecryptionResults(resultsWithoutDevice, txn); const resultsWithFoundDevice = resultsWithoutDevice.filter(r => !r.isVerificationUnknown); const resultsToEventIdMap = resultsWithFoundDevice.reduce((map, r) => { diff --git a/src/matrix/e2ee/common.js b/src/matrix/e2ee/common.ts similarity index 57% rename from src/matrix/e2ee/common.js rename to src/matrix/e2ee/common.ts index 9c5fe66c95..63cb389db9 100644 --- a/src/matrix/e2ee/common.js +++ b/src/matrix/e2ee/common.ts @@ -15,9 +15,15 @@ limitations under the License. */ import anotherjson from "another-json"; -import {createEnum} from "../../utils/enum"; -export const DecryptionSource = createEnum("Sync", "Timeline", "Retry"); +import type {UnsentStateEvent} from "../room/common"; +import type {ILogItem} from "../../logging/types"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; + +export enum DecryptionSource { + Sync, Timeline, Retry +}; // use common prefix so it's easy to clear properties that are not e2ee related during session clear export const SESSION_E2EE_KEY_PREFIX = "e2ee:"; @@ -25,29 +31,52 @@ export const OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2"; export const MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2"; export class DecryptionError extends Error { - constructor(code, event, detailsObj = null) { + constructor(private readonly code: string, private readonly event: object, private readonly detailsObj?: object) { super(`Decryption error ${code}${detailsObj ? ": "+JSON.stringify(detailsObj) : ""}`); - this.code = code; - this.event = event; - this.details = detailsObj; } } export const SIGNATURE_ALGORITHM = "ed25519"; -export function getEd25519Signature(signedValue, userId, deviceOrKeyId) { +export type SignedValue = { + signatures: {[userId: string]: {[keyId: string]: string}} + unsigned?: object +} + +// we store device keys (and cross-signing) in the format we get them from the server +// as that is what the signature is calculated on, so to verify and sign, we need +// it in this format anyway. +export type DeviceKey = SignedValue & { + readonly user_id: string; + readonly device_id: string; + readonly algorithms: ReadonlyArray; + readonly keys: {[keyId: string]: string}; + readonly unsigned: { + device_display_name?: string + } +} + +export function getDeviceEd25519Key(deviceKey: DeviceKey): string { + return deviceKey.keys[`ed25519:${deviceKey.device_id}`]; +} + +export function getDeviceCurve25519Key(deviceKey: DeviceKey): string { + return deviceKey.keys[`curve25519:${deviceKey.device_id}`]; +} + +export function getEd25519Signature(signedValue: SignedValue, userId: string, deviceOrKeyId: string) { return signedValue?.signatures?.[userId]?.[`${SIGNATURE_ALGORITHM}:${deviceOrKeyId}`]; } -export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Key, value, log = undefined) { +export function verifyEd25519Signature(olmUtil: Olm.Utility, userId: string, deviceOrKeyId: string, ed25519Key: string, value: SignedValue, log?: ILogItem) { const signature = getEd25519Signature(value, userId, deviceOrKeyId); if (!signature) { log?.set("no_signature", true); return false; } - const clone = Object.assign({}, value); - delete clone.unsigned; - delete clone.signatures; + const clone = Object.assign({}, value) as object; + delete clone["unsigned"]; + delete clone["signatures"]; const canonicalJson = anotherjson.stringify(clone); try { // throws when signature is invalid @@ -63,7 +92,7 @@ export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Ke } } -export function createRoomEncryptionEvent() { +export function createRoomEncryptionEvent(): UnsentStateEvent { return { "type": "m.room.encryption", "state_key": "", @@ -75,16 +104,14 @@ export function createRoomEncryptionEvent() { } } +export enum HistoryVisibility { + Joined = "joined", + Invited = "invited", + WorldReadable = "world_readable", + Shared = "shared", +}; -// Use enum when converting to TS -export const HistoryVisibility = Object.freeze({ - Joined: "joined", - Invited: "invited", - WorldReadable: "world_readable", - Shared: "shared", -}); - -export function shouldShareKey(membership, historyVisibility) { +export function shouldShareKey(membership: string, historyVisibility: HistoryVisibility) { switch (historyVisibility) { case HistoryVisibility.WorldReadable: return true; diff --git a/src/matrix/e2ee/megolm/keybackup/types.ts b/src/matrix/e2ee/megolm/keybackup/types.ts index ce56cca74c..f433a7d1d0 100644 --- a/src/matrix/e2ee/megolm/keybackup/types.ts +++ b/src/matrix/e2ee/megolm/keybackup/types.ts @@ -42,7 +42,7 @@ export type SessionInfo = { } export type MegOlmSessionKeyInfo = { - algorithm: MEGOLM_ALGORITHM, + algorithm: typeof MEGOLM_ALGORITHM, sender_key: string, sender_claimed_keys: {[algorithm: string]: string}, forwarding_curve25519_key_chain: string[], diff --git a/src/matrix/e2ee/olm/Encryption.ts b/src/matrix/e2ee/olm/Encryption.ts index 5fd1f25bb8..0b55238769 100644 --- a/src/matrix/e2ee/olm/Encryption.ts +++ b/src/matrix/e2ee/olm/Encryption.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {groupByWithCreator} from "../../../utils/groupBy"; -import {verifyEd25519Signature, OLM_ALGORITHM} from "../common.js"; +import {verifyEd25519Signature, OLM_ALGORITHM, getDeviceCurve25519Key, getDeviceEd25519Key} from "../common.js"; import {createSessionEntry} from "./Session"; import type {OlmMessage, OlmPayload, OlmEncryptedMessageContent} from "./types"; @@ -24,7 +24,7 @@ import type {LockMap} from "../../../utils/LockMap"; import {Lock, MultiLock, ILock} from "../../../utils/Lock"; import type {Storage} from "../../storage/idb/Storage"; import type {Transaction} from "../../storage/idb/Transaction"; -import type {DeviceIdentity} from "../../storage/idb/stores/DeviceIdentityStore"; +import type {DeviceKey} from "../common"; import type {HomeServerApi} from "../../net/HomeServerApi"; import type {ILogItem} from "../../../logging/types"; import type * as OlmNamespace from "@matrix-org/olm"; @@ -99,7 +99,7 @@ export class Encryption { return new MultiLock(locks); } - async encrypt(type: string, content: Record, devices: DeviceIdentity[], hsApi: HomeServerApi, log: ILogItem): Promise { + async encrypt(type: string, content: Record, devices: DeviceKey[], hsApi: HomeServerApi, log: ILogItem): Promise { let messages: EncryptedMessage[] = []; for (let i = 0; i < devices.length ; i += MAX_BATCH_SIZE) { const batchDevices = devices.slice(i, i + MAX_BATCH_SIZE); @@ -115,12 +115,12 @@ export class Encryption { return messages; } - async _encryptForMaxDevices(type: string, content: Record, devices: DeviceIdentity[], hsApi: HomeServerApi, log: ILogItem): Promise { + async _encryptForMaxDevices(type: string, content: Record, devices: DeviceKey[], hsApi: HomeServerApi, log: ILogItem): Promise { // TODO: see if we can only hold some of the locks until after the /keys/claim call (if needed) // take a lock on all senderKeys so decryption and other calls to encrypt (should not happen) // don't modify the sessions at the same time const locks = await Promise.all(devices.map(device => { - return this.senderKeyLock.takeLock(device.curve25519Key); + return this.senderKeyLock.takeLock(getDeviceCurve25519Key(device)); })); try { const { @@ -158,10 +158,10 @@ export class Encryption { } } - async _findExistingSessions(devices: DeviceIdentity[]): Promise<{devicesWithoutSession: DeviceIdentity[], existingEncryptionTargets: EncryptionTarget[]}> { + async _findExistingSessions(devices: DeviceKey[]): Promise<{devicesWithoutSession: DeviceKey[], existingEncryptionTargets: EncryptionTarget[]}> { const txn = await this.storage.readTxn([this.storage.storeNames.olmSessions]); const sessionIdsForDevice = await Promise.all(devices.map(async device => { - return await txn.olmSessions.getSessionIds(device.curve25519Key); + return await txn.olmSessions.getSessionIds(getDeviceCurve25519Key(device)); })); const devicesWithoutSession = devices.filter((_, i) => { const sessionIds = sessionIdsForDevice[i]; @@ -184,36 +184,36 @@ export class Encryption { const plaintext = JSON.stringify(this._buildPlainTextMessageForDevice(type, content, device)); const message = session!.encrypt(plaintext); const encryptedContent = { - algorithm: OLM_ALGORITHM, + algorithm: OLM_ALGORITHM as typeof OLM_ALGORITHM, sender_key: this.account.identityKeys.curve25519, ciphertext: { - [device.curve25519Key]: message + [getDeviceCurve25519Key(device)]: message } }; return encryptedContent; } - _buildPlainTextMessageForDevice(type: string, content: Record, device: DeviceIdentity): OlmPayload { + _buildPlainTextMessageForDevice(type: string, content: Record, device: DeviceKey): OlmPayload { return { keys: { "ed25519": this.account.identityKeys.ed25519 }, recipient_keys: { - "ed25519": device.ed25519Key + "ed25519": getDeviceEd25519Key(device) }, - recipient: device.userId, + recipient: device.user_id, sender: this.ownUserId, content, type } } - async _createNewSessions(devicesWithoutSession: DeviceIdentity[], hsApi: HomeServerApi, timestamp: number, log: ILogItem): Promise { + async _createNewSessions(devicesWithoutSession: DeviceKey[], hsApi: HomeServerApi, timestamp: number, log: ILogItem): Promise { const newEncryptionTargets = await log.wrap("claim", log => this._claimOneTimeKeys(hsApi, devicesWithoutSession, log)); try { for (const target of newEncryptionTargets) { const {device, oneTimeKey} = target; - target.session = await this.account.createOutboundOlmSession(device.curve25519Key, oneTimeKey); + target.session = await this.account.createOutboundOlmSession(getDeviceCurve25519Key(device), oneTimeKey); } await this._storeSessions(newEncryptionTargets, timestamp); } catch (err) { @@ -225,16 +225,16 @@ export class Encryption { return newEncryptionTargets; } - async _claimOneTimeKeys(hsApi: HomeServerApi, deviceIdentities: DeviceIdentity[], log: ILogItem): Promise { + async _claimOneTimeKeys(hsApi: HomeServerApi, deviceIdentities: DeviceKey[], log: ILogItem): Promise { // create a Map> const devicesByUser = groupByWithCreator(deviceIdentities, - (device: DeviceIdentity) => device.userId, - (): Map => new Map(), - (deviceMap: Map, device: DeviceIdentity) => deviceMap.set(device.deviceId, device) + (device: DeviceKey) => device.user_id, + (): Map => new Map(), + (deviceMap: Map, device: DeviceKey) => deviceMap.set(device.device_id, device) ); const oneTimeKeys = Array.from(devicesByUser.entries()).reduce((usersObj, [userId, deviceMap]) => { usersObj[userId] = Array.from(deviceMap.values()).reduce((devicesObj, device) => { - devicesObj[device.deviceId] = OTK_ALGORITHM; + devicesObj[device.device_id] = OTK_ALGORITHM; return devicesObj; }, {}); return usersObj; @@ -250,7 +250,7 @@ export class Encryption { return this._verifyAndCreateOTKTargets(userKeyMap, devicesByUser, log); } - _verifyAndCreateOTKTargets(userKeyMap: ClaimedOTKResponse, devicesByUser: Map>, log: ILogItem): EncryptionTarget[] { + _verifyAndCreateOTKTargets(userKeyMap: ClaimedOTKResponse, devicesByUser: Map>, log: ILogItem): EncryptionTarget[] { const verifiedEncryptionTargets: EncryptionTarget[] = []; for (const [userId, userSection] of Object.entries(userKeyMap)) { for (const [deviceId, deviceSection] of Object.entries(userSection)) { @@ -260,7 +260,7 @@ export class Encryption { const device = devicesByUser.get(userId)?.get(deviceId); if (device) { const isValidSignature = verifyEd25519Signature( - this.olmUtil, userId, deviceId, device.ed25519Key, keySection, log); + this.olmUtil, userId, deviceId, getDeviceEd25519Key(device), keySection, log); if (isValidSignature) { const target = EncryptionTarget.fromOTK(device, keySection.key); verifiedEncryptionTargets.push(target); @@ -281,7 +281,7 @@ export class Encryption { try { await Promise.all(encryptionTargets.map(async encryptionTarget => { const sessionEntry = await txn.olmSessions.get( - encryptionTarget.device.curve25519Key, encryptionTarget.sessionId!); + getDeviceCurve25519Key(encryptionTarget.device), encryptionTarget.sessionId!); if (sessionEntry && !failed) { const olmSession = new this.olm.Session(); olmSession.unpickle(this.pickleKey, sessionEntry.session); @@ -303,7 +303,7 @@ export class Encryption { try { for (const target of encryptionTargets) { const sessionEntry = createSessionEntry( - target.session!, target.device.curve25519Key, timestamp, this.pickleKey); + target.session!, getDeviceCurve25519Key(target.device), timestamp, this.pickleKey); txn.olmSessions.set(sessionEntry); } } catch (err) { @@ -323,16 +323,16 @@ class EncryptionTarget { public session: Olm.Session | null = null; constructor( - public readonly device: DeviceIdentity, + public readonly device: DeviceKey, public readonly oneTimeKey: string | null, public readonly sessionId: string | null ) {} - static fromOTK(device: DeviceIdentity, oneTimeKey: string): EncryptionTarget { + static fromOTK(device: DeviceKey, oneTimeKey: string): EncryptionTarget { return new EncryptionTarget(device, oneTimeKey, null); } - static fromSessionId(device: DeviceIdentity, sessionId: string): EncryptionTarget { + static fromSessionId(device: DeviceKey, sessionId: string): EncryptionTarget { return new EncryptionTarget(device, null, sessionId); } @@ -346,6 +346,6 @@ class EncryptionTarget { export class EncryptedMessage { constructor( public readonly content: OlmEncryptedMessageContent, - public readonly device: DeviceIdentity + public readonly device: DeviceKey ) {} } diff --git a/src/matrix/e2ee/olm/types.ts b/src/matrix/e2ee/olm/types.ts index 5302dad80a..164854ad03 100644 --- a/src/matrix/e2ee/olm/types.ts +++ b/src/matrix/e2ee/olm/types.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import type {OLM_ALGORITHM} from "../common"; + export const enum OlmPayloadType { PreKey = 0, Normal = 1 @@ -25,7 +27,7 @@ export type OlmMessage = { } export type OlmEncryptedMessageContent = { - algorithm?: "m.olm.v1.curve25519-aes-sha2" + algorithm?: typeof OLM_ALGORITHM sender_key?: string, ciphertext?: { [deviceCurve25519Key: string]: OlmMessage diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 4ea2538923..9931be835e 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -173,7 +173,7 @@ export class BaseRoom extends EventEmitter { const isTimelineOpen = this._isTimelineOpen; if (isTimelineOpen) { // read to fetch devices if timeline is open - stores.push(this._storage.storeNames.deviceIdentities); + stores.push(this._storage.storeNames.deviceKeys); } const writeTxn = await this._storage.readWriteTxn(stores); let decryption; diff --git a/src/matrix/room/RoomBeingCreated.ts b/src/matrix/room/RoomBeingCreated.ts index b2c9dafbd9..4e908aa2f4 100644 --- a/src/matrix/room/RoomBeingCreated.ts +++ b/src/matrix/room/RoomBeingCreated.ts @@ -20,7 +20,7 @@ import {MediaRepository} from "../net/MediaRepository"; import {EventEmitter} from "../../utils/EventEmitter"; import {AttachmentUpload} from "./AttachmentUpload"; import {loadProfiles, Profile, UserIdProfile} from "../profile"; -import {RoomType} from "./common"; +import {RoomType, UnsentStateEvent} from "./common"; import type {HomeServerApi} from "../net/HomeServerApi"; import type {ILogItem} from "../../logging/types"; @@ -37,7 +37,7 @@ type CreateRoomPayload = { invite?: string[]; room_alias_name?: string; creation_content?: {"m.federate": boolean}; - initial_state: { type: string; state_key: string; content: Record }[]; + initial_state: UnsentStateEvent[]; power_level_content_override?: Record; } diff --git a/src/matrix/room/common.ts b/src/matrix/room/common.ts index 2ce8b5dd00..1174d09d79 100644 --- a/src/matrix/room/common.ts +++ b/src/matrix/room/common.ts @@ -28,6 +28,8 @@ export function isRedacted(event) { return !!event?.unsigned?.redacted_because; } +export type UnsentStateEvent = { type: string; state_key: string; content: Record }; + export enum RoomStatus { None = 1 << 0, BeingCreated = 1 << 1, diff --git a/src/matrix/storage/common.ts b/src/matrix/storage/common.ts index adebcdd65f..bf9ce39bc7 100644 --- a/src/matrix/storage/common.ts +++ b/src/matrix/storage/common.ts @@ -26,7 +26,7 @@ export enum StoreNames { timelineFragments = "timelineFragments", pendingEvents = "pendingEvents", userIdentities = "userIdentities", - deviceIdentities = "deviceIdentities", + deviceKeys = "deviceKeys", olmSessions = "olmSessions", inboundGroupSessions = "inboundGroupSessions", outboundGroupSessions = "outboundGroupSessions", diff --git a/src/matrix/storage/idb/Transaction.ts b/src/matrix/storage/idb/Transaction.ts index 532ffd1d27..4c76608ca1 100644 --- a/src/matrix/storage/idb/Transaction.ts +++ b/src/matrix/storage/idb/Transaction.ts @@ -29,7 +29,7 @@ import {RoomMemberStore} from "./stores/RoomMemberStore"; import {TimelineFragmentStore} from "./stores/TimelineFragmentStore"; import {PendingEventStore} from "./stores/PendingEventStore"; import {UserIdentityStore} from "./stores/UserIdentityStore"; -import {DeviceIdentityStore} from "./stores/DeviceIdentityStore"; +import {DeviceKeyStore} from "./stores/DeviceKeyStore"; import {CrossSigningKeyStore} from "./stores/CrossSigningKeyStore"; import {OlmSessionStore} from "./stores/OlmSessionStore"; import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore"; @@ -142,8 +142,8 @@ export class Transaction { return this._store(StoreNames.userIdentities, idbStore => new UserIdentityStore(idbStore)); } - get deviceIdentities(): DeviceIdentityStore { - return this._store(StoreNames.deviceIdentities, idbStore => new DeviceIdentityStore(idbStore)); + get deviceKeys(): DeviceKeyStore { + return this._store(StoreNames.deviceKeys, idbStore => new DeviceKeyStore(idbStore)); } get crossSigningKeys(): CrossSigningKeyStore { diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 3d1e714f33..200f4089ed 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -35,7 +35,7 @@ export const schema: MigrationFunc[] = [ addInboundSessionBackupIndex, migrateBackupStatus, createCallStore, - createCrossSigningKeyStore + createCrossSigningKeyStoreAndRenameDeviceIdentities ]; // TODO: how to deal with git merge conflicts of this array? @@ -277,7 +277,10 @@ function createCallStore(db: IDBDatabase) : void { db.createObjectStore("calls", {keyPath: "key"}); } -//v18 create calls store -function createCrossSigningKeyStore(db: IDBDatabase) : void { +//v18 create calls store and rename deviceIdentities to deviceKeys +function createCrossSigningKeyStoreAndRenameDeviceIdentities(db: IDBDatabase) : void { db.createObjectStore("crossSigningKeys", {keyPath: "key"}); + db.deleteObjectStore("deviceIdentities"); + const deviceKeys = db.createObjectStore("deviceKeys", {keyPath: "key"}); + deviceKeys.createIndex("byCurve25519Key", "curve25519Key", {unique: true}); } diff --git a/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts b/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts index a2fa9ecbd1..dc5804ef66 100644 --- a/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts +++ b/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts @@ -16,15 +16,15 @@ limitations under the License. import {MAX_UNICODE, MIN_UNICODE} from "./common"; import {Store} from "../Store"; +import type {SignedValue} from "../../../e2ee/common"; -// we store cross-signing keys in the format we get them from the server -// as that is what the signature is calculated on, so to verify, we need +// we store cross-signing (and device) keys in the format we get them from the server +// as that is what the signature is calculated on, so to verify and sign, we need // it in this format anyway. -export type CrossSigningKey = { +export type CrossSigningKey = SignedValue & { readonly user_id: string; readonly usage: ReadonlyArray; readonly keys: {[keyId: string]: string}; - readonly signatures: {[userId: string]: {[keyId: string]: string}} } type CrossSigningKeyEntry = CrossSigningKey & { diff --git a/src/matrix/storage/idb/stores/DeviceIdentityStore.ts b/src/matrix/storage/idb/stores/DeviceKeyStore.ts similarity index 63% rename from src/matrix/storage/idb/stores/DeviceIdentityStore.ts rename to src/matrix/storage/idb/stores/DeviceKeyStore.ts index 2936f07981..897d645329 100644 --- a/src/matrix/storage/idb/stores/DeviceIdentityStore.ts +++ b/src/matrix/storage/idb/stores/DeviceKeyStore.ts @@ -16,15 +16,13 @@ limitations under the License. import {MAX_UNICODE, MIN_UNICODE} from "./common"; import {Store} from "../Store"; +import {getDeviceCurve25519Key} from "../../../e2ee/common"; +import type {DeviceKey} from "../../../e2ee/common"; -export interface DeviceIdentity { - userId: string; - deviceId: string; - ed25519Key: string; +type DeviceKeyEntry = { + key: string; // key in storage, not a crypto key curve25519Key: string; - algorithms: string[]; - displayName: string; - key: string; + deviceKey: DeviceKey } function encodeKey(userId: string, deviceId: string): string { @@ -36,23 +34,24 @@ function decodeKey(key: string): { userId: string, deviceId: string } { return {userId, deviceId}; } -export class DeviceIdentityStore { - private _store: Store; +export class DeviceKeyStore { + private _store: Store; - constructor(store: Store) { + constructor(store: Store) { this._store = store; } - getAllForUserId(userId: string): Promise { - const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, "")); - return this._store.selectWhile(range, device => { - return device.userId === userId; + async getAllForUserId(userId: string): Promise { + const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, MIN_UNICODE)); + const entries = await this._store.selectWhile(range, device => { + return device.deviceKey.user_id === userId; }); + return entries.map(e => e.deviceKey); } async getAllDeviceIds(userId: string): Promise { const deviceIds: string[] = []; - const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, "")); + const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, MIN_UNICODE)); await this._store.iterateKeys(range, key => { const decodedKey = decodeKey(key as string); // prevent running into the next room @@ -65,17 +64,21 @@ export class DeviceIdentityStore { return deviceIds; } - get(userId: string, deviceId: string): Promise { - return this._store.get(encodeKey(userId, deviceId)); + async get(userId: string, deviceId: string): Promise { + return (await this._store.get(encodeKey(userId, deviceId)))?.deviceKey; } - set(deviceIdentity: DeviceIdentity): void { - deviceIdentity.key = encodeKey(deviceIdentity.userId, deviceIdentity.deviceId); - this._store.put(deviceIdentity); + set(deviceKey: DeviceKey): void { + this._store.put({ + key: encodeKey(deviceKey.user_id, deviceKey.device_id), + curve25519Key: getDeviceCurve25519Key(deviceKey)!, + deviceKey + }); } - getByCurve25519Key(curve25519Key: string): Promise { - return this._store.index("byCurve25519Key").get(curve25519Key); + async getByCurve25519Key(curve25519Key: string): Promise { + const entry = await this._store.index("byCurve25519Key").get(curve25519Key); + return entry?.deviceKey; } remove(userId: string, deviceId: string): void { From 683e055757075373a0236a9a96a6e4f50cc65b41 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 1 Mar 2023 16:59:24 +0530 Subject: [PATCH 017/168] WIP --- src/matrix/verification/CrossSigning.ts | 1 + .../verification/SAS/SASVerification.ts | 27 ++-- .../verification/SAS/channel/Channel.ts | 135 ++++++++++++++++-- src/matrix/verification/SAS/channel/types.ts | 20 +++ .../SAS/stages/BaseSASVerificationStage.ts | 2 + ...onStage.ts => RequestVerificationStage.ts} | 17 ++- .../stages/SelectVerificationMethodStage.ts | 93 ++++++++++++ ...tage.ts => SendAcceptVerificationStage.ts} | 44 +++--- .../verification/SAS/stages/SendKeyStage.ts | 33 +++-- .../verification/SAS/stages/constants.ts | 14 ++ 10 files changed, 308 insertions(+), 78 deletions(-) create mode 100644 src/matrix/verification/SAS/channel/types.ts rename src/matrix/verification/SAS/stages/{StartVerificationStage.ts => RequestVerificationStage.ts} (78%) create mode 100644 src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts rename src/matrix/verification/SAS/stages/{AcceptVerificationStage.ts => SendAcceptVerificationStage.ts} (68%) create mode 100644 src/matrix/verification/SAS/stages/constants.ts diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index c5f227865f..83b9505215 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -129,6 +129,7 @@ export class CrossSigning { otherUserId: userId, platform: this.platform, deviceMessageHandler: this.deviceMessageHandler, + log }); return new SASVerification({ room, diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 960e63a63e..aa19a6e79a 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -13,10 +13,7 @@ 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 {StartVerificationStage} from "./stages/StartVerificationStage"; -import {WaitForIncomingMessageStage} from "./stages/WaitForIncomingMessageStage"; -import {AcceptVerificationStage} from "./stages/AcceptVerificationStage"; -import {SendKeyStage} from "./stages/SendKeyStage"; +import {RequestVerificationStage} from "./stages/RequestVerificationStage"; import type {ILogItem} from "../../../logging/types"; import type {Room} from "../../room/Room.js"; import type {Platform} from "../../../platform/web/Platform.js"; @@ -48,23 +45,23 @@ export class SASVerification { // channel.send("m.key.verification.request", {}, log); try { const options = { room, ourUser, otherUserId, log, olmSas, olmUtil, channel }; - let stage: BaseSASVerificationStage = new StartVerificationStage(options); + let stage: BaseSASVerificationStage = new RequestVerificationStage(options); this.startStage = stage; - stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.ready", options)); - stage = stage.nextStage; + // stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.ready", options)); + // stage = stage.nextStage; - stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.start", options)); - stage = stage.nextStage; + // stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.start", options)); + // stage = stage.nextStage; - stage.setNextStage(new AcceptVerificationStage(options)); - stage = stage.nextStage; + // stage.setNextStage(new AcceptVerificationStage(options)); + // stage = stage.nextStage; - stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.key", options)); - stage = stage.nextStage; + // stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.key", options)); + // stage = stage.nextStage; - stage.setNextStage(new SendKeyStage(options)); - stage = stage.nextStage; + // stage.setNextStage(new SendKeyStage(options)); + // stage = stage.nextStage; console.log("startStage", this.startStage); } finally { diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index e2de538a49..3b39061628 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -20,21 +20,38 @@ import type {ILogItem} from "../../../../logging/types"; import type {Platform} from "../../../../platform/web/Platform.js"; import type {DeviceMessageHandler} from "../../../DeviceMessageHandler.js"; import {makeTxnId} from "../../../common.js"; +import {CancelTypes, VerificationEventTypes} from "./types"; + +const messageFromErrorType = { + [CancelTypes.UserCancelled]: "User cancelled this request.", + [CancelTypes.InvalidMessage]: "Invalid Message.", + [CancelTypes.KeyMismatch]: "Key Mismatch.", + [CancelTypes.OtherUserAccepted]: "Another device has accepted this request.", + [CancelTypes.TimedOut]: "Timed Out", + [CancelTypes.UnexpectedMessage]: "Unexpected Message.", + [CancelTypes.UnknownMethod]: "Unknown method.", + [CancelTypes.UnknownTransaction]: "Unknown Transaction.", + [CancelTypes.UserMismatch]: "User Mismatch", +} const enum ChannelType { MessageEvent, ToDeviceMessage, } -const enum VerificationEventTypes { - Request = "m.key.verification.request", - Ready = "m.key.verification.ready", -} - export interface IChannel { send(eventType: string, content: any, log: ILogItem): Promise; - waitForEvent(eventType: string): any; + waitForEvent(eventType: string): Promise; type: ChannelType; + id: string; + sentMessages: Map; + receivedMessages: Map; + localMessages: Map; + setStartMessage(content: any): void; + setInitiatedByUs(value: boolean): void; + initiatedByUs: boolean; + startMessage: any; + cancelVerification(cancellationType: CancelTypes): Promise; } type Options = { @@ -43,6 +60,7 @@ type Options = { otherUserId: string; platform: Platform; deviceMessageHandler: DeviceMessageHandler; + log: ILogItem; } export class ToDeviceChannel implements IChannel { @@ -51,15 +69,22 @@ export class ToDeviceChannel implements IChannel { private readonly otherUserId: string; private readonly platform: Platform; private readonly deviceMessageHandler: DeviceMessageHandler; - private readonly sentMessages: Map = new Map(); - private readonly receivedMessages: Map = new Map(); + public readonly sentMessages: Map = new Map(); + public readonly receivedMessages: Map = new Map(); + public readonly localMessages: Map = new Map(); private readonly waitMap: Map}> = new Map(); + private readonly log: ILogItem; + private otherUserDeviceId: string; + public startMessage: any; + public id: string; + private _initiatedByUs: boolean; constructor(options: Options) { this.hsApi = options.hsApi; this.deviceTracker = options.deviceTracker; this.otherUserId = options.otherUserId; this.platform = options.platform; + this.log = options.log; this.deviceMessageHandler = options.deviceMessageHandler; // todo: find a way to dispose this subscription this.deviceMessageHandler.on("message", ({unencrypted}) => this.handleDeviceMessage(unencrypted)) @@ -74,17 +99,28 @@ export class ToDeviceChannel implements IChannel { if (eventType === VerificationEventTypes.Request) { // Handle this case specially await this.handleRequestEventSpecially(eventType, content, log); + this.sentMessages.set(eventType, content); return; } + Object.assign(content, { transaction_id: this.id }); + const payload = { + messages: { + [this.otherUserId]: { + // check if the following is undefined? + [this.otherUserDeviceId]: content + } + } + } + await this.hsApi.sendToDevice(eventType, payload, this.id, { log }).response(); + this.sentMessages.set(eventType, content); }); } async handleRequestEventSpecially(eventType: string, content: any, log: ILogItem) { await log.wrap("ToDeviceChannel.handleRequestEventSpecially", async () => { - const devices = await this.deviceTracker.devicesForUsers([this.otherUserId], this.hsApi, log); - console.log("devices", devices); const timestamp = this.platform.clock.now(); const txnId = makeTxnId(); + this.id = txnId; Object.assign(content, { timestamp, transaction_id: txnId }); const payload = { messages: { @@ -93,14 +129,64 @@ export class ToDeviceChannel implements IChannel { } } } - this.hsApi.sendToDevice(eventType, payload, txnId, { log }); + await this.hsApi.sendToDevice(eventType, payload, txnId, { log }).response(); }); } - handleDeviceMessage(event) { - console.log("event", event); - this.resolveAnyWaits(event); - this.receivedMessages.set(event.type, event); + + private handleDeviceMessage(event) { + this.log.wrap("ToDeviceChannel.handleDeviceMessage", (log) => { + console.log("event", event); + log.set("event", event); + this.resolveAnyWaits(event); + this.receivedMessages.set(event.type, event); + if (event.type === VerificationEventTypes.Ready) { + this.handleReadyMessage(event, log); + } + }); + } + + private async handleReadyMessage(event, log: ILogItem) { + try { + const fromDevice = event.content.from_device; + this.otherUserDeviceId = fromDevice; + // We need to send cancel messages to all other devices + const devices = await this.deviceTracker.devicesForUsers([this.otherUserId], this.hsApi, log); + const otherDevices = devices.filter(device => device.deviceId !== fromDevice); + const cancelMessage = { + code: CancelTypes.OtherUserAccepted, + reason: "An user already accepted this request!", + transaction_id: this.id, + }; + const deviceMessages = otherDevices.reduce((acc, device) => { acc[device.deviceId] = cancelMessage; return acc; }, {}); + const payload = { + messages: { + [this.otherUserId]: deviceMessages + } + } + await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, this.id, { log }).response(); + } + catch (e) { + console.log(e); + // Do something here + } + } + + async cancelVerification(cancellationType: CancelTypes) { + await this.log.wrap("Channel.cancelVerification", async log => { + const payload = { + messages: { + [this.otherUserId]: { + [this.otherUserDeviceId]: { + code: cancellationType, + reason: messageFromErrorType[cancellationType], + transaction_id: this.id, + } + } + } + } + await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, this.id, { log }).response(); + }); } private resolveAnyWaits(event) { @@ -113,15 +199,34 @@ export class ToDeviceChannel implements IChannel { } waitForEvent(eventType: string): Promise { + // Check if we already received the message + const receivedMessage = this.receivedMessages.get(eventType); + if (receivedMessage) { + return Promise.resolve(receivedMessage); + } + // Check if we're already waiting for this message const existingWait = this.waitMap.get(eventType); if (existingWait) { return existingWait.promise; } let resolve; + // Add to wait map const promise = new Promise(r => { resolve = r; }); this.waitMap.set(eventType, { resolve, promise }); return promise; } + + setStartMessage(event) { + this.startMessage = event; + } + + setInitiatedByUs(value: boolean): void { + this._initiatedByUs = value; + } + + get initiatedByUs(): boolean { + return this._initiatedByUs; + }; } diff --git a/src/matrix/verification/SAS/channel/types.ts b/src/matrix/verification/SAS/channel/types.ts new file mode 100644 index 0000000000..68c5bb89dd --- /dev/null +++ b/src/matrix/verification/SAS/channel/types.ts @@ -0,0 +1,20 @@ +export const enum VerificationEventTypes { + Request = "m.key.verification.request", + Ready = "m.key.verification.ready", + Start = "m.key.verification.start", + Accept = "m.key.verification.accept", + Key = "m.key.verification.key", + Cancel = "m.key.verification.cancel", +} + +export const enum CancelTypes { + UserCancelled = "m.user", + TimedOut = "m.timeout", + UnknownTransaction = "m.unknown_transaction", + UnknownMethod = "m.unknown_method", + UnexpectedMessage = "m.unexpected_message", + KeyMismatch = "m.key_mismatch", + UserMismatch = "m.user_mismatch", + InvalidMessage = "m.invalid_message", + OtherUserAccepted = "m.accepted", +} diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index 8d275f4cd4..31ed908f3b 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -47,9 +47,11 @@ export abstract class BaseSASVerificationStage extends Disposables { protected previousResult: undefined | any; protected _nextStage: BaseSASVerificationStage; protected channel: IChannel; + protected options: Options; constructor(options: Options) { super(); + this.options = options; this.room = options.room; this.ourUser = options.ourUser; this.otherUserId = options.otherUserId; diff --git a/src/matrix/verification/SAS/stages/StartVerificationStage.ts b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts similarity index 78% rename from src/matrix/verification/SAS/stages/StartVerificationStage.ts rename to src/matrix/verification/SAS/stages/RequestVerificationStage.ts index c31346a6e6..fe00649b91 100644 --- a/src/matrix/verification/SAS/stages/StartVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts @@ -15,8 +15,10 @@ limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {FragmentBoundaryEntry} from "../../../room/timeline/entries/FragmentBoundaryEntry.js"; +import {SelectVerificationMethodStage} from "./SelectVerificationMethodStage"; +import {VerificationEventTypes} from "../channel/types"; -export class StartVerificationStage extends BaseSASVerificationStage { +export class RequestVerificationStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("StartVerificationStage.completeStage", async (log) => { @@ -27,13 +29,14 @@ export class StartVerificationStage extends BaseSASVerificationStage { // "msgtype": "m.key.verification.request", // "to": this.otherUserId, }; - const promise = this.trackEventId(); + // const promise = this.trackEventId(); // await this.room.sendEvent("m.room.message", content, null, log); - await this.channel.send("m.key.verification.request", content, log); - const c = await this.channel.waitForEvent("m.key.verification.ready"); - const eventId = await promise; - console.log("eventId", eventId); - this.setRequestEventId(eventId); + await this.channel.send(VerificationEventTypes.Request, content, log); + this._nextStage = new SelectVerificationMethodStage(this.options); + const readyContent = await this.channel.waitForEvent("m.key.verification.ready"); + // const eventId = await promise; + // console.log("eventId", eventId); + // this.setRequestEventId(eventId); this.dispose(); }); } diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts new file mode 100644 index 0000000000..2807abd634 --- /dev/null +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -0,0 +1,93 @@ +/* +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 {BaseSASVerificationStage} from "./BaseSASVerificationStage"; +import {KEY_AGREEMENT_LIST, HASHES_LIST, MAC_LIST, SAS_LIST} from "./constants"; +import {CancelTypes, VerificationEventTypes} from "../channel/types"; +import type {ILogItem} from "../../../../logging/types"; +import {SendAcceptVerificationStage} from "./SendAcceptVerificationStage"; + +export class SelectVerificationMethodStage extends BaseSASVerificationStage { + private hasSentStartMessage = false; + // should somehow emit something that tells the ui to hide the select option + private allowSelection = true; + + async completeStage() { + await this.log.wrap("SelectVerificationMethodStage.completeStage", async (log) => { + const startMessage = this.channel.waitForEvent(VerificationEventTypes.Start); + const acceptMessage = this.channel.waitForEvent(VerificationEventTypes.Accept); + const { content } = await Promise.race([startMessage, acceptMessage]); + if (content.method) { + // We received the start message + this.allowSelection = false; + if (this.hasSentStartMessage) { + await this.resolveStartConflict(); + } + else { + this.channel.setStartMessage(this.channel.receivedMessages.get(VerificationEventTypes.Start)); + this.channel.setInitiatedByUs(false); + } + } + else { + // We received the accept message + this.channel.setStartMessage(this.channel.sentMessages.get(VerificationEventTypes.Start)); + this.channel.setInitiatedByUs(true); + } + if (!this.channel.initiatedByUs) { + // We need to send the accept message next + this.setNextStage(new SendAcceptVerificationStage(this.options)); + } + this.dispose(); + }); + } + + async resolveStartConflict() { + const receivedStartMessage = this.channel.receivedMessages.get(VerificationEventTypes.Start); + const sentStartMessage = this.channel.sentMessages.get(VerificationEventTypes.Start); + if (receivedStartMessage.content.method !== sentStartMessage.content.method) { + await this.channel.cancelVerification(CancelTypes.UnexpectedMessage); + return; + } + // todo: what happens if we are verifying devices? user-ids would be the same in that case! + // In the case of conflict, the lexicographically smaller id wins + if (this.ourUser.userId < this.otherUserId) { + // use our stat message + this.channel.setStartMessage(sentStartMessage); + this.channel.setInitiatedByUs(true); + } + else { + this.channel.setStartMessage(receivedStartMessage); + this.channel.setInitiatedByUs(false); + } + } + + async selectEmojiMethod(log: ILogItem) { + if (!this.allowSelection) { return; } + const content = { + method: "m.sas.v1", + from_device: this.ourUser.deviceId, + key_agreement_protocols: KEY_AGREEMENT_LIST, + hashes: HASHES_LIST, + message_authentication_codes: MAC_LIST, + short_authentication_string: SAS_LIST, + }; + await this.channel.send(VerificationEventTypes.Start, content, log); + this.hasSentStartMessage = true; + } + + get type() { + return "m.key.verification.request"; + } +} diff --git a/src/matrix/verification/SAS/stages/AcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts similarity index 68% rename from src/matrix/verification/SAS/stages/AcceptVerificationStage.ts rename to src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts index a8d320467c..9b1b2fcc1c 100644 --- a/src/matrix/verification/SAS/stages/AcceptVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts @@ -15,30 +15,18 @@ limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import anotherjson from "another-json"; - -// From element-web -type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; -type MacMethod = "hkdf-hmac-sha256.v2" | "org.matrix.msc3783.hkdf-hmac-sha256" | "hkdf-hmac-sha256" | "hmac-sha256"; - -const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; -const HASHES_LIST = ["sha256"]; -const MAC_LIST: MacMethod[] = [ - "hkdf-hmac-sha256.v2", - "org.matrix.msc3783.hkdf-hmac-sha256", - "hkdf-hmac-sha256", - "hmac-sha256", -]; -const SAS_LIST = ["decimal", "emoji"]; -const SAS_SET = new Set(SAS_LIST); - -export class AcceptVerificationStage extends BaseSASVerificationStage { +import type { KeyAgreement, MacMethod } from "./constants"; +import {HASHES_LIST, MAC_LIST, SAS_SET, KEY_AGREEMENT_LIST} from "./constants"; +import { VerificationEventTypes } from "../channel/types"; +import { SendKeyStage } from "./SendKeyStage"; +export class SendAcceptVerificationStage extends BaseSASVerificationStage { async completeStage() { - await this.log.wrap("AcceptVerificationStage.completeStage", async (log) => { - const event = this.previousResult["m.key.verification.start"]; + await this.log.wrap("SAcceptVerificationStage.completeStage", async (log) => { + const event = this.channel.startMessage; const content = { ...event.content, - "m.relates_to": event.relation, + // "m.relates_to": event.relation, }; console.log("content from event", content); const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0]; @@ -63,12 +51,16 @@ export class AcceptVerificationStage extends BaseSASVerificationStage { rel_type: "m.reference", } }; - await this.room.sendEvent("m.key.verification.accept", contentToSend, null, log); - this.nextStage?.setResultFromPreviousStage({ - ...this.previousResult, - [this.type]: contentToSend, - "our_pub_key": ourPubKey, - }); + // await this.room.sendEvent("m.key.verification.accept", contentToSend, null, log); + await this.channel.send(VerificationEventTypes.Accept, contentToSend, log); + this.channel.localMessages.set("our_pub_key", ourPubKey); + await this.channel.waitForEvent(VerificationEventTypes.Key); + this._nextStage = new SendKeyStage(this.options); + // this.nextStage?.setResultFromPreviousStage({ + // ...this.previousResult, + // [this.type]: contentToSend, + // "our_pub_key": ourPubKey, + // }); this.dispose(); }); } diff --git a/src/matrix/verification/SAS/stages/SendKeyStage.ts b/src/matrix/verification/SAS/stages/SendKeyStage.ts index f4e6d74337..199ec5d62b 100644 --- a/src/matrix/verification/SAS/stages/SendKeyStage.ts +++ b/src/matrix/verification/SAS/stages/SendKeyStage.ts @@ -16,6 +16,7 @@ limitations under the License. import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {generateEmojiSas} from "../generator"; import {ILogItem} from "../../../../lib"; +import { VerificationEventTypes } from "../channel/types"; // From element-web type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; @@ -41,31 +42,30 @@ type SASUserInfo = { type SASUserInfoCollection = { our: SASUserInfo; their: SASUserInfo; - requestId: string; + id: string; + initiatedByMe: boolean; }; const calculateKeyAgreement = { // eslint-disable-next-line @typescript-eslint/naming-convention "curve25519-hkdf-sha256": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array { - console.log("sas.requestId", sas.requestId); + console.log("sas.requestId", sas.id); const ourInfo = `${sas.our.userId}|${sas.our.deviceId}|` + `${sas.our.publicKey}|`; const theirInfo = `${sas.their.userId}|${sas.their.deviceId}|${sas.their.publicKey}|`; console.log("ourInfo", ourInfo); console.log("theirInfo", theirInfo); - const initiatedByMe = false; const sasInfo = "MATRIX_KEY_VERIFICATION_SAS|" + - (initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.requestId; + (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.id; console.log("sasInfo", sasInfo); return olmSAS.generate_bytes(sasInfo, bytes); }, "curve25519": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array { const ourInfo = `${sas.our.userId}${sas.our.deviceId}`; const theirInfo = `${sas.their.userId}${sas.their.deviceId}`; - const initiatedByMe = false; const sasInfo = "MATRIX_KEY_VERIFICATION_SAS" + - (initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.requestId; + (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.id; return olmSAS.generate_bytes(sasInfo, bytes); }, } as const; @@ -87,17 +87,18 @@ export class SendKeyStage extends BaseSASVerificationStage { private async sendKey(key: string, log: ILogItem): Promise { const contentToSend = { key, - "m.relates_to": { - event_id: this.requestEventId, - rel_type: "m.reference", - }, + // "m.relates_to": { + // event_id: this.requestEventId, + // rel_type: "m.reference", + // }, }; - await this.room.sendEvent("m.key.verification.key", contentToSend, null, log); + await this.channel.send(VerificationEventTypes.Key, contentToSend, log); + // await this.room.sendEvent("m.key.verification.key", contentToSend, null, log); } private generateSASBytes(): Uint8Array { - const keyAgreement = this.previousResult["m.key.verification.accept"].key_agreement_protocol; - const otherUserDeviceId = this.previousResult["m.key.verification.start"].content.from_device; + const keyAgreement = this.channel.sentMessages.get(VerificationEventTypes.Accept).key_agreement_protocol; + const otherUserDeviceId = this.channel.startMessage.content.from_device; const sasBytes = calculateKeyAgreement[keyAgreement]({ our: { userId: this.ourUser.userId, @@ -109,7 +110,8 @@ export class SendKeyStage extends BaseSASVerificationStage { deviceId: otherUserDeviceId, publicKey: this.theirKey, }, - requestId: this.requestEventId, + id: this.channel.id, + initiatedByMe: this.channel.initiatedByUs, }, this.olmSAS, 6); return sasBytes; } @@ -119,7 +121,8 @@ export class SendKeyStage extends BaseSASVerificationStage { } get theirKey(): string { - return this.previousResult["m.key.verification.key"].content.key; + const { content } = this.channel.receivedMessages.get(VerificationEventTypes.Key); + return content.key; } } diff --git a/src/matrix/verification/SAS/stages/constants.ts b/src/matrix/verification/SAS/stages/constants.ts new file mode 100644 index 0000000000..8112ce4453 --- /dev/null +++ b/src/matrix/verification/SAS/stages/constants.ts @@ -0,0 +1,14 @@ +// From element-web +export type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; +export type MacMethod = "hkdf-hmac-sha256.v2" | "org.matrix.msc3783.hkdf-hmac-sha256" | "hkdf-hmac-sha256" | "hmac-sha256"; + +export const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; +export const HASHES_LIST = ["sha256"]; +export const MAC_LIST: MacMethod[] = [ + "hkdf-hmac-sha256.v2", + "org.matrix.msc3783.hkdf-hmac-sha256", + "hkdf-hmac-sha256", + "hmac-sha256", +]; +export const SAS_LIST = ["decimal", "emoji"]; +export const SAS_SET = new Set(SAS_LIST); From daf66e1d6c742df080bc269019c2cf649c1ef1ea Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 2 Mar 2023 15:02:42 +0100 Subject: [PATCH 018/168] implement signing users and other devices --- src/matrix/e2ee/Account.js | 2 +- src/matrix/e2ee/DeviceTracker.ts | 8 +- src/matrix/e2ee/common.ts | 4 +- .../idb/stores/CrossSigningKeyStore.ts | 11 +- src/matrix/verification/CrossSigning.ts | 124 +++++++++++++----- src/matrix/verification/common.ts | 20 +-- 6 files changed, 101 insertions(+), 68 deletions(-) diff --git a/src/matrix/e2ee/Account.js b/src/matrix/e2ee/Account.js index b0dd15461e..8fa2db0254 100644 --- a/src/matrix/e2ee/Account.js +++ b/src/matrix/e2ee/Account.js @@ -259,7 +259,7 @@ export class Account { return obj; } - getDeviceKeysToSignWithCrossSigning() { + getUnsignedDeviceKey() { const identityKeys = JSON.parse(this._account.identity_keys()); return this._keysAsSignableObject(identityKeys); } diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index c8e9df096f..2b2728e15f 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -19,7 +19,7 @@ import {HistoryVisibility, shouldShareKey, DeviceKey, getDeviceEd25519Key, getDe import {RoomMember} from "../room/members/RoomMember.js"; import {getKeyUsage, getKeyEd25519Key, getKeyUserId, KeyUsage} from "../verification/CrossSigning"; import {MemberChange} from "../room/members/RoomMember"; -import type {CrossSigningKey} from "../storage/idb/stores/CrossSigningKeyStore"; +import type {CrossSigningKey} from "../verification/CrossSigning"; import type {HomeServerApi} from "../net/HomeServerApi"; import type {ObservableMap} from "../../observable/map"; import type {Room} from "../room/Room"; @@ -160,7 +160,7 @@ export class DeviceTracker { } } - async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi, log: ILogItem) { + async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi, log: ILogItem): Promise { return await log.wrap({l: "DeviceTracker.getCrossSigningKeyForUser", id: userId, usage}, async log => { let txn = await this._storage.readTxn([ this._storage.storeNames.userIdentities, @@ -495,6 +495,7 @@ export class DeviceTracker { /** * Can be used to decide which users to share keys with. * Assumes room is already tracked. Call `trackRoom` first if unsure. + * This will not return the device key for our own user, as we don't need to share keys with ourselves. */ async devicesForRoomMembers(roomId: string, userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { const txn = await this._storage.readTxn([ @@ -506,6 +507,7 @@ export class DeviceTracker { /** * Cannot be used to decide which users to share keys with. * Does not assume membership to any room or whether any room is tracked. + * This will return device keys for our own user, including our own device. */ async devicesForUsers(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { const txn = await this._storage.readTxn([ @@ -527,7 +529,7 @@ export class DeviceTracker { return this._devicesForUserIdentities(upToDateIdentities, outdatedUserIds, hsApi, log); } - /** gets a single device */ + /** Gets a single device */ async deviceForId(userId: string, deviceId: string, hsApi: HomeServerApi, log: ILogItem) { const txn = await this._storage.readTxn([ this._storage.storeNames.deviceKeys, diff --git a/src/matrix/e2ee/common.ts b/src/matrix/e2ee/common.ts index 63cb389db9..27078135d9 100644 --- a/src/matrix/e2ee/common.ts +++ b/src/matrix/e2ee/common.ts @@ -39,7 +39,7 @@ export class DecryptionError extends Error { export const SIGNATURE_ALGORITHM = "ed25519"; export type SignedValue = { - signatures: {[userId: string]: {[keyId: string]: string}} + signatures?: {[userId: string]: {[keyId: string]: string}} unsigned?: object } @@ -64,7 +64,7 @@ export function getDeviceCurve25519Key(deviceKey: DeviceKey): string { return deviceKey.keys[`curve25519:${deviceKey.device_id}`]; } -export function getEd25519Signature(signedValue: SignedValue, userId: string, deviceOrKeyId: string) { +export function getEd25519Signature(signedValue: SignedValue, userId: string, deviceOrKeyId: string): string | undefined { return signedValue?.signatures?.[userId]?.[`${SIGNATURE_ALGORITHM}:${deviceOrKeyId}`]; } diff --git a/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts b/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts index dc5804ef66..32100acadb 100644 --- a/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts +++ b/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts @@ -16,16 +16,7 @@ limitations under the License. import {MAX_UNICODE, MIN_UNICODE} from "./common"; import {Store} from "../Store"; -import type {SignedValue} from "../../../e2ee/common"; - -// we store cross-signing (and device) keys in the format we get them from the server -// as that is what the signature is calculated on, so to verify and sign, we need -// it in this format anyway. -export type CrossSigningKey = SignedValue & { - readonly user_id: string; - readonly usage: ReadonlyArray; - readonly keys: {[keyId: string]: string}; -} +import type {CrossSigningKey} from "../../../verification/CrossSigning"; type CrossSigningKeyEntry = CrossSigningKey & { key: string; // key in storage, not a crypto key diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index d3b6bc904a..e58c89e87a 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -14,19 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ILogItem } from "../../lib"; +import {pkSign} from "./common"; + import type {SecretStorage} from "../ssss/SecretStorage"; import type {Storage} from "../storage/idb/Storage"; import type {Platform} from "../../platform/web/Platform"; import type {DeviceTracker} from "../e2ee/DeviceTracker"; -import type * as OlmNamespace from "@matrix-org/olm"; import type {HomeServerApi} from "../net/HomeServerApi"; import type {Account} from "../e2ee/Account"; -import { ILogItem } from "../../lib"; -import {pkSign} from "./common"; -import type {ISignatures} from "./common"; - +import type {SignedValue, DeviceKey} from "../e2ee/common"; +import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; +// we store cross-signing (and device) keys in the format we get them from the server +// as that is what the signature is calculated on, so to verify and sign, we need +// it in this format anyway. +export type CrossSigningKey = SignedValue & { + readonly user_id: string; + readonly usage: ReadonlyArray; + readonly keys: {[keyId: string]: string}; +} + export enum KeyUsage { Master = "master", SelfSigning = "self_signing", @@ -68,63 +77,108 @@ export class CrossSigning { log.wrap("CrossSigning.init", async log => { // TODO: use errorboundary here const txn = await this.storage.readTxn([this.storage.storeNames.accountData]); - - const mskSeed = await this.secretStorage.readSecret("m.cross_signing.master", txn); + const privateMasterKey = await this.getSigningKey(KeyUsage.Master); const signing = new this.olm.PkSigning(); let derivedPublicKey; try { - const seed = new Uint8Array(this.platform.encoding.base64.decode(mskSeed)); - derivedPublicKey = signing.init_with_seed(seed); + derivedPublicKey = signing.init_with_seed(privateMasterKey); } finally { signing.free(); } - const masterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log); - log.set({publishedMasterKey: masterKey, derivedPublicKey}); - this._isMasterKeyTrusted = masterKey === derivedPublicKey; + const publishedMasterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log); + const publisedEd25519Key = publishedMasterKey && getKeyEd25519Key(publishedMasterKey); + log.set({publishedMasterKey: publisedEd25519Key, derivedPublicKey}); + this._isMasterKeyTrusted = !!publisedEd25519Key && publisedEd25519Key === derivedPublicKey; log.set("isMasterKeyTrusted", this.isMasterKeyTrusted); }); } - async signOwnDevice(log: ILogItem) { - log.wrap("CrossSigning.signOwnDevice", async log => { + get isMasterKeyTrusted(): boolean { + return this._isMasterKeyTrusted; + } + + /** returns our own device key signed by our self-signing key. Other signatures will be missing. */ + async signOwnDevice(log: ILogItem): Promise { + return log.wrap("CrossSigning.signOwnDevice", async log => { if (!this._isMasterKeyTrusted) { log.set("mskNotTrusted", true); return; } - const deviceKey = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning(); - const signedDeviceKey = await this.signDeviceData(deviceKey); + const ownDeviceKey = this.e2eeAccount.getUnsignedDeviceKey() as DeviceKey; + return this.signDeviceKey(ownDeviceKey, log); + }); + } + + /** @return the signed device key for the given device id */ + async signDevice(deviceId: string, log: ILogItem): Promise { + return log.wrap("CrossSigning.signDevice", async log => { + log.set("id", deviceId); + if (!this._isMasterKeyTrusted) { + log.set("mskNotTrusted", true); + return; + } + // need to be able to get the msk for the user + const keyToSign = await this.deviceTracker.deviceForId(this.ownUserId, deviceId, this.hsApi, log); + if (!keyToSign) { + return undefined; + } + return this.signDeviceKey(keyToSign, log); + }); + } + + /** @return the signed MSK for the given user id */ + async signUser(userId: string, log: ILogItem): Promise { + return log.wrap("CrossSigning.signUser", async log => { + log.set("id", userId); + if (!this._isMasterKeyTrusted) { + log.set("mskNotTrusted", true); + return; + } + // need to be able to get the msk for the user + const keyToSign = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log); + if (!keyToSign) { + return undefined; + } + const signingKey = await this.getSigningKey(KeyUsage.UserSigning); + // add signature to keyToSign + pkSign(this.olm, keyToSign, signingKey, userId, ""); const payload = { - [signedDeviceKey["user_id"]]: { - [signedDeviceKey["device_id"]]: signedDeviceKey + [keyToSign.user_id]: { + [getKeyEd25519Key(keyToSign)!]: keyToSign } }; const request = this.hsApi.uploadSignatures(payload, {log}); await request.response(); + return keyToSign; }); } - signDevice(deviceId: string) { - // need to get the device key for the device - } - - signUser(userId: string) { - // need to be able to get the msk for the user + private async signDeviceKey(keyToSign: DeviceKey, log: ILogItem): Promise { + const signingKey = await this.getSigningKey(KeyUsage.SelfSigning); + // add signature to keyToSign + pkSign(this.olm, keyToSign, signingKey, this.ownUserId, ""); + // so the payload format of a signature is a map from userid to key id of the signed key + // (without the algoritm prefix though according to example, e.g. just device id or base 64 public key) + // to the complete signed key with the signature of the signing key in the signatures section. + const payload = { + [keyToSign.user_id]: { + [keyToSign.device_id]: keyToSign + } + }; + const request = this.hsApi.uploadSignatures(payload, {log}); + await request.response(); + return keyToSign; } - private async signDeviceData(data: T): Promise { + private async getSigningKey(usage: KeyUsage): Promise { const txn = await this.storage.readTxn([this.storage.storeNames.accountData]); - const seedStr = await this.secretStorage.readSecret(`m.cross_signing.self_signing`, txn); + const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`, txn); const seed = new Uint8Array(this.platform.encoding.base64.decode(seedStr)); - pkSign(this.olm, data, seed, this.ownUserId, ""); - return data as T & { signatures: ISignatures }; - } - - get isMasterKeyTrusted(): boolean { - return this._isMasterKeyTrusted; + return seed; } } -export function getKeyUsage(keyInfo): KeyUsage | undefined { +export function getKeyUsage(keyInfo: CrossSigningKey): KeyUsage | undefined { if (!Array.isArray(keyInfo.usage) || keyInfo.usage.length !== 1) { return undefined; } @@ -138,7 +192,7 @@ export function getKeyUsage(keyInfo): KeyUsage | undefined { const algorithm = "ed25519"; const prefix = `${algorithm}:`; -export function getKeyEd25519Key(keyInfo): string | undefined { +export function getKeyEd25519Key(keyInfo: CrossSigningKey): string | undefined { const ed25519KeyIds = Object.keys(keyInfo.keys).filter(keyId => keyId.startsWith(prefix)); if (ed25519KeyIds.length !== 1) { return undefined; @@ -148,6 +202,6 @@ export function getKeyEd25519Key(keyInfo): string | undefined { return publicKey; } -export function getKeyUserId(keyInfo): string | undefined { +export function getKeyUserId(keyInfo: CrossSigningKey): string | undefined { return keyInfo["user_id"]; } diff --git a/src/matrix/verification/common.ts b/src/matrix/verification/common.ts index 369b561826..de9b1b1b97 100644 --- a/src/matrix/verification/common.ts +++ b/src/matrix/verification/common.ts @@ -16,24 +16,10 @@ limitations under the License. import { PkSigning } from "@matrix-org/olm"; import anotherjson from "another-json"; +import type {SignedValue} from "../e2ee/common"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; -export interface IObject { - unsigned?: object; - signatures?: ISignatures; -} - -export interface ISignatures { - [entity: string]: { - [keyId: string]: string; - }; -} - -export interface ISigned { - signatures?: ISignatures; -} - // from matrix-js-sdk /** * Sign a JSON object using public key cryptography @@ -45,7 +31,7 @@ export interface ISigned { * @param pubKey - The public key (ignored if key is a seed) * @returns the signature for the object */ - export function pkSign(olmUtil: Olm, obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string { + export function pkSign(olmUtil: Olm, obj: SignedValue, key: Uint8Array | PkSigning, userId: string, pubKey: string): string { let createdKey = false; if (key instanceof Uint8Array) { const keyObj = new olmUtil.PkSigning(); @@ -69,4 +55,4 @@ export interface ISigned { key.free(); } } -} \ No newline at end of file +} From a9412aa57c3b35780202b297576444dac2fe5b04 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:12:56 +0100 Subject: [PATCH 019/168] fix import paths after TS conversion --- src/matrix/DeviceMessageHandler.js | 2 +- src/matrix/Session.js | 4 ++-- src/matrix/e2ee/DeviceTracker.ts | 2 +- src/matrix/e2ee/RoomEncryption.js | 2 +- src/matrix/e2ee/megolm/Decryption.ts | 3 +-- src/matrix/e2ee/megolm/Encryption.js | 2 +- src/matrix/e2ee/megolm/decryption/DecryptionChanges.js | 2 +- src/matrix/e2ee/megolm/decryption/SessionDecryption.ts | 2 +- src/matrix/e2ee/olm/Decryption.ts | 2 +- src/matrix/e2ee/olm/Encryption.ts | 2 +- src/matrix/room/BaseRoom.js | 2 +- src/matrix/room/Room.js | 2 +- src/matrix/room/RoomSummary.js | 2 +- src/matrix/ssss/index.ts | 2 +- src/matrix/storage/idb/schema.ts | 2 +- src/matrix/storage/idb/stores/SessionStore.ts | 2 +- 16 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/matrix/DeviceMessageHandler.js b/src/matrix/DeviceMessageHandler.js index f6e7cad7f1..78b384ab8d 100644 --- a/src/matrix/DeviceMessageHandler.js +++ b/src/matrix/DeviceMessageHandler.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {OLM_ALGORITHM} from "./e2ee/common.js"; +import {OLM_ALGORITHM} from "./e2ee/common"; import {countBy, groupBy} from "../utils/groupBy"; import {LRUCache} from "../utils/LRUCache"; diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 35f713f658..9aa0494d4a 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -33,9 +33,9 @@ import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader"; import {KeyBackup} from "./e2ee/megolm/keybackup/KeyBackup"; import {CrossSigning} from "./verification/CrossSigning"; import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js"; -import {MEGOLM_ALGORITHM} from "./e2ee/common.js"; +import {MEGOLM_ALGORITHM} from "./e2ee/common"; import {RoomEncryption} from "./e2ee/RoomEncryption.js"; -import {DeviceTracker} from "./e2ee/DeviceTracker.js"; +import {DeviceTracker} from "./e2ee/DeviceTracker"; import {LockMap} from "../utils/LockMap"; import {groupBy} from "../utils/groupBy"; import { diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 2b2728e15f..0464377ab6 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {verifyEd25519Signature, getEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js"; +import {verifyEd25519Signature, getEd25519Signature, SIGNATURE_ALGORITHM} from "./common"; import {HistoryVisibility, shouldShareKey, DeviceKey, getDeviceEd25519Key, getDeviceCurve25519Key} from "./common"; import {RoomMember} from "../room/members/RoomMember.js"; import {getKeyUsage, getKeyEd25519Key, getKeyUserId, KeyUsage} from "../verification/CrossSigning"; diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 4711289230..bd0defaccd 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MEGOLM_ALGORITHM, DecryptionSource} from "./common.js"; +import {MEGOLM_ALGORITHM, DecryptionSource} from "./common"; import {groupEventsBySession} from "./megolm/decryption/utils"; import {mergeMap} from "../../utils/mergeMap"; import {groupBy} from "../../utils/groupBy"; diff --git a/src/matrix/e2ee/megolm/Decryption.ts b/src/matrix/e2ee/megolm/Decryption.ts index e139e8c9a0..c2d562074f 100644 --- a/src/matrix/e2ee/megolm/Decryption.ts +++ b/src/matrix/e2ee/megolm/Decryption.ts @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {DecryptionError} from "../common.js"; import {DecryptionPreparation} from "./decryption/DecryptionPreparation.js"; import {SessionDecryption} from "./decryption/SessionDecryption"; -import {MEGOLM_ALGORITHM} from "../common.js"; +import {DecryptionError, MEGOLM_ALGORITHM} from "../common"; import {validateEvent, groupEventsBySession} from "./decryption/utils"; import {keyFromStorage, keyFromDeviceMessage, keyFromBackup} from "./decryption/RoomKey"; import type {RoomKey, IncomingRoomKey} from "./decryption/RoomKey"; diff --git a/src/matrix/e2ee/megolm/Encryption.js b/src/matrix/e2ee/megolm/Encryption.js index eb5f68d304..681344fe98 100644 --- a/src/matrix/e2ee/megolm/Encryption.js +++ b/src/matrix/e2ee/megolm/Encryption.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MEGOLM_ALGORITHM} from "../common.js"; +import {MEGOLM_ALGORITHM} from "../common"; import {OutboundRoomKey} from "./decryption/RoomKey"; export class Encryption { diff --git a/src/matrix/e2ee/megolm/decryption/DecryptionChanges.js b/src/matrix/e2ee/megolm/decryption/DecryptionChanges.js index b45ab6dd94..24226e25b0 100644 --- a/src/matrix/e2ee/megolm/decryption/DecryptionChanges.js +++ b/src/matrix/e2ee/megolm/decryption/DecryptionChanges.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {DecryptionError} from "../../common.js"; +import {DecryptionError} from "../../common"; export class DecryptionChanges { constructor(roomId, results, errors, replayEntries) { diff --git a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts index ca294460c6..72af718cdb 100644 --- a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts +++ b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {DecryptionResult} from "../../DecryptionResult"; -import {DecryptionError} from "../../common.js"; +import {DecryptionError} from "../../common"; import {ReplayDetectionEntry} from "./ReplayDetectionEntry"; import type {RoomKey} from "./RoomKey"; import type {KeyLoader, OlmDecryptionResult} from "./KeyLoader"; diff --git a/src/matrix/e2ee/olm/Decryption.ts b/src/matrix/e2ee/olm/Decryption.ts index 0f96f2fc6e..e1546b0b2b 100644 --- a/src/matrix/e2ee/olm/Decryption.ts +++ b/src/matrix/e2ee/olm/Decryption.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {DecryptionError} from "../common.js"; +import {DecryptionError} from "../common"; import {groupBy} from "../../../utils/groupBy"; import {MultiLock, ILock} from "../../../utils/Lock"; import {Session} from "./Session"; diff --git a/src/matrix/e2ee/olm/Encryption.ts b/src/matrix/e2ee/olm/Encryption.ts index 0b55238769..c4fee911f7 100644 --- a/src/matrix/e2ee/olm/Encryption.ts +++ b/src/matrix/e2ee/olm/Encryption.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {groupByWithCreator} from "../../../utils/groupBy"; -import {verifyEd25519Signature, OLM_ALGORITHM, getDeviceCurve25519Key, getDeviceEd25519Key} from "../common.js"; +import {verifyEd25519Signature, OLM_ALGORITHM, getDeviceCurve25519Key, getDeviceEd25519Key} from "../common"; import {createSessionEntry} from "./Session"; import type {OlmMessage, OlmPayload, OlmEncryptedMessageContent} from "./types"; diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 9931be835e..7ab8a209dc 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -26,7 +26,7 @@ import {MemberList} from "./members/MemberList.js"; import {Heroes} from "./members/Heroes.js"; import {EventEntry} from "./timeline/entries/EventEntry.js"; import {ObservedEventMap} from "./ObservedEventMap.js"; -import {DecryptionSource} from "../e2ee/common.js"; +import {DecryptionSource} from "../e2ee/common"; import {ensureLogItem} from "../../logging/utils"; import {PowerLevels} from "./PowerLevels.js"; import {RetainedObservableValue} from "../../observable/value"; diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index b87d7a88cd..47da3c03af 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -22,7 +22,7 @@ import {SendQueue} from "./sending/SendQueue.js"; import {WrappedError} from "../error.js" import {Heroes} from "./members/Heroes.js"; import {AttachmentUpload} from "./AttachmentUpload.js"; -import {DecryptionSource} from "../e2ee/common.js"; +import {DecryptionSource} from "../e2ee/common"; import {iterateResponseStateEvents} from "./common"; import {PowerLevels, EVENT_TYPE as POWERLEVELS_EVENT_TYPE } from "./PowerLevels.js"; diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index 6260868332..8e1619cae7 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MEGOLM_ALGORITHM} from "../e2ee/common.js"; +import {MEGOLM_ALGORITHM} from "../e2ee/common"; import {iterateResponseStateEvents} from "./common"; function applyTimelineEntries(data, timelineEntries, isInitialSync, canMarkUnread, ownUserId) { diff --git a/src/matrix/ssss/index.ts b/src/matrix/ssss/index.ts index fd4c22456c..02f3290e70 100644 --- a/src/matrix/ssss/index.ts +++ b/src/matrix/ssss/index.ts @@ -17,7 +17,7 @@ limitations under the License. import {KeyDescription, Key} from "./common"; import {keyFromPassphrase} from "./passphrase"; import {keyFromRecoveryKey} from "./recoveryKey"; -import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common.js"; +import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common"; import type {Storage} from "../storage/idb/Storage"; import type {Transaction} from "../storage/idb/Transaction"; import type {KeyDescriptionData} from "./common"; diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 200f4089ed..c8d260cd71 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -2,7 +2,7 @@ import {IDOMStorage} from "./types"; import {ITransaction} from "./QueryTarget"; import {iterateCursor, NOT_DONE, reqAsPromise} from "./utils"; import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js"; -import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common.js"; +import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common"; import {SummaryData} from "../../room/RoomSummary"; import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore"; import {InboundGroupSessionStore, InboundGroupSessionEntry, BackupStatus, KeySource} from "./stores/InboundGroupSessionStore"; diff --git a/src/matrix/storage/idb/stores/SessionStore.ts b/src/matrix/storage/idb/stores/SessionStore.ts index 9ae9bb7e21..24b7099abf 100644 --- a/src/matrix/storage/idb/stores/SessionStore.ts +++ b/src/matrix/storage/idb/stores/SessionStore.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {Store} from "../Store"; import {IDOMStorage} from "../types"; -import {SESSION_E2EE_KEY_PREFIX} from "../../../e2ee/common.js"; +import {SESSION_E2EE_KEY_PREFIX} from "../../../e2ee/common"; import {parse, stringify} from "../../../../utils/typedJSON"; import type {ILogItem} from "../../../../logging/types"; From 4dce93e5ef5dcecce31556acdfd3bb4ab417d3cf Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:13:15 +0100 Subject: [PATCH 020/168] make sure the key property doesn't leak out of the storage layer as it ends up in the value we're signing and uploading, corrupting the signature --- .../storage/idb/stores/CrossSigningKeyStore.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts b/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts index 32100acadb..bbda15c05d 100644 --- a/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts +++ b/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts @@ -18,7 +18,8 @@ import {MAX_UNICODE, MIN_UNICODE} from "./common"; import {Store} from "../Store"; import type {CrossSigningKey} from "../../../verification/CrossSigning"; -type CrossSigningKeyEntry = CrossSigningKey & { +type CrossSigningKeyEntry = { + crossSigningKey: CrossSigningKey key: string; // key in storage, not a crypto key } @@ -38,14 +39,15 @@ export class CrossSigningKeyStore { this._store = store; } - get(userId: string, deviceId: string): Promise { - return this._store.get(encodeKey(userId, deviceId)); + async get(userId: string, deviceId: string): Promise { + return (await this._store.get(encodeKey(userId, deviceId)))?.crossSigningKey; } set(crossSigningKey: CrossSigningKey): void { - const deviceIdentityEntry = crossSigningKey as CrossSigningKeyEntry; - deviceIdentityEntry.key = encodeKey(crossSigningKey["user_id"], crossSigningKey.usage[0]); - this._store.put(deviceIdentityEntry); + this._store.put({ + key:encodeKey(crossSigningKey["user_id"], crossSigningKey.usage[0]), + crossSigningKey + }); } remove(userId: string, usage: string): void { From 20a6fcda72e65f42f9d46846535351134e54b466 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:14:05 +0100 Subject: [PATCH 021/168] don't allow signing own user --- src/matrix/verification/CrossSigning.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index e58c89e87a..a3bd5f4778 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -135,6 +135,10 @@ export class CrossSigning { return; } // need to be able to get the msk for the user + // can't sign own user + if (userId === this.ownUserId) { + return; + } const keyToSign = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log); if (!keyToSign) { return undefined; From 504d869b385160620d04ea05000af7ddaf7d99f7 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:14:27 +0100 Subject: [PATCH 022/168] provide correct user id for signing key owner when signing other user --- src/matrix/verification/CrossSigning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index a3bd5f4778..6430c0a7bd 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -145,7 +145,7 @@ export class CrossSigning { } const signingKey = await this.getSigningKey(KeyUsage.UserSigning); // add signature to keyToSign - pkSign(this.olm, keyToSign, signingKey, userId, ""); + pkSign(this.olm, keyToSign, signingKey, this.ownUserId, ""); const payload = { [keyToSign.user_id]: { [getKeyEd25519Key(keyToSign)!]: keyToSign From 34b113b26eb6eb6053f7b1a2bb5b648535310922 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:14:50 +0100 Subject: [PATCH 023/168] don't upload pre-existing signatures when signing --- src/matrix/verification/CrossSigning.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 6430c0a7bd..67a5616abf 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -122,6 +122,7 @@ export class CrossSigning { if (!keyToSign) { return undefined; } + delete keyToSign.signatures; return this.signDeviceKey(keyToSign, log); }); } @@ -143,6 +144,7 @@ export class CrossSigning { if (!keyToSign) { return undefined; } + delete keyToSign.signatures; const signingKey = await this.getSigningKey(KeyUsage.UserSigning); // add signature to keyToSign pkSign(this.olm, keyToSign, signingKey, this.ownUserId, ""); From 3a303ff84d9b6ea160ed8b2d587ad07da9b20c0c Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:15:05 +0100 Subject: [PATCH 024/168] cleanup comments --- src/matrix/verification/CrossSigning.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 67a5616abf..77b489d27e 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -117,7 +117,6 @@ export class CrossSigning { log.set("mskNotTrusted", true); return; } - // need to be able to get the msk for the user const keyToSign = await this.deviceTracker.deviceForId(this.ownUserId, deviceId, this.hsApi, log); if (!keyToSign) { return undefined; @@ -135,7 +134,6 @@ export class CrossSigning { log.set("mskNotTrusted", true); return; } - // need to be able to get the msk for the user // can't sign own user if (userId === this.ownUserId) { return; From fa662db70b0c34b54cf1fa4c5d2ea86ee679046b Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:16:53 +0100 Subject: [PATCH 025/168] show cross-sign user option in right panel --- .../rightpanel/MemberDetailsViewModel.js | 8 ++++++++ .../web/ui/css/themes/element/theme.css | 1 + .../ui/session/rightpanel/MemberDetailsView.js | 18 +++++++++++++----- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index b3c8278c96..774f64af62 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -54,6 +54,14 @@ export class MemberDetailsViewModel extends ViewModel { this.emitChange("role"); } + async signUser() { + if (this._session.crossSigning) { + await this.logger.run("MemberDetailsViewModel.signUser", async log => { + await this._session.crossSigning.signUser(this.userId, log); + }); + } + } + get avatarLetter() { return avatarInitials(this.name); } diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 4c617386fa..ca64e15a12 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1182,6 +1182,7 @@ button.RoomDetailsView_row::after { border: none; background: none; cursor: pointer; + text-align: left; } .LazyListParent { diff --git a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js index 5d2f9387e3..72bd9e37e4 100644 --- a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js +++ b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js @@ -41,14 +41,22 @@ export class MemberDetailsView extends TemplateView { } _createOptions(t, vm) { + const options = [ + t.a({href: vm.linkToUser, target: "_blank", rel: "noopener"}, vm.i18n`Open Link to User`), + t.button({className: "text", onClick: () => vm.openDirectMessage()}, vm.i18n`Open direct message`) + ]; + if (vm.features.crossSigning) { + const onClick = () => { + if (confirm("You don't want to do this with any account but a test account. This will cross-sign this user without verifying their keys first. You won't be able to undo this apart from resetting your cross-signing keys.")) { + vm.signUser(); + } + } + options.push(t.button({className: "text", onClick}, vm.i18n`Cross-sign user (DO NOT USE, TESTING ONLY)`)) + } return t.div({ className: "MemberDetailsView_section" }, [ t.div({className: "MemberDetailsView_label"}, vm.i18n`Options`), - t.div({className: "MemberDetailsView_options"}, - [ - t.a({href: vm.linkToUser, target: "_blank", rel: "noopener"}, vm.i18n`Open Link to User`), - t.button({className: "text", onClick: () => vm.openDirectMessage()}, vm.i18n`Open direct message`) - ]) + t.div({className: "MemberDetailsView_options"}, options) ]); } } From 9789e5881d9165dbeb598fef60c22492861afd58 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:29:30 +0100 Subject: [PATCH 026/168] cleanup --- src/platform/web/ui/session/rightpanel/MemberDetailsView.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js index 72bd9e37e4..caa8037fdb 100644 --- a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js +++ b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js @@ -47,10 +47,10 @@ export class MemberDetailsView extends TemplateView { ]; if (vm.features.crossSigning) { const onClick = () => { - if (confirm("You don't want to do this with any account but a test account. This will cross-sign this user without verifying their keys first. You won't be able to undo this apart from resetting your cross-signing keys.")) { - vm.signUser(); - } + if (confirm("You don't want to do this with any account but a test account. This will cross-sign this user without verifying their keys first. You won't be able to undo this apart from resetting your cross-signing keys.")) { + vm.signUser(); } + }; options.push(t.button({className: "text", onClick}, vm.i18n`Cross-sign user (DO NOT USE, TESTING ONLY)`)) } return t.div({ className: "MemberDetailsView_section" }, From 1dc3acad036e01f5b8880ebaccaf65d16e7b2c2b Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:32:46 +0100 Subject: [PATCH 027/168] use enum for device tracking status --- src/matrix/e2ee/DeviceTracker.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 0464377ab6..bbfec900b7 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -29,20 +29,22 @@ import type {Transaction} from "../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; -const TRACKING_STATUS_OUTDATED = 0; -const TRACKING_STATUS_UPTODATE = 1; +enum DeviceTrackingStatus { + Outdated = 0, + UpToDate = 1 +} export type UserIdentity = { userId: string, roomIds: string[], - deviceTrackingStatus: number, + deviceTrackingStatus: DeviceTrackingStatus, } function createUserIdentity(userId: string, initialRoomId?: string): UserIdentity { return { userId: userId, roomIds: initialRoomId ? [initialRoomId] : [], - deviceTrackingStatus: TRACKING_STATUS_OUTDATED, + deviceTrackingStatus: DeviceTrackingStatus.Outdated, }; } @@ -87,7 +89,7 @@ export class DeviceTracker { const user = await userIdentities.get(userId); if (user) { log.log({l: "outdated", id: userId}); - user.deviceTrackingStatus = TRACKING_STATUS_OUTDATED; + user.deviceTrackingStatus = DeviceTrackingStatus.Outdated; userIdentities.set(user); } })); @@ -167,7 +169,7 @@ export class DeviceTracker { this._storage.storeNames.crossSigningKeys, ]); let userIdentity = await txn.userIdentities.get(userId); - if (userIdentity && userIdentity.deviceTrackingStatus !== TRACKING_STATUS_OUTDATED) { + if (userIdentity && userIdentity.deviceTrackingStatus !== DeviceTrackingStatus.Outdated) { return await txn.crossSigningKeys.get(userId, usage); } // fetch from hs @@ -345,7 +347,7 @@ export class DeviceTracker { // checked, we could share keys with that user without them being in the room identity = createUserIdentity(userId); } - identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE; + identity.deviceTrackingStatus = DeviceTrackingStatus.UpToDate; txn.userIdentities.set(identity); return allDeviceKeys; @@ -518,9 +520,9 @@ export class DeviceTracker { const outdatedUserIds: string[] = []; await Promise.all(userIds.map(async userId => { const i = await txn.userIdentities.get(userId); - if (i && i.deviceTrackingStatus === TRACKING_STATUS_UPTODATE) { + if (i && i.deviceTrackingStatus === DeviceTrackingStatus.UpToDate) { upToDateIdentities.push(i); - } else if (!i || i.deviceTrackingStatus === TRACKING_STATUS_OUTDATED) { + } else if (!i || i.deviceTrackingStatus === DeviceTrackingStatus.Outdated) { // allow fetching for userIdentities we don't know about yet, // as we don't assume the room is tracked here. outdatedUserIds.push(userId); @@ -599,9 +601,9 @@ export class DeviceTracker { // also exclude any userId which doesn't have a userIdentity yet. return identity && identity.roomIds.includes(roomId); }) as UserIdentity[]; // undefined has been filter out - const upToDateIdentities = identities.filter(i => i.deviceTrackingStatus === TRACKING_STATUS_UPTODATE); + const upToDateIdentities = identities.filter(i => i.deviceTrackingStatus === DeviceTrackingStatus.UpToDate); const outdatedUserIds = identities - .filter(i => i.deviceTrackingStatus === TRACKING_STATUS_OUTDATED) + .filter(i => i.deviceTrackingStatus === DeviceTrackingStatus.Outdated) .map(i => i.userId); let devices = await this._devicesForUserIdentities(upToDateIdentities, outdatedUserIds, hsApi, log); // filter out our own device as we should never share keys with it. @@ -761,12 +763,12 @@ export function tests() { assert.deepEqual(await txn.userIdentities.get("@alice:hs.tld"), { userId: "@alice:hs.tld", roomIds: [roomId], - deviceTrackingStatus: TRACKING_STATUS_OUTDATED + deviceTrackingStatus: DeviceTrackingStatus.Outdated }); assert.deepEqual(await txn.userIdentities.get("@bob:hs.tld"), { userId: "@bob:hs.tld", roomIds: [roomId], - deviceTrackingStatus: TRACKING_STATUS_OUTDATED + deviceTrackingStatus: DeviceTrackingStatus.Outdated }); assert.equal(await txn.userIdentities.get("@charly:hs.tld"), undefined); }, From 7d806b03b37da83311a02b49cf3b4607d0684ebd Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 3 Mar 2023 11:33:19 +0100 Subject: [PATCH 028/168] mark all existing user identities outdated as cross-signing keys missing --- src/matrix/storage/idb/schema.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index c8d260cd71..ce825edb2b 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -13,6 +13,7 @@ import {encodeScopeTypeKey} from "./stores/OperationStore"; import {MAX_UNICODE} from "./stores/common"; import {ILogItem} from "../../../logging/types"; +import type {UserIdentity} from "../../e2ee/DeviceTracker"; export type MigrationFunc = (db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem) => Promise | void; // FUNCTIONS SHOULD ONLY BE APPENDED!! @@ -35,7 +36,7 @@ export const schema: MigrationFunc[] = [ addInboundSessionBackupIndex, migrateBackupStatus, createCallStore, - createCrossSigningKeyStoreAndRenameDeviceIdentities + applyCrossSigningChanges ]; // TODO: how to deal with git merge conflicts of this array? @@ -277,10 +278,16 @@ function createCallStore(db: IDBDatabase) : void { db.createObjectStore("calls", {keyPath: "key"}); } -//v18 create calls store and rename deviceIdentities to deviceKeys -function createCrossSigningKeyStoreAndRenameDeviceIdentities(db: IDBDatabase) : void { +//v18 add crossSigningKeys store, rename deviceIdentities to deviceKeys and empties userIdentities +async function applyCrossSigningChanges(db: IDBDatabase, txn: IDBTransaction) : Promise { db.createObjectStore("crossSigningKeys", {keyPath: "key"}); db.deleteObjectStore("deviceIdentities"); const deviceKeys = db.createObjectStore("deviceKeys", {keyPath: "key"}); deviceKeys.createIndex("byCurve25519Key", "curve25519Key", {unique: true}); + // mark all userIdentities as outdated as cross-signing keys won't be stored + const userIdentities = txn.objectStore("userIdentities"); + await iterateCursor(userIdentities.openCursor(), (value, key, cursor) => { + value.deviceTrackingStatus = 0 // outdated; + return NOT_DONE; + }); } From c747d5f22828ec8397f97308506492acd1ed81c5 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 3 Mar 2023 11:34:09 +0100 Subject: [PATCH 029/168] rename deviceTrackingStatus to keysTrackingStatus as this field also reflects the tracking status of the cross-signing keys for a given user. --- src/matrix/e2ee/DeviceTracker.ts | 25 ++++++++++--------- src/matrix/storage/idb/schema.ts | 5 +++- .../storage/idb/stores/UserIdentityStore.ts | 7 +----- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index bbfec900b7..98221523e9 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -29,7 +29,8 @@ import type {Transaction} from "../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; -enum DeviceTrackingStatus { +// tracking status for cross-signing and device keys +export enum KeysTrackingStatus { Outdated = 0, UpToDate = 1 } @@ -37,14 +38,14 @@ enum DeviceTrackingStatus { export type UserIdentity = { userId: string, roomIds: string[], - deviceTrackingStatus: DeviceTrackingStatus, + keysTrackingStatus: KeysTrackingStatus, } function createUserIdentity(userId: string, initialRoomId?: string): UserIdentity { return { userId: userId, roomIds: initialRoomId ? [initialRoomId] : [], - deviceTrackingStatus: DeviceTrackingStatus.Outdated, + keysTrackingStatus: KeysTrackingStatus.Outdated, }; } @@ -89,7 +90,7 @@ export class DeviceTracker { const user = await userIdentities.get(userId); if (user) { log.log({l: "outdated", id: userId}); - user.deviceTrackingStatus = DeviceTrackingStatus.Outdated; + user.keysTrackingStatus = KeysTrackingStatus.Outdated; userIdentities.set(user); } })); @@ -169,7 +170,7 @@ export class DeviceTracker { this._storage.storeNames.crossSigningKeys, ]); let userIdentity = await txn.userIdentities.get(userId); - if (userIdentity && userIdentity.deviceTrackingStatus !== DeviceTrackingStatus.Outdated) { + if (userIdentity && userIdentity.keysTrackingStatus !== KeysTrackingStatus.Outdated) { return await txn.crossSigningKeys.get(userId, usage); } // fetch from hs @@ -347,7 +348,7 @@ export class DeviceTracker { // checked, we could share keys with that user without them being in the room identity = createUserIdentity(userId); } - identity.deviceTrackingStatus = DeviceTrackingStatus.UpToDate; + identity.keysTrackingStatus = KeysTrackingStatus.UpToDate; txn.userIdentities.set(identity); return allDeviceKeys; @@ -520,9 +521,9 @@ export class DeviceTracker { const outdatedUserIds: string[] = []; await Promise.all(userIds.map(async userId => { const i = await txn.userIdentities.get(userId); - if (i && i.deviceTrackingStatus === DeviceTrackingStatus.UpToDate) { + if (i && i.keysTrackingStatus === KeysTrackingStatus.UpToDate) { upToDateIdentities.push(i); - } else if (!i || i.deviceTrackingStatus === DeviceTrackingStatus.Outdated) { + } else if (!i || i.keysTrackingStatus === KeysTrackingStatus.Outdated) { // allow fetching for userIdentities we don't know about yet, // as we don't assume the room is tracked here. outdatedUserIds.push(userId); @@ -601,9 +602,9 @@ export class DeviceTracker { // also exclude any userId which doesn't have a userIdentity yet. return identity && identity.roomIds.includes(roomId); }) as UserIdentity[]; // undefined has been filter out - const upToDateIdentities = identities.filter(i => i.deviceTrackingStatus === DeviceTrackingStatus.UpToDate); + const upToDateIdentities = identities.filter(i => i.keysTrackingStatus === KeysTrackingStatus.UpToDate); const outdatedUserIds = identities - .filter(i => i.deviceTrackingStatus === DeviceTrackingStatus.Outdated) + .filter(i => i.keysTrackingStatus === KeysTrackingStatus.Outdated) .map(i => i.userId); let devices = await this._devicesForUserIdentities(upToDateIdentities, outdatedUserIds, hsApi, log); // filter out our own device as we should never share keys with it. @@ -763,12 +764,12 @@ export function tests() { assert.deepEqual(await txn.userIdentities.get("@alice:hs.tld"), { userId: "@alice:hs.tld", roomIds: [roomId], - deviceTrackingStatus: DeviceTrackingStatus.Outdated + keysTrackingStatus: KeysTrackingStatus.Outdated }); assert.deepEqual(await txn.userIdentities.get("@bob:hs.tld"), { userId: "@bob:hs.tld", roomIds: [roomId], - deviceTrackingStatus: DeviceTrackingStatus.Outdated + keysTrackingStatus: KeysTrackingStatus.Outdated }); assert.equal(await txn.userIdentities.get("@charly:hs.tld"), undefined); }, diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index ce825edb2b..c4c4bd6111 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -14,6 +14,7 @@ import {MAX_UNICODE} from "./stores/common"; import {ILogItem} from "../../../logging/types"; import type {UserIdentity} from "../../e2ee/DeviceTracker"; +import {KeysTrackingStatus} from "../../e2ee/DeviceTracker"; export type MigrationFunc = (db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem) => Promise | void; // FUNCTIONS SHOULD ONLY BE APPENDED!! @@ -285,9 +286,11 @@ async function applyCrossSigningChanges(db: IDBDatabase, txn: IDBTransaction) : const deviceKeys = db.createObjectStore("deviceKeys", {keyPath: "key"}); deviceKeys.createIndex("byCurve25519Key", "curve25519Key", {unique: true}); // mark all userIdentities as outdated as cross-signing keys won't be stored + // also rename the deviceTrackingStatus field to keysTrackingStatus const userIdentities = txn.objectStore("userIdentities"); await iterateCursor(userIdentities.openCursor(), (value, key, cursor) => { - value.deviceTrackingStatus = 0 // outdated; + delete value["deviceTrackingStatus"]; + value.keysTrackingStatus = KeysTrackingStatus.Outdated; return NOT_DONE; }); } diff --git a/src/matrix/storage/idb/stores/UserIdentityStore.ts b/src/matrix/storage/idb/stores/UserIdentityStore.ts index 1c55baf094..76bb208034 100644 --- a/src/matrix/storage/idb/stores/UserIdentityStore.ts +++ b/src/matrix/storage/idb/stores/UserIdentityStore.ts @@ -14,12 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {Store} from "../Store"; - -interface UserIdentity { - userId: string; - roomIds: string[]; - deviceTrackingStatus: number; -} +import type {UserIdentity} from "../../../e2ee/DeviceTracker"; export class UserIdentityStore { private _store: Store; From 2563aa23e13ba8ae677283891c2dd9744430cc68 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 3 Mar 2023 11:56:51 +0100 Subject: [PATCH 030/168] actually write modified values in migration --- src/matrix/storage/idb/schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index c4c4bd6111..191d3c1d80 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -291,6 +291,7 @@ async function applyCrossSigningChanges(db: IDBDatabase, txn: IDBTransaction) : await iterateCursor(userIdentities.openCursor(), (value, key, cursor) => { delete value["deviceTrackingStatus"]; value.keysTrackingStatus = KeysTrackingStatus.Outdated; + cursor.update(value); return NOT_DONE; }); } From 08984ad1bc0b06903c095ba31632ed3f8bf3e272 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 3 Mar 2023 11:57:15 +0100 Subject: [PATCH 031/168] log amount of marked user identities in migration --- src/matrix/storage/idb/schema.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 191d3c1d80..fe7cd90023 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -279,8 +279,8 @@ function createCallStore(db: IDBDatabase) : void { db.createObjectStore("calls", {keyPath: "key"}); } -//v18 add crossSigningKeys store, rename deviceIdentities to deviceKeys and empties userIdentities -async function applyCrossSigningChanges(db: IDBDatabase, txn: IDBTransaction) : Promise { +//v18 add crossSigningKeys store, rename deviceIdentities to deviceKeys and empties userIdentities +async function applyCrossSigningChanges(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem) : Promise { db.createObjectStore("crossSigningKeys", {keyPath: "key"}); db.deleteObjectStore("deviceIdentities"); const deviceKeys = db.createObjectStore("deviceKeys", {keyPath: "key"}); @@ -288,10 +288,13 @@ async function applyCrossSigningChanges(db: IDBDatabase, txn: IDBTransaction) : // mark all userIdentities as outdated as cross-signing keys won't be stored // also rename the deviceTrackingStatus field to keysTrackingStatus const userIdentities = txn.objectStore("userIdentities"); + let counter = 0; await iterateCursor(userIdentities.openCursor(), (value, key, cursor) => { delete value["deviceTrackingStatus"]; value.keysTrackingStatus = KeysTrackingStatus.Outdated; cursor.update(value); + counter += 1; return NOT_DONE; }); + log.set("marked_outdated", counter); } From eff495c36d825e7bd7079fe6806595766fcf53e1 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 3 Mar 2023 11:57:29 +0100 Subject: [PATCH 032/168] also delete old crossSigningKeys field on userIdentities --- src/matrix/storage/idb/schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index fe7cd90023..9b4d55471d 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -291,6 +291,7 @@ async function applyCrossSigningChanges(db: IDBDatabase, txn: IDBTransaction, lo let counter = 0; await iterateCursor(userIdentities.openCursor(), (value, key, cursor) => { delete value["deviceTrackingStatus"]; + delete value["crossSigningKeys"]; value.keysTrackingStatus = KeysTrackingStatus.Outdated; cursor.update(value); counter += 1; From c2ee824c1c9882546c074dfeb5bf133d5f3c2c68 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 3 Mar 2023 12:03:31 +0100 Subject: [PATCH 033/168] fix lint warning from previous cross-signing PR --- src/platform/web/ui/session/settings/KeyBackupSettingsView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js index a68a80b3ed..6a886e3a30 100644 --- a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js @@ -60,7 +60,7 @@ export class KeyBackupSettingsView extends TemplateView { }), t.if(vm => vm.canSignOwnDevice, t => { return t.button({ - onClick: disableTargetCallback(async evt => { + onClick: disableTargetCallback(async () => { await vm.signOwnDevice(); }) }, "Sign own device"); From 774efc17d9eb989a9743c61c219abe051660ad5e Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 3 Mar 2023 12:15:54 +0100 Subject: [PATCH 034/168] extract method to sign key, as most params are always the same --- src/matrix/verification/CrossSigning.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 77b489d27e..f98c469a48 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -145,7 +145,7 @@ export class CrossSigning { delete keyToSign.signatures; const signingKey = await this.getSigningKey(KeyUsage.UserSigning); // add signature to keyToSign - pkSign(this.olm, keyToSign, signingKey, this.ownUserId, ""); + this.signKey(keyToSign, signingKey); const payload = { [keyToSign.user_id]: { [getKeyEd25519Key(keyToSign)!]: keyToSign @@ -160,7 +160,7 @@ export class CrossSigning { private async signDeviceKey(keyToSign: DeviceKey, log: ILogItem): Promise { const signingKey = await this.getSigningKey(KeyUsage.SelfSigning); // add signature to keyToSign - pkSign(this.olm, keyToSign, signingKey, this.ownUserId, ""); + this.signKey(keyToSign, signingKey); // so the payload format of a signature is a map from userid to key id of the signed key // (without the algoritm prefix though according to example, e.g. just device id or base 64 public key) // to the complete signed key with the signature of the signing key in the signatures section. @@ -180,6 +180,10 @@ export class CrossSigning { const seed = new Uint8Array(this.platform.encoding.base64.decode(seedStr)); return seed; } + + private signKey(keyToSign: DeviceKey | CrossSigningKey, signingKey: Uint8Array) { + pkSign(this.olm, keyToSign, signingKey, this.ownUserId, ""); + } } export function getKeyUsage(keyInfo: CrossSigningKey): KeyUsage | undefined { From 4c7f7849115510233ba7b6d8497dbc12264be567 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 3 Mar 2023 15:21:37 +0100 Subject: [PATCH 035/168] implement verifying signaturs for user trust (green shield/red shield) --- src/matrix/Session.js | 1 + src/matrix/verification/CrossSigning.ts | 41 ++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 9aa0494d4a..82eeba688a 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -339,6 +339,7 @@ export class Session { secretStorage, platform: this._platform, olm: this._olm, + olmUtil: this._olmUtil, deviceTracker: this._deviceTracker, hsApi: this._hsApi, ownUserId: this.userId, diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index f98c469a48..3d2fb93064 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -16,6 +16,7 @@ limitations under the License. import { ILogItem } from "../../lib"; import {pkSign} from "./common"; +import {verifyEd25519Signature} from "../e2ee/common"; import type {SecretStorage} from "../ssss/SecretStorage"; import type {Storage} from "../storage/idb/Storage"; @@ -48,6 +49,7 @@ export class CrossSigning { private readonly platform: Platform; private readonly deviceTracker: DeviceTracker; private readonly olm: Olm; + private readonly olmUtil: Olm.Utility; private readonly hsApi: HomeServerApi; private readonly ownUserId: string; private readonly e2eeAccount: Account; @@ -68,13 +70,14 @@ export class CrossSigning { this.platform = options.platform; this.deviceTracker = options.deviceTracker; this.olm = options.olm; + this.olmUtil = options.olmUtil; this.hsApi = options.hsApi; this.ownUserId = options.ownUserId; this.e2eeAccount = options.e2eeAccount } async init(log: ILogItem) { - log.wrap("CrossSigning.init", async log => { + await log.wrap("CrossSigning.init", async log => { // TODO: use errorboundary here const txn = await this.storage.readTxn([this.storage.storeNames.accountData]); const privateMasterKey = await this.getSigningKey(KeyUsage.Master); @@ -157,6 +160,34 @@ export class CrossSigning { }); } + async isUserTrusted(userId: string, log: ILogItem): Promise { + return log.wrap("isUserTrusted", async log => { + log.set("id", userId); + if (!this.isMasterKeyTrusted) { + return false; + } + const theirDeviceKeys = await log.wrap("get their devices", log => this.deviceTracker.devicesForUsers([userId], this.hsApi, log)); + const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, log)); + if (!theirSSK) { + return false; + } + const hasUnsignedDevice = theirDeviceKeys.some(dk => log.wrap({l: "verify device", id: dk.device_id}, log => !this.hasValidSignatureFrom(dk, theirSSK, log))); + if (hasUnsignedDevice) { + return false; + } + const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log)); + if (!theirMSK || !log.wrap("verify their ssk", log => this.hasValidSignatureFrom(theirSSK, theirMSK, log))) { + return false; + } + const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, log)); + if (!ourUSK || !log.wrap("verify their msk", log => this.hasValidSignatureFrom(theirMSK, ourUSK, log))) { + return false; + } + const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log)); + return !!ourMSK && log.wrap("verify our usk", log => this.hasValidSignatureFrom(ourUSK, ourMSK, log)); + }); + } + private async signDeviceKey(keyToSign: DeviceKey, log: ILogItem): Promise { const signingKey = await this.getSigningKey(KeyUsage.SelfSigning); // add signature to keyToSign @@ -184,6 +215,14 @@ export class CrossSigning { private signKey(keyToSign: DeviceKey | CrossSigningKey, signingKey: Uint8Array) { pkSign(this.olm, keyToSign, signingKey, this.ownUserId, ""); } + + private hasValidSignatureFrom(key: DeviceKey | CrossSigningKey, signingKey: CrossSigningKey, log: ILogItem): boolean { + const pubKey = getKeyEd25519Key(signingKey); + if (!pubKey) { + return false; + } + return verifyEd25519Signature(this.olmUtil, signingKey.user_id, pubKey, pubKey, key, log); + } } export function getKeyUsage(keyInfo: CrossSigningKey): KeyUsage | undefined { From 149f18790464fcb68df58ac352c8a372c0addf04 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 3 Mar 2023 15:22:02 +0100 Subject: [PATCH 036/168] expose user trust in member panel --- .../session/rightpanel/MemberDetailsViewModel.js | 12 ++++++++++++ .../ui/session/rightpanel/MemberDetailsView.js | 15 +++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index 774f64af62..df622aae46 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -29,10 +29,22 @@ export class MemberDetailsViewModel extends ViewModel { this._session = options.session; this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange())); this.track(this._observableMember.subscribe( () => this._onMemberChange())); + this._isTrusted = false; + this.init(); // TODO: call this from parent view model and do something smart with error view model if it fails async? + } + + async init() { + if (this.features.crossSigning) { + this._isTrusted = await this.logger.run({l: "MemberDetailsViewModel.verify user", id: this._member.userId}, log => { + return this._session.crossSigning.isUserTrusted(this._member.userId, log); + }); + this.emitChange("isTrusted"); + } } get name() { return this._member.name; } get userId() { return this._member.userId; } + get isTrusted() { return this._isTrusted; } get type() { return "member-details"; } get shouldShowBackButton() { return true; } diff --git a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js index caa8037fdb..45504a74e5 100644 --- a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js +++ b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js @@ -19,15 +19,22 @@ import {TemplateView} from "../../general/TemplateView"; export class MemberDetailsView extends TemplateView { render(t, vm) { + const securityNodes = [ + t.p(vm.isEncrypted ? + vm.i18n`Messages in this room are end-to-end encrypted.` : + vm.i18n`Messages in this room are not end-to-end encrypted.`), + ] + + if (vm.features.crossSigning) { + securityNodes.push(t.p(vm => vm.isTrusted ? vm.i18n`This user is trusted` : vm.i18n`This user is not trusted`)); + } + return t.div({className: "MemberDetailsView"}, [ t.view(new AvatarView(vm, 128)), t.div({className: "MemberDetailsView_name"}, t.h2(vm => vm.name)), t.div({className: "MemberDetailsView_id"}, vm.userId), this._createSection(t, vm.i18n`Role`, vm => vm.role), - this._createSection(t, vm.i18n`Security`, vm.isEncrypted ? - vm.i18n`Messages in this room are end-to-end encrypted.` : - vm.i18n`Messages in this room are not end-to-end encrypted.` - ), + this._createSection(t, vm.i18n`Security`, securityNodes), this._createOptions(t, vm) ]); } From e00d02a599f2067cdc448745e4be1433a17a5d22 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 3 Mar 2023 16:18:30 +0100 Subject: [PATCH 037/168] fix ts error --- src/matrix/verification/CrossSigning.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 3d2fb93064..975f0d4369 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -61,6 +61,7 @@ export class CrossSigning { deviceTracker: DeviceTracker, platform: Platform, olm: Olm, + olmUtil: Olm.Utility, ownUserId: string, hsApi: HomeServerApi, e2eeAccount: Account From 78b5d69eb552c5015a126db7e5e105ede019237c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 4 Mar 2023 22:23:32 +0530 Subject: [PATCH 038/168] Upgrade olm --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e367bc09c1..3451dfa98e 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "xxhashjs": "^0.2.2" }, "dependencies": { - "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz", "another-json": "^0.2.0", "base64-arraybuffer": "^0.2.0", "dompurify": "^2.3.0", diff --git a/yarn.lock b/yarn.lock index 876917a8d9..0d83d5568a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -52,9 +52,9 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== -"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz": - version "3.2.8" - resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz#8d53636d045e1776e2a2ec6613e57330dd9ce856" +"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": + version "3.2.14" + resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984" "@matrixdotorg/structured-logviewer@^0.0.3": version "0.0.3" From c9b462c80323300c619fd3b2b967525c7817d609 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 4 Mar 2023 22:30:53 +0530 Subject: [PATCH 039/168] Implement mac and done stage --- src/matrix/verification/CrossSigning.ts | 5 +- .../verification/SAS/SASVerification.ts | 25 ++--- .../verification/SAS/channel/Channel.ts | 7 +- src/matrix/verification/SAS/channel/types.ts | 2 + src/matrix/verification/SAS/mac.ts | 31 ++++++ .../SAS/stages/BaseSASVerificationStage.ts | 12 +++ .../verification/SAS/stages/SendDoneStage.ts | 32 +++++++ .../verification/SAS/stages/SendKeyStage.ts | 21 +++- .../verification/SAS/stages/SendMacStage.ts | 78 +++++++++++++++ .../verification/SAS/stages/VerifyMacStage.ts | 95 +++++++++++++++++++ 10 files changed, 285 insertions(+), 23 deletions(-) create mode 100644 src/matrix/verification/SAS/mac.ts create mode 100644 src/matrix/verification/SAS/stages/SendDoneStage.ts create mode 100644 src/matrix/verification/SAS/stages/SendMacStage.ts create mode 100644 src/matrix/verification/SAS/stages/VerifyMacStage.ts diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 83b9505215..b1312e9085 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -139,7 +139,10 @@ export class CrossSigning { ourUser: { userId: this.ownUserId, deviceId: this.deviceId }, otherUserId: userId, log, - channel + channel, + e2eeAccount: this.e2eeAccount, + deviceTracker: this.deviceTracker, + hsApi: this.hsApi, }); } } diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index aa19a6e79a..54dcb9ea66 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -18,8 +18,11 @@ import type {ILogItem} from "../../../logging/types"; import type {Room} from "../../room/Room.js"; import type {Platform} from "../../../platform/web/Platform.js"; import type {BaseSASVerificationStage, UserData} from "./stages/BaseSASVerificationStage"; +import type {Account} from "../../e2ee/Account.js"; +import type {DeviceTracker} from "../../e2ee/DeviceTracker.js"; import type * as OlmNamespace from "@matrix-org/olm"; import {IChannel} from "./channel/Channel"; +import {HomeServerApi} from "../../net/HomeServerApi"; type Olm = typeof OlmNamespace; @@ -32,6 +35,9 @@ type Options = { otherUserId: string; channel: IChannel; log: ILogItem; + e2eeAccount: Account; + deviceTracker: DeviceTracker; + hsApi: HomeServerApi; } export class SASVerification { @@ -39,29 +45,14 @@ export class SASVerification { private olmSas: Olm.SAS; constructor(options: Options) { - const { room, ourUser, otherUserId, log, olmUtil, olm, channel } = options; + const { room, ourUser, otherUserId, log, olmUtil, olm, channel, e2eeAccount, deviceTracker, hsApi } = options; const olmSas = new olm.SAS(); this.olmSas = olmSas; // channel.send("m.key.verification.request", {}, log); try { - const options = { room, ourUser, otherUserId, log, olmSas, olmUtil, channel }; + const options = { room, ourUser, otherUserId, log, olmSas, olmUtil, channel, e2eeAccount, deviceTracker, hsApi }; let stage: BaseSASVerificationStage = new RequestVerificationStage(options); this.startStage = stage; - - // stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.ready", options)); - // stage = stage.nextStage; - - // stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.start", options)); - // stage = stage.nextStage; - - // stage.setNextStage(new AcceptVerificationStage(options)); - // stage = stage.nextStage; - - // stage.setNextStage(new WaitForIncomingMessageStage("m.key.verification.key", options)); - // stage = stage.nextStage; - - // stage.setNextStage(new SendKeyStage(options)); - // stage = stage.nextStage; console.log("startStage", this.startStage); } finally { diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 3b39061628..3612de16e4 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -44,6 +44,7 @@ export interface IChannel { waitForEvent(eventType: string): Promise; type: ChannelType; id: string; + otherUserDeviceId: string; sentMessages: Map; receivedMessages: Map; localMessages: Map; @@ -74,7 +75,7 @@ export class ToDeviceChannel implements IChannel { public readonly localMessages: Map = new Map(); private readonly waitMap: Map}> = new Map(); private readonly log: ILogItem; - private otherUserDeviceId: string; + public otherUserDeviceId: string; public startMessage: any; public id: string; private _initiatedByUs: boolean; @@ -99,7 +100,7 @@ export class ToDeviceChannel implements IChannel { if (eventType === VerificationEventTypes.Request) { // Handle this case specially await this.handleRequestEventSpecially(eventType, content, log); - this.sentMessages.set(eventType, content); + this.sentMessages.set(eventType, {content}); return; } Object.assign(content, { transaction_id: this.id }); @@ -112,7 +113,7 @@ export class ToDeviceChannel implements IChannel { } } await this.hsApi.sendToDevice(eventType, payload, this.id, { log }).response(); - this.sentMessages.set(eventType, content); + this.sentMessages.set(eventType, {content}); }); } diff --git a/src/matrix/verification/SAS/channel/types.ts b/src/matrix/verification/SAS/channel/types.ts index 68c5bb89dd..f459f22839 100644 --- a/src/matrix/verification/SAS/channel/types.ts +++ b/src/matrix/verification/SAS/channel/types.ts @@ -5,6 +5,8 @@ export const enum VerificationEventTypes { Accept = "m.key.verification.accept", Key = "m.key.verification.key", Cancel = "m.key.verification.cancel", + Mac = "m.key.verification.mac", + Done = "m.key.verification.done", } export const enum CancelTypes { diff --git a/src/matrix/verification/SAS/mac.ts b/src/matrix/verification/SAS/mac.ts new file mode 100644 index 0000000000..9a6edddafe --- /dev/null +++ b/src/matrix/verification/SAS/mac.ts @@ -0,0 +1,31 @@ +/* +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. +*/ + +const macMethods = { + "hkdf-hmac-sha256": "calculate_mac", + "org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64", + "hkdf-hmac-sha256.v2": "calculate_mac_fixed_base64", + "hmac-sha256": "calculate_mac_long_kdf", +} as const; + +export type MacMethod = keyof typeof macMethods; + +export function createCalculateMAC(olmSAS: Olm.SAS, method: MacMethod) { + return function (input: string, info: string): string { + const mac = olmSAS[macMethods[method]](input, info); + return mac; + }; +} diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index 31ed908f3b..0e37188e5f 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -16,8 +16,11 @@ limitations under the License. import type {ILogItem} from "../../../../lib.js"; import type {Room} from "../../../room/Room.js"; import type * as OlmNamespace from "@matrix-org/olm"; +import type {Account} from "../../../e2ee/Account.js"; +import type {DeviceTracker} from "../../../e2ee/DeviceTracker.js"; import {Disposables} from "../../../../utils/Disposables"; import {IChannel} from "../channel/Channel.js"; +import {HomeServerApi} from "../../../net/HomeServerApi.js"; type Olm = typeof OlmNamespace; @@ -34,6 +37,9 @@ export type Options = { olmSas: Olm.SAS; olmUtil: Olm.Utility; channel: IChannel; + e2eeAccount: Account; + deviceTracker: DeviceTracker; + hsApi: HomeServerApi; } export abstract class BaseSASVerificationStage extends Disposables { @@ -48,6 +54,9 @@ export abstract class BaseSASVerificationStage extends Disposables { protected _nextStage: BaseSASVerificationStage; protected channel: IChannel; protected options: Options; + protected e2eeAccount: Account; + protected deviceTracker: DeviceTracker; + protected hsApi: HomeServerApi; constructor(options: Options) { super(); @@ -59,6 +68,9 @@ export abstract class BaseSASVerificationStage extends Disposables { this.olmSAS = options.olmSas; this.olmUtil = options.olmUtil; this.channel = options.channel; + this.e2eeAccount = options.e2eeAccount; + this.deviceTracker = options.deviceTracker; + this.hsApi = options.hsApi; } setRequestEventId(id: string) { diff --git a/src/matrix/verification/SAS/stages/SendDoneStage.ts b/src/matrix/verification/SAS/stages/SendDoneStage.ts new file mode 100644 index 0000000000..d19ab7238b --- /dev/null +++ b/src/matrix/verification/SAS/stages/SendDoneStage.ts @@ -0,0 +1,32 @@ +/* +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 {BaseSASVerificationStage} from "./BaseSASVerificationStage"; +import {VerificationEventTypes} from "../channel/types"; + + +export class SendDoneStage extends BaseSASVerificationStage { + + async completeStage() { + await this.log.wrap("VerifyMacStage.completeStage", async (log) => { + await this.channel.send(VerificationEventTypes.Done, {}, log); + this.dispose(); + }); + } + + get type() { + return "m.key.verification.accept"; + } +} diff --git a/src/matrix/verification/SAS/stages/SendKeyStage.ts b/src/matrix/verification/SAS/stages/SendKeyStage.ts index 199ec5d62b..c97e423a24 100644 --- a/src/matrix/verification/SAS/stages/SendKeyStage.ts +++ b/src/matrix/verification/SAS/stages/SendKeyStage.ts @@ -16,7 +16,8 @@ limitations under the License. import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {generateEmojiSas} from "../generator"; import {ILogItem} from "../../../../lib"; -import { VerificationEventTypes } from "../channel/types"; +import {VerificationEventTypes} from "../channel/types"; +import {SendMacStage} from "./SendMacStage"; // From element-web type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; @@ -71,15 +72,24 @@ const calculateKeyAgreement = { } as const; export class SendKeyStage extends BaseSASVerificationStage { + private resolve: () => void; async completeStage() { await this.log.wrap("SendKeyStage.completeStage", async (log) => { + const emojiConfirmationPromise: Promise = new Promise(r => { + this.resolve = r; + }); this.olmSAS.set_their_key(this.theirKey); const ourSasKey = this.olmSAS.get_pubkey(); await this.sendKey(ourSasKey, log); const sasBytes = this.generateSASBytes(); const emoji = generateEmojiSas(Array.from(sasBytes)); console.log("emoji", emoji); + if (this.channel.initiatedByUs) { + await this.channel.waitForEvent(VerificationEventTypes.Key); + } + // await emojiConfirmationPromise; + this._nextStage = new SendMacStage(this.options); this.dispose(); }); } @@ -97,7 +107,7 @@ export class SendKeyStage extends BaseSASVerificationStage { } private generateSASBytes(): Uint8Array { - const keyAgreement = this.channel.sentMessages.get(VerificationEventTypes.Accept).key_agreement_protocol; + const keyAgreement = this.channel.sentMessages.get(VerificationEventTypes.Accept).content.key_agreement_protocol; const otherUserDeviceId = this.channel.startMessage.content.from_device; const sasBytes = calculateKeyAgreement[keyAgreement]({ our: { @@ -116,6 +126,13 @@ export class SendKeyStage extends BaseSASVerificationStage { return sasBytes; } + emojiMatch(match: boolean) { + if (!match) { + // cancel the verification + } + + } + get type() { return "m.key.verification.accept"; } diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts new file mode 100644 index 0000000000..4d8f492c0f --- /dev/null +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -0,0 +1,78 @@ +/* +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 {BaseSASVerificationStage} from "./BaseSASVerificationStage"; +import {ILogItem} from "../../../../lib"; +import {VerificationEventTypes} from "../channel/types"; +import type * as OlmNamespace from "@matrix-org/olm"; +import {createCalculateMAC} from "../mac"; +import {VerifyMacStage} from "./VerifyMacStage"; +type Olm = typeof OlmNamespace; + +export class SendMacStage extends BaseSASVerificationStage { + private calculateMAC: (input: string, info: string) => string; + + async completeStage() { + await this.log.wrap("SendMacStage.completeStage", async (log) => { + let acceptMessage; + if (this.channel.initiatedByUs) { + acceptMessage = this.channel.receivedMessages.get(VerificationEventTypes.Accept).content; + } + else { + acceptMessage = this.channel.sentMessages.get(VerificationEventTypes.Accept).content; + } + const macMethod = acceptMessage.message_authentication_code; + this.calculateMAC = createCalculateMAC(this.olmSAS, macMethod); + await this.sendMAC(log); + this.dispose(); + }); + } + + private async sendMAC(log: ILogItem): Promise { + const mac: Record = {}; + const keyList: string[] = []; + const baseInfo = + "MATRIX_KEY_VERIFICATION_MAC" + + this.ourUser.userId + + this.ourUser.deviceId + + this.otherUserId + + this.channel.otherUserDeviceId + + this.channel.id; + + const deviceKeyId = `ed25519:${this.ourUser.deviceId}`; + const deviceKeys = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning(); + mac[deviceKeyId] = this.calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId); + keyList.push(deviceKeyId); + + const {masterKey: crossSigningKey} = await this.deviceTracker.getCrossSigningKeysForUser(this.ourUser.userId, this.hsApi, log); + console.log("masterKey", crossSigningKey); + if (crossSigningKey) { + const crossSigningKeyId = `ed25519:${crossSigningKey}`; + mac[crossSigningKeyId] = this.calculateMAC(crossSigningKey, baseInfo + crossSigningKeyId); + keyList.push(crossSigningKeyId); + } + + const keys = this.calculateMAC(keyList.sort().join(","), baseInfo + "KEY_IDS"); + console.log("result", mac, keys); + await this.channel.send(VerificationEventTypes.Mac, { mac, keys }, log); + await this.channel.waitForEvent(VerificationEventTypes.Mac); + this._nextStage = new VerifyMacStage(this.options); + } + + get type() { + return "m.key.verification.accept"; + } +} + diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts new file mode 100644 index 0000000000..634b8ccb5b --- /dev/null +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -0,0 +1,95 @@ +/* +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 {BaseSASVerificationStage} from "./BaseSASVerificationStage"; +import {ILogItem} from "../../../../lib"; +import {VerificationEventTypes} from "../channel/types"; +import {createCalculateMAC} from "../mac"; +import type * as OlmNamespace from "@matrix-org/olm"; +import { SendDoneStage } from "./SendDoneStage"; +type Olm = typeof OlmNamespace; + +export type KeyVerifier = (keyId: string, device: any, keyInfo: string) => void; + +export class VerifyMacStage extends BaseSASVerificationStage { + private calculateMAC: (input: string, info: string) => string; + + async completeStage() { + await this.log.wrap("VerifyMacStage.completeStage", async (log) => { + let acceptMessage; + if (this.channel.initiatedByUs) { + acceptMessage = this.channel.receivedMessages.get(VerificationEventTypes.Accept).content; + } + else { + acceptMessage = this.channel.sentMessages.get(VerificationEventTypes.Accept).content; + } + const macMethod = acceptMessage.message_authentication_code; + this.calculateMAC = createCalculateMAC(this.olmSAS, macMethod); + await this.checkMAC(log); + await this.channel.waitForEvent(VerificationEventTypes.Done); + this._nextStage = new SendDoneStage(this.options); + this.dispose(); + }); + } + + private async checkMAC(log: ILogItem): Promise { + const {content} = this.channel.receivedMessages.get(VerificationEventTypes.Mac); + const baseInfo = + "MATRIX_KEY_VERIFICATION_MAC" + + this.otherUserId + + this.channel.otherUserDeviceId + + this.ourUser.userId + + this.ourUser.deviceId + + this.channel.id; + + if ( content.keys !== this.calculateMAC(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS")) { + // cancel when MAC does not match! + console.log("Keys MAC Verification failed"); + } + + await this.verifyKeys(content.mac, (keyId, key, keyInfo) => { + if (keyInfo !== this.calculateMAC(key, baseInfo + keyId)) { + // cancel when MAC does not match! + console.log("mac obj MAC Verification failed"); + } + }, log); + } + + protected async verifyKeys(keys: Record, verifier: KeyVerifier, log: ILogItem): Promise { + const userId = this.otherUserId; + for (const [keyId, keyInfo] of Object.entries(keys)) { + const deviceId = keyId.split(":", 2)[1]; + const device = await this.deviceTracker.deviceForId(userId, deviceId, this.hsApi, log); + if (device) { + verifier(keyId, device.ed25519Key, keyInfo); + // todo: mark device as verified here + } else { + // If we were not able to find the device, then deviceId is actually the master signing key! + const msk = deviceId; + const {masterKey} = await this.deviceTracker.getCrossSigningKeysForUser(userId, this.hsApi, log); + if (masterKey === msk) { + verifier(keyId, masterKey, keyInfo); + // todo: mark user as verified her + } else { + // logger.warn(`verification: Could not find device ${deviceId} to verify`); + } + } + } + } + + get type() { + return "m.key.verification.accept"; + } +} From 4540ba2f37e4c3109930b14979070c55a86d6e54 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 5 Mar 2023 15:25:09 +0530 Subject: [PATCH 040/168] Implement send ready stage --- .../verification/SAS/stages/SendReadyStage.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/matrix/verification/SAS/stages/SendReadyStage.ts diff --git a/src/matrix/verification/SAS/stages/SendReadyStage.ts b/src/matrix/verification/SAS/stages/SendReadyStage.ts new file mode 100644 index 0000000000..ffd7823bd2 --- /dev/null +++ b/src/matrix/verification/SAS/stages/SendReadyStage.ts @@ -0,0 +1,38 @@ +/* +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 {BaseSASVerificationStage} from "./BaseSASVerificationStage"; +import {VerificationEventTypes} from "../channel/types"; + +export class SendReadyStage extends BaseSASVerificationStage { + + async completeStage() { + await this.log.wrap("StartVerificationStage.completeStage", async (log) => { + const content = { + // "body": `${this.ourUser.userId} is requesting to verify your device, but your client does not support verification, so you may need to use a different verification method.`, + "from_device": this.ourUser.deviceId, + "methods": ["m.sas.v1"], + // "msgtype": "m.key.verification.request", + // "to": this.otherUserId, + }; + await this.channel.send(VerificationEventTypes.Ready, content, log); + this.dispose(); + }); + } + + get type() { + return "m.key.verification.request"; + } +} From b3cc07cf1ea594ef94c7aad6dc6dfb7ce6f061a3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 6 Mar 2023 16:22:45 +0530 Subject: [PATCH 041/168] Accept verification from device message --- src/domain/session/room/RoomViewModel.js | 3 +- src/matrix/verification/CrossSigning.ts | 18 +- .../verification/SAS/SASVerification.ts | 22 ++- .../verification/SAS/channel/Channel.ts | 18 +- src/matrix/verification/SAS/channel/types.ts | 3 + .../SAS/stages/BaseSASVerificationStage.ts | 3 - .../SAS/stages/CalculateSASStage.ts | 157 ++++++++++++++++++ .../SAS/stages/RequestVerificationStage.ts | 44 ++--- .../stages/SelectVerificationMethodStage.ts | 2 +- .../SAS/stages/SendAcceptVerificationStage.ts | 3 +- .../verification/SAS/stages/SendKeyStage.ts | 138 ++------------- .../verification/SAS/stages/SendMacStage.ts | 4 +- .../verification/SAS/stages/SendReadyStage.ts | 2 + 13 files changed, 245 insertions(+), 172 deletions(-) create mode 100644 src/matrix/verification/SAS/stages/CalculateSASStage.ts diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 9af9aa47e6..a36f099caa 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -28,7 +28,6 @@ import {LocalMedia} from "../../../matrix/calls/LocalMedia"; // this is a breaking SDK change though to make this option mandatory import {tileClassForEntry as defaultTileClassForEntry} from "./timeline/tiles/index"; import {joinRoom} from "../../../matrix/room/joinRoom"; -import {SASVerification} from "../../../matrix/verification/SAS/SASVerification"; export class RoomViewModel extends ErrorReportViewModel { constructor(options) { @@ -53,7 +52,7 @@ export class RoomViewModel extends ErrorReportViewModel { async _startCrossSigning(otherUserId) { await this.logAndCatch("startCrossSigning", async log => { const session = this.getOption("session"); - const sas = session.crossSigning?.startVerification(this._room, otherUserId, log); + const sas = session.crossSigning?.startVerification(otherUserId, log); await sas.start(); }); } diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index b1312e9085..2f152481be 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -28,6 +28,7 @@ import type {ISignatures} from "./common"; import {SASVerification} from "./SAS/SASVerification"; import {ToDeviceChannel} from "./SAS/channel/Channel"; import type {DeviceMessageHandler} from "../DeviceMessageHandler.js"; +import {VerificationEventTypes} from "./SAS/channel/types"; type Olm = typeof OlmNamespace; @@ -69,6 +70,17 @@ export class CrossSigning { this.deviceId = options.deviceId; this.e2eeAccount = options.e2eeAccount this.deviceMessageHandler = options.deviceMessageHandler; + + this.deviceMessageHandler.on("message", async ({ unencrypted: unencryptedEvent }) => { + console.log("unencrypted event", unencryptedEvent); + if (unencryptedEvent.type === VerificationEventTypes.Request || + unencryptedEvent.type === VerificationEventTypes.Start) { + await this.platform.logger.run("Start verification from request", async (log) => { + const sas = this.startVerification(unencryptedEvent.sender, log, unencryptedEvent); + await sas.start(); + }); + } + }) } async init(log: ILogItem) { @@ -122,7 +134,7 @@ export class CrossSigning { return this._isMasterKeyTrusted; } - startVerification(room: Room, userId: string, log: ILogItem): SASVerification { + startVerification(userId: string, log: ILogItem, event?: any): SASVerification { const channel = new ToDeviceChannel({ deviceTracker: this.deviceTracker, hsApi: this.hsApi, @@ -130,10 +142,8 @@ export class CrossSigning { platform: this.platform, deviceMessageHandler: this.deviceMessageHandler, log - }); + }, event); return new SASVerification({ - room, - platform: this.platform, olm: this.olm, olmUtil: this.olmUtil, ourUser: { userId: this.ownUserId, deviceId: this.deviceId }, diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 54dcb9ea66..76839d9829 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -15,20 +15,19 @@ limitations under the License. */ import {RequestVerificationStage} from "./stages/RequestVerificationStage"; import type {ILogItem} from "../../../logging/types"; -import type {Room} from "../../room/Room.js"; -import type {Platform} from "../../../platform/web/Platform.js"; import type {BaseSASVerificationStage, UserData} from "./stages/BaseSASVerificationStage"; import type {Account} from "../../e2ee/Account.js"; import type {DeviceTracker} from "../../e2ee/DeviceTracker.js"; import type * as OlmNamespace from "@matrix-org/olm"; import {IChannel} from "./channel/Channel"; import {HomeServerApi} from "../../net/HomeServerApi"; +import {VerificationEventTypes} from "./channel/types"; +import {SendReadyStage} from "./stages/SendReadyStage"; +import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; type Olm = typeof OlmNamespace; type Options = { - room: Room; - platform: Platform; olm: Olm; olmUtil: Olm.Utility; ourUser: UserData; @@ -45,13 +44,22 @@ export class SASVerification { private olmSas: Olm.SAS; constructor(options: Options) { - const { room, ourUser, otherUserId, log, olmUtil, olm, channel, e2eeAccount, deviceTracker, hsApi } = options; + const { ourUser, otherUserId, log, olmUtil, olm, channel, e2eeAccount, deviceTracker, hsApi } = options; const olmSas = new olm.SAS(); this.olmSas = olmSas; // channel.send("m.key.verification.request", {}, log); try { - const options = { room, ourUser, otherUserId, log, olmSas, olmUtil, channel, e2eeAccount, deviceTracker, hsApi }; - let stage: BaseSASVerificationStage = new RequestVerificationStage(options); + const options = { ourUser, otherUserId, log, olmSas, olmUtil, channel, e2eeAccount, deviceTracker, hsApi }; + let stage: BaseSASVerificationStage; + if (channel.receivedMessages.get(VerificationEventTypes.Start)) { + stage = new SelectVerificationMethodStage(options); + } + else if (channel.receivedMessages.get(VerificationEventTypes.Request)) { + stage = new SendReadyStage(options); + } + else { + stage = new RequestVerificationStage(options); + } this.startStage = stage; console.log("startStage", this.startStage); } diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 3612de16e4..3339a7d266 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -32,6 +32,8 @@ const messageFromErrorType = { [CancelTypes.UnknownMethod]: "Unknown method.", [CancelTypes.UnknownTransaction]: "Unknown Transaction.", [CancelTypes.UserMismatch]: "User Mismatch", + [CancelTypes.MismatchedCommitment]: "Hash commitment does not match.", + [CancelTypes.MismatchedSAS]: "Emoji/decimal does not match.", } const enum ChannelType { @@ -80,7 +82,11 @@ export class ToDeviceChannel implements IChannel { public id: string; private _initiatedByUs: boolean; - constructor(options: Options) { + /** + * + * @param startingMessage Create the channel with existing message in the receivedMessage buffer + */ + constructor(options: Options, startingMessage?: any) { this.hsApi = options.hsApi; this.deviceTracker = options.deviceTracker; this.otherUserId = options.otherUserId; @@ -89,6 +95,16 @@ export class ToDeviceChannel implements IChannel { this.deviceMessageHandler = options.deviceMessageHandler; // todo: find a way to dispose this subscription this.deviceMessageHandler.on("message", ({unencrypted}) => this.handleDeviceMessage(unencrypted)) + // Copy over request message + if (startingMessage) { + /** + * startingMessage may be the ready message or the start message. + */ + const eventType = startingMessage.content.method ? VerificationEventTypes.Start : VerificationEventTypes.Request; + this.id = startingMessage.content.transaction_id; + this.receivedMessages.set(eventType, startingMessage); + this.otherUserDeviceId = startingMessage.content.from_device; + } } get type() { diff --git a/src/matrix/verification/SAS/channel/types.ts b/src/matrix/verification/SAS/channel/types.ts index f459f22839..9a02f421d7 100644 --- a/src/matrix/verification/SAS/channel/types.ts +++ b/src/matrix/verification/SAS/channel/types.ts @@ -19,4 +19,7 @@ export const enum CancelTypes { UserMismatch = "m.user_mismatch", InvalidMessage = "m.invalid_message", OtherUserAccepted = "m.accepted", + // SAS specific + MismatchedCommitment = "m.mismatched_commitment", + MismatchedSAS = "m.mismatched_sas", } diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index 0e37188e5f..82de5572f8 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -30,7 +30,6 @@ export type UserData = { } export type Options = { - room: Room; ourUser: UserData; otherUserId: string; log: ILogItem; @@ -43,7 +42,6 @@ export type Options = { } export abstract class BaseSASVerificationStage extends Disposables { - protected room: Room; protected ourUser: UserData; protected otherUserId: string; protected log: ILogItem; @@ -61,7 +59,6 @@ export abstract class BaseSASVerificationStage extends Disposables { constructor(options: Options) { super(); this.options = options; - this.room = options.room; this.ourUser = options.ourUser; this.otherUserId = options.otherUserId; this.log = options.log; diff --git a/src/matrix/verification/SAS/stages/CalculateSASStage.ts b/src/matrix/verification/SAS/stages/CalculateSASStage.ts new file mode 100644 index 0000000000..4e20ff1d81 --- /dev/null +++ b/src/matrix/verification/SAS/stages/CalculateSASStage.ts @@ -0,0 +1,157 @@ +/* +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 {BaseSASVerificationStage} from "./BaseSASVerificationStage"; +import {CancelTypes, VerificationEventTypes} from "../channel/types"; +import {generateEmojiSas} from "../generator"; +import anotherjson from "another-json"; +import { ILogItem } from "../../../../lib"; +import { SendMacStage } from "./SendMacStage"; + +// From element-web +type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; +type MacMethod = "hkdf-hmac-sha256.v2" | "org.matrix.msc3783.hkdf-hmac-sha256" | "hkdf-hmac-sha256" | "hmac-sha256"; + +const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; +const HASHES_LIST = ["sha256"]; +const MAC_LIST: MacMethod[] = [ + "hkdf-hmac-sha256.v2", + "org.matrix.msc3783.hkdf-hmac-sha256", + "hkdf-hmac-sha256", + "hmac-sha256", +]; +const SAS_LIST = ["decimal", "emoji"]; +const SAS_SET = new Set(SAS_LIST); + + +type SASUserInfo = { + userId: string; + deviceId: string; + publicKey: string; +} +type SASUserInfoCollection = { + our: SASUserInfo; + their: SASUserInfo; + id: string; + initiatedByMe: boolean; +}; + +const calculateKeyAgreement = { + // eslint-disable-next-line @typescript-eslint/naming-convention + "curve25519-hkdf-sha256": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array { + console.log("sas.requestId", sas.id); + const ourInfo = `${sas.our.userId}|${sas.our.deviceId}|` + `${sas.our.publicKey}|`; + const theirInfo = `${sas.their.userId}|${sas.their.deviceId}|${sas.their.publicKey}|`; + console.log("ourInfo", ourInfo); + console.log("theirInfo", theirInfo); + const sasInfo = + "MATRIX_KEY_VERIFICATION_SAS|" + + (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.id; + console.log("sasInfo", sasInfo); + return olmSAS.generate_bytes(sasInfo, bytes); + }, + "curve25519": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array { + const ourInfo = `${sas.our.userId}${sas.our.deviceId}`; + const theirInfo = `${sas.their.userId}${sas.their.deviceId}`; + const sasInfo = + "MATRIX_KEY_VERIFICATION_SAS" + + (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.id; + return olmSAS.generate_bytes(sasInfo, bytes); + }, +} as const; + +export class CalculateSASStage extends BaseSASVerificationStage { + private resolve: () => void; + + async completeStage() { + await this.log.wrap("CalculateSASStage.completeStage", async (log) => { + // 1. Check the hash commitment + if (this.needsToVerifyHashCommitment) { + if (!await this.verifyHashCommitment(log)) { return; } + } + // 2. Calculate the SAS + const emojiConfirmationPromise: Promise = new Promise(r => { + this.resolve = r; + }); + this.olmSAS.set_their_key(this.theirKey); + const sasBytes = this.generateSASBytes(); + const emoji = generateEmojiSas(Array.from(sasBytes)); + console.log("emoji", emoji); + this._nextStage = new SendMacStage(this.options); + this.dispose(); + }); + } + + async verifyHashCommitment(log: ILogItem) { + return await log.wrap("CalculateSASStage.verifyHashCommitment", async () => { + const acceptMessage = this.channel.receivedMessages.get(VerificationEventTypes.Accept).content; + const keyMessage = this.channel.receivedMessages.get(VerificationEventTypes.Key).content; + const commitmentStr = keyMessage.key + anotherjson.stringify(acceptMessage); + const receivedCommitment = acceptMessage.commitment; + if (this.olmUtil.sha256(commitmentStr) !== receivedCommitment) { + log.set("Commitment mismatched!", {}); + // cancel the process! + await this.channel.cancelVerification(CancelTypes.MismatchedCommitment); + return false; + } + return true; + }); + } + + private get needsToVerifyHashCommitment(): boolean { + if (this.channel.initiatedByUs) { + // If we sent the start message, we also received the accept message + // The commitment is in the accept message, so we need to verify it. + return true; + } + return false; + } + + private generateSASBytes(): Uint8Array { + const keyAgreement = this.channel.sentMessages.get(VerificationEventTypes.Accept).content.key_agreement_protocol; + const otherUserDeviceId = this.channel.startMessage.content.from_device; + const sasBytes = calculateKeyAgreement[keyAgreement]({ + our: { + userId: this.ourUser.userId, + deviceId: this.ourUser.deviceId, + publicKey: this.olmSAS.get_pubkey(), + }, + their: { + userId: this.otherUserId, + deviceId: otherUserDeviceId, + publicKey: this.theirKey, + }, + id: this.channel.id, + initiatedByMe: this.channel.initiatedByUs, + }, this.olmSAS, 6); + return sasBytes; + } + + emojiMatch(match: boolean) { + if (!match) { + // cancel the verification + } + + } + + get theirKey(): string { + const { content } = this.channel.receivedMessages.get(VerificationEventTypes.Key); + return content.key; + } + + get type() { + return "m.key.verification.accept"; + } +} diff --git a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts index fe00649b91..1b9e2e6e4a 100644 --- a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {FragmentBoundaryEntry} from "../../../room/timeline/entries/FragmentBoundaryEntry.js"; +// import {FragmentBoundaryEntry} from "../../../room/timeline/entries/FragmentBoundaryEntry.js"; import {SelectVerificationMethodStage} from "./SelectVerificationMethodStage"; import {VerificationEventTypes} from "../channel/types"; @@ -41,27 +41,27 @@ export class RequestVerificationStage extends BaseSASVerificationStage { }); } - private trackEventId(): Promise { - return new Promise(resolve => { - this.track( - this.room._timeline.entries.subscribe({ - onAdd: (_, entry) => { - if (entry instanceof FragmentBoundaryEntry) { - return; - } - if (!entry.isPending && - entry.content["msgtype"] === "m.key.verification.request" && - entry.content["from_device"] === this.ourUser.deviceId) { - console.log("found event", entry); - resolve(entry.id); - } - }, - onRemove: () => { /**noop*/ }, - onUpdate: () => { /**noop*/ }, - }) - ); - }); - } + // private trackEventId(): Promise { + // return new Promise(resolve => { + // this.track( + // this.room._timeline.entries.subscribe({ + // onAdd: (_, entry) => { + // if (entry instanceof FragmentBoundaryEntry) { + // return; + // } + // if (!entry.isPending && + // entry.content["msgtype"] === "m.key.verification.request" && + // entry.content["from_device"] === this.ourUser.deviceId) { + // console.log("found event", entry); + // resolve(entry.id); + // } + // }, + // onRemove: () => { /**noop*/ }, + // onUpdate: () => { /**noop*/ }, + // }) + // ); + // }); + // } get type() { return "m.key.verification.request"; diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index 2807abd634..dfe6dc35f5 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -88,6 +88,6 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { } get type() { - return "m.key.verification.request"; + return "SelectVerificationStage"; } } diff --git a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts index 9b1b2fcc1c..6b964445d4 100644 --- a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts @@ -22,13 +22,12 @@ import { SendKeyStage } from "./SendKeyStage"; export class SendAcceptVerificationStage extends BaseSASVerificationStage { async completeStage() { - await this.log.wrap("SAcceptVerificationStage.completeStage", async (log) => { + await this.log.wrap("SendAcceptVerificationStage.completeStage", async (log) => { const event = this.channel.startMessage; const content = { ...event.content, // "m.relates_to": event.relation, }; - console.log("content from event", content); const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0]; const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0]; const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0]; diff --git a/src/matrix/verification/SAS/stages/SendKeyStage.ts b/src/matrix/verification/SAS/stages/SendKeyStage.ts index c97e423a24..c9455663fd 100644 --- a/src/matrix/verification/SAS/stages/SendKeyStage.ts +++ b/src/matrix/verification/SAS/stages/SendKeyStage.ts @@ -14,146 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {generateEmojiSas} from "../generator"; -import {ILogItem} from "../../../../lib"; import {VerificationEventTypes} from "../channel/types"; -import {SendMacStage} from "./SendMacStage"; - -// From element-web -type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; -type MacMethod = "hkdf-hmac-sha256.v2" | "org.matrix.msc3783.hkdf-hmac-sha256" | "hkdf-hmac-sha256" | "hmac-sha256"; - -const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; -const HASHES_LIST = ["sha256"]; -const MAC_LIST: MacMethod[] = [ - "hkdf-hmac-sha256.v2", - "org.matrix.msc3783.hkdf-hmac-sha256", - "hkdf-hmac-sha256", - "hmac-sha256", -]; -const SAS_LIST = ["decimal", "emoji"]; -const SAS_SET = new Set(SAS_LIST); - - -type SASUserInfo = { - userId: string; - deviceId: string; - publicKey: string; -} -type SASUserInfoCollection = { - our: SASUserInfo; - their: SASUserInfo; - id: string; - initiatedByMe: boolean; -}; - -const calculateKeyAgreement = { - // eslint-disable-next-line @typescript-eslint/naming-convention - "curve25519-hkdf-sha256": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array { - console.log("sas.requestId", sas.id); - const ourInfo = `${sas.our.userId}|${sas.our.deviceId}|` + `${sas.our.publicKey}|`; - const theirInfo = `${sas.their.userId}|${sas.their.deviceId}|${sas.their.publicKey}|`; - console.log("ourInfo", ourInfo); - console.log("theirInfo", theirInfo); - const sasInfo = - "MATRIX_KEY_VERIFICATION_SAS|" + - (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.id; - console.log("sasInfo", sasInfo); - return olmSAS.generate_bytes(sasInfo, bytes); - }, - "curve25519": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array { - const ourInfo = `${sas.our.userId}${sas.our.deviceId}`; - const theirInfo = `${sas.their.userId}${sas.their.deviceId}`; - const sasInfo = - "MATRIX_KEY_VERIFICATION_SAS" + - (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.id; - return olmSAS.generate_bytes(sasInfo, bytes); - }, -} as const; +import {CalculateSASStage} from "./CalculateSASStage"; export class SendKeyStage extends BaseSASVerificationStage { - private resolve: () => void; async completeStage() { await this.log.wrap("SendKeyStage.completeStage", async (log) => { - const emojiConfirmationPromise: Promise = new Promise(r => { - this.resolve = r; - }); - this.olmSAS.set_their_key(this.theirKey); const ourSasKey = this.olmSAS.get_pubkey(); - await this.sendKey(ourSasKey, log); - const sasBytes = this.generateSASBytes(); - const emoji = generateEmojiSas(Array.from(sasBytes)); - console.log("emoji", emoji); - if (this.channel.initiatedByUs) { - await this.channel.waitForEvent(VerificationEventTypes.Key); - } - // await emojiConfirmationPromise; - this._nextStage = new SendMacStage(this.options); + await this.channel.send(VerificationEventTypes.Key, {key: ourSasKey}, log); + /** + * We may have already got the key in SendAcceptVerificationStage, + * in which case waitForEvent will return a resolved promise with + * that content. Otherwise, waitForEvent will actually wait for the + * key. + */ + await this.channel.waitForEvent(VerificationEventTypes.Key); + this._nextStage = new CalculateSASStage(this.options) this.dispose(); }); } - private async sendKey(key: string, log: ILogItem): Promise { - const contentToSend = { - key, - // "m.relates_to": { - // event_id: this.requestEventId, - // rel_type: "m.reference", - // }, - }; - await this.channel.send(VerificationEventTypes.Key, contentToSend, log); - // await this.room.sendEvent("m.key.verification.key", contentToSend, null, log); - } - - private generateSASBytes(): Uint8Array { - const keyAgreement = this.channel.sentMessages.get(VerificationEventTypes.Accept).content.key_agreement_protocol; - const otherUserDeviceId = this.channel.startMessage.content.from_device; - const sasBytes = calculateKeyAgreement[keyAgreement]({ - our: { - userId: this.ourUser.userId, - deviceId: this.ourUser.deviceId, - publicKey: this.olmSAS.get_pubkey(), - }, - their: { - userId: this.otherUserId, - deviceId: otherUserDeviceId, - publicKey: this.theirKey, - }, - id: this.channel.id, - initiatedByMe: this.channel.initiatedByUs, - }, this.olmSAS, 6); - return sasBytes; - } - - emojiMatch(match: boolean) { - if (!match) { - // cancel the verification - } - - } - get type() { return "m.key.verification.accept"; } - - get theirKey(): string { - const { content } = this.channel.receivedMessages.get(VerificationEventTypes.Key); - return content.key; - } -} - -function intersection(anArray: T[], aSet: Set): T[] { - return Array.isArray(anArray) ? anArray.filter((x) => aSet.has(x)) : []; } - -// function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas { -// const sas: IGeneratedSas = {}; -// for (const method of methods) { -// if (method in sasGenerators) { -// // @ts-ignore - ts doesn't like us mixing types like this -// sas[method] = sasGenerators[method](Array.from(sasBytes)); -// } -// } -// return sas; -// } diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts index 4d8f492c0f..5ffc56a926 100644 --- a/src/matrix/verification/SAS/stages/SendMacStage.ts +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -36,6 +36,8 @@ export class SendMacStage extends BaseSASVerificationStage { const macMethod = acceptMessage.message_authentication_code; this.calculateMAC = createCalculateMAC(this.olmSAS, macMethod); await this.sendMAC(log); + await this.channel.waitForEvent(VerificationEventTypes.Mac); + this._nextStage = new VerifyMacStage(this.options); this.dispose(); }); } @@ -67,8 +69,6 @@ export class SendMacStage extends BaseSASVerificationStage { const keys = this.calculateMAC(keyList.sort().join(","), baseInfo + "KEY_IDS"); console.log("result", mac, keys); await this.channel.send(VerificationEventTypes.Mac, { mac, keys }, log); - await this.channel.waitForEvent(VerificationEventTypes.Mac); - this._nextStage = new VerifyMacStage(this.options); } get type() { diff --git a/src/matrix/verification/SAS/stages/SendReadyStage.ts b/src/matrix/verification/SAS/stages/SendReadyStage.ts index ffd7823bd2..e70d895343 100644 --- a/src/matrix/verification/SAS/stages/SendReadyStage.ts +++ b/src/matrix/verification/SAS/stages/SendReadyStage.ts @@ -15,6 +15,7 @@ limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {VerificationEventTypes} from "../channel/types"; +import { SelectVerificationMethodStage } from "./SelectVerificationMethodStage"; export class SendReadyStage extends BaseSASVerificationStage { @@ -28,6 +29,7 @@ export class SendReadyStage extends BaseSASVerificationStage { // "to": this.otherUserId, }; await this.channel.send(VerificationEventTypes.Ready, content, log); + this._nextStage = new SelectVerificationMethodStage(this.options); this.dispose(); }); } From bae18c037f3103a4163e8283556255605daf5a3a Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 7 Mar 2023 10:53:32 +0100 Subject: [PATCH 042/168] return enum explaining user trust level rather than boolean --- src/matrix/e2ee/DeviceTracker.ts | 4 +- src/matrix/e2ee/common.ts | 14 +++- src/matrix/e2ee/olm/Encryption.ts | 4 +- src/matrix/verification/CrossSigning.ts | 99 ++++++++++++++++++++----- 4 files changed, 93 insertions(+), 28 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 98221523e9..ae00b1e0db 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {verifyEd25519Signature, getEd25519Signature, SIGNATURE_ALGORITHM} from "./common"; +import {verifyEd25519Signature, getEd25519Signature, SIGNATURE_ALGORITHM, SignatureVerification} from "./common"; import {HistoryVisibility, shouldShareKey, DeviceKey, getDeviceEd25519Key, getDeviceCurve25519Key} from "./common"; import {RoomMember} from "../room/members/RoomMember.js"; import {getKeyUsage, getKeyEd25519Key, getKeyUserId, KeyUsage} from "../verification/CrossSigning"; @@ -462,7 +462,7 @@ export class DeviceTracker { log.log("ed25519 and/or curve25519 key invalid").set({deviceKey}); return false; } - const isValid = verifyEd25519Signature(this._olmUtil, userId, deviceId, ed25519Key, deviceKey, log); + const isValid = verifyEd25519Signature(this._olmUtil, userId, deviceId, ed25519Key, deviceKey, log) === SignatureVerification.Valid; if (!isValid) { log.log({ l: "ignore device with invalid signature", diff --git a/src/matrix/e2ee/common.ts b/src/matrix/e2ee/common.ts index 27078135d9..c8a5ec0f4c 100644 --- a/src/matrix/e2ee/common.ts +++ b/src/matrix/e2ee/common.ts @@ -68,11 +68,17 @@ export function getEd25519Signature(signedValue: SignedValue, userId: string, de return signedValue?.signatures?.[userId]?.[`${SIGNATURE_ALGORITHM}:${deviceOrKeyId}`]; } -export function verifyEd25519Signature(olmUtil: Olm.Utility, userId: string, deviceOrKeyId: string, ed25519Key: string, value: SignedValue, log?: ILogItem) { +export enum SignatureVerification { + Valid, + Invalid, + NotSigned, +} + +export function verifyEd25519Signature(olmUtil: Olm.Utility, userId: string, deviceOrKeyId: string, ed25519Key: string, value: SignedValue, log?: ILogItem): SignatureVerification { const signature = getEd25519Signature(value, userId, deviceOrKeyId); if (!signature) { log?.set("no_signature", true); - return false; + return SignatureVerification.NotSigned; } const clone = Object.assign({}, value) as object; delete clone["unsigned"]; @@ -81,14 +87,14 @@ export function verifyEd25519Signature(olmUtil: Olm.Utility, userId: string, dev try { // throws when signature is invalid olmUtil.ed25519_verify(ed25519Key, canonicalJson, signature); - return true; + return SignatureVerification.Valid; } catch (err) { if (log) { const logItem = log.log({l: "Invalid signature, ignoring.", ed25519Key, canonicalJson, signature}); logItem.error = err; logItem.logLevel = log.level.Warn; } - return false; + return SignatureVerification.Invalid; } } diff --git a/src/matrix/e2ee/olm/Encryption.ts b/src/matrix/e2ee/olm/Encryption.ts index c4fee911f7..ef16ba45b6 100644 --- a/src/matrix/e2ee/olm/Encryption.ts +++ b/src/matrix/e2ee/olm/Encryption.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {groupByWithCreator} from "../../../utils/groupBy"; -import {verifyEd25519Signature, OLM_ALGORITHM, getDeviceCurve25519Key, getDeviceEd25519Key} from "../common"; +import {verifyEd25519Signature, OLM_ALGORITHM, getDeviceCurve25519Key, getDeviceEd25519Key, SignatureVerification} from "../common"; import {createSessionEntry} from "./Session"; import type {OlmMessage, OlmPayload, OlmEncryptedMessageContent} from "./types"; @@ -260,7 +260,7 @@ export class Encryption { const device = devicesByUser.get(userId)?.get(deviceId); if (device) { const isValidSignature = verifyEd25519Signature( - this.olmUtil, userId, deviceId, getDeviceEd25519Key(device), keySection, log); + this.olmUtil, userId, deviceId, getDeviceEd25519Key(device), keySection, log) === SignatureVerification.Valid; if (isValidSignature) { const target = EncryptionTarget.fromOTK(device, keySection.key); verifiedEncryptionTargets.push(target); diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 975f0d4369..c35914bb74 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -16,7 +16,7 @@ limitations under the License. import { ILogItem } from "../../lib"; import {pkSign} from "./common"; -import {verifyEd25519Signature} from "../e2ee/common"; +import {verifyEd25519Signature, SignatureVerification} from "../e2ee/common"; import type {SecretStorage} from "../ssss/SecretStorage"; import type {Storage} from "../storage/idb/Storage"; @@ -43,6 +43,27 @@ export enum KeyUsage { UserSigning = "user_signing" }; +export enum UserTrust { + /** We trust the user, the whole signature chain checks out from our MSK to all of their device keys. */ + Trusted = 1, + /** We haven't signed this user's identity yet. Verify this user first to sign it. */ + UserNotSigned, + /** We have signed the user already, but the signature isn't valid. + One possible cause could be that an attacker is uploading signatures in our name. */ + UserSignatureMismatch, + /** We trust the user, but they don't trust one of their devices. */ + UserDeviceNotSigned, + /** We trust the user, but the signatures of one of their devices is invalid. + * One possible cause could be that an attacker is uploading signatures in their name. */ + UserDeviceSignatureMismatch, + /** The user doesn't have a valid signature for the SSK with their MSK, or the SSK is missing. + * This likely means bootstrapping cross-signing on their end didn't finish correctly. */ + UserSetupError, + /** We don't have a valid signature for our SSK with our MSK, the SSK is missing, or we don't trust our own MSK. + * This likely means bootstrapping cross-signing on our end didn't finish correctly. */ + OwnSetupError +} + export class CrossSigning { private readonly storage: Storage; private readonly secretStorage: SecretStorage; @@ -161,31 +182,69 @@ export class CrossSigning { }); } - async isUserTrusted(userId: string, log: ILogItem): Promise { - return log.wrap("isUserTrusted", async log => { + async getUserTrust(userId: string, log: ILogItem): Promise { + return log.wrap("getUserTrust", async log => { log.set("id", userId); if (!this.isMasterKeyTrusted) { - return false; + return UserTrust.OwnSetupError; } - const theirDeviceKeys = await log.wrap("get their devices", log => this.deviceTracker.devicesForUsers([userId], this.hsApi, log)); - const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, log)); - if (!theirSSK) { - return false; + const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log)); + if (!ourMSK) { + return UserTrust.OwnSetupError; + } + const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, log)); + if (!ourUSK) { + return UserTrust.OwnSetupError; } - const hasUnsignedDevice = theirDeviceKeys.some(dk => log.wrap({l: "verify device", id: dk.device_id}, log => !this.hasValidSignatureFrom(dk, theirSSK, log))); - if (hasUnsignedDevice) { - return false; + const ourUSKVerification = log.wrap("verify our usk", log => this.hasValidSignatureFrom(ourUSK, ourMSK, log)); + if (ourUSKVerification !== SignatureVerification.Valid) { + return UserTrust.OwnSetupError; } const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log)); - if (!theirMSK || !log.wrap("verify their ssk", log => this.hasValidSignatureFrom(theirSSK, theirMSK, log))) { - return false; + if (!theirMSK) { + /* assume that when they don't have an MSK, they've never enabled cross-signing on their client + (or it's not supported) rather than assuming a setup error on their side. + Later on, for their SSK, we _do_ assume it's a setup error as it doesn't make sense to have an MSK without a SSK */ + return UserTrust.UserNotSigned; } - const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, log)); - if (!ourUSK || !log.wrap("verify their msk", log => this.hasValidSignatureFrom(theirMSK, ourUSK, log))) { - return false; + const theirMSKVerification = log.wrap("verify their msk", log => this.hasValidSignatureFrom(theirMSK, ourUSK, log)); + if (theirMSKVerification !== SignatureVerification.Valid) { + if (theirMSKVerification === SignatureVerification.NotSigned) { + return UserTrust.UserNotSigned; + } else { /* SignatureVerification.Invalid */ + return UserTrust.UserSignatureMismatch; + } } - const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log)); - return !!ourMSK && log.wrap("verify our usk", log => this.hasValidSignatureFrom(ourUSK, ourMSK, log)); + const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, log)); + if (!theirSSK) { + return UserTrust.UserSetupError; + } + const theirSSKVerification = log.wrap("verify their ssk", log => this.hasValidSignatureFrom(theirSSK, theirMSK, log)); + if (theirSSKVerification !== SignatureVerification.Valid) { + return UserTrust.UserSetupError; + } + const theirDeviceKeys = await log.wrap("get their devices", log => this.deviceTracker.devicesForUsers([userId], this.hsApi, log)); + const lowestDeviceVerification = theirDeviceKeys.reduce((lowest, dk) => log.wrap({l: "verify device", id: dk.device_id}, log => { + const verification = this.hasValidSignatureFrom(dk, theirSSK, log); + // first Invalid, then NotSigned, then Valid + if (lowest === SignatureVerification.Invalid || verification === SignatureVerification.Invalid) { + return SignatureVerification.Invalid; + } else if (lowest === SignatureVerification.NotSigned || verification === SignatureVerification.NotSigned) { + return SignatureVerification.NotSigned; + } else if (lowest === SignatureVerification.Valid || verification === SignatureVerification.Valid) { + return SignatureVerification.Valid; + } + // should never happen as we went over all the enum options + return SignatureVerification.Invalid; + }), SignatureVerification.Valid); + if (lowestDeviceVerification !== SignatureVerification.Valid) { + if (lowestDeviceVerification === SignatureVerification.NotSigned) { + return UserTrust.UserDeviceNotSigned; + } else { /* SignatureVerification.Invalid */ + return UserTrust.UserDeviceSignatureMismatch; + } + } + return UserTrust.Trusted; }); } @@ -217,10 +276,10 @@ export class CrossSigning { pkSign(this.olm, keyToSign, signingKey, this.ownUserId, ""); } - private hasValidSignatureFrom(key: DeviceKey | CrossSigningKey, signingKey: CrossSigningKey, log: ILogItem): boolean { + private hasValidSignatureFrom(key: DeviceKey | CrossSigningKey, signingKey: CrossSigningKey, log: ILogItem): SignatureVerification { const pubKey = getKeyEd25519Key(signingKey); if (!pubKey) { - return false; + return SignatureVerification.NotSigned; } return verifyEd25519Signature(this.olmUtil, signingKey.user_id, pubKey, pubKey, key, log); } From f1ecad5b58bd2e7450926111fb073ca91cee9851 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 7 Mar 2023 10:54:07 +0100 Subject: [PATCH 043/168] adjust UI to more detailed trust level --- .../rightpanel/MemberDetailsViewModel.js | 37 +++++++++++++++++-- .../session/rightpanel/MemberDetailsView.js | 2 + 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index df622aae46..0eabf33a06 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -17,6 +17,7 @@ limitations under the License. import {ViewModel} from "../../ViewModel"; import {RoomType} from "../../../matrix/room/common"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; +import {UserTrust} from "../../../matrix/verification/CrossSigning"; export class MemberDetailsViewModel extends ViewModel { constructor(options) { @@ -29,14 +30,14 @@ export class MemberDetailsViewModel extends ViewModel { this._session = options.session; this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange())); this.track(this._observableMember.subscribe( () => this._onMemberChange())); - this._isTrusted = false; + this._userTrust = undefined; this.init(); // TODO: call this from parent view model and do something smart with error view model if it fails async? } async init() { if (this.features.crossSigning) { - this._isTrusted = await this.logger.run({l: "MemberDetailsViewModel.verify user", id: this._member.userId}, log => { - return this._session.crossSigning.isUserTrusted(this._member.userId, log); + this._userTrust = await this.logger.run({l: "MemberDetailsViewModel.get user trust", id: this._member.userId}, log => { + return this._session.crossSigning.getUserTrust(this._member.userId, log); }); this.emitChange("isTrusted"); } @@ -44,7 +45,35 @@ export class MemberDetailsViewModel extends ViewModel { get name() { return this._member.name; } get userId() { return this._member.userId; } - get isTrusted() { return this._isTrusted; } + get isTrusted() { return this._userTrust === UserTrust.Trusted; } + get trustDescription() { + switch (this._userTrust) { + case UserTrust.Trusted: return this.i18n`You have verified this user. This user has verified all of their sessions.`; + case UserTrust.UserNotSigned: return this.i18n`You have not verified this user.`; + case UserTrust.UserSignatureMismatch: return this.i18n`You appear to have signed this user, but the signature is invalid.`; + case UserTrust.UserDeviceNotSigned: return this.i18n`You have verified this user, but they have one or more unverified sessions.`; + case UserTrust.UserDeviceSignatureMismatch: return this.i18n`This user has a session signature that is invalid.`; + case UserTrust.UserSetupError: return this.i18n`This user hasn't set up cross-signing correctly`; + case UserTrust.OwnSetupError: return this.i18n`Cross-signing wasn't set up correctly on your side.`; + default: return this.i18n`Pendingโ€ฆ`; + } + } + get trustShieldColor() { + if (!this._isEncrypted) { + return undefined; + } + switch (this._userTrust) { + case undefined: + case UserTrust.OwnSetupError: + return undefined; + case UserTrust.Trusted: + return "green"; + case UserTrust.UserNotSigned: + return "black"; + default: + return "red"; + } + } get type() { return "member-details"; } get shouldShowBackButton() { return true; } diff --git a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js index 45504a74e5..aa70d5b43b 100644 --- a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js +++ b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js @@ -27,6 +27,8 @@ export class MemberDetailsView extends TemplateView { if (vm.features.crossSigning) { securityNodes.push(t.p(vm => vm.isTrusted ? vm.i18n`This user is trusted` : vm.i18n`This user is not trusted`)); + securityNodes.push(t.p(vm => vm.trustDescription)); + securityNodes.push(t.p(["Shield color: ", vm => vm.trustShieldColor])); } return t.div({className: "MemberDetailsView"}, From a065189836e0c07769b7260e0d583e762d81143a Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 7 Mar 2023 11:00:52 +0100 Subject: [PATCH 044/168] delay signature validation of cross-signing keys until calculating trust always store them, if not we'll think that the user hasn't uploaded the cross-signing keys if we don't store them in spite of invalid or missing signature. --- src/matrix/e2ee/DeviceTracker.ts | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index ae00b1e0db..8a6e351b20 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -264,9 +264,9 @@ export class DeviceTracker { "token": this._getSyncToken() }, {log}).response(); - const masterKeys = log.wrap("master keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["master_keys"], KeyUsage.Master, undefined, log)); - const selfSigningKeys = log.wrap("self-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["self_signing_keys"], KeyUsage.SelfSigning, masterKeys, log)); - const userSigningKeys = log.wrap("user-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["user_signing_keys"], KeyUsage.UserSigning, masterKeys, log)); + const masterKeys = log.wrap("master keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["master_keys"], KeyUsage.Master, log)); + const selfSigningKeys = log.wrap("self-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["self_signing_keys"], KeyUsage.SelfSigning, log)); + const userSigningKeys = log.wrap("user-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["user_signing_keys"], KeyUsage.UserSigning, log)); const deviceKeys = log.wrap("device keys", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log)); const txn = await this._storage.readWriteTxn([ this._storage.storeNames.userIdentities, @@ -354,16 +354,14 @@ export class DeviceTracker { return allDeviceKeys; } - _filterVerifiedCrossSigningKeys(crossSigningKeysResponse: {[userId: string]: CrossSigningKey}, usage, parentKeys: Map | undefined, log): Map { + _filterVerifiedCrossSigningKeys(crossSigningKeysResponse: {[userId: string]: CrossSigningKey}, usage: KeyUsage, log: ILogItem): Map { const keys: Map = new Map(); if (!crossSigningKeysResponse) { return keys; } for (const [userId, keyInfo] of Object.entries(crossSigningKeysResponse)) { log.wrap({l: userId}, log => { - const parentKeyInfo = parentKeys?.get(userId); - const parentKey = parentKeyInfo && getKeyEd25519Key(parentKeyInfo); - if (this._validateCrossSigningKey(userId, keyInfo, usage, parentKey, log)) { + if (this._validateCrossSigningKey(userId, keyInfo, usage, log)) { keys.set(getKeyUserId(keyInfo)!, keyInfo); } }); @@ -371,7 +369,7 @@ export class DeviceTracker { return keys; } - _validateCrossSigningKey(userId: string, keyInfo: CrossSigningKey, usage: KeyUsage, parentKey: string | undefined, log: ILogItem): boolean { + _validateCrossSigningKey(userId: string, keyInfo: CrossSigningKey, usage: KeyUsage, log: ILogItem): boolean { if (getKeyUserId(keyInfo) !== userId) { log.log({l: "user_id mismatch", userId: keyInfo["user_id"]}); return false; @@ -385,24 +383,6 @@ export class DeviceTracker { log.log({l: "no ed25519 key", keys: keyInfo.keys}); return false; } - const isSelfSigned = usage === "master"; - const keyToVerifyWith = isSelfSigned ? publicKey : parentKey; - if (!keyToVerifyWith) { - log.log("signing_key not found"); - return false; - } - const hasSignature = !!getEd25519Signature(keyInfo, userId, keyToVerifyWith); - // self-signature is optional for now, not all keys seem to have it - if (!hasSignature && keyToVerifyWith !== publicKey) { - log.log({l: "signature not found", key: keyToVerifyWith}); - return false; - } - if (hasSignature) { - if(!verifyEd25519Signature(this._olmUtil, userId, keyToVerifyWith, keyToVerifyWith, keyInfo, log)) { - log.log("signature mismatch"); - return false; - } - } return true; } From 0b51fc0168d7237c9266ea7e8f30dc7d2eaf0aa1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Mar 2023 17:27:27 +0530 Subject: [PATCH 045/168] Throw specific error when cancelled --- src/matrix/verification/CrossSigning.ts | 20 +++++- .../verification/SAS/SASVerification.ts | 15 +++- .../SAS/VerificationCancelledError.ts | 25 +++++++ .../verification/SAS/channel/Channel.ts | 59 ++++++++++++---- .../SAS/stages/BaseSASVerificationStage.ts | 11 --- .../SAS/stages/CalculateSASStage.ts | 17 ++--- .../SAS/stages/RequestVerificationStage.ts | 39 +---------- .../stages/SelectVerificationMethodStage.ts | 12 ++-- .../SAS/stages/SendAcceptVerificationStage.ts | 25 ++----- .../verification/SAS/stages/SendDoneStage.ts | 6 -- .../verification/SAS/stages/SendKeyStage.ts | 7 +- .../verification/SAS/stages/SendMacStage.ts | 6 +- .../verification/SAS/stages/SendReadyStage.ts | 12 +--- .../verification/SAS/stages/VerifyMacStage.ts | 8 +-- .../SAS/stages/WaitForIncomingMessageStage.ts | 69 ------------------- 15 files changed, 125 insertions(+), 206 deletions(-) create mode 100644 src/matrix/verification/SAS/VerificationCancelledError.ts delete mode 100644 src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 2f152481be..40b905858f 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -45,6 +45,7 @@ export class CrossSigning { private readonly deviceMessageHandler: DeviceMessageHandler; private _isMasterKeyTrusted: boolean = false; private readonly deviceId: string; + private sasVerificationInProgress?: SASVerification; constructor(options: { storage: Storage, @@ -72,12 +73,20 @@ export class CrossSigning { this.deviceMessageHandler = options.deviceMessageHandler; this.deviceMessageHandler.on("message", async ({ unencrypted: unencryptedEvent }) => { + if (this.sasVerificationInProgress && + ( + !this.sasVerificationInProgress.finished || + // If the start message is for the previous sasverification, ignore it. + this.sasVerificationInProgress.channel.id === unencryptedEvent.content.transaction_id + )) { + return; + } console.log("unencrypted event", unencryptedEvent); if (unencryptedEvent.type === VerificationEventTypes.Request || unencryptedEvent.type === VerificationEventTypes.Start) { await this.platform.logger.run("Start verification from request", async (log) => { const sas = this.startVerification(unencryptedEvent.sender, log, unencryptedEvent); - await sas.start(); + await sas?.start(); }); } }) @@ -134,7 +143,10 @@ export class CrossSigning { return this._isMasterKeyTrusted; } - startVerification(userId: string, log: ILogItem, event?: any): SASVerification { + startVerification(userId: string, log: ILogItem, event?: any): SASVerification | undefined { + if (this.sasVerificationInProgress && !this.sasVerificationInProgress.finished) { + return; + } const channel = new ToDeviceChannel({ deviceTracker: this.deviceTracker, hsApi: this.hsApi, @@ -143,7 +155,8 @@ export class CrossSigning { deviceMessageHandler: this.deviceMessageHandler, log }, event); - return new SASVerification({ + + this.sasVerificationInProgress = new SASVerification({ olm: this.olm, olmUtil: this.olmUtil, ourUser: { userId: this.ownUserId, deviceId: this.deviceId }, @@ -154,6 +167,7 @@ export class CrossSigning { deviceTracker: this.deviceTracker, hsApi: this.hsApi, }); + return this.sasVerificationInProgress; } } diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 76839d9829..6f6f67b97d 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -24,6 +24,7 @@ import {HomeServerApi} from "../../net/HomeServerApi"; import {VerificationEventTypes} from "./channel/types"; import {SendReadyStage} from "./stages/SendReadyStage"; import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; +import {VerificationCancelledError} from "./VerificationCancelledError"; type Olm = typeof OlmNamespace; @@ -42,14 +43,16 @@ type Options = { export class SASVerification { private startStage: BaseSASVerificationStage; private olmSas: Olm.SAS; + public finished: boolean = false; + public readonly channel: IChannel; constructor(options: Options) { const { ourUser, otherUserId, log, olmUtil, olm, channel, e2eeAccount, deviceTracker, hsApi } = options; const olmSas = new olm.SAS(); this.olmSas = olmSas; - // channel.send("m.key.verification.request", {}, log); + this.channel = channel; try { - const options = { ourUser, otherUserId, log, olmSas, olmUtil, channel, e2eeAccount, deviceTracker, hsApi }; + const options = { ourUser, otherUserId, log, olmSas, olmUtil, channel, e2eeAccount, deviceTracker, hsApi}; let stage: BaseSASVerificationStage; if (channel.receivedMessages.get(VerificationEventTypes.Start)) { stage = new SelectVerificationMethodStage(options); @@ -71,12 +74,20 @@ export class SASVerification { try { let stage = this.startStage; do { + console.log("Running next stage"); await stage.completeStage(); stage = stage.nextStage; } while (stage); } + catch (e) { + if (!(e instanceof VerificationCancelledError)) { + throw e; + } + console.log("Caught error in start()"); + } finally { this.olmSas.free(); + this.finished = true; } } } diff --git a/src/matrix/verification/SAS/VerificationCancelledError.ts b/src/matrix/verification/SAS/VerificationCancelledError.ts new file mode 100644 index 0000000000..12d2a40222 --- /dev/null +++ b/src/matrix/verification/SAS/VerificationCancelledError.ts @@ -0,0 +1,25 @@ +/* +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. +*/ + +export class VerificationCancelledError extends Error { + get name(): string { + return "VerificationCancelledError"; + } + + get message(): string { + return "Verification is cancelled!"; + } +} diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 3339a7d266..24c1b6fb8e 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -21,9 +21,11 @@ import type {Platform} from "../../../../platform/web/Platform.js"; import type {DeviceMessageHandler} from "../../../DeviceMessageHandler.js"; import {makeTxnId} from "../../../common.js"; import {CancelTypes, VerificationEventTypes} from "./types"; +import {Disposables} from "../../../../lib"; +import {VerificationCancelledError} from "../VerificationCancelledError"; const messageFromErrorType = { - [CancelTypes.UserCancelled]: "User cancelled this request.", + [CancelTypes.UserCancelled]: "User declined", [CancelTypes.InvalidMessage]: "Invalid Message.", [CancelTypes.KeyMismatch]: "Key Mismatch.", [CancelTypes.OtherUserAccepted]: "Another device has accepted this request.", @@ -49,12 +51,12 @@ export interface IChannel { otherUserDeviceId: string; sentMessages: Map; receivedMessages: Map; - localMessages: Map; setStartMessage(content: any): void; setInitiatedByUs(value: boolean): void; initiatedByUs: boolean; startMessage: any; cancelVerification(cancellationType: CancelTypes): Promise; + getEvent(eventType: VerificationEventTypes.Accept): any; } type Options = { @@ -66,7 +68,7 @@ type Options = { log: ILogItem; } -export class ToDeviceChannel implements IChannel { +export class ToDeviceChannel extends Disposables implements IChannel { private readonly hsApi: HomeServerApi; private readonly deviceTracker: DeviceTracker; private readonly otherUserId: string; @@ -74,19 +76,20 @@ export class ToDeviceChannel implements IChannel { private readonly deviceMessageHandler: DeviceMessageHandler; public readonly sentMessages: Map = new Map(); public readonly receivedMessages: Map = new Map(); - public readonly localMessages: Map = new Map(); - private readonly waitMap: Map}> = new Map(); + private readonly waitMap: Map}> = new Map(); private readonly log: ILogItem; public otherUserDeviceId: string; public startMessage: any; public id: string; private _initiatedByUs: boolean; + private _isCancelled = false; /** * * @param startingMessage Create the channel with existing message in the receivedMessage buffer */ constructor(options: Options, startingMessage?: any) { + super(); this.hsApi = options.hsApi; this.deviceTracker = options.deviceTracker; this.otherUserId = options.otherUserId; @@ -94,7 +97,10 @@ export class ToDeviceChannel implements IChannel { this.log = options.log; this.deviceMessageHandler = options.deviceMessageHandler; // todo: find a way to dispose this subscription - this.deviceMessageHandler.on("message", ({unencrypted}) => this.handleDeviceMessage(unencrypted)) + this.track(this.deviceMessageHandler.disposableOn("message", ({ unencrypted }) => this.handleDeviceMessage(unencrypted))); + this.track(() => { + this.waitMap.forEach((value) => { value.reject(new VerificationCancelledError()); }); + }); // Copy over request message if (startingMessage) { /** @@ -105,14 +111,22 @@ export class ToDeviceChannel implements IChannel { this.receivedMessages.set(eventType, startingMessage); this.otherUserDeviceId = startingMessage.content.from_device; } + (window as any).foo = () => this.cancelVerification(CancelTypes.OtherUserAccepted); } get type() { return ChannelType.ToDeviceMessage; } + get isCancelled(): boolean { + return this._isCancelled; + } + async send(eventType: string, content: any, log: ILogItem): Promise { await log.wrap("ToDeviceChannel.send", async () => { + if (this.isCancelled) { + throw new VerificationCancelledError(); + } if (eventType === VerificationEventTypes.Request) { // Handle this case specially await this.handleRequestEventSpecially(eventType, content, log); @@ -128,12 +142,12 @@ export class ToDeviceChannel implements IChannel { } } } - await this.hsApi.sendToDevice(eventType, payload, this.id, { log }).response(); + await this.hsApi.sendToDevice(eventType, payload, makeTxnId(), { log }).response(); this.sentMessages.set(eventType, {content}); }); } - async handleRequestEventSpecially(eventType: string, content: any, log: ILogItem) { + private async handleRequestEventSpecially(eventType: string, content: any, log: ILogItem) { await log.wrap("ToDeviceChannel.handleRequestEventSpecially", async () => { const timestamp = this.platform.clock.now(); const txnId = makeTxnId(); @@ -146,10 +160,14 @@ export class ToDeviceChannel implements IChannel { } } } - await this.hsApi.sendToDevice(eventType, payload, txnId, { log }).response(); + await this.hsApi.sendToDevice(eventType, payload, makeTxnId(), { log }).response(); }); } + getEvent(eventType: VerificationEventTypes.Accept) { + return this.receivedMessages.get(eventType) ?? this.sentMessages.get(eventType); + } + private handleDeviceMessage(event) { this.log.wrap("ToDeviceChannel.handleDeviceMessage", (log) => { @@ -159,6 +177,11 @@ export class ToDeviceChannel implements IChannel { this.receivedMessages.set(event.type, event); if (event.type === VerificationEventTypes.Ready) { this.handleReadyMessage(event, log); + return; + } + if (event.type === VerificationEventTypes.Cancel) { + this.dispose(); + return; } }); } @@ -181,7 +204,7 @@ export class ToDeviceChannel implements IChannel { [this.otherUserId]: deviceMessages } } - await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, this.id, { log }).response(); + await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, makeTxnId(), { log }).response(); } catch (e) { console.log(e); @@ -191,6 +214,9 @@ export class ToDeviceChannel implements IChannel { async cancelVerification(cancellationType: CancelTypes) { await this.log.wrap("Channel.cancelVerification", async log => { + if (this.isCancelled) { + throw new VerificationCancelledError(); + } const payload = { messages: { [this.otherUserId]: { @@ -202,7 +228,9 @@ export class ToDeviceChannel implements IChannel { } } } - await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, this.id, { log }).response(); + await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, makeTxnId(), { log }).response(); + this._isCancelled = true; + this.dispose(); }); } @@ -226,12 +254,13 @@ export class ToDeviceChannel implements IChannel { if (existingWait) { return existingWait.promise; } - let resolve; + let resolve, reject; // Add to wait map - const promise = new Promise(r => { - resolve = r; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; }); - this.waitMap.set(eventType, { resolve, promise }); + this.waitMap.set(eventType, { resolve, reject, promise }); return promise; } diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index 82de5572f8..a61a2ce94d 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -70,16 +70,6 @@ export abstract class BaseSASVerificationStage extends Disposables { this.hsApi = options.hsApi; } - setRequestEventId(id: string) { - this.requestEventId = id; - // todo: can this race with incoming message? - this.nextStage?.setRequestEventId(id); - } - - setResultFromPreviousStage(result?: any) { - this.previousResult = result; - } - setNextStage(stage: BaseSASVerificationStage) { this._nextStage = stage; } @@ -88,6 +78,5 @@ export abstract class BaseSASVerificationStage extends Disposables { return this._nextStage; } - abstract get type(): string; abstract completeStage(): Promise; } diff --git a/src/matrix/verification/SAS/stages/CalculateSASStage.ts b/src/matrix/verification/SAS/stages/CalculateSASStage.ts index 4e20ff1d81..cea4d297c2 100644 --- a/src/matrix/verification/SAS/stages/CalculateSASStage.ts +++ b/src/matrix/verification/SAS/stages/CalculateSASStage.ts @@ -88,8 +88,8 @@ export class CalculateSASStage extends BaseSASVerificationStage { this.olmSAS.set_their_key(this.theirKey); const sasBytes = this.generateSASBytes(); const emoji = generateEmojiSas(Array.from(sasBytes)); - console.log("emoji", emoji); - this._nextStage = new SendMacStage(this.options); + console.log("Emoji calculated:", emoji); + this.setNextStage(new SendMacStage(this.options)); this.dispose(); }); } @@ -98,9 +98,10 @@ export class CalculateSASStage extends BaseSASVerificationStage { return await log.wrap("CalculateSASStage.verifyHashCommitment", async () => { const acceptMessage = this.channel.receivedMessages.get(VerificationEventTypes.Accept).content; const keyMessage = this.channel.receivedMessages.get(VerificationEventTypes.Key).content; - const commitmentStr = keyMessage.key + anotherjson.stringify(acceptMessage); + const commitmentStr = keyMessage.key + anotherjson.stringify(this.channel.startMessage.content); const receivedCommitment = acceptMessage.commitment; - if (this.olmUtil.sha256(commitmentStr) !== receivedCommitment) { + const hash = this.olmUtil.sha256(commitmentStr); + if (hash !== receivedCommitment) { log.set("Commitment mismatched!", {}); // cancel the process! await this.channel.cancelVerification(CancelTypes.MismatchedCommitment); @@ -120,8 +121,8 @@ export class CalculateSASStage extends BaseSASVerificationStage { } private generateSASBytes(): Uint8Array { - const keyAgreement = this.channel.sentMessages.get(VerificationEventTypes.Accept).content.key_agreement_protocol; - const otherUserDeviceId = this.channel.startMessage.content.from_device; + const keyAgreement = this.channel.getEvent(VerificationEventTypes.Accept).content.key_agreement_protocol; + const otherUserDeviceId = this.channel.otherUserDeviceId; const sasBytes = calculateKeyAgreement[keyAgreement]({ our: { userId: this.ourUser.userId, @@ -150,8 +151,4 @@ export class CalculateSASStage extends BaseSASVerificationStage { const { content } = this.channel.receivedMessages.get(VerificationEventTypes.Key); return content.key; } - - get type() { - return "m.key.verification.accept"; - } } diff --git a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts index 1b9e2e6e4a..a5e4c6de26 100644 --- a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts @@ -19,51 +19,16 @@ import {SelectVerificationMethodStage} from "./SelectVerificationMethodStage"; import {VerificationEventTypes} from "../channel/types"; export class RequestVerificationStage extends BaseSASVerificationStage { - async completeStage() { await this.log.wrap("StartVerificationStage.completeStage", async (log) => { const content = { - // "body": `${this.ourUser.userId} is requesting to verify your device, but your client does not support verification, so you may need to use a different verification method.`, "from_device": this.ourUser.deviceId, "methods": ["m.sas.v1"], - // "msgtype": "m.key.verification.request", - // "to": this.otherUserId, }; - // const promise = this.trackEventId(); - // await this.room.sendEvent("m.room.message", content, null, log); await this.channel.send(VerificationEventTypes.Request, content, log); - this._nextStage = new SelectVerificationMethodStage(this.options); - const readyContent = await this.channel.waitForEvent("m.key.verification.ready"); - // const eventId = await promise; - // console.log("eventId", eventId); - // this.setRequestEventId(eventId); + this.setNextStage(new SelectVerificationMethodStage(this.options)); + await this.channel.waitForEvent("m.key.verification.ready"); this.dispose(); }); } - - // private trackEventId(): Promise { - // return new Promise(resolve => { - // this.track( - // this.room._timeline.entries.subscribe({ - // onAdd: (_, entry) => { - // if (entry instanceof FragmentBoundaryEntry) { - // return; - // } - // if (!entry.isPending && - // entry.content["msgtype"] === "m.key.verification.request" && - // entry.content["from_device"] === this.ourUser.deviceId) { - // console.log("found event", entry); - // resolve(entry.id); - // } - // }, - // onRemove: () => { /**noop*/ }, - // onUpdate: () => { /**noop*/ }, - // }) - // ); - // }); - // } - - get type() { - return "m.key.verification.request"; - } } diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index dfe6dc35f5..af95d70ee2 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -18,6 +18,7 @@ import {KEY_AGREEMENT_LIST, HASHES_LIST, MAC_LIST, SAS_LIST} from "./constants"; import {CancelTypes, VerificationEventTypes} from "../channel/types"; import type {ILogItem} from "../../../../logging/types"; import {SendAcceptVerificationStage} from "./SendAcceptVerificationStage"; +import {SendKeyStage} from "./SendKeyStage"; export class SelectVerificationMethodStage extends BaseSASVerificationStage { private hasSentStartMessage = false; @@ -26,6 +27,7 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SelectVerificationMethodStage.completeStage", async (log) => { + (window as any).select = () => this.selectEmojiMethod(log); const startMessage = this.channel.waitForEvent(VerificationEventTypes.Start); const acceptMessage = this.channel.waitForEvent(VerificationEventTypes.Accept); const { content } = await Promise.race([startMessage, acceptMessage]); @@ -45,7 +47,11 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { this.channel.setStartMessage(this.channel.sentMessages.get(VerificationEventTypes.Start)); this.channel.setInitiatedByUs(true); } - if (!this.channel.initiatedByUs) { + if (this.channel.initiatedByUs) { + await acceptMessage; + this.setNextStage(new SendKeyStage(this.options)); + } + else { // We need to send the accept message next this.setNextStage(new SendAcceptVerificationStage(this.options)); } @@ -86,8 +92,4 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { await this.channel.send(VerificationEventTypes.Start, content, log); this.hasSentStartMessage = true; } - - get type() { - return "SelectVerificationStage"; - } } diff --git a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts index 6b964445d4..9c7c86be8e 100644 --- a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts @@ -15,19 +15,14 @@ limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import anotherjson from "another-json"; -import type { KeyAgreement, MacMethod } from "./constants"; import {HASHES_LIST, MAC_LIST, SAS_SET, KEY_AGREEMENT_LIST} from "./constants"; -import { VerificationEventTypes } from "../channel/types"; -import { SendKeyStage } from "./SendKeyStage"; +import {VerificationEventTypes} from "../channel/types"; +import {SendKeyStage} from "./SendKeyStage"; export class SendAcceptVerificationStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SendAcceptVerificationStage.completeStage", async (log) => { - const event = this.channel.startMessage; - const content = { - ...event.content, - // "m.relates_to": event.relation, - }; + const { content } = this.channel.startMessage; const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0]; const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0]; const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0]; @@ -50,24 +45,12 @@ export class SendAcceptVerificationStage extends BaseSASVerificationStage { rel_type: "m.reference", } }; - // await this.room.sendEvent("m.key.verification.accept", contentToSend, null, log); await this.channel.send(VerificationEventTypes.Accept, contentToSend, log); - this.channel.localMessages.set("our_pub_key", ourPubKey); await this.channel.waitForEvent(VerificationEventTypes.Key); - this._nextStage = new SendKeyStage(this.options); - // this.nextStage?.setResultFromPreviousStage({ - // ...this.previousResult, - // [this.type]: contentToSend, - // "our_pub_key": ourPubKey, - // }); + this.setNextStage(new SendKeyStage(this.options)); this.dispose(); }); } - - - get type() { - return "m.key.verification.accept"; - } } function intersection(anArray: T[], aSet: Set): T[] { diff --git a/src/matrix/verification/SAS/stages/SendDoneStage.ts b/src/matrix/verification/SAS/stages/SendDoneStage.ts index d19ab7238b..d090b3c0a5 100644 --- a/src/matrix/verification/SAS/stages/SendDoneStage.ts +++ b/src/matrix/verification/SAS/stages/SendDoneStage.ts @@ -16,17 +16,11 @@ limitations under the License. import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {VerificationEventTypes} from "../channel/types"; - export class SendDoneStage extends BaseSASVerificationStage { - async completeStage() { await this.log.wrap("VerifyMacStage.completeStage", async (log) => { await this.channel.send(VerificationEventTypes.Done, {}, log); this.dispose(); }); } - - get type() { - return "m.key.verification.accept"; - } } diff --git a/src/matrix/verification/SAS/stages/SendKeyStage.ts b/src/matrix/verification/SAS/stages/SendKeyStage.ts index c9455663fd..af2c8e4964 100644 --- a/src/matrix/verification/SAS/stages/SendKeyStage.ts +++ b/src/matrix/verification/SAS/stages/SendKeyStage.ts @@ -18,7 +18,6 @@ import {VerificationEventTypes} from "../channel/types"; import {CalculateSASStage} from "./CalculateSASStage"; export class SendKeyStage extends BaseSASVerificationStage { - async completeStage() { await this.log.wrap("SendKeyStage.completeStage", async (log) => { const ourSasKey = this.olmSAS.get_pubkey(); @@ -30,12 +29,8 @@ export class SendKeyStage extends BaseSASVerificationStage { * key. */ await this.channel.waitForEvent(VerificationEventTypes.Key); - this._nextStage = new CalculateSASStage(this.options) + this.setNextStage(new CalculateSASStage(this.options)); this.dispose(); }); } - - get type() { - return "m.key.verification.accept"; - } } diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts index 5ffc56a926..f3759ab6a7 100644 --- a/src/matrix/verification/SAS/stages/SendMacStage.ts +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -37,7 +37,7 @@ export class SendMacStage extends BaseSASVerificationStage { this.calculateMAC = createCalculateMAC(this.olmSAS, macMethod); await this.sendMAC(log); await this.channel.waitForEvent(VerificationEventTypes.Mac); - this._nextStage = new VerifyMacStage(this.options); + this.setNextStage(new VerifyMacStage(this.options)); this.dispose(); }); } @@ -70,9 +70,5 @@ export class SendMacStage extends BaseSASVerificationStage { console.log("result", mac, keys); await this.channel.send(VerificationEventTypes.Mac, { mac, keys }, log); } - - get type() { - return "m.key.verification.accept"; - } } diff --git a/src/matrix/verification/SAS/stages/SendReadyStage.ts b/src/matrix/verification/SAS/stages/SendReadyStage.ts index e70d895343..b4591579ac 100644 --- a/src/matrix/verification/SAS/stages/SendReadyStage.ts +++ b/src/matrix/verification/SAS/stages/SendReadyStage.ts @@ -15,26 +15,18 @@ limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {VerificationEventTypes} from "../channel/types"; -import { SelectVerificationMethodStage } from "./SelectVerificationMethodStage"; +import {SelectVerificationMethodStage} from "./SelectVerificationMethodStage"; export class SendReadyStage extends BaseSASVerificationStage { - async completeStage() { await this.log.wrap("StartVerificationStage.completeStage", async (log) => { const content = { - // "body": `${this.ourUser.userId} is requesting to verify your device, but your client does not support verification, so you may need to use a different verification method.`, "from_device": this.ourUser.deviceId, "methods": ["m.sas.v1"], - // "msgtype": "m.key.verification.request", - // "to": this.otherUserId, }; await this.channel.send(VerificationEventTypes.Ready, content, log); - this._nextStage = new SelectVerificationMethodStage(this.options); + this.setNextStage(new SelectVerificationMethodStage(this.options)); this.dispose(); }); } - - get type() { - return "m.key.verification.request"; - } } diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 634b8ccb5b..2eb370185f 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -18,7 +18,7 @@ import {ILogItem} from "../../../../lib"; import {VerificationEventTypes} from "../channel/types"; import {createCalculateMAC} from "../mac"; import type * as OlmNamespace from "@matrix-org/olm"; -import { SendDoneStage } from "./SendDoneStage"; +import {SendDoneStage} from "./SendDoneStage"; type Olm = typeof OlmNamespace; export type KeyVerifier = (keyId: string, device: any, keyInfo: string) => void; @@ -39,7 +39,7 @@ export class VerifyMacStage extends BaseSASVerificationStage { this.calculateMAC = createCalculateMAC(this.olmSAS, macMethod); await this.checkMAC(log); await this.channel.waitForEvent(VerificationEventTypes.Done); - this._nextStage = new SendDoneStage(this.options); + this.setNextStage(new SendDoneStage(this.options)); this.dispose(); }); } @@ -88,8 +88,4 @@ export class VerifyMacStage extends BaseSASVerificationStage { } } } - - get type() { - return "m.key.verification.accept"; - } } diff --git a/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts b/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts deleted file mode 100644 index 087883acbe..0000000000 --- a/src/matrix/verification/SAS/stages/WaitForIncomingMessageStage.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* -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 {BaseSASVerificationStage, Options} from "./BaseSASVerificationStage"; -import {FragmentBoundaryEntry} from "../../../room/timeline/entries/FragmentBoundaryEntry.js"; - -export class WaitForIncomingMessageStage extends BaseSASVerificationStage { - constructor(private messageType: string, options: Options) { - super(options); - } - - async completeStage() { - await this.log.wrap("WaitForIncomingMessageStage.completeStage", async (log) => { - const entry = await this.fetchMessageEventsFromTimeline(); - console.log("content", entry); - this.nextStage?.setResultFromPreviousStage({ - ...this.previousResult, - [this.messageType]: entry - }); - this.dispose(); - }); - } - - private fetchMessageEventsFromTimeline() { - // todo: add timeout after 10 mins - return new Promise(resolve => { - this.track( - this.room._timeline.entries.subscribe({ - onAdd: (_, entry) => { - if (entry.eventType === this.messageType && - entry.relatedEventId === this.requestEventId) { - resolve(entry); - } - }, - onRemove: () => { }, - onUpdate: () => { }, - }) - ); - const remoteEntries = this.room._timeline.remoteEntries; - // In case we were slow and the event is already added to the timeline, - for (const entry of remoteEntries) { - if (entry instanceof FragmentBoundaryEntry) { - return; - } - if (entry.eventType === this.messageType && - entry.relatedEventId === this.requestEventId) { - resolve(entry); - } - } - }); - } - - get type() { - return this.messageType; - } -} - From a69246fb5ad32e2b241e415f8c73f813bb9f57ad Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 7 Mar 2023 14:40:11 +0100 Subject: [PATCH 046/168] return undefined if we don't have the signing key --- src/matrix/verification/CrossSigning.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index c35914bb74..05be9ba199 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -165,10 +165,13 @@ export class CrossSigning { } const keyToSign = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log); if (!keyToSign) { - return undefined; + return; } - delete keyToSign.signatures; const signingKey = await this.getSigningKey(KeyUsage.UserSigning); + if (!signingKey) { + return; + } + delete keyToSign.signatures; // add signature to keyToSign this.signKey(keyToSign, signingKey); const payload = { @@ -248,8 +251,11 @@ export class CrossSigning { }); } - private async signDeviceKey(keyToSign: DeviceKey, log: ILogItem): Promise { + private async signDeviceKey(keyToSign: DeviceKey, log: ILogItem): Promise { const signingKey = await this.getSigningKey(KeyUsage.SelfSigning); + if (!signingKey) { + return undefined; + } // add signature to keyToSign this.signKey(keyToSign, signingKey); // so the payload format of a signature is a map from userid to key id of the signed key @@ -265,11 +271,12 @@ export class CrossSigning { return keyToSign; } - private async getSigningKey(usage: KeyUsage): Promise { + private async getSigningKey(usage: KeyUsage): Promise { const txn = await this.storage.readTxn([this.storage.storeNames.accountData]); const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`, txn); - const seed = new Uint8Array(this.platform.encoding.base64.decode(seedStr)); - return seed; + if (seedStr) { + return new Uint8Array(this.platform.encoding.base64.decode(seedStr)); + } } private signKey(keyToSign: DeviceKey | CrossSigningKey, signingKey: Uint8Array) { From 1f8fb93ba2d91c67f4b10fdc1877e67e794e2439 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Mar 2023 23:38:04 +0530 Subject: [PATCH 047/168] Implement timeout and cancel --- src/matrix/verification/CrossSigning.ts | 1 + .../verification/SAS/SASVerification.ts | 39 +++++++++++-------- .../verification/SAS/channel/Channel.ts | 17 ++++++-- .../SAS/stages/CalculateSASStage.ts | 3 +- .../SAS/stages/RequestVerificationStage.ts | 1 - .../SAS/stages/SendAcceptVerificationStage.ts | 5 ++- 6 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 40b905858f..2e4ebd2626 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -166,6 +166,7 @@ export class CrossSigning { e2eeAccount: this.e2eeAccount, deviceTracker: this.deviceTracker, hsApi: this.hsApi, + platform: this.platform, }); return this.sasVerificationInProgress; } diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 6f6f67b97d..cab57283a6 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -21,10 +21,12 @@ import type {DeviceTracker} from "../../e2ee/DeviceTracker.js"; import type * as OlmNamespace from "@matrix-org/olm"; import {IChannel} from "./channel/Channel"; import {HomeServerApi} from "../../net/HomeServerApi"; -import {VerificationEventTypes} from "./channel/types"; +import {CancelTypes, VerificationEventTypes} from "./channel/types"; import {SendReadyStage} from "./stages/SendReadyStage"; import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; import {VerificationCancelledError} from "./VerificationCancelledError"; +import {Timeout} from "../../../platform/types/types"; +import {Platform} from "../../../platform/web/Platform.js"; type Olm = typeof OlmNamespace; @@ -38,6 +40,7 @@ type Options = { e2eeAccount: Account; deviceTracker: DeviceTracker; hsApi: HomeServerApi; + platform: Platform; } export class SASVerification { @@ -45,29 +48,30 @@ export class SASVerification { private olmSas: Olm.SAS; public finished: boolean = false; public readonly channel: IChannel; + private readonly timeout: Timeout; constructor(options: Options) { - const { ourUser, otherUserId, log, olmUtil, olm, channel, e2eeAccount, deviceTracker, hsApi } = options; + const { olm, channel, platform } = options; const olmSas = new olm.SAS(); this.olmSas = olmSas; this.channel = channel; - try { - const options = { ourUser, otherUserId, log, olmSas, olmUtil, channel, e2eeAccount, deviceTracker, hsApi}; - let stage: BaseSASVerificationStage; - if (channel.receivedMessages.get(VerificationEventTypes.Start)) { - stage = new SelectVerificationMethodStage(options); - } - else if (channel.receivedMessages.get(VerificationEventTypes.Request)) { - stage = new SendReadyStage(options); - } - else { - stage = new RequestVerificationStage(options); - } - this.startStage = stage; - console.log("startStage", this.startStage); + this.timeout = platform.clock.createTimeout(10 * 60 * 1000); + this.timeout.elapsed().then(() => { + // Cancel verification after 10 minutes + // todo: catch error here? + channel.cancelVerification(CancelTypes.TimedOut); + }); + const stageOptions = {...options, olmSas}; + if (channel.receivedMessages.get(VerificationEventTypes.Start)) { + this.startStage = new SelectVerificationMethodStage(stageOptions); } - finally { + else if (channel.receivedMessages.get(VerificationEventTypes.Request)) { + this.startStage = new SendReadyStage(stageOptions); + } + else { + this.startStage = new RequestVerificationStage(stageOptions); } + console.log("startStage", this.startStage); } async start() { @@ -88,6 +92,7 @@ export class SASVerification { finally { this.olmSas.free(); this.finished = true; + this.timeout.abort(); } } } diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 24c1b6fb8e..239b265d32 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -96,8 +96,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { this.platform = options.platform; this.log = options.log; this.deviceMessageHandler = options.deviceMessageHandler; - // todo: find a way to dispose this subscription - this.track(this.deviceMessageHandler.disposableOn("message", ({ unencrypted }) => this.handleDeviceMessage(unencrypted))); + this.track(this.deviceMessageHandler.disposableOn("message", async ({ unencrypted }) => await this.handleDeviceMessage(unencrypted))); this.track(() => { this.waitMap.forEach((value) => { value.reject(new VerificationCancelledError()); }); }); @@ -169,8 +168,18 @@ export class ToDeviceChannel extends Disposables implements IChannel { } - private handleDeviceMessage(event) { - this.log.wrap("ToDeviceChannel.handleDeviceMessage", (log) => { + private async handleDeviceMessage(event) { + await this.log.wrap("ToDeviceChannel.handleDeviceMessage", async (log) => { + if (event.content.transaction_id !== this.id) { + /** + * When a device receives an unknown transaction_id, it should send an appropriate + * m.key.verification.cancel message to the other device indicating as such. + * This does not apply for inbound m.key.verification.start or m.key.verification.cancel messages. + */ + console.log("Received event with unknown transaction id: ", event); + await this.cancelVerification(CancelTypes.UnknownTransaction); + return; + } console.log("event", event); log.set("event", event); this.resolveAnyWaits(event); diff --git a/src/matrix/verification/SAS/stages/CalculateSASStage.ts b/src/matrix/verification/SAS/stages/CalculateSASStage.ts index cea4d297c2..4fd3ed5738 100644 --- a/src/matrix/verification/SAS/stages/CalculateSASStage.ts +++ b/src/matrix/verification/SAS/stages/CalculateSASStage.ts @@ -140,9 +140,10 @@ export class CalculateSASStage extends BaseSASVerificationStage { return sasBytes; } - emojiMatch(match: boolean) { + async emojiMatch(match: boolean) { if (!match) { // cancel the verification + await this.channel.cancelVerification(CancelTypes.MismatchedSAS); } } diff --git a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts index a5e4c6de26..3b273fd88c 100644 --- a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -// import {FragmentBoundaryEntry} from "../../../room/timeline/entries/FragmentBoundaryEntry.js"; import {SelectVerificationMethodStage} from "./SelectVerificationMethodStage"; import {VerificationEventTypes} from "../channel/types"; diff --git a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts index 9c7c86be8e..b9112bbc80 100644 --- a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts @@ -16,7 +16,7 @@ limitations under the License. import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import anotherjson from "another-json"; import {HASHES_LIST, MAC_LIST, SAS_SET, KEY_AGREEMENT_LIST} from "./constants"; -import {VerificationEventTypes} from "../channel/types"; +import {CancelTypes, VerificationEventTypes} from "../channel/types"; import {SendKeyStage} from "./SendKeyStage"; export class SendAcceptVerificationStage extends BaseSASVerificationStage { @@ -29,7 +29,8 @@ export class SendAcceptVerificationStage extends BaseSASVerificationStage { const sasMethods = intersection(content.short_authentication_string, SAS_SET); if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) { // todo: ensure this cancels the verification - throw new Error("Descriptive error here!"); + await this.channel.cancelVerification(CancelTypes.UnknownMethod); + return; } const ourPubKey = this.olmSAS.get_pubkey(); const commitmentStr = ourPubKey + anotherjson.stringify(content); From 760da6277a1ee8e8648ab32f33911956d9d82012 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 9 Mar 2023 09:08:01 +0100 Subject: [PATCH 048/168] remove unused transaction --- src/matrix/verification/CrossSigning.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 05be9ba199..e491512994 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -101,7 +101,6 @@ export class CrossSigning { async init(log: ILogItem) { await log.wrap("CrossSigning.init", async log => { // TODO: use errorboundary here - const txn = await this.storage.readTxn([this.storage.storeNames.accountData]); const privateMasterKey = await this.getSigningKey(KeyUsage.Master); const signing = new this.olm.PkSigning(); let derivedPublicKey; From 780dfeb199c24a2e52c3fd813c172ded11bbfe6d Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 13 Mar 2023 09:15:49 +0100 Subject: [PATCH 049/168] WIP --- src/matrix/Session.js | 123 ++++++++++-------- src/matrix/e2ee/DeviceTracker.ts | 10 +- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 16 ++- src/matrix/ssss/SecretStorage.ts | 37 +++++- .../storage/idb/stores/AccountDataStore.ts | 6 +- src/matrix/verification/CrossSigning.ts | 44 +++++-- 6 files changed, 160 insertions(+), 76 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 82eeba688a..0b5b857741 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -252,16 +252,14 @@ export class Session { this._keyBackup.get().dispose(); this._keyBackup.set(null); } + // TODO: stop cross-signing const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); - // and create key backup, which needs to read from accountData - const readTxn = await this._storage.readTxn([ - this._storage.storeNames.accountData, - ]); - if (await this._createKeyBackup(key, readTxn, log)) { + if (await this._tryLoadSecretStorage(key, undefined, log)) { // only after having read a secret, write the key // as we only find out if it was good if the MAC verification succeeds await this._writeSSSSKey(key, log); - this._keyBackup.get().flush(log); + await this._keyBackup?.start(log); + await this._crossSigning?.start(log); return key; } else { throw new Error("Could not read key backup with the given key"); @@ -317,12 +315,35 @@ export class Session { this._keyBackup.get().dispose(); this._keyBackup.set(null); } + // TODO: stop cross-signing } - _createKeyBackup(ssssKey, txn, log) { - return log.wrap("enable key backup", async log => { - try { - const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); + _tryLoadSecretStorage(ssssKey, existingTxn, log) { + return log.wrap("enable secret storage", async log => { + const txn = existingTxn ?? await this._storage.readTxn([ + this._storage.storeNames.accountData, + this._storage.storeNames.crossSigningKeys, + this._storage.storeNames.userIdentities, + ]); + const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); + const isValid = await secretStorage.hasValidKeyForAnyAccountData(txn); + log.set("isValid", isValid); + if (isValid) { + await this._loadSecretStorageServices(secretStorage, txn, log); + } + if (!this._keyBackup.get()) { + // null means key backup isn't configured yet + // as opposed to undefined, which means we're still checking + this._keyBackup.set(null); + } + return isValid; + }); + } + + _loadSecretStorageServices(secretStorage, txn, log) { + try { + await log.wrap("enable key backup", async log => { + // TODO: delay network request here until start() const keyBackup = await KeyBackup.fromSecretStorage( this._platform, this._olm, @@ -333,22 +354,6 @@ export class Session { txn ); if (keyBackup) { - if (this._features.crossSigning) { - this._crossSigning = new CrossSigning({ - storage: this._storage, - secretStorage, - platform: this._platform, - olm: this._olm, - olmUtil: this._olmUtil, - deviceTracker: this._deviceTracker, - hsApi: this._hsApi, - ownUserId: this.userId, - e2eeAccount: this._e2eeAccount - }); - await log.wrap("enable cross-signing", log => { - return this._crossSigning.init(log); - }); - } for (const room of this._rooms.values()) { if (room.isEncrypted) { room.enableKeyBackup(keyBackup); @@ -359,11 +364,28 @@ export class Session { } else { log.set("no_backup", true); } - } catch (err) { - log.catch(err); + }); + 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, + hsApi: this._hsApi, + ownUserId: this.userId, + e2eeAccount: this._e2eeAccount + }); + if (crossSigning.load(txn, log)) { + this._crossSigning = crossSigning; + } + }); } - return false; - }); + } catch (err) { + log.catch(err); + } } /** @@ -467,6 +489,8 @@ export class Session { this._storage.storeNames.timelineEvents, this._storage.storeNames.timelineFragments, this._storage.storeNames.pendingEvents, + this._storage.storeNames.accountData, + this._storage.storeNames.crossSigningKeys, ]); // restore session object this._syncInfo = await txn.session.get("sync"); @@ -484,6 +508,11 @@ export class Session { if (this._e2eeAccount) { log.set("keys", this._e2eeAccount.identityKeys); this._setupEncryption(); + // try set up session backup if we stored the ssss key + const ssssKey = await ssssReadKey(txn); + if (ssssKey) { + await this._tryLoadSecretStorage(ssssKey, txn, log); + } } } const pendingEventsByRoomId = await this._getPendingEventsByRoom(txn); @@ -544,35 +573,21 @@ export class Session { // TODO: what can we do if this throws? await txn.complete(); } - // enable session backup, this requests the latest backup version - if (!this._keyBackup.get()) { - if (dehydratedDevice) { - await log.wrap("SSSSKeyFromDehydratedDeviceKey", async log => { - const ssssKey = await createSSSSKeyFromDehydratedDeviceKey(dehydratedDevice.key, this._storage, this._platform); - if (ssssKey) { + // try if the key used to decrypt the dehydrated device also fits for secret storage + if (dehydratedDevice) { + await log.wrap("SSSSKeyFromDehydratedDeviceKey", async log => { + const ssssKey = await createSSSSKeyFromDehydratedDeviceKey(dehydratedDevice.key, this._storage, this._platform); + if (ssssKey) { + if (await this._tryLoadSecretStorage(ssssKey, undefined, log)) { log.set("success", true); await this._writeSSSSKey(ssssKey); } - }); - } - const txn = await this._storage.readTxn([ - this._storage.storeNames.session, - this._storage.storeNames.accountData, - ]); - // try set up session backup if we stored the ssss key - const ssssKey = await ssssReadKey(txn); - if (ssssKey) { - // txn will end here as this does a network request - if (await this._createKeyBackup(ssssKey, txn, log)) { - this._keyBackup.get()?.flush(log); } - } - if (!this._keyBackup.get()) { - // null means key backup isn't configured yet - // as opposed to undefined, which means we're still checking - this._keyBackup.set(null); - } + }); } + this._keyBackup?.start(log); + this._crossSigning?.start(log); + // restore unfinished operations, like sending out room keys const opsTxn = await this._storage.readWriteTxn([ this._storage.storeNames.operations diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 8a6e351b20..23bdc31eea 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -163,16 +163,20 @@ export class DeviceTracker { } } - async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi, log: ILogItem): Promise { + async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi | undefined, existingTxn: Transaction | undefined, log: ILogItem): Promise { return await log.wrap({l: "DeviceTracker.getCrossSigningKeyForUser", id: userId, usage}, async log => { - let txn = await this._storage.readTxn([ + const txn = existingTxn ?? await this._storage.readTxn([ this._storage.storeNames.userIdentities, this._storage.storeNames.crossSigningKeys, ]); - let userIdentity = await txn.userIdentities.get(userId); + const userIdentity = await txn.userIdentities.get(userId); if (userIdentity && userIdentity.keysTrackingStatus !== KeysTrackingStatus.Outdated) { return await txn.crossSigningKeys.get(userId, usage); } + // not allowed to access the network, bail out + if (!hsApi) { + return undefined; + } // fetch from hs const keys = await this._queryKeys([userId], hsApi, log); switch (usage) { diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index bcfbf85aa7..0ef610ffe3 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -38,15 +38,14 @@ const KEYS_PER_REQUEST = 200; export class KeyBackup { public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); - private _stopped = false; private _needsNewKey = false; private _hasBackedUpAllKeys = false; private _error?: Error; + private crypto?: Curve25519.BackupEncryption; constructor( private readonly backupInfo: BackupInfo, - private readonly crypto: Curve25519.BackupEncryption, private readonly hsApi: HomeServerApi, private readonly keyLoader: KeyLoader, private readonly storage: Storage, @@ -61,6 +60,9 @@ export class KeyBackup { get hasBackedUpAllKeys(): boolean { return this._hasBackedUpAllKeys; } async getRoomKey(roomId: string, sessionId: string, log: ILogItem): Promise { + if (this.needsNewKey || !this.crypto) { + return; + } const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response(); if (!sessionResponse.session_data) { return; @@ -77,6 +79,12 @@ export class KeyBackup { return txn.inboundGroupSessions.markAllAsNotBackedUp(); } + start(log: ILogItem) { + + // fetch latest version + this.flush(log); + } + flush(log: ILogItem): void { if (!this.operationInProgress.get()) { log.wrapDetached("flush key backup", async log => { @@ -184,7 +192,7 @@ export class KeyBackup { } dispose() { - this.crypto.dispose(); + this.crypto?.dispose(); } static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction): Promise { @@ -194,7 +202,7 @@ export class KeyBackup { const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo; if (backupInfo.algorithm === Curve25519.Algorithm) { const crypto = Curve25519.BackupEncryption.fromAuthData(backupInfo.auth_data, privateKey, olm); - return new KeyBackup(backupInfo, crypto, hsApi, keyLoader, storage, platform); + return new KeyBackup(backupInfo, privateKey, hsApi, keyLoader, storage, platform); } else { throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`); } diff --git a/src/matrix/ssss/SecretStorage.ts b/src/matrix/ssss/SecretStorage.ts index c026b4534d..ebdcd13a4c 100644 --- a/src/matrix/ssss/SecretStorage.ts +++ b/src/matrix/ssss/SecretStorage.ts @@ -16,6 +16,8 @@ limitations under the License. import type {Key} from "./common"; import type {Platform} from "../../platform/web/Platform.js"; import type {Transaction} from "../storage/idb/Transaction"; +import type {Storage} from "../storage/idb/Storage"; +import type {AccountDataEntry} from "../storage/idb/stores/AccountDataStore"; type EncryptedData = { iv: string; @@ -23,6 +25,18 @@ type EncryptedData = { mac: string; } +export enum DecryptionFailure { + NotEncryptedWithKey, + BadMAC, + UnsupportedAlgorithm, +} + +class DecryptionError extends Error { + constructor(msg: string, public readonly reason: DecryptionFailure) { + super(msg); + } +} + export class SecretStorage { private readonly _key: Key; private readonly _platform: Platform; @@ -32,20 +46,37 @@ export class SecretStorage { this._platform = platform; } + async hasValidKeyForAnyAccountData(txn: Transaction) { + const allAccountData = await txn.accountData.getAll(); + for (const accountData of allAccountData) { + try { + const secret = await this._decryptAccountData(accountData); + return true; // decryption succeeded + } catch (err) { + continue; + } + } + return false; + } + async readSecret(name: string, txn: Transaction): Promise { const accountData = await txn.accountData.get(name); if (!accountData) { return; } + return await this._decryptAccountData(accountData); + } + + async _decryptAccountData(accountData: AccountDataEntry): Promise { const encryptedData = accountData?.content?.encrypted?.[this._key.id] as EncryptedData; if (!encryptedData) { - throw new Error(`Secret ${accountData.type} is not encrypted for key ${this._key.id}`); + throw new DecryptionError(`Secret ${accountData.type} is not encrypted for key ${this._key.id}`, DecryptionFailure.NotEncryptedWithKey); } if (this._key.algorithm === "m.secret_storage.v1.aes-hmac-sha2") { return await this._decryptAESSecret(accountData.type, encryptedData); } else { - throw new Error(`Unsupported algorithm for key ${this._key.id}: ${this._key.algorithm}`); + throw new DecryptionError(`Unsupported algorithm for key ${this._key.id}: ${this._key.algorithm}`, DecryptionFailure.UnsupportedAlgorithm); } } @@ -68,7 +99,7 @@ export class SecretStorage { ciphertextBytes, "SHA-256"); if (!isVerified) { - throw new Error("Bad MAC"); + throw new DecryptionError("Bad MAC", DecryptionFailure.BadMAC); } const plaintextBytes = await this._platform.crypto.aes.decryptCTR({ diff --git a/src/matrix/storage/idb/stores/AccountDataStore.ts b/src/matrix/storage/idb/stores/AccountDataStore.ts index 2081ad8feb..33c8a1621b 100644 --- a/src/matrix/storage/idb/stores/AccountDataStore.ts +++ b/src/matrix/storage/idb/stores/AccountDataStore.ts @@ -16,7 +16,7 @@ limitations under the License. import {Store} from "../Store"; import {Content} from "../../types"; -interface AccountDataEntry { +export interface AccountDataEntry { type: string; content: Content; } @@ -35,4 +35,8 @@ export class AccountDataStore { set(event: AccountDataEntry): void { this._store.put(event); } + + async getAll(): Promise> { + return await this._store.selectAll(); + } } diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index e491512994..1abc3702ef 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -20,6 +20,7 @@ import {verifyEd25519Signature, SignatureVerification} from "../e2ee/common"; import type {SecretStorage} from "../ssss/SecretStorage"; import type {Storage} from "../storage/idb/Storage"; +import type {Transaction} from "../storage/idb/Transaction"; import type {Platform} from "../../platform/web/Platform"; import type {DeviceTracker} from "../e2ee/DeviceTracker"; import type {HomeServerApi} from "../net/HomeServerApi"; @@ -98,10 +99,25 @@ export class CrossSigning { this.e2eeAccount = options.e2eeAccount } - async init(log: ILogItem) { - await log.wrap("CrossSigning.init", async log => { + async load(txn: Transaction, log: ILogItem) { + // try to verify the msk without accessing the network + return await this.verifyMSKFrom4S(undefined, txn, log); + } + + async start(log: ILogItem) { + if (!this.isMasterKeyTrusted) { + // try to verify the msk _with_ access to the network + return await this.verifyMSKFrom4S(this.hsApi, undefined, log); + } + } + + private async verifyMSKFrom4S(hsApi: HomeServerApi | undefined, txn: Transaction | undefined, log: ILogItem): Promise { + return await log.wrap("CrossSigning.verifyMSKFrom4S", async log => { // TODO: use errorboundary here - const privateMasterKey = await this.getSigningKey(KeyUsage.Master); + const privateMasterKey = await this.getSigningKey(KeyUsage.Master, txn); + if (!privateMasterKey) { + return false; + } const signing = new this.olm.PkSigning(); let derivedPublicKey; try { @@ -109,11 +125,15 @@ export class CrossSigning { } finally { signing.free(); } - const publishedMasterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log); + const publishedMasterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, hsApi, txn, log); + if (!publishedMasterKey) { + return false; + } const publisedEd25519Key = publishedMasterKey && getKeyEd25519Key(publishedMasterKey); log.set({publishedMasterKey: publisedEd25519Key, derivedPublicKey}); this._isMasterKeyTrusted = !!publisedEd25519Key && publisedEd25519Key === derivedPublicKey; log.set("isMasterKeyTrusted", this.isMasterKeyTrusted); + return this.isMasterKeyTrusted; }); } @@ -162,7 +182,7 @@ export class CrossSigning { if (userId === this.ownUserId) { return; } - const keyToSign = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log); + const keyToSign = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, undefined, log); if (!keyToSign) { return; } @@ -190,11 +210,11 @@ export class CrossSigning { if (!this.isMasterKeyTrusted) { return UserTrust.OwnSetupError; } - const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log)); + const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, txn, log)); if (!ourMSK) { return UserTrust.OwnSetupError; } - const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, log)); + const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, txn, log)); if (!ourUSK) { return UserTrust.OwnSetupError; } @@ -202,7 +222,7 @@ export class CrossSigning { if (ourUSKVerification !== SignatureVerification.Valid) { return UserTrust.OwnSetupError; } - const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log)); + const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, txn, log)); if (!theirMSK) { /* assume that when they don't have an MSK, they've never enabled cross-signing on their client (or it's not supported) rather than assuming a setup error on their side. @@ -217,7 +237,7 @@ export class CrossSigning { return UserTrust.UserSignatureMismatch; } } - const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, log)); + const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, txn, log)); if (!theirSSK) { return UserTrust.UserSetupError; } @@ -270,8 +290,10 @@ export class CrossSigning { return keyToSign; } - private async getSigningKey(usage: KeyUsage): Promise { - const txn = await this.storage.readTxn([this.storage.storeNames.accountData]); + private async getSigningKey(usage: KeyUsage, existingTxn?: Transaction): Promise { + const txn = existingTxn ?? await this.storage.readTxn([ + this.storage.storeNames.accountData, + ]); const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`, txn); if (seedStr) { return new Uint8Array(this.platform.encoding.base64.decode(seedStr)); From 2e653d5f7684c1108efbd43e9190699d54453ebe Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 13 Mar 2023 21:11:40 +0530 Subject: [PATCH 050/168] Write a class that generates fixtures for test --- src/fixtures/matrix/sas/events.ts | 174 ++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 src/fixtures/matrix/sas/events.ts diff --git a/src/fixtures/matrix/sas/events.ts b/src/fixtures/matrix/sas/events.ts new file mode 100644 index 0000000000..753a6e97fb --- /dev/null +++ b/src/fixtures/matrix/sas/events.ts @@ -0,0 +1,174 @@ +/** + POSSIBLE STATES: + (following are messages received, not messages sent) + ready -> accept -> key -> mac -> done + ready -> start -> key -> mac -> done + ready -> start -> accept -> key -> mac -> done (when start resolved to use yours) + element does not send you request! + start -> key -> mac -> done + start -> accept -> key -> mac -> done + accept -> key -> mac -> done +*/ + +import {VerificationEventTypes} from "../../../matrix/verification/SAS/channel/types"; + +function generateResponses(userId: string, deviceId: string, txnId: string) { + const readyMessage = { + content: { + methods: ["m.sas.v1", "m.qr_code.show.v1", "m.reciprocate.v1"], + transaction_id: txnId, + from_device: deviceId, + }, + type: "m.key.verification.ready", + sender: userId, + }; + const startMessage = { + content: { + method: "m.sas.v1", + from_device: deviceId, + key_agreement_protocols: ["curve25519-hkdf-sha256", "curve25519"], + hashes: ["sha256"], + message_authentication_codes: [ + "hkdf-hmac-sha256.v2", + "org.matrix.msc3783.hkdf-hmac-sha256", + "hkdf-hmac-sha256", + "hmac-sha256", + ], + short_authentication_string: ["decimal", "emoji"], + transaction_id: txnId, + }, + type: "m.key.verification.start", + sender: userId, + }; + const acceptMessage = { + content: { + key_agreement_protocol: "curve25519-hkdf-sha256", + hash: "sha256", + message_authentication_code: "hkdf-hmac-sha256.v2", + short_authentication_string: ["decimal", "emoji"], + commitment: "h2YJESkiXwoGF+i5luu0YmPAKuAsWVeC2VaZOwdzggE", + transaction_id: txnId, + }, + type: "m.key.verification.accept", + sender: userId, + }; + const keyMessage = { + content: { + key: "7XA92bSIAq14R69308U80wsJR0K4KAydFG1HtVRYBFA", + transaction_id: txnId, + }, + type: "m.key.verification.key", + sender: userId, + }; + const macMessage = { + content: { + mac: { + "ed25519:FWKXUYUHTF": + "uMOgfISlZTGja2VHmdnK/xe1JNGi7irTzdaVAYSs6Q8", + "ed25519:Ot8Y58PueQ7hJVpYWAJkg2qaREJAY/UhGZYOrsd52oo": + "SavNqO8PPcAp0+eoLwlU4JWpuMm8GdGuMopPFaS8alY", + }, + keys: "cHnoX3rt9x86RUUb1nyFOa4U/dCJty+EmXCYPeNg6uU", + transaction_id: txnId, + }, + type: "m.key.verification.mac", + sender: userId, + }; + const doneMessage = { + content: { + transaction_id: txnId, + }, + type: "m.key.verification.done", + sender: userId, + }; + const result = {}; + for (const message of [readyMessage, startMessage, keyMessage, macMessage, doneMessage, acceptMessage]) { + result[message.type] = message; + } + return result; +} + +const enum COMBINATIONS { + YOU_SENT_REQUEST, + YOU_SENT_START, + THEY_SENT_START, +} + +export class SASFixtures { + private order: COMBINATIONS[] = []; + private _youWinConflict: boolean = false; + + constructor(private userId: string, private deviceId: string, private txnId: string) { } + + youSentRequest() { + this.order.push(COMBINATIONS.YOU_SENT_REQUEST); + return this; + } + + youSentStart() { + this.order.push(COMBINATIONS.YOU_SENT_START); + return this; + } + + theySentStart() { + this.order.push(COMBINATIONS.THEY_SENT_START); + return this; + } + + youWinConflict() { + this._youWinConflict = true; + return this; + } + + theyWinConflict() { + this._youWinConflict = false; + return this; + } + + fixtures(): Map { + const responses = generateResponses(this.userId, this.deviceId, this.txnId); + const array: any[] = []; + const addToArray = (type) => array.push([type, responses[type]]); + let i = 0; + while(i < this.order.length) { + const item = this.order[i]; + switch (item) { + case COMBINATIONS.YOU_SENT_REQUEST: + addToArray(VerificationEventTypes.Ready); + break; + case COMBINATIONS.THEY_SENT_START: { + addToArray(VerificationEventTypes.Start); + const nextItem = this.order[i+1]; + if (nextItem === COMBINATIONS.YOU_SENT_START) { + if (this._youWinConflict) { + addToArray(VerificationEventTypes.Accept); + i = i + 2; + continue; + } + } + break; + } + case COMBINATIONS.YOU_SENT_START: { + const nextItem = this.order[i+1] + if (nextItem === COMBINATIONS.THEY_SENT_START) { + if (this._youWinConflict) { + addToArray(VerificationEventTypes.Accept); + + } + break; + } + if (this.order[i-1] === COMBINATIONS.THEY_SENT_START) { + break; + } + addToArray(VerificationEventTypes.Accept); + break; + } + } + i = i + 1; + } + addToArray(VerificationEventTypes.Key); + addToArray(VerificationEventTypes.Mac); + addToArray(VerificationEventTypes.Done); + return new Map(array); + } +} From 720585b8f243f2108d7c0622240a5e58cbf3ff3d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 13 Mar 2023 21:17:22 +0530 Subject: [PATCH 051/168] Write unit tests --- src/matrix/verification/CrossSigning.ts | 3 +- .../verification/SAS/SASVerification.ts | 455 +++++++++++++++++- .../verification/SAS/channel/MockChannel.ts | 133 +++++ .../SAS/stages/BaseSASVerificationStage.ts | 6 +- .../stages/SelectVerificationMethodStage.ts | 4 +- .../SAS/stages/SendAcceptVerificationStage.ts | 1 - .../verification/SAS/stages/VerifyMacStage.ts | 7 +- src/matrix/verification/SAS/types.ts | 20 + 8 files changed, 608 insertions(+), 21 deletions(-) create mode 100644 src/matrix/verification/SAS/channel/MockChannel.ts create mode 100644 src/matrix/verification/SAS/types.ts diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 2e4ebd2626..fcb0e1c76b 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -21,7 +21,6 @@ import type {DeviceTracker} from "../e2ee/DeviceTracker"; import type * as OlmNamespace from "@matrix-org/olm"; import type {HomeServerApi} from "../net/HomeServerApi"; import type {Account} from "../e2ee/Account"; -import type {Room} from "../room/Room.js"; import { ILogItem } from "../../lib"; import {pkSign} from "./common"; import type {ISignatures} from "./common"; @@ -166,7 +165,7 @@ export class CrossSigning { e2eeAccount: this.e2eeAccount, deviceTracker: this.deviceTracker, hsApi: this.hsApi, - platform: this.platform, + clock: this.platform.clock, }); return this.sasVerificationInProgress; } diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index cab57283a6..ffec1f9055 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -26,7 +26,9 @@ import {SendReadyStage} from "./stages/SendReadyStage"; import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; import {VerificationCancelledError} from "./VerificationCancelledError"; import {Timeout} from "../../../platform/types/types"; -import {Platform} from "../../../platform/web/Platform.js"; +import {Clock} from "../../../platform/web/dom/Clock.js"; +import {EventEmitter} from "../../../utils/EventEmitter"; +import {SASProgressEvents} from "./types"; type Olm = typeof OlmNamespace; @@ -40,7 +42,7 @@ type Options = { e2eeAccount: Account; deviceTracker: DeviceTracker; hsApi: HomeServerApi; - platform: Platform; + clock: Clock; } export class SASVerification { @@ -49,19 +51,20 @@ export class SASVerification { public finished: boolean = false; public readonly channel: IChannel; private readonly timeout: Timeout; + public readonly eventEmitter: EventEmitter = new EventEmitter(); constructor(options: Options) { - const { olm, channel, platform } = options; + const { olm, channel, clock } = options; const olmSas = new olm.SAS(); this.olmSas = olmSas; this.channel = channel; - this.timeout = platform.clock.createTimeout(10 * 60 * 1000); + this.timeout = clock.createTimeout(10 * 60 * 1000); this.timeout.elapsed().then(() => { - // Cancel verification after 10 minutes - // todo: catch error here? - channel.cancelVerification(CancelTypes.TimedOut); - }); - const stageOptions = {...options, olmSas}; + return channel.cancelVerification(CancelTypes.TimedOut); + }).catch(() => { + // todo: why do we do nothing here? + }); + const stageOptions = {...options, olmSas, eventEmitter: this.eventEmitter}; if (channel.receivedMessages.get(VerificationEventTypes.Start)) { this.startStage = new SelectVerificationMethodStage(stageOptions); } @@ -78,7 +81,7 @@ export class SASVerification { try { let stage = this.startStage; do { - console.log("Running next stage"); + console.log("Running stage", stage.constructor.name); await stage.completeStage(); stage = stage.nextStage; } while (stage); @@ -87,12 +90,440 @@ export class SASVerification { if (!(e instanceof VerificationCancelledError)) { throw e; } - console.log("Caught error in start()"); } finally { this.olmSas.free(); - this.finished = true; this.timeout.abort(); + this.finished = true; } } } + +import {HomeServer} from "../../../mocks/HomeServer.js"; +import Olm from "@matrix-org/olm/olm.js"; +import {MockChannel} from "./channel/MockChannel"; +import {Clock as MockClock} from "../../../mocks/Clock.js"; +import {NullLogger} from "../../../logging/NullLogger"; +import {SASFixtures} from "../../../fixtures/matrix/sas/events"; +import {SendKeyStage} from "./stages/SendKeyStage"; +import {CalculateSASStage} from "./stages/CalculateSASStage"; +import {SendMacStage} from "./stages/SendMacStage"; +import {VerifyMacStage} from "./stages/VerifyMacStage"; +import {SendDoneStage} from "./stages/SendDoneStage"; +import {SendAcceptVerificationStage} from "./stages/SendAcceptVerificationStage"; + +export function tests() { + + async function createSASRequest( + ourUserId: string, + ourDeviceId: string, + theirUserId: string, + theirDeviceId: string, + txnId: string, + receivedMessages, + startingMessage?: any + ) { + const homeserverMock = new HomeServer(); + const hsApi = homeserverMock.api; + const olm = Olm; + await olm.init(); + const olmUtil = new Olm.Utility(); + const e2eeAccount = { + getDeviceKeysToSignWithCrossSigning: () => { + return { + keys: { + [`ed25519:${ourDeviceId}`]: + "srsWWbrnQFIOmUSdrt3cS/unm03qAIgXcWwQg9BegKs", + }, + }; + }, + }; + const deviceTracker = { + getCrossSigningKeysForUser: (userId, _hsApi, _) => { + let masterKey = + userId === ourUserId + ? "5HIrEawRiiQioViNfezPDWfPWH2pdaw3pbQNHEVN2jM" + : "Ot8Y58PueQ7hJVpYWAJkg2qaREJAY/UhGZYOrsd52oo"; + return { masterKey }; + }, + deviceForId: (_userId, _deviceId, _hsApi, _log) => { + return { + ed25519Key: "D8w9mrokGdEZPdPgrU0kQkYi4vZyzKEBfvGyZsGK7+Q", + }; + }, + }; + const channel = new MockChannel( + theirDeviceId, + theirUserId, + ourDeviceId, + ourUserId, + receivedMessages, + deviceTracker, + txnId, + olm, + startingMessage, + ); + const clock = new MockClock(); + const logger = new NullLogger(); + return logger.run("log", (log) => { + // @ts-ignore + const sas = new SASVerification({ + channel, + clock, + hsApi, + deviceTracker, + e2eeAccount, + olm, + olmUtil, + otherUserId: theirUserId!, + ourUser: { deviceId: ourDeviceId!, userId: ourUserId! }, + log, + }); + // @ts-ignore + channel.setOlmSas(sas.olmSas); + return { sas, clock, logger }; + }); + } + + return { + "Order of stages created matches expected order when I sent request, they sent start": async (assert) => { + const ourDeviceId = "ILQHOACESQ"; + const ourUserId = "@foobaraccount:matrix.org"; + const theirUserId = "@foobaraccount3:matrix.org"; + const theirDeviceId = "FWKXUYUHTF"; + const txnId = "t150836b91a7bed"; + const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) + .youSentRequest() + .theySentStart() + .fixtures(); + const { sas } = await createSASRequest( + ourUserId, + ourDeviceId, + theirUserId, + theirDeviceId, + txnId, + receivedMessages + ); + await sas.start(); + const expectedOrder = [ + RequestVerificationStage, + SelectVerificationMethodStage, + SendAcceptVerificationStage, + SendKeyStage, + CalculateSASStage, + SendMacStage, + VerifyMacStage, + SendDoneStage + ] + //@ts-ignore + let stage = sas.startStage; + for (const stageClass of expectedOrder) { + assert.strictEqual(stage instanceof stageClass, true); + stage = stage.nextStage; + } + assert.strictEqual(sas.finished, true); + }, + "Order of stages created matches expected order when I sent request, I sent start": async (assert) => { + const ourDeviceId = "ILQHOACESQ"; + const ourUserId = "@foobaraccount:matrix.org"; + const theirUserId = "@foobaraccount3:matrix.org"; + const theirDeviceId = "FWKXUYUHTF"; + const txnId = "t150836b91a7bed"; + const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) + .youSentRequest() + .youSentStart() + .fixtures(); + const { sas, logger } = await createSASRequest( + ourUserId, + ourDeviceId, + theirUserId, + theirDeviceId, + txnId, + receivedMessages + ); + sas.eventEmitter.on("SelectVerificationStage", (stage) => { + logger.run("send start", async (log) => { + await stage?.selectEmojiMethod(log); + }); + }); + await sas.start(); + const expectedOrder = [ + RequestVerificationStage, + SelectVerificationMethodStage, + SendKeyStage, + CalculateSASStage, + SendMacStage, + VerifyMacStage, + SendDoneStage + ] + //@ts-ignore + let stage = sas.startStage; + for (const stageClass of expectedOrder) { + assert.strictEqual(stage instanceof stageClass, true); + stage = stage.nextStage; + } + assert.strictEqual(sas.finished, true); + }, + "Order of stages created matches expected order when request is received": async (assert) => { + const ourDeviceId = "ILQHOACESQ"; + const ourUserId = "@foobaraccount:matrix.org"; + const theirUserId = "@foobaraccount3:matrix.org"; + const theirDeviceId = "FWKXUYUHTF"; + const txnId = "t150836b91a7bed"; + const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) + .theySentStart() + .fixtures(); + const startingMessage = receivedMessages.get(VerificationEventTypes.Start); + const { sas } = await createSASRequest( + ourUserId, + ourDeviceId, + theirUserId, + theirDeviceId, + txnId, + receivedMessages, + startingMessage, + ); + await sas.start(); + const expectedOrder = [ + SelectVerificationMethodStage, + SendAcceptVerificationStage, + SendKeyStage, + CalculateSASStage, + SendMacStage, + VerifyMacStage, + SendDoneStage + ] + //@ts-ignore + let stage = sas.startStage; + for (const stageClass of expectedOrder) { + assert.strictEqual(stage instanceof stageClass, true); + stage = stage.nextStage; + } + assert.strictEqual(sas.finished, true); + }, + "Order of stages created matches expected order when request is sent with start conflict (they win)": async (assert) => { + const ourDeviceId = "ILQHOACESQ"; + const ourUserId = "@foobaraccount:matrix.org"; + const theirUserId = "@foobaraccount3:matrix.org"; + const theirDeviceId = "FWKXUYUHTF"; + const txnId = "t150836b91a7bed"; + const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) + .youSentRequest() + .theySentStart() + .youSentStart() + .theyWinConflict() + .fixtures(); + const { sas } = await createSASRequest( + ourUserId, + ourDeviceId, + theirUserId, + theirDeviceId, + txnId, + receivedMessages + ); + await sas.start(); + const expectedOrder = [ + RequestVerificationStage, + SelectVerificationMethodStage, + SendAcceptVerificationStage, + SendKeyStage, + CalculateSASStage, + SendMacStage, + VerifyMacStage, + SendDoneStage + ] + //@ts-ignore + let stage = sas.startStage; + for (const stageClass of expectedOrder) { + assert.strictEqual(stage instanceof stageClass, true); + stage = stage.nextStage; + } + assert.strictEqual(sas.finished, true); + }, + "Order of stages created matches expected order when request is sent with start conflict (I win)": async (assert) => { + const ourDeviceId = "ILQHOACESQ"; + const ourUserId = "@foobaraccount3:matrix.org"; + const theirUserId = "@foobaraccount:matrix.org"; + const theirDeviceId = "FWKXUYUHTF"; + const txnId = "t150836b91a7bed"; + const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) + .youSentRequest() + .theySentStart() + .youSentStart() + .youWinConflict() + .fixtures(); + const { sas, logger } = await createSASRequest( + ourUserId, + ourDeviceId, + theirUserId, + theirDeviceId, + txnId, + receivedMessages + ); + sas.eventEmitter.on("SelectVerificationStage", (stage) => { + logger.run("send start", async (log) => { + await stage?.selectEmojiMethod(log); + }); + }); + await sas.start(); + const expectedOrder = [ + RequestVerificationStage, + SelectVerificationMethodStage, + SendKeyStage, + CalculateSASStage, + SendMacStage, + VerifyMacStage, + SendDoneStage + ] + //@ts-ignore + let stage = sas.startStage; + for (const stageClass of expectedOrder) { + assert.strictEqual(stage instanceof stageClass, true); + stage = stage.nextStage; + } + assert.strictEqual(sas.finished, true); + }, + "Order of stages created matches expected order when request is received with start conflict (they win)": async (assert) => { + const ourDeviceId = "ILQHOACESQ"; + const ourUserId = "@foobaraccount:matrix.org"; + const theirUserId = "@foobaraccount3:matrix.org"; + const theirDeviceId = "FWKXUYUHTF"; + const txnId = "t150836b91a7bed"; + const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) + .theySentStart() + .youSentStart() + .theyWinConflict() + .fixtures(); + const startingMessage = receivedMessages.get(VerificationEventTypes.Start); + console.log(receivedMessages); + const { sas } = await createSASRequest( + ourUserId, + ourDeviceId, + theirUserId, + theirDeviceId, + txnId, + receivedMessages, + startingMessage, + ); + await sas.start(); + const expectedOrder = [ + SelectVerificationMethodStage, + SendAcceptVerificationStage, + SendKeyStage, + CalculateSASStage, + SendMacStage, + VerifyMacStage, + SendDoneStage + ] + //@ts-ignore + let stage = sas.startStage; + for (const stageClass of expectedOrder) { + console.log("Checking", stageClass.constructor.name, stage.constructor.name); + assert.strictEqual(stage instanceof stageClass, true); + stage = stage.nextStage; + } + assert.strictEqual(sas.finished, true); + }, + "Order of stages created matches expected order when request is received with start conflict (I win)": async (assert) => { + const ourDeviceId = "ILQHOACESQ"; + const ourUserId = "@foobaraccount3:matrix.org"; + const theirUserId = "@foobaraccount:matrix.org"; + const theirDeviceId = "FWKXUYUHTF"; + const txnId = "t150836b91a7bed"; + const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) + .theySentStart() + .youSentStart() + .youWinConflict() + .fixtures(); + const startingMessage = receivedMessages.get(VerificationEventTypes.Start); + console.log(receivedMessages); + const { sas, logger } = await createSASRequest( + ourUserId, + ourDeviceId, + theirUserId, + theirDeviceId, + txnId, + receivedMessages, + startingMessage, + ); + sas.eventEmitter.on("SelectVerificationStage", (stage) => { + logger.run("send start", async (log) => { + await stage?.selectEmojiMethod(log); + }); + }); + await sas.start(); + const expectedOrder = [ + SelectVerificationMethodStage, + SendKeyStage, + CalculateSASStage, + SendMacStage, + VerifyMacStage, + SendDoneStage + ] + //@ts-ignore + let stage = sas.startStage; + for (const stageClass of expectedOrder) { + console.log("Checking", stageClass.constructor.name, stage.constructor.name); + assert.strictEqual(stage instanceof stageClass, true); + stage = stage.nextStage; + } + assert.strictEqual(sas.finished, true); + }, + "Verification is cancelled after 10 minutes": async (assert) => { + const ourDeviceId = "ILQHOACESQ"; + const ourUserId = "@foobaraccount:matrix.org"; + const theirUserId = "@foobaraccount3:matrix.org"; + const theirDeviceId = "FWKXUYUHTF"; + const txnId = "t150836b91a7bed"; + const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) + .youSentRequest() + .theySentStart() + .fixtures(); + console.log("receivedMessages", receivedMessages); + const { sas, clock } = await createSASRequest( + ourUserId, + ourDeviceId, + theirUserId, + theirDeviceId, + txnId, + receivedMessages + ); + const promise = sas.start(); + clock.elapse(10 * 60 * 1000); + try { + await promise; + } + catch (e) { + assert.strictEqual(e instanceof VerificationCancelledError, true); + } + assert.strictEqual(sas.finished, true); + }, + "Verification is cancelled when there's no common hash algorithm": async (assert) => { + const ourDeviceId = "ILQHOACESQ"; + const ourUserId = "@foobaraccount:matrix.org"; + const theirUserId = "@foobaraccount3:matrix.org"; + const theirDeviceId = "FWKXUYUHTF"; + const txnId = "t150836b91a7bed"; + const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) + .youSentRequest() + .theySentStart() + .fixtures(); + receivedMessages.get(VerificationEventTypes.Start).content.key_agreement_protocols = ["foo"]; + const { sas } = await createSASRequest( + ourUserId, + ourDeviceId, + theirUserId, + theirDeviceId, + txnId, + receivedMessages + ); + try { + await sas.start() + } + catch (e) { + assert.strictEqual(e instanceof VerificationCancelledError, true); + } + assert.strictEqual(sas.finished, true); + }, + } +} diff --git a/src/matrix/verification/SAS/channel/MockChannel.ts b/src/matrix/verification/SAS/channel/MockChannel.ts new file mode 100644 index 0000000000..7f4a766a09 --- /dev/null +++ b/src/matrix/verification/SAS/channel/MockChannel.ts @@ -0,0 +1,133 @@ +import type {ILogItem} from "../../../../lib"; +import {createCalculateMAC} from "../mac"; +import {VerificationCancelledError} from "../VerificationCancelledError"; +import {IChannel} from "./Channel"; +import {CancelTypes, VerificationEventTypes} from "./types"; +import anotherjson from "another-json"; + +interface ITestChannel extends IChannel { + setOlmSas(olmSas): void; +} + +export class MockChannel implements ITestChannel { + public sentMessages: Map = new Map(); + public receivedMessages: Map = new Map(); + public initiatedByUs: boolean; + public startMessage: any; + public isCancelled: boolean = false; + private olmSas: any; + + constructor( + public otherUserDeviceId: string, + public otherUserId: string, + public ourUserDeviceId: string, + public ourUserId: string, + private fixtures: Map, + private deviceTracker: any, + public id: string, + private olm: any, + startingMessage?: any, + ) { + if (startingMessage) { + const eventType = startingMessage.content.method ? VerificationEventTypes.Start : VerificationEventTypes.Request; + this.id = startingMessage.content.transaction_id; + this.receivedMessages.set(eventType, startingMessage); + } + } + + async send(eventType: string, content: any, _: ILogItem) { + if (this.isCancelled) { + throw new VerificationCancelledError(); + } + Object.assign(content, { transaction_id: this.id }); + this.sentMessages.set(eventType, {content}); + } + + async waitForEvent(eventType: string): Promise { + if (this.isCancelled) { + throw new VerificationCancelledError(); + } + const event = this.fixtures.get(eventType); + if (event) { + this.receivedMessages.set(eventType, event); + } + else { + await new Promise(() => {}); + } + if (eventType === VerificationEventTypes.Mac) { + await this.recalculateMAC(); + } + if(eventType === VerificationEventTypes.Accept && this.startMessage) { + } + return event; + } + + private recalculateCommitment() { + const acceptMessage = this.getEvent(VerificationEventTypes.Accept)?.content; + if (!acceptMessage) { + return; + } + const {content} = this.startMessage; + const {content: keyMessage} = this.fixtures.get(VerificationEventTypes.Key); + const key = keyMessage.key; + const commitmentStr = key + anotherjson.stringify(content); + const olmUtil = new this.olm.Utility(); + const commitment = olmUtil.sha256(commitmentStr); + olmUtil.free(); + acceptMessage.commitment = commitment; + } + + private async recalculateMAC() { + // We need to replace the mac with calculated mac + const baseInfo = + "MATRIX_KEY_VERIFICATION_MAC" + + this.otherUserId + + this.otherUserDeviceId + + this.ourUserId + + this.ourUserDeviceId + + this.id; + const { content: macContent } = this.receivedMessages.get(VerificationEventTypes.Mac); + const macMethod = this.getEvent(VerificationEventTypes.Accept).content.message_authentication_code; + const calculateMac = createCalculateMAC(this.olmSas, macMethod); + const input = Object.keys(macContent.mac).sort().join(","); + const properMac = calculateMac(input, baseInfo + "KEY_IDS"); + macContent.keys = properMac; + for (const keyId of Object.keys(macContent.mac)) { + const deviceId = keyId.split(":", 2)[1]; + const device = await this.deviceTracker.deviceForId(this.otherUserDeviceId, deviceId); + if (device) { + macContent.mac[keyId] = calculateMac(device.ed25519Key, baseInfo + keyId); + } + else { + const {masterKey} = await this.deviceTracker.getCrossSigningKeysForUser(this.otherUserId); + macContent.mac[keyId] = calculateMac(masterKey, baseInfo + keyId); + } + } + } + + setStartMessage(event: any): void { + this.startMessage = event; + this.recalculateCommitment(); + } + + setInitiatedByUs(value: boolean): void { + this.initiatedByUs = value; + } + + async cancelVerification(_: CancelTypes): Promise { + console.log("MockChannel.cancelVerification()"); + this.isCancelled = true; + } + + getEvent(eventType: VerificationEventTypes.Accept): any { + return this.receivedMessages.get(eventType) ?? this.sentMessages.get(eventType); + } + + setOlmSas(olmSas: any): void { + this.olmSas = olmSas; + } + + get type() { + return 0; + } +} diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index a61a2ce94d..a923376b9e 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ import type {ILogItem} from "../../../../lib.js"; -import type {Room} from "../../../room/Room.js"; import type * as OlmNamespace from "@matrix-org/olm"; import type {Account} from "../../../e2ee/Account.js"; import type {DeviceTracker} from "../../../e2ee/DeviceTracker.js"; import {Disposables} from "../../../../utils/Disposables"; import {IChannel} from "../channel/Channel.js"; import {HomeServerApi} from "../../../net/HomeServerApi.js"; +import {SASProgressEvents} from "../types.js"; +import {EventEmitter} from "../../../../utils/EventEmitter"; type Olm = typeof OlmNamespace; @@ -39,6 +40,7 @@ export type Options = { e2eeAccount: Account; deviceTracker: DeviceTracker; hsApi: HomeServerApi; + eventEmitter: EventEmitter } export abstract class BaseSASVerificationStage extends Disposables { @@ -55,6 +57,7 @@ export abstract class BaseSASVerificationStage extends Disposables { protected e2eeAccount: Account; protected deviceTracker: DeviceTracker; protected hsApi: HomeServerApi; + protected eventEmitter: EventEmitter; constructor(options: Options) { super(); @@ -68,6 +71,7 @@ export abstract class BaseSASVerificationStage extends Disposables { this.e2eeAccount = options.e2eeAccount; this.deviceTracker = options.deviceTracker; this.hsApi = options.hsApi; + this.eventEmitter = options.eventEmitter; } setNextStage(stage: BaseSASVerificationStage) { diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index af95d70ee2..a6cef8b990 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -27,7 +27,7 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SelectVerificationMethodStage.completeStage", async (log) => { - (window as any).select = () => this.selectEmojiMethod(log); + this.eventEmitter.emit("SelectVerificationStage", this); const startMessage = this.channel.waitForEvent(VerificationEventTypes.Start); const acceptMessage = this.channel.waitForEvent(VerificationEventTypes.Accept); const { content } = await Promise.race([startMessage, acceptMessage]); @@ -59,7 +59,7 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { }); } - async resolveStartConflict() { + private async resolveStartConflict() { const receivedStartMessage = this.channel.receivedMessages.get(VerificationEventTypes.Start); const sentStartMessage = this.channel.sentMessages.get(VerificationEventTypes.Start); if (receivedStartMessage.content.method !== sentStartMessage.content.method) { diff --git a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts index b9112bbc80..57ec1fbe4a 100644 --- a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts @@ -28,7 +28,6 @@ export class SendAcceptVerificationStage extends BaseSASVerificationStage { const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0]; const sasMethods = intersection(content.short_authentication_string, SAS_SET); if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) { - // todo: ensure this cancels the verification await this.channel.cancelVerification(CancelTypes.UnknownMethod); return; } diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 2eb370185f..441d1bc4db 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -54,14 +54,15 @@ export class VerifyMacStage extends BaseSASVerificationStage { this.ourUser.deviceId + this.channel.id; - if ( content.keys !== this.calculateMAC(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS")) { - // cancel when MAC does not match! + const calculatedMAC = this.calculateMAC(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS"); + if (content.keys !== calculatedMAC) { + // todo: cancel when MAC does not match! console.log("Keys MAC Verification failed"); } await this.verifyKeys(content.mac, (keyId, key, keyInfo) => { if (keyInfo !== this.calculateMAC(key, baseInfo + keyId)) { - // cancel when MAC does not match! + // todo: cancel when MAC does not match! console.log("mac obj MAC Verification failed"); } }, log); diff --git a/src/matrix/verification/SAS/types.ts b/src/matrix/verification/SAS/types.ts new file mode 100644 index 0000000000..52e7c97a37 --- /dev/null +++ b/src/matrix/verification/SAS/types.ts @@ -0,0 +1,20 @@ +/* +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 {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; + +export type SASProgressEvents = { + SelectVerificationStage: SelectVerificationMethodStage; +} From 9c82dd7ce3ac57c71656d23199bbedb768d14580 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 00:54:00 +0530 Subject: [PATCH 052/168] Refactor code --- .../verification/SAS/SASVerification.ts | 82 +++++++++++++++++++ .../verification/SAS/channel/Channel.ts | 27 +++--- .../verification/SAS/channel/MockChannel.ts | 1 + .../stages/SelectVerificationMethodStage.ts | 16 +--- 4 files changed, 97 insertions(+), 29 deletions(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index ffec1f9055..98784ea329 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -469,6 +469,88 @@ export function tests() { } assert.strictEqual(sas.finished, true); }, + "Order of stages created matches expected order when request is sent with start conflict (I win), same user": async (assert) => { + const ourDeviceId = "FWKXUYUHTF"; + const ourUserId = "@foobaraccount3:matrix.org"; + const theirUserId = "@foobaraccount3:matrix.org"; + const theirDeviceId = "ILQHOACESQ"; + const txnId = "t150836b91a7bed"; + const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) + .youSentRequest() + .theySentStart() + .youSentStart() + .youWinConflict() + .fixtures(); + const { sas, logger } = await createSASRequest( + ourUserId, + ourDeviceId, + theirUserId, + theirDeviceId, + txnId, + receivedMessages + ); + sas.eventEmitter.on("SelectVerificationStage", (stage) => { + logger.run("send start", async (log) => { + await stage?.selectEmojiMethod(log); + }); + }); + await sas.start(); + const expectedOrder = [ + RequestVerificationStage, + SelectVerificationMethodStage, + SendKeyStage, + CalculateSASStage, + SendMacStage, + VerifyMacStage, + SendDoneStage + ] + //@ts-ignore + let stage = sas.startStage; + for (const stageClass of expectedOrder) { + assert.strictEqual(stage instanceof stageClass, true); + stage = stage.nextStage; + } + assert.strictEqual(sas.finished, true); + }, + "Order of stages created matches expected order when request is sent with start conflict (they win), same user": async (assert) => { + const ourDeviceId = "ILQHOACESQ"; + const ourUserId = "@foobaraccount3:matrix.org"; + const theirUserId = "@foobaraccount3:matrix.org"; + const theirDeviceId = "FWKXUYUHTF"; + const txnId = "t150836b91a7bed"; + const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) + .youSentRequest() + .theySentStart() + .youSentStart() + .theyWinConflict() + .fixtures(); + const { sas } = await createSASRequest( + ourUserId, + ourDeviceId, + theirUserId, + theirDeviceId, + txnId, + receivedMessages + ); + await sas.start(); + const expectedOrder = [ + RequestVerificationStage, + SelectVerificationMethodStage, + SendAcceptVerificationStage, + SendKeyStage, + CalculateSASStage, + SendMacStage, + VerifyMacStage, + SendDoneStage + ] + //@ts-ignore + let stage = sas.startStage; + for (const stageClass of expectedOrder) { + assert.strictEqual(stage instanceof stageClass, true); + stage = stage.nextStage; + } + assert.strictEqual(sas.finished, true); + }, "Verification is cancelled after 10 minutes": async (assert) => { const ourDeviceId = "ILQHOACESQ"; const ourUserId = "@foobaraccount:matrix.org"; diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 239b265d32..68816a5f48 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -38,21 +38,14 @@ const messageFromErrorType = { [CancelTypes.MismatchedSAS]: "Emoji/decimal does not match.", } -const enum ChannelType { - MessageEvent, - ToDeviceMessage, -} - export interface IChannel { send(eventType: string, content: any, log: ILogItem): Promise; waitForEvent(eventType: string): Promise; - type: ChannelType; id: string; otherUserDeviceId: string; sentMessages: Map; receivedMessages: Map; setStartMessage(content: any): void; - setInitiatedByUs(value: boolean): void; initiatedByUs: boolean; startMessage: any; cancelVerification(cancellationType: CancelTypes): Promise; @@ -71,6 +64,7 @@ type Options = { export class ToDeviceChannel extends Disposables implements IChannel { private readonly hsApi: HomeServerApi; private readonly deviceTracker: DeviceTracker; + private ourDeviceId: string; private readonly otherUserId: string; private readonly platform: Platform; private readonly deviceMessageHandler: DeviceMessageHandler; @@ -110,11 +104,6 @@ export class ToDeviceChannel extends Disposables implements IChannel { this.receivedMessages.set(eventType, startingMessage); this.otherUserDeviceId = startingMessage.content.from_device; } - (window as any).foo = () => this.cancelVerification(CancelTypes.OtherUserAccepted); - } - - get type() { - return ChannelType.ToDeviceMessage; } get isCancelled(): boolean { @@ -126,10 +115,14 @@ export class ToDeviceChannel extends Disposables implements IChannel { if (this.isCancelled) { throw new VerificationCancelledError(); } + if (eventType === VerificationEventTypes.Request || eventType === VerificationEventTypes.Ready) { + this.ourDeviceId = content.from_device; + } if (eventType === VerificationEventTypes.Request) { // Handle this case specially await this.handleRequestEventSpecially(eventType, content, log); this.sentMessages.set(eventType, {content}); + this.ourDeviceId = content.from_device; return; } Object.assign(content, { transaction_id: this.id }); @@ -170,6 +163,9 @@ export class ToDeviceChannel extends Disposables implements IChannel { private async handleDeviceMessage(event) { await this.log.wrap("ToDeviceChannel.handleDeviceMessage", async (log) => { + if (!event.type.startsWith("m.key.verification.")) { + return; + } if (event.content.transaction_id !== this.id) { /** * When a device receives an unknown transaction_id, it should send an appropriate @@ -201,7 +197,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { this.otherUserDeviceId = fromDevice; // We need to send cancel messages to all other devices const devices = await this.deviceTracker.devicesForUsers([this.otherUserId], this.hsApi, log); - const otherDevices = devices.filter(device => device.deviceId !== fromDevice); + const otherDevices = devices.filter(device => device.deviceId !== fromDevice && device.deviceId !== this.ourDeviceId); const cancelMessage = { code: CancelTypes.OtherUserAccepted, reason: "An user already accepted this request!", @@ -275,10 +271,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { setStartMessage(event) { this.startMessage = event; - } - - setInitiatedByUs(value: boolean): void { - this._initiatedByUs = value; + this._initiatedByUs = event.content.from_device === this.ourDeviceId; } get initiatedByUs(): boolean { diff --git a/src/matrix/verification/SAS/channel/MockChannel.ts b/src/matrix/verification/SAS/channel/MockChannel.ts index 7f4a766a09..0816673d2e 100644 --- a/src/matrix/verification/SAS/channel/MockChannel.ts +++ b/src/matrix/verification/SAS/channel/MockChannel.ts @@ -107,6 +107,7 @@ export class MockChannel implements ITestChannel { setStartMessage(event: any): void { this.startMessage = event; + this.initiatedByUs = event.content.from_device === this.ourUserDeviceId; this.recalculateCommitment(); } diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index a6cef8b990..5bed7d690d 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -39,13 +39,11 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { } else { this.channel.setStartMessage(this.channel.receivedMessages.get(VerificationEventTypes.Start)); - this.channel.setInitiatedByUs(false); } } else { // We received the accept message this.channel.setStartMessage(this.channel.sentMessages.get(VerificationEventTypes.Start)); - this.channel.setInitiatedByUs(true); } if (this.channel.initiatedByUs) { await acceptMessage; @@ -66,17 +64,11 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { await this.channel.cancelVerification(CancelTypes.UnexpectedMessage); return; } - // todo: what happens if we are verifying devices? user-ids would be the same in that case! // In the case of conflict, the lexicographically smaller id wins - if (this.ourUser.userId < this.otherUserId) { - // use our stat message - this.channel.setStartMessage(sentStartMessage); - this.channel.setInitiatedByUs(true); - } - else { - this.channel.setStartMessage(receivedStartMessage); - this.channel.setInitiatedByUs(false); - } + const our = this.ourUser.userId === this.otherUserId ? this.ourUser.deviceId : this.ourUser.userId; + const their = this.ourUser.userId === this.otherUserId ? this.channel.otherUserDeviceId : this.otherUserId; + const startMessageToUse = our < their ? sentStartMessage : receivedStartMessage; + this.channel.setStartMessage(startMessageToUse); } async selectEmojiMethod(log: ILogItem) { From fd96d5843d52db8aba42623d41c4a9861df6e78c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 14:13:57 +0530 Subject: [PATCH 053/168] Throw error if verification was cancelled --- src/matrix/verification/SAS/channel/Channel.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 68816a5f48..be6e6fe492 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -249,6 +249,9 @@ export class ToDeviceChannel extends Disposables implements IChannel { } waitForEvent(eventType: string): Promise { + if (this._isCancelled) { + throw new VerificationCancelledError(); + } // Check if we already received the message const receivedMessage = this.receivedMessages.get(eventType); if (receivedMessage) { From 806e672806c811f3983fc1030100f8a29d2fbfdb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 14:21:07 +0530 Subject: [PATCH 054/168] Convert console.log to logger calls --- .../verification/SAS/stages/VerifyMacStage.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 441d1bc4db..36d1869a7a 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {ILogItem} from "../../../../lib"; -import {VerificationEventTypes} from "../channel/types"; +import {CancelTypes, VerificationEventTypes} from "../channel/types"; import {createCalculateMAC} from "../mac"; import type * as OlmNamespace from "@matrix-org/olm"; import {SendDoneStage} from "./SendDoneStage"; @@ -56,14 +56,17 @@ export class VerifyMacStage extends BaseSASVerificationStage { const calculatedMAC = this.calculateMAC(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS"); if (content.keys !== calculatedMAC) { - // todo: cancel when MAC does not match! - console.log("Keys MAC Verification failed"); + log.log({ l: "MAC verification failed for keys field", keys: content.keys, calculated: calculatedMAC }); + this.channel.cancelVerification(CancelTypes.KeyMismatch); + return; } await this.verifyKeys(content.mac, (keyId, key, keyInfo) => { - if (keyInfo !== this.calculateMAC(key, baseInfo + keyId)) { - // todo: cancel when MAC does not match! - console.log("mac obj MAC Verification failed"); + const calculatedMAC = this.calculateMAC(key, baseInfo + keyId); + if (keyInfo !== calculatedMAC) { + log.log({ l: "Mac verification failed for key", keyMac: keyInfo, calculated: calculatedMAC }); + this.channel.cancelVerification(CancelTypes.KeyMismatch); + return; } }, log); } From dedf64d01172fd307c30a2213d06a0bd870e238b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 14:28:33 +0530 Subject: [PATCH 055/168] Base stage class does not need disposable --- src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts | 3 +-- src/matrix/verification/SAS/stages/CalculateSASStage.ts | 1 - src/matrix/verification/SAS/stages/RequestVerificationStage.ts | 1 - .../verification/SAS/stages/SelectVerificationMethodStage.ts | 1 - .../verification/SAS/stages/SendAcceptVerificationStage.ts | 1 - src/matrix/verification/SAS/stages/SendDoneStage.ts | 1 - src/matrix/verification/SAS/stages/SendKeyStage.ts | 1 - src/matrix/verification/SAS/stages/SendMacStage.ts | 1 - src/matrix/verification/SAS/stages/SendReadyStage.ts | 1 - src/matrix/verification/SAS/stages/VerifyMacStage.ts | 1 - 10 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index a923376b9e..fc20f8b6ad 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -43,7 +43,7 @@ export type Options = { eventEmitter: EventEmitter } -export abstract class BaseSASVerificationStage extends Disposables { +export abstract class BaseSASVerificationStage { protected ourUser: UserData; protected otherUserId: string; protected log: ILogItem; @@ -60,7 +60,6 @@ export abstract class BaseSASVerificationStage extends Disposables { protected eventEmitter: EventEmitter; constructor(options: Options) { - super(); this.options = options; this.ourUser = options.ourUser; this.otherUserId = options.otherUserId; diff --git a/src/matrix/verification/SAS/stages/CalculateSASStage.ts b/src/matrix/verification/SAS/stages/CalculateSASStage.ts index 4fd3ed5738..4f98855406 100644 --- a/src/matrix/verification/SAS/stages/CalculateSASStage.ts +++ b/src/matrix/verification/SAS/stages/CalculateSASStage.ts @@ -90,7 +90,6 @@ export class CalculateSASStage extends BaseSASVerificationStage { const emoji = generateEmojiSas(Array.from(sasBytes)); console.log("Emoji calculated:", emoji); this.setNextStage(new SendMacStage(this.options)); - this.dispose(); }); } diff --git a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts index 3b273fd88c..9b7f99d40f 100644 --- a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts @@ -27,7 +27,6 @@ export class RequestVerificationStage extends BaseSASVerificationStage { await this.channel.send(VerificationEventTypes.Request, content, log); this.setNextStage(new SelectVerificationMethodStage(this.options)); await this.channel.waitForEvent("m.key.verification.ready"); - this.dispose(); }); } } diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index 5bed7d690d..97ce76efc8 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -53,7 +53,6 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { // We need to send the accept message next this.setNextStage(new SendAcceptVerificationStage(this.options)); } - this.dispose(); }); } diff --git a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts index 57ec1fbe4a..c61e100e5b 100644 --- a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts @@ -48,7 +48,6 @@ export class SendAcceptVerificationStage extends BaseSASVerificationStage { await this.channel.send(VerificationEventTypes.Accept, contentToSend, log); await this.channel.waitForEvent(VerificationEventTypes.Key); this.setNextStage(new SendKeyStage(this.options)); - this.dispose(); }); } } diff --git a/src/matrix/verification/SAS/stages/SendDoneStage.ts b/src/matrix/verification/SAS/stages/SendDoneStage.ts index d090b3c0a5..526a76142a 100644 --- a/src/matrix/verification/SAS/stages/SendDoneStage.ts +++ b/src/matrix/verification/SAS/stages/SendDoneStage.ts @@ -20,7 +20,6 @@ export class SendDoneStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("VerifyMacStage.completeStage", async (log) => { await this.channel.send(VerificationEventTypes.Done, {}, log); - this.dispose(); }); } } diff --git a/src/matrix/verification/SAS/stages/SendKeyStage.ts b/src/matrix/verification/SAS/stages/SendKeyStage.ts index af2c8e4964..56d8cc78ea 100644 --- a/src/matrix/verification/SAS/stages/SendKeyStage.ts +++ b/src/matrix/verification/SAS/stages/SendKeyStage.ts @@ -30,7 +30,6 @@ export class SendKeyStage extends BaseSASVerificationStage { */ await this.channel.waitForEvent(VerificationEventTypes.Key); this.setNextStage(new CalculateSASStage(this.options)); - this.dispose(); }); } } diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts index f3759ab6a7..ac18676bd4 100644 --- a/src/matrix/verification/SAS/stages/SendMacStage.ts +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -38,7 +38,6 @@ export class SendMacStage extends BaseSASVerificationStage { await this.sendMAC(log); await this.channel.waitForEvent(VerificationEventTypes.Mac); this.setNextStage(new VerifyMacStage(this.options)); - this.dispose(); }); } diff --git a/src/matrix/verification/SAS/stages/SendReadyStage.ts b/src/matrix/verification/SAS/stages/SendReadyStage.ts index b4591579ac..c59b4538c5 100644 --- a/src/matrix/verification/SAS/stages/SendReadyStage.ts +++ b/src/matrix/verification/SAS/stages/SendReadyStage.ts @@ -26,7 +26,6 @@ export class SendReadyStage extends BaseSASVerificationStage { }; await this.channel.send(VerificationEventTypes.Ready, content, log); this.setNextStage(new SelectVerificationMethodStage(this.options)); - this.dispose(); }); } } diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 36d1869a7a..7559a35285 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -40,7 +40,6 @@ export class VerifyMacStage extends BaseSASVerificationStage { await this.checkMAC(log); await this.channel.waitForEvent(VerificationEventTypes.Done); this.setNextStage(new SendDoneStage(this.options)); - this.dispose(); }); } From d70dd660c553f2d72b2210d1f3b837eac1b56ea0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 15:42:02 +0530 Subject: [PATCH 056/168] Refactor code 1. Remove unused properties from base stage 2. Split UserData into fields 3. Write getter for channel prop --- src/matrix/verification/CrossSigning.ts | 3 +- .../verification/SAS/SASVerification.ts | 8 +++-- .../SAS/stages/BaseSASVerificationStage.ts | 36 +++++++++---------- .../SAS/stages/CalculateSASStage.ts | 6 ++-- .../SAS/stages/RequestVerificationStage.ts | 2 +- .../stages/SelectVerificationMethodStage.ts | 6 ++-- .../SAS/stages/SendAcceptVerificationStage.ts | 5 --- .../verification/SAS/stages/SendMacStage.ts | 10 +++--- .../verification/SAS/stages/SendReadyStage.ts | 2 +- .../verification/SAS/stages/VerifyMacStage.ts | 6 ++-- 10 files changed, 41 insertions(+), 43 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index fcb0e1c76b..e787824189 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -158,7 +158,8 @@ export class CrossSigning { this.sasVerificationInProgress = new SASVerification({ olm: this.olm, olmUtil: this.olmUtil, - ourUser: { userId: this.ownUserId, deviceId: this.deviceId }, + ourUserId: this.ownUserId, + ourUserDeviceId: this.deviceId, otherUserId: userId, log, channel, diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 98784ea329..24b3b5b5de 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {RequestVerificationStage} from "./stages/RequestVerificationStage"; import type {ILogItem} from "../../../logging/types"; -import type {BaseSASVerificationStage, UserData} from "./stages/BaseSASVerificationStage"; +import type {BaseSASVerificationStage} from "./stages/BaseSASVerificationStage"; import type {Account} from "../../e2ee/Account.js"; import type {DeviceTracker} from "../../e2ee/DeviceTracker.js"; import type * as OlmNamespace from "@matrix-org/olm"; @@ -35,7 +35,8 @@ type Olm = typeof OlmNamespace; type Options = { olm: Olm; olmUtil: Olm.Utility; - ourUser: UserData; + ourUserId: string; + ourUserDeviceId: string; otherUserId: string; channel: IChannel; log: ILogItem; @@ -176,7 +177,8 @@ export function tests() { olm, olmUtil, otherUserId: theirUserId!, - ourUser: { deviceId: ourDeviceId!, userId: ourUserId! }, + ourUserId, + ourUserDeviceId: ourDeviceId, log, }); // @ts-ignore diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index fc20f8b6ad..0eb26a0256 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -13,25 +13,17 @@ 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 {ILogItem} from "../../../../lib.js"; -import type * as OlmNamespace from "@matrix-org/olm"; +import type {ILogItem} from "../../../../logging/types"; import type {Account} from "../../../e2ee/Account.js"; import type {DeviceTracker} from "../../../e2ee/DeviceTracker.js"; -import {Disposables} from "../../../../utils/Disposables"; -import {IChannel} from "../channel/Channel.js"; -import {HomeServerApi} from "../../../net/HomeServerApi.js"; -import {SASProgressEvents} from "../types.js"; +import {IChannel} from "../channel/Channel"; +import {HomeServerApi} from "../../../net/HomeServerApi"; +import {SASProgressEvents} from "../types"; import {EventEmitter} from "../../../../utils/EventEmitter"; -type Olm = typeof OlmNamespace; - -export type UserData = { - userId: string; - deviceId: string; -} - export type Options = { - ourUser: UserData; + ourUserId: string; + ourUserDeviceId: string; otherUserId: string; log: ILogItem; olmSas: Olm.SAS; @@ -44,13 +36,12 @@ export type Options = { } export abstract class BaseSASVerificationStage { - protected ourUser: UserData; + protected ourUserId: string; + protected ourUserDeviceId: string; protected otherUserId: string; protected log: ILogItem; protected olmSAS: Olm.SAS; protected olmUtil: Olm.Utility; - protected requestEventId: string; - protected previousResult: undefined | any; protected _nextStage: BaseSASVerificationStage; protected channel: IChannel; protected options: Options; @@ -61,7 +52,8 @@ export abstract class BaseSASVerificationStage { constructor(options: Options) { this.options = options; - this.ourUser = options.ourUser; + this.ourUserId = options.ourUserId; + this.ourUserDeviceId = options.ourUserDeviceId this.otherUserId = options.otherUserId; this.log = options.log; this.olmSAS = options.olmSas; @@ -81,5 +73,13 @@ export abstract class BaseSASVerificationStage { return this._nextStage; } + get otherUserDeviceId(): string { + const id = this.channel.otherUserDeviceId; + if (!id) { + throw new Error("Accessed otherUserDeviceId before it was set in channel!"); + } + return id; + } + abstract completeStage(): Promise; } diff --git a/src/matrix/verification/SAS/stages/CalculateSASStage.ts b/src/matrix/verification/SAS/stages/CalculateSASStage.ts index 4f98855406..92b62cee86 100644 --- a/src/matrix/verification/SAS/stages/CalculateSASStage.ts +++ b/src/matrix/verification/SAS/stages/CalculateSASStage.ts @@ -121,11 +121,11 @@ export class CalculateSASStage extends BaseSASVerificationStage { private generateSASBytes(): Uint8Array { const keyAgreement = this.channel.getEvent(VerificationEventTypes.Accept).content.key_agreement_protocol; - const otherUserDeviceId = this.channel.otherUserDeviceId; + const otherUserDeviceId = this.otherUserDeviceId; const sasBytes = calculateKeyAgreement[keyAgreement]({ our: { - userId: this.ourUser.userId, - deviceId: this.ourUser.deviceId, + userId: this.ourUserId, + deviceId: this.ourUserDeviceId, publicKey: this.olmSAS.get_pubkey(), }, their: { diff --git a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts index 9b7f99d40f..781afcd004 100644 --- a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/RequestVerificationStage.ts @@ -21,7 +21,7 @@ export class RequestVerificationStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("StartVerificationStage.completeStage", async (log) => { const content = { - "from_device": this.ourUser.deviceId, + "from_device": this.ourUserDeviceId, "methods": ["m.sas.v1"], }; await this.channel.send(VerificationEventTypes.Request, content, log); diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index 97ce76efc8..6eaa40f734 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -64,8 +64,8 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { return; } // In the case of conflict, the lexicographically smaller id wins - const our = this.ourUser.userId === this.otherUserId ? this.ourUser.deviceId : this.ourUser.userId; - const their = this.ourUser.userId === this.otherUserId ? this.channel.otherUserDeviceId : this.otherUserId; + const our = this.ourUserId === this.otherUserId ? this.ourUserDeviceId : this.ourUserId; + const their = this.ourUserId === this.otherUserId ? this.otherUserDeviceId : this.otherUserId; const startMessageToUse = our < their ? sentStartMessage : receivedStartMessage; this.channel.setStartMessage(startMessageToUse); } @@ -74,7 +74,7 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { if (!this.allowSelection) { return; } const content = { method: "m.sas.v1", - from_device: this.ourUser.deviceId, + from_device: this.ourUserDeviceId, key_agreement_protocols: KEY_AGREEMENT_LIST, hashes: HASHES_LIST, message_authentication_codes: MAC_LIST, diff --git a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts index c61e100e5b..23da172303 100644 --- a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts @@ -38,12 +38,7 @@ export class SendAcceptVerificationStage extends BaseSASVerificationStage { hash: hashMethod, message_authentication_code: macMethod, short_authentication_string: sasMethods, - // TODO: use selected hash function (when we support multiple) commitment: this.olmUtil.sha256(commitmentStr), - "m.relates_to": { - event_id: this.requestEventId, - rel_type: "m.reference", - } }; await this.channel.send(VerificationEventTypes.Accept, contentToSend, log); await this.channel.waitForEvent(VerificationEventTypes.Key); diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts index ac18676bd4..8cbd41cb24 100644 --- a/src/matrix/verification/SAS/stages/SendMacStage.ts +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -46,18 +46,18 @@ export class SendMacStage extends BaseSASVerificationStage { const keyList: string[] = []; const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + - this.ourUser.userId + - this.ourUser.deviceId + + this.ourUserId + + this.ourUserDeviceId + this.otherUserId + - this.channel.otherUserDeviceId + + this.otherUserDeviceId + this.channel.id; - const deviceKeyId = `ed25519:${this.ourUser.deviceId}`; + const deviceKeyId = `ed25519:${this.ourUserDeviceId}`; const deviceKeys = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning(); mac[deviceKeyId] = this.calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId); keyList.push(deviceKeyId); - const {masterKey: crossSigningKey} = await this.deviceTracker.getCrossSigningKeysForUser(this.ourUser.userId, this.hsApi, log); + const {masterKey: crossSigningKey} = await this.deviceTracker.getCrossSigningKeysForUser(this.ourUserId, this.hsApi, log); console.log("masterKey", crossSigningKey); if (crossSigningKey) { const crossSigningKeyId = `ed25519:${crossSigningKey}`; diff --git a/src/matrix/verification/SAS/stages/SendReadyStage.ts b/src/matrix/verification/SAS/stages/SendReadyStage.ts index c59b4538c5..aeaba262d2 100644 --- a/src/matrix/verification/SAS/stages/SendReadyStage.ts +++ b/src/matrix/verification/SAS/stages/SendReadyStage.ts @@ -21,7 +21,7 @@ export class SendReadyStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("StartVerificationStage.completeStage", async (log) => { const content = { - "from_device": this.ourUser.deviceId, + "from_device": this.ourUserDeviceId, "methods": ["m.sas.v1"], }; await this.channel.send(VerificationEventTypes.Ready, content, log); diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 7559a35285..4484aa5289 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -48,9 +48,9 @@ export class VerifyMacStage extends BaseSASVerificationStage { const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this.otherUserId + - this.channel.otherUserDeviceId + - this.ourUser.userId + - this.ourUser.deviceId + + this.otherUserDeviceId + + this.ourUserId + + this.ourUserDeviceId + this.channel.id; const calculatedMAC = this.calculateMAC(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS"); From 65c0afb027ef687940e965cccaf368fac12c5e39 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 15:59:13 +0530 Subject: [PATCH 057/168] Rename class --- src/matrix/verification/SAS/SASVerification.ts | 16 ++++++++-------- ...nStage.ts => SendRequestVerificationStage.ts} | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) rename src/matrix/verification/SAS/stages/{RequestVerificationStage.ts => SendRequestVerificationStage.ts} (87%) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 24b3b5b5de..dc4e5d5407 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -13,7 +13,7 @@ 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 {RequestVerificationStage} from "./stages/RequestVerificationStage"; +import {SendRequestVerificationStage} from "./stages/SendRequestVerificationStage"; import type {ILogItem} from "../../../logging/types"; import type {BaseSASVerificationStage} from "./stages/BaseSASVerificationStage"; import type {Account} from "../../e2ee/Account.js"; @@ -73,7 +73,7 @@ export class SASVerification { this.startStage = new SendReadyStage(stageOptions); } else { - this.startStage = new RequestVerificationStage(stageOptions); + this.startStage = new SendRequestVerificationStage(stageOptions); } console.log("startStage", this.startStage); } @@ -208,7 +208,7 @@ export function tests() { ); await sas.start(); const expectedOrder = [ - RequestVerificationStage, + SendRequestVerificationStage, SelectVerificationMethodStage, SendAcceptVerificationStage, SendKeyStage, @@ -250,7 +250,7 @@ export function tests() { }); await sas.start(); const expectedOrder = [ - RequestVerificationStage, + SendRequestVerificationStage, SelectVerificationMethodStage, SendKeyStage, CalculateSASStage, @@ -325,7 +325,7 @@ export function tests() { ); await sas.start(); const expectedOrder = [ - RequestVerificationStage, + SendRequestVerificationStage, SelectVerificationMethodStage, SendAcceptVerificationStage, SendKeyStage, @@ -369,7 +369,7 @@ export function tests() { }); await sas.start(); const expectedOrder = [ - RequestVerificationStage, + SendRequestVerificationStage, SelectVerificationMethodStage, SendKeyStage, CalculateSASStage, @@ -498,7 +498,7 @@ export function tests() { }); await sas.start(); const expectedOrder = [ - RequestVerificationStage, + SendRequestVerificationStage, SelectVerificationMethodStage, SendKeyStage, CalculateSASStage, @@ -536,7 +536,7 @@ export function tests() { ); await sas.start(); const expectedOrder = [ - RequestVerificationStage, + SendRequestVerificationStage, SelectVerificationMethodStage, SendAcceptVerificationStage, SendKeyStage, diff --git a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts b/src/matrix/verification/SAS/stages/SendRequestVerificationStage.ts similarity index 87% rename from src/matrix/verification/SAS/stages/RequestVerificationStage.ts rename to src/matrix/verification/SAS/stages/SendRequestVerificationStage.ts index 781afcd004..1c4c3d343d 100644 --- a/src/matrix/verification/SAS/stages/RequestVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendRequestVerificationStage.ts @@ -17,9 +17,9 @@ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {SelectVerificationMethodStage} from "./SelectVerificationMethodStage"; import {VerificationEventTypes} from "../channel/types"; -export class RequestVerificationStage extends BaseSASVerificationStage { +export class SendRequestVerificationStage extends BaseSASVerificationStage { async completeStage() { - await this.log.wrap("StartVerificationStage.completeStage", async (log) => { + await this.log.wrap("SendRequestVerificationStage.completeStage", async (log) => { const content = { "from_device": this.ourUserDeviceId, "methods": ["m.sas.v1"], From 8e08916502e11be5d5dcb6d6ba6cc00b0f38cc9f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 16:21:29 +0530 Subject: [PATCH 058/168] Remove magic string --- .../verification/SAS/stages/SendRequestVerificationStage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/SAS/stages/SendRequestVerificationStage.ts b/src/matrix/verification/SAS/stages/SendRequestVerificationStage.ts index 1c4c3d343d..f3ea42b04b 100644 --- a/src/matrix/verification/SAS/stages/SendRequestVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendRequestVerificationStage.ts @@ -26,7 +26,7 @@ export class SendRequestVerificationStage extends BaseSASVerificationStage { }; await this.channel.send(VerificationEventTypes.Request, content, log); this.setNextStage(new SelectVerificationMethodStage(this.options)); - await this.channel.waitForEvent("m.key.verification.ready"); + await this.channel.waitForEvent(VerificationEventTypes.Ready); }); } } From c08e136d2569c1957f840b3578d99fdc56e75596 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 17:02:18 +0530 Subject: [PATCH 059/168] Add more logging --- .../stages/SelectVerificationMethodStage.ts | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index 6eaa40f734..ca0b379b2c 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -14,15 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {KEY_AGREEMENT_LIST, HASHES_LIST, MAC_LIST, SAS_LIST} from "./constants"; import {CancelTypes, VerificationEventTypes} from "../channel/types"; -import type {ILogItem} from "../../../../logging/types"; +import {KEY_AGREEMENT_LIST, HASHES_LIST, MAC_LIST, SAS_LIST} from "./constants"; import {SendAcceptVerificationStage} from "./SendAcceptVerificationStage"; import {SendKeyStage} from "./SendKeyStage"; +import type {ILogItem} from "../../../../logging/types"; export class SelectVerificationMethodStage extends BaseSASVerificationStage { private hasSentStartMessage = false; - // should somehow emit something that tells the ui to hide the select option private allowSelection = true; async completeStage() { @@ -35,7 +34,7 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { // We received the start message this.allowSelection = false; if (this.hasSentStartMessage) { - await this.resolveStartConflict(); + await this.resolveStartConflict(log); } else { this.channel.setStartMessage(this.channel.receivedMessages.get(VerificationEventTypes.Start)); @@ -56,18 +55,30 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { }); } - private async resolveStartConflict() { - const receivedStartMessage = this.channel.receivedMessages.get(VerificationEventTypes.Start); - const sentStartMessage = this.channel.sentMessages.get(VerificationEventTypes.Start); - if (receivedStartMessage.content.method !== sentStartMessage.content.method) { - await this.channel.cancelVerification(CancelTypes.UnexpectedMessage); - return; - } - // In the case of conflict, the lexicographically smaller id wins - const our = this.ourUserId === this.otherUserId ? this.ourUserDeviceId : this.ourUserId; - const their = this.ourUserId === this.otherUserId ? this.otherUserDeviceId : this.otherUserId; - const startMessageToUse = our < their ? sentStartMessage : receivedStartMessage; - this.channel.setStartMessage(startMessageToUse); + private async resolveStartConflict(log: ILogItem) { + await log.wrap("resolveStartConflict", async () => { + const receivedStartMessage = this.channel.receivedMessages.get(VerificationEventTypes.Start); + const sentStartMessage = this.channel.sentMessages.get(VerificationEventTypes.Start); + if (receivedStartMessage.content.method !== sentStartMessage.content.method) { + /** + * If the two m.key.verification.start messages do not specify the same verification method, + * then the verification should be cancelled with a code of m.unexpected_message. + */ + log.log({ + l: "Methods don't match for the start messages", + received: receivedStartMessage.content.method, + sent: sentStartMessage.content.method, + }); + await this.channel.cancelVerification(CancelTypes.UnexpectedMessage); + return; + } + // In the case of conflict, the lexicographically smaller id wins + const our = this.ourUserId === this.otherUserId ? this.ourUserDeviceId : this.ourUserId; + const their = this.ourUserId === this.otherUserId ? this.otherUserDeviceId : this.otherUserId; + const startMessageToUse = our < their ? sentStartMessage : receivedStartMessage; + log.log({ l: "Start message resolved", message: startMessageToUse, our, their }) + this.channel.setStartMessage(startMessageToUse); + }); } async selectEmojiMethod(log: ILogItem) { From fc867892c68e1335e466c96d357024ee4b4ae923 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 18:05:41 +0530 Subject: [PATCH 060/168] Fix formatting --- .../verification/SAS/stages/SendAcceptVerificationStage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts index 23da172303..784961b533 100644 --- a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts @@ -18,8 +18,8 @@ import anotherjson from "another-json"; import {HASHES_LIST, MAC_LIST, SAS_SET, KEY_AGREEMENT_LIST} from "./constants"; import {CancelTypes, VerificationEventTypes} from "../channel/types"; import {SendKeyStage} from "./SendKeyStage"; -export class SendAcceptVerificationStage extends BaseSASVerificationStage { +export class SendAcceptVerificationStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SendAcceptVerificationStage.completeStage", async (log) => { const { content } = this.channel.startMessage; From ec66e88180f2e8f933bcacf510fe0f86bb4df2f4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 18:07:11 +0530 Subject: [PATCH 061/168] Fix comment --- src/matrix/verification/SAS/stages/SendKeyStage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/SAS/stages/SendKeyStage.ts b/src/matrix/verification/SAS/stages/SendKeyStage.ts index 56d8cc78ea..0e00890b5d 100644 --- a/src/matrix/verification/SAS/stages/SendKeyStage.ts +++ b/src/matrix/verification/SAS/stages/SendKeyStage.ts @@ -26,7 +26,7 @@ export class SendKeyStage extends BaseSASVerificationStage { * We may have already got the key in SendAcceptVerificationStage, * in which case waitForEvent will return a resolved promise with * that content. Otherwise, waitForEvent will actually wait for the - * key. + * key message. */ await this.channel.waitForEvent(VerificationEventTypes.Key); this.setNextStage(new CalculateSASStage(this.options)); From 2cde9b2f33b1489c1df7d8f1c4f9759e00c13379 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 22:58:05 +0530 Subject: [PATCH 062/168] Refactor SendMacStage - Convert property to argument - Remove unnecessary olm type - Use Channel.getEvent - Fix ILogItem import --- .../verification/SAS/stages/SendMacStage.ts | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts index 8cbd41cb24..51ad3131cd 100644 --- a/src/matrix/verification/SAS/stages/SendMacStage.ts +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -14,34 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {ILogItem} from "../../../../lib"; +import {ILogItem} from "../../../../logging/types"; import {VerificationEventTypes} from "../channel/types"; -import type * as OlmNamespace from "@matrix-org/olm"; import {createCalculateMAC} from "../mac"; import {VerifyMacStage} from "./VerifyMacStage"; -type Olm = typeof OlmNamespace; export class SendMacStage extends BaseSASVerificationStage { - private calculateMAC: (input: string, info: string) => string; - async completeStage() { await this.log.wrap("SendMacStage.completeStage", async (log) => { - let acceptMessage; - if (this.channel.initiatedByUs) { - acceptMessage = this.channel.receivedMessages.get(VerificationEventTypes.Accept).content; - } - else { - acceptMessage = this.channel.sentMessages.get(VerificationEventTypes.Accept).content; - } + const acceptMessage = this.channel.getEvent(VerificationEventTypes.Accept).content; const macMethod = acceptMessage.message_authentication_code; - this.calculateMAC = createCalculateMAC(this.olmSAS, macMethod); - await this.sendMAC(log); + const calculateMAC = createCalculateMAC(this.olmSAS, macMethod); + await this.sendMAC(calculateMAC, log); await this.channel.waitForEvent(VerificationEventTypes.Mac); this.setNextStage(new VerifyMacStage(this.options)); }); } - private async sendMAC(log: ILogItem): Promise { + private async sendMAC(calculateMAC: (input: string, info: string) => string, log: ILogItem): Promise { const mac: Record = {}; const keyList: string[] = []; const baseInfo = @@ -54,19 +44,17 @@ export class SendMacStage extends BaseSASVerificationStage { const deviceKeyId = `ed25519:${this.ourUserDeviceId}`; const deviceKeys = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning(); - mac[deviceKeyId] = this.calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId); + mac[deviceKeyId] = calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId); keyList.push(deviceKeyId); const {masterKey: crossSigningKey} = await this.deviceTracker.getCrossSigningKeysForUser(this.ourUserId, this.hsApi, log); - console.log("masterKey", crossSigningKey); if (crossSigningKey) { const crossSigningKeyId = `ed25519:${crossSigningKey}`; - mac[crossSigningKeyId] = this.calculateMAC(crossSigningKey, baseInfo + crossSigningKeyId); + mac[crossSigningKeyId] = calculateMAC(crossSigningKey, baseInfo + crossSigningKeyId); keyList.push(crossSigningKeyId); } - const keys = this.calculateMAC(keyList.sort().join(","), baseInfo + "KEY_IDS"); - console.log("result", mac, keys); + const keys = calculateMAC(keyList.sort().join(","), baseInfo + "KEY_IDS"); await this.channel.send(VerificationEventTypes.Mac, { mac, keys }, log); } } From f54a4d107efb708985f1f9033f86acd261ae7482 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 23:01:03 +0530 Subject: [PATCH 063/168] Refactor SendReadyStage - Change log identifier string --- src/matrix/verification/SAS/stages/SendReadyStage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/SAS/stages/SendReadyStage.ts b/src/matrix/verification/SAS/stages/SendReadyStage.ts index aeaba262d2..78a2448c5f 100644 --- a/src/matrix/verification/SAS/stages/SendReadyStage.ts +++ b/src/matrix/verification/SAS/stages/SendReadyStage.ts @@ -19,7 +19,7 @@ import {SelectVerificationMethodStage} from "./SelectVerificationMethodStage"; export class SendReadyStage extends BaseSASVerificationStage { async completeStage() { - await this.log.wrap("StartVerificationStage.completeStage", async (log) => { + await this.log.wrap("SendReadyStage.completeStage", async (log) => { const content = { "from_device": this.ourUserDeviceId, "methods": ["m.sas.v1"], From d60214da10970d7669dd021709000764384914c9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 23:02:27 +0530 Subject: [PATCH 064/168] Fix string in logger --- src/matrix/verification/SAS/stages/SendDoneStage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/SAS/stages/SendDoneStage.ts b/src/matrix/verification/SAS/stages/SendDoneStage.ts index 526a76142a..915a697ac2 100644 --- a/src/matrix/verification/SAS/stages/SendDoneStage.ts +++ b/src/matrix/verification/SAS/stages/SendDoneStage.ts @@ -18,7 +18,7 @@ import {VerificationEventTypes} from "../channel/types"; export class SendDoneStage extends BaseSASVerificationStage { async completeStage() { - await this.log.wrap("VerifyMacStage.completeStage", async (log) => { + await this.log.wrap("SendDoneStage.completeStage", async (log) => { await this.channel.send(VerificationEventTypes.Done, {}, log); }); } From d41746e8b72cea57191a5ccba977baf1ce4063a1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 23:25:00 +0530 Subject: [PATCH 065/168] Refactor SendAcceptVerificationStage --- .../SAS/stages/SendAcceptVerificationStage.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts index 784961b533..b921a6a82c 100644 --- a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts @@ -13,40 +13,41 @@ 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 {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import anotherjson from "another-json"; +import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {HASHES_LIST, MAC_LIST, SAS_SET, KEY_AGREEMENT_LIST} from "./constants"; import {CancelTypes, VerificationEventTypes} from "../channel/types"; import {SendKeyStage} from "./SendKeyStage"; +// from element-web +function intersection(anArray: T[], aSet: Set): T[] { + return Array.isArray(anArray) ? anArray.filter((x) => aSet.has(x)) : []; +} + export class SendAcceptVerificationStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SendAcceptVerificationStage.completeStage", async (log) => { - const { content } = this.channel.startMessage; - const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0]; - const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0]; - const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0]; - const sasMethods = intersection(content.short_authentication_string, SAS_SET); - if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) { + const {content: startMessage} = this.channel.startMessage; + const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(startMessage.key_agreement_protocols))[0]; + const hashMethod = intersection(HASHES_LIST, new Set(startMessage.hashes))[0]; + const macMethod = intersection(MAC_LIST, new Set(startMessage.message_authentication_codes))[0]; + const sasMethod = intersection(startMessage.short_authentication_string, SAS_SET); + if (!keyAgreement || !hashMethod || !macMethod || !sasMethod.length) { await this.channel.cancelVerification(CancelTypes.UnknownMethod); return; } const ourPubKey = this.olmSAS.get_pubkey(); - const commitmentStr = ourPubKey + anotherjson.stringify(content); - const contentToSend = { + const commitmentStr = ourPubKey + anotherjson.stringify(startMessage); + const content = { key_agreement_protocol: keyAgreement, hash: hashMethod, message_authentication_code: macMethod, - short_authentication_string: sasMethods, + short_authentication_string: sasMethod, commitment: this.olmUtil.sha256(commitmentStr), }; - await this.channel.send(VerificationEventTypes.Accept, contentToSend, log); + await this.channel.send(VerificationEventTypes.Accept, content, log); await this.channel.waitForEvent(VerificationEventTypes.Key); this.setNextStage(new SendKeyStage(this.options)); }); } } - -function intersection(anArray: T[], aSet: Set): T[] { - return Array.isArray(anArray) ? anArray.filter((x) => aSet.has(x)) : []; -} From ed70feb316993807582a00b28759941a74f54b0b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Mar 2023 23:58:49 +0530 Subject: [PATCH 066/168] Refactor CalculateSASStage - Expose emoji from stage - Await promise that resolves when emoji is matched - Modify tests --- src/domain/session/room/RoomViewModel.js | 4 ++ .../verification/SAS/SASVerification.ts | 3 + .../SAS/stages/CalculateSASStage.ts | 64 ++++++++----------- src/matrix/verification/SAS/types.ts | 2 + 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index a36f099caa..11b2a92ea6 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -53,6 +53,10 @@ export class RoomViewModel extends ErrorReportViewModel { await this.logAndCatch("startCrossSigning", async log => { const session = this.getOption("session"); const sas = session.crossSigning?.startVerification(otherUserId, log); + sas.eventEmitter.on("EmojiGenerated", (stage) => { + console.log("Emoji calculated:", stage.emoji); + stage.setEmojiMatch(true); + }); await sas.start(); }); } diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index dc4e5d5407..a07555d277 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -183,6 +183,9 @@ export function tests() { }); // @ts-ignore channel.setOlmSas(sas.olmSas); + sas.eventEmitter.on("EmojiGenerated", async (stage) => { + await stage?.setEmojiMatch(true); + }); return { sas, clock, logger }; }); } diff --git a/src/matrix/verification/SAS/stages/CalculateSASStage.ts b/src/matrix/verification/SAS/stages/CalculateSASStage.ts index 92b62cee86..1c50c869db 100644 --- a/src/matrix/verification/SAS/stages/CalculateSASStage.ts +++ b/src/matrix/verification/SAS/stages/CalculateSASStage.ts @@ -13,34 +13,20 @@ 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 anotherjson from "another-json"; import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {CancelTypes, VerificationEventTypes} from "../channel/types"; import {generateEmojiSas} from "../generator"; -import anotherjson from "another-json"; -import { ILogItem } from "../../../../lib"; -import { SendMacStage } from "./SendMacStage"; - -// From element-web -type KeyAgreement = "curve25519-hkdf-sha256" | "curve25519"; -type MacMethod = "hkdf-hmac-sha256.v2" | "org.matrix.msc3783.hkdf-hmac-sha256" | "hkdf-hmac-sha256" | "hmac-sha256"; - -const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; -const HASHES_LIST = ["sha256"]; -const MAC_LIST: MacMethod[] = [ - "hkdf-hmac-sha256.v2", - "org.matrix.msc3783.hkdf-hmac-sha256", - "hkdf-hmac-sha256", - "hmac-sha256", -]; -const SAS_LIST = ["decimal", "emoji"]; -const SAS_SET = new Set(SAS_LIST); - +import {ILogItem} from "../../../../logging/types"; +import {SendMacStage} from "./SendMacStage"; +import {VerificationCancelledError} from "../VerificationCancelledError"; type SASUserInfo = { userId: string; deviceId: string; publicKey: string; -} +}; + type SASUserInfoCollection = { our: SASUserInfo; their: SASUserInfo; @@ -51,15 +37,11 @@ type SASUserInfoCollection = { const calculateKeyAgreement = { // eslint-disable-next-line @typescript-eslint/naming-convention "curve25519-hkdf-sha256": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array { - console.log("sas.requestId", sas.id); const ourInfo = `${sas.our.userId}|${sas.our.deviceId}|` + `${sas.our.publicKey}|`; const theirInfo = `${sas.their.userId}|${sas.their.deviceId}|${sas.their.publicKey}|`; - console.log("ourInfo", ourInfo); - console.log("theirInfo", theirInfo); const sasInfo = "MATRIX_KEY_VERIFICATION_SAS|" + (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas.id; - console.log("sasInfo", sasInfo); return olmSAS.generate_bytes(sasInfo, bytes); }, "curve25519": function (sas: SASUserInfoCollection, olmSAS: Olm.SAS, bytes: number): Uint8Array { @@ -74,21 +56,26 @@ const calculateKeyAgreement = { export class CalculateSASStage extends BaseSASVerificationStage { private resolve: () => void; + private reject: (error: VerificationCancelledError) => void; + + public emoji: ReturnType; async completeStage() { await this.log.wrap("CalculateSASStage.completeStage", async (log) => { // 1. Check the hash commitment - if (this.needsToVerifyHashCommitment) { - if (!await this.verifyHashCommitment(log)) { return; } + if (this.needsToVerifyHashCommitment && !await this.verifyHashCommitment(log)) { + return; } // 2. Calculate the SAS - const emojiConfirmationPromise: Promise = new Promise(r => { - this.resolve = r; + const emojiConfirmationPromise: Promise = new Promise((res, rej) => { + this.resolve = res; + this.reject = rej; }); this.olmSAS.set_their_key(this.theirKey); const sasBytes = this.generateSASBytes(); - const emoji = generateEmojiSas(Array.from(sasBytes)); - console.log("Emoji calculated:", emoji); + this.emoji = generateEmojiSas(Array.from(sasBytes)); + this.eventEmitter.emit("EmojiGenerated", this); + await emojiConfirmationPromise; this.setNextStage(new SendMacStage(this.options)); }); } @@ -101,8 +88,7 @@ export class CalculateSASStage extends BaseSASVerificationStage { const receivedCommitment = acceptMessage.commitment; const hash = this.olmUtil.sha256(commitmentStr); if (hash !== receivedCommitment) { - log.set("Commitment mismatched!", {}); - // cancel the process! + log.log({l: "Commitment mismatched!", received: receivedCommitment, calculated: hash}); await this.channel.cancelVerification(CancelTypes.MismatchedCommitment); return false; } @@ -112,7 +98,7 @@ export class CalculateSASStage extends BaseSASVerificationStage { private get needsToVerifyHashCommitment(): boolean { if (this.channel.initiatedByUs) { - // If we sent the start message, we also received the accept message + // If we sent the start message, we also received the accept message. // The commitment is in the accept message, so we need to verify it. return true; } @@ -139,16 +125,18 @@ export class CalculateSASStage extends BaseSASVerificationStage { return sasBytes; } - async emojiMatch(match: boolean) { - if (!match) { - // cancel the verification + async setEmojiMatch(match: boolean) { + if (match) { + this.resolve(); + } + else { await this.channel.cancelVerification(CancelTypes.MismatchedSAS); + this.reject(new VerificationCancelledError()); } - } get theirKey(): string { - const { content } = this.channel.receivedMessages.get(VerificationEventTypes.Key); + const {content} = this.channel.receivedMessages.get(VerificationEventTypes.Key); return content.key; } } diff --git a/src/matrix/verification/SAS/types.ts b/src/matrix/verification/SAS/types.ts index 52e7c97a37..d7be6921db 100644 --- a/src/matrix/verification/SAS/types.ts +++ b/src/matrix/verification/SAS/types.ts @@ -13,8 +13,10 @@ 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 {CalculateSASStage} from "./stages/CalculateSASStage"; import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; export type SASProgressEvents = { SelectVerificationStage: SelectVerificationMethodStage; + EmojiGenerated: CalculateSASStage; } From a5743e868e9676478bc29b54f59caf830dbd46c9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Mar 2023 00:12:33 +0530 Subject: [PATCH 067/168] Refactor VerifyMacStage --- .../verification/SAS/stages/VerifyMacStage.ts | 41 ++++++------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 4484aa5289..91a9f2637d 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -14,36 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {ILogItem} from "../../../../lib"; +import {ILogItem} from "../../../../logging/types"; import {CancelTypes, VerificationEventTypes} from "../channel/types"; import {createCalculateMAC} from "../mac"; -import type * as OlmNamespace from "@matrix-org/olm"; import {SendDoneStage} from "./SendDoneStage"; -type Olm = typeof OlmNamespace; export type KeyVerifier = (keyId: string, device: any, keyInfo: string) => void; export class VerifyMacStage extends BaseSASVerificationStage { - private calculateMAC: (input: string, info: string) => string; - async completeStage() { await this.log.wrap("VerifyMacStage.completeStage", async (log) => { - let acceptMessage; - if (this.channel.initiatedByUs) { - acceptMessage = this.channel.receivedMessages.get(VerificationEventTypes.Accept).content; - } - else { - acceptMessage = this.channel.sentMessages.get(VerificationEventTypes.Accept).content; - } + const acceptMessage = this.channel.getEvent(VerificationEventTypes.Accept).content; const macMethod = acceptMessage.message_authentication_code; - this.calculateMAC = createCalculateMAC(this.olmSAS, macMethod); - await this.checkMAC(log); + const calculateMAC = createCalculateMAC(this.olmSAS, macMethod); + await this.checkMAC(calculateMAC, log); await this.channel.waitForEvent(VerificationEventTypes.Done); this.setNextStage(new SendDoneStage(this.options)); }); } - private async checkMAC(log: ILogItem): Promise { + private async checkMAC(calculateMAC: (input: string, info: string) => string, log: ILogItem): Promise { const {content} = this.channel.receivedMessages.get(VerificationEventTypes.Mac); const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + @@ -53,7 +43,7 @@ export class VerifyMacStage extends BaseSASVerificationStage { this.ourUserDeviceId + this.channel.id; - const calculatedMAC = this.calculateMAC(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS"); + const calculatedMAC = calculateMAC(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS"); if (content.keys !== calculatedMAC) { log.log({ l: "MAC verification failed for keys field", keys: content.keys, calculated: calculatedMAC }); this.channel.cancelVerification(CancelTypes.KeyMismatch); @@ -61,9 +51,9 @@ export class VerifyMacStage extends BaseSASVerificationStage { } await this.verifyKeys(content.mac, (keyId, key, keyInfo) => { - const calculatedMAC = this.calculateMAC(key, baseInfo + keyId); + const calculatedMAC = calculateMAC(key, baseInfo + keyId); if (keyInfo !== calculatedMAC) { - log.log({ l: "Mac verification failed for key", keyMac: keyInfo, calculated: calculatedMAC }); + log.log({ l: "Mac verification failed for key", keyMac: keyInfo, calculatedMAC, keyId, key }); this.channel.cancelVerification(CancelTypes.KeyMismatch); return; } @@ -73,21 +63,16 @@ export class VerifyMacStage extends BaseSASVerificationStage { protected async verifyKeys(keys: Record, verifier: KeyVerifier, log: ILogItem): Promise { const userId = this.otherUserId; for (const [keyId, keyInfo] of Object.entries(keys)) { - const deviceId = keyId.split(":", 2)[1]; - const device = await this.deviceTracker.deviceForId(userId, deviceId, this.hsApi, log); + const deviceIdOrMSK = keyId.split(":", 2)[1]; + const device = await this.deviceTracker.deviceForId(userId, deviceIdOrMSK, this.hsApi, log); if (device) { verifier(keyId, device.ed25519Key, keyInfo); // todo: mark device as verified here } else { - // If we were not able to find the device, then deviceId is actually the master signing key! - const msk = deviceId; + // If we were not able to find the device, then deviceIdOrMSK is actually the MSK! const {masterKey} = await this.deviceTracker.getCrossSigningKeysForUser(userId, this.hsApi, log); - if (masterKey === msk) { - verifier(keyId, masterKey, keyInfo); - // todo: mark user as verified her - } else { - // logger.warn(`verification: Could not find device ${deviceId} to verify`); - } + verifier(keyId, masterKey, keyInfo); + // todo: mark user as verified here } } } From 672b0ac13d88fbce3d5719c76909f39fd8c3e5de Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Mar 2023 00:31:23 +0530 Subject: [PATCH 068/168] Refactor SASVerification class --- .../verification/SAS/SASVerification.ts | 23 +++++++++++-------- src/matrix/verification/SAS/mac.ts | 7 +++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index a07555d277..95aa33c68f 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -51,7 +51,7 @@ export class SASVerification { private olmSas: Olm.SAS; public finished: boolean = false; public readonly channel: IChannel; - private readonly timeout: Timeout; + private timeout: Timeout; public readonly eventEmitter: EventEmitter = new EventEmitter(); constructor(options: Options) { @@ -59,12 +59,7 @@ export class SASVerification { const olmSas = new olm.SAS(); this.olmSas = olmSas; this.channel = channel; - this.timeout = clock.createTimeout(10 * 60 * 1000); - this.timeout.elapsed().then(() => { - return channel.cancelVerification(CancelTypes.TimedOut); - }).catch(() => { - // todo: why do we do nothing here? - }); + this.setupCancelAfterTimeout(clock); const stageOptions = {...options, olmSas, eventEmitter: this.eventEmitter}; if (channel.receivedMessages.get(VerificationEventTypes.Start)) { this.startStage = new SelectVerificationMethodStage(stageOptions); @@ -75,14 +70,24 @@ export class SASVerification { else { this.startStage = new SendRequestVerificationStage(stageOptions); } - console.log("startStage", this.startStage); + } + + private async setupCancelAfterTimeout(clock: Clock) { + try { + const tenMinutes = 10 * 60 * 1000; + this.timeout = clock.createTimeout(tenMinutes); + await this.timeout.elapsed(); + await this.channel.cancelVerification(CancelTypes.TimedOut); + } + catch { + // Ignore errors + } } async start() { try { let stage = this.startStage; do { - console.log("Running stage", stage.constructor.name); await stage.completeStage(); stage = stage.nextStage; } while (stage); diff --git a/src/matrix/verification/SAS/mac.ts b/src/matrix/verification/SAS/mac.ts index 9a6edddafe..e52e8c2c10 100644 --- a/src/matrix/verification/SAS/mac.ts +++ b/src/matrix/verification/SAS/mac.ts @@ -13,15 +13,14 @@ 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 {MacMethod} from "./stages/constants"; -const macMethods = { +const macMethods: Record = { "hkdf-hmac-sha256": "calculate_mac", "org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64", "hkdf-hmac-sha256.v2": "calculate_mac_fixed_base64", "hmac-sha256": "calculate_mac_long_kdf", -} as const; - -export type MacMethod = keyof typeof macMethods; +}; export function createCalculateMAC(olmSAS: Olm.SAS, method: MacMethod) { return function (input: string, info: string): string { From 190465918e616c0c5cbbd6eb82246be468085071 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Mar 2023 14:07:07 +0530 Subject: [PATCH 069/168] Remove comment --- src/matrix/verification/CrossSigning.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index e787824189..49dc693a83 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -21,7 +21,7 @@ import type {DeviceTracker} from "../e2ee/DeviceTracker"; import type * as OlmNamespace from "@matrix-org/olm"; import type {HomeServerApi} from "../net/HomeServerApi"; import type {Account} from "../e2ee/Account"; -import { ILogItem } from "../../lib"; +import {ILogItem} from "../../lib"; import {pkSign} from "./common"; import type {ISignatures} from "./common"; import {SASVerification} from "./SAS/SASVerification"; @@ -80,7 +80,6 @@ export class CrossSigning { )) { return; } - console.log("unencrypted event", unencryptedEvent); if (unencryptedEvent.type === VerificationEventTypes.Request || unencryptedEvent.type === VerificationEventTypes.Start) { await this.platform.logger.run("Start verification from request", async (log) => { From fc6e56b0ad86e55e8034c95b309eaaf167544eed Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Mar 2023 14:36:14 +0530 Subject: [PATCH 070/168] Pass log last --- src/domain/session/room/RoomViewModel.js | 2 +- src/matrix/verification/CrossSigning.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 11b2a92ea6..0d74535055 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -52,7 +52,7 @@ export class RoomViewModel extends ErrorReportViewModel { async _startCrossSigning(otherUserId) { await this.logAndCatch("startCrossSigning", async log => { const session = this.getOption("session"); - const sas = session.crossSigning?.startVerification(otherUserId, log); + const sas = session.crossSigning?.startVerification(otherUserId, undefined, log); sas.eventEmitter.on("EmojiGenerated", (stage) => { console.log("Emoji calculated:", stage.emoji); stage.setEmojiMatch(true); diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 49dc693a83..5e85bb5652 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -83,7 +83,7 @@ export class CrossSigning { if (unencryptedEvent.type === VerificationEventTypes.Request || unencryptedEvent.type === VerificationEventTypes.Start) { await this.platform.logger.run("Start verification from request", async (log) => { - const sas = this.startVerification(unencryptedEvent.sender, log, unencryptedEvent); + const sas = this.startVerification(unencryptedEvent.sender, unencryptedEvent, log); await sas?.start(); }); } @@ -141,7 +141,7 @@ export class CrossSigning { return this._isMasterKeyTrusted; } - startVerification(userId: string, log: ILogItem, event?: any): SASVerification | undefined { + startVerification(userId: string, startingMessage: any, log: ILogItem): SASVerification | undefined { if (this.sasVerificationInProgress && !this.sasVerificationInProgress.finished) { return; } @@ -152,7 +152,7 @@ export class CrossSigning { platform: this.platform, deviceMessageHandler: this.deviceMessageHandler, log - }, event); + }, startingMessage); this.sasVerificationInProgress = new SASVerification({ olm: this.olm, From cd9b3406cd6cdb6d6b8eb99543f215e0676d2bb2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Mar 2023 16:30:14 +0530 Subject: [PATCH 071/168] Refactor Channel --- src/matrix/verification/CrossSigning.ts | 2 +- .../verification/SAS/SASVerification.ts | 6 +- .../verification/SAS/channel/Channel.ts | 119 ++++++++++-------- .../verification/SAS/channel/MockChannel.ts | 27 ++-- src/matrix/verification/SAS/channel/types.ts | 2 +- .../SAS/stages/CalculateSASStage.ts | 8 +- .../stages/SelectVerificationMethodStage.ts | 8 +- .../verification/SAS/stages/SendMacStage.ts | 2 +- .../verification/SAS/stages/VerifyMacStage.ts | 4 +- 9 files changed, 97 insertions(+), 81 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 5e85bb5652..b5af8b8dfc 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -149,7 +149,7 @@ export class CrossSigning { deviceTracker: this.deviceTracker, hsApi: this.hsApi, otherUserId: userId, - platform: this.platform, + clock: this.platform.clock, deviceMessageHandler: this.deviceMessageHandler, log }, startingMessage); diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 95aa33c68f..f249118a05 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -59,12 +59,13 @@ export class SASVerification { const olmSas = new olm.SAS(); this.olmSas = olmSas; this.channel = channel; + this.channel.setOurDeviceId(options.ourUserDeviceId); this.setupCancelAfterTimeout(clock); const stageOptions = {...options, olmSas, eventEmitter: this.eventEmitter}; - if (channel.receivedMessages.get(VerificationEventTypes.Start)) { + if (channel.getReceivedMessage(VerificationEventTypes.Start)) { this.startStage = new SelectVerificationMethodStage(stageOptions); } - else if (channel.receivedMessages.get(VerificationEventTypes.Request)) { + else if (channel.getReceivedMessage(VerificationEventTypes.Request)) { this.startStage = new SendReadyStage(stageOptions); } else { @@ -161,7 +162,6 @@ export function tests() { const channel = new MockChannel( theirDeviceId, theirUserId, - ourDeviceId, ourUserId, receivedMessages, deviceTracker, diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index be6e6fe492..c874586d06 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -17,18 +17,18 @@ limitations under the License. import type {HomeServerApi} from "../../../net/HomeServerApi"; import type {DeviceTracker} from "../../../e2ee/DeviceTracker.js"; import type {ILogItem} from "../../../../logging/types"; -import type {Platform} from "../../../../platform/web/Platform.js"; +import type {Clock} from "../../../../platform/web/dom/Clock.js"; import type {DeviceMessageHandler} from "../../../DeviceMessageHandler.js"; import {makeTxnId} from "../../../common.js"; import {CancelTypes, VerificationEventTypes} from "./types"; -import {Disposables} from "../../../../lib"; +import {Disposables} from "../../../../utils/Disposables"; import {VerificationCancelledError} from "../VerificationCancelledError"; const messageFromErrorType = { [CancelTypes.UserCancelled]: "User declined", [CancelTypes.InvalidMessage]: "Invalid Message.", [CancelTypes.KeyMismatch]: "Key Mismatch.", - [CancelTypes.OtherUserAccepted]: "Another device has accepted this request.", + [CancelTypes.OtherDeviceAccepted]: "Another device has accepted this request.", [CancelTypes.TimedOut]: "Timed Out", [CancelTypes.UnexpectedMessage]: "Unexpected Message.", [CancelTypes.UnknownMethod]: "Unknown method.", @@ -41,22 +41,23 @@ const messageFromErrorType = { export interface IChannel { send(eventType: string, content: any, log: ILogItem): Promise; waitForEvent(eventType: string): Promise; - id: string; - otherUserDeviceId: string; - sentMessages: Map; - receivedMessages: Map; + getSentMessage(event: VerificationEventTypes): any; + getReceivedMessage(event: VerificationEventTypes): any; setStartMessage(content: any): void; - initiatedByUs: boolean; - startMessage: any; + setOurDeviceId(id: string): void; cancelVerification(cancellationType: CancelTypes): Promise; - getEvent(eventType: VerificationEventTypes.Accept): any; + acceptMessage: any; + startMessage: any; + initiatedByUs: boolean; + id: string; + otherUserDeviceId: string; } type Options = { hsApi: HomeServerApi; deviceTracker: DeviceTracker; otherUserId: string; - platform: Platform; + clock: Clock; deviceMessageHandler: DeviceMessageHandler; log: ILogItem; } @@ -66,10 +67,10 @@ export class ToDeviceChannel extends Disposables implements IChannel { private readonly deviceTracker: DeviceTracker; private ourDeviceId: string; private readonly otherUserId: string; - private readonly platform: Platform; + private readonly clock: Clock; private readonly deviceMessageHandler: DeviceMessageHandler; - public readonly sentMessages: Map = new Map(); - public readonly receivedMessages: Map = new Map(); + private readonly sentMessages: Map = new Map(); + private readonly receivedMessages: Map = new Map(); private readonly waitMap: Map}> = new Map(); private readonly log: ILogItem; public otherUserDeviceId: string; @@ -87,21 +88,28 @@ export class ToDeviceChannel extends Disposables implements IChannel { this.hsApi = options.hsApi; this.deviceTracker = options.deviceTracker; this.otherUserId = options.otherUserId; - this.platform = options.platform; + this.clock = options.clock; this.log = options.log; this.deviceMessageHandler = options.deviceMessageHandler; - this.track(this.deviceMessageHandler.disposableOn("message", async ({ unencrypted }) => await this.handleDeviceMessage(unencrypted))); + this.track( + this.deviceMessageHandler.disposableOn( + "message", + async ({ unencrypted }) => + await this.handleDeviceMessage(unencrypted) + ) + ); this.track(() => { - this.waitMap.forEach((value) => { value.reject(new VerificationCancelledError()); }); + this.waitMap.forEach((value) => { + value.reject(new VerificationCancelledError()); + }); }); // Copy over request message if (startingMessage) { /** * startingMessage may be the ready message or the start message. */ - const eventType = startingMessage.content.method ? VerificationEventTypes.Start : VerificationEventTypes.Request; this.id = startingMessage.content.transaction_id; - this.receivedMessages.set(eventType, startingMessage); + this.receivedMessages.set(startingMessage.type, startingMessage); this.otherUserDeviceId = startingMessage.content.from_device; } } @@ -110,26 +118,20 @@ export class ToDeviceChannel extends Disposables implements IChannel { return this._isCancelled; } - async send(eventType: string, content: any, log: ILogItem): Promise { + async send(eventType: VerificationEventTypes, content: any, log: ILogItem): Promise { await log.wrap("ToDeviceChannel.send", async () => { if (this.isCancelled) { throw new VerificationCancelledError(); } - if (eventType === VerificationEventTypes.Request || eventType === VerificationEventTypes.Ready) { - this.ourDeviceId = content.from_device; - } if (eventType === VerificationEventTypes.Request) { // Handle this case specially await this.handleRequestEventSpecially(eventType, content, log); - this.sentMessages.set(eventType, {content}); - this.ourDeviceId = content.from_device; return; } Object.assign(content, { transaction_id: this.id }); const payload = { messages: { [this.otherUserId]: { - // check if the following is undefined? [this.otherUserDeviceId]: content } } @@ -139,9 +141,9 @@ export class ToDeviceChannel extends Disposables implements IChannel { }); } - private async handleRequestEventSpecially(eventType: string, content: any, log: ILogItem) { + private async handleRequestEventSpecially(eventType: VerificationEventTypes, content: any, log: ILogItem) { await log.wrap("ToDeviceChannel.handleRequestEventSpecially", async () => { - const timestamp = this.platform.clock.now(); + const timestamp = this.clock.now(); const txnId = makeTxnId(); this.id = txnId; Object.assign(content, { timestamp, transaction_id: txnId }); @@ -153,11 +155,21 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } await this.hsApi.sendToDevice(eventType, payload, makeTxnId(), { log }).response(); + this.sentMessages.set(eventType, {content}); }); } - getEvent(eventType: VerificationEventTypes.Accept) { - return this.receivedMessages.get(eventType) ?? this.sentMessages.get(eventType); + getReceivedMessage(event: VerificationEventTypes) { + return this.receivedMessages.get(event); + } + + getSentMessage(event: VerificationEventTypes) { + return this.sentMessages.get(event); + } + + get acceptMessage(): any { + return this.receivedMessages.get(VerificationEventTypes.Accept) ?? + this.sentMessages.get(VerificationEventTypes.Accept); } @@ -177,7 +189,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { return; } console.log("event", event); - log.set("event", event); + log.log({ l: "event", event }); this.resolveAnyWaits(event); this.receivedMessages.set(event.type, event); if (event.type === VerificationEventTypes.Ready) { @@ -185,6 +197,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { return; } if (event.type === VerificationEventTypes.Cancel) { + this._isCancelled = true; this.dispose(); return; } @@ -192,30 +205,24 @@ export class ToDeviceChannel extends Disposables implements IChannel { } private async handleReadyMessage(event, log: ILogItem) { - try { - const fromDevice = event.content.from_device; - this.otherUserDeviceId = fromDevice; - // We need to send cancel messages to all other devices - const devices = await this.deviceTracker.devicesForUsers([this.otherUserId], this.hsApi, log); - const otherDevices = devices.filter(device => device.deviceId !== fromDevice && device.deviceId !== this.ourDeviceId); - const cancelMessage = { - code: CancelTypes.OtherUserAccepted, - reason: "An user already accepted this request!", - transaction_id: this.id, - }; - const deviceMessages = otherDevices.reduce((acc, device) => { acc[device.deviceId] = cancelMessage; return acc; }, {}); - const payload = { - messages: { - [this.otherUserId]: deviceMessages - } + const fromDevice = event.content.from_device; + this.otherUserDeviceId = fromDevice; + // We need to send cancel messages to all other devices + const devices = await this.deviceTracker.devicesForUsers([this.otherUserId], this.hsApi, log); + const otherDevices = devices.filter(device => device.deviceId !== fromDevice && device.deviceId !== this.ourDeviceId); + const cancelMessage = { + code: CancelTypes.OtherDeviceAccepted, + reason: messageFromErrorType[CancelTypes.OtherDeviceAccepted], + transaction_id: this.id, + }; + const deviceMessages = otherDevices.reduce((acc, device) => { acc[device.deviceId] = cancelMessage; return acc; }, {}); + const payload = { + messages: { + [this.otherUserId]: deviceMessages } - await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, makeTxnId(), { log }).response(); - } - catch (e) { - console.log(e); - // Do something here } - } + await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, makeTxnId(), { log }).response(); + } async cancelVerification(cancellationType: CancelTypes) { await this.log.wrap("Channel.cancelVerification", async log => { @@ -248,7 +255,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } - waitForEvent(eventType: string): Promise { + waitForEvent(eventType: VerificationEventTypes): Promise { if (this._isCancelled) { throw new VerificationCancelledError(); } @@ -272,6 +279,10 @@ export class ToDeviceChannel extends Disposables implements IChannel { return promise; } + setOurDeviceId(id: string) { + this.ourDeviceId = id; + } + setStartMessage(event) { this.startMessage = event; this._initiatedByUs = event.content.from_device === this.ourDeviceId; diff --git a/src/matrix/verification/SAS/channel/MockChannel.ts b/src/matrix/verification/SAS/channel/MockChannel.ts index 0816673d2e..b2de0f3f7e 100644 --- a/src/matrix/verification/SAS/channel/MockChannel.ts +++ b/src/matrix/verification/SAS/channel/MockChannel.ts @@ -16,11 +16,11 @@ export class MockChannel implements ITestChannel { public startMessage: any; public isCancelled: boolean = false; private olmSas: any; + public ourUserDeviceId: string; constructor( public otherUserDeviceId: string, public otherUserId: string, - public ourUserDeviceId: string, public ourUserId: string, private fixtures: Map, private deviceTracker: any, @@ -63,7 +63,7 @@ export class MockChannel implements ITestChannel { } private recalculateCommitment() { - const acceptMessage = this.getEvent(VerificationEventTypes.Accept)?.content; + const acceptMessage = this.acceptMessage?.content; if (!acceptMessage) { return; } @@ -87,7 +87,7 @@ export class MockChannel implements ITestChannel { this.ourUserDeviceId + this.id; const { content: macContent } = this.receivedMessages.get(VerificationEventTypes.Mac); - const macMethod = this.getEvent(VerificationEventTypes.Accept).content.message_authentication_code; + const macMethod = this.acceptMessage.content.message_authentication_code; const calculateMac = createCalculateMAC(this.olmSas, macMethod); const input = Object.keys(macContent.mac).sort().join(","); const properMac = calculateMac(input, baseInfo + "KEY_IDS"); @@ -111,8 +111,8 @@ export class MockChannel implements ITestChannel { this.recalculateCommitment(); } - setInitiatedByUs(value: boolean): void { - this.initiatedByUs = value; + setOurDeviceId(id: string) { + this.ourUserDeviceId = id; } async cancelVerification(_: CancelTypes): Promise { @@ -120,15 +120,20 @@ export class MockChannel implements ITestChannel { this.isCancelled = true; } - getEvent(eventType: VerificationEventTypes.Accept): any { - return this.receivedMessages.get(eventType) ?? this.sentMessages.get(eventType); + get acceptMessage(): any { + return this.receivedMessages.get(VerificationEventTypes.Accept) ?? + this.sentMessages.get(VerificationEventTypes.Accept); } - setOlmSas(olmSas: any): void { - this.olmSas = olmSas; + getReceivedMessage(event: VerificationEventTypes) { + return this.receivedMessages.get(event); + } + + getSentMessage(event: VerificationEventTypes) { + return this.sentMessages.get(event); } - get type() { - return 0; + setOlmSas(olmSas: any): void { + this.olmSas = olmSas; } } diff --git a/src/matrix/verification/SAS/channel/types.ts b/src/matrix/verification/SAS/channel/types.ts index 9a02f421d7..de6a999bfa 100644 --- a/src/matrix/verification/SAS/channel/types.ts +++ b/src/matrix/verification/SAS/channel/types.ts @@ -18,7 +18,7 @@ export const enum CancelTypes { KeyMismatch = "m.key_mismatch", UserMismatch = "m.user_mismatch", InvalidMessage = "m.invalid_message", - OtherUserAccepted = "m.accepted", + OtherDeviceAccepted = "m.accepted", // SAS specific MismatchedCommitment = "m.mismatched_commitment", MismatchedSAS = "m.mismatched_sas", diff --git a/src/matrix/verification/SAS/stages/CalculateSASStage.ts b/src/matrix/verification/SAS/stages/CalculateSASStage.ts index 1c50c869db..daa744d4f1 100644 --- a/src/matrix/verification/SAS/stages/CalculateSASStage.ts +++ b/src/matrix/verification/SAS/stages/CalculateSASStage.ts @@ -82,8 +82,8 @@ export class CalculateSASStage extends BaseSASVerificationStage { async verifyHashCommitment(log: ILogItem) { return await log.wrap("CalculateSASStage.verifyHashCommitment", async () => { - const acceptMessage = this.channel.receivedMessages.get(VerificationEventTypes.Accept).content; - const keyMessage = this.channel.receivedMessages.get(VerificationEventTypes.Key).content; + const acceptMessage = this.channel.getReceivedMessage(VerificationEventTypes.Accept).content; + const keyMessage = this.channel.getReceivedMessage(VerificationEventTypes.Key).content; const commitmentStr = keyMessage.key + anotherjson.stringify(this.channel.startMessage.content); const receivedCommitment = acceptMessage.commitment; const hash = this.olmUtil.sha256(commitmentStr); @@ -106,7 +106,7 @@ export class CalculateSASStage extends BaseSASVerificationStage { } private generateSASBytes(): Uint8Array { - const keyAgreement = this.channel.getEvent(VerificationEventTypes.Accept).content.key_agreement_protocol; + const keyAgreement = this.channel.acceptMessage.content.key_agreement_protocol; const otherUserDeviceId = this.otherUserDeviceId; const sasBytes = calculateKeyAgreement[keyAgreement]({ our: { @@ -136,7 +136,7 @@ export class CalculateSASStage extends BaseSASVerificationStage { } get theirKey(): string { - const {content} = this.channel.receivedMessages.get(VerificationEventTypes.Key); + const {content} = this.channel.getReceivedMessage(VerificationEventTypes.Key); return content.key; } } diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index ca0b379b2c..9108ca71a7 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -37,12 +37,12 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { await this.resolveStartConflict(log); } else { - this.channel.setStartMessage(this.channel.receivedMessages.get(VerificationEventTypes.Start)); + this.channel.setStartMessage(this.channel.getReceivedMessage(VerificationEventTypes.Start)); } } else { // We received the accept message - this.channel.setStartMessage(this.channel.sentMessages.get(VerificationEventTypes.Start)); + this.channel.setStartMessage(this.channel.getSentMessage(VerificationEventTypes.Start)); } if (this.channel.initiatedByUs) { await acceptMessage; @@ -57,8 +57,8 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { private async resolveStartConflict(log: ILogItem) { await log.wrap("resolveStartConflict", async () => { - const receivedStartMessage = this.channel.receivedMessages.get(VerificationEventTypes.Start); - const sentStartMessage = this.channel.sentMessages.get(VerificationEventTypes.Start); + const receivedStartMessage = this.channel.getReceivedMessage(VerificationEventTypes.Start); + const sentStartMessage = this.channel.getSentMessage(VerificationEventTypes.Start); if (receivedStartMessage.content.method !== sentStartMessage.content.method) { /** * If the two m.key.verification.start messages do not specify the same verification method, diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts index 51ad3131cd..e80df19faa 100644 --- a/src/matrix/verification/SAS/stages/SendMacStage.ts +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -22,7 +22,7 @@ import {VerifyMacStage} from "./VerifyMacStage"; export class SendMacStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SendMacStage.completeStage", async (log) => { - const acceptMessage = this.channel.getEvent(VerificationEventTypes.Accept).content; + const acceptMessage = this.channel.acceptMessage.content; const macMethod = acceptMessage.message_authentication_code; const calculateMAC = createCalculateMAC(this.olmSAS, macMethod); await this.sendMAC(calculateMAC, log); diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 91a9f2637d..55cb80e38a 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -24,7 +24,7 @@ export type KeyVerifier = (keyId: string, device: any, keyInfo: string) => void; export class VerifyMacStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("VerifyMacStage.completeStage", async (log) => { - const acceptMessage = this.channel.getEvent(VerificationEventTypes.Accept).content; + const acceptMessage = this.channel.acceptMessage.content; const macMethod = acceptMessage.message_authentication_code; const calculateMAC = createCalculateMAC(this.olmSAS, macMethod); await this.checkMAC(calculateMAC, log); @@ -34,7 +34,7 @@ export class VerifyMacStage extends BaseSASVerificationStage { } private async checkMAC(calculateMAC: (input: string, info: string) => string, log: ILogItem): Promise { - const {content} = this.channel.receivedMessages.get(VerificationEventTypes.Mac); + const {content} = this.channel.getReceivedMessage(VerificationEventTypes.Mac); const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this.otherUserId + From 610bbcc1ae3ea89184055629db569d728849f8f1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Mar 2023 21:19:00 +0530 Subject: [PATCH 072/168] Remove code from room vm --- src/domain/session/room/RoomViewModel.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 0d74535055..31608a62e7 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -49,18 +49,6 @@ export class RoomViewModel extends ErrorReportViewModel { this._setupCallViewModel(); } - async _startCrossSigning(otherUserId) { - await this.logAndCatch("startCrossSigning", async log => { - const session = this.getOption("session"); - const sas = session.crossSigning?.startVerification(otherUserId, undefined, log); - sas.eventEmitter.on("EmojiGenerated", (stage) => { - console.log("Emoji calculated:", stage.emoji); - stage.setEmojiMatch(true); - }); - await sas.start(); - }); - } - _setupCallViewModel() { if (!this.features.calls) { return; From ed9fc14f238cfcab565b86c966d10ed7865c2063 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Mar 2023 21:46:46 +0530 Subject: [PATCH 073/168] Fix import --- src/domain/session/toast/ToastCollectionViewModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/toast/ToastCollectionViewModel.ts b/src/domain/session/toast/ToastCollectionViewModel.ts index df4da88fc9..95871cb2c5 100644 --- a/src/domain/session/toast/ToastCollectionViewModel.ts +++ b/src/domain/session/toast/ToastCollectionViewModel.ts @@ -21,7 +21,7 @@ import type {GroupCall} from "../../../matrix/calls/group/GroupCall"; import type {Room} from "../../../matrix/room/Room.js"; import type {Session} from "../../../matrix/Session.js"; import type {SegmentType} from "../../navigation"; -import { RoomStatus } from "../../../lib"; +import {RoomStatus} from "../../../matrix/room/common"; type Options = { session: Session; From 9d8c045c105910ac595c4ed0018809b9896635a8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Mar 2023 21:47:25 +0530 Subject: [PATCH 074/168] Move import up --- src/domain/session/toast/ToastCollectionViewModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/toast/ToastCollectionViewModel.ts b/src/domain/session/toast/ToastCollectionViewModel.ts index 95871cb2c5..59931a5c5c 100644 --- a/src/domain/session/toast/ToastCollectionViewModel.ts +++ b/src/domain/session/toast/ToastCollectionViewModel.ts @@ -17,11 +17,11 @@ limitations under the License. import {CallToastNotificationViewModel} from "./CallToastNotificationViewModel"; import {ObservableArray} from "../../../observable"; import {ViewModel, Options as BaseOptions} from "../../ViewModel"; +import {RoomStatus} from "../../../matrix/room/common"; import type {GroupCall} from "../../../matrix/calls/group/GroupCall"; import type {Room} from "../../../matrix/room/Room.js"; import type {Session} from "../../../matrix/Session.js"; import type {SegmentType} from "../../navigation"; -import {RoomStatus} from "../../../matrix/room/common"; type Options = { session: Session; From dd59f37dce0eba80f807ea973d79c0a66c64c6db Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 21 Mar 2023 18:24:46 +0100 Subject: [PATCH 075/168] WIP2 --- src/domain/AccountSetupViewModel.js | 2 +- .../session/settings/KeyBackupViewModel.js | 223 --------------- .../session/settings/KeyBackupViewModel.ts | 270 ++++++++++++++++++ .../session/settings/SettingsViewModel.js | 2 +- src/matrix/Session.js | 34 +-- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 130 ++++++--- src/matrix/ssss/SecretStorage.ts | 12 +- src/platform/web/ui/login/AccountSetupView.js | 2 +- ...ttingsView.js => KeyBackupSettingsView.ts} | 53 ++-- .../web/ui/session/settings/SettingsView.js | 2 +- src/utils/AbortableOperation.ts | 14 +- src/utils/Deferred.ts | 41 +++ 12 files changed, 473 insertions(+), 312 deletions(-) delete mode 100644 src/domain/session/settings/KeyBackupViewModel.js create mode 100644 src/domain/session/settings/KeyBackupViewModel.ts rename src/platform/web/ui/session/settings/{KeyBackupSettingsView.js => KeyBackupSettingsView.ts} (73%) create mode 100644 src/utils/Deferred.ts diff --git a/src/domain/AccountSetupViewModel.js b/src/domain/AccountSetupViewModel.js index e7c1301f4f..3e6435823f 100644 --- a/src/domain/AccountSetupViewModel.js +++ b/src/domain/AccountSetupViewModel.js @@ -16,7 +16,7 @@ limitations under the License. import {ViewModel} from "./ViewModel"; import {KeyType} from "../matrix/ssss/index"; -import {Status} from "./session/settings/KeyBackupViewModel.js"; +import {Status} from "./session/settings/KeyBackupViewModel"; export class AccountSetupViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js deleted file mode 100644 index 22135e414e..0000000000 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ /dev/null @@ -1,223 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {ViewModel} from "../../ViewModel"; -import {KeyType} from "../../../matrix/ssss/index"; -import {createEnum} from "../../../utils/enum"; -import {FlatMapObservableValue} from "../../../observable/value"; - -export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable"); -export const BackupWriteStatus = createEnum("Writing", "Stopped", "Done", "Pending"); - -export class KeyBackupViewModel extends ViewModel { - constructor(options) { - super(options); - this._session = options.session; - this._error = null; - this._isBusy = false; - this._dehydratedDeviceId = undefined; - this._status = undefined; - this._backupOperation = new FlatMapObservableValue(this._session.keyBackup, keyBackup => keyBackup.operationInProgress); - this._progress = new FlatMapObservableValue(this._backupOperation, op => op.progress); - this.track(this._backupOperation.subscribe(() => { - // see if needsNewKey might be set - this._reevaluateStatus(); - this.emitChange("isBackingUp"); - })); - this.track(this._progress.subscribe(() => this.emitChange("backupPercentage"))); - this._reevaluateStatus(); - this.track(this._session.keyBackup.subscribe(() => { - if (this._reevaluateStatus()) { - this.emitChange("status"); - } - })); - } - - _reevaluateStatus() { - if (this._isBusy) { - return false; - } - let status; - const keyBackup = this._session.keyBackup.get(); - if (keyBackup) { - status = keyBackup.needsNewKey ? Status.NewVersionAvailable : Status.Enabled; - } else if (keyBackup === null) { - status = this.showPhraseSetup() ? Status.SetupPhrase : Status.SetupKey; - } else { - status = Status.Pending; - } - const changed = status !== this._status; - this._status = status; - return changed; - } - - get decryptAction() { - return this.i18n`Set up`; - } - - get purpose() { - return this.i18n`set up key backup`; - } - - offerDehydratedDeviceSetup() { - return true; - } - - get dehydratedDeviceId() { - return this._dehydratedDeviceId; - } - - get isBusy() { - return this._isBusy; - } - - get backupVersion() { - return this._session.keyBackup.get()?.version; - } - - get isMasterKeyTrusted() { - return this._session.crossSigning?.isMasterKeyTrusted ?? false; - } - - get canSignOwnDevice() { - return !!this._session.crossSigning; - } - - async signOwnDevice() { - if (this._session.crossSigning) { - await this.logger.run("KeyBackupViewModel.signOwnDevice", async log => { - await this._session.crossSigning.signOwnDevice(log); - }); - } - } - - get backupWriteStatus() { - const keyBackup = this._session.keyBackup.get(); - if (!keyBackup) { - return BackupWriteStatus.Pending; - } else if (keyBackup.hasStopped) { - return BackupWriteStatus.Stopped; - } - const operation = keyBackup.operationInProgress.get(); - if (operation) { - return BackupWriteStatus.Writing; - } else if (keyBackup.hasBackedUpAllKeys) { - return BackupWriteStatus.Done; - } else { - return BackupWriteStatus.Pending; - } - } - - get backupError() { - return this._session.keyBackup.get()?.error?.message; - } - - get status() { - return this._status; - } - - get error() { - return this._error?.message; - } - - showPhraseSetup() { - if (this._status === Status.SetupKey) { - this._status = Status.SetupPhrase; - this.emitChange("status"); - } - } - - showKeySetup() { - if (this._status === Status.SetupPhrase) { - this._status = Status.SetupKey; - this.emitChange("status"); - } - } - - async _enterCredentials(keyType, credential, setupDehydratedDevice) { - if (credential) { - try { - this._isBusy = true; - this.emitChange("isBusy"); - const key = await this._session.enableSecretStorage(keyType, credential); - if (setupDehydratedDevice) { - this._dehydratedDeviceId = await this._session.setupDehydratedDevice(key); - } - } catch (err) { - console.error(err); - this._error = err; - this.emitChange("error"); - } finally { - this._isBusy = false; - this._reevaluateStatus(); - this.emitChange(""); - } - } - } - - enterSecurityPhrase(passphrase, setupDehydratedDevice) { - this._enterCredentials(KeyType.Passphrase, passphrase, setupDehydratedDevice); - } - - enterSecurityKey(securityKey, setupDehydratedDevice) { - this._enterCredentials(KeyType.RecoveryKey, securityKey, setupDehydratedDevice); - } - - async disable() { - try { - this._isBusy = true; - this.emitChange("isBusy"); - await this._session.disableSecretStorage(); - } catch (err) { - console.error(err); - this._error = err; - this.emitChange("error"); - } finally { - this._isBusy = false; - this._reevaluateStatus(); - this.emitChange(""); - } - } - - get isBackingUp() { - return !!this._backupOperation.get(); - } - - get backupPercentage() { - const progress = this._progress.get(); - if (progress) { - return Math.round((progress.finished / progress.total) * 100); - } - return 0; - } - - get backupInProgressLabel() { - const progress = this._progress.get(); - if (progress) { - return this.i18n`${progress.finished} of ${progress.total}`; - } - return this.i18n`โ€ฆ`; - } - - cancelBackup() { - this._backupOperation.get()?.abort(); - } - - startBackup() { - this._session.keyBackup.get()?.flush(); - } -} - diff --git a/src/domain/session/settings/KeyBackupViewModel.ts b/src/domain/session/settings/KeyBackupViewModel.ts new file mode 100644 index 0000000000..cdfd40817a --- /dev/null +++ b/src/domain/session/settings/KeyBackupViewModel.ts @@ -0,0 +1,270 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "../../ViewModel"; +import {SegmentType} from "../../navigation/index"; +import {KeyType} from "../../../matrix/ssss/index"; + +import type {Options as BaseOptions} from "../../ViewModel"; +import type {Session} from "../../../matrix/Session"; +import type {Disposable} from "../../../utils/Disposables"; +import type {KeyBackup, Progress} from "../../../matrix/e2ee/megolm/keybackup/KeyBackup"; +import type {CrossSigning} from "../../../matrix/verification/CrossSigning"; + +export enum Status { + Enabled, + Setup, + Pending, + NewVersionAvailable +}; + +export enum BackupWriteStatus { + Writing, + Stopped, + Done, + Pending +}; + +type Options = { + session: Session, +} & BaseOptions; + +export class KeyBackupViewModel extends ViewModel { + private _error?: Error = undefined; + private _isBusy = false; + private _dehydratedDeviceId?: string = undefined; + private _status = Status.Pending; + private _backupOperationSubscription?: Disposable = undefined; + private _keyBackupSubscription?: Disposable = undefined; + private _progress?: Progress = undefined; + private _setupKeyType = KeyType.RecoveryKey; + + constructor(options) { + super(options); + const onKeyBackupSet = (keyBackup: KeyBackup | undefined) => { + if (keyBackup && !this._keyBackupSubscription) { + this._keyBackupSubscription = this.track(this._session.keyBackup.disposableOn("change", () => { + this._onKeyBackupChange(); + })); + } else if (!keyBackup && this._keyBackupSubscription) { + this._keyBackupSubscription = this.disposeTracked(this._keyBackupSubscription); + } + this._onKeyBackupChange(); // update status + }; + this.track(this._session.keyBackup.subscribe(onKeyBackupSet)); + onKeyBackupSet(this._keyBackup); + } + + private get _session(): Session { + return this.getOption("session"); + } + + private get _keyBackup(): KeyBackup | undefined { + return this._session.keyBackup.get(); + } + + private get _crossSigning(): CrossSigning | undefined { + return this._session.crossSigning.get(); + } + + private _onKeyBackupChange() { + const keyBackup = this._keyBackup; + if (keyBackup) { + const {operationInProgress} = keyBackup; + if (operationInProgress && !this._backupOperationSubscription) { + this._backupOperationSubscription = this.track(operationInProgress.disposableOn("change", () => { + this._progress = operationInProgress.progress; + this.emitChange("backupPercentage"); + })); + } else if (this._backupOperationSubscription && !operationInProgress) { + this._backupOperationSubscription = this.disposeTracked(this._backupOperationSubscription); + this._progress = undefined; + } + } + this.emitChange("status"); + } + + get status(): Status { + const keyBackup = this._keyBackup; + if (keyBackup) { + if (keyBackup.needsNewKey) { + return Status.NewVersionAvailable; + } else if (keyBackup.version === undefined) { + return Status.Pending; + } else { + return keyBackup.needsNewKey ? Status.NewVersionAvailable : Status.Enabled; + } + } else { + return Status.Setup; + } + } + + get decryptAction(): string { + return this.i18n`Set up`; + } + + get purpose(): string { + return this.i18n`set up key backup`; + } + + offerDehydratedDeviceSetup(): boolean { + return true; + } + + get dehydratedDeviceId(): string | undefined { + return this._dehydratedDeviceId; + } + + get isBusy(): boolean { + return this._isBusy; + } + + get backupVersion(): string { + return this._keyBackup?.version ?? ""; + } + + get isMasterKeyTrusted(): boolean { + return this._crossSigning?.isMasterKeyTrusted ?? false; + } + + get canSignOwnDevice(): boolean { + return !!this._crossSigning; + } + + async signOwnDevice(): Promise { + const crossSigning = this._crossSigning; + if (crossSigning) { + await this.logger.run("KeyBackupViewModel.signOwnDevice", async log => { + await crossSigning.signOwnDevice(log); + }); + } + } + + get backupWriteStatus(): BackupWriteStatus { + const keyBackup = this._keyBackup; + if (!keyBackup || keyBackup.version === undefined) { + return BackupWriteStatus.Pending; + } else if (keyBackup.hasStopped) { + return BackupWriteStatus.Stopped; + } + const operation = keyBackup.operationInProgress; + if (operation) { + return BackupWriteStatus.Writing; + } else if (keyBackup.hasBackedUpAllKeys) { + return BackupWriteStatus.Done; + } else { + return BackupWriteStatus.Pending; + } + } + + get backupError(): string | undefined { + return this._keyBackup?.error?.message; + } + + get error(): string | undefined { + return this._error?.message; + } + + showPhraseSetup(): void { + if (this._status === Status.Setup) { + this._setupKeyType = KeyType.Passphrase; + this.emitChange("setupKeyType"); + } + } + + showKeySetup(): void { + if (this._status === Status.Setup) { + this._setupKeyType = KeyType.Passphrase; + this.emitChange("setupKeyType"); + } + } + + get setupKeyType(): KeyType { + return this._setupKeyType; + } + + private async _enterCredentials(keyType, credential, setupDehydratedDevice): Promise { + if (credential) { + try { + this._isBusy = true; + this.emitChange("isBusy"); + const key = await this._session.enableSecretStorage(keyType, credential); + if (setupDehydratedDevice) { + this._dehydratedDeviceId = await this._session.setupDehydratedDevice(key); + } + } catch (err) { + console.error(err); + this._error = err; + this.emitChange("error"); + } finally { + this._isBusy = false; + this.emitChange(); + } + } + } + + enterSecurityPhrase(passphrase, setupDehydratedDevice): Promise { + return this._enterCredentials(KeyType.Passphrase, passphrase, setupDehydratedDevice); + } + + enterSecurityKey(securityKey, setupDehydratedDevice): Promise { + return this._enterCredentials(KeyType.RecoveryKey, securityKey, setupDehydratedDevice); + } + + async disable(): Promise { + try { + this._isBusy = true; + this.emitChange("isBusy"); + await this._session.disableSecretStorage(); + } catch (err) { + console.error(err); + this._error = err; + this.emitChange("error"); + } finally { + this._isBusy = false; + this.emitChange(); + } + } + + get isBackingUp(): boolean { + return this._keyBackup?.operationInProgress !== undefined; + } + + get backupPercentage(): number { + if (this._progress) { + return Math.round((this._progress.finished / this._progress.total) * 100); + } + return 0; + } + + get backupInProgressLabel(): string { + if (this._progress) { + return this.i18n`${this._progress.finished} of ${this._progress.total}`; + } + return this.i18n`โ€ฆ`; + } + + cancelBackup(): void { + this._keyBackup?.operationInProgress?.abort(); + } + + startBackup(): void { + this.logger.run("KeyBackupViewModel.startBackup", log => { + this._keyBackup?.flush(log); + }); + } +} + diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index f8420a5346..7f4cab5931 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -15,7 +15,7 @@ limitations under the License. */ import {ViewModel} from "../../ViewModel"; -import {KeyBackupViewModel} from "./KeyBackupViewModel.js"; +import {KeyBackupViewModel} from "./KeyBackupViewModel"; import {FeaturesViewModel} from "./FeaturesViewModel"; import {submitLogsFromSessionToDefaultServer} from "../../../domain/rageshake"; diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 0b5b857741..b10b282439 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -90,7 +90,7 @@ export class Session { this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; this._keyBackup = new ObservableValue(undefined); - this._crossSigning = undefined; + this._crossSigning = new ObservableValue(undefined); this._observedRoomStatus = new Map(); if (olm) { @@ -250,7 +250,7 @@ export class Session { } if (this._keyBackup.get()) { this._keyBackup.get().dispose(); - this._keyBackup.set(null); + this._keyBackup.set(undefined); } // TODO: stop cross-signing const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); @@ -258,8 +258,8 @@ export class Session { // only after having read a secret, write the key // as we only find out if it was good if the MAC verification succeeds await this._writeSSSSKey(key, log); - await this._keyBackup?.start(log); - await this._crossSigning?.start(log); + await this._keyBackup.get()?.start(log); + await this._crossSigning.get()?.start(log); return key; } else { throw new Error("Could not read key backup with the given key"); @@ -331,29 +331,21 @@ export class Session { if (isValid) { await this._loadSecretStorageServices(secretStorage, txn, log); } - if (!this._keyBackup.get()) { - // null means key backup isn't configured yet - // as opposed to undefined, which means we're still checking - this._keyBackup.set(null); - } return isValid; }); } - _loadSecretStorageServices(secretStorage, txn, log) { + async _loadSecretStorageServices(secretStorage, txn, log) { try { await log.wrap("enable key backup", async log => { - // TODO: delay network request here until start() - const keyBackup = await KeyBackup.fromSecretStorage( - this._platform, - this._olm, - secretStorage, + const keyBackup = new KeyBackup( this._hsApi, + this._olm, this._keyLoader, this._storage, - txn + this._platform, ); - if (keyBackup) { + if (await keyBackup.load(secretStorage, txn)) { for (const room of this._rooms.values()) { if (room.isEncrypted) { room.enableKeyBackup(keyBackup); @@ -378,8 +370,8 @@ export class Session { ownUserId: this.userId, e2eeAccount: this._e2eeAccount }); - if (crossSigning.load(txn, log)) { - this._crossSigning = crossSigning; + if (await crossSigning.load(txn, log)) { + this._crossSigning.set(crossSigning); } }); } @@ -585,8 +577,8 @@ export class Session { } }); } - this._keyBackup?.start(log); - this._crossSigning?.start(log); + this._keyBackup.get()?.start(log); + this._crossSigning.get()?.start(log); // restore unfinished operations, like sending out room keys const opsTxn = await this._storage.readWriteTxn([ diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 0ef610ffe3..8e9a4a81f5 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -20,6 +20,8 @@ import {MEGOLM_ALGORITHM} from "../../common"; import * as Curve25519 from "./Curve25519"; import {AbortableOperation} from "../../../../utils/AbortableOperation"; import {ObservableValue} from "../../../../observable/value"; +import {Deferred} from "../../../../utils/Deferred"; +import {EventEmitter} from "../../../../utils/EventEmitter"; import {SetAbortableFn} from "../../../../utils/AbortableOperation"; import type {BackupInfo, SessionData, SessionKeyInfo, SessionInfo, KeyBackupPayload} from "./types"; @@ -31,43 +33,69 @@ import type {Storage} from "../../../storage/idb/Storage"; import type {ILogItem} from "../../../../logging/types"; import type {Platform} from "../../../../platform/web/Platform"; import type {Transaction} from "../../../storage/idb/Transaction"; +import type {IHomeServerRequest} from "../../../net/HomeServerRequest"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; const KEYS_PER_REQUEST = 200; -export class KeyBackup { - public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); +// a set of fields we need to store once we've fetched +// the backup info from the homeserver, which happens in start() +class BackupConfig { + constructor( + public readonly info: BackupInfo, + public readonly crypto: Curve25519.BackupEncryption + ) {} +} + +export class KeyBackup extends EventEmitter<{change: never}> { + private _operationInProgress?: AbortableOperation, Progress>; private _stopped = false; private _needsNewKey = false; private _hasBackedUpAllKeys = false; private _error?: Error; private crypto?: Curve25519.BackupEncryption; + private backupInfo?: BackupInfo; + private privateKey?: Uint8Array; + private backupConfigDeferred: Deferred = new Deferred(); + private backupInfoRequest?: IHomeServerRequest; constructor( - private readonly backupInfo: BackupInfo, private readonly hsApi: HomeServerApi, + private readonly olm: Olm, private readonly keyLoader: KeyLoader, private readonly storage: Storage, private readonly platform: Platform, private readonly maxDelay: number = 10000 - ) {} + ) { + super(); + // doing the network request for getting the backup info + // and hence creating the crypto instance depending on the chose algorithm + // is delayed until start() is called, but we want to already take requests + // for fetching the room keys, so put the crypto and backupInfo in a deferred. + this.backupConfigDeferred = new Deferred(); + } get hasStopped(): boolean { return this._stopped; } get error(): Error | undefined { return this._error; } - get version(): string { return this.backupInfo.version; } + get version(): string | undefined { return this.backupConfigDeferred.value?.info?.version; } get needsNewKey(): boolean { return this._needsNewKey; } get hasBackedUpAllKeys(): boolean { return this._hasBackedUpAllKeys; } + get operationInProgress(): AbortableOperation, Progress> | undefined { return this._operationInProgress; } async getRoomKey(roomId: string, sessionId: string, log: ILogItem): Promise { - if (this.needsNewKey || !this.crypto) { + if (this.needsNewKey) { + return; + } + const backupConfig = await this.backupConfigDeferred.promise; + if (!backupConfig) { return; } - const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response(); + const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(backupConfig.info.version, roomId, sessionId, {log}).response(); if (!sessionResponse.session_data) { return; } - const sessionKeyInfo = this.crypto.decryptRoomKey(sessionResponse.session_data as SessionData); + const sessionKeyInfo = backupConfig.crypto.decryptRoomKey(sessionResponse.session_data as SessionData); if (sessionKeyInfo?.algorithm === MEGOLM_ALGORITHM) { return keyFromBackup(roomId, sessionId, sessionKeyInfo); } else if (sessionKeyInfo?.algorithm) { @@ -79,14 +107,53 @@ export class KeyBackup { return txn.inboundGroupSessions.markAllAsNotBackedUp(); } - start(log: ILogItem) { + async load(secretStorage: SecretStorage, txn: Transaction) { + // TODO: no exception here we should anticipate? + const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); + if (base64PrivateKey) { + this.privateKey = new Uint8Array(this.platform.encoding.base64.decode(base64PrivateKey)); + return true; + } else { + this.backupConfigDeferred.resolve(undefined); + return false; + } + } - // fetch latest version - this.flush(log); + async start(log: ILogItem) { + await log.wrap("KeyBackup.start", async log => { + if (this.privateKey && !this.backupInfoRequest) { + let backupInfo: BackupInfo; + try { + this.backupInfoRequest = this.hsApi.roomKeysVersion(undefined, {log}); + backupInfo = await this.backupInfoRequest.response() as BackupInfo; + } catch (err) { + if (err.name === "AbortError") { + log.set("aborted", true); + return; + } else { + throw err; + } + } finally { + this.backupInfoRequest = undefined; + } + // TODO: what if backupInfo is undefined or we get 404 or something? + if (backupInfo.algorithm === Curve25519.Algorithm) { + const crypto = Curve25519.BackupEncryption.fromAuthData(backupInfo.auth_data, this.privateKey, this.olm); + this.backupConfigDeferred.resolve(new BackupConfig(backupInfo, crypto)); + this.emit("change"); + } else { + this.backupConfigDeferred.resolve(undefined); + log.log({l: `Unknown backup algorithm`, algorithm: backupInfo.algorithm}); + } + this.privateKey = undefined; + } + // fetch latest version + this.flush(log); + }); } flush(log: ILogItem): void { - if (!this.operationInProgress.get()) { + if (!this._operationInProgress) { log.wrapDetached("flush key backup", async log => { if (this._needsNewKey) { log.set("needsNewKey", this._needsNewKey); @@ -96,7 +163,8 @@ export class KeyBackup { this._error = undefined; this._hasBackedUpAllKeys = false; const operation = this._runFlushOperation(log); - this.operationInProgress.set(operation); + this._operationInProgress = operation; + this.emit("change"); try { await operation.result; this._hasBackedUpAllKeys = true; @@ -113,13 +181,18 @@ export class KeyBackup { } log.catch(err); } - this.operationInProgress.set(undefined); + this._operationInProgress = undefined; + this.emit("change"); }); } } private _runFlushOperation(log: ILogItem): AbortableOperation, Progress> { return new AbortableOperation(async (setAbortable, setProgress) => { + const backupConfig = await this.backupConfigDeferred.promise; + if (!backupConfig) { + return; + } let total = 0; let amountFinished = 0; while (true) { @@ -138,8 +211,8 @@ export class KeyBackup { log.set("total", total); return; } - const payload = await this.encodeKeysForBackup(keysNeedingBackup); - const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log}); + const payload = await this.encodeKeysForBackup(keysNeedingBackup, backupConfig.crypto); + const uploadRequest = this.hsApi.uploadRoomKeysToBackup(backupConfig.info.version, payload, {log}); setAbortable(uploadRequest); await uploadRequest.response(); await this.markKeysAsBackedUp(keysNeedingBackup, setAbortable); @@ -149,7 +222,7 @@ export class KeyBackup { }); } - private async encodeKeysForBackup(roomKeys: RoomKey[]): Promise { + private async encodeKeysForBackup(roomKeys: RoomKey[], crypto: Curve25519.BackupEncryption): Promise { const payload: KeyBackupPayload = { rooms: {} }; const payloadRooms = payload.rooms; for (const key of roomKeys) { @@ -157,7 +230,7 @@ export class KeyBackup { if (!roomPayload) { roomPayload = payloadRooms[key.roomId] = { sessions: {} }; } - roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key); + roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key, crypto); } return payload; } @@ -178,7 +251,7 @@ export class KeyBackup { await txn.complete(); } - private async encodeRoomKey(roomKey: RoomKey): Promise { + private async encodeRoomKey(roomKey: RoomKey, crypto: Curve25519.BackupEncryption): Promise { return await this.keyLoader.useKey(roomKey, session => { const firstMessageIndex = session.first_known_index(); const sessionKey = session.export_session(firstMessageIndex); @@ -186,27 +259,14 @@ export class KeyBackup { first_message_index: firstMessageIndex, forwarded_count: 0, is_verified: false, - session_data: this.crypto.encryptRoomKey(roomKey, sessionKey) + session_data: crypto.encryptRoomKey(roomKey, sessionKey) }; }); } dispose() { - this.crypto?.dispose(); - } - - static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction): Promise { - const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); - if (base64PrivateKey) { - const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey)); - const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo; - if (backupInfo.algorithm === Curve25519.Algorithm) { - const crypto = Curve25519.BackupEncryption.fromAuthData(backupInfo.auth_data, privateKey, olm); - return new KeyBackup(backupInfo, privateKey, hsApi, keyLoader, storage, platform); - } else { - throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`); - } - } + this.backupInfoRequest?.abort(); + this.backupConfigDeferred.value?.crypto?.dispose(); } } diff --git a/src/matrix/ssss/SecretStorage.ts b/src/matrix/ssss/SecretStorage.ts index ebdcd13a4c..093382a8e9 100644 --- a/src/matrix/ssss/SecretStorage.ts +++ b/src/matrix/ssss/SecretStorage.ts @@ -50,10 +50,20 @@ export class SecretStorage { const allAccountData = await txn.accountData.getAll(); for (const accountData of allAccountData) { try { + // TODO: fix this, using the webcrypto api closes the transaction + if (accountData.type === "m.megolm_backup.v1") { + return true; + } else { + continue; + } const secret = await this._decryptAccountData(accountData); return true; // decryption succeeded } catch (err) { - continue; + if (err instanceof DecryptionError && err.reason !== DecryptionFailure.NotEncryptedWithKey) { + throw err; + } else { + continue; + } } } return false; diff --git a/src/platform/web/ui/login/AccountSetupView.js b/src/platform/web/ui/login/AccountSetupView.js index e0d416931b..cf2b544fda 100644 --- a/src/platform/web/ui/login/AccountSetupView.js +++ b/src/platform/web/ui/login/AccountSetupView.js @@ -15,7 +15,7 @@ limitations under the License. */ import {TemplateView} from "../general/TemplateView"; -import {KeyBackupSettingsView} from "../session/settings/KeyBackupSettingsView.js"; +import {KeyBackupSettingsView} from "../session/settings/KeyBackupSettingsView"; export class AccountSetupView extends TemplateView { render(t, vm) { diff --git a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js b/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts similarity index 73% rename from src/platform/web/ui/session/settings/KeyBackupSettingsView.js rename to src/platform/web/ui/session/settings/KeyBackupSettingsView.ts index 6a886e3a30..28c4febf27 100644 --- a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts @@ -14,32 +14,41 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView} from "../../general/TemplateView"; +import {TemplateView, Builder} from "../../general/TemplateView"; import {disableTargetCallback} from "../../general/utils"; +import {ViewNode} from "../../general/types"; +import {KeyBackupViewModel, Status, BackupWriteStatus} from "../../../../../domain/session/settings/KeyBackupViewModel"; +import {KeyType} from "../../../../../matrix/ssss/index"; -export class KeyBackupSettingsView extends TemplateView { - render(t, vm) { +export class KeyBackupSettingsView extends TemplateView { + render(t: Builder, vm: KeyBackupViewModel): ViewNode { return t.div([ t.map(vm => vm.status, (status, t, vm) => { switch (status) { - case "Enabled": return renderEnabled(t, vm); - case "NewVersionAvailable": return renderNewVersionAvailable(t, vm); - case "SetupKey": return renderEnableFromKey(t, vm); - case "SetupPhrase": return renderEnableFromPhrase(t, vm); - case "Pending": return t.p(vm.i18n`Waiting to go onlineโ€ฆ`); + case Status.Enabled: return renderEnabled(t, vm); + case Status.NewVersionAvailable: return renderNewVersionAvailable(t, vm); + case Status.Setup: { + if (vm.setupKeyType === KeyType.Passphrase) { + return renderEnableFromPhrase(t, vm); + } else { + return renderEnableFromKey(t, vm); + } + break; + } + case Status.Pending: return t.p(vm.i18n`Waiting to go onlineโ€ฆ`); } }), t.map(vm => vm.backupWriteStatus, (status, t, vm) => { switch (status) { - case "Writing": { + case BackupWriteStatus.Writing: { const progress = t.progress({ - min: 0, - max: 100, + min: 0+"", + max: 100+"", value: vm => vm.backupPercentage, }); return t.div([`Backup in progress `, progress, " ", vm => vm.backupInProgressLabel]); } - case "Stopped": { + case BackupWriteStatus.Stopped: { let label; const error = vm.backupError; if (error) { @@ -47,12 +56,12 @@ export class KeyBackupSettingsView extends TemplateView { } else { label = `Backup has stopped`; } - return t.p(label, " ", t.button({onClick: () => vm.startBackup()}, `Backup now`)); + return t.p([label, " ", t.button({onClick: () => vm.startBackup()}, `Backup now`)]); } - case "Done": + case BackupWriteStatus.Done: return t.p(`All keys are backed up.`); default: - return null; + return undefined; } }), t.if(vm => vm.isMasterKeyTrusted, t => { @@ -70,7 +79,7 @@ export class KeyBackupSettingsView extends TemplateView { } } -function renderEnabled(t, vm) { +function renderEnabled(t: Builder, vm: KeyBackupViewModel): ViewNode { const items = [ t.p([vm.i18n`Key backup is enabled, using backup version ${vm.backupVersion}. `, t.button({onClick: () => vm.disable()}, vm.i18n`Disable`)]) ]; @@ -80,14 +89,14 @@ function renderEnabled(t, vm) { return t.div(items); } -function renderNewVersionAvailable(t, vm) { +function renderNewVersionAvailable(t: Builder, vm: KeyBackupViewModel): ViewNode { const items = [ t.p([vm.i18n`A new backup version has been created from another device. Disable key backup and enable it again with the new key.`, t.button({onClick: () => vm.disable()}, vm.i18n`Disable`)]) ]; return t.div(items); } -function renderEnableFromKey(t, vm) { +function renderEnableFromKey(t: Builder, vm: KeyBackupViewModel): ViewNode { const useASecurityPhrase = t.button({className: "link", onClick: () => vm.showPhraseSetup()}, vm.i18n`use a security phrase`); return t.div([ t.p(vm.i18n`Enter your secret storage security key below to ${vm.purpose}, which will enable you to decrypt messages received before you logged into this session. The security key is a code of 12 groups of 4 characters separated by a space that Element created for you when setting up security.`), @@ -97,7 +106,7 @@ function renderEnableFromKey(t, vm) { ]); } -function renderEnableFromPhrase(t, vm) { +function renderEnableFromPhrase(t: Builder, vm: KeyBackupViewModel): ViewNode { const useASecurityKey = t.button({className: "link", onClick: () => vm.showKeySetup()}, vm.i18n`use your security key`); return t.div([ t.p(vm.i18n`Enter your secret storage security phrase below to ${vm.purpose}, which will enable you to decrypt messages received before you logged into this session. The security phrase is a freeform secret phrase you optionally chose when setting up security in Element. It is different from your password to login, unless you chose to set them to the same value.`), @@ -107,7 +116,7 @@ function renderEnableFromPhrase(t, vm) { ]); } -function renderEnableFieldRow(t, vm, label, callback) { +function renderEnableFieldRow(t, vm, label, callback): ViewNode { let setupDehydrationCheck; const eventHandler = () => callback(input.value, setupDehydrationCheck?.checked || false); const input = t.input({type: "password", disabled: vm => vm.isBusy, placeholder: label}); @@ -131,8 +140,8 @@ function renderEnableFieldRow(t, vm, label, callback) { ]); } -function renderError(t) { - return t.if(vm => vm.error, (t, vm) => { +function renderError(t: Builder): ViewNode { + return t.if(vm => vm.error !== undefined, (t, vm) => { return t.div([ t.p({className: "error"}, vm => vm.i18n`Could not enable key backup: ${vm.error}.`), t.p(vm.i18n`Try double checking that you did not mix up your security key, security phrase and login password as explained above.`) diff --git a/src/platform/web/ui/session/settings/SettingsView.js b/src/platform/web/ui/session/settings/SettingsView.js index aea1108af0..4035281fc8 100644 --- a/src/platform/web/ui/session/settings/SettingsView.js +++ b/src/platform/web/ui/session/settings/SettingsView.js @@ -16,7 +16,7 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView"; import {disableTargetCallback} from "../../general/utils"; -import {KeyBackupSettingsView} from "./KeyBackupSettingsView.js" +import {KeyBackupSettingsView} from "./KeyBackupSettingsView" import {FeaturesView} from "./FeaturesView" export class SettingsView extends TemplateView { diff --git a/src/utils/AbortableOperation.ts b/src/utils/AbortableOperation.ts index e0afecd38d..3592c95103 100644 --- a/src/utils/AbortableOperation.ts +++ b/src/utils/AbortableOperation.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableValue, ObservableValue} from "../observable/value"; +import {EventEmitter} from "../utils/EventEmitter"; export interface IAbortable { abort(); @@ -24,25 +24,27 @@ export type SetAbortableFn = (a: IAbortable) => typeof a; export type SetProgressFn

= (progress: P) => void; type RunFn = (setAbortable: SetAbortableFn, setProgress: SetProgressFn

) => T; -export class AbortableOperation implements IAbortable { +export class AbortableOperation extends EventEmitter<{change: keyof AbortableOperation}> implements IAbortable { public readonly result: T; private _abortable?: IAbortable; - private _progress: ObservableValue

; + private _progress?: P; constructor(run: RunFn) { + super(); this._abortable = undefined; const setAbortable: SetAbortableFn = abortable => { this._abortable = abortable; return abortable; }; - this._progress = new ObservableValue

(undefined); + this._progress = undefined; const setProgress: SetProgressFn

= (progress: P) => { - this._progress.set(progress); + this._progress = progress; + this.emit("change", "progress"); }; this.result = run(setAbortable, setProgress); } - get progress(): BaseObservableValue

{ + get progress(): P | undefined { return this._progress; } diff --git a/src/utils/Deferred.ts b/src/utils/Deferred.ts new file mode 100644 index 0000000000..430fe996c4 --- /dev/null +++ b/src/utils/Deferred.ts @@ -0,0 +1,41 @@ +/* +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. +*/ + + +export class Deferred { + public readonly promise: Promise; + public readonly resolve: (value: T) => void; + public readonly reject: (err: Error) => void; + private _value?: T; + + constructor() { + let resolve; + let reject; + this.promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }) + this.resolve = (value: T) => { + this._value = value; + resolve(value); + }; + this.reject = reject; + } + + get value(): T | undefined { + return this._value; + } +} From a1086a71392a9b78ebe56aeb7a52057b24d34755 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 22 Mar 2023 14:16:02 +0530 Subject: [PATCH 076/168] Add support for arbitrary notifications --- src/domain/session/toast/IToastCollection.ts | 6 ++ .../session/toast/ToastCollectionViewModel.ts | 73 ++------------ .../CallToastNotificationViewModel.ts | 12 +-- .../calls/CallsToastCollectionVIewModel.ts | 99 +++++++++++++++++++ .../toast/CallToastNotificationView.ts | 2 +- .../ui/session/toast/ToastCollectionView.ts | 2 +- 6 files changed, 121 insertions(+), 73 deletions(-) create mode 100644 src/domain/session/toast/IToastCollection.ts rename src/domain/session/toast/{ => calls}/CallToastNotificationViewModel.ts (89%) create mode 100644 src/domain/session/toast/calls/CallsToastCollectionVIewModel.ts diff --git a/src/domain/session/toast/IToastCollection.ts b/src/domain/session/toast/IToastCollection.ts new file mode 100644 index 0000000000..c8fbeee6bf --- /dev/null +++ b/src/domain/session/toast/IToastCollection.ts @@ -0,0 +1,6 @@ +import {ObservableArray} from "../../../observable"; +import type {BaseToastNotificationViewModel} from "./BaseToastNotificationViewModel"; + +export interface IToastCollection { + toastViewModels: ObservableArray; +} diff --git a/src/domain/session/toast/ToastCollectionViewModel.ts b/src/domain/session/toast/ToastCollectionViewModel.ts index 59931a5c5c..19649433bc 100644 --- a/src/domain/session/toast/ToastCollectionViewModel.ts +++ b/src/domain/session/toast/ToastCollectionViewModel.ts @@ -14,83 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {CallToastNotificationViewModel} from "./CallToastNotificationViewModel"; -import {ObservableArray} from "../../../observable"; +import {ConcatList} from "../../../observable"; import {ViewModel, Options as BaseOptions} from "../../ViewModel"; -import {RoomStatus} from "../../../matrix/room/common"; -import type {GroupCall} from "../../../matrix/calls/group/GroupCall"; -import type {Room} from "../../../matrix/room/Room.js"; +import {CallToastCollectionViewModel} from "./calls/CallsToastCollectionVIewModel"; import type {Session} from "../../../matrix/Session.js"; import type {SegmentType} from "../../navigation"; +import type {BaseToastNotificationViewModel} from "./BaseToastNotificationViewModel"; type Options = { session: Session; } & BaseOptions; export class ToastCollectionViewModel extends ViewModel { - public readonly toastViewModels: ObservableArray = new ObservableArray(); + public readonly toastViewModels: ConcatList; constructor(options: Options) { super(options); const session = this.getOption("session"); - if (this.features.calls) { - const callsObservableMap = session.callHandler.calls; - this.track(callsObservableMap.subscribe(this)); - } - } - - async onAdd(_, call: GroupCall) { - if (this._shouldShowNotification(call)) { - const room = await this._findRoomForCall(call); - const dismiss = () => { - const idx = this.toastViewModels.array.findIndex(vm => vm.call === call); - if (idx !== -1) { - this.toastViewModels.remove(idx); - } - }; - this.toastViewModels.append( - new CallToastNotificationViewModel(this.childOptions({ call, room, dismiss })) - ); - } - } - - onRemove(_, call: GroupCall) { - const idx = this.toastViewModels.array.findIndex(vm => vm.call === call); - if (idx !== -1) { - this.toastViewModels.remove(idx); - } - } - - onUpdate(_, call: GroupCall) { - const idx = this.toastViewModels.array.findIndex(vm => vm.call === call); - if (idx !== -1) { - this.toastViewModels.update(idx, this.toastViewModels.at(idx)!); - } - } - - onReset() { - for (let i = 0; i < this.toastViewModels.length; ++i) { - this.toastViewModels.remove(i); - } - } - - private async _findRoomForCall(call: GroupCall): Promise { - const id = call.roomId; - const session = this.getOption("session"); - const rooms = session.rooms; - // Make sure that we know of this room, - // otherwise wait for it to come through sync - const observable = await session.observeRoomStatus(id); - await observable.waitFor(s => s === RoomStatus.Joined).promise; - const room = rooms.get(id); - return room; - } - - private _shouldShowNotification(call: GroupCall): boolean { - const currentlyOpenedRoomId = this.navigation.path.get("room")?.value; - if (!call.isLoadedFromStorage && call.roomId !== currentlyOpenedRoomId && !call.usesFoci) { - return true; - } - return false; + const vms = [ + this.track(new CallToastCollectionViewModel(this.childOptions({ session }))), + ].map(vm => vm.toastViewModels); + this.toastViewModels = new ConcatList(...vms); } } diff --git a/src/domain/session/toast/CallToastNotificationViewModel.ts b/src/domain/session/toast/calls/CallToastNotificationViewModel.ts similarity index 89% rename from src/domain/session/toast/CallToastNotificationViewModel.ts rename to src/domain/session/toast/calls/CallToastNotificationViewModel.ts index 5c7883347c..eab28bb59b 100644 --- a/src/domain/session/toast/CallToastNotificationViewModel.ts +++ b/src/domain/session/toast/calls/CallToastNotificationViewModel.ts @@ -13,12 +13,12 @@ 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 {GroupCall} from "../../../matrix/calls/group/GroupCall"; -import type {Room} from "../../../matrix/room/Room.js"; -import {IAvatarContract, avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; -import {LocalMedia} from "../../../matrix/calls/LocalMedia"; -import {BaseClassOptions, BaseToastNotificationViewModel} from "./BaseToastNotificationViewModel"; -import {SegmentType} from "../../navigation"; +import type {GroupCall} from "../../../../matrix/calls/group/GroupCall"; +import type {Room} from "../../../../matrix/room/Room.js"; +import {IAvatarContract, avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../../avatar"; +import {LocalMedia} from "../../../../matrix/calls/LocalMedia"; +import {BaseClassOptions, BaseToastNotificationViewModel} from ".././BaseToastNotificationViewModel"; +import {SegmentType} from "../../../navigation"; type Options = { call: GroupCall; diff --git a/src/domain/session/toast/calls/CallsToastCollectionVIewModel.ts b/src/domain/session/toast/calls/CallsToastCollectionVIewModel.ts new file mode 100644 index 0000000000..4dd9a73a36 --- /dev/null +++ b/src/domain/session/toast/calls/CallsToastCollectionVIewModel.ts @@ -0,0 +1,99 @@ + +/* +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 {CallToastNotificationViewModel} from "./CallToastNotificationViewModel"; +import {ObservableArray} from "../../../../observable"; +import {ViewModel, Options as BaseOptions} from "../../../ViewModel"; +import {RoomStatus} from "../../../../matrix/room/common"; +import type {GroupCall} from "../../../../matrix/calls/group/GroupCall"; +import type {Room} from "../../../../matrix/room/Room.js"; +import type {Session} from "../../../../matrix/Session.js"; +import type {SegmentType} from "../../../navigation"; +import type {IToastCollection} from "../IToastCollection"; + +type Options = { + session: Session; +} & BaseOptions; + + +export class CallToastCollectionViewModel extends ViewModel implements IToastCollection { + public readonly toastViewModels: ObservableArray = new ObservableArray(); + + constructor(options: Options) { + super(options); + const session = this.getOption("session"); + if (this.features.calls) { + const callsObservableMap = session.callHandler.calls; + this.track(callsObservableMap.subscribe(this)); + } + } + + async onAdd(_, call: GroupCall) { + if (this._shouldShowNotification(call)) { + const room = await this._findRoomForCall(call); + const dismiss = () => { + const idx = this.toastViewModels.array.findIndex(vm => vm.call === call); + if (idx !== -1) { + this.toastViewModels.remove(idx); + } + }; + this.toastViewModels.append( + new CallToastNotificationViewModel(this.childOptions({ call, room, dismiss })) + ); + } + } + + onRemove(_, call: GroupCall) { + const idx = this.toastViewModels.array.findIndex(vm => vm.call === call); + if (idx !== -1) { + this.toastViewModels.remove(idx); + } + } + + onUpdate(_, call: GroupCall) { + const idx = this.toastViewModels.array.findIndex(vm => vm.call === call); + if (idx !== -1) { + this.toastViewModels.update(idx, this.toastViewModels.at(idx)!); + } + } + + onReset() { + for (let i = 0; i < this.toastViewModels.length; ++i) { + this.toastViewModels.remove(i); + } + } + + private async _findRoomForCall(call: GroupCall): Promise { + const id = call.roomId; + const session = this.getOption("session"); + const rooms = session.rooms; + // Make sure that we know of this room, + // otherwise wait for it to come through sync + const observable = await session.observeRoomStatus(id); + await observable.waitFor(s => s === RoomStatus.Joined).promise; + const room = rooms.get(id); + return room; + } + + private _shouldShowNotification(call: GroupCall): boolean { + const currentlyOpenedRoomId = this.navigation.path.get("room")?.value; + if (!call.isLoadedFromStorage && call.roomId !== currentlyOpenedRoomId && !call.usesFoci) { + return true; + } + return false; + } +} diff --git a/src/platform/web/ui/session/toast/CallToastNotificationView.ts b/src/platform/web/ui/session/toast/CallToastNotificationView.ts index 50adcc7b33..093a5d0fe6 100644 --- a/src/platform/web/ui/session/toast/CallToastNotificationView.ts +++ b/src/platform/web/ui/session/toast/CallToastNotificationView.ts @@ -17,7 +17,7 @@ limitations under the License. import {AvatarView} from "../../AvatarView.js"; import {ErrorView} from "../../general/ErrorView"; import {TemplateView, Builder} from "../../general/TemplateView"; -import type {CallToastNotificationViewModel} from "../../../../../domain/session/toast/CallToastNotificationViewModel"; +import type {CallToastNotificationViewModel} from "../../../../../domain/session/toast/calls/CallToastNotificationViewModel"; export class CallToastNotificationView extends TemplateView { render(t: Builder, vm: CallToastNotificationViewModel) { diff --git a/src/platform/web/ui/session/toast/ToastCollectionView.ts b/src/platform/web/ui/session/toast/ToastCollectionView.ts index 3dc99c77d6..3f38609dde 100644 --- a/src/platform/web/ui/session/toast/ToastCollectionView.ts +++ b/src/platform/web/ui/session/toast/ToastCollectionView.ts @@ -17,7 +17,7 @@ limitations under the License. import {CallToastNotificationView} from "./CallToastNotificationView"; import {ListView} from "../../general/ListView"; import {TemplateView, Builder} from "../../general/TemplateView"; -import type {CallToastNotificationViewModel} from "../../../../../domain/session/toast/CallToastNotificationViewModel"; +import type {CallToastNotificationViewModel} from "../../../../../domain/session/toast/calls/CallToastNotificationViewModel"; import type {ToastCollectionViewModel} from "../../../../../domain/session/toast/ToastCollectionViewModel"; export class ToastCollectionView extends TemplateView { From 93d37aeb93f98fc6b5845877438ec7c38584aa25 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 22 Mar 2023 15:05:58 +0530 Subject: [PATCH 077/168] Create views based on viewmodel --- .../session/toast/BaseToastNotificationViewModel.ts | 2 ++ .../session/toast/ToastCollectionViewModel.ts | 2 +- .../toast/calls/CallToastNotificationViewModel.ts | 4 ++++ ...IewModel.ts => CallsToastCollectionViewModel.ts} | 0 .../web/ui/session/toast/ToastCollectionView.ts | 13 ++++++++++++- 5 files changed, 19 insertions(+), 2 deletions(-) rename src/domain/session/toast/calls/{CallsToastCollectionVIewModel.ts => CallsToastCollectionViewModel.ts} (100%) diff --git a/src/domain/session/toast/BaseToastNotificationViewModel.ts b/src/domain/session/toast/BaseToastNotificationViewModel.ts index 41e20e42d3..133a8d367c 100644 --- a/src/domain/session/toast/BaseToastNotificationViewModel.ts +++ b/src/domain/session/toast/BaseToastNotificationViewModel.ts @@ -32,4 +32,6 @@ export abstract class BaseToastNotificationViewModel { await this.logAndCatch("CallToastNotificationViewModel.join", async (log) => { const stream = await this.platform.mediaDevices.getMediaTracks(false, true); diff --git a/src/domain/session/toast/calls/CallsToastCollectionVIewModel.ts b/src/domain/session/toast/calls/CallsToastCollectionViewModel.ts similarity index 100% rename from src/domain/session/toast/calls/CallsToastCollectionVIewModel.ts rename to src/domain/session/toast/calls/CallsToastCollectionViewModel.ts diff --git a/src/platform/web/ui/session/toast/ToastCollectionView.ts b/src/platform/web/ui/session/toast/ToastCollectionView.ts index 3f38609dde..a3d734d2fc 100644 --- a/src/platform/web/ui/session/toast/ToastCollectionView.ts +++ b/src/platform/web/ui/session/toast/ToastCollectionView.ts @@ -17,15 +17,26 @@ limitations under the License. import {CallToastNotificationView} from "./CallToastNotificationView"; import {ListView} from "../../general/ListView"; import {TemplateView, Builder} from "../../general/TemplateView"; +import type {IView} from "../../general/types"; import type {CallToastNotificationViewModel} from "../../../../../domain/session/toast/calls/CallToastNotificationViewModel"; import type {ToastCollectionViewModel} from "../../../../../domain/session/toast/ToastCollectionViewModel"; +import type {BaseToastNotificationViewModel} from "../../../../../domain/session/toast/BaseToastNotificationViewModel"; + +function toastViewModelToView(vm: BaseToastNotificationViewModel): IView { + switch (vm.kind) { + case "calls": + return new CallToastNotificationView(vm as CallToastNotificationViewModel); + default: + throw new Error(`Cannot find view class for notification kind ${vm.kind}`); + } +} export class ToastCollectionView extends TemplateView { render(t: Builder, vm: ToastCollectionViewModel) { const view = new ListView({ list: vm.toastViewModels, parentProvidesUpdates: false, - }, (vm: CallToastNotificationViewModel) => new CallToastNotificationView(vm)); + }, (vm: CallToastNotificationViewModel) => toastViewModelToView(vm)); return t.div({ className: "ToastCollectionView" }, [ t.view(view), ]); From 762a91bd162846608534f96ce9229804ed74e296 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 24 Mar 2023 13:42:19 +0100 Subject: [PATCH 078/168] don't reuse existing transaction to read from 4S, as webcrypto terminates idb transactions --- src/domain/SessionLoadViewModel.js | 2 +- .../session/settings/KeyBackupViewModel.ts | 2 +- src/matrix/Session.js | 46 +++++++++---------- src/matrix/e2ee/DeviceTracker.ts | 4 +- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 5 +- src/matrix/ssss/SecretStorage.ts | 22 +++++---- src/matrix/verification/CrossSigning.ts | 27 +++++------ 7 files changed, 52 insertions(+), 56 deletions(-) diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 6a63145f4a..75ecfec19c 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -78,7 +78,7 @@ export class SessionLoadViewModel extends ViewModel { this._ready(client); } if (loadError) { - console.error("session load error", loadError); + console.error("session load error", loadError.stack); } } catch (err) { this._error = err; diff --git a/src/domain/session/settings/KeyBackupViewModel.ts b/src/domain/session/settings/KeyBackupViewModel.ts index cdfd40817a..3426191bd1 100644 --- a/src/domain/session/settings/KeyBackupViewModel.ts +++ b/src/domain/session/settings/KeyBackupViewModel.ts @@ -56,7 +56,7 @@ export class KeyBackupViewModel extends ViewModel { super(options); const onKeyBackupSet = (keyBackup: KeyBackup | undefined) => { if (keyBackup && !this._keyBackupSubscription) { - this._keyBackupSubscription = this.track(this._session.keyBackup.disposableOn("change", () => { + this._keyBackupSubscription = this.track(this._session.keyBackup.get().disposableOn("change", () => { this._onKeyBackupChange(); })); } else if (!keyBackup && this._keyBackupSubscription) { diff --git a/src/matrix/Session.js b/src/matrix/Session.js index b10b282439..655552865e 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -254,7 +254,7 @@ export class Session { } // TODO: stop cross-signing const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); - if (await this._tryLoadSecretStorage(key, undefined, log)) { + if (await this._tryLoadSecretStorage(key, log)) { // only after having read a secret, write the key // as we only find out if it was good if the MAC verification succeeds await this._writeSSSSKey(key, log); @@ -318,24 +318,19 @@ export class Session { // TODO: stop cross-signing } - _tryLoadSecretStorage(ssssKey, existingTxn, log) { + _tryLoadSecretStorage(ssssKey, log) { return log.wrap("enable secret storage", async log => { - const txn = existingTxn ?? await this._storage.readTxn([ - this._storage.storeNames.accountData, - this._storage.storeNames.crossSigningKeys, - this._storage.storeNames.userIdentities, - ]); - const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); - const isValid = await secretStorage.hasValidKeyForAnyAccountData(txn); + const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform, storage: this._storage}); + const isValid = await secretStorage.hasValidKeyForAnyAccountData(); log.set("isValid", isValid); if (isValid) { - await this._loadSecretStorageServices(secretStorage, txn, log); + await this._loadSecretStorageServices(secretStorage, log); } return isValid; }); } - async _loadSecretStorageServices(secretStorage, txn, log) { + async _loadSecretStorageServices(secretStorage, log) { try { await log.wrap("enable key backup", async log => { const keyBackup = new KeyBackup( @@ -345,7 +340,7 @@ export class Session { this._storage, this._platform, ); - if (await keyBackup.load(secretStorage, txn)) { + if (await keyBackup.load(secretStorage, log)) { for (const room of this._rooms.values()) { if (room.isEncrypted) { room.enableKeyBackup(keyBackup); @@ -370,7 +365,7 @@ export class Session { ownUserId: this.userId, e2eeAccount: this._e2eeAccount }); - if (await crossSigning.load(txn, log)) { + if (await crossSigning.load(log)) { this._crossSigning.set(crossSigning); } }); @@ -497,15 +492,8 @@ export class Session { olmWorker: this._olmWorker, txn }); - if (this._e2eeAccount) { - log.set("keys", this._e2eeAccount.identityKeys); - this._setupEncryption(); - // try set up session backup if we stored the ssss key - const ssssKey = await ssssReadKey(txn); - if (ssssKey) { - await this._tryLoadSecretStorage(ssssKey, txn, log); - } - } + log.set("keys", this._e2eeAccount.identityKeys); + this._setupEncryption(); } const pendingEventsByRoomId = await this._getPendingEventsByRoom(txn); // load invites @@ -530,6 +518,14 @@ export class Session { room.setInvite(invite); } } + if (this._olm && this._e2eeAccount) { + // try set up session backup and cross-signing if we stored the ssss key + const ssssKey = await ssssReadKey(txn); + if (ssssKey) { + // this will close the txn above, so we do it last + await this._tryLoadSecretStorage(ssssKey, log); + } + } } dispose() { @@ -570,15 +566,15 @@ export class Session { await log.wrap("SSSSKeyFromDehydratedDeviceKey", async log => { const ssssKey = await createSSSSKeyFromDehydratedDeviceKey(dehydratedDevice.key, this._storage, this._platform); if (ssssKey) { - if (await this._tryLoadSecretStorage(ssssKey, undefined, log)) { + if (await this._tryLoadSecretStorage(ssssKey, log)) { log.set("success", true); await this._writeSSSSKey(ssssKey); } } }); } - this._keyBackup.get()?.start(log); - this._crossSigning.get()?.start(log); + await this._keyBackup.get()?.start(log); + await this._crossSigning.get()?.start(log); // restore unfinished operations, like sending out room keys const opsTxn = await this._storage.readWriteTxn([ diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 23bdc31eea..3a50a890d2 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -163,9 +163,9 @@ export class DeviceTracker { } } - async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi | undefined, existingTxn: Transaction | undefined, log: ILogItem): Promise { + async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi | undefined, log: ILogItem): Promise { return await log.wrap({l: "DeviceTracker.getCrossSigningKeyForUser", id: userId, usage}, async log => { - const txn = existingTxn ?? await this._storage.readTxn([ + const txn = await this._storage.readTxn([ this._storage.storeNames.userIdentities, this._storage.storeNames.crossSigningKeys, ]); diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 8e9a4a81f5..da1075021d 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -107,9 +107,8 @@ export class KeyBackup extends EventEmitter<{change: never}> { return txn.inboundGroupSessions.markAllAsNotBackedUp(); } - async load(secretStorage: SecretStorage, txn: Transaction) { - // TODO: no exception here we should anticipate? - const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); + async load(secretStorage: SecretStorage, log: ILogItem) { + const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1"); if (base64PrivateKey) { this.privateKey = new Uint8Array(this.platform.encoding.base64.decode(base64PrivateKey)); return true; diff --git a/src/matrix/ssss/SecretStorage.ts b/src/matrix/ssss/SecretStorage.ts index 093382a8e9..4c767bbbfe 100644 --- a/src/matrix/ssss/SecretStorage.ts +++ b/src/matrix/ssss/SecretStorage.ts @@ -40,22 +40,22 @@ class DecryptionError extends Error { export class SecretStorage { private readonly _key: Key; private readonly _platform: Platform; + private readonly _storage: Storage; - constructor({key, platform}: {key: Key, platform: Platform}) { + constructor({key, platform, storage}: {key: Key, platform: Platform, storage: Storage}) { this._key = key; this._platform = platform; + this._storage = storage; } - async hasValidKeyForAnyAccountData(txn: Transaction) { + /** this method will auto-commit any indexeddb transaction because of its use of the webcrypto api */ + async hasValidKeyForAnyAccountData() { + const txn = await this._storage.readTxn([ + this._storage.storeNames.accountData, + ]); const allAccountData = await txn.accountData.getAll(); for (const accountData of allAccountData) { try { - // TODO: fix this, using the webcrypto api closes the transaction - if (accountData.type === "m.megolm_backup.v1") { - return true; - } else { - continue; - } const secret = await this._decryptAccountData(accountData); return true; // decryption succeeded } catch (err) { @@ -69,7 +69,11 @@ export class SecretStorage { return false; } - async readSecret(name: string, txn: Transaction): Promise { + /** this method will auto-commit any indexeddb transaction because of its use of the webcrypto api */ + async readSecret(name: string): Promise { + const txn = await this._storage.readTxn([ + this._storage.storeNames.accountData, + ]); const accountData = await txn.accountData.get(name); if (!accountData) { return; diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 1abc3702ef..196190ae18 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -99,22 +99,22 @@ export class CrossSigning { this.e2eeAccount = options.e2eeAccount } - async load(txn: Transaction, log: ILogItem) { + async load(log: ILogItem) { // try to verify the msk without accessing the network - return await this.verifyMSKFrom4S(undefined, txn, log); + return await this.verifyMSKFrom4S(false, log); } async start(log: ILogItem) { if (!this.isMasterKeyTrusted) { // try to verify the msk _with_ access to the network - return await this.verifyMSKFrom4S(this.hsApi, undefined, log); + return await this.verifyMSKFrom4S(true, log); } } - private async verifyMSKFrom4S(hsApi: HomeServerApi | undefined, txn: Transaction | undefined, log: ILogItem): Promise { + private async verifyMSKFrom4S(allowNetwork: boolean, log: ILogItem): Promise { return await log.wrap("CrossSigning.verifyMSKFrom4S", async log => { // TODO: use errorboundary here - const privateMasterKey = await this.getSigningKey(KeyUsage.Master, txn); + const privateMasterKey = await this.getSigningKey(KeyUsage.Master); if (!privateMasterKey) { return false; } @@ -125,7 +125,7 @@ export class CrossSigning { } finally { signing.free(); } - const publishedMasterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, hsApi, txn, log); + const publishedMasterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, allowNetwork ? this.hsApi : undefined, log); if (!publishedMasterKey) { return false; } @@ -210,11 +210,11 @@ export class CrossSigning { if (!this.isMasterKeyTrusted) { return UserTrust.OwnSetupError; } - const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, txn, log)); + const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, undefined, log)); if (!ourMSK) { return UserTrust.OwnSetupError; } - const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, txn, log)); + const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, undefined, log)); if (!ourUSK) { return UserTrust.OwnSetupError; } @@ -222,7 +222,7 @@ export class CrossSigning { if (ourUSKVerification !== SignatureVerification.Valid) { return UserTrust.OwnSetupError; } - const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, txn, log)); + const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, undefined, log)); if (!theirMSK) { /* assume that when they don't have an MSK, they've never enabled cross-signing on their client (or it's not supported) rather than assuming a setup error on their side. @@ -237,7 +237,7 @@ export class CrossSigning { return UserTrust.UserSignatureMismatch; } } - const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, txn, log)); + const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, undefined, log)); if (!theirSSK) { return UserTrust.UserSetupError; } @@ -290,11 +290,8 @@ export class CrossSigning { return keyToSign; } - private async getSigningKey(usage: KeyUsage, existingTxn?: Transaction): Promise { - const txn = existingTxn ?? await this.storage.readTxn([ - this.storage.storeNames.accountData, - ]); - const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`, txn); + private async getSigningKey(usage: KeyUsage): Promise { + const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`); if (seedStr) { return new Uint8Array(this.platform.encoding.base64.decode(seedStr)); } From e2ae5e716e33ce23735e20bfc126903bf71b397d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 19:17:14 +0530 Subject: [PATCH 079/168] Do not emit for now --- src/matrix/DeviceMessageHandler.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/matrix/DeviceMessageHandler.js b/src/matrix/DeviceMessageHandler.js index af2966d7e4..d8743c31b8 100644 --- a/src/matrix/DeviceMessageHandler.js +++ b/src/matrix/DeviceMessageHandler.js @@ -114,9 +114,10 @@ export class DeviceMessageHandler extends EventEmitter{ } _emitEncryptedEvents(decryptionResults) { - for (const result of decryptionResults) { - this.emit("message", { encrypted: result }); - } + // 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 }); + // } } } From 321775b800cd3ad0a04269c2d75add87fffd8214 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 19:18:31 +0530 Subject: [PATCH 080/168] Rename CancelTypes -> CancelReason --- .../verification/SAS/SASVerification.ts | 4 +-- .../verification/SAS/channel/Channel.ts | 34 +++++++++---------- .../verification/SAS/channel/MockChannel.ts | 4 +-- src/matrix/verification/SAS/channel/types.ts | 2 +- .../SAS/stages/CalculateSASStage.ts | 6 ++-- .../stages/SelectVerificationMethodStage.ts | 4 +-- .../SAS/stages/SendAcceptVerificationStage.ts | 4 +-- .../verification/SAS/stages/VerifyMacStage.ts | 6 ++-- 8 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index f249118a05..355acd2668 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -21,7 +21,7 @@ import type {DeviceTracker} from "../../e2ee/DeviceTracker.js"; import type * as OlmNamespace from "@matrix-org/olm"; import {IChannel} from "./channel/Channel"; import {HomeServerApi} from "../../net/HomeServerApi"; -import {CancelTypes, VerificationEventTypes} from "./channel/types"; +import {CancelReason, VerificationEventTypes} from "./channel/types"; import {SendReadyStage} from "./stages/SendReadyStage"; import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; import {VerificationCancelledError} from "./VerificationCancelledError"; @@ -78,7 +78,7 @@ export class SASVerification { const tenMinutes = 10 * 60 * 1000; this.timeout = clock.createTimeout(tenMinutes); await this.timeout.elapsed(); - await this.channel.cancelVerification(CancelTypes.TimedOut); + await this.channel.cancelVerification(CancelReason.TimedOut); } catch { // Ignore errors diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index c874586d06..0fb2cb7c2a 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -20,22 +20,22 @@ import type {ILogItem} from "../../../../logging/types"; import type {Clock} from "../../../../platform/web/dom/Clock.js"; import type {DeviceMessageHandler} from "../../../DeviceMessageHandler.js"; import {makeTxnId} from "../../../common.js"; -import {CancelTypes, VerificationEventTypes} from "./types"; +import {CancelReason, VerificationEventTypes} from "./types"; import {Disposables} from "../../../../utils/Disposables"; import {VerificationCancelledError} from "../VerificationCancelledError"; const messageFromErrorType = { - [CancelTypes.UserCancelled]: "User declined", - [CancelTypes.InvalidMessage]: "Invalid Message.", - [CancelTypes.KeyMismatch]: "Key Mismatch.", - [CancelTypes.OtherDeviceAccepted]: "Another device has accepted this request.", - [CancelTypes.TimedOut]: "Timed Out", - [CancelTypes.UnexpectedMessage]: "Unexpected Message.", - [CancelTypes.UnknownMethod]: "Unknown method.", - [CancelTypes.UnknownTransaction]: "Unknown Transaction.", - [CancelTypes.UserMismatch]: "User Mismatch", - [CancelTypes.MismatchedCommitment]: "Hash commitment does not match.", - [CancelTypes.MismatchedSAS]: "Emoji/decimal does not match.", + [CancelReason.UserCancelled]: "User declined", + [CancelReason.InvalidMessage]: "Invalid Message.", + [CancelReason.KeyMismatch]: "Key Mismatch.", + [CancelReason.OtherDeviceAccepted]: "Another device has accepted this request.", + [CancelReason.TimedOut]: "Timed Out", + [CancelReason.UnexpectedMessage]: "Unexpected Message.", + [CancelReason.UnknownMethod]: "Unknown method.", + [CancelReason.UnknownTransaction]: "Unknown Transaction.", + [CancelReason.UserMismatch]: "User Mismatch", + [CancelReason.MismatchedCommitment]: "Hash commitment does not match.", + [CancelReason.MismatchedSAS]: "Emoji/decimal does not match.", } export interface IChannel { @@ -45,7 +45,7 @@ export interface IChannel { getReceivedMessage(event: VerificationEventTypes): any; setStartMessage(content: any): void; setOurDeviceId(id: string): void; - cancelVerification(cancellationType: CancelTypes): Promise; + cancelVerification(cancellationType: CancelReason): Promise; acceptMessage: any; startMessage: any; initiatedByUs: boolean; @@ -185,7 +185,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { * This does not apply for inbound m.key.verification.start or m.key.verification.cancel messages. */ console.log("Received event with unknown transaction id: ", event); - await this.cancelVerification(CancelTypes.UnknownTransaction); + await this.cancelVerification(CancelReason.UnknownTransaction); return; } console.log("event", event); @@ -211,8 +211,8 @@ export class ToDeviceChannel extends Disposables implements IChannel { const devices = await this.deviceTracker.devicesForUsers([this.otherUserId], this.hsApi, log); const otherDevices = devices.filter(device => device.deviceId !== fromDevice && device.deviceId !== this.ourDeviceId); const cancelMessage = { - code: CancelTypes.OtherDeviceAccepted, - reason: messageFromErrorType[CancelTypes.OtherDeviceAccepted], + code: CancelReason.OtherDeviceAccepted, + reason: messageFromErrorType[CancelReason.OtherDeviceAccepted], transaction_id: this.id, }; const deviceMessages = otherDevices.reduce((acc, device) => { acc[device.deviceId] = cancelMessage; return acc; }, {}); @@ -224,7 +224,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, makeTxnId(), { log }).response(); } - async cancelVerification(cancellationType: CancelTypes) { + async cancelVerification(cancellationType: CancelReason) { await this.log.wrap("Channel.cancelVerification", async log => { if (this.isCancelled) { throw new VerificationCancelledError(); diff --git a/src/matrix/verification/SAS/channel/MockChannel.ts b/src/matrix/verification/SAS/channel/MockChannel.ts index b2de0f3f7e..a9cc4dea30 100644 --- a/src/matrix/verification/SAS/channel/MockChannel.ts +++ b/src/matrix/verification/SAS/channel/MockChannel.ts @@ -2,7 +2,7 @@ import type {ILogItem} from "../../../../lib"; import {createCalculateMAC} from "../mac"; import {VerificationCancelledError} from "../VerificationCancelledError"; import {IChannel} from "./Channel"; -import {CancelTypes, VerificationEventTypes} from "./types"; +import {CancelReason, VerificationEventTypes} from "./types"; import anotherjson from "another-json"; interface ITestChannel extends IChannel { @@ -115,7 +115,7 @@ export class MockChannel implements ITestChannel { this.ourUserDeviceId = id; } - async cancelVerification(_: CancelTypes): Promise { + async cancelVerification(_: CancelReason): Promise { console.log("MockChannel.cancelVerification()"); this.isCancelled = true; } diff --git a/src/matrix/verification/SAS/channel/types.ts b/src/matrix/verification/SAS/channel/types.ts index de6a999bfa..400404b724 100644 --- a/src/matrix/verification/SAS/channel/types.ts +++ b/src/matrix/verification/SAS/channel/types.ts @@ -9,7 +9,7 @@ export const enum VerificationEventTypes { Done = "m.key.verification.done", } -export const enum CancelTypes { +export const enum CancelReason { UserCancelled = "m.user", TimedOut = "m.timeout", UnknownTransaction = "m.unknown_transaction", diff --git a/src/matrix/verification/SAS/stages/CalculateSASStage.ts b/src/matrix/verification/SAS/stages/CalculateSASStage.ts index daa744d4f1..fc509aeaf5 100644 --- a/src/matrix/verification/SAS/stages/CalculateSASStage.ts +++ b/src/matrix/verification/SAS/stages/CalculateSASStage.ts @@ -15,7 +15,7 @@ limitations under the License. */ import anotherjson from "another-json"; import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {CancelTypes, VerificationEventTypes} from "../channel/types"; +import {CancelReason, VerificationEventTypes} from "../channel/types"; import {generateEmojiSas} from "../generator"; import {ILogItem} from "../../../../logging/types"; import {SendMacStage} from "./SendMacStage"; @@ -89,7 +89,7 @@ export class CalculateSASStage extends BaseSASVerificationStage { const hash = this.olmUtil.sha256(commitmentStr); if (hash !== receivedCommitment) { log.log({l: "Commitment mismatched!", received: receivedCommitment, calculated: hash}); - await this.channel.cancelVerification(CancelTypes.MismatchedCommitment); + await this.channel.cancelVerification(CancelReason.MismatchedCommitment); return false; } return true; @@ -130,7 +130,7 @@ export class CalculateSASStage extends BaseSASVerificationStage { this.resolve(); } else { - await this.channel.cancelVerification(CancelTypes.MismatchedSAS); + await this.channel.cancelVerification(CancelReason.MismatchedSAS); this.reject(new VerificationCancelledError()); } } diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index 9108ca71a7..78f6aaf9c8 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {CancelTypes, VerificationEventTypes} from "../channel/types"; +import {CancelReason, VerificationEventTypes} from "../channel/types"; import {KEY_AGREEMENT_LIST, HASHES_LIST, MAC_LIST, SAS_LIST} from "./constants"; import {SendAcceptVerificationStage} from "./SendAcceptVerificationStage"; import {SendKeyStage} from "./SendKeyStage"; @@ -69,7 +69,7 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { received: receivedStartMessage.content.method, sent: sentStartMessage.content.method, }); - await this.channel.cancelVerification(CancelTypes.UnexpectedMessage); + await this.channel.cancelVerification(CancelReason.UnexpectedMessage); return; } // In the case of conflict, the lexicographically smaller id wins diff --git a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts index b921a6a82c..c69d41df1a 100644 --- a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts @@ -16,7 +16,7 @@ limitations under the License. import anotherjson from "another-json"; import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {HASHES_LIST, MAC_LIST, SAS_SET, KEY_AGREEMENT_LIST} from "./constants"; -import {CancelTypes, VerificationEventTypes} from "../channel/types"; +import {CancelReason, VerificationEventTypes} from "../channel/types"; import {SendKeyStage} from "./SendKeyStage"; // from element-web @@ -33,7 +33,7 @@ export class SendAcceptVerificationStage extends BaseSASVerificationStage { const macMethod = intersection(MAC_LIST, new Set(startMessage.message_authentication_codes))[0]; const sasMethod = intersection(startMessage.short_authentication_string, SAS_SET); if (!keyAgreement || !hashMethod || !macMethod || !sasMethod.length) { - await this.channel.cancelVerification(CancelTypes.UnknownMethod); + await this.channel.cancelVerification(CancelReason.UnknownMethod); return; } const ourPubKey = this.olmSAS.get_pubkey(); diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 55cb80e38a..8366abfc84 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {ILogItem} from "../../../../logging/types"; -import {CancelTypes, VerificationEventTypes} from "../channel/types"; +import {CancelReason, VerificationEventTypes} from "../channel/types"; import {createCalculateMAC} from "../mac"; import {SendDoneStage} from "./SendDoneStage"; @@ -46,7 +46,7 @@ export class VerifyMacStage extends BaseSASVerificationStage { const calculatedMAC = calculateMAC(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS"); if (content.keys !== calculatedMAC) { log.log({ l: "MAC verification failed for keys field", keys: content.keys, calculated: calculatedMAC }); - this.channel.cancelVerification(CancelTypes.KeyMismatch); + this.channel.cancelVerification(CancelReason.KeyMismatch); return; } @@ -54,7 +54,7 @@ export class VerifyMacStage extends BaseSASVerificationStage { const calculatedMAC = calculateMAC(key, baseInfo + keyId); if (keyInfo !== calculatedMAC) { log.log({ l: "Mac verification failed for key", keyMac: keyInfo, calculatedMAC, keyId, key }); - this.channel.cancelVerification(CancelTypes.KeyMismatch); + this.channel.cancelVerification(CancelReason.KeyMismatch); return; } }, log); From 1c09f20778d5a9b2c4527860e4670eb264b6dc4a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 19:28:49 +0530 Subject: [PATCH 081/168] Pass device-id through options --- src/matrix/verification/CrossSigning.ts | 1 + src/matrix/verification/SAS/SASVerification.ts | 2 +- src/matrix/verification/SAS/channel/Channel.ts | 7 ++----- src/matrix/verification/SAS/channel/MockChannel.ts | 6 +----- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index b5af8b8dfc..369487df60 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -151,6 +151,7 @@ export class CrossSigning { otherUserId: userId, clock: this.platform.clock, deviceMessageHandler: this.deviceMessageHandler, + ourUserDeviceId: this.deviceId, log }, startingMessage); diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 355acd2668..3831d413c1 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -59,7 +59,6 @@ export class SASVerification { const olmSas = new olm.SAS(); this.olmSas = olmSas; this.channel = channel; - this.channel.setOurDeviceId(options.ourUserDeviceId); this.setupCancelAfterTimeout(clock); const stageOptions = {...options, olmSas, eventEmitter: this.eventEmitter}; if (channel.getReceivedMessage(VerificationEventTypes.Start)) { @@ -163,6 +162,7 @@ export function tests() { theirDeviceId, theirUserId, ourUserId, + ourDeviceId, receivedMessages, deviceTracker, txnId, diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 0fb2cb7c2a..35bd384292 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -44,7 +44,6 @@ export interface IChannel { getSentMessage(event: VerificationEventTypes): any; getReceivedMessage(event: VerificationEventTypes): any; setStartMessage(content: any): void; - setOurDeviceId(id: string): void; cancelVerification(cancellationType: CancelReason): Promise; acceptMessage: any; startMessage: any; @@ -60,6 +59,7 @@ type Options = { clock: Clock; deviceMessageHandler: DeviceMessageHandler; log: ILogItem; + ourUserDeviceId: string; } export class ToDeviceChannel extends Disposables implements IChannel { @@ -88,6 +88,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { this.hsApi = options.hsApi; this.deviceTracker = options.deviceTracker; this.otherUserId = options.otherUserId; + this.ourDeviceId = options.ourUserDeviceId; this.clock = options.clock; this.log = options.log; this.deviceMessageHandler = options.deviceMessageHandler; @@ -279,10 +280,6 @@ export class ToDeviceChannel extends Disposables implements IChannel { return promise; } - setOurDeviceId(id: string) { - this.ourDeviceId = id; - } - setStartMessage(event) { this.startMessage = event; this._initiatedByUs = event.content.from_device === this.ourDeviceId; diff --git a/src/matrix/verification/SAS/channel/MockChannel.ts b/src/matrix/verification/SAS/channel/MockChannel.ts index a9cc4dea30..bb51662709 100644 --- a/src/matrix/verification/SAS/channel/MockChannel.ts +++ b/src/matrix/verification/SAS/channel/MockChannel.ts @@ -16,12 +16,12 @@ export class MockChannel implements ITestChannel { public startMessage: any; public isCancelled: boolean = false; private olmSas: any; - public ourUserDeviceId: string; constructor( public otherUserDeviceId: string, public otherUserId: string, public ourUserId: string, + public ourUserDeviceId: string, private fixtures: Map, private deviceTracker: any, public id: string, @@ -111,10 +111,6 @@ export class MockChannel implements ITestChannel { this.recalculateCommitment(); } - setOurDeviceId(id: string) { - this.ourUserDeviceId = id; - } - async cancelVerification(_: CancelReason): Promise { console.log("MockChannel.cancelVerification()"); this.isCancelled = true; From 589bc161f741d2aeba197ede3a80171bf1170432 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 19:33:41 +0530 Subject: [PATCH 082/168] Inherit from EventEmitter --- src/matrix/verification/SAS/SASVerification.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 3831d413c1..e105f5b2b6 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -46,21 +46,21 @@ type Options = { clock: Clock; } -export class SASVerification { +export class SASVerification extends EventEmitter { private startStage: BaseSASVerificationStage; private olmSas: Olm.SAS; public finished: boolean = false; public readonly channel: IChannel; private timeout: Timeout; - public readonly eventEmitter: EventEmitter = new EventEmitter(); constructor(options: Options) { + super(); const { olm, channel, clock } = options; const olmSas = new olm.SAS(); this.olmSas = olmSas; this.channel = channel; this.setupCancelAfterTimeout(clock); - const stageOptions = {...options, olmSas, eventEmitter: this.eventEmitter}; + const stageOptions = {...options, olmSas, eventEmitter: this}; if (channel.getReceivedMessage(VerificationEventTypes.Start)) { this.startStage = new SelectVerificationMethodStage(stageOptions); } @@ -188,7 +188,7 @@ export function tests() { }); // @ts-ignore channel.setOlmSas(sas.olmSas); - sas.eventEmitter.on("EmojiGenerated", async (stage) => { + sas.on("EmojiGenerated", async (stage) => { await stage?.setEmojiMatch(true); }); return { sas, clock, logger }; @@ -251,7 +251,7 @@ export function tests() { txnId, receivedMessages ); - sas.eventEmitter.on("SelectVerificationStage", (stage) => { + sas.on("SelectVerificationStage", (stage) => { logger.run("send start", async (log) => { await stage?.selectEmojiMethod(log); }); @@ -370,7 +370,7 @@ export function tests() { txnId, receivedMessages ); - sas.eventEmitter.on("SelectVerificationStage", (stage) => { + sas.on("SelectVerificationStage", (stage) => { logger.run("send start", async (log) => { await stage?.selectEmojiMethod(log); }); @@ -456,7 +456,7 @@ export function tests() { receivedMessages, startingMessage, ); - sas.eventEmitter.on("SelectVerificationStage", (stage) => { + sas.on("SelectVerificationStage", (stage) => { logger.run("send start", async (log) => { await stage?.selectEmojiMethod(log); }); @@ -499,7 +499,7 @@ export function tests() { txnId, receivedMessages ); - sas.eventEmitter.on("SelectVerificationStage", (stage) => { + sas.on("SelectVerificationStage", (stage) => { logger.run("send start", async (log) => { await stage?.selectEmojiMethod(log); }); From 7c6bcbc09cdd976bfdb6a74cea9fb75bdf08f8b2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 19:36:27 +0530 Subject: [PATCH 083/168] Add explaining comment --- .../verification/SAS/stages/SelectVerificationMethodStage.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index 78f6aaf9c8..f4cccfbf39 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -91,6 +91,11 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { message_authentication_codes: MAC_LIST, short_authentication_string: SAS_LIST, }; + /** + * Once we send the start event, we should eventually receive the accept message. + * This will cause the Promise.race in completeStage() to resolve and we'll move + * to the next stage (where we will send the key). + */ await this.channel.send(VerificationEventTypes.Start, content, log); this.hasSentStartMessage = true; } From 225a778d1a143d2459dd3c89a95496f9dd8a43b5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 20:25:19 +0530 Subject: [PATCH 084/168] Use deferred --- .../verification/SAS/channel/Channel.ts | 14 +++---- src/utils/Deferred.ts | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 src/utils/Deferred.ts diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 35bd384292..700bd569ab 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -23,6 +23,7 @@ import {makeTxnId} from "../../../common.js"; import {CancelReason, VerificationEventTypes} from "./types"; import {Disposables} from "../../../../utils/Disposables"; import {VerificationCancelledError} from "../VerificationCancelledError"; +import {Deferred} from "../../../../utils/Deferred"; const messageFromErrorType = { [CancelReason.UserCancelled]: "User declined", @@ -71,7 +72,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { private readonly deviceMessageHandler: DeviceMessageHandler; private readonly sentMessages: Map = new Map(); private readonly receivedMessages: Map = new Map(); - private readonly waitMap: Map}> = new Map(); + private readonly waitMap: Map> = new Map(); private readonly log: ILogItem; public otherUserDeviceId: string; public startMessage: any; @@ -270,14 +271,9 @@ export class ToDeviceChannel extends Disposables implements IChannel { if (existingWait) { return existingWait.promise; } - let resolve, reject; - // Add to wait map - const promise = new Promise((_resolve, _reject) => { - resolve = _resolve; - reject = _reject; - }); - this.waitMap.set(eventType, { resolve, reject, promise }); - return promise; + const deferred = new Deferred(); + this.waitMap.set(eventType, deferred); + return deferred.promise; } setStartMessage(event) { diff --git a/src/utils/Deferred.ts b/src/utils/Deferred.ts new file mode 100644 index 0000000000..051708e4e7 --- /dev/null +++ b/src/utils/Deferred.ts @@ -0,0 +1,40 @@ +/* +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. +*/ + +export class Deferred { + public readonly promise: Promise; + public readonly resolve: (value: T) => void; + public readonly reject: (err: Error) => void; + private _value?: T; + + constructor() { + let resolve; + let reject; + this.promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }) + this.resolve = (value: T) => { + this._value = value; + resolve(value); + }; + this.reject = reject; + } + + get value(): T | undefined { + return this._value; + } +} From ae60c30ab88078e8d093ec4f899760442fdec99c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 20:27:11 +0530 Subject: [PATCH 085/168] VerificationEventTypes -> VerificationEventType --- src/fixtures/matrix/sas/events.ts | 20 +++++------ src/matrix/verification/CrossSigning.ts | 6 ++-- .../verification/SAS/SASVerification.ts | 14 ++++---- .../verification/SAS/channel/Channel.ts | 34 +++++++++---------- .../verification/SAS/channel/MockChannel.ts | 20 +++++------ src/matrix/verification/SAS/channel/types.ts | 2 +- .../SAS/stages/CalculateSASStage.ts | 8 ++--- .../stages/SelectVerificationMethodStage.ts | 16 ++++----- .../SAS/stages/SendAcceptVerificationStage.ts | 6 ++-- .../verification/SAS/stages/SendDoneStage.ts | 4 +-- .../verification/SAS/stages/SendKeyStage.ts | 6 ++-- .../verification/SAS/stages/SendMacStage.ts | 6 ++-- .../verification/SAS/stages/SendReadyStage.ts | 4 +-- .../stages/SendRequestVerificationStage.ts | 6 ++-- .../verification/SAS/stages/VerifyMacStage.ts | 6 ++-- 15 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/fixtures/matrix/sas/events.ts b/src/fixtures/matrix/sas/events.ts index 753a6e97fb..b3c8228f8a 100644 --- a/src/fixtures/matrix/sas/events.ts +++ b/src/fixtures/matrix/sas/events.ts @@ -10,7 +10,7 @@ accept -> key -> mac -> done */ -import {VerificationEventTypes} from "../../../matrix/verification/SAS/channel/types"; +import {VerificationEventType} from "../../../matrix/verification/SAS/channel/types"; function generateResponses(userId: string, deviceId: string, txnId: string) { const readyMessage = { @@ -125,7 +125,7 @@ export class SASFixtures { return this; } - fixtures(): Map { + fixtures(): Map { const responses = generateResponses(this.userId, this.deviceId, this.txnId); const array: any[] = []; const addToArray = (type) => array.push([type, responses[type]]); @@ -134,14 +134,14 @@ export class SASFixtures { const item = this.order[i]; switch (item) { case COMBINATIONS.YOU_SENT_REQUEST: - addToArray(VerificationEventTypes.Ready); + addToArray(VerificationEventType.Ready); break; case COMBINATIONS.THEY_SENT_START: { - addToArray(VerificationEventTypes.Start); + addToArray(VerificationEventType.Start); const nextItem = this.order[i+1]; if (nextItem === COMBINATIONS.YOU_SENT_START) { if (this._youWinConflict) { - addToArray(VerificationEventTypes.Accept); + addToArray(VerificationEventType.Accept); i = i + 2; continue; } @@ -152,7 +152,7 @@ export class SASFixtures { const nextItem = this.order[i+1] if (nextItem === COMBINATIONS.THEY_SENT_START) { if (this._youWinConflict) { - addToArray(VerificationEventTypes.Accept); + addToArray(VerificationEventType.Accept); } break; @@ -160,15 +160,15 @@ export class SASFixtures { if (this.order[i-1] === COMBINATIONS.THEY_SENT_START) { break; } - addToArray(VerificationEventTypes.Accept); + addToArray(VerificationEventType.Accept); break; } } i = i + 1; } - addToArray(VerificationEventTypes.Key); - addToArray(VerificationEventTypes.Mac); - addToArray(VerificationEventTypes.Done); + addToArray(VerificationEventType.Key); + addToArray(VerificationEventType.Mac); + addToArray(VerificationEventType.Done); return new Map(array); } } diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 369487df60..dee5053dc2 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -27,7 +27,7 @@ import type {ISignatures} from "./common"; import {SASVerification} from "./SAS/SASVerification"; import {ToDeviceChannel} from "./SAS/channel/Channel"; import type {DeviceMessageHandler} from "../DeviceMessageHandler.js"; -import {VerificationEventTypes} from "./SAS/channel/types"; +import {VerificationEventType} from "./SAS/channel/types"; type Olm = typeof OlmNamespace; @@ -80,8 +80,8 @@ export class CrossSigning { )) { return; } - if (unencryptedEvent.type === VerificationEventTypes.Request || - unencryptedEvent.type === VerificationEventTypes.Start) { + if (unencryptedEvent.type === VerificationEventType.Request || + unencryptedEvent.type === VerificationEventType.Start) { await this.platform.logger.run("Start verification from request", async (log) => { const sas = this.startVerification(unencryptedEvent.sender, unencryptedEvent, log); await sas?.start(); diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index e105f5b2b6..551d196f48 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -21,7 +21,7 @@ import type {DeviceTracker} from "../../e2ee/DeviceTracker.js"; import type * as OlmNamespace from "@matrix-org/olm"; import {IChannel} from "./channel/Channel"; import {HomeServerApi} from "../../net/HomeServerApi"; -import {CancelReason, VerificationEventTypes} from "./channel/types"; +import {CancelReason, VerificationEventType} from "./channel/types"; import {SendReadyStage} from "./stages/SendReadyStage"; import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; import {VerificationCancelledError} from "./VerificationCancelledError"; @@ -61,10 +61,10 @@ export class SASVerification extends EventEmitter { this.channel = channel; this.setupCancelAfterTimeout(clock); const stageOptions = {...options, olmSas, eventEmitter: this}; - if (channel.getReceivedMessage(VerificationEventTypes.Start)) { + if (channel.getReceivedMessage(VerificationEventType.Start)) { this.startStage = new SelectVerificationMethodStage(stageOptions); } - else if (channel.getReceivedMessage(VerificationEventTypes.Request)) { + else if (channel.getReceivedMessage(VerificationEventType.Request)) { this.startStage = new SendReadyStage(stageOptions); } else { @@ -283,7 +283,7 @@ export function tests() { const receivedMessages = new SASFixtures(theirUserId, theirDeviceId, txnId) .theySentStart() .fixtures(); - const startingMessage = receivedMessages.get(VerificationEventTypes.Start); + const startingMessage = receivedMessages.get(VerificationEventType.Start); const { sas } = await createSASRequest( ourUserId, ourDeviceId, @@ -404,7 +404,7 @@ export function tests() { .youSentStart() .theyWinConflict() .fixtures(); - const startingMessage = receivedMessages.get(VerificationEventTypes.Start); + const startingMessage = receivedMessages.get(VerificationEventType.Start); console.log(receivedMessages); const { sas } = await createSASRequest( ourUserId, @@ -445,7 +445,7 @@ export function tests() { .youSentStart() .youWinConflict() .fixtures(); - const startingMessage = receivedMessages.get(VerificationEventTypes.Start); + const startingMessage = receivedMessages.get(VerificationEventType.Start); console.log(receivedMessages); const { sas, logger } = await createSASRequest( ourUserId, @@ -600,7 +600,7 @@ export function tests() { .youSentRequest() .theySentStart() .fixtures(); - receivedMessages.get(VerificationEventTypes.Start).content.key_agreement_protocols = ["foo"]; + receivedMessages.get(VerificationEventType.Start).content.key_agreement_protocols = ["foo"]; const { sas } = await createSASRequest( ourUserId, ourDeviceId, diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 700bd569ab..495352f3ab 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -20,7 +20,7 @@ import type {ILogItem} from "../../../../logging/types"; import type {Clock} from "../../../../platform/web/dom/Clock.js"; import type {DeviceMessageHandler} from "../../../DeviceMessageHandler.js"; import {makeTxnId} from "../../../common.js"; -import {CancelReason, VerificationEventTypes} from "./types"; +import {CancelReason, VerificationEventType} from "./types"; import {Disposables} from "../../../../utils/Disposables"; import {VerificationCancelledError} from "../VerificationCancelledError"; import {Deferred} from "../../../../utils/Deferred"; @@ -42,8 +42,8 @@ const messageFromErrorType = { export interface IChannel { send(eventType: string, content: any, log: ILogItem): Promise; waitForEvent(eventType: string): Promise; - getSentMessage(event: VerificationEventTypes): any; - getReceivedMessage(event: VerificationEventTypes): any; + getSentMessage(event: VerificationEventType): any; + getReceivedMessage(event: VerificationEventType): any; setStartMessage(content: any): void; cancelVerification(cancellationType: CancelReason): Promise; acceptMessage: any; @@ -70,8 +70,8 @@ export class ToDeviceChannel extends Disposables implements IChannel { private readonly otherUserId: string; private readonly clock: Clock; private readonly deviceMessageHandler: DeviceMessageHandler; - private readonly sentMessages: Map = new Map(); - private readonly receivedMessages: Map = new Map(); + private readonly sentMessages: Map = new Map(); + private readonly receivedMessages: Map = new Map(); private readonly waitMap: Map> = new Map(); private readonly log: ILogItem; public otherUserDeviceId: string; @@ -120,12 +120,12 @@ export class ToDeviceChannel extends Disposables implements IChannel { return this._isCancelled; } - async send(eventType: VerificationEventTypes, content: any, log: ILogItem): Promise { + async send(eventType: VerificationEventType, content: any, log: ILogItem): Promise { await log.wrap("ToDeviceChannel.send", async () => { if (this.isCancelled) { throw new VerificationCancelledError(); } - if (eventType === VerificationEventTypes.Request) { + if (eventType === VerificationEventType.Request) { // Handle this case specially await this.handleRequestEventSpecially(eventType, content, log); return; @@ -143,7 +143,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { }); } - private async handleRequestEventSpecially(eventType: VerificationEventTypes, content: any, log: ILogItem) { + private async handleRequestEventSpecially(eventType: VerificationEventType, content: any, log: ILogItem) { await log.wrap("ToDeviceChannel.handleRequestEventSpecially", async () => { const timestamp = this.clock.now(); const txnId = makeTxnId(); @@ -161,17 +161,17 @@ export class ToDeviceChannel extends Disposables implements IChannel { }); } - getReceivedMessage(event: VerificationEventTypes) { + getReceivedMessage(event: VerificationEventType) { return this.receivedMessages.get(event); } - getSentMessage(event: VerificationEventTypes) { + getSentMessage(event: VerificationEventType) { return this.sentMessages.get(event); } get acceptMessage(): any { - return this.receivedMessages.get(VerificationEventTypes.Accept) ?? - this.sentMessages.get(VerificationEventTypes.Accept); + return this.receivedMessages.get(VerificationEventType.Accept) ?? + this.sentMessages.get(VerificationEventType.Accept); } @@ -194,11 +194,11 @@ export class ToDeviceChannel extends Disposables implements IChannel { log.log({ l: "event", event }); this.resolveAnyWaits(event); this.receivedMessages.set(event.type, event); - if (event.type === VerificationEventTypes.Ready) { + if (event.type === VerificationEventType.Ready) { this.handleReadyMessage(event, log); return; } - if (event.type === VerificationEventTypes.Cancel) { + if (event.type === VerificationEventType.Cancel) { this._isCancelled = true; this.dispose(); return; @@ -223,7 +223,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { [this.otherUserId]: deviceMessages } } - await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, makeTxnId(), { log }).response(); + await this.hsApi.sendToDevice(VerificationEventType.Cancel, payload, makeTxnId(), { log }).response(); } async cancelVerification(cancellationType: CancelReason) { @@ -242,7 +242,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } } - await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, makeTxnId(), { log }).response(); + await this.hsApi.sendToDevice(VerificationEventType.Cancel, payload, makeTxnId(), { log }).response(); this._isCancelled = true; this.dispose(); }); @@ -257,7 +257,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } - waitForEvent(eventType: VerificationEventTypes): Promise { + waitForEvent(eventType: VerificationEventType): Promise { if (this._isCancelled) { throw new VerificationCancelledError(); } diff --git a/src/matrix/verification/SAS/channel/MockChannel.ts b/src/matrix/verification/SAS/channel/MockChannel.ts index bb51662709..6f7077909b 100644 --- a/src/matrix/verification/SAS/channel/MockChannel.ts +++ b/src/matrix/verification/SAS/channel/MockChannel.ts @@ -2,7 +2,7 @@ import type {ILogItem} from "../../../../lib"; import {createCalculateMAC} from "../mac"; import {VerificationCancelledError} from "../VerificationCancelledError"; import {IChannel} from "./Channel"; -import {CancelReason, VerificationEventTypes} from "./types"; +import {CancelReason, VerificationEventType} from "./types"; import anotherjson from "another-json"; interface ITestChannel extends IChannel { @@ -29,7 +29,7 @@ export class MockChannel implements ITestChannel { startingMessage?: any, ) { if (startingMessage) { - const eventType = startingMessage.content.method ? VerificationEventTypes.Start : VerificationEventTypes.Request; + const eventType = startingMessage.content.method ? VerificationEventType.Start : VerificationEventType.Request; this.id = startingMessage.content.transaction_id; this.receivedMessages.set(eventType, startingMessage); } @@ -54,10 +54,10 @@ export class MockChannel implements ITestChannel { else { await new Promise(() => {}); } - if (eventType === VerificationEventTypes.Mac) { + if (eventType === VerificationEventType.Mac) { await this.recalculateMAC(); } - if(eventType === VerificationEventTypes.Accept && this.startMessage) { + if(eventType === VerificationEventType.Accept && this.startMessage) { } return event; } @@ -68,7 +68,7 @@ export class MockChannel implements ITestChannel { return; } const {content} = this.startMessage; - const {content: keyMessage} = this.fixtures.get(VerificationEventTypes.Key); + const {content: keyMessage} = this.fixtures.get(VerificationEventType.Key); const key = keyMessage.key; const commitmentStr = key + anotherjson.stringify(content); const olmUtil = new this.olm.Utility(); @@ -86,7 +86,7 @@ export class MockChannel implements ITestChannel { this.ourUserId + this.ourUserDeviceId + this.id; - const { content: macContent } = this.receivedMessages.get(VerificationEventTypes.Mac); + const { content: macContent } = this.receivedMessages.get(VerificationEventType.Mac); const macMethod = this.acceptMessage.content.message_authentication_code; const calculateMac = createCalculateMAC(this.olmSas, macMethod); const input = Object.keys(macContent.mac).sort().join(","); @@ -117,15 +117,15 @@ export class MockChannel implements ITestChannel { } get acceptMessage(): any { - return this.receivedMessages.get(VerificationEventTypes.Accept) ?? - this.sentMessages.get(VerificationEventTypes.Accept); + return this.receivedMessages.get(VerificationEventType.Accept) ?? + this.sentMessages.get(VerificationEventType.Accept); } - getReceivedMessage(event: VerificationEventTypes) { + getReceivedMessage(event: VerificationEventType) { return this.receivedMessages.get(event); } - getSentMessage(event: VerificationEventTypes) { + getSentMessage(event: VerificationEventType) { return this.sentMessages.get(event); } diff --git a/src/matrix/verification/SAS/channel/types.ts b/src/matrix/verification/SAS/channel/types.ts index 400404b724..4a1483b7d3 100644 --- a/src/matrix/verification/SAS/channel/types.ts +++ b/src/matrix/verification/SAS/channel/types.ts @@ -1,4 +1,4 @@ -export const enum VerificationEventTypes { +export const enum VerificationEventType { Request = "m.key.verification.request", Ready = "m.key.verification.ready", Start = "m.key.verification.start", diff --git a/src/matrix/verification/SAS/stages/CalculateSASStage.ts b/src/matrix/verification/SAS/stages/CalculateSASStage.ts index fc509aeaf5..d602d18d9a 100644 --- a/src/matrix/verification/SAS/stages/CalculateSASStage.ts +++ b/src/matrix/verification/SAS/stages/CalculateSASStage.ts @@ -15,7 +15,7 @@ limitations under the License. */ import anotherjson from "another-json"; import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {CancelReason, VerificationEventTypes} from "../channel/types"; +import {CancelReason, VerificationEventType} from "../channel/types"; import {generateEmojiSas} from "../generator"; import {ILogItem} from "../../../../logging/types"; import {SendMacStage} from "./SendMacStage"; @@ -82,8 +82,8 @@ export class CalculateSASStage extends BaseSASVerificationStage { async verifyHashCommitment(log: ILogItem) { return await log.wrap("CalculateSASStage.verifyHashCommitment", async () => { - const acceptMessage = this.channel.getReceivedMessage(VerificationEventTypes.Accept).content; - const keyMessage = this.channel.getReceivedMessage(VerificationEventTypes.Key).content; + const acceptMessage = this.channel.getReceivedMessage(VerificationEventType.Accept).content; + const keyMessage = this.channel.getReceivedMessage(VerificationEventType.Key).content; const commitmentStr = keyMessage.key + anotherjson.stringify(this.channel.startMessage.content); const receivedCommitment = acceptMessage.commitment; const hash = this.olmUtil.sha256(commitmentStr); @@ -136,7 +136,7 @@ export class CalculateSASStage extends BaseSASVerificationStage { } get theirKey(): string { - const {content} = this.channel.getReceivedMessage(VerificationEventTypes.Key); + const {content} = this.channel.getReceivedMessage(VerificationEventType.Key); return content.key; } } diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index f4cccfbf39..da6099ee71 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {CancelReason, VerificationEventTypes} from "../channel/types"; +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"; @@ -27,8 +27,8 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SelectVerificationMethodStage.completeStage", async (log) => { this.eventEmitter.emit("SelectVerificationStage", this); - const startMessage = this.channel.waitForEvent(VerificationEventTypes.Start); - const acceptMessage = this.channel.waitForEvent(VerificationEventTypes.Accept); + const startMessage = this.channel.waitForEvent(VerificationEventType.Start); + const acceptMessage = this.channel.waitForEvent(VerificationEventType.Accept); const { content } = await Promise.race([startMessage, acceptMessage]); if (content.method) { // We received the start message @@ -37,12 +37,12 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { await this.resolveStartConflict(log); } else { - this.channel.setStartMessage(this.channel.getReceivedMessage(VerificationEventTypes.Start)); + this.channel.setStartMessage(this.channel.getReceivedMessage(VerificationEventType.Start)); } } else { // We received the accept message - this.channel.setStartMessage(this.channel.getSentMessage(VerificationEventTypes.Start)); + this.channel.setStartMessage(this.channel.getSentMessage(VerificationEventType.Start)); } if (this.channel.initiatedByUs) { await acceptMessage; @@ -57,8 +57,8 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { private async resolveStartConflict(log: ILogItem) { await log.wrap("resolveStartConflict", async () => { - const receivedStartMessage = this.channel.getReceivedMessage(VerificationEventTypes.Start); - const sentStartMessage = this.channel.getSentMessage(VerificationEventTypes.Start); + const receivedStartMessage = this.channel.getReceivedMessage(VerificationEventType.Start); + const sentStartMessage = this.channel.getSentMessage(VerificationEventType.Start); if (receivedStartMessage.content.method !== sentStartMessage.content.method) { /** * If the two m.key.verification.start messages do not specify the same verification method, @@ -96,7 +96,7 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { * This will cause the Promise.race in completeStage() to resolve and we'll move * to the next stage (where we will send the key). */ - await this.channel.send(VerificationEventTypes.Start, content, log); + await this.channel.send(VerificationEventType.Start, content, log); this.hasSentStartMessage = true; } } diff --git a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts index c69d41df1a..1606a53fff 100644 --- a/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendAcceptVerificationStage.ts @@ -16,7 +16,7 @@ limitations under the License. import anotherjson from "another-json"; import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {HASHES_LIST, MAC_LIST, SAS_SET, KEY_AGREEMENT_LIST} from "./constants"; -import {CancelReason, VerificationEventTypes} from "../channel/types"; +import {CancelReason, VerificationEventType} from "../channel/types"; import {SendKeyStage} from "./SendKeyStage"; // from element-web @@ -45,8 +45,8 @@ export class SendAcceptVerificationStage extends BaseSASVerificationStage { short_authentication_string: sasMethod, commitment: this.olmUtil.sha256(commitmentStr), }; - await this.channel.send(VerificationEventTypes.Accept, content, log); - await this.channel.waitForEvent(VerificationEventTypes.Key); + await this.channel.send(VerificationEventType.Accept, content, log); + await this.channel.waitForEvent(VerificationEventType.Key); this.setNextStage(new SendKeyStage(this.options)); }); } diff --git a/src/matrix/verification/SAS/stages/SendDoneStage.ts b/src/matrix/verification/SAS/stages/SendDoneStage.ts index 915a697ac2..2d3195b11a 100644 --- a/src/matrix/verification/SAS/stages/SendDoneStage.ts +++ b/src/matrix/verification/SAS/stages/SendDoneStage.ts @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {VerificationEventTypes} from "../channel/types"; +import {VerificationEventType} from "../channel/types"; export class SendDoneStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SendDoneStage.completeStage", async (log) => { - await this.channel.send(VerificationEventTypes.Done, {}, log); + await this.channel.send(VerificationEventType.Done, {}, log); }); } } diff --git a/src/matrix/verification/SAS/stages/SendKeyStage.ts b/src/matrix/verification/SAS/stages/SendKeyStage.ts index 0e00890b5d..4f9f45def0 100644 --- a/src/matrix/verification/SAS/stages/SendKeyStage.ts +++ b/src/matrix/verification/SAS/stages/SendKeyStage.ts @@ -14,21 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {VerificationEventTypes} from "../channel/types"; +import {VerificationEventType} from "../channel/types"; import {CalculateSASStage} from "./CalculateSASStage"; export class SendKeyStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SendKeyStage.completeStage", async (log) => { const ourSasKey = this.olmSAS.get_pubkey(); - await this.channel.send(VerificationEventTypes.Key, {key: ourSasKey}, log); + await this.channel.send(VerificationEventType.Key, {key: ourSasKey}, log); /** * We may have already got the key in SendAcceptVerificationStage, * in which case waitForEvent will return a resolved promise with * that content. Otherwise, waitForEvent will actually wait for the * key message. */ - await this.channel.waitForEvent(VerificationEventTypes.Key); + await this.channel.waitForEvent(VerificationEventType.Key); this.setNextStage(new CalculateSASStage(this.options)); }); } diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts index e80df19faa..b8b397807e 100644 --- a/src/matrix/verification/SAS/stages/SendMacStage.ts +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {ILogItem} from "../../../../logging/types"; -import {VerificationEventTypes} from "../channel/types"; +import {VerificationEventType} from "../channel/types"; import {createCalculateMAC} from "../mac"; import {VerifyMacStage} from "./VerifyMacStage"; @@ -26,7 +26,7 @@ export class SendMacStage extends BaseSASVerificationStage { const macMethod = acceptMessage.message_authentication_code; const calculateMAC = createCalculateMAC(this.olmSAS, macMethod); await this.sendMAC(calculateMAC, log); - await this.channel.waitForEvent(VerificationEventTypes.Mac); + await this.channel.waitForEvent(VerificationEventType.Mac); this.setNextStage(new VerifyMacStage(this.options)); }); } @@ -55,7 +55,7 @@ export class SendMacStage extends BaseSASVerificationStage { } const keys = calculateMAC(keyList.sort().join(","), baseInfo + "KEY_IDS"); - await this.channel.send(VerificationEventTypes.Mac, { mac, keys }, log); + await this.channel.send(VerificationEventType.Mac, { mac, keys }, log); } } diff --git a/src/matrix/verification/SAS/stages/SendReadyStage.ts b/src/matrix/verification/SAS/stages/SendReadyStage.ts index 78a2448c5f..c4fc0cc6a4 100644 --- a/src/matrix/verification/SAS/stages/SendReadyStage.ts +++ b/src/matrix/verification/SAS/stages/SendReadyStage.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; -import {VerificationEventTypes} from "../channel/types"; +import {VerificationEventType} from "../channel/types"; import {SelectVerificationMethodStage} from "./SelectVerificationMethodStage"; export class SendReadyStage extends BaseSASVerificationStage { @@ -24,7 +24,7 @@ export class SendReadyStage extends BaseSASVerificationStage { "from_device": this.ourUserDeviceId, "methods": ["m.sas.v1"], }; - await this.channel.send(VerificationEventTypes.Ready, content, log); + await this.channel.send(VerificationEventType.Ready, content, log); this.setNextStage(new SelectVerificationMethodStage(this.options)); }); } diff --git a/src/matrix/verification/SAS/stages/SendRequestVerificationStage.ts b/src/matrix/verification/SAS/stages/SendRequestVerificationStage.ts index f3ea42b04b..73c8019e90 100644 --- a/src/matrix/verification/SAS/stages/SendRequestVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/SendRequestVerificationStage.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {SelectVerificationMethodStage} from "./SelectVerificationMethodStage"; -import {VerificationEventTypes} from "../channel/types"; +import {VerificationEventType} from "../channel/types"; export class SendRequestVerificationStage extends BaseSASVerificationStage { async completeStage() { @@ -24,9 +24,9 @@ export class SendRequestVerificationStage extends BaseSASVerificationStage { "from_device": this.ourUserDeviceId, "methods": ["m.sas.v1"], }; - await this.channel.send(VerificationEventTypes.Request, content, log); + await this.channel.send(VerificationEventType.Request, content, log); this.setNextStage(new SelectVerificationMethodStage(this.options)); - await this.channel.waitForEvent(VerificationEventTypes.Ready); + await this.channel.waitForEvent(VerificationEventType.Ready); }); } } diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 8366abfc84..e19f695a35 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseSASVerificationStage} from "./BaseSASVerificationStage"; import {ILogItem} from "../../../../logging/types"; -import {CancelReason, VerificationEventTypes} from "../channel/types"; +import {CancelReason, VerificationEventType} from "../channel/types"; import {createCalculateMAC} from "../mac"; import {SendDoneStage} from "./SendDoneStage"; @@ -28,13 +28,13 @@ export class VerifyMacStage extends BaseSASVerificationStage { const macMethod = acceptMessage.message_authentication_code; const calculateMAC = createCalculateMAC(this.olmSAS, macMethod); await this.checkMAC(calculateMAC, log); - await this.channel.waitForEvent(VerificationEventTypes.Done); + await this.channel.waitForEvent(VerificationEventType.Done); this.setNextStage(new SendDoneStage(this.options)); }); } private async checkMAC(calculateMAC: (input: string, info: string) => string, log: ILogItem): Promise { - const {content} = this.channel.getReceivedMessage(VerificationEventTypes.Mac); + const {content} = this.channel.getReceivedMessage(VerificationEventType.Mac); const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this.otherUserId + From 8ea484e8627d3c0e234d1f7431a7c954a6e8ff94 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 20:30:48 +0530 Subject: [PATCH 086/168] Inline code --- .../verification/SAS/stages/CalculateSASStage.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/matrix/verification/SAS/stages/CalculateSASStage.ts b/src/matrix/verification/SAS/stages/CalculateSASStage.ts index d602d18d9a..4a991897ce 100644 --- a/src/matrix/verification/SAS/stages/CalculateSASStage.ts +++ b/src/matrix/verification/SAS/stages/CalculateSASStage.ts @@ -63,7 +63,7 @@ export class CalculateSASStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("CalculateSASStage.completeStage", async (log) => { // 1. Check the hash commitment - if (this.needsToVerifyHashCommitment && !await this.verifyHashCommitment(log)) { + if (this.channel.initiatedByUs && !await this.verifyHashCommitment(log)) { return; } // 2. Calculate the SAS @@ -96,15 +96,6 @@ export class CalculateSASStage extends BaseSASVerificationStage { }); } - private get needsToVerifyHashCommitment(): boolean { - if (this.channel.initiatedByUs) { - // If we sent the start message, we also received the accept message. - // The commitment is in the accept message, so we need to verify it. - return true; - } - return false; - } - private generateSASBytes(): Uint8Array { const keyAgreement = this.channel.acceptMessage.content.key_agreement_protocol; const otherUserDeviceId = this.otherUserDeviceId; From 2f7e67d48a0638b3830dd5a045c7568e0720c240 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 20:32:19 +0530 Subject: [PATCH 087/168] Change type --- src/matrix/verification/SAS/channel/Channel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 495352f3ab..8b45613d8a 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -40,8 +40,8 @@ const messageFromErrorType = { } export interface IChannel { - send(eventType: string, content: any, log: ILogItem): Promise; - waitForEvent(eventType: string): Promise; + send(eventType: VerificationEventType, content: any, log: ILogItem): Promise; + waitForEvent(eventType: VerificationEventType): Promise; getSentMessage(event: VerificationEventType): any; getReceivedMessage(event: VerificationEventType): any; setStartMessage(content: any): void; From 90faad551a5b67ace68f7917d7aa38d212e31dac Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 24 Mar 2023 23:07:22 +0100 Subject: [PATCH 088/168] remove txn argument that was removed in previous commit --- src/matrix/verification/CrossSigning.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 196190ae18..bd661e7d6c 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -210,11 +210,11 @@ export class CrossSigning { if (!this.isMasterKeyTrusted) { return UserTrust.OwnSetupError; } - const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, undefined, log)); + const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log)); if (!ourMSK) { return UserTrust.OwnSetupError; } - const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, undefined, log)); + const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, log)); if (!ourUSK) { return UserTrust.OwnSetupError; } @@ -222,7 +222,7 @@ export class CrossSigning { if (ourUSKVerification !== SignatureVerification.Valid) { return UserTrust.OwnSetupError; } - const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, undefined, log)); + const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log)); if (!theirMSK) { /* assume that when they don't have an MSK, they've never enabled cross-signing on their client (or it's not supported) rather than assuming a setup error on their side. @@ -237,7 +237,7 @@ export class CrossSigning { return UserTrust.UserSignatureMismatch; } } - const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, undefined, log)); + const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, log)); if (!theirSSK) { return UserTrust.UserSetupError; } From d170c6f7871cf86f504fe26e0f01799164eae01d Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 24 Mar 2023 23:10:54 +0100 Subject: [PATCH 089/168] crossSigning is an observable value now --- src/domain/session/rightpanel/MemberDetailsViewModel.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index 0eabf33a06..1a5dabd85c 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -30,6 +30,9 @@ export class MemberDetailsViewModel extends ViewModel { this._session = options.session; this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange())); this.track(this._observableMember.subscribe( () => this._onMemberChange())); + this.track(this._session.crossSigning.subscribe(() => { + this.emitChange("isTrusted"); + })); this._userTrust = undefined; this.init(); // TODO: call this from parent view model and do something smart with error view model if it fails async? } @@ -37,7 +40,7 @@ export class MemberDetailsViewModel extends ViewModel { async init() { if (this.features.crossSigning) { this._userTrust = await this.logger.run({l: "MemberDetailsViewModel.get user trust", id: this._member.userId}, log => { - return this._session.crossSigning.getUserTrust(this._member.userId, log); + return this._session.crossSigning.get()?.getUserTrust(this._member.userId, log); }); this.emitChange("isTrusted"); } From 9383246f8de6627a49a1e4651cb375203e78d507 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 24 Mar 2023 23:14:30 +0100 Subject: [PATCH 090/168] remove obsolete parameter here as well --- src/matrix/verification/CrossSigning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index bd661e7d6c..2be443e52b 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -182,7 +182,7 @@ export class CrossSigning { if (userId === this.ownUserId) { return; } - const keyToSign = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, undefined, log); + const keyToSign = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log); if (!keyToSign) { return; } From eaa7de8a551d77946c86288cbc540b27f0631231 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 24 Mar 2023 23:16:54 +0100 Subject: [PATCH 091/168] fix import --- src/matrix/verification/CrossSigning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index f98c469a48..2a4d21f29f 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ILogItem } from "../../lib"; +import {ILogItem} from "../../logging/types"; import {pkSign} from "./common"; import type {SecretStorage} from "../ssss/SecretStorage"; From 6abc918ce89c17cbc4d2654727250e55ab38c704 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 27 Mar 2023 10:54:44 +0200 Subject: [PATCH 092/168] show shield as icon --- .../rightpanel/MemberDetailsViewModel.js | 5 ++- src/platform/web/ui/css/right-panel.css | 31 +++++++++++++++++++ .../element/icons/verification-error.svg | 3 ++ .../ui/css/themes/element/icons/verified.svg | 3 ++ .../session/rightpanel/MemberDetailsView.js | 7 +++-- 5 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 src/platform/web/ui/css/themes/element/icons/verification-error.svg create mode 100644 src/platform/web/ui/css/themes/element/icons/verified.svg diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index 1a5dabd85c..d4e1151fa4 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -31,7 +31,7 @@ export class MemberDetailsViewModel extends ViewModel { this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange())); this.track(this._observableMember.subscribe( () => this._onMemberChange())); this.track(this._session.crossSigning.subscribe(() => { - this.emitChange("isTrusted"); + this.emitChange("trustShieldColor"); })); this._userTrust = undefined; this.init(); // TODO: call this from parent view model and do something smart with error view model if it fails async? @@ -42,13 +42,12 @@ export class MemberDetailsViewModel extends ViewModel { this._userTrust = await this.logger.run({l: "MemberDetailsViewModel.get user trust", id: this._member.userId}, log => { return this._session.crossSigning.get()?.getUserTrust(this._member.userId, log); }); - this.emitChange("isTrusted"); + this.emitChange("trustShieldColor"); } } get name() { return this._member.name; } get userId() { return this._member.userId; } - get isTrusted() { return this._userTrust === UserTrust.Trusted; } get trustDescription() { switch (this._userTrust) { case UserTrust.Trusted: return this.i18n`You have verified this user. This user has verified all of their sessions.`; diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index 92a89c0a1f..5af0e6a059 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -19,6 +19,37 @@ text-align: center; } +.MemberDetailsView_shield_container { + display: flex; + gap: 4px; +} + +.MemberDetailsView_shield_red, .MemberDetailsView_shield_green, .MemberDetailsView_shield_black { + background-size: contain; + background-repeat: no-repeat; + width: 24px; + height: 24px; + display: block; + flex-shrink: 0; +} + +.MemberDetailsView_shield_description { + flex-grow: 1; + margin: 0; +} + +.MemberDetailsView_shield_red { + background-image: url("./icons/verification-error.svg?primary=error-color"); +} + +.MemberDetailsView_shield_green { + background-image: url("./icons/verified.svg?primary=accent-color"); +} + +.MemberDetailsView_shield_black { + background-image: url("./icons/encryption-status.svg?primary=text-color"); +} + .RoomDetailsView_label, .RoomDetailsView_row, .RoomDetailsView, .MemberDetailsView, .EncryptionIconView { display: flex; align-items: center; diff --git a/src/platform/web/ui/css/themes/element/icons/verification-error.svg b/src/platform/web/ui/css/themes/element/icons/verification-error.svg new file mode 100644 index 0000000000..9733f563b8 --- /dev/null +++ b/src/platform/web/ui/css/themes/element/icons/verification-error.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/platform/web/ui/css/themes/element/icons/verified.svg b/src/platform/web/ui/css/themes/element/icons/verified.svg new file mode 100644 index 0000000000..340891f113 --- /dev/null +++ b/src/platform/web/ui/css/themes/element/icons/verified.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js index aa70d5b43b..c02d8d73fd 100644 --- a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js +++ b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js @@ -26,9 +26,10 @@ export class MemberDetailsView extends TemplateView { ] if (vm.features.crossSigning) { - securityNodes.push(t.p(vm => vm.isTrusted ? vm.i18n`This user is trusted` : vm.i18n`This user is not trusted`)); - securityNodes.push(t.p(vm => vm.trustDescription)); - securityNodes.push(t.p(["Shield color: ", vm => vm.trustShieldColor])); + securityNodes.push(t.div({className: "MemberDetailsView_shield_container"}, [ + t.span({className: vm => `MemberDetailsView_shield_${vm.trustShieldColor}`}), + t.p({className: "MemberDetailsView_shield_description"}, vm => vm.trustDescription) + ])); } return t.div({className: "MemberDetailsView"}, From 21729a60490af4debebe0903d1078503a043bdb1 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 27 Mar 2023 10:57:26 +0200 Subject: [PATCH 093/168] add newlines between getters --- src/domain/session/rightpanel/MemberDetailsViewModel.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index d4e1151fa4..b73bf4bb88 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -47,7 +47,9 @@ export class MemberDetailsViewModel extends ViewModel { } get name() { return this._member.name; } + get userId() { return this._member.userId; } + get trustDescription() { switch (this._userTrust) { case UserTrust.Trusted: return this.i18n`You have verified this user. This user has verified all of their sessions.`; @@ -60,6 +62,7 @@ export class MemberDetailsViewModel extends ViewModel { default: return this.i18n`Pendingโ€ฆ`; } } + get trustShieldColor() { if (!this._isEncrypted) { return undefined; @@ -78,7 +81,9 @@ export class MemberDetailsViewModel extends ViewModel { } get type() { return "member-details"; } + get shouldShowBackButton() { return true; } + get previousSegmentName() { return "members"; } get role() { From 22140614ec3ec864bba6e23e030b50d19f3482dd Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 27 Mar 2023 11:06:30 +0200 Subject: [PATCH 094/168] clear cross-signing object when disabling 4s --- src/matrix/Session.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 655552865e..4acdd56a79 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -252,7 +252,9 @@ export class Session { this._keyBackup.get().dispose(); this._keyBackup.set(undefined); } - // TODO: stop cross-signing + if (this._crossSigning.get()) { + 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 @@ -313,9 +315,11 @@ export class Session { } } this._keyBackup.get().dispose(); - this._keyBackup.set(null); + this._keyBackup.set(undefined); + } + if (this._crossSigning.get()) { + this._crossSigning.set(undefined); } - // TODO: stop cross-signing } _tryLoadSecretStorage(ssssKey, log) { From acba597e8bfefb49d066dfdca129cecbaf356b88 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 27 Mar 2023 15:31:46 -0500 Subject: [PATCH 095/168] Label magic number --- src/matrix/room/PowerLevels.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/PowerLevels.js b/src/matrix/room/PowerLevels.js index 63a5b0b0a1..aefbfaf154 100644 --- a/src/matrix/room/PowerLevels.js +++ b/src/matrix/room/PowerLevels.js @@ -16,6 +16,9 @@ limitations under the License. export const EVENT_TYPE = "m.room.power_levels"; +// See https://spec.matrix.org/latest/client-server-api/#mroompower_levels +const STATE_DEFAULT_POWER_LEVEL = 50; + export class PowerLevels { constructor({powerLevelEvent, createEvent, ownUserId, membership}) { this._plEvent = powerLevelEvent; @@ -70,8 +73,7 @@ export class PowerLevels { if (typeof level === "number") { return level; } else { - // TODO: Why does this default to 50? - return 50; + return STATE_DEFAULT_POWER_LEVEL; } } From 98d4dfd8e6aa77760a53ac11c5544f6f5af42482 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 27 Mar 2023 15:37:28 -0500 Subject: [PATCH 096/168] Move copy function to platform --- src/domain/session/room/timeline/tiles/BaseMessageTile.js | 3 +-- src/platform/web/Platform.js | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 3412b3f95c..90da4bffa3 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -17,7 +17,6 @@ limitations under the License. import {SimpleTile} from "./SimpleTile.js"; import {ReactionsViewModel} from "../ReactionsViewModel.js"; import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar"; -import {copyPlaintext} from "../../../../../platform/web/dom/utils"; export class BaseMessageTile extends SimpleTile { @@ -47,7 +46,7 @@ export class BaseMessageTile extends SimpleTile { } copyPermalink() { - copyPlaintext(this.permaLink); + this.platform.copyPlaintext(this.permaLink); } get senderProfileLink() { diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index be8c997078..e2f722e6dd 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -43,6 +43,7 @@ import {MediaDevicesWrapper} from "./dom/MediaDevices"; import {DOMWebRTC} from "./dom/WebRTC"; import {ThemeLoader} from "./theming/ThemeLoader"; import {TimeFormatter} from "./dom/TimeFormatter"; +import {copyPlaintext} from "./dom/utils"; function addScript(src) { return new Promise(function (resolve, reject) { @@ -283,6 +284,10 @@ export class Platform { } } + async copyPlaintext(text) { + return await copyPlaintext(text); + } + restart() { document.location.reload(); } From 10c92c56f59cb5193f235eb128816dcb4373fd95 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Mar 2023 12:58:23 +0530 Subject: [PATCH 097/168] Fix tests and code to use new data structure --- src/matrix/verification/SAS/SASVerification.ts | 18 ++++++++++++++---- src/matrix/verification/SAS/channel/Channel.ts | 4 ++-- .../verification/SAS/channel/MockChannel.ts | 8 +++++--- .../verification/SAS/stages/SendMacStage.ts | 8 +++++++- .../verification/SAS/stages/VerifyMacStage.ts | 11 +++++++++-- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 551d196f48..5850ba5d73 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -145,16 +145,25 @@ export function tests() { }, }; const deviceTracker = { - getCrossSigningKeysForUser: (userId, _hsApi, _) => { + getCrossSigningKeyForUser: (userId, __, _hsApi, _) => { let masterKey = userId === ourUserId ? "5HIrEawRiiQioViNfezPDWfPWH2pdaw3pbQNHEVN2jM" : "Ot8Y58PueQ7hJVpYWAJkg2qaREJAY/UhGZYOrsd52oo"; - return { masterKey }; + return { + user_id: userId, + usage: ["master"], + keys: { + [`ed25519:${masterKey}`]: masterKey, + } + }; }, - deviceForId: (_userId, _deviceId, _hsApi, _log) => { + deviceForId: (_userId, deviceId, _hsApi, _log) => { return { - ed25519Key: "D8w9mrokGdEZPdPgrU0kQkYi4vZyzKEBfvGyZsGK7+Q", + device_id: deviceId, + keys: { + [`ed25519:${deviceId}`]: "D8w9mrokGdEZPdPgrU0kQkYi4vZyzKEBfvGyZsGK7+Q", + } }; }, }; @@ -177,6 +186,7 @@ export function tests() { channel, clock, hsApi, + // @ts-ignore deviceTracker, e2eeAccount, olm, diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 8b45613d8a..6492a6aea1 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -211,13 +211,13 @@ export class ToDeviceChannel extends Disposables implements IChannel { this.otherUserDeviceId = fromDevice; // We need to send cancel messages to all other devices const devices = await this.deviceTracker.devicesForUsers([this.otherUserId], this.hsApi, log); - const otherDevices = devices.filter(device => device.deviceId !== fromDevice && device.deviceId !== this.ourDeviceId); + const otherDevices = devices.filter(device => device.device_id !== fromDevice && device.device_id !== this.ourDeviceId); const cancelMessage = { code: CancelReason.OtherDeviceAccepted, reason: messageFromErrorType[CancelReason.OtherDeviceAccepted], transaction_id: this.id, }; - const deviceMessages = otherDevices.reduce((acc, device) => { acc[device.deviceId] = cancelMessage; return acc; }, {}); + const deviceMessages = otherDevices.reduce((acc, device) => { acc[device.device_id] = cancelMessage; return acc; }, {}); const payload = { messages: { [this.otherUserId]: deviceMessages diff --git a/src/matrix/verification/SAS/channel/MockChannel.ts b/src/matrix/verification/SAS/channel/MockChannel.ts index 6f7077909b..50197ba4bc 100644 --- a/src/matrix/verification/SAS/channel/MockChannel.ts +++ b/src/matrix/verification/SAS/channel/MockChannel.ts @@ -3,6 +3,8 @@ import {createCalculateMAC} from "../mac"; import {VerificationCancelledError} from "../VerificationCancelledError"; import {IChannel} from "./Channel"; import {CancelReason, VerificationEventType} from "./types"; +import {getKeyEd25519Key} from "../../CrossSigning"; +import {getDeviceEd25519Key} from "../../../e2ee/common"; import anotherjson from "another-json"; interface ITestChannel extends IChannel { @@ -96,10 +98,11 @@ export class MockChannel implements ITestChannel { const deviceId = keyId.split(":", 2)[1]; const device = await this.deviceTracker.deviceForId(this.otherUserDeviceId, deviceId); if (device) { - macContent.mac[keyId] = calculateMac(device.ed25519Key, baseInfo + keyId); + macContent.mac[keyId] = calculateMac(getDeviceEd25519Key(device), baseInfo + keyId); } else { - const {masterKey} = await this.deviceTracker.getCrossSigningKeysForUser(this.otherUserId); + const key = await this.deviceTracker.getCrossSigningKeyForUser(this.otherUserId); + const masterKey = getKeyEd25519Key(key)!; macContent.mac[keyId] = calculateMac(masterKey, baseInfo + keyId); } } @@ -112,7 +115,6 @@ export class MockChannel implements ITestChannel { } async cancelVerification(_: CancelReason): Promise { - console.log("MockChannel.cancelVerification()"); this.isCancelled = true; } diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts index b8b397807e..14384d3a25 100644 --- a/src/matrix/verification/SAS/stages/SendMacStage.ts +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -18,6 +18,7 @@ import {ILogItem} from "../../../../logging/types"; import {VerificationEventType} from "../channel/types"; import {createCalculateMAC} from "../mac"; import {VerifyMacStage} from "./VerifyMacStage"; +import {getKeyEd25519Key, KeyUsage} from "../../CrossSigning"; export class SendMacStage extends BaseSASVerificationStage { async completeStage() { @@ -47,7 +48,12 @@ export class SendMacStage extends BaseSASVerificationStage { mac[deviceKeyId] = calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId); keyList.push(deviceKeyId); - const {masterKey: crossSigningKey} = await this.deviceTracker.getCrossSigningKeysForUser(this.ourUserId, this.hsApi, log); + const key = await this.deviceTracker.getCrossSigningKeyForUser(this.ourUserId, KeyUsage.Master, this.hsApi, log); + if (!key) { + log.log({ l: "Fetching msk failed", userId: this.ourUserId }); + throw new Error("Fetching MSK for user failed!"); + } + const crossSigningKey = getKeyEd25519Key(key); if (crossSigningKey) { const crossSigningKeyId = `ed25519:${crossSigningKey}`; mac[crossSigningKeyId] = calculateMAC(crossSigningKey, baseInfo + crossSigningKeyId); diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index e19f695a35..40e908c6a2 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -18,6 +18,8 @@ import {ILogItem} from "../../../../logging/types"; import {CancelReason, VerificationEventType} from "../channel/types"; import {createCalculateMAC} from "../mac"; import {SendDoneStage} from "./SendDoneStage"; +import {KeyUsage, getKeyEd25519Key} from "../../CrossSigning"; +import {getDeviceEd25519Key} from "../../../e2ee/common"; export type KeyVerifier = (keyId: string, device: any, keyInfo: string) => void; @@ -66,11 +68,16 @@ export class VerifyMacStage extends BaseSASVerificationStage { const deviceIdOrMSK = keyId.split(":", 2)[1]; const device = await this.deviceTracker.deviceForId(userId, deviceIdOrMSK, this.hsApi, log); if (device) { - verifier(keyId, device.ed25519Key, keyInfo); + verifier(keyId, getDeviceEd25519Key(device), keyInfo); // todo: mark device as verified here } else { // If we were not able to find the device, then deviceIdOrMSK is actually the MSK! - const {masterKey} = await this.deviceTracker.getCrossSigningKeysForUser(userId, this.hsApi, log); + const key = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log); + if (!key) { + log.log({ l: "Fetching msk failed", userId }); + throw new Error("Fetching MSK for user failed!"); + } + const masterKey = getKeyEd25519Key(key); verifier(keyId, masterKey, keyInfo); // todo: mark user as verified here } From 1c923a720bc77b8fcb4da4b1179f207dcf9be67e Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 28 Mar 2023 11:33:59 +0200 Subject: [PATCH 098/168] fix login not working --- src/matrix/Session.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 4acdd56a79..b999ba6b1e 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -496,7 +496,9 @@ export class Session { olmWorker: this._olmWorker, txn }); - log.set("keys", this._e2eeAccount.identityKeys); + if (this._e2eeAccount) { + log.set("keys", this._e2eeAccount.identityKeys); + } this._setupEncryption(); } const pendingEventsByRoomId = await this._getPendingEventsByRoom(txn); From 3b17dc60b5cd938a4352efbfb108c6a6af451df2 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 28 Mar 2023 11:48:36 +0200 Subject: [PATCH 099/168] fix not being able to switch to passphrase mode anymore for key backup --- .../session/settings/KeyBackupViewModel.ts | 24 ++++++++----------- .../session/settings/KeyBackupSettingsView.ts | 10 ++------ 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.ts b/src/domain/session/settings/KeyBackupViewModel.ts index 3426191bd1..663d1ea6f8 100644 --- a/src/domain/session/settings/KeyBackupViewModel.ts +++ b/src/domain/session/settings/KeyBackupViewModel.ts @@ -26,7 +26,8 @@ import type {CrossSigning} from "../../../matrix/verification/CrossSigning"; export enum Status { Enabled, - Setup, + SetupWithPassphrase, + SetupWithRecoveryKey, Pending, NewVersionAvailable }; @@ -108,7 +109,10 @@ export class KeyBackupViewModel extends ViewModel { return keyBackup.needsNewKey ? Status.NewVersionAvailable : Status.Enabled; } } else { - return Status.Setup; + switch (this._setupKeyType) { + case KeyType.RecoveryKey: return Status.SetupWithRecoveryKey; + case KeyType.Passphrase: return Status.SetupWithPassphrase; + } } } @@ -179,21 +183,13 @@ export class KeyBackupViewModel extends ViewModel { } showPhraseSetup(): void { - if (this._status === Status.Setup) { - this._setupKeyType = KeyType.Passphrase; - this.emitChange("setupKeyType"); - } + this._setupKeyType = KeyType.Passphrase; + this.emitChange("status"); } showKeySetup(): void { - if (this._status === Status.Setup) { - this._setupKeyType = KeyType.Passphrase; - this.emitChange("setupKeyType"); - } - } - - get setupKeyType(): KeyType { - return this._setupKeyType; + this._setupKeyType = KeyType.RecoveryKey; + this.emitChange("status"); } private async _enterCredentials(keyType, credential, setupDehydratedDevice): Promise { diff --git a/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts b/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts index 28c4febf27..668f4f17e2 100644 --- a/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts @@ -27,14 +27,8 @@ export class KeyBackupSettingsView extends TemplateView { switch (status) { case Status.Enabled: return renderEnabled(t, vm); case Status.NewVersionAvailable: return renderNewVersionAvailable(t, vm); - case Status.Setup: { - if (vm.setupKeyType === KeyType.Passphrase) { - return renderEnableFromPhrase(t, vm); - } else { - return renderEnableFromKey(t, vm); - } - break; - } + case Status.SetupWithPassphrase: return renderEnableFromPhrase(t, vm); + case Status.SetupWithRecoveryKey: return renderEnableFromKey(t, vm); case Status.Pending: return t.p(vm.i18n`Waiting to go onlineโ€ฆ`); } }), From 58f73630b638140b07d923e13a4c42024213c739 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 28 Mar 2023 12:16:20 +0200 Subject: [PATCH 100/168] fix crossSigning never getting enabled if you haven't fetched your own keys yet --- src/matrix/verification/CrossSigning.ts | 32 ++++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index fc4589ef1a..83ac3f05e5 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -65,6 +65,13 @@ export enum UserTrust { OwnSetupError } +enum MSKVerification { + NoPrivKey, + NoPubKey, + DerivedPubKeyMismatch, + Valid +} + export class CrossSigning { private readonly storage: Storage; private readonly secretStorage: SecretStorage; @@ -99,24 +106,27 @@ export class CrossSigning { this.e2eeAccount = options.e2eeAccount } - async load(log: ILogItem) { + /** @return {boolean} whether cross signing has been enabled on this account */ + async load(log: ILogItem): Promise { // try to verify the msk without accessing the network - return await this.verifyMSKFrom4S(false, log); + const verification = await this.verifyMSKFrom4S(false, log); + return verification !== MSKVerification.NoPrivKey; } - async start(log: ILogItem) { + async start(log: ILogItem): Promise { if (!this.isMasterKeyTrusted) { // try to verify the msk _with_ access to the network - return await this.verifyMSKFrom4S(true, log); + await this.verifyMSKFrom4S(true, log); } } - private async verifyMSKFrom4S(allowNetwork: boolean, log: ILogItem): Promise { + private async verifyMSKFrom4S(allowNetwork: boolean, log: ILogItem): Promise { return await log.wrap("CrossSigning.verifyMSKFrom4S", async log => { // TODO: use errorboundary here const privateMasterKey = await this.getSigningKey(KeyUsage.Master); if (!privateMasterKey) { - return false; + log.set("failure", "no_priv_msk"); + return MSKVerification.NoPrivKey; } const signing = new this.olm.PkSigning(); let derivedPublicKey; @@ -127,13 +137,17 @@ export class CrossSigning { } const publishedMasterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, allowNetwork ? this.hsApi : undefined, log); if (!publishedMasterKey) { - return false; + log.set("failure", "no_pub_msk"); + return MSKVerification.NoPubKey; } const publisedEd25519Key = publishedMasterKey && getKeyEd25519Key(publishedMasterKey); log.set({publishedMasterKey: publisedEd25519Key, derivedPublicKey}); this._isMasterKeyTrusted = !!publisedEd25519Key && publisedEd25519Key === derivedPublicKey; - log.set("isMasterKeyTrusted", this.isMasterKeyTrusted); - return this.isMasterKeyTrusted; + if (!this._isMasterKeyTrusted) { + return MSKVerification.DerivedPubKeyMismatch; + log.set("failure", "mismatch"); + } + return MSKVerification.Valid; }); } From ac9c2443150502cf13a88bb4a6b881093ea351cb Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 28 Mar 2023 12:39:55 +0200 Subject: [PATCH 101/168] fix logging after return --- src/matrix/verification/CrossSigning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 83ac3f05e5..7ba65c1c12 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -144,8 +144,8 @@ export class CrossSigning { log.set({publishedMasterKey: publisedEd25519Key, derivedPublicKey}); this._isMasterKeyTrusted = !!publisedEd25519Key && publisedEd25519Key === derivedPublicKey; if (!this._isMasterKeyTrusted) { - return MSKVerification.DerivedPubKeyMismatch; log.set("failure", "mismatch"); + return MSKVerification.DerivedPubKeyMismatch; } return MSKVerification.Valid; }); From 30c0da3cd7d93647619c412e4a6d3555efe81ebd Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 28 Mar 2023 12:40:49 +0200 Subject: [PATCH 102/168] expand all parents of item that has an error --- src/logging/ConsoleReporter.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/logging/ConsoleReporter.ts b/src/logging/ConsoleReporter.ts index 328b4c239f..9a43a123c5 100644 --- a/src/logging/ConsoleReporter.ts +++ b/src/logging/ConsoleReporter.ts @@ -49,12 +49,26 @@ function filterValues(values: LogItemValues): LogItemValues | null { }, null); } +function hasChildWithError(item: LogItem): boolean { + if (item.error) { + return true; + } + if (item.children) { + for(const c of item.children) { + if (hasChildWithError(c)) { + return true; + } + } + } + return false; +} + function printToConsole(item: LogItem): void { const label = `${itemCaption(item)} (@${item.start}ms, duration: ${item.duration}ms)`; const filteredValues = filterValues(item.values); const shouldGroup = item.children || filteredValues; if (shouldGroup) { - if (item.error) { + if (hasChildWithError(item)) { console.group(label); } else { console.groupCollapsed(label); From cc4da5c7a729fa692a182edc4df36854d8e8ec40 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 28 Mar 2023 18:14:09 +0200 Subject: [PATCH 103/168] fix ts errors with latest tsc 4.x version (as used on CI) --- src/matrix/calls/TurnServerSource.ts | 2 ++ .../e2ee/megolm/decryption/KeyLoader.ts | 2 +- .../megolm/decryption/SessionDecryption.ts | 4 ++- src/platform/web/dom/MediaDevices.ts | 2 +- src/platform/web/ui/general/TemplateView.ts | 20 ++++++------ yarn.lock | 32 ++++++++++--------- 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/matrix/calls/TurnServerSource.ts b/src/matrix/calls/TurnServerSource.ts index a9163349ca..dd8e8f549d 100644 --- a/src/matrix/calls/TurnServerSource.ts +++ b/src/matrix/calls/TurnServerSource.ts @@ -152,6 +152,8 @@ function toIceServer(settings: TurnServerSettings): RTCIceServer { urls: settings.uris, username: settings.username, credential: settings.password, + // @ts-ignore + // this field is deprecated but providing it nonetheless credentialType: "password" } } diff --git a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts index 884203a34a..ee3996aae6 100644 --- a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts +++ b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts @@ -243,7 +243,7 @@ export function tests() { get keySource(): KeySource { return KeySource.DeviceMessage; } loadInto(session: Olm.InboundGroupSession) { - const mockSession = session as MockInboundSession; + const mockSession = session as unknown as MockInboundSession; mockSession.sessionId = this.sessionId; mockSession.firstKnownIndex = this._firstKnownIndex; } diff --git a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts index 72af718cdb..508e625834 100644 --- a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts +++ b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts @@ -58,7 +58,9 @@ export class SessionDecryption { this.decryptionRequests!.push(request); decryptionResult = await request.response(); } else { - decryptionResult = session.decrypt(ciphertext) as OlmDecryptionResult; + // the return type of Olm.InboundGroupSession::decrypt is likely wrong, message_index is a number and not a string AFAIK + // getting it fixed upstream but fixing it like this for now. + decryptionResult = session.decrypt(ciphertext) as unknown as OlmDecryptionResult; } const {plaintext} = decryptionResult!; let payload; diff --git a/src/platform/web/dom/MediaDevices.ts b/src/platform/web/dom/MediaDevices.ts index d6439faa11..04d3e8e4ca 100644 --- a/src/platform/web/dom/MediaDevices.ts +++ b/src/platform/web/dom/MediaDevices.ts @@ -65,7 +65,7 @@ export class MediaDevicesWrapper implements IMediaDevices { }; } - private getScreenshareContraints(): DisplayMediaStreamConstraints { + private getScreenshareContraints(): MediaStreamConstraints { return { audio: false, video: true, diff --git a/src/platform/web/ui/general/TemplateView.ts b/src/platform/web/ui/general/TemplateView.ts index 3b65ed9cd1..68a6fa4e8e 100644 --- a/src/platform/web/ui/general/TemplateView.ts +++ b/src/platform/web/ui/general/TemplateView.ts @@ -29,17 +29,17 @@ function objHasFns(obj: ClassNames): obj is { [className: string]: bool return false; } -export type RenderFn = (t: Builder, vm: T) => ViewNode; -type TextBinding = (T) => string | number | boolean | undefined | null; -type Child = NonBoundChild | TextBinding; -type Children = Child | Child[]; +export type RenderFn = (t: Builder, vm: T) => ViewNode; +type TextBinding = (T) => string | number | boolean | undefined | null; +type Child = NonBoundChild | TextBinding; +type Children = Child | Child[]; type EventHandler = ((event: Event) => void); type AttributeStaticValue = string | boolean; -type AttributeBinding = (value: T) => AttributeStaticValue; -export type AttrValue = AttributeStaticValue | AttributeBinding | EventHandler | ClassNames; -export type Attributes = { [attribute: string]: AttrValue }; -type ElementFn = (attributes?: Attributes | Children, children?: Children) => Element; -export type Builder = TemplateBuilder & { [tagName in typeof TAG_NAMES[string][number]]: ElementFn }; +type AttributeBinding = (value: T) => AttributeStaticValue; +export type AttrValue = AttributeStaticValue | AttributeBinding | EventHandler | ClassNames; +export type Attributes = { [attribute: string]: AttrValue }; +type ElementFn = (attributes?: Attributes | Children, children?: Children) => Element; +export type Builder = TemplateBuilder & { [tagName in typeof TAG_NAMES[string][number]]: ElementFn }; /** Bindable template. Renders once, and allows bindings for given nodes. If you need @@ -394,7 +394,7 @@ for (const [ns, tags] of Object.entries(TAG_NAMES)) { } } -export class InlineTemplateView extends TemplateView { +export class InlineTemplateView extends TemplateView { private _render: RenderFn; constructor(value: T, render: RenderFn) { diff --git a/yarn.lock b/yarn.lock index 876917a8d9..0db770d2e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -83,12 +83,14 @@ fastq "^1.6.0" "@playwright/test@^1.27.1": - version "1.27.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.27.1.tgz#9364d1e02021261211c8ff586d903faa79ce95c4" - integrity sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A== + version "1.32.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.32.1.tgz#749c9791adb048c266277a39ba0f7e33fe593ffe" + integrity sha512-FTwjCuhlm1qHUGf4hWjfr64UMJD/z0hXYbk+O387Ioe6WdyZQ+0TBDAc6P+pHjx2xCv1VYNgrKbYrNixFWy4Dg== dependencies: "@types/node" "*" - playwright-core "1.27.1" + playwright-core "1.32.1" + optionalDependencies: + fsevents "2.3.2" "@trysound/sax@0.2.0": version "0.2.0" @@ -101,9 +103,9 @@ integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== "@types/node@*": - version "18.7.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.13.tgz#23e6c5168333480d454243378b69e861ab5c011a" - integrity sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw== + version "18.15.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.10.tgz#4ee2171c3306a185d1208dad5f44dae3dee4cfe3" + integrity sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ== "@typescript-eslint/eslint-plugin@^4.29.2": version "4.29.2" @@ -1003,7 +1005,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@~2.3.2: +fsevents@2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -1382,10 +1384,10 @@ picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== -playwright-core@1.27.1: - version "1.27.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.27.1.tgz#840ef662e55a3ed759d8b5d3d00a5f885a7184f4" - integrity sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q== +playwright-core@1.32.1: + version "1.32.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.32.1.tgz#5a10c32403323b07d75ea428ebeed866a80b76a1" + integrity sha512-KZYUQC10mXD2Am1rGlidaalNGYk3LU1vZqqNk0gT4XPty1jOqgup8KDP8l2CUlqoNKhXM5IfGjWgW37xvGllBA== postcss-css-variables@^0.18.0: version "0.18.0" @@ -1689,9 +1691,9 @@ type-fest@^0.20.2: integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== typescript@^4.7.0: - version "4.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" - integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== typeson-registry@^1.0.0-alpha.20: version "1.0.0-alpha.39" From 6c294b1ab1a7078936ea677698309dab4e150686 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 28 Mar 2023 23:32:54 +0200 Subject: [PATCH 104/168] fix wrong import path that crept in merge again --- src/matrix/verification/CrossSigning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index a00edbf7d7..5046a2f953 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -22,7 +22,7 @@ import type {Platform} from "../../platform/web/Platform"; import type {DeviceTracker} from "../e2ee/DeviceTracker"; import type {HomeServerApi} from "../net/HomeServerApi"; import type {Account} from "../e2ee/Account"; -import {ILogItem} from "../../lib"; +import type {ILogItem} from "../../logging/types"; import {pkSign} from "./common"; import {SASVerification} from "./SAS/SASVerification"; import {ToDeviceChannel} from "./SAS/channel/Channel"; From c92fd6069d0b5d190999c77c7281b8e786a9bd28 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 28 Mar 2023 23:33:53 +0200 Subject: [PATCH 105/168] group imports and import types --- src/matrix/verification/CrossSigning.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 5046a2f953..ba90bc3b10 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -15,7 +15,10 @@ limitations under the License. */ import {verifyEd25519Signature, SignatureVerification} from "../e2ee/common"; - +import {pkSign} from "./common"; +import {SASVerification} from "./SAS/SASVerification"; +import {ToDeviceChannel} from "./SAS/channel/Channel"; +import {VerificationEventType} from "./SAS/channel/types"; import type {SecretStorage} from "../ssss/SecretStorage"; import type {Storage} from "../storage/idb/Storage"; import type {Platform} from "../../platform/web/Platform"; @@ -23,14 +26,9 @@ import type {DeviceTracker} from "../e2ee/DeviceTracker"; import type {HomeServerApi} from "../net/HomeServerApi"; import type {Account} from "../e2ee/Account"; import type {ILogItem} from "../../logging/types"; -import {pkSign} from "./common"; -import {SASVerification} from "./SAS/SASVerification"; -import {ToDeviceChannel} from "./SAS/channel/Channel"; import type {DeviceMessageHandler} from "../DeviceMessageHandler.js"; -import {VerificationEventType} from "./SAS/channel/types"; import type {SignedValue, DeviceKey} from "../e2ee/common"; import type * as OlmNamespace from "@matrix-org/olm"; - type Olm = typeof OlmNamespace; // we store cross-signing (and device) keys in the format we get them from the server From 0f7ef6912fc9d8c4fc3d87a1b33df4f78350c91d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Mar 2023 20:56:06 +0530 Subject: [PATCH 106/168] WIP: Add views/view-models --- src/domain/navigation/index.ts | 4 +- src/domain/session/SessionViewModel.js | 25 +++++- .../session/settings/KeyBackupViewModel.ts | 4 + .../DeviceVerificationViewModel.ts | 89 +++++++++++++++++++ .../stages/SelectMethodViewModel.ts | 50 +++++++++++ .../stages/VerificationCancelledViewModel.ts | 38 ++++++++ .../stages/VerificationCompleteViewModel.ts | 35 ++++++++ .../stages/VerifyEmojisViewModel.ts | 42 +++++++++ .../stages/WaitingForOtherUserViewModel.ts | 29 ++++++ .../verification/SAS/SASVerification.ts | 7 ++ .../verification/SAS/channel/Channel.ts | 24 +++-- .../verification/SAS/channel/MockChannel.ts | 1 + .../stages/SelectVerificationMethodStage.ts | 9 ++ .../verification/SAS/stages/SendDoneStage.ts | 3 +- src/matrix/verification/SAS/types.ts | 3 + .../ui/css/themes/element/icons/verified.svg | 2 +- .../web/ui/css/themes/element/theme.css | 88 ++++++++++++++++++ src/platform/web/ui/session/SessionView.js | 3 + .../session/settings/KeyBackupSettingsView.ts | 23 +++-- .../verification/DeviceVerificationView.ts | 57 ++++++++++++ .../verification/stages/SelectMethodView.ts | 62 +++++++++++++ .../stages/VerificationCancelledView.ts | 81 +++++++++++++++++ .../stages/VerificationCompleteView.ts | 45 ++++++++++ .../verification/stages/VerifyEmojisView.ts | 79 ++++++++++++++++ .../stages/WaitingForOtherUserView.ts | 46 ++++++++++ 25 files changed, 831 insertions(+), 18 deletions(-) create mode 100644 src/domain/session/verification/DeviceVerificationViewModel.ts create mode 100644 src/domain/session/verification/stages/SelectMethodViewModel.ts create mode 100644 src/domain/session/verification/stages/VerificationCancelledViewModel.ts create mode 100644 src/domain/session/verification/stages/VerificationCompleteViewModel.ts create mode 100644 src/domain/session/verification/stages/VerifyEmojisViewModel.ts create mode 100644 src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts create mode 100644 src/platform/web/ui/session/verification/DeviceVerificationView.ts create mode 100644 src/platform/web/ui/session/verification/stages/SelectMethodView.ts create mode 100644 src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts create mode 100644 src/platform/web/ui/session/verification/stages/VerificationCompleteView.ts create mode 100644 src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts create mode 100644 src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.ts diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index a2705944f7..c8f62a7bf4 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -34,6 +34,8 @@ export type SegmentType = { "details": true; "members": true; "member": string; + "device-verification": true; + "join-room": true; }; export function createNavigation(): Navigation { @@ -51,7 +53,7 @@ function allowsChild(parent: Segment | undefined, child: Segment { + this._updateVerification(verificationOpen); + })); + this._updateVerification(verification.get()); + const lightbox = this.navigation.observe("lightbox"); this.track(lightbox.subscribe(eventId => { this._updateLightbox(eventId); @@ -143,7 +151,8 @@ export class SessionViewModel extends ViewModel { this._gridViewModel || this._settingsViewModel || this._createRoomViewModel || - this._joinRoomViewModel + this._joinRoomViewModel || + this._verificationViewModel ); } @@ -179,6 +188,10 @@ export class SessionViewModel extends ViewModel { return this._joinRoomViewModel; } + get verificationViewModel() { + return this._verificationViewModel; + } + get toastCollectionViewModel() { return this._toastCollectionViewModel; } @@ -327,6 +340,16 @@ export class SessionViewModel extends ViewModel { this.emitChange("activeMiddleViewModel"); } + _updateVerification(verificationOpen) { + if (this._verificationViewModel) { + this._verificationViewModel = this.disposeTracked(this._verificationViewModel); + } + if (verificationOpen) { + this._verificationViewModel = this.track(new DeviceVerificationViewModel(this.childOptions({ session: this._client.session }))); + } + this.emitChange("activeMiddleViewModel"); + } + _updateLightbox(eventId) { if (this._lightboxViewModel) { this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel); diff --git a/src/domain/session/settings/KeyBackupViewModel.ts b/src/domain/session/settings/KeyBackupViewModel.ts index 663d1ea6f8..43681a29b2 100644 --- a/src/domain/session/settings/KeyBackupViewModel.ts +++ b/src/domain/session/settings/KeyBackupViewModel.ts @@ -157,6 +157,10 @@ export class KeyBackupViewModel extends ViewModel { } } + navigateToVerification(): void { + this.navigation.push("device-verification", true); + } + get backupWriteStatus(): BackupWriteStatus { const keyBackup = this._keyBackup; if (!keyBackup || keyBackup.version === undefined) { diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts new file mode 100644 index 0000000000..2c471ca57a --- /dev/null +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -0,0 +1,89 @@ +/* +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 {Options as BaseOptions} from "../../ViewModel"; +import {SegmentType} from "../../navigation/index"; +import {ErrorReportViewModel} from "../../ErrorReportViewModel"; +import {WaitingForOtherUserViewModel} from "./stages/WaitingForOtherUserViewModel"; +import {VerificationCancelledViewModel} from "./stages/VerificationCancelledViewModel"; +import {SelectMethodViewModel} from "./stages/SelectMethodViewModel"; +import {VerifyEmojisViewModel} from "./stages/VerifyEmojisViewModel"; +import {VerificationCompleteViewModel} from "./stages/VerificationCompleteViewModel"; +import type {Session} from "../../../matrix/Session.js"; +import type {SASVerification} from "../../../matrix/verification/SAS/SASVerification"; + +type Options = BaseOptions & { + session: Session; +}; + +export class DeviceVerificationViewModel extends ErrorReportViewModel { + private session: Session; + private sas: SASVerification; + private _currentStageViewModel: any; + + constructor(options: Readonly) { + super(options); + this.session = options.session; + this.createAndStartSasVerification(); + this._currentStageViewModel = this.track( + new WaitingForOtherUserViewModel( + this.childOptions({ sas: this.sas }) + ) + ); + } + + async createAndStartSasVerification(): Promise { + await this.logAndCatch("DeviceVerificationViewModel.createAndStartSasVerification", (log) => { + // todo: can crossSigning be undefined? + const crossSigning = this.session.crossSigning; + // todo: should be called createSasVerification + this.sas = crossSigning.startVerification(this.session.userId, undefined, log); + const emitter = this.sas.eventEmitter; + this.track(emitter.disposableOn("SelectVerificationStage", (stage) => { + this.createViewModelAndEmit( + new SelectMethodViewModel(this.childOptions({ sas: this.sas, stage: stage!, })) + ); + })); + this.track(emitter.disposableOn("EmojiGenerated", (stage) => { + this.createViewModelAndEmit( + new VerifyEmojisViewModel(this.childOptions({ stage: stage!, })) + ); + })); + this.track(emitter.disposableOn("VerificationCancelled", (cancellation) => { + this.createViewModelAndEmit( + new VerificationCancelledViewModel( + this.childOptions({ cancellationCode: cancellation!.code, cancelledByUs: cancellation!.cancelledByUs, }) + )); + })); + this.track(emitter.disposableOn("VerificationCompleted", (deviceId) => { + this.createViewModelAndEmit( + new VerificationCompleteViewModel(this.childOptions({ deviceId: deviceId! })) + ); + })); + return this.sas.start(); + }); + } + + private createViewModelAndEmit(vm) { + this._currentStageViewModel = this.disposeTracked(this._currentStageViewModel); + this._currentStageViewModel = this.track(vm); + this.emitChange("currentStageViewModel"); + } + + get currentStageViewModel() { + return this._currentStageViewModel; + } +} diff --git a/src/domain/session/verification/stages/SelectMethodViewModel.ts b/src/domain/session/verification/stages/SelectMethodViewModel.ts new file mode 100644 index 0000000000..681a2e468f --- /dev/null +++ b/src/domain/session/verification/stages/SelectMethodViewModel.ts @@ -0,0 +1,50 @@ +/* +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 {SegmentType} from "../../../navigation/index"; +import {ErrorReportViewModel} from "../../../ErrorReportViewModel"; +import type {Options as BaseOptions} from "../../../ViewModel"; +import type {Session} from "../../../../matrix/Session.js"; +import type {SASVerification} from "../../../../matrix/verification/SAS/SASVerification"; +import type {SelectVerificationMethodStage} from "../../../../matrix/verification/SAS/stages/SelectVerificationMethodStage"; + +type Options = BaseOptions & { + sas: SASVerification; + stage: SelectVerificationMethodStage; + session: Session; +}; + +export class SelectMethodViewModel extends ErrorReportViewModel { + public hasProceeded: boolean = false; + + async proceed() { + await this.logAndCatch("SelectMethodViewModel.proceed", async (log) => { + await this.options.stage.selectEmojiMethod(log); + this.hasProceeded = true; + this.emitChange("hasProceeded"); + }); + } + + async cancel() { + await this.logAndCatch("SelectMethodViewModel.cancel", async () => { + await this.options.sas.abort(); + }); + } + + get deviceName() { + return this.options.stage.otherDeviceName; + } +} diff --git a/src/domain/session/verification/stages/VerificationCancelledViewModel.ts b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts new file mode 100644 index 0000000000..ad01d31203 --- /dev/null +++ b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts @@ -0,0 +1,38 @@ +/* +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 as BaseOptions} from "../../../ViewModel"; +import {SegmentType} from "../../../navigation/index"; +import {CancelTypes} from "../../../../matrix/verification/SAS/channel/types"; + +type Options = BaseOptions & { + cancellationCode: CancelTypes; + cancelledByUs: boolean; +}; + +export class VerificationCancelledViewModel extends ViewModel { + get cancelCode(): CancelTypes { + return this.options.cancellationCode; + } + + get isCancelledByUs(): boolean { + return this.options.cancelledByUs; + } + + gotoSettings() { + this.navigation.push("settings", true); + } +} diff --git a/src/domain/session/verification/stages/VerificationCompleteViewModel.ts b/src/domain/session/verification/stages/VerificationCompleteViewModel.ts new file mode 100644 index 0000000000..a6982eb91e --- /dev/null +++ b/src/domain/session/verification/stages/VerificationCompleteViewModel.ts @@ -0,0 +1,35 @@ +/* +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 {SegmentType} from "../../../navigation/index"; +import {ErrorReportViewModel} from "../../../ErrorReportViewModel"; +import type {Options as BaseOptions} from "../../../ViewModel"; +import type {Session} from "../../../../matrix/Session.js"; + +type Options = BaseOptions & { + deviceId: string; + session: Session; +}; + +export class VerificationCompleteViewModel extends ErrorReportViewModel { + get otherDeviceId(): string { + return this.options.deviceId; + } + + gotoSettings() { + this.navigation.push("settings", true); + } +} diff --git a/src/domain/session/verification/stages/VerifyEmojisViewModel.ts b/src/domain/session/verification/stages/VerifyEmojisViewModel.ts new file mode 100644 index 0000000000..14868176ab --- /dev/null +++ b/src/domain/session/verification/stages/VerifyEmojisViewModel.ts @@ -0,0 +1,42 @@ +/* +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 {SegmentType} from "../../../navigation/index"; +import {ErrorReportViewModel} from "../../../ErrorReportViewModel"; +import type {Options as BaseOptions} from "../../../ViewModel"; +import type {Session} from "../../../../matrix/Session.js"; +import type {CalculateSASStage} from "../../../../matrix/verification/SAS/stages/CalculateSASStage"; + +type Options = BaseOptions & { + stage: CalculateSASStage; + session: Session; +}; + +export class VerifyEmojisViewModel extends ErrorReportViewModel { + public isWaiting: boolean = false; + + async setEmojiMatch(match: boolean) { + await this.logAndCatch("VerifyEmojisViewModel.setEmojiMatch", async () => { + await this.options.stage.setEmojiMatch(match); + this.isWaiting = true; + this.emitChange("isWaiting"); + }); + } + + get emojis() { + return this.options.stage.emoji; + } +} diff --git a/src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts b/src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts new file mode 100644 index 0000000000..408ef88448 --- /dev/null +++ b/src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts @@ -0,0 +1,29 @@ +/* +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 as BaseOptions} from "../../../ViewModel"; +import {SegmentType} from "../../../navigation/index"; +import type {SASVerification} from "../../../../matrix/verification/SAS/SASVerification"; + +type Options = BaseOptions & { + sas: SASVerification; +}; + +export class WaitingForOtherUserViewModel extends ViewModel { + async cancel() { + await this.options.sas.abort(); + } +} diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 5850ba5d73..42a7e47d3a 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -84,6 +84,10 @@ export class SASVerification extends EventEmitter { } } + async abort() { + await this.channel.cancelVerification(CancelTypes.UserCancelled); + } + async start() { try { let stage = this.startStage; @@ -98,6 +102,9 @@ export class SASVerification extends EventEmitter { } } finally { + if (this.channel.isCancelled) { + this.eventEmitter.emit("VerificationCancelled", this.channel.cancellation); + } this.olmSas.free(); this.timeout.abort(); this.finished = true; diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 6492a6aea1..9763b95c04 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -49,6 +49,8 @@ export interface IChannel { acceptMessage: any; startMessage: any; initiatedByUs: boolean; + isCancelled: boolean; + cancellation: { code: CancelTypes, cancelledByUs: boolean }; id: string; otherUserDeviceId: string; } @@ -78,7 +80,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { public startMessage: any; public id: string; private _initiatedByUs: boolean; - private _isCancelled = false; + private _cancellation: { code: CancelTypes, cancelledByUs: boolean }; /** * @@ -116,8 +118,12 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } + get cancellation() { + return this._cancellation; + }; + get isCancelled(): boolean { - return this._isCancelled; + return !!this._cancellation; } async send(eventType: VerificationEventType, content: any, log: ILogItem): Promise { @@ -198,8 +204,8 @@ export class ToDeviceChannel extends Disposables implements IChannel { this.handleReadyMessage(event, log); return; } - if (event.type === VerificationEventType.Cancel) { - this._isCancelled = true; + if (event.type === VerificationEventTypes.Cancel) { + this._cancellation = { code: event.content.code, cancelledByUs: false }; this.dispose(); return; } @@ -234,7 +240,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { const payload = { messages: { [this.otherUserId]: { - [this.otherUserDeviceId]: { + [this.otherUserDeviceId ?? "*"]: { code: cancellationType, reason: messageFromErrorType[cancellationType], transaction_id: this.id, @@ -242,8 +248,8 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } } - await this.hsApi.sendToDevice(VerificationEventType.Cancel, payload, makeTxnId(), { log }).response(); - this._isCancelled = true; + await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, makeTxnId(), { log }).response(); + this._cancellation = { code: cancellationType, cancelledByUs: true }; this.dispose(); }); } @@ -257,8 +263,8 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } - waitForEvent(eventType: VerificationEventType): Promise { - if (this._isCancelled) { + waitForEvent(eventType: VerificationEventTypes): Promise { + if (this.isCancelled) { throw new VerificationCancelledError(); } // Check if we already received the message diff --git a/src/matrix/verification/SAS/channel/MockChannel.ts b/src/matrix/verification/SAS/channel/MockChannel.ts index 50197ba4bc..9553a92d8b 100644 --- a/src/matrix/verification/SAS/channel/MockChannel.ts +++ b/src/matrix/verification/SAS/channel/MockChannel.ts @@ -17,6 +17,7 @@ export class MockChannel implements ITestChannel { public initiatedByUs: boolean; public startMessage: any; public isCancelled: boolean = false; + public cancellation: { code: CancelTypes; cancelledByUs: boolean; }; private olmSas: any; constructor( diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index da6099ee71..db499d2ec8 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -23,9 +23,11 @@ import type {ILogItem} from "../../../../logging/types"; export class SelectVerificationMethodStage extends BaseSASVerificationStage { private hasSentStartMessage = false; private allowSelection = true; + public otherDeviceName: string; async completeStage() { await this.log.wrap("SelectVerificationMethodStage.completeStage", async (log) => { + await this.findDeviceName(log); this.eventEmitter.emit("SelectVerificationStage", this); const startMessage = this.channel.waitForEvent(VerificationEventType.Start); const acceptMessage = this.channel.waitForEvent(VerificationEventType.Accept); @@ -81,6 +83,13 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { }); } + private async findDeviceName(log: ILogItem) { + await log.wrap("SelectVerificationMethodStage.findDeviceName", async () => { + const device = await this.options.deviceTracker.deviceForId(this.otherUserId, this.otherUserDeviceId, this.options.hsApi, log); + this.otherDeviceName = device.displayName; + }) + } + async selectEmojiMethod(log: ILogItem) { if (!this.allowSelection) { return; } const content = { diff --git a/src/matrix/verification/SAS/stages/SendDoneStage.ts b/src/matrix/verification/SAS/stages/SendDoneStage.ts index 2d3195b11a..53b8a37e58 100644 --- a/src/matrix/verification/SAS/stages/SendDoneStage.ts +++ b/src/matrix/verification/SAS/stages/SendDoneStage.ts @@ -19,7 +19,8 @@ import {VerificationEventType} from "../channel/types"; export class SendDoneStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SendDoneStage.completeStage", async (log) => { - await this.channel.send(VerificationEventType.Done, {}, log); + this.eventEmitter.emit("VerificationCompleted", this.otherUserDeviceId); + await this.channel.send(VerificationEventTypes.Done, {}, log); }); } } diff --git a/src/matrix/verification/SAS/types.ts b/src/matrix/verification/SAS/types.ts index d7be6921db..3bfd742ec9 100644 --- a/src/matrix/verification/SAS/types.ts +++ b/src/matrix/verification/SAS/types.ts @@ -13,10 +13,13 @@ 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 {CancelTypes} from "./channel/types"; import {CalculateSASStage} from "./stages/CalculateSASStage"; import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; export type SASProgressEvents = { SelectVerificationStage: SelectVerificationMethodStage; EmojiGenerated: CalculateSASStage; + VerificationCompleted: string; + VerificationCancelled: { code: CancelTypes, cancelledByUs: boolean }; } diff --git a/src/platform/web/ui/css/themes/element/icons/verified.svg b/src/platform/web/ui/css/themes/element/icons/verified.svg index 340891f113..d158e607d8 100644 --- a/src/platform/web/ui/css/themes/element/icons/verified.svg +++ b/src/platform/web/ui/css/themes/element/icons/verified.svg @@ -1,3 +1,3 @@ - \ No newline at end of file + diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index ca64e15a12..3f2c19e31b 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1354,3 +1354,91 @@ button.RoomDetailsView_row::after { width: 100px; height: 40px; } + +.VerificationCompleteView, +.DeviceVerificationView, +.SelectMethodView { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.VerificationCompleteView__heading, +.VerifyEmojisView__heading, +.SelectMethodView__heading, +.WaitingForOtherUserView__heading { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + justify-content: center; + padding: 8px; +} + +.VerificationCompleteView>*, +.SelectMethodView>*, +.VerifyEmojisView>*, +.WaitingForOtherUserView>* { + padding: 16px; +} + +.VerificationCompleteView__title, +.VerifyEmojisView__title, +.SelectMethodView__title, +.WaitingForOtherUserView__title { + text-align: center; + margin: 0; +} + +.VerificationCancelledView__description, +.VerificationCompleteView__description, +.VerifyEmojisView__description, +.SelectMethodView__description, +.WaitingForOtherUserView__description { + text-align: center; + margin: 0; +} + +.VerificationCancelledView__actions, +.SelectMethodView__actions, +.VerifyEmojisView__actions, +.WaitingForOtherUserView__actions { + display: flex; + justify-content: center; + gap: 12px; + padding: 16px; +} + +.EmojiCollection { + display: flex; + justify-content: center; + gap: 16px; +} + +.EmojiContainer__emoji { + font-size: 3.2rem; +} + +.VerifyEmojisView__waiting, +.EmojiContainer__name, +.EmojiContainer__emoji { + display: flex; + justify-content: center; + align-items: center; +} + +.EmojiContainer__name { + font-weight: bold; +} + +.VerifyEmojisView__waiting { + gap: 12px; +} + +.VerificationCompleteView__icon { + background: url("./icons//verified.svg?primary=accent-color") no-repeat; + background-size: contain; + width: 128px; + height: 128px; +} diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index 9f84e872ad..8156085cc5 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -30,6 +30,7 @@ import {CreateRoomView} from "./CreateRoomView.js"; import {RightPanelView} from "./rightpanel/RightPanelView.js"; import {viewClassForTile} from "./room/common"; import {JoinRoomView} from "./JoinRoomView"; +import {DeviceVerificationView} from "./verification/DeviceVerificationView"; import {ToastCollectionView} from "./toast/ToastCollectionView"; export class SessionView extends TemplateView { @@ -53,6 +54,8 @@ export class SessionView extends TemplateView { return new CreateRoomView(vm.createRoomViewModel); } else if (vm.joinRoomViewModel) { return new JoinRoomView(vm.joinRoomViewModel); + } else if (vm.verificationViewModel) { + return new DeviceVerificationView(vm.verificationViewModel); } else if (vm.currentRoomViewModel) { if (vm.currentRoomViewModel.kind === "invite") { return new InviteView(vm.currentRoomViewModel); diff --git a/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts b/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts index 668f4f17e2..7c3d64914f 100644 --- a/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts @@ -62,11 +62,24 @@ export class KeyBackupSettingsView extends TemplateView { return t.p("Cross-signing master key found and trusted.") }), t.if(vm => vm.canSignOwnDevice, t => { - return t.button({ - onClick: disableTargetCallback(async () => { - await vm.signOwnDevice(); - }) - }, "Sign own device"); + return t.div([ + t.button( + { + onClick: disableTargetCallback(async (evt) => { + await vm.signOwnDevice(); + }), + }, + "Sign own device" + ), + t.button( + { + onClick: disableTargetCallback(async () => { + vm.navigateToVerification(); + }), + }, + "Verify by emoji" + ), + ]); }), ]); diff --git a/src/platform/web/ui/session/verification/DeviceVerificationView.ts b/src/platform/web/ui/session/verification/DeviceVerificationView.ts new file mode 100644 index 0000000000..ed599031b5 --- /dev/null +++ b/src/platform/web/ui/session/verification/DeviceVerificationView.ts @@ -0,0 +1,57 @@ +/* +Copyright 2022 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 {TemplateView} from "../../general/TemplateView"; +import {WaitingForOtherUserViewModel} from "../../../../../domain/session/verification/stages/WaitingForOtherUserViewModel"; +import {DeviceVerificationViewModel} from "../../../../../domain/session/verification/DeviceVerificationViewModel"; +import {VerificationCancelledViewModel} from "../../../../../domain/session/verification/stages/VerificationCancelledViewModel"; +import {WaitingForOtherUserView} from "./stages/WaitingForOtherUserView"; +import {VerificationCancelledView} from "./stages/VerificationCancelledView"; +import {SelectMethodViewModel} from "../../../../../domain/session/verification/stages/SelectMethodViewModel"; +import {SelectMethodView} from "./stages/SelectMethodView"; +import {VerifyEmojisViewModel} from "../../../../../domain/session/verification/stages/VerifyEmojisViewModel"; +import {VerifyEmojisView} from "./stages/VerifyEmojisView"; +import {VerificationCompleteViewModel} from "../../../../../domain/session/verification/stages/VerificationCompleteViewModel"; +import {VerificationCompleteView} from "./stages/VerificationCompleteView"; + +export class DeviceVerificationView extends TemplateView { + render(t, vm) { + return t.div({ + className: { + "middle": true, + "DeviceVerificationView": true, + } + }, [ + t.mapView(vm => vm.currentStageViewModel, (stageVm) => { + if (stageVm instanceof WaitingForOtherUserViewModel) { + return new WaitingForOtherUserView(stageVm); + } + else if (stageVm instanceof VerificationCancelledViewModel) { + return new VerificationCancelledView(stageVm); + } + else if (stageVm instanceof SelectMethodViewModel) { + return new SelectMethodView(stageVm); + } + else if (stageVm instanceof VerifyEmojisViewModel) { + return new VerifyEmojisView(stageVm); + } + else if (stageVm instanceof VerificationCompleteViewModel) { + return new VerificationCompleteView(stageVm); + } + }) + ]) + } +} diff --git a/src/platform/web/ui/session/verification/stages/SelectMethodView.ts b/src/platform/web/ui/session/verification/stages/SelectMethodView.ts new file mode 100644 index 0000000000..9e665f312d --- /dev/null +++ b/src/platform/web/ui/session/verification/stages/SelectMethodView.ts @@ -0,0 +1,62 @@ +/* +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 {TemplateView} from "../../../general/TemplateView"; +import {spinner} from "../../../common.js" +import type {SelectMethodViewModel} from "../../../../../../domain/session/verification/stages/SelectMethodViewModel"; + +export class SelectMethodView extends TemplateView { + render(t) { + return t.div({ className: "SelectMethodView" }, [ + t.map(vm => vm.hasProceeded, (hasProceeded, t, vm) => { + if (hasProceeded) { + return spinner(t); + } + else return t.div([ + t.div({ className: "SelectMethodView__heading" }, [ + t.h2( { className: "SelectMethodView__title" }, vm.i18n`Verify device '${vm.deviceName}' by comparing emojis?`), + ]), + t.p({ className: "SelectMethodView__description" }, + vm.i18n`You are about to verify your other device by comparing emojis.` + ), + t.div({ className: "SelectMethodView__actions" }, [ + t.button( + { + className: { + "button-action": true, + primary: true, + destructive: true, + }, + onclick: () => vm.cancel(), + }, + "Cancel" + ), + t.button( + { + className: { + "button-action": true, + primary: true, + }, + onclick: () => vm.proceed(), + }, + "Proceed" + ), + ]), + ]); + }), + ]); + } +} diff --git a/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts b/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts new file mode 100644 index 0000000000..d2afa98d8f --- /dev/null +++ b/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts @@ -0,0 +1,81 @@ +/* +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 {TemplateView} from "../../../general/TemplateView"; +import {VerificationCancelledViewModel} from "../../../../../../domain/session/verification/stages/VerificationCancelledViewModel"; +import {CancelTypes} from "../../../../../../matrix/verification/SAS/channel/types"; + +export class VerificationCancelledView extends TemplateView { + render(t, vm: VerificationCancelledViewModel) { + const headerTextStart = vm.isCancelledByUs ? "You" : "The other device"; + + return t.div( + { + className: "VerificationCancelledView", + }, + [ + t.h2( + { className: "VerificationCancelledView__title" }, + vm.i18n`${headerTextStart} cancelled the verification!` + ), + t.p( + { className: "VerificationCancelledView__description" }, + vm.i18n`${this.getDescriptionFromCancellationCode(vm.cancelCode, vm.isCancelledByUs)}` + ), + t.div({ className: "VerificationCancelledView__actions" }, [ + t.button({ + className: { + "button-action": true, + "primary": true, + }, + onclick: () => vm.gotoSettings(), + }, "Got it") + ]), + ] + ); + } + + getDescriptionFromCancellationCode(code: CancelTypes, isCancelledByUs: boolean): string { + const descriptionsWhenWeCancelled = { + // [CancelTypes.UserCancelled]: NO_NEED_FOR_DESCRIPTION_HERE + [CancelTypes.InvalidMessage]: "You other device sent an invalid message.", + [CancelTypes.KeyMismatch]: "The key could not be verified.", + // [CancelTypes.OtherDeviceAccepted]: "Another device has accepted this request.", + [CancelTypes.TimedOut]: "The verification process timed out.", + [CancelTypes.UnexpectedMessage]: "Your other device sent an unexpected message.", + [CancelTypes.UnknownMethod]: "Your other device is using an unknown method for verification.", + [CancelTypes.UnknownTransaction]: "Your other device sent a message with an unknown transaction id.", + [CancelTypes.UserMismatch]: "The expected user did not match the user verified.", + [CancelTypes.MismatchedCommitment]: "The hash commitment does not match.", + [CancelTypes.MismatchedSAS]: "The emoji/decimal did not match.", + } + const descriptionsWhenTheyCancelled = { + [CancelTypes.UserCancelled]: "Your other device cancelled the verification!", + [CancelTypes.InvalidMessage]: "Invalid message sent to the other device.", + [CancelTypes.KeyMismatch]: "The other device could not verify our keys", + // [CancelTypes.OtherDeviceAccepted]: "Another device has accepted this request.", + [CancelTypes.TimedOut]: "The verification process timed out.", + [CancelTypes.UnexpectedMessage]: "Unexpected message sent to the other device.", + [CancelTypes.UnknownMethod]: "Your other device does not understand the method you chose", + [CancelTypes.UnknownTransaction]: "Your other device rejected our message.", + [CancelTypes.UserMismatch]: "The expected user did not match the user verified.", + [CancelTypes.MismatchedCommitment]: "Your other device was not able to verify the hash commitment", + [CancelTypes.MismatchedSAS]: "The emoji/decimal did not match.", + } + const map = isCancelledByUs ? descriptionsWhenWeCancelled : descriptionsWhenTheyCancelled; + return map[code] ?? ""; + } +} diff --git a/src/platform/web/ui/session/verification/stages/VerificationCompleteView.ts b/src/platform/web/ui/session/verification/stages/VerificationCompleteView.ts new file mode 100644 index 0000000000..7edc4e40d3 --- /dev/null +++ b/src/platform/web/ui/session/verification/stages/VerificationCompleteView.ts @@ -0,0 +1,45 @@ +/* +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 {TemplateView} from "../../../general/TemplateView"; +import type {VerificationCompleteViewModel} from "../../../../../../domain/session/verification/stages/VerificationCompleteViewModel"; + +export class VerificationCompleteView extends TemplateView { + render(t, vm: VerificationCompleteViewModel) { + return t.div({ className: "VerificationCompleteView" }, [ + t.div({className: "VerificationCompleteView__icon"}), + t.div({ className: "VerificationCompleteView__heading" }, [ + t.h2( + { className: "VerificationCompleteView__title" }, + vm.i18n`Verification completed successfully!` + ), + ]), + t.p( + { className: "VerificationCompleteView__description" }, + vm.i18n`You successfully verified device ${vm.otherDeviceId}` + ), + t.div({ className: "VerificationCompleteView__actions" }, [ + t.button({ + className: { + "button-action": true, + "primary": true, + }, + onclick: () => vm.gotoSettings(), + }, "Got it") + ]), + ]); + } +} diff --git a/src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts b/src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts new file mode 100644 index 0000000000..9f7b312b46 --- /dev/null +++ b/src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts @@ -0,0 +1,79 @@ +/* +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 {TemplateView} from "../../../general/TemplateView"; +import {spinner} from "../../../common.js" +import type {VerifyEmojisViewModel} from "../../../../../../domain/session/verification/stages/VerifyEmojisViewModel"; + +export class VerifyEmojisView extends TemplateView { + render(t, vm: VerifyEmojisViewModel) { + const emojiList = vm.emojis.reduce((acc, [emoji, name]) => { + const e = t.div({ className: "EmojiContainer" }, [ + t.div({ className: "EmojiContainer__emoji" }, emoji), + t.div({ className: "EmojiContainer__name" }, name), + ]); + acc.push(e); + return acc; + }, [] as any); + const emojiCollection = t.div({ className: "EmojiCollection" }, emojiList); + return t.div({ className: "VerifyEmojisView" }, [ + t.div({ className: "VerifyEmojisView__heading" }, [ + t.h2( + { className: "VerifyEmojisView__title" }, + vm.i18n`Do the emojis match?` + ), + ]), + t.p( + { className: "VerifyEmojisView__description" }, + vm.i18n`Confirm the emoji below are displayed on both devices, in the same order:` + ), + t.div({ className: "VerifyEmojisView__emojis" }, emojiCollection), + t.map(vm => vm.isWaiting, (isWaiting, t, vm) => { + if (isWaiting) { + return t.div({ className: "VerifyEmojisView__waiting" }, [ + spinner(t), + t.span(vm.i18n`Waiting for you to verify on your other device`), + ]); + } + else { + return t.div({ className: "VerifyEmojisView__actions" }, [ + t.button( + { + className: { + "button-action": true, + primary: true, + destructive: true, + }, + onclick: () => vm.setEmojiMatch(false), + }, + vm.i18n`They don't match` + ), + t.button( + { + className: { + "button-action": true, + primary: true, + }, + onclick: () => vm.setEmojiMatch(true), + }, + vm.i18n`They match` + ), + ]); + } + }) + ]); + } +} diff --git a/src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.ts b/src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.ts new file mode 100644 index 0000000000..007b258eed --- /dev/null +++ b/src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.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 {TemplateView} from "../../../general/TemplateView"; +import {spinner} from "../../../common.js"; +import {WaitingForOtherUserViewModel} from "../../../../../../domain/session/verification/stages/WaitingForOtherUserViewModel"; + +export class WaitingForOtherUserView extends TemplateView { + render(t, vm) { + return t.div({ className: "WaitingForOtherUserView" }, [ + t.div({ className: "WaitingForOtherUserView__heading" }, [ + spinner(t), + t.h2( + { className: "WaitingForOtherUserView__title" }, + vm.i18n`Waiting for any of your device to accept the verification request` + ), + ]), + t.p({ className: "WaitingForOtherUserView__description" }, + vm.i18n`Accept the request from the device you wish to verify!` + ), + t.div({ className: "WaitingForOtherUserView__actions" }, + t.button({ + className: { + "button-action": true, + "primary": true, + "destructive": true, + }, + onclick: () => vm.cancel(), + }, "Cancel") + ), + ]); + } +} From 4c6a240e74278f9513a7b0477b19b3bd692dbfdd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 13:54:45 +0530 Subject: [PATCH 107/168] WIP: Toast notification --- .../session/toast/ToastCollectionViewModel.ts | 4 +- .../VerificationToastCollectionViewModel.ts | 59 ++++++++++++++ .../VerificationToastNotificationViewModel.ts | 53 +++++++++++++ .../DeviceVerificationViewModel.ts | 79 ++++++++++++------- src/matrix/verification/CrossSigning.ts | 10 ++- .../verification/SAS/SASVerification.ts | 11 +++ .../web/ui/css/themes/element/theme.css | 46 ++++++++++- .../ui/session/toast/ToastCollectionView.ts | 4 + .../VerificationToastNotificationView.ts | 45 +++++++++++ 9 files changed, 277 insertions(+), 34 deletions(-) create mode 100644 src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts create mode 100644 src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts create mode 100644 src/platform/web/ui/session/toast/VerificationToastNotificationView.ts diff --git a/src/domain/session/toast/ToastCollectionViewModel.ts b/src/domain/session/toast/ToastCollectionViewModel.ts index 44a75144f9..60dfe2e058 100644 --- a/src/domain/session/toast/ToastCollectionViewModel.ts +++ b/src/domain/session/toast/ToastCollectionViewModel.ts @@ -17,6 +17,7 @@ limitations under the License. import {ConcatList} from "../../../observable"; import {ViewModel, Options as BaseOptions} from "../../ViewModel"; import {CallToastCollectionViewModel} from "./calls/CallsToastCollectionViewModel"; +import {VerificationToastCollectionViewModel} from "./verification/VerificationToastCollectionViewModel"; import type {Session} from "../../../matrix/Session.js"; import type {SegmentType} from "../../navigation"; import type {BaseToastNotificationViewModel} from "./BaseToastNotificationViewModel"; @@ -31,8 +32,9 @@ export class ToastCollectionViewModel extends ViewModel { constructor(options: Options) { super(options); const session = this.getOption("session"); - const vms = [ + const vms: any = [ this.track(new CallToastCollectionViewModel(this.childOptions({ session }))), + this.track(new VerificationToastCollectionViewModel(this.childOptions({session}))), ].map(vm => vm.toastViewModels); this.toastViewModels = new ConcatList(...vms); } diff --git a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts new file mode 100644 index 0000000000..0a0f661949 --- /dev/null +++ b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts @@ -0,0 +1,59 @@ + +/* +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 {VerificationToastNotificationViewModel} from "./VerificationToastNotificationViewModel"; +import {ObservableArray} from "../../../../observable"; +import {ViewModel, Options as BaseOptions} from "../../../ViewModel"; +import type {Session} from "../../../../matrix/Session.js"; +import type {SegmentType} from "../../../navigation"; +import type {IToastCollection} from "../IToastCollection"; +import { SASVerification } from "../../../../matrix/verification/SAS/SASVerification"; + +type Options = { + session: Session; +} & BaseOptions; + + + +export class VerificationToastCollectionViewModel extends ViewModel implements IToastCollection { + public readonly toastViewModels: ObservableArray = new ObservableArray(); + + constructor(options: Options) { + super(options); + this.observeSASRequests(); + } + + async observeSASRequests() { + const session = this.getOption("session"); + if (this.features.crossSigning) { + // todo: hack; remove + await new Promise(r => setTimeout(r, 3000)); + const sasObservable = session.crossSigning.receivedSASVerification; + this.track(sasObservable.subscribe((sas) => this.createToast(sas))); + } + } + + private createToast(sas: SASVerification) { + const dismiss = () => { + const idx = this.toastViewModels.array.findIndex(vm => vm.sas === sas); + if (idx !== -1) { + this.toastViewModels.remove(idx); + } + }; + this.toastViewModels.append(new VerificationToastNotificationViewModel(this.childOptions({ sas, dismiss }))); + } +} diff --git a/src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts b/src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts new file mode 100644 index 0000000000..9b1770fb14 --- /dev/null +++ b/src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts @@ -0,0 +1,53 @@ +/* +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 {BaseClassOptions, BaseToastNotificationViewModel} from ".././BaseToastNotificationViewModel"; +import {SegmentType} from "../../../navigation"; +import type {SASVerification} from "../../../../matrix/verification/SAS/SASVerification"; + +type Options = { + sas: SASVerification; +} & BaseClassOptions; + +type MinimumNeededSegmentType = { + "device-verification": true; +}; + +export class VerificationToastNotificationViewModel = Options> extends BaseToastNotificationViewModel { + constructor(options: O) { + super(options); + } + + get kind(): "verification" { + return "verification"; + } + + get sas(): SASVerification { + return this.getOption("sas"); + } + + get otherDeviceId(): string { + return this.sas.otherDeviceId; + } + + accept() { + // @ts-ignore + this.navigation.push("device-verification", true); + this.dismiss(); + } + +} + + diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 2c471ca57a..f8cff30ec1 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -27,6 +27,7 @@ import type {SASVerification} from "../../../matrix/verification/SAS/SASVerifica type Options = BaseOptions & { session: Session; + sas: SASVerification; }; export class DeviceVerificationViewModel extends ErrorReportViewModel { @@ -37,46 +38,66 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel) { super(options); this.session = options.session; - this.createAndStartSasVerification(); - this._currentStageViewModel = this.track( - new WaitingForOtherUserViewModel( - this.childOptions({ sas: this.sas }) - ) - ); + const existingSas = this.session.crossSigning.receivedSASVerification.get(); + if (existingSas) { + // SAS already created from request + this.startWithExistingSAS(existingSas); + } + else { + // We are about to send the request + this.createAndStartSasVerification(); + this._currentStageViewModel = this.track( + new WaitingForOtherUserViewModel( + this.childOptions({ sas: this.sas }) + ) + ); + } + } + + private async startWithExistingSAS(sas: SASVerification) { + await this.logAndCatch("DeviceVerificationViewModel.startWithExistingSAS", (log) => { + this.sas = sas; + this.hookToEvents(); + return this.sas.start(); + }); } - async createAndStartSasVerification(): Promise { + private async createAndStartSasVerification(): Promise { await this.logAndCatch("DeviceVerificationViewModel.createAndStartSasVerification", (log) => { // todo: can crossSigning be undefined? const crossSigning = this.session.crossSigning; // todo: should be called createSasVerification this.sas = crossSigning.startVerification(this.session.userId, undefined, log); - const emitter = this.sas.eventEmitter; - this.track(emitter.disposableOn("SelectVerificationStage", (stage) => { - this.createViewModelAndEmit( - new SelectMethodViewModel(this.childOptions({ sas: this.sas, stage: stage!, })) - ); - })); - this.track(emitter.disposableOn("EmojiGenerated", (stage) => { - this.createViewModelAndEmit( - new VerifyEmojisViewModel(this.childOptions({ stage: stage!, })) - ); - })); - this.track(emitter.disposableOn("VerificationCancelled", (cancellation) => { - this.createViewModelAndEmit( - new VerificationCancelledViewModel( - this.childOptions({ cancellationCode: cancellation!.code, cancelledByUs: cancellation!.cancelledByUs, }) - )); - })); - this.track(emitter.disposableOn("VerificationCompleted", (deviceId) => { - this.createViewModelAndEmit( - new VerificationCompleteViewModel(this.childOptions({ deviceId: deviceId! })) - ); - })); + this.hookToEvents(); return this.sas.start(); }); } + private hookToEvents() { + const emitter = this.sas.eventEmitter; + this.track(emitter.disposableOn("SelectVerificationStage", (stage) => { + this.createViewModelAndEmit( + new SelectMethodViewModel(this.childOptions({ sas: this.sas, stage: stage!, })) + ); + })); + this.track(emitter.disposableOn("EmojiGenerated", (stage) => { + this.createViewModelAndEmit( + new VerifyEmojisViewModel(this.childOptions({ stage: stage!, })) + ); + })); + this.track(emitter.disposableOn("VerificationCancelled", (cancellation) => { + this.createViewModelAndEmit( + new VerificationCancelledViewModel( + this.childOptions({ cancellationCode: cancellation!.code, cancelledByUs: cancellation!.cancelledByUs, }) + )); + })); + this.track(emitter.disposableOn("VerificationCompleted", (deviceId) => { + this.createViewModelAndEmit( + new VerificationCompleteViewModel(this.childOptions({ deviceId: deviceId! })) + ); + })); + } + private createViewModelAndEmit(vm) { this._currentStageViewModel = this.disposeTracked(this._currentStageViewModel); this._currentStageViewModel = this.track(vm); diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index ba90bc3b10..5d4ffef43a 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -19,6 +19,7 @@ import {pkSign} from "./common"; import {SASVerification} from "./SAS/SASVerification"; import {ToDeviceChannel} from "./SAS/channel/Channel"; import {VerificationEventType} from "./SAS/channel/types"; +import {ObservableValue} from "../../observable/value/ObservableValue"; import type {SecretStorage} from "../ssss/SecretStorage"; import type {Storage} from "../storage/idb/Storage"; import type {Platform} from "../../platform/web/Platform"; @@ -29,6 +30,7 @@ import type {ILogItem} from "../../logging/types"; import type {DeviceMessageHandler} from "../DeviceMessageHandler.js"; import type {SignedValue, DeviceKey} from "../e2ee/common"; import type * as OlmNamespace from "@matrix-org/olm"; + type Olm = typeof OlmNamespace; // we store cross-signing (and device) keys in the format we get them from the server @@ -88,6 +90,7 @@ export class CrossSigning { private _isMasterKeyTrusted: boolean = false; private readonly deviceId: string; private sasVerificationInProgress?: SASVerification; + public receivedSASVerification: ObservableValue = new ObservableValue(undefined); constructor(options: { storage: Storage, @@ -125,9 +128,10 @@ export class CrossSigning { } if (unencryptedEvent.type === VerificationEventType.Request || unencryptedEvent.type === VerificationEventType.Start) { - await this.platform.logger.run("Start verification from request", async (log) => { - const sas = this.startVerification(unencryptedEvent.sender, unencryptedEvent, log); - await sas?.start(); + this.platform.logger.run("Start verification from request", (log) => { + //todo: We can have more than one sas requests + this.sasVerificationInProgress = this.startVerification(unencryptedEvent.sender, unencryptedEvent, log); + this.receivedSASVerification.set(this.sasVerificationInProgress!); }); } }) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 42a7e47d3a..41a42b4594 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -110,6 +110,17 @@ export class SASVerification extends EventEmitter { this.finished = true; } } + + get otherDeviceId() { + return this.channel?.otherUserDeviceId; + } + + /** + * Returns true if we were created because a "request" message was received + */ + get isStartingWithRequestMessage(): boolean { + return this.startStage instanceof SendReadyStage; + } } import {HomeServer} from "../../../mocks/HomeServer.js"; diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 3f2c19e31b..31e627540b 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1264,27 +1264,52 @@ button.RoomDetailsView_row::after { padding: 0; } +.VerificationToastNotificationView:not(:first-child), .CallToastNotificationView:not(:first-child) { margin-top: 12px; } +.VerificationToastNotificationView { + display: flex; + flex-direction: column; +} + .CallToastNotificationView { display: grid; grid-template-rows: 40px 1fr 1fr 48px; row-gap: 4px; - width: 260px; +} + + +.VerificationToastNotificationView, +.CallToastNotificationView { background-color: var(--background-color-secondary); border-radius: 8px; color: var(--text-color); box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.5); } +.CallToastNotificationView { + width: 260px; +} + +.VerificationToastNotificationView { + width: 248px; +} + +.VerificationToastNotificationView__top { + padding: 8px; + display: flex; +} + .CallToastNotificationView__top { display: grid; grid-template-columns: auto 176px auto; align-items: center; justify-items: center; } + +.VerificationToastNotificationView__dismiss-btn, .CallToastNotificationView__dismiss-btn { background: center var(--background-color-secondary--darker-5) url("./icons/dismiss.svg?primary=text-color") no-repeat; border-radius: 100%; @@ -1292,11 +1317,16 @@ button.RoomDetailsView_row::after { width: 15px; } +.VerificationToastNotificationView__title, .CallToastNotificationView__name { font-weight: 600; width: 100%; } +.VerificationToastNotificationView__description { + padding: 8px; +} + .CallToastNotificationView__description { margin-left: 42px; } @@ -1350,11 +1380,25 @@ button.RoomDetailsView_row::after { margin-right: 10px; } +.VerificationToastNotificationView__action { + display: flex; + justify-content: space-between; + padding: 8px; +} + .CallToastNotificationView__action .button-action { width: 100px; height: 40px; } +.VerificationToastNotificationView__action .button-action { + width: 100px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; +} + .VerificationCompleteView, .DeviceVerificationView, .SelectMethodView { diff --git a/src/platform/web/ui/session/toast/ToastCollectionView.ts b/src/platform/web/ui/session/toast/ToastCollectionView.ts index a3d734d2fc..cf4fd58f09 100644 --- a/src/platform/web/ui/session/toast/ToastCollectionView.ts +++ b/src/platform/web/ui/session/toast/ToastCollectionView.ts @@ -15,17 +15,21 @@ limitations under the License. */ import {CallToastNotificationView} from "./CallToastNotificationView"; +import {VerificationToastNotificationView} from "./VerificationToastNotificationView"; import {ListView} from "../../general/ListView"; import {TemplateView, Builder} from "../../general/TemplateView"; import type {IView} from "../../general/types"; import type {CallToastNotificationViewModel} from "../../../../../domain/session/toast/calls/CallToastNotificationViewModel"; import type {ToastCollectionViewModel} from "../../../../../domain/session/toast/ToastCollectionViewModel"; import type {BaseToastNotificationViewModel} from "../../../../../domain/session/toast/BaseToastNotificationViewModel"; +import type {VerificationToastNotificationViewModel} from "../../../../../domain/session/toast/verification/VerificationToastNotificationViewModel"; function toastViewModelToView(vm: BaseToastNotificationViewModel): IView { switch (vm.kind) { case "calls": return new CallToastNotificationView(vm as CallToastNotificationViewModel); + case "verification": + return new VerificationToastNotificationView(vm as VerificationToastNotificationViewModel); default: throw new Error(`Cannot find view class for notification kind ${vm.kind}`); } diff --git a/src/platform/web/ui/session/toast/VerificationToastNotificationView.ts b/src/platform/web/ui/session/toast/VerificationToastNotificationView.ts new file mode 100644 index 0000000000..691dc30e90 --- /dev/null +++ b/src/platform/web/ui/session/toast/VerificationToastNotificationView.ts @@ -0,0 +1,45 @@ +/* +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 {TemplateView, Builder} from "../../general/TemplateView"; +import type {VerificationToastNotificationViewModel} from "../../../../../domain/session/toast/verification/VerificationToastNotificationViewModel"; + +export class VerificationToastNotificationView extends TemplateView { + render(t: Builder, vm: VerificationToastNotificationViewModel) { + return t.div({ className: "VerificationToastNotificationView" }, [ + t.div({ className: "VerificationToastNotificationView__top" }, [ + t.span({ className: "VerificationToastNotificationView__title" }, + vm.i18n`Device Verification`), + t.button({ + className: "button-action VerificationToastNotificationView__dismiss-btn", + onClick: () => vm.dismiss(), + }), + ]), + t.div({ className: "VerificationToastNotificationView__description" }, [ + t.span(vm.i18n`Do you want to verify device ${vm.otherDeviceId}?`), + ]), + t.div({ className: "VerificationToastNotificationView__action" }, [ + t.button({ + className: "button-action primary destructive", + onClick: () => vm.dismiss(), + }, vm.i18n`Ignore`), + t.button({ + className: "button-action primary", + onClick: () => vm.accept(), + }, vm.i18n`Accept`), + ]), + ]); + } +} From 90ce3f5d86295183a51e299093e82ad811b4fa9d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 15:26:05 +0530 Subject: [PATCH 108/168] Remove toast when receiving cancel --- .../VerificationToastCollectionViewModel.ts | 11 ++++++++++- src/matrix/verification/CrossSigning.ts | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts index 0a0f661949..fab17a20cf 100644 --- a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts +++ b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts @@ -43,7 +43,16 @@ export class VerificationToastCollectionViewModel extends ViewModel setTimeout(r, 3000)); const sasObservable = session.crossSigning.receivedSASVerification; - this.track(sasObservable.subscribe((sas) => this.createToast(sas))); + this.track( + sasObservable.subscribe((sas) => { + if (sas) { + this.createToast(sas); + } + else { + this.toastViewModels.remove(0); + } + }) + ); } } diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 5d4ffef43a..d355a81a6f 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -118,6 +118,10 @@ export class CrossSigning { this.deviceMessageHandler = options.deviceMessageHandler; this.deviceMessageHandler.on("message", async ({ unencrypted: unencryptedEvent }) => { + if (unencryptedEvent.type === VerificationEventTypes.Cancel && + this.sasVerificationInProgress?.channel.id === unencryptedEvent.content.transaction_id) { + this.receivedSASVerification.set(undefined); + } if (this.sasVerificationInProgress && ( !this.sasVerificationInProgress.finished || From 4aa86c6dd2c347cc0c6e988ef805b91b3afd41c8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 17:47:20 +0530 Subject: [PATCH 109/168] Support multiple requests --- src/domain/navigation/index.ts | 2 +- src/domain/session/SessionViewModel.js | 11 ++--- .../VerificationToastCollectionViewModel.ts | 43 +++++++++++-------- .../VerificationToastNotificationViewModel.ts | 14 +++--- .../DeviceVerificationViewModel.ts | 17 ++++---- src/matrix/verification/CrossSigning.ts | 30 +++++++------ src/matrix/verification/SAS/SASRequest.ts | 31 +++++++++++++ 7 files changed, 97 insertions(+), 51 deletions(-) create mode 100644 src/matrix/verification/SAS/SASRequest.ts diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index c8f62a7bf4..5904e71543 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -34,7 +34,7 @@ export type SegmentType = { "details": true; "members": true; "member": string; - "device-verification": true; + "device-verification": string; "join-room": true; }; diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index b952b3295f..e0e7772968 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -98,8 +98,8 @@ export class SessionViewModel extends ViewModel { this._updateJoinRoom(joinRoom.get()); const verification = this.navigation.observe("device-verification"); - this.track(verification.subscribe((verificationOpen) => { - this._updateVerification(verificationOpen); + this.track(verification.subscribe((txnId) => { + this._updateVerification(txnId); })); this._updateVerification(verification.get()); @@ -340,12 +340,13 @@ export class SessionViewModel extends ViewModel { this.emitChange("activeMiddleViewModel"); } - _updateVerification(verificationOpen) { + _updateVerification(txnId) { if (this._verificationViewModel) { this._verificationViewModel = this.disposeTracked(this._verificationViewModel); } - if (verificationOpen) { - this._verificationViewModel = this.track(new DeviceVerificationViewModel(this.childOptions({ session: this._client.session }))); + if (txnId) { + const request = this._client.session.crossSigning.receivedSASVerifications.get(txnId); + this._verificationViewModel = this.track(new DeviceVerificationViewModel(this.childOptions({ session: this._client.session, request }))); } this.emitChange("activeMiddleViewModel"); } diff --git a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts index fab17a20cf..e2ba96ae3c 100644 --- a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts +++ b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts @@ -21,14 +21,12 @@ import {ViewModel, Options as BaseOptions} from "../../../ViewModel"; import type {Session} from "../../../../matrix/Session.js"; import type {SegmentType} from "../../../navigation"; import type {IToastCollection} from "../IToastCollection"; -import { SASVerification } from "../../../../matrix/verification/SAS/SASVerification"; +import type {SASRequest} from "../../../../matrix/verification/SAS/SASRequest"; type Options = { session: Session; } & BaseOptions; - - export class VerificationToastCollectionViewModel extends ViewModel implements IToastCollection { public readonly toastViewModels: ObservableArray = new ObservableArray(); @@ -42,27 +40,38 @@ export class VerificationToastCollectionViewModel extends ViewModel setTimeout(r, 3000)); - const sasObservable = session.crossSigning.receivedSASVerification; - this.track( - sasObservable.subscribe((sas) => { - if (sas) { - this.createToast(sas); - } - else { - this.toastViewModels.remove(0); - } - }) - ); + const map = session.crossSigning.receivedSASVerifications; + this.track(map.subscribe(this)); } } - private createToast(sas: SASVerification) { + async onAdd(_, request: SASRequest) { const dismiss = () => { - const idx = this.toastViewModels.array.findIndex(vm => vm.sas === sas); + const idx = this.toastViewModels.array.findIndex(vm => vm.request.id === request.id); if (idx !== -1) { this.toastViewModels.remove(idx); } }; - this.toastViewModels.append(new VerificationToastNotificationViewModel(this.childOptions({ sas, dismiss }))); + this.toastViewModels.append(new VerificationToastNotificationViewModel(this.childOptions({ request, dismiss }))); + } + + onRemove(_, request: SASRequest) { + const idx = this.toastViewModels.array.findIndex(vm => vm.request.id === request.id); + if (idx !== -1) { + this.toastViewModels.remove(idx); + } + } + + onUpdate(_, request: SASRequest) { + const idx = this.toastViewModels.array.findIndex(vm => vm.request.id === request.id); + if (idx !== -1) { + this.toastViewModels.update(idx, this.toastViewModels.at(idx)!); + } + } + + onReset() { + for (let i = 0; i < this.toastViewModels.length; ++i) { + this.toastViewModels.remove(i); + } } } diff --git a/src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts b/src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts index 9b1770fb14..b3d7de3524 100644 --- a/src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts +++ b/src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts @@ -15,14 +15,14 @@ limitations under the License. */ import {BaseClassOptions, BaseToastNotificationViewModel} from ".././BaseToastNotificationViewModel"; import {SegmentType} from "../../../navigation"; -import type {SASVerification} from "../../../../matrix/verification/SAS/SASVerification"; +import {SASRequest} from "../../../../matrix/verification/SAS/SASRequest"; type Options = { - sas: SASVerification; + request: SASRequest; } & BaseClassOptions; type MinimumNeededSegmentType = { - "device-verification": true; + "device-verification": string; }; export class VerificationToastNotificationViewModel = Options> extends BaseToastNotificationViewModel { @@ -34,17 +34,17 @@ export class VerificationToastNotificationViewModel { @@ -38,10 +39,10 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel) { super(options); this.session = options.session; - const existingSas = this.session.crossSigning.receivedSASVerification.get(); - if (existingSas) { + const sasRequest = options.request; + if (options.request) { // SAS already created from request - this.startWithExistingSAS(existingSas); + this.startWithSASRequest(sasRequest); } else { // We are about to send the request @@ -54,9 +55,10 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { - this.sas = sas; + const crossSigning = this.session.crossSigning; + this.sas = crossSigning.startVerification(request, log); this.hookToEvents(); return this.sas.start(); }); @@ -66,8 +68,7 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { // todo: can crossSigning be undefined? const crossSigning = this.session.crossSigning; - // todo: should be called createSasVerification - this.sas = crossSigning.startVerification(this.session.userId, undefined, log); + this.sas = crossSigning.startVerification(this.session.userId, log); this.hookToEvents(); return this.sas.start(); }); diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index d355a81a6f..2273c44210 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -19,7 +19,8 @@ import {pkSign} from "./common"; import {SASVerification} from "./SAS/SASVerification"; import {ToDeviceChannel} from "./SAS/channel/Channel"; import {VerificationEventType} from "./SAS/channel/types"; -import {ObservableValue} from "../../observable/value/ObservableValue"; +import {ObservableMap} from "../../observable/map"; +import {SASRequest} from "./SAS/SASRequest"; import type {SecretStorage} from "../ssss/SecretStorage"; import type {Storage} from "../storage/idb/Storage"; import type {Platform} from "../../platform/web/Platform"; @@ -90,7 +91,7 @@ export class CrossSigning { private _isMasterKeyTrusted: boolean = false; private readonly deviceId: string; private sasVerificationInProgress?: SASVerification; - public receivedSASVerification: ObservableValue = new ObservableValue(undefined); + public receivedSASVerifications: ObservableMap = new ObservableMap(); constructor(options: { storage: Storage, @@ -118,24 +119,23 @@ export class CrossSigning { this.deviceMessageHandler = options.deviceMessageHandler; this.deviceMessageHandler.on("message", async ({ unencrypted: unencryptedEvent }) => { - if (unencryptedEvent.type === VerificationEventTypes.Cancel && - this.sasVerificationInProgress?.channel.id === unencryptedEvent.content.transaction_id) { - this.receivedSASVerification.set(undefined); + const txnId = unencryptedEvent.content.transaction_id; + if (unencryptedEvent.type === VerificationEventType.Cancel) { + this.receivedSASVerifications.remove(txnId); + return; } if (this.sasVerificationInProgress && ( !this.sasVerificationInProgress.finished || // If the start message is for the previous sasverification, ignore it. - this.sasVerificationInProgress.channel.id === unencryptedEvent.content.transaction_id + this.sasVerificationInProgress.channel.id === txnId )) { return; } if (unencryptedEvent.type === VerificationEventType.Request || unencryptedEvent.type === VerificationEventType.Start) { - this.platform.logger.run("Start verification from request", (log) => { - //todo: We can have more than one sas requests - this.sasVerificationInProgress = this.startVerification(unencryptedEvent.sender, unencryptedEvent, log); - this.receivedSASVerification.set(this.sasVerificationInProgress!); + this.platform.logger.run("Start verification from request", () => { + this.receivedSASVerifications.set(txnId, new SASRequest(unencryptedEvent)); }); } }) @@ -190,14 +190,18 @@ export class CrossSigning { return this._isMasterKeyTrusted; } - startVerification(userId: string, startingMessage: any, log: ILogItem): SASVerification | undefined { + 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; } + const otherUserId = requestOrUserId instanceof SASRequest ? requestOrUserId.sender : requestOrUserId; + const startingMessage = requestOrUserId instanceof SASRequest ? requestOrUserId.startingMessage : undefined; const channel = new ToDeviceChannel({ deviceTracker: this.deviceTracker, hsApi: this.hsApi, - otherUserId: userId, + otherUserId, clock: this.platform.clock, deviceMessageHandler: this.deviceMessageHandler, ourUserDeviceId: this.deviceId, @@ -209,7 +213,7 @@ export class CrossSigning { olmUtil: this.olmUtil, ourUserId: this.ownUserId, ourUserDeviceId: this.deviceId, - otherUserId: userId, + otherUserId, log, channel, e2eeAccount: this.e2eeAccount, diff --git a/src/matrix/verification/SAS/SASRequest.ts b/src/matrix/verification/SAS/SASRequest.ts new file mode 100644 index 0000000000..69bc197a77 --- /dev/null +++ b/src/matrix/verification/SAS/SASRequest.ts @@ -0,0 +1,31 @@ +/* +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. +*/ + +export class SASRequest { + constructor(public readonly startingMessage: any) {} + + get deviceId(): string { + return this.startingMessage.content.from_device; + } + + get sender(): string { + return this.startingMessage.sender; + } + + get id(): string { + return this.startingMessage.content.transaction_id; + } +} From 16c144868af455071869e2a0f5fcc12d91cdd6a3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 17:51:17 +0530 Subject: [PATCH 110/168] Refactor code --- .../VerificationToastCollectionViewModel.ts | 2 +- .../DeviceVerificationViewModel.ts | 23 +++++-------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts index e2ba96ae3c..2acd0412c8 100644 --- a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts +++ b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts @@ -38,7 +38,7 @@ export class VerificationToastCollectionViewModel extends ViewModel setTimeout(r, 3000)); const map = session.crossSigning.receivedSASVerifications; this.track(map.subscribe(this)); diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 5807898f31..b30fd61f50 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -41,12 +41,11 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { const crossSigning = this.session.crossSigning; - this.sas = crossSigning.startVerification(request, log); - this.hookToEvents(); + this.sas = crossSigning.startVerification(requestOrUserId, log); + this.addEventListeners(); return this.sas.start(); }); } - private async createAndStartSasVerification(): Promise { - await this.logAndCatch("DeviceVerificationViewModel.createAndStartSasVerification", (log) => { - // todo: can crossSigning be undefined? - const crossSigning = this.session.crossSigning; - this.sas = crossSigning.startVerification(this.session.userId, log); - this.hookToEvents(); - return this.sas.start(); - }); - } - - private hookToEvents() { + private addEventListeners() { const emitter = this.sas.eventEmitter; this.track(emitter.disposableOn("SelectVerificationStage", (stage) => { this.createViewModelAndEmit( From 15ab7e7a728006c3eb5addd5801a659aa9b99cff Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 24 Mar 2023 18:03:39 +0530 Subject: [PATCH 111/168] Create viewmodel inside start method --- .../session/verification/DeviceVerificationViewModel.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index b30fd61f50..4b3516688b 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -46,11 +46,6 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel Date: Fri, 24 Mar 2023 21:05:18 +0530 Subject: [PATCH 112/168] Fix rebase --- .../DeviceVerificationViewModel.ts | 9 ++-- .../stages/VerificationCancelledViewModel.ts | 6 +-- .../verification/SAS/SASVerification.ts | 2 +- .../verification/SAS/channel/Channel.ts | 10 ++-- .../verification/SAS/channel/MockChannel.ts | 2 +- .../verification/SAS/stages/SendDoneStage.ts | 2 +- src/matrix/verification/SAS/types.ts | 4 +- .../stages/VerificationCancelledView.ts | 48 +++++++++---------- 8 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 4b3516688b..4ca0080004 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -62,24 +62,23 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { + this.track(this.sas.disposableOn("SelectVerificationStage", (stage) => { this.createViewModelAndEmit( new SelectMethodViewModel(this.childOptions({ sas: this.sas, stage: stage!, })) ); })); - this.track(emitter.disposableOn("EmojiGenerated", (stage) => { + this.track(this.sas.disposableOn("EmojiGenerated", (stage) => { this.createViewModelAndEmit( new VerifyEmojisViewModel(this.childOptions({ stage: stage!, })) ); })); - this.track(emitter.disposableOn("VerificationCancelled", (cancellation) => { + this.track(this.sas.disposableOn("VerificationCancelled", (cancellation) => { this.createViewModelAndEmit( new VerificationCancelledViewModel( this.childOptions({ cancellationCode: cancellation!.code, cancelledByUs: cancellation!.cancelledByUs, }) )); })); - this.track(emitter.disposableOn("VerificationCompleted", (deviceId) => { + this.track(this.sas.disposableOn("VerificationCompleted", (deviceId) => { this.createViewModelAndEmit( new VerificationCompleteViewModel(this.childOptions({ deviceId: deviceId! })) ); diff --git a/src/domain/session/verification/stages/VerificationCancelledViewModel.ts b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts index ad01d31203..9f2bd18057 100644 --- a/src/domain/session/verification/stages/VerificationCancelledViewModel.ts +++ b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts @@ -16,15 +16,15 @@ limitations under the License. import {ViewModel, Options as BaseOptions} from "../../../ViewModel"; import {SegmentType} from "../../../navigation/index"; -import {CancelTypes} from "../../../../matrix/verification/SAS/channel/types"; +import {CancelReason} from "../../../../matrix/verification/SAS/channel/types"; type Options = BaseOptions & { - cancellationCode: CancelTypes; + cancellationCode: CancelReason; cancelledByUs: boolean; }; export class VerificationCancelledViewModel extends ViewModel { - get cancelCode(): CancelTypes { + get cancelCode(): CancelReason { return this.options.cancellationCode; } diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 41a42b4594..fcd86eb70c 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -85,7 +85,7 @@ export class SASVerification extends EventEmitter { } async abort() { - await this.channel.cancelVerification(CancelTypes.UserCancelled); + await this.channel.cancelVerification(CancelReason.UserCancelled); } async start() { diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 9763b95c04..fc2260fa9d 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -50,7 +50,7 @@ export interface IChannel { startMessage: any; initiatedByUs: boolean; isCancelled: boolean; - cancellation: { code: CancelTypes, cancelledByUs: boolean }; + cancellation: { code: CancelReason, cancelledByUs: boolean }; id: string; otherUserDeviceId: string; } @@ -80,7 +80,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { public startMessage: any; public id: string; private _initiatedByUs: boolean; - private _cancellation: { code: CancelTypes, cancelledByUs: boolean }; + private _cancellation: { code: CancelReason, cancelledByUs: boolean }; /** * @@ -204,7 +204,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { this.handleReadyMessage(event, log); return; } - if (event.type === VerificationEventTypes.Cancel) { + if (event.type === VerificationEventType.Cancel) { this._cancellation = { code: event.content.code, cancelledByUs: false }; this.dispose(); return; @@ -248,7 +248,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } } - await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, makeTxnId(), { log }).response(); + await this.hsApi.sendToDevice(VerificationEventType.Cancel, payload, makeTxnId(), { log }).response(); this._cancellation = { code: cancellationType, cancelledByUs: true }; this.dispose(); }); @@ -263,7 +263,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } - waitForEvent(eventType: VerificationEventTypes): Promise { + waitForEvent(eventType: VerificationEventType): Promise { if (this.isCancelled) { throw new VerificationCancelledError(); } diff --git a/src/matrix/verification/SAS/channel/MockChannel.ts b/src/matrix/verification/SAS/channel/MockChannel.ts index 9553a92d8b..64ae14564f 100644 --- a/src/matrix/verification/SAS/channel/MockChannel.ts +++ b/src/matrix/verification/SAS/channel/MockChannel.ts @@ -17,7 +17,7 @@ export class MockChannel implements ITestChannel { public initiatedByUs: boolean; public startMessage: any; public isCancelled: boolean = false; - public cancellation: { code: CancelTypes; cancelledByUs: boolean; }; + public cancellation: { code: CancelReason; cancelledByUs: boolean; }; private olmSas: any; constructor( diff --git a/src/matrix/verification/SAS/stages/SendDoneStage.ts b/src/matrix/verification/SAS/stages/SendDoneStage.ts index 53b8a37e58..95167e2473 100644 --- a/src/matrix/verification/SAS/stages/SendDoneStage.ts +++ b/src/matrix/verification/SAS/stages/SendDoneStage.ts @@ -20,7 +20,7 @@ export class SendDoneStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SendDoneStage.completeStage", async (log) => { this.eventEmitter.emit("VerificationCompleted", this.otherUserDeviceId); - await this.channel.send(VerificationEventTypes.Done, {}, log); + await this.channel.send(VerificationEventType.Done, {}, log); }); } } diff --git a/src/matrix/verification/SAS/types.ts b/src/matrix/verification/SAS/types.ts index 3bfd742ec9..528a2e5512 100644 --- a/src/matrix/verification/SAS/types.ts +++ b/src/matrix/verification/SAS/types.ts @@ -13,7 +13,7 @@ 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 {CancelTypes} from "./channel/types"; +import {CancelReason} from "./channel/types"; import {CalculateSASStage} from "./stages/CalculateSASStage"; import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; @@ -21,5 +21,5 @@ export type SASProgressEvents = { SelectVerificationStage: SelectVerificationMethodStage; EmojiGenerated: CalculateSASStage; VerificationCompleted: string; - VerificationCancelled: { code: CancelTypes, cancelledByUs: boolean }; + VerificationCancelled: { code: CancelReason, cancelledByUs: boolean }; } diff --git a/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts b/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts index d2afa98d8f..28cebf8078 100644 --- a/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts +++ b/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts @@ -16,7 +16,7 @@ limitations under the License. import {TemplateView} from "../../../general/TemplateView"; import {VerificationCancelledViewModel} from "../../../../../../domain/session/verification/stages/VerificationCancelledViewModel"; -import {CancelTypes} from "../../../../../../matrix/verification/SAS/channel/types"; +import {CancelReason} from "../../../../../../matrix/verification/SAS/channel/types"; export class VerificationCancelledView extends TemplateView { render(t, vm: VerificationCancelledViewModel) { @@ -48,32 +48,32 @@ export class VerificationCancelledView extends TemplateView Date: Mon, 27 Mar 2023 14:20:20 +0530 Subject: [PATCH 113/168] Track view-model instance --- .../verification/VerificationToastCollectionViewModel.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts index 2acd0412c8..d6b0795bd3 100644 --- a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts +++ b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts @@ -52,7 +52,9 @@ export class VerificationToastCollectionViewModel extends ViewModel Date: Mon, 27 Mar 2023 14:25:10 +0530 Subject: [PATCH 114/168] Remove "any" type --- src/domain/session/toast/ToastCollectionViewModel.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/session/toast/ToastCollectionViewModel.ts b/src/domain/session/toast/ToastCollectionViewModel.ts index 60dfe2e058..6b2bdcebaa 100644 --- a/src/domain/session/toast/ToastCollectionViewModel.ts +++ b/src/domain/session/toast/ToastCollectionViewModel.ts @@ -21,6 +21,7 @@ import {VerificationToastCollectionViewModel} from "./verification/VerificationT import type {Session} from "../../../matrix/Session.js"; import type {SegmentType} from "../../navigation"; import type {BaseToastNotificationViewModel} from "./BaseToastNotificationViewModel"; +import type {IToastCollection} from "./IToastCollection"; type Options = { session: Session; @@ -32,7 +33,7 @@ export class ToastCollectionViewModel extends ViewModel { constructor(options: Options) { super(options); const session = this.getOption("session"); - const vms: any = [ + const vms: IToastCollection["toastViewModels"][] = [ this.track(new CallToastCollectionViewModel(this.childOptions({ session }))), this.track(new VerificationToastCollectionViewModel(this.childOptions({session}))), ].map(vm => vm.toastViewModels); From 918ee6bf1daad0cf2efb8ee39ebc48af8f0cd066 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Mar 2023 14:32:10 +0530 Subject: [PATCH 115/168] Change log string --- src/domain/session/verification/DeviceVerificationViewModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 4ca0080004..7a9248f0c3 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -50,7 +50,7 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { + await this.logAndCatch("DeviceVerificationViewModel.start", (log) => { const crossSigning = this.session.crossSigning; this.sas = crossSigning.startVerification(requestOrUserId, log); this.addEventListeners(); From d32d0def369ae72a2650454f5c94990fbef20c94 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Mar 2023 14:34:38 +0530 Subject: [PATCH 116/168] Fix emit --- src/matrix/verification/SAS/SASVerification.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index fcd86eb70c..9d2de3dff2 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -103,7 +103,7 @@ export class SASVerification extends EventEmitter { } finally { if (this.channel.isCancelled) { - this.eventEmitter.emit("VerificationCancelled", this.channel.cancellation); + this.emit("VerificationCancelled", this.channel.cancellation); } this.olmSas.free(); this.timeout.abort(); From 0588d04742626e76f0709a4d7cf1d8bacbaa61aa Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Mar 2023 14:39:25 +0530 Subject: [PATCH 117/168] Pass in cancellation object --- .../verification/DeviceVerificationViewModel.ts | 7 ++++--- .../stages/VerificationCancelledViewModel.ts | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 7a9248f0c3..58debd285b 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -75,9 +75,10 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { this.createViewModelAndEmit( new VerificationCancelledViewModel( - this.childOptions({ cancellationCode: cancellation!.code, cancelledByUs: cancellation!.cancelledByUs, }) - )); - })); + this.childOptions({ cancellation: cancellation! }) + ) + ); + })); this.track(this.sas.disposableOn("VerificationCompleted", (deviceId) => { this.createViewModelAndEmit( new VerificationCompleteViewModel(this.childOptions({ deviceId: deviceId! })) diff --git a/src/domain/session/verification/stages/VerificationCancelledViewModel.ts b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts index 9f2bd18057..01f1176848 100644 --- a/src/domain/session/verification/stages/VerificationCancelledViewModel.ts +++ b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts @@ -16,20 +16,20 @@ limitations under the License. import {ViewModel, Options as BaseOptions} from "../../../ViewModel"; import {SegmentType} from "../../../navigation/index"; -import {CancelReason} from "../../../../matrix/verification/SAS/channel/types"; +import type {CancelReason} from "../../../../matrix/verification/SAS/channel/types"; +import type {IChannel} from "../../../../matrix/verification/SAS/channel/Channel"; type Options = BaseOptions & { - cancellationCode: CancelReason; - cancelledByUs: boolean; + cancellation: IChannel["cancellation"]; }; export class VerificationCancelledViewModel extends ViewModel { get cancelCode(): CancelReason { - return this.options.cancellationCode; + return this.options.cancellation.code; } get isCancelledByUs(): boolean { - return this.options.cancelledByUs; + return this.options.cancellation.cancelledByUs; } gotoSettings() { From ac1a16d548adef23fac3ac6bdd355c1ed913e93d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Mar 2023 14:45:57 +0530 Subject: [PATCH 118/168] Remove unused code --- src/matrix/verification/SAS/SASVerification.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 9d2de3dff2..5587eccd47 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -110,17 +110,6 @@ export class SASVerification extends EventEmitter { this.finished = true; } } - - get otherDeviceId() { - return this.channel?.otherUserDeviceId; - } - - /** - * Returns true if we were created because a "request" message was received - */ - get isStartingWithRequestMessage(): boolean { - return this.startStage instanceof SendReadyStage; - } } import {HomeServer} from "../../../mocks/HomeServer.js"; From 7e2823be5e8f391d79c406db612e6a8c56b08aae Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Mar 2023 14:49:29 +0530 Subject: [PATCH 119/168] Import as types --- src/matrix/verification/SAS/SASVerification.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 5587eccd47..d4b1714313 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -19,14 +19,14 @@ import type {BaseSASVerificationStage} from "./stages/BaseSASVerificationStage"; import type {Account} from "../../e2ee/Account.js"; import type {DeviceTracker} from "../../e2ee/DeviceTracker.js"; import type * as OlmNamespace from "@matrix-org/olm"; -import {IChannel} from "./channel/Channel"; -import {HomeServerApi} from "../../net/HomeServerApi"; +import type {IChannel} from "./channel/Channel"; +import type {HomeServerApi} from "../../net/HomeServerApi"; +import type {Timeout} from "../../../platform/types/types"; +import type {Clock} from "../../../platform/web/dom/Clock.js"; import {CancelReason, VerificationEventType} from "./channel/types"; import {SendReadyStage} from "./stages/SendReadyStage"; import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; import {VerificationCancelledError} from "./VerificationCancelledError"; -import {Timeout} from "../../../platform/types/types"; -import {Clock} from "../../../platform/web/dom/Clock.js"; import {EventEmitter} from "../../../utils/EventEmitter"; import {SASProgressEvents} from "./types"; From 8becb2b60567891a6828d356aa7bd0b4f68550c3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Mar 2023 14:51:01 +0530 Subject: [PATCH 120/168] Import as type --- src/matrix/verification/SAS/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/verification/SAS/types.ts b/src/matrix/verification/SAS/types.ts index 528a2e5512..a46ee0856c 100644 --- a/src/matrix/verification/SAS/types.ts +++ b/src/matrix/verification/SAS/types.ts @@ -13,13 +13,13 @@ 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 {CancelReason} from "./channel/types"; -import {CalculateSASStage} from "./stages/CalculateSASStage"; -import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; +import type {IChannel} from "./channel/Channel"; +import type {CalculateSASStage} from "./stages/CalculateSASStage"; +import type {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; export type SASProgressEvents = { SelectVerificationStage: SelectVerificationMethodStage; EmojiGenerated: CalculateSASStage; VerificationCompleted: string; - VerificationCancelled: { code: CancelReason, cancelledByUs: boolean }; + VerificationCancelled: IChannel["cancellation"]; } From 5fa4afa021649e73603e5942cdac4fe7a430d183 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Mar 2023 14:53:50 +0530 Subject: [PATCH 121/168] Combine css styles --- src/platform/web/ui/css/themes/element/theme.css | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 31e627540b..11501bce59 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1430,11 +1430,7 @@ button.RoomDetailsView_row::after { .VerificationCompleteView__title, .VerifyEmojisView__title, .SelectMethodView__title, -.WaitingForOtherUserView__title { - text-align: center; - margin: 0; -} - +.WaitingForOtherUserView__title, .VerificationCancelledView__description, .VerificationCompleteView__description, .VerifyEmojisView__description, From 41ebf13156380054365714f600bdabd72032580e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Mar 2023 16:36:27 +0530 Subject: [PATCH 122/168] Some more changes --- .../stages/SelectMethodViewModel.ts | 4 ++++ .../stages/VerificationCancelledViewModel.ts | 4 ++++ .../stages/VerificationCompleteViewModel.ts | 4 ++++ .../stages/VerifyEmojisViewModel.ts | 4 ++++ .../stages/WaitingForOtherUserViewModel.ts | 4 ++++ .../verification/DeviceVerificationView.ts | 20 ++++++++----------- .../verification/stages/SelectMethodView.ts | 4 ++-- .../stages/VerificationCancelledView.ts | 7 ++----- .../stages/VerificationCompleteView.ts | 4 ++-- .../verification/stages/VerifyEmojisView.ts | 4 ++-- .../stages/WaitingForOtherUserView.ts | 4 ++-- 11 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/domain/session/verification/stages/SelectMethodViewModel.ts b/src/domain/session/verification/stages/SelectMethodViewModel.ts index 681a2e468f..e88d0f83c0 100644 --- a/src/domain/session/verification/stages/SelectMethodViewModel.ts +++ b/src/domain/session/verification/stages/SelectMethodViewModel.ts @@ -47,4 +47,8 @@ export class SelectMethodViewModel extends ErrorReportViewModel { - render(t, vm) { + render(t: Builder) { return t.div({ className: { "middle": true, @@ -36,21 +31,22 @@ export class DeviceVerificationView extends TemplateView vm.currentStageViewModel, (stageVm) => { - if (stageVm instanceof WaitingForOtherUserViewModel) { + if (stageVm.kind === "waiting-for-user") { return new WaitingForOtherUserView(stageVm); } - else if (stageVm instanceof VerificationCancelledViewModel) { + else if (stageVm.kind === "verification-cancelled") { return new VerificationCancelledView(stageVm); } - else if (stageVm instanceof SelectMethodViewModel) { + else if (stageVm.kind === "select-method") { return new SelectMethodView(stageVm); } - else if (stageVm instanceof VerifyEmojisViewModel) { + else if (stageVm.kind === "verify-emojis") { return new VerifyEmojisView(stageVm); } - else if (stageVm instanceof VerificationCompleteViewModel) { + else if (stageVm.kind === "verification-completed") { return new VerificationCompleteView(stageVm); } + return null; }) ]) } diff --git a/src/platform/web/ui/session/verification/stages/SelectMethodView.ts b/src/platform/web/ui/session/verification/stages/SelectMethodView.ts index 9e665f312d..e760370063 100644 --- a/src/platform/web/ui/session/verification/stages/SelectMethodView.ts +++ b/src/platform/web/ui/session/verification/stages/SelectMethodView.ts @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView} from "../../../general/TemplateView"; +import {Builder, TemplateView} from "../../../general/TemplateView"; import {spinner} from "../../../common.js" import type {SelectMethodViewModel} from "../../../../../../domain/session/verification/stages/SelectMethodViewModel"; export class SelectMethodView extends TemplateView { - render(t) { + render(t: Builder) { return t.div({ className: "SelectMethodView" }, [ t.map(vm => vm.hasProceeded, (hasProceeded, t, vm) => { if (hasProceeded) { diff --git a/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts b/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts index 28cebf8078..d2832ddb9d 100644 --- a/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts +++ b/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView} from "../../../general/TemplateView"; +import {Builder, TemplateView} from "../../../general/TemplateView"; import {VerificationCancelledViewModel} from "../../../../../../domain/session/verification/stages/VerificationCancelledViewModel"; import {CancelReason} from "../../../../../../matrix/verification/SAS/channel/types"; export class VerificationCancelledView extends TemplateView { - render(t, vm: VerificationCancelledViewModel) { + render(t: Builder, vm: VerificationCancelledViewModel) { const headerTextStart = vm.isCancelledByUs ? "You" : "The other device"; return t.div( @@ -50,10 +50,8 @@ export class VerificationCancelledView extends TemplateView { - render(t, vm: VerificationCompleteViewModel) { + render(t: Builder, vm: VerificationCompleteViewModel) { return t.div({ className: "VerificationCompleteView" }, [ t.div({className: "VerificationCompleteView__icon"}), t.div({ className: "VerificationCompleteView__heading" }, [ diff --git a/src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts b/src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts index 9f7b312b46..32aba69178 100644 --- a/src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts +++ b/src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView} from "../../../general/TemplateView"; +import {Builder, TemplateView} from "../../../general/TemplateView"; import {spinner} from "../../../common.js" import type {VerifyEmojisViewModel} from "../../../../../../domain/session/verification/stages/VerifyEmojisViewModel"; export class VerifyEmojisView extends TemplateView { - render(t, vm: VerifyEmojisViewModel) { + render(t: Builder, vm: VerifyEmojisViewModel) { const emojiList = vm.emojis.reduce((acc, [emoji, name]) => { const e = t.div({ className: "EmojiContainer" }, [ t.div({ className: "EmojiContainer__emoji" }, emoji), diff --git a/src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.ts b/src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.ts index 007b258eed..0018a4b3cd 100644 --- a/src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.ts +++ b/src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.ts @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView} from "../../../general/TemplateView"; +import {Builder, TemplateView} from "../../../general/TemplateView"; import {spinner} from "../../../common.js"; import {WaitingForOtherUserViewModel} from "../../../../../../domain/session/verification/stages/WaitingForOtherUserViewModel"; export class WaitingForOtherUserView extends TemplateView { - render(t, vm) { + render(t: Builder, vm: WaitingForOtherUserViewModel) { return t.div({ className: "WaitingForOtherUserView" }, [ t.div({ className: "WaitingForOtherUserView__heading" }, [ spinner(t), From e0b3e9f4c46f7e203997d8864ec263ae7cec479b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Mar 2023 16:52:32 +0530 Subject: [PATCH 123/168] Use optional chaining --- .../verification/DeviceVerificationView.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/platform/web/ui/session/verification/DeviceVerificationView.ts b/src/platform/web/ui/session/verification/DeviceVerificationView.ts index 93672b899e..a64f146470 100644 --- a/src/platform/web/ui/session/verification/DeviceVerificationView.ts +++ b/src/platform/web/ui/session/verification/DeviceVerificationView.ts @@ -30,21 +30,21 @@ export class DeviceVerificationView extends TemplateView vm.currentStageViewModel, (stageVm) => { - if (stageVm.kind === "waiting-for-user") { - return new WaitingForOtherUserView(stageVm); + t.mapView(vm => vm.currentStageViewModel, (vm) => { + if (vm?.kind === "waiting-for-user") { + return new WaitingForOtherUserView(vm); } - else if (stageVm.kind === "verification-cancelled") { - return new VerificationCancelledView(stageVm); + else if (vm?.kind === "verification-cancelled") { + return new VerificationCancelledView(vm); } - else if (stageVm.kind === "select-method") { - return new SelectMethodView(stageVm); + else if (vm?.kind === "select-method") { + return new SelectMethodView(vm); } - else if (stageVm.kind === "verify-emojis") { - return new VerifyEmojisView(stageVm); + else if (vm?.kind === "verify-emojis") { + return new VerifyEmojisView(vm); } - else if (stageVm.kind === "verification-completed") { - return new VerificationCompleteView(stageVm); + else if (vm?.kind === "verification-completed") { + return new VerificationCompleteView(vm); } return null; }) From f822a7a3449705825b4e82a6c89264be3475ebd9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Mar 2023 11:50:20 +0530 Subject: [PATCH 124/168] Wrap in feature flag --- src/domain/session/SessionViewModel.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index e0e7772968..689f7c01df 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -97,11 +97,14 @@ export class SessionViewModel extends ViewModel { })); this._updateJoinRoom(joinRoom.get()); + if (this._client.session.features.crossSigning) { + const verification = this.navigation.observe("device-verification"); - this.track(verification.subscribe((txnId) => { - this._updateVerification(txnId); - })); - this._updateVerification(verification.get()); + this.track(verification.subscribe((txnId) => { + this._updateVerification(txnId); + })); + this._updateVerification(verification.get()); + } const lightbox = this.navigation.observe("lightbox"); this.track(lightbox.subscribe(eventId => { From 53c0fc2934a170c5f07f84275df35f50c4d220bf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Mar 2023 15:08:47 +0530 Subject: [PATCH 125/168] Fix rebase --- src/domain/navigation/index.ts | 2 +- src/domain/session/SessionViewModel.js | 13 ++++++------- .../session/toast/ToastCollectionViewModel.ts | 16 +++++++++++----- .../VerificationToastCollectionViewModel.ts | 15 +++++---------- .../VerificationToastNotificationViewModel.ts | 2 +- .../verification/DeviceVerificationViewModel.ts | 2 +- .../web/ui/session/toast/ToastCollectionView.ts | 3 ++- 7 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 5904e71543..4a573e157f 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -34,7 +34,7 @@ export type SegmentType = { "details": true; "members": true; "member": string; - "device-verification": string; + "device-verification": string | boolean; "join-room": true; }; diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 689f7c01df..b80c1c0560 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -97,13 +97,12 @@ export class SessionViewModel extends ViewModel { })); this._updateJoinRoom(joinRoom.get()); - if (this._client.session.features.crossSigning) { - - const verification = this.navigation.observe("device-verification"); - this.track(verification.subscribe((txnId) => { - this._updateVerification(txnId); - })); - this._updateVerification(verification.get()); + if (this.features.crossSigning) { + const verification = this.navigation.observe("device-verification"); + this.track(verification.subscribe((txnId) => { + this._updateVerification(txnId); + })); + this._updateVerification(verification.get()); } const lightbox = this.navigation.observe("lightbox"); diff --git a/src/domain/session/toast/ToastCollectionViewModel.ts b/src/domain/session/toast/ToastCollectionViewModel.ts index 6b2bdcebaa..0d31b6ee24 100644 --- a/src/domain/session/toast/ToastCollectionViewModel.ts +++ b/src/domain/session/toast/ToastCollectionViewModel.ts @@ -33,10 +33,16 @@ export class ToastCollectionViewModel extends ViewModel { constructor(options: Options) { super(options); const session = this.getOption("session"); - const vms: IToastCollection["toastViewModels"][] = [ - this.track(new CallToastCollectionViewModel(this.childOptions({ session }))), - this.track(new VerificationToastCollectionViewModel(this.childOptions({session}))), - ].map(vm => vm.toastViewModels); - this.toastViewModels = new ConcatList(...vms); + const collectionVms: IToastCollection[] = []; + if (this.features.calls) { + collectionVms.push(this.track(new CallToastCollectionViewModel(this.childOptions({ session })))); + } + if (this.features.crossSigning) { + collectionVms.push(this.track(new VerificationToastCollectionViewModel(this.childOptions({ session })))); + } + const vms: IToastCollection["toastViewModels"][] = collectionVms.map(vm => vm.toastViewModels); + if (vms.length !== 0) { + this.toastViewModels = new ConcatList(...vms); + } } } diff --git a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts index d6b0795bd3..5dfb76eb9f 100644 --- a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts +++ b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts @@ -32,18 +32,13 @@ export class VerificationToastCollectionViewModel extends ViewModel { + this.track(crossSigning.receivedSASVerifications.subscribe(this)); + }) + ); } - async observeSASRequests() { - const session = this.getOption("session"); - if (this.features.crossSigning) { - // todo: hack to wait for crossSigning; remove - await new Promise(r => setTimeout(r, 3000)); - const map = session.crossSigning.receivedSASVerifications; - this.track(map.subscribe(this)); - } - } async onAdd(_, request: SASRequest) { const dismiss = () => { diff --git a/src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts b/src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts index b3d7de3524..9bf1e2f7c1 100644 --- a/src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts +++ b/src/domain/session/toast/verification/VerificationToastNotificationViewModel.ts @@ -22,7 +22,7 @@ type Options = { } & BaseClassOptions; type MinimumNeededSegmentType = { - "device-verification": string; + "device-verification": string | boolean; }; export class VerificationToastNotificationViewModel = Options> extends BaseToastNotificationViewModel { diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 58debd285b..33be113f88 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -51,7 +51,7 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { - const crossSigning = this.session.crossSigning; + const crossSigning = this.session.crossSigning.get(); this.sas = crossSigning.startVerification(requestOrUserId, log); this.addEventListeners(); if (typeof requestOrUserId === "string") { diff --git a/src/platform/web/ui/session/toast/ToastCollectionView.ts b/src/platform/web/ui/session/toast/ToastCollectionView.ts index cf4fd58f09..18a9c8edde 100644 --- a/src/platform/web/ui/session/toast/ToastCollectionView.ts +++ b/src/platform/web/ui/session/toast/ToastCollectionView.ts @@ -41,8 +41,9 @@ export class ToastCollectionView extends TemplateView list: vm.toastViewModels, parentProvidesUpdates: false, }, (vm: CallToastNotificationViewModel) => toastViewModelToView(vm)); + return t.div({ className: "ToastCollectionView" }, [ - t.view(view), + t.if(vm => !!vm.toastViewModels, (t) => t.view(view)), ]); } } From 6e2cd3597fee4a84013063d4a9ac0beffdd44614 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Mar 2023 16:52:25 +0530 Subject: [PATCH 126/168] Fix rebase again --- .../VerificationToastCollectionViewModel.ts | 12 +++++++----- src/matrix/Session.js | 1 + .../SAS/stages/SelectVerificationMethodStage.ts | 6 +++++- .../web/ui/session/toast/ToastCollectionView.ts | 12 ++++++------ 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts index 5dfb76eb9f..2cd3da0188 100644 --- a/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts +++ b/src/domain/session/toast/verification/VerificationToastCollectionViewModel.ts @@ -32,11 +32,13 @@ export class VerificationToastCollectionViewModel extends ViewModel { - this.track(crossSigning.receivedSASVerifications.subscribe(this)); - }) - ); + this.subscribeToSASRequests(); + } + + private async subscribeToSASRequests() { + await this.getOption("session").crossSigning.waitFor(v => !!v).promise; + const crossSigning = this.getOption("session").crossSigning.get(); + this.track(crossSigning.receivedSASVerifications.subscribe(this)); } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index b999ba6b1e..5d16f03b94 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -365,6 +365,7 @@ export class Session { olm: this._olm, olmUtil: this._olmUtil, deviceTracker: this._deviceTracker, + deviceMessageHandler: this._deviceMessageHandler, hsApi: this._hsApi, ownUserId: this.userId, e2eeAccount: this._e2eeAccount diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index db499d2ec8..aa2302fb91 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -86,7 +86,11 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { private async findDeviceName(log: ILogItem) { await log.wrap("SelectVerificationMethodStage.findDeviceName", async () => { const device = await this.options.deviceTracker.deviceForId(this.otherUserId, this.otherUserDeviceId, this.options.hsApi, log); - this.otherDeviceName = device.displayName; + if (!device) { + log.log({ l: "Cannot find device", userId: this.otherUserId, deviceId: this.otherUserDeviceId }); + throw new Error("Cannot find device"); + } + this.otherDeviceName = device.unsigned.device_display_name ?? device.device_id; }) } diff --git a/src/platform/web/ui/session/toast/ToastCollectionView.ts b/src/platform/web/ui/session/toast/ToastCollectionView.ts index 18a9c8edde..7bce15aef7 100644 --- a/src/platform/web/ui/session/toast/ToastCollectionView.ts +++ b/src/platform/web/ui/session/toast/ToastCollectionView.ts @@ -37,13 +37,13 @@ function toastViewModelToView(vm: BaseToastNotificationViewModel): IView { export class ToastCollectionView extends TemplateView { render(t: Builder, vm: ToastCollectionViewModel) { - const view = new ListView({ - list: vm.toastViewModels, - parentProvidesUpdates: false, - }, (vm: CallToastNotificationViewModel) => toastViewModelToView(vm)); - return t.div({ className: "ToastCollectionView" }, [ - t.if(vm => !!vm.toastViewModels, (t) => t.view(view)), + t.ifView(vm => !!vm.toastViewModels, t => { + return new ListView({ + list: vm.toastViewModels, + parentProvidesUpdates: false, + }, (vm: CallToastNotificationViewModel) => toastViewModelToView(vm)); + }), ]); } } From 9080263bc6f4b038aa15729ddc52da8000db7c21 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Mar 2023 17:41:55 +0530 Subject: [PATCH 127/168] Fix SAS failing --- src/domain/session/SessionViewModel.js | 2 +- src/matrix/Session.js | 3 ++- src/matrix/verification/SAS/SASVerification.ts | 3 +++ src/matrix/verification/SAS/stages/SendMacStage.ts | 8 ++++++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index b80c1c0560..dc8589d164 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -347,7 +347,7 @@ export class SessionViewModel extends ViewModel { this._verificationViewModel = this.disposeTracked(this._verificationViewModel); } if (txnId) { - const request = this._client.session.crossSigning.receivedSASVerifications.get(txnId); + const request = this._client.session.crossSigning.get()?.receivedSASVerifications.get(txnId); this._verificationViewModel = this.track(new DeviceVerificationViewModel(this.childOptions({ session: this._client.session, request }))); } this.emitChange("activeMiddleViewModel"); diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 5d16f03b94..c5aedcb38e 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -368,7 +368,8 @@ export class Session { deviceMessageHandler: this._deviceMessageHandler, hsApi: this._hsApi, ownUserId: this.userId, - e2eeAccount: this._e2eeAccount + e2eeAccount: this._e2eeAccount, + deviceId: this.deviceId, }); if (await crossSigning.load(log)) { this._crossSigning.set(crossSigning); diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index d4b1714313..ba2dc713a4 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -170,6 +170,9 @@ export function tests() { device_id: deviceId, keys: { [`ed25519:${deviceId}`]: "D8w9mrokGdEZPdPgrU0kQkYi4vZyzKEBfvGyZsGK7+Q", + }, + unsigned: { + device_display_name: "lala10", } }; }, diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts index 14384d3a25..3973e30927 100644 --- a/src/matrix/verification/SAS/stages/SendMacStage.ts +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -44,8 +44,12 @@ export class SendMacStage extends BaseSASVerificationStage { this.channel.id; const deviceKeyId = `ed25519:${this.ourUserDeviceId}`; - const deviceKeys = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning(); - mac[deviceKeyId] = calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId); + const device = await this.deviceTracker.deviceForId(this.ourUserId, this.ourUserDeviceId, this.hsApi, log); + if (!device) { + log.log({ l: "Fetching device failed", userId: this.ourUserId, deviceId: this.ourUserDeviceId }); + throw new Error("Fetching device for user failed!"); + } + mac[deviceKeyId] = calculateMAC(device.keys[deviceKeyId], baseInfo + deviceKeyId); keyList.push(deviceKeyId); const key = await this.deviceTracker.getCrossSigningKeyForUser(this.ourUserId, KeyUsage.Master, this.hsApi, log); From 6e054fcb805ec3250735590e762504c4923b1c60 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Tue, 28 Mar 2023 17:43:48 +0530 Subject: [PATCH 128/168] Update src/platform/web/ui/css/themes/element/theme.css Co-authored-by: Bruno Windels <274386+bwindels@users.noreply.github.com> --- src/platform/web/ui/css/themes/element/theme.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 11501bce59..5f13bb7ce8 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1477,7 +1477,7 @@ button.RoomDetailsView_row::after { } .VerificationCompleteView__icon { - background: url("./icons//verified.svg?primary=accent-color") no-repeat; + background: url("./icons/verified.svg?primary=accent-color") no-repeat; background-size: contain; width: 128px; height: 128px; From 6fefc1549ef66e71ab0d205973d59269077423f6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Mar 2023 17:45:03 +0530 Subject: [PATCH 129/168] Change method name --- .../verification/DeviceVerificationViewModel.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 33be113f88..4747dfdd43 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -55,7 +55,7 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel { - this.createViewModelAndEmit( + this.updateCurrentStageViewModel( new SelectMethodViewModel(this.childOptions({ sas: this.sas, stage: stage!, })) ); })); this.track(this.sas.disposableOn("EmojiGenerated", (stage) => { - this.createViewModelAndEmit( + this.updateCurrentStageViewModel( new VerifyEmojisViewModel(this.childOptions({ stage: stage!, })) ); })); this.track(this.sas.disposableOn("VerificationCancelled", (cancellation) => { - this.createViewModelAndEmit( + this.updateCurrentStageViewModel( new VerificationCancelledViewModel( this.childOptions({ cancellation: cancellation! }) ) ); })); this.track(this.sas.disposableOn("VerificationCompleted", (deviceId) => { - this.createViewModelAndEmit( + this.updateCurrentStageViewModel( new VerificationCompleteViewModel(this.childOptions({ deviceId: deviceId! })) ); })); } - private createViewModelAndEmit(vm) { + private updateCurrentStageViewModel(vm) { this._currentStageViewModel = this.disposeTracked(this._currentStageViewModel); this._currentStageViewModel = this.track(vm); this.emitChange("currentStageViewModel"); From b2d6a783659b1a3c896595080479ed21fa93b0c1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Mar 2023 17:55:40 +0530 Subject: [PATCH 130/168] Remove property --- .../session/verification/DeviceVerificationViewModel.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 4747dfdd43..76dab1a5fb 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -32,26 +32,24 @@ type Options = BaseOptions & { }; export class DeviceVerificationViewModel extends ErrorReportViewModel { - private session: Session; private sas: SASVerification; private _currentStageViewModel: any; constructor(options: Readonly) { super(options); - this.session = options.session; const sasRequest = options.request; if (options.request) { this.start(sasRequest); } else { // We are about to send the request - this.start(this.session.userId); + this.start(this.getOption("session").userId); } } private async start(requestOrUserId: SASRequest | string) { await this.logAndCatch("DeviceVerificationViewModel.start", (log) => { - const crossSigning = this.session.crossSigning.get(); + const crossSigning = this.getOption("session").crossSigning.get(); this.sas = crossSigning.startVerification(requestOrUserId, log); this.addEventListeners(); if (typeof requestOrUserId === "string") { From 38a82b2cb229dfae0a6bc81288c795399ec45bf0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Mar 2023 17:57:19 +0530 Subject: [PATCH 131/168] Use getter --- .../session/verification/stages/VerifyEmojisViewModel.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/domain/session/verification/stages/VerifyEmojisViewModel.ts b/src/domain/session/verification/stages/VerifyEmojisViewModel.ts index 9f6dc92b04..061a8e08ab 100644 --- a/src/domain/session/verification/stages/VerifyEmojisViewModel.ts +++ b/src/domain/session/verification/stages/VerifyEmojisViewModel.ts @@ -26,12 +26,12 @@ type Options = BaseOptions & { }; export class VerifyEmojisViewModel extends ErrorReportViewModel { - public isWaiting: boolean = false; + private _isWaiting: boolean = false; async setEmojiMatch(match: boolean) { await this.logAndCatch("VerifyEmojisViewModel.setEmojiMatch", async () => { await this.options.stage.setEmojiMatch(match); - this.isWaiting = true; + this._isWaiting = true; this.emitChange("isWaiting"); }); } @@ -43,4 +43,8 @@ export class VerifyEmojisViewModel extends ErrorReportViewModel Date: Tue, 28 Mar 2023 17:59:58 +0530 Subject: [PATCH 132/168] cancellation can be undefined --- .../verification/stages/VerificationCancelledViewModel.ts | 4 ++-- src/matrix/verification/SAS/channel/Channel.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/domain/session/verification/stages/VerificationCancelledViewModel.ts b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts index 9e4d3e919c..75cc0e5d70 100644 --- a/src/domain/session/verification/stages/VerificationCancelledViewModel.ts +++ b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts @@ -25,11 +25,11 @@ type Options = BaseOptions & { export class VerificationCancelledViewModel extends ViewModel { get cancelCode(): CancelReason { - return this.options.cancellation.code; + return this.options.cancellation!.code; } get isCancelledByUs(): boolean { - return this.options.cancellation.cancelledByUs; + return this.options.cancellation!.cancelledByUs; } gotoSettings() { diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index fc2260fa9d..10adbd7f84 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -50,7 +50,7 @@ export interface IChannel { startMessage: any; initiatedByUs: boolean; isCancelled: boolean; - cancellation: { code: CancelReason, cancelledByUs: boolean }; + cancellation?: { code: CancelReason, cancelledByUs: boolean }; id: string; otherUserDeviceId: string; } @@ -80,7 +80,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { public startMessage: any; public id: string; private _initiatedByUs: boolean; - private _cancellation: { code: CancelReason, cancelledByUs: boolean }; + private _cancellation?: { code: CancelReason, cancelledByUs: boolean }; /** * @@ -118,7 +118,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } - get cancellation() { + get cancellation(): IChannel["cancellation"] { return this._cancellation; }; From 6a8007fe28f8b96c6a72448944eab9a2cc705eea Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Mar 2023 18:03:08 +0530 Subject: [PATCH 133/168] Use switch case --- .../verification/DeviceVerificationView.ts | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/platform/web/ui/session/verification/DeviceVerificationView.ts b/src/platform/web/ui/session/verification/DeviceVerificationView.ts index a64f146470..94afbe90ff 100644 --- a/src/platform/web/ui/session/verification/DeviceVerificationView.ts +++ b/src/platform/web/ui/session/verification/DeviceVerificationView.ts @@ -31,22 +31,14 @@ export class DeviceVerificationView extends TemplateView vm.currentStageViewModel, (vm) => { - if (vm?.kind === "waiting-for-user") { - return new WaitingForOtherUserView(vm); + switch (vm) { + case "waiting-for-user": return new WaitingForOtherUserView(vm); + case "verification-cancelled": return new VerificationCancelledView(vm); + case "select-method": return new SelectMethodView(vm); + case "verify-emojis": return new VerifyEmojisView(vm); + case "verification-completed": return new VerificationCompleteView(vm); + default: return null; } - else if (vm?.kind === "verification-cancelled") { - return new VerificationCancelledView(vm); - } - else if (vm?.kind === "select-method") { - return new SelectMethodView(vm); - } - else if (vm?.kind === "verify-emojis") { - return new VerifyEmojisView(vm); - } - else if (vm?.kind === "verification-completed") { - return new VerificationCompleteView(vm); - } - return null; }) ]) } From 9884ee24eb5812bebcc6e466a3716f0a672572a0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Mar 2023 18:13:38 +0530 Subject: [PATCH 134/168] Fix render error --- .../web/ui/session/verification/DeviceVerificationView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/verification/DeviceVerificationView.ts b/src/platform/web/ui/session/verification/DeviceVerificationView.ts index 94afbe90ff..d107ca1347 100644 --- a/src/platform/web/ui/session/verification/DeviceVerificationView.ts +++ b/src/platform/web/ui/session/verification/DeviceVerificationView.ts @@ -31,7 +31,7 @@ export class DeviceVerificationView extends TemplateView vm.currentStageViewModel, (vm) => { - switch (vm) { + switch (vm?.kind) { case "waiting-for-user": return new WaitingForOtherUserView(vm); case "verification-cancelled": return new VerificationCancelledView(vm); case "select-method": return new SelectMethodView(vm); From 7eb1c09a7546514f81b1aff7304c41a0164d946f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Mar 2023 18:51:09 +0530 Subject: [PATCH 135/168] Use e2ee account --- src/matrix/verification/SAS/stages/SendMacStage.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts index 3973e30927..30a45e6e71 100644 --- a/src/matrix/verification/SAS/stages/SendMacStage.ts +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -44,12 +44,8 @@ export class SendMacStage extends BaseSASVerificationStage { this.channel.id; const deviceKeyId = `ed25519:${this.ourUserDeviceId}`; - const device = await this.deviceTracker.deviceForId(this.ourUserId, this.ourUserDeviceId, this.hsApi, log); - if (!device) { - log.log({ l: "Fetching device failed", userId: this.ourUserId, deviceId: this.ourUserDeviceId }); - throw new Error("Fetching device for user failed!"); - } - mac[deviceKeyId] = calculateMAC(device.keys[deviceKeyId], baseInfo + deviceKeyId); + const deviceKeys = this.e2eeAccount.getUnsignedDeviceKey(); + mac[deviceKeyId] = calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId); keyList.push(deviceKeyId); const key = await this.deviceTracker.getCrossSigningKeyForUser(this.ourUserId, KeyUsage.Master, this.hsApi, log); From ce018781f112368685482cfaadcae98d3635f37c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 29 Mar 2023 14:58:43 +0530 Subject: [PATCH 136/168] Make code more clear --- src/matrix/verification/CrossSigning.ts | 49 +++++++++++++++---------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 2273c44210..b75ed1f49f 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -119,25 +119,7 @@ export class CrossSigning { this.deviceMessageHandler = options.deviceMessageHandler; this.deviceMessageHandler.on("message", async ({ unencrypted: unencryptedEvent }) => { - const txnId = unencryptedEvent.content.transaction_id; - if (unencryptedEvent.type === VerificationEventType.Cancel) { - this.receivedSASVerifications.remove(txnId); - return; - } - if (this.sasVerificationInProgress && - ( - !this.sasVerificationInProgress.finished || - // If the start message is for the previous sasverification, ignore it. - this.sasVerificationInProgress.channel.id === txnId - )) { - return; - } - if (unencryptedEvent.type === VerificationEventType.Request || - unencryptedEvent.type === VerificationEventType.Start) { - this.platform.logger.run("Start verification from request", () => { - this.receivedSASVerifications.set(txnId, new SASRequest(unencryptedEvent)); - }); - } + this._handleSASDeviceMessage(unencryptedEvent); }) } @@ -224,6 +206,35 @@ export class CrossSigning { return this.sasVerificationInProgress; } + private _handleSASDeviceMessage(event: any) { + const txnId = event.content.transaction_id; + /** + * If we receive an event for the current/previously finished + * SAS verification, we should ignore it because SASVerification + * object 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. */ async signOwnDevice(log: ILogItem): Promise { return log.wrap("CrossSigning.signOwnDevice", async log => { From 67cc426b855a804323989a2f446e38c9b278c36e Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 30 Mar 2023 09:37:46 +0000 Subject: [PATCH 137/168] Update src/matrix/verification/CrossSigning.ts --- src/matrix/verification/CrossSigning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index b75ed1f49f..b3d214438d 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -210,7 +210,7 @@ export class CrossSigning { const txnId = event.content.transaction_id; /** * If we receive an event for the current/previously finished - * SAS verification, we should ignore it because SASVerification + * SAS verification, we should ignore it because the device channel * object will take care of it (if needed). */ const shouldIgnoreEvent = this.sasVerificationInProgress?.channel.id === txnId; From f158197685d43c83cc636e2a24bc0f0015a60a4a Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 30 Mar 2023 09:37:54 +0000 Subject: [PATCH 138/168] Update src/matrix/verification/CrossSigning.ts --- src/matrix/verification/CrossSigning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index b3d214438d..a6fb309114 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -211,7 +211,7 @@ export class CrossSigning { /** * If we receive an event for the current/previously finished * SAS verification, we should ignore it because the device channel - * object will take care of it (if needed). + * 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; } From 3f5e2af09343c415870beb174c168239479744ee Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 30 Mar 2023 15:47:25 +0530 Subject: [PATCH 139/168] Abort SAS when disposing vm --- .../session/verification/DeviceVerificationViewModel.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts index 76dab1a5fb..3257c78482 100644 --- a/src/domain/session/verification/DeviceVerificationViewModel.ts +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -90,6 +90,13 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel {/** ignore */}); + } + super.dispose(); + } + get currentStageViewModel() { return this._currentStageViewModel; } From 244d56b60fe974d0562d48167e37acd32c9e7943 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 30 Mar 2023 16:09:30 +0530 Subject: [PATCH 140/168] Fix broken tests --- src/matrix/verification/SAS/SASVerification.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index ba2dc713a4..fff697f283 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -142,7 +142,7 @@ export function tests() { await olm.init(); const olmUtil = new Olm.Utility(); const e2eeAccount = { - getDeviceKeysToSignWithCrossSigning: () => { + getUnsignedDeviceKey: () => { return { keys: { [`ed25519:${ourDeviceId}`]: From b8e282377e338c71eaf213de3d3bf848d23d3440 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 30 Mar 2023 16:09:46 +0530 Subject: [PATCH 141/168] Log mac method --- .../verification/SAS/channel/MockChannel.ts | 51 ++++++++++--------- src/matrix/verification/SAS/mac.ts | 9 ++-- .../verification/SAS/stages/SendMacStage.ts | 8 +-- .../verification/SAS/stages/VerifyMacStage.ts | 6 +-- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/matrix/verification/SAS/channel/MockChannel.ts b/src/matrix/verification/SAS/channel/MockChannel.ts index 64ae14564f..cb99013817 100644 --- a/src/matrix/verification/SAS/channel/MockChannel.ts +++ b/src/matrix/verification/SAS/channel/MockChannel.ts @@ -6,6 +6,7 @@ import {CancelReason, VerificationEventType} from "./types"; import {getKeyEd25519Key} from "../../CrossSigning"; import {getDeviceEd25519Key} from "../../../e2ee/common"; import anotherjson from "another-json"; +import {NullLogger} from "../../../../logging/NullLogger"; interface ITestChannel extends IChannel { setOlmSas(olmSas): void; @@ -82,31 +83,33 @@ export class MockChannel implements ITestChannel { private async recalculateMAC() { // We need to replace the mac with calculated mac - const baseInfo = - "MATRIX_KEY_VERIFICATION_MAC" + - this.otherUserId + - this.otherUserDeviceId + - this.ourUserId + - this.ourUserDeviceId + - this.id; - const { content: macContent } = this.receivedMessages.get(VerificationEventType.Mac); - const macMethod = this.acceptMessage.content.message_authentication_code; - const calculateMac = createCalculateMAC(this.olmSas, macMethod); - const input = Object.keys(macContent.mac).sort().join(","); - const properMac = calculateMac(input, baseInfo + "KEY_IDS"); - macContent.keys = properMac; - for (const keyId of Object.keys(macContent.mac)) { - const deviceId = keyId.split(":", 2)[1]; - const device = await this.deviceTracker.deviceForId(this.otherUserDeviceId, deviceId); - if (device) { - macContent.mac[keyId] = calculateMac(getDeviceEd25519Key(device), baseInfo + keyId); + await new NullLogger().run("log", async (log) => { + const baseInfo = + "MATRIX_KEY_VERIFICATION_MAC" + + this.otherUserId + + this.otherUserDeviceId + + this.ourUserId + + this.ourUserDeviceId + + this.id; + const { content: macContent } = this.receivedMessages.get(VerificationEventType.Mac); + const macMethod = this.acceptMessage.content.message_authentication_code; + const calculateMac = createCalculateMAC(this.olmSas, macMethod); + const input = Object.keys(macContent.mac).sort().join(","); + const properMac = calculateMac(input, baseInfo + "KEY_IDS", log); + macContent.keys = properMac; + for (const keyId of Object.keys(macContent.mac)) { + const deviceId = keyId.split(":", 2)[1]; + const device = await this.deviceTracker.deviceForId(this.otherUserDeviceId, deviceId); + if (device) { + macContent.mac[keyId] = calculateMac(getDeviceEd25519Key(device), baseInfo + keyId, log); + } + else { + const key = await this.deviceTracker.getCrossSigningKeyForUser(this.otherUserId); + const masterKey = getKeyEd25519Key(key)!; + macContent.mac[keyId] = calculateMac(masterKey, baseInfo + keyId, log); + } } - else { - const key = await this.deviceTracker.getCrossSigningKeyForUser(this.otherUserId); - const masterKey = getKeyEd25519Key(key)!; - macContent.mac[keyId] = calculateMac(masterKey, baseInfo + keyId); - } - } + }); } setStartMessage(event: any): void { diff --git a/src/matrix/verification/SAS/mac.ts b/src/matrix/verification/SAS/mac.ts index e52e8c2c10..54e1c1e796 100644 --- a/src/matrix/verification/SAS/mac.ts +++ b/src/matrix/verification/SAS/mac.ts @@ -13,6 +13,7 @@ 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 {ILogItem} from "../../../logging/types"; import type {MacMethod} from "./stages/constants"; const macMethods: Record = { @@ -23,8 +24,10 @@ const macMethods: Record = { }; export function createCalculateMAC(olmSAS: Olm.SAS, method: MacMethod) { - return function (input: string, info: string): string { - const mac = olmSAS[macMethods[method]](input, info); - return mac; + return function (input: string, info: string, log: ILogItem): string { + return log.wrap({ l: "calculate MAC", method}, () => { + const mac = olmSAS[macMethods[method]](input, info); + return mac; + }); }; } diff --git a/src/matrix/verification/SAS/stages/SendMacStage.ts b/src/matrix/verification/SAS/stages/SendMacStage.ts index 30a45e6e71..5f8fe87249 100644 --- a/src/matrix/verification/SAS/stages/SendMacStage.ts +++ b/src/matrix/verification/SAS/stages/SendMacStage.ts @@ -32,7 +32,7 @@ export class SendMacStage extends BaseSASVerificationStage { }); } - private async sendMAC(calculateMAC: (input: string, info: string) => string, log: ILogItem): Promise { + private async sendMAC(calculateMAC: (input: string, info: string, log: ILogItem) => string, log: ILogItem): Promise { const mac: Record = {}; const keyList: string[] = []; const baseInfo = @@ -45,7 +45,7 @@ export class SendMacStage extends BaseSASVerificationStage { const deviceKeyId = `ed25519:${this.ourUserDeviceId}`; const deviceKeys = this.e2eeAccount.getUnsignedDeviceKey(); - mac[deviceKeyId] = calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId); + mac[deviceKeyId] = calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId, log); keyList.push(deviceKeyId); const key = await this.deviceTracker.getCrossSigningKeyForUser(this.ourUserId, KeyUsage.Master, this.hsApi, log); @@ -56,11 +56,11 @@ export class SendMacStage extends BaseSASVerificationStage { const crossSigningKey = getKeyEd25519Key(key); if (crossSigningKey) { const crossSigningKeyId = `ed25519:${crossSigningKey}`; - mac[crossSigningKeyId] = calculateMAC(crossSigningKey, baseInfo + crossSigningKeyId); + mac[crossSigningKeyId] = calculateMAC(crossSigningKey, baseInfo + crossSigningKeyId, log); keyList.push(crossSigningKeyId); } - const keys = calculateMAC(keyList.sort().join(","), baseInfo + "KEY_IDS"); + const keys = calculateMAC(keyList.sort().join(","), baseInfo + "KEY_IDS", log); await this.channel.send(VerificationEventType.Mac, { mac, keys }, log); } } diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 40e908c6a2..6d635cce4e 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -35,7 +35,7 @@ export class VerifyMacStage extends BaseSASVerificationStage { }); } - private async checkMAC(calculateMAC: (input: string, info: string) => string, log: ILogItem): Promise { + private async checkMAC(calculateMAC: (input: string, info: string, log: ILogItem) => string, log: ILogItem): Promise { const {content} = this.channel.getReceivedMessage(VerificationEventType.Mac); const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + @@ -45,7 +45,7 @@ export class VerifyMacStage extends BaseSASVerificationStage { this.ourUserDeviceId + this.channel.id; - const calculatedMAC = calculateMAC(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS"); + const calculatedMAC = calculateMAC(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS", log); if (content.keys !== calculatedMAC) { log.log({ l: "MAC verification failed for keys field", keys: content.keys, calculated: calculatedMAC }); this.channel.cancelVerification(CancelReason.KeyMismatch); @@ -53,7 +53,7 @@ export class VerifyMacStage extends BaseSASVerificationStage { } await this.verifyKeys(content.mac, (keyId, key, keyInfo) => { - const calculatedMAC = calculateMAC(key, baseInfo + keyId); + const calculatedMAC = calculateMAC(key, baseInfo + keyId, log); if (keyInfo !== calculatedMAC) { log.log({ l: "Mac verification failed for key", keyMac: keyInfo, calculatedMAC, keyId, key }); this.channel.cancelVerification(CancelReason.KeyMismatch); From 74fe7427af784caa9c3976ed3fcd96f4b2ac70ed Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 30 Mar 2023 14:39:39 +0200 Subject: [PATCH 142/168] sign device or user when mac check out during sas --- src/matrix/verification/CrossSigning.ts | 1 + .../verification/SAS/SASVerification.ts | 4 +++- .../SAS/stages/BaseSASVerificationStage.ts | 2 ++ .../verification/SAS/stages/VerifyMacStage.ts | 23 +++++++++++++------ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index a6fb309114..d9725c9a9d 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -202,6 +202,7 @@ export class CrossSigning { deviceTracker: this.deviceTracker, hsApi: this.hsApi, clock: this.platform.clock, + crossSigning: this, }); return this.sasVerificationInProgress; } diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index fff697f283..14e52007bd 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -29,6 +29,7 @@ import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodSt import {VerificationCancelledError} from "./VerificationCancelledError"; import {EventEmitter} from "../../../utils/EventEmitter"; import {SASProgressEvents} from "./types"; +import type {CrossSigning} from "../CrossSigning"; type Olm = typeof OlmNamespace; @@ -44,6 +45,7 @@ type Options = { deviceTracker: DeviceTracker; hsApi: HomeServerApi; clock: Clock; + crossSigning: CrossSigning } export class SASVerification extends EventEmitter { @@ -60,7 +62,7 @@ export class SASVerification extends EventEmitter { this.olmSas = olmSas; this.channel = channel; this.setupCancelAfterTimeout(clock); - const stageOptions = {...options, olmSas, eventEmitter: this}; + const stageOptions = {...options, olmSas, eventEmitter: this, crossSigning: options.crossSigning}; if (channel.getReceivedMessage(VerificationEventType.Start)) { this.startStage = new SelectVerificationMethodStage(stageOptions); } diff --git a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts index 0eb26a0256..1c2506e093 100644 --- a/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts +++ b/src/matrix/verification/SAS/stages/BaseSASVerificationStage.ts @@ -16,6 +16,7 @@ limitations under the License. import type {ILogItem} from "../../../../logging/types"; import type {Account} from "../../../e2ee/Account.js"; import type {DeviceTracker} from "../../../e2ee/DeviceTracker.js"; +import type {CrossSigning} from "../../CrossSigning"; import {IChannel} from "../channel/Channel"; import {HomeServerApi} from "../../../net/HomeServerApi"; import {SASProgressEvents} from "../types"; @@ -33,6 +34,7 @@ export type Options = { deviceTracker: DeviceTracker; hsApi: HomeServerApi; eventEmitter: EventEmitter + crossSigning: CrossSigning } export abstract class BaseSASVerificationStage { diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 6d635cce4e..7fa66cc5c8 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -21,7 +21,7 @@ import {SendDoneStage} from "./SendDoneStage"; import {KeyUsage, getKeyEd25519Key} from "../../CrossSigning"; import {getDeviceEd25519Key} from "../../../e2ee/common"; -export type KeyVerifier = (keyId: string, device: any, keyInfo: string) => void; +export type KeyVerifier = (keyId: string, publicKey: string, keyInfo: string) => boolean; export class VerifyMacStage extends BaseSASVerificationStage { async completeStage() { @@ -54,11 +54,12 @@ export class VerifyMacStage extends BaseSASVerificationStage { await this.verifyKeys(content.mac, (keyId, key, keyInfo) => { const calculatedMAC = calculateMAC(key, baseInfo + keyId, log); - if (keyInfo !== calculatedMAC) { + const matches = keyInfo === calculatedMAC; + if (!matches) { log.log({ l: "Mac verification failed for key", keyMac: keyInfo, calculatedMAC, keyId, key }); this.channel.cancelVerification(CancelReason.KeyMismatch); - return; } + return matches; }, log); } @@ -68,8 +69,12 @@ export class VerifyMacStage extends BaseSASVerificationStage { const deviceIdOrMSK = keyId.split(":", 2)[1]; const device = await this.deviceTracker.deviceForId(userId, deviceIdOrMSK, this.hsApi, log); if (device) { - verifier(keyId, getDeviceEd25519Key(device), keyInfo); - // todo: mark device as verified here + if (verifier(keyId, getDeviceEd25519Key(device), keyInfo)) { + await log.wrap("signing device", async log => { + const signedKey = await this.options.crossSigning.signDevice(device.device_id, log); + log.set("success", !!signedKey); + }); + } } else { // If we were not able to find the device, then deviceIdOrMSK is actually the MSK! const key = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log); @@ -78,8 +83,12 @@ export class VerifyMacStage extends BaseSASVerificationStage { throw new Error("Fetching MSK for user failed!"); } const masterKey = getKeyEd25519Key(key); - verifier(keyId, masterKey, keyInfo); - // todo: mark user as verified here + if(masterKey && verifier(keyId, masterKey, keyInfo)) { + await log.wrap("signing user", async log => { + const signedKey = await this.options.crossSigning.signUser(userId, log); + log.set("success", !!signedKey); + }); + } } } } From c2b6c44a68dd58eba1dd7b231066579d78b873e3 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 30 Mar 2023 14:40:58 +0200 Subject: [PATCH 143/168] actually, don't need to pass this, it's already in options --- src/matrix/verification/SAS/SASVerification.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 14e52007bd..52ea4ca553 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -62,7 +62,7 @@ export class SASVerification extends EventEmitter { this.olmSas = olmSas; this.channel = channel; this.setupCancelAfterTimeout(clock); - const stageOptions = {...options, olmSas, eventEmitter: this, crossSigning: options.crossSigning}; + const stageOptions = {...options, olmSas, eventEmitter: this}; if (channel.getReceivedMessage(VerificationEventType.Start)) { this.startStage = new SelectVerificationMethodStage(stageOptions); } From ab65745b07867c3906111ce42a1087b18385f010 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 30 Mar 2023 14:45:59 +0200 Subject: [PATCH 144/168] fix tests --- src/matrix/verification/SAS/SASVerification.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 52ea4ca553..b6265db584 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -128,7 +128,6 @@ import {SendDoneStage} from "./stages/SendDoneStage"; import {SendAcceptVerificationStage} from "./stages/SendAcceptVerificationStage"; export function tests() { - async function createSASRequest( ourUserId: string, ourDeviceId: string, @@ -190,6 +189,7 @@ export function tests() { olm, startingMessage, ); + const crossSigning = new MockCrossSigning() as unknown as CrossSigning; const clock = new MockClock(); const logger = new NullLogger(); return logger.run("log", (log) => { @@ -207,6 +207,7 @@ export function tests() { ourUserId, ourUserDeviceId: ourDeviceId, log, + crossSigning }); // @ts-ignore channel.setOlmSas(sas.olmSas); @@ -217,6 +218,16 @@ export function tests() { }); } + class MockCrossSigning { + signDevice(deviceId: string, log: ILogItem) { + return Promise.resolve({}); // device keys, means signing succeeded + } + + signUser(userId: string, log: ILogItem) { + return Promise.resolve({}); // cross-signing keys, means signing succeeded + } + } + return { "Order of stages created matches expected order when I sent request, they sent start": async (assert) => { const ourDeviceId = "ILQHOACESQ"; From f6599708b95079f15c9283822076702ca616efaa Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 30 Mar 2023 17:01:51 +0200 Subject: [PATCH 145/168] implementing observing user trust so UI can update when signing --- .../rightpanel/MemberDetailsViewModel.js | 46 +++++++----- src/matrix/e2ee/DeviceTracker.ts | 10 +++ src/matrix/verification/CrossSigning.ts | 71 +++++++++++++++---- 3 files changed, 95 insertions(+), 32 deletions(-) diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index b73bf4bb88..52cbc7b8df 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -30,20 +30,14 @@ export class MemberDetailsViewModel extends ViewModel { this._session = options.session; this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange())); this.track(this._observableMember.subscribe( () => this._onMemberChange())); - this.track(this._session.crossSigning.subscribe(() => { - this.emitChange("trustShieldColor"); - })); this._userTrust = undefined; - this.init(); // TODO: call this from parent view model and do something smart with error view model if it fails async? - } - - async init() { + this._userTrustSubscription = undefined; if (this.features.crossSigning) { - this._userTrust = await this.logger.run({l: "MemberDetailsViewModel.get user trust", id: this._member.userId}, log => { - return this._session.crossSigning.get()?.getUserTrust(this._member.userId, log); - }); - this.emitChange("trustShieldColor"); + this.track(this._session.crossSigning.subscribe(() => { + this._onCrossSigningChange(); + })); } + this._onCrossSigningChange(); } get name() { return this._member.name; } @@ -51,7 +45,8 @@ export class MemberDetailsViewModel extends ViewModel { get userId() { return this._member.userId; } get trustDescription() { - switch (this._userTrust) { + switch (this._userTrust?.get()) { + case undefined: return this.i18n`Please waitโ€ฆ`; case UserTrust.Trusted: return this.i18n`You have verified this user. This user has verified all of their sessions.`; case UserTrust.UserNotSigned: return this.i18n`You have not verified this user.`; case UserTrust.UserSignatureMismatch: return this.i18n`You appear to have signed this user, but the signature is invalid.`; @@ -59,18 +54,17 @@ export class MemberDetailsViewModel extends ViewModel { case UserTrust.UserDeviceSignatureMismatch: return this.i18n`This user has a session signature that is invalid.`; case UserTrust.UserSetupError: return this.i18n`This user hasn't set up cross-signing correctly`; case UserTrust.OwnSetupError: return this.i18n`Cross-signing wasn't set up correctly on your side.`; - default: return this.i18n`Pendingโ€ฆ`; } } get trustShieldColor() { if (!this._isEncrypted) { - return undefined; + return ""; } - switch (this._userTrust) { + switch (this._userTrust?.get()) { case undefined: case UserTrust.OwnSetupError: - return undefined; + return ""; case UserTrust.Trusted: return "green"; case UserTrust.UserNotSigned: @@ -103,9 +97,10 @@ export class MemberDetailsViewModel extends ViewModel { } async signUser() { - if (this._session.crossSigning) { + const crossSigning = this._session.crossSigning.get(); + if (crossSigning) { await this.logger.run("MemberDetailsViewModel.signUser", async log => { - await this._session.crossSigning.signUser(this.userId, log); + await crossSigning.signUser(this.userId, log); }); } } @@ -150,4 +145,19 @@ export class MemberDetailsViewModel extends ViewModel { } this.navigation.push("room", roomId); } + + _onCrossSigningChange() { + const crossSigning = this._session.crossSigning.get(); + this._userTrustSubscription = this.disposeTracked(this._userTrustSubscription); + this._userTrust = undefined; + if (crossSigning) { + this.logger.run("MemberDetailsViewModel.observeUserTrust", log => { + this._userTrust = crossSigning.observeUserTrust(this.userId, log); + this._userTrustSubscription = this.track(this._userTrust.subscribe(trust => { + this.emitChange("trustShieldColor"); + })); + }); + } + this.emitChange("trustShieldColor"); + } } diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 3a50a890d2..dc3e400844 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -163,6 +163,16 @@ export class DeviceTracker { } } + async invalidateUserKeys(userId: string): Promise { + const txn = await this._storage.readWriteTxn([this._storage.storeNames.userIdentities]); + const userIdentity = await txn.userIdentities.get(userId); + if (userIdentity) { + userIdentity.keysTrackingStatus = KeysTrackingStatus.Outdated; + txn.userIdentities.set(userIdentity); + } + await txn.complete(); + } + async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi | undefined, log: ILogItem): Promise { return await log.wrap({l: "DeviceTracker.getCrossSigningKeyForUser", id: userId, usage}, async log => { const txn = await this._storage.readTxn([ diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index d9725c9a9d..99e07632da 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -15,6 +15,7 @@ limitations under the License. */ import {verifyEd25519Signature, SignatureVerification} from "../e2ee/common"; +import {BaseObservableValue, RetainedObservableValue} from "../../observable/value"; import {pkSign} from "./common"; import {SASVerification} from "./SAS/SASVerification"; import {ToDeviceChannel} from "./SAS/channel/Channel"; @@ -89,6 +90,7 @@ export class CrossSigning { private readonly e2eeAccount: Account; private readonly deviceMessageHandler: DeviceMessageHandler; private _isMasterKeyTrusted: boolean = false; + private readonly observedUsers: Map> = new Map(); private readonly deviceId: string; private sasVerificationInProgress?: SASVerification; public receivedSASVerifications: ObservableMap = new ObservableMap(); @@ -295,50 +297,59 @@ export class CrossSigning { }; const request = this.hsApi.uploadSignatures(payload, {log}); await request.response(); + // we don't write the signatures to storage, as we don't want to have too many special + // cases in the trust algorithm, so instead we just clear the cross signing keys + // so that they will be refetched when trust is recalculated + await this.deviceTracker.invalidateUserKeys(userId); + this.emitUserTrustUpdate(userId, log); return keyToSign; }); } - async getUserTrust(userId: string, log: ILogItem): Promise { - return log.wrap("getUserTrust", async log => { + getUserTrust(userId: string, log: ILogItem): Promise { + return log.wrap("CrossSigning.getUserTrust", async log => { log.set("id", userId); + const logResult = (trust: UserTrust): UserTrust => { + log.set("result", trust); + return trust; + }; if (!this.isMasterKeyTrusted) { - return UserTrust.OwnSetupError; + return logResult(UserTrust.OwnSetupError); } const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log)); if (!ourMSK) { - return UserTrust.OwnSetupError; + return logResult(UserTrust.OwnSetupError); } const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, log)); if (!ourUSK) { - return UserTrust.OwnSetupError; + return logResult(UserTrust.OwnSetupError); } const ourUSKVerification = log.wrap("verify our usk", log => this.hasValidSignatureFrom(ourUSK, ourMSK, log)); if (ourUSKVerification !== SignatureVerification.Valid) { - return UserTrust.OwnSetupError; + return logResult(UserTrust.OwnSetupError); } const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log)); if (!theirMSK) { /* assume that when they don't have an MSK, they've never enabled cross-signing on their client (or it's not supported) rather than assuming a setup error on their side. Later on, for their SSK, we _do_ assume it's a setup error as it doesn't make sense to have an MSK without a SSK */ - return UserTrust.UserNotSigned; + return logResult(UserTrust.UserNotSigned); } const theirMSKVerification = log.wrap("verify their msk", log => this.hasValidSignatureFrom(theirMSK, ourUSK, log)); if (theirMSKVerification !== SignatureVerification.Valid) { if (theirMSKVerification === SignatureVerification.NotSigned) { - return UserTrust.UserNotSigned; + return logResult(UserTrust.UserNotSigned); } else { /* SignatureVerification.Invalid */ - return UserTrust.UserSignatureMismatch; + return logResult(UserTrust.UserSignatureMismatch); } } const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, log)); if (!theirSSK) { - return UserTrust.UserSetupError; + return logResult(UserTrust.UserSetupError); } const theirSSKVerification = log.wrap("verify their ssk", log => this.hasValidSignatureFrom(theirSSK, theirMSK, log)); if (theirSSKVerification !== SignatureVerification.Valid) { - return UserTrust.UserSetupError; + return logResult(UserTrust.UserSetupError); } const theirDeviceKeys = await log.wrap("get their devices", log => this.deviceTracker.devicesForUsers([userId], this.hsApi, log)); const lowestDeviceVerification = theirDeviceKeys.reduce((lowest, dk) => log.wrap({l: "verify device", id: dk.device_id}, log => { @@ -356,13 +367,30 @@ export class CrossSigning { }), SignatureVerification.Valid); if (lowestDeviceVerification !== SignatureVerification.Valid) { if (lowestDeviceVerification === SignatureVerification.NotSigned) { - return UserTrust.UserDeviceNotSigned; + return logResult(UserTrust.UserDeviceNotSigned); } else { /* SignatureVerification.Invalid */ - return UserTrust.UserDeviceSignatureMismatch; + return logResult(UserTrust.UserDeviceSignatureMismatch); } } - return UserTrust.Trusted; + return logResult(UserTrust.Trusted); + }); + } + + observeUserTrust(userId: string, log: ILogItem): BaseObservableValue { + const existingValue = this.observedUsers.get(userId); + if (existingValue) { + return existingValue; + } + const observable = new RetainedObservableValue(undefined, () => { + this.observedUsers.delete(userId); + }); + this.observedUsers.set(userId, observable); + log.wrapDetached("get user trust", async log => { + if (observable.get() === undefined) { + observable.set(await this.getUserTrust(userId, log)); + } }); + return observable; } private async signDeviceKey(keyToSign: DeviceKey, log: ILogItem): Promise { @@ -382,6 +410,11 @@ export class CrossSigning { }; const request = this.hsApi.uploadSignatures(payload, {log}); await request.response(); + // we don't write the signatures to storage, as we don't want to have too many special + // cases in the trust algorithm, so instead we just clear the cross signing keys + // so that they will be refetched when trust is recalculated + await this.deviceTracker.invalidateUserKeys(this.ownUserId); + this.emitUserTrustUpdate(this.ownUserId, log); return keyToSign; } @@ -403,6 +436,16 @@ export class CrossSigning { } return verifyEd25519Signature(this.olmUtil, signingKey.user_id, pubKey, pubKey, key, log); } + + private emitUserTrustUpdate(userId: string, log: ILogItem) { + const observable = this.observedUsers.get(userId); + if (observable && observable.get() !== undefined) { + observable.set(undefined); + log.wrapDetached("update user trust", async log => { + observable.set(await this.getUserTrust(userId, log)); + }); + } + } } export function getKeyUsage(keyInfo: CrossSigningKey): KeyUsage | undefined { From a1b7696b91caacd7d957e069e40d6e0d643a4d89 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 30 Mar 2023 17:08:23 +0200 Subject: [PATCH 146/168] fix lint --- src/domain/session/rightpanel/MemberDetailsViewModel.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index 52cbc7b8df..3b303d0439 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -46,7 +46,6 @@ export class MemberDetailsViewModel extends ViewModel { get trustDescription() { switch (this._userTrust?.get()) { - case undefined: return this.i18n`Please waitโ€ฆ`; case UserTrust.Trusted: return this.i18n`You have verified this user. This user has verified all of their sessions.`; case UserTrust.UserNotSigned: return this.i18n`You have not verified this user.`; case UserTrust.UserSignatureMismatch: return this.i18n`You appear to have signed this user, but the signature is invalid.`; @@ -54,6 +53,9 @@ export class MemberDetailsViewModel extends ViewModel { case UserTrust.UserDeviceSignatureMismatch: return this.i18n`This user has a session signature that is invalid.`; case UserTrust.UserSetupError: return this.i18n`This user hasn't set up cross-signing correctly`; case UserTrust.OwnSetupError: return this.i18n`Cross-signing wasn't set up correctly on your side.`; + case undefined: + default: // adding default as well because jslint can't check for switch exhaustiveness + return this.i18n`Please waitโ€ฆ`; } } @@ -153,7 +155,7 @@ export class MemberDetailsViewModel extends ViewModel { if (crossSigning) { this.logger.run("MemberDetailsViewModel.observeUserTrust", log => { this._userTrust = crossSigning.observeUserTrust(this.userId, log); - this._userTrustSubscription = this.track(this._userTrust.subscribe(trust => { + this._userTrustSubscription = this.track(this._userTrust.subscribe(() => { this.emitChange("trustShieldColor"); })); }); From c8769514f33b67314afaab74475d4de8f71d4f8f Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 31 Mar 2023 11:57:10 +0200 Subject: [PATCH 147/168] adjust comment to reflect which keys we're talking about --- src/matrix/verification/CrossSigning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 99e07632da..591d57cc97 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -411,7 +411,7 @@ export class CrossSigning { const request = this.hsApi.uploadSignatures(payload, {log}); await request.response(); // we don't write the signatures to storage, as we don't want to have too many special - // cases in the trust algorithm, so instead we just clear the cross signing keys + // cases in the trust algorithm, so instead we just clear the device keys // so that they will be refetched when trust is recalculated await this.deviceTracker.invalidateUserKeys(this.ownUserId); this.emitUserTrustUpdate(this.ownUserId, log); From 016f9ff3004ccf807d6fae511141689c3fc62c19 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 31 Mar 2023 15:54:45 +0530 Subject: [PATCH 148/168] Send done before waiting for message --- src/matrix/verification/SAS/stages/SendDoneStage.ts | 3 ++- src/matrix/verification/SAS/stages/VerifyMacStage.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/verification/SAS/stages/SendDoneStage.ts b/src/matrix/verification/SAS/stages/SendDoneStage.ts index 95167e2473..dcdeba3a02 100644 --- a/src/matrix/verification/SAS/stages/SendDoneStage.ts +++ b/src/matrix/verification/SAS/stages/SendDoneStage.ts @@ -19,8 +19,9 @@ import {VerificationEventType} from "../channel/types"; export class SendDoneStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SendDoneStage.completeStage", async (log) => { - this.eventEmitter.emit("VerificationCompleted", this.otherUserDeviceId); await this.channel.send(VerificationEventType.Done, {}, log); + await this.channel.waitForEvent(VerificationEventType.Done); + this.eventEmitter.emit("VerificationCompleted", this.otherUserDeviceId); }); } } diff --git a/src/matrix/verification/SAS/stages/VerifyMacStage.ts b/src/matrix/verification/SAS/stages/VerifyMacStage.ts index 7fa66cc5c8..a1ef55157a 100644 --- a/src/matrix/verification/SAS/stages/VerifyMacStage.ts +++ b/src/matrix/verification/SAS/stages/VerifyMacStage.ts @@ -30,7 +30,6 @@ export class VerifyMacStage extends BaseSASVerificationStage { const macMethod = acceptMessage.message_authentication_code; const calculateMAC = createCalculateMAC(this.olmSAS, macMethod); await this.checkMAC(calculateMAC, log); - await this.channel.waitForEvent(VerificationEventType.Done); this.setNextStage(new SendDoneStage(this.options)); }); } From 7e20440328c425bc75292bf7b0b2e1e954dd79ae Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 31 Mar 2023 17:05:09 +0530 Subject: [PATCH 149/168] Dispose cross-signing --- src/matrix/Session.js | 12 ++++++++++-- src/matrix/verification/CrossSigning.ts | 12 +++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index c5aedcb38e..23c9cce6fe 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -252,7 +252,9 @@ export class Session { this._keyBackup.get().dispose(); this._keyBackup.set(undefined); } - if (this._crossSigning.get()) { + 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); @@ -317,7 +319,9 @@ export class Session { this._keyBackup.get().dispose(); this._keyBackup.set(undefined); } - if (this._crossSigning.get()) { + const crossSigning = this._crossSigning.get(); + if (crossSigning) { + crossSigning.dispose(); this._crossSigning.set(undefined); } } @@ -374,6 +378,9 @@ export class Session { if (await crossSigning.load(log)) { this._crossSigning.set(crossSigning); } + else { + crossSigning.dispose(); + } }); } } catch (err) { @@ -547,6 +554,7 @@ export class Session { this._e2eeAccount = undefined; this._callHandler?.dispose(); this._callHandler = undefined; + this._crossSigning.get()?.dispose(); for (const room of this._rooms.values()) { room.dispose(); } diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 591d57cc97..c23c2f54bc 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -119,10 +119,8 @@ export class CrossSigning { this.deviceId = options.deviceId; this.e2eeAccount = options.e2eeAccount this.deviceMessageHandler = options.deviceMessageHandler; - - this.deviceMessageHandler.on("message", async ({ unencrypted: unencryptedEvent }) => { - this._handleSASDeviceMessage(unencryptedEvent); - }) + this.handleSASDeviceMessage = this.handleSASDeviceMessage.bind(this); + this.deviceMessageHandler.on("message", this.handleSASDeviceMessage); } /** @return {boolean} whether cross signing has been enabled on this account */ @@ -209,7 +207,7 @@ export class CrossSigning { return this.sasVerificationInProgress; } - private _handleSASDeviceMessage(event: any) { + private handleSASDeviceMessage({ unencrypted: event }) { const txnId = event.content.transaction_id; /** * If we receive an event for the current/previously finished @@ -376,6 +374,10 @@ export class CrossSigning { }); } + dispose(): void { + this.deviceMessageHandler.off("message", this.handleSASDeviceMessage); + } + observeUserTrust(userId: string, log: ILogItem): BaseObservableValue { const existingValue = this.observedUsers.get(userId); if (existingValue) { From 457f1e52c99c3a6a03517380dfadafdc222b88d2 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 6 Apr 2023 15:02:56 +0200 Subject: [PATCH 150/168] export BlobHandle and add method to create handle from buffer with unfiltered mimetype (for thirdroom) --- src/lib.ts | 1 + src/platform/web/dom/BlobHandle.js | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/lib.ts b/src/lib.ts index a6f609f4e0..f5e16ecd2a 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -19,6 +19,7 @@ export type {ILogItem} from "./logging/types"; export {IDBLogPersister} from "./logging/IDBLogPersister"; export {ConsoleReporter} from "./logging/ConsoleReporter"; export {Platform} from "./platform/web/Platform.js"; +export {BlobHandle} from "./platform/web/dom/BlobHandle"; export {Client, LoadStatus} from "./matrix/Client.js"; export {RoomStatus} from "./matrix/room/common"; // export everything needed to observe state events on all rooms using session.observeRoomState diff --git a/src/platform/web/dom/BlobHandle.js b/src/platform/web/dom/BlobHandle.js index 32dd94c0e2..932fa53c5c 100644 --- a/src/platform/web/dom/BlobHandle.js +++ b/src/platform/web/dom/BlobHandle.js @@ -74,12 +74,23 @@ const ALLOWED_BLOB_MIMETYPES = { const DEFAULT_MIMETYPE = 'application/octet-stream'; export class BlobHandle { + /** + * @internal + * Don't use the constructor directly, instead use fromBuffer, fromBlob or fromBufferUnsafe + * */ constructor(blob, buffer = null) { this._blob = blob; this._buffer = buffer; this._url = null; } + /** Does not filter out mimetypes that could execute embedded javascript. + * It's up to the callee of this method to ensure that the blob won't be + * rendered by the browser in a way that could allow cross-signing scripting. */ + static fromBufferUnsafe(buffer, mimetype) { + return new BlobHandle(new Blob([buffer], {type: mimetype}), buffer); + } + static fromBuffer(buffer, mimetype) { mimetype = mimetype ? mimetype.split(";")[0].trim() : ''; if (!ALLOWED_BLOB_MIMETYPES[mimetype]) { From 30197107105cec06d96a96a0a11322ee95d360de Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 7 Apr 2023 09:52:08 +0200 Subject: [PATCH 151/168] expose method on BlobHandle to create a handle without mimetype filtering --- src/platform/web/Platform.js | 3 ++- src/platform/web/dom/BlobHandle.js | 15 +++++---------- src/platform/web/dom/ImageHandle.js | 3 ++- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index be8c997078..50ec60a528 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -300,7 +300,8 @@ export class Platform { const file = input.files[0]; this._container.removeChild(input); if (file) { - resolve({name: file.name, blob: BlobHandle.fromBlob(file)}); + // ok to not filter mimetypes as these are local files + resolve({name: file.name, blob: BlobHandle.fromBlobUnsafe(file)}); } else { resolve(); } diff --git a/src/platform/web/dom/BlobHandle.js b/src/platform/web/dom/BlobHandle.js index 932fa53c5c..2d5231e025 100644 --- a/src/platform/web/dom/BlobHandle.js +++ b/src/platform/web/dom/BlobHandle.js @@ -76,7 +76,7 @@ const DEFAULT_MIMETYPE = 'application/octet-stream'; export class BlobHandle { /** * @internal - * Don't use the constructor directly, instead use fromBuffer, fromBlob or fromBufferUnsafe + * Don't use the constructor directly, instead use fromBuffer or fromBlobUnsafe * */ constructor(blob, buffer = null) { this._blob = blob; @@ -84,13 +84,6 @@ export class BlobHandle { this._url = null; } - /** Does not filter out mimetypes that could execute embedded javascript. - * It's up to the callee of this method to ensure that the blob won't be - * rendered by the browser in a way that could allow cross-signing scripting. */ - static fromBufferUnsafe(buffer, mimetype) { - return new BlobHandle(new Blob([buffer], {type: mimetype}), buffer); - } - static fromBuffer(buffer, mimetype) { mimetype = mimetype ? mimetype.split(";")[0].trim() : ''; if (!ALLOWED_BLOB_MIMETYPES[mimetype]) { @@ -99,8 +92,10 @@ export class BlobHandle { return new BlobHandle(new Blob([buffer], {type: mimetype}), buffer); } - static fromBlob(blob) { - // ok to not filter mimetypes as these are local files + /** Does not filter out mimetypes that could execute embedded javascript. + * It's up to the callee of this method to ensure that the blob won't be + * rendered by the browser in a way that could allow cross-signing scripting. */ + static fromBlobUnsafe(blob) { return new BlobHandle(blob); } diff --git a/src/platform/web/dom/ImageHandle.js b/src/platform/web/dom/ImageHandle.js index 4ac3a6cd2e..19fd8c5944 100644 --- a/src/platform/web/dom/ImageHandle.js +++ b/src/platform/web/dom/ImageHandle.js @@ -64,7 +64,8 @@ export class ImageHandle { } else { throw new Error("canvas can't be turned into blob"); } - const blob = BlobHandle.fromBlob(nativeBlob); + // unsafe is ok because it's a jpeg or png image + const blob = BlobHandle.fromBlobUnsafe(nativeBlob); return new ImageHandle(blob, scaledWidth, scaledHeight, null); } From ee5105c04a77a7738ca90bc91f8759250811541d Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 7 Apr 2023 16:45:30 +0200 Subject: [PATCH 152/168] sdk version 0.1.2 --- scripts/sdk/base-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index 92df93c53d..b5e6e2d90b 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -1,7 +1,7 @@ { "name": "hydrogen-view-sdk", "description": "Embeddable matrix client library, including view components", - "version": "0.1.1", + "version": "0.1.2", "main": "./lib-build/hydrogen.cjs.js", "exports": { ".": { From 82a7c9d4bfcdc3e2f5628be04abb7b66cdde5220 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Apr 2023 11:18:35 +0530 Subject: [PATCH 153/168] Use new prop names --- src/matrix/common.js | 4 ++-- src/matrix/e2ee/RoomEncryption.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/matrix/common.js b/src/matrix/common.js index 7cd72ae1ac..489846bb71 100644 --- a/src/matrix/common.js +++ b/src/matrix/common.js @@ -33,11 +33,11 @@ export function isTxnId(txnId) { } export function formatToDeviceMessagesPayload(messages) { - const messagesByUser = groupBy(messages, message => message.device.userId); + const messagesByUser = groupBy(messages, message => message.device.user_id); const payload = { messages: Array.from(messagesByUser.entries()).reduce((userMap, [userId, messages]) => { userMap[userId] = messages.reduce((deviceMap, message) => { - deviceMap[message.device.deviceId] = message.content; + deviceMap[message.device.device_id] = message.content; return deviceMap; }, {}); return userMap; diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index bd0defaccd..241ee83c9a 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -353,7 +353,7 @@ export class RoomEncryption { this._historyVisibility = await this._loadHistoryVisibilityIfNeeded(this._historyVisibility); await this._deviceTracker.trackRoom(this._room, this._historyVisibility, log); const devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi, log); - const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set())); + const userIds = Array.from(devices.reduce((set, device) => set.add(device.user_id), new Set())); let writeOpTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]); let operation; @@ -431,8 +431,8 @@ export class RoomEncryption { await log.wrap("send", log => this._sendMessagesToDevices(ENCRYPTED_TYPE, messages, hsApi, log)); if (missingDevices.length) { await log.wrap("missingDevices", async log => { - log.set("devices", missingDevices.map(d => d.deviceId)); - const unsentUserIds = operation.userIds.filter(userId => missingDevices.some(d => d.userId === userId)); + log.set("devices", missingDevices.map(d => d.device_id)); + const unsentUserIds = operation.userIds.filter(userId => missingDevices.some(d => d.user_id === userId)); log.set("unsentUserIds", unsentUserIds); operation.userIds = unsentUserIds; // first remove the users that we've sent the keys already from the operation, @@ -459,11 +459,11 @@ export class RoomEncryption { // TODO: make this use _sendMessagesToDevices async _sendSharedMessageToDevices(type, message, devices, hsApi, log) { - const devicesByUser = groupBy(devices, device => device.userId); + const devicesByUser = groupBy(devices, device => device.user_id); const payload = { messages: Array.from(devicesByUser.entries()).reduce((userMap, [userId, devices]) => { userMap[userId] = devices.reduce((deviceMap, device) => { - deviceMap[device.deviceId] = message; + deviceMap[device.device_id] = message; return deviceMap; }, {}); return userMap; From b52489cc9096fddd93fc5254bb5f3120d5763468 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 11 Apr 2023 16:22:20 +0200 Subject: [PATCH 154/168] we pass Member as a DeviceKey here, so also create the right getters --- src/matrix/calls/group/Member.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/matrix/calls/group/Member.ts b/src/matrix/calls/group/Member.ts index 5d67bafe3e..d08a398853 100644 --- a/src/matrix/calls/group/Member.ts +++ b/src/matrix/calls/group/Member.ts @@ -183,6 +183,16 @@ export class Member { return this.callDeviceMembership.device_id; } + /** @internal, to emulate deviceKey properties when calling formatToDeviceMessagesPayload */ + get user_id(): string { + return this.userId; + } + + /** @internal, to emulate deviceKey properties when calling formatToDeviceMessagesPayload */ + get device_id(): string { + return this.deviceId; + } + /** session id of the member */ get sessionId(): string { return this.callDeviceMembership.session_id; From 2ae2c217561b3ef28dc3d75a3df9f8535863bf97 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 17 Apr 2023 13:07:42 +0530 Subject: [PATCH 155/168] Export contents from ./feature --- src/lib.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.ts b/src/lib.ts index f5e16ecd2a..072d082db4 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -22,6 +22,7 @@ export {Platform} from "./platform/web/Platform.js"; export {BlobHandle} from "./platform/web/dom/BlobHandle"; export {Client, LoadStatus} from "./matrix/Client.js"; export {RoomStatus} from "./matrix/room/common"; +export {FeatureSet, FeatureFlag} from "./features"; // export everything needed to observe state events on all rooms using session.observeRoomState export type {RoomStateHandler} from "./matrix/room/state/types"; export type {MemberChange} from "./matrix/room/members/RoomMember"; From 13d198d7bc065ab1146c7b1816a82b980ebaf422 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 17 Apr 2023 18:38:47 +0530 Subject: [PATCH 156/168] Update docs --- doc/SDK.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/SDK.md b/doc/SDK.md index ba021ad149..ec4f41133c 100644 --- a/doc/SDK.md +++ b/doc/SDK.md @@ -32,7 +32,8 @@ import { createRouter, RoomViewModel, TimelineView, - viewClassForTile + viewClassForTile, + FeatureSet } from "hydrogen-view-sdk"; import downloadSandboxPath from 'hydrogen-view-sdk/download-sandbox.html?url'; import workerPath from 'hydrogen-view-sdk/main.js?url'; @@ -81,12 +82,14 @@ async function main() { const {session} = client; // looks for room corresponding to #element-dev:matrix.org, assuming it is already joined const room = session.rooms.get("!bEWtlqtDwCLFIAKAcv:matrix.org"); + const features = await FeatureSet.load(platform.settingsStorage); const vm = new RoomViewModel({ room, ownUserId: session.userId, platform, urlRouter: urlRouter, navigation, + features, }); await vm.load(); const view = new TimelineView(vm.timelineViewModel, viewClassForTile); From a35f6af7db1795e16822a088f129b43841856b80 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 17 Apr 2023 20:23:16 +0530 Subject: [PATCH 157/168] Add command to install olm as well --- doc/SDK.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/SDK.md b/doc/SDK.md index ec4f41133c..7782625397 100644 --- a/doc/SDK.md +++ b/doc/SDK.md @@ -15,6 +15,7 @@ yarn create vite cd yarn yarn add hydrogen-view-sdk +yarn add https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz ``` You should see a `index.html` in the project root directory, containing an element with `id="app"`. Add the attribute `class="hydrogen"` to this element, as the CSS we'll include from the SDK assumes for now that the app is rendered in an element with this classname. From 1686b3df073273b0563164548ee26394c841f06a Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 26 Apr 2023 16:21:50 -0500 Subject: [PATCH 158/168] Accommodate long dates in sticky date headers Example: `Wednesday, November 16, 2022` --- src/platform/web/ui/css/themes/element/timeline.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 4a82260572..91c069d323 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -433,7 +433,7 @@ only loads when the top comes into view*/ .DateHeader time { margin: 0 auto; padding: 12px 4px; - width: 250px; + max-width: 350px; padding: 12px; display: block; color: var(--light-text-color); From 99e67fedc3baa4621d8fc2d6021fbf5550fcc4e3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 1 May 2023 23:44:42 +0530 Subject: [PATCH 159/168] Load call handler before using it --- src/matrix/Session.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 23c9cce6fe..8fa9ef595e 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -78,6 +78,9 @@ export class Session { this._roomsBeingCreated = new ObservableMap(); this._user = new User(sessionInfo.userId); this._roomStateHandler = new RoomStateHandlerSet(); + if (features.calls) { + this._setupCallHandler(); + } this._deviceMessageHandler = new DeviceMessageHandler({storage, callHandler: this._callHandler}); this._olm = olm; this._olmUtil = null; @@ -106,10 +109,6 @@ export class Session { this._createRoomEncryption = this._createRoomEncryption.bind(this); this._forgetArchivedRoom = this._forgetArchivedRoom.bind(this); this.needsKeyBackup = new ObservableValue(false); - - if (features.calls) { - this._setupCallHandler(); - } } get fingerprintKey() { From 72c6172a4550710f750790d5ca9c900adeb0384a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 3 May 2023 00:01:59 +0530 Subject: [PATCH 160/168] Implement a method to discard logs --- src/logging/LogItem.ts | 11 +++++++++++ src/logging/types.ts | 1 + src/matrix/calls/group/GroupCall.ts | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/src/logging/LogItem.ts b/src/logging/LogItem.ts index 5aaabcc45a..91ad4e8fb5 100644 --- a/src/logging/LogItem.ts +++ b/src/logging/LogItem.ts @@ -28,6 +28,7 @@ export class LogItem implements ILogItem { protected _logger: Logger; private _filterCreator?: FilterCreator; private _children?: Array; + private _discard: boolean = false; constructor(labelOrValues: LabelOrValues, logLevel: LogLevel, logger: Logger, filterCreator?: FilterCreator) { this._logger = logger; @@ -38,6 +39,13 @@ export class LogItem implements ILogItem { this._filterCreator = filterCreator; } + /** + * Prevents this log item from being present in the exported output. + */ + discard(): void { + this._discard = true; + } + /** start a new root log item and run it detached mode, see Logger.runDetached */ runDetached(labelOrValues: LabelOrValues, callback: LogCallback, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem { return this._logger.runDetached(labelOrValues, callback, logLevel, filterCreator); @@ -119,6 +127,9 @@ export class LogItem implements ILogItem { } serialize(filter: LogFilter, parentStartTime: number | undefined, forced: boolean): ISerializedItem | undefined { + if (this._discard) { + return; + } if (this._filterCreator) { try { filter = this._filterCreator(new LogFilter(filter), this); diff --git a/src/logging/types.ts b/src/logging/types.ts index 5443642396..7e81d92918 100644 --- a/src/logging/types.ts +++ b/src/logging/types.ts @@ -55,6 +55,7 @@ export interface ILogItem { finish(): void; forceFinish(): void; child(labelOrValues: LabelOrValues, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem; + discard(): void; } /* extend both ILogger and ILogItem from this interface, but need to rename ILogger.run => wrap then. Or both to `span`? diff --git a/src/matrix/calls/group/GroupCall.ts b/src/matrix/calls/group/GroupCall.ts index a47a3ff291..2a580c5cf9 100644 --- a/src/matrix/calls/group/GroupCall.ts +++ b/src/matrix/calls/group/GroupCall.ts @@ -424,6 +424,11 @@ export class GroupCall extends EventEmitter<{change: never}> { member.dispose(); this._members.remove(memberKey); log.set("removed", true); + } else { + // We don't want to pollute the logs with all the expired members. + // This can be an issue for long lived calls that have had a large number + // of users join and leave at some point in time. + log.discard(); } return; } From d9bdf1156d912d01532ab489c775fc25a560c010 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 3 May 2023 14:30:51 +0530 Subject: [PATCH 161/168] Add discard method to NullLogItem --- src/logging/NullLogger.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/logging/NullLogger.ts b/src/logging/NullLogger.ts index 83e5fc0037..0952704b09 100644 --- a/src/logging/NullLogger.ts +++ b/src/logging/NullLogger.ts @@ -74,6 +74,10 @@ export class NullLogItem implements ILogItem { this.logger = logger; } + discard(): void { + // noop + } + wrap(_: LabelOrValues, callback: LogCallback): T { return this.run(callback); } From f28b23b81b326dd88814fb1779429a45cfaa7cc5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 4 May 2023 22:05:12 +0530 Subject: [PATCH 162/168] Add workflow file --- .github/workflows/sdk-release.js.yml | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/sdk-release.js.yml diff --git a/.github/workflows/sdk-release.js.yml b/.github/workflows/sdk-release.js.yml new file mode 100644 index 0000000000..85d35d141e --- /dev/null +++ b/.github/workflows/sdk-release.js.yml @@ -0,0 +1,46 @@ +# Must only be called from `release#published` triggers +name: Publish to npm +on: + push: + tags: + - sdk-v** +jobs: + npm: + name: Publish to npm + runs-on: ubuntu-latest + steps: + - name: ๐Ÿงฎ Checkout code + uses: actions/checkout@v3 + + - name: ๐Ÿ”ง Yarn cache + uses: actions/setup-node@v3 + with: + cache: "yarn" + registry-url: "https://registry.npmjs.org" + + - name: ๐Ÿ”จ Install dependencies + run: "yarn install --prefer-offline --frozen-lockfile" + + - name: Run Unit tests + run: "yarn test" + + - name: Run Lint Checks + run: "yarn run lint-ci" + + - name: Run Typescript Checks + run: "yarn run tsc" + + - name: Run SDK tests + run: "yarn test:sdk" + + - name: Build SDK + run: "yarn build:sdk" + + - name: ๐Ÿš€ Publish to npm + id: npm-publish + uses: JS-DevTools/npm-publish@v2 + with: + token: ${{ secrets.NPM_TOKEN }} + access: public + package: ./target + dry-run: true From d31ba7cf8c8bf195ff3e0b3799f7854d3e5c834c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 5 May 2023 00:48:16 +0530 Subject: [PATCH 163/168] Remove comment --- .github/workflows/sdk-release.js.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/sdk-release.js.yml b/.github/workflows/sdk-release.js.yml index 85d35d141e..37ca691465 100644 --- a/.github/workflows/sdk-release.js.yml +++ b/.github/workflows/sdk-release.js.yml @@ -1,9 +1,8 @@ -# Must only be called from `release#published` triggers name: Publish to npm on: push: tags: - - sdk-v** + - "sdk-v**" jobs: npm: name: Publish to npm From fcd7f91ce9b451140decd2299f26ac31fd1d572e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 5 May 2023 01:32:18 +0530 Subject: [PATCH 164/168] Lock node version --- .github/workflows/sdk-release.js.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/sdk-release.js.yml b/.github/workflows/sdk-release.js.yml index 37ca691465..c6403ae565 100644 --- a/.github/workflows/sdk-release.js.yml +++ b/.github/workflows/sdk-release.js.yml @@ -7,6 +7,11 @@ jobs: npm: name: Publish to npm runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.1.0] + steps: - name: ๐Ÿงฎ Checkout code uses: actions/checkout@v3 From c6fa81b72439b4ee021f0b1ba324fc873a5b05e6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 5 May 2023 01:40:02 +0530 Subject: [PATCH 165/168] Use setup node --- .github/workflows/sdk-release.js.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/sdk-release.js.yml b/.github/workflows/sdk-release.js.yml index c6403ae565..e11ae692a1 100644 --- a/.github/workflows/sdk-release.js.yml +++ b/.github/workflows/sdk-release.js.yml @@ -16,6 +16,11 @@ jobs: - name: ๐Ÿงฎ Checkout code uses: actions/checkout@v3 + - name: Install tools + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - name: ๐Ÿ”ง Yarn cache uses: actions/setup-node@v3 with: From c45a84e11049c5cd4c7fab6a49b63444fbf457c4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 5 May 2023 16:54:26 +0530 Subject: [PATCH 166/168] Check if e2eeAccount is available first --- src/matrix/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 8fa9ef595e..2f4e0f1d05 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -795,7 +795,7 @@ export class Session { // to-device messages, to help us avoid throwing away one-time-keys that we // are about to receive messages for // (https://github.com/vector-im/riot-web/issues/2782). - if (!isCatchupSync) { + if (this._e2eeAccount && !isCatchupSync) { const needsToUploadOTKs = await this._e2eeAccount.generateOTKsIfNeeded(this._storage, log); if (needsToUploadOTKs) { await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log)); From afc5b68e54af4c2573c8964bf73643b321b85930 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 8 May 2023 17:52:34 +0530 Subject: [PATCH 167/168] export submitLogsToRageshakeServer --- src/lib.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.ts b/src/lib.ts index 238fd85afa..74282b4e62 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -28,6 +28,7 @@ export {CallIntent} from "./matrix/calls/callEventTypes"; export {OidcApi} from "./matrix/net/OidcApi"; export {OIDCLoginMethod} from "./matrix/login/OIDCLoginMethod"; export { makeTxnId } from './matrix/common' +export { submitLogsToRageshakeServer } from './domain/rageshake' // export everything needed to observe state events on all rooms using session.observeRoomState export type {RoomStateHandler} from "./matrix/room/state/types"; export type {MemberChange} from "./matrix/room/members/RoomMember"; From c20e2913ae7dfc99f3ebdc1242021e1e4a2143bb Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Thu, 11 May 2023 08:58:57 +0530 Subject: [PATCH 168/168] fix member dispose mutating global member options --- src/matrix/calls/group/Member.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/matrix/calls/group/Member.ts b/src/matrix/calls/group/Member.ts index d08a398853..9c9c7713d0 100644 --- a/src/matrix/calls/group/Member.ts +++ b/src/matrix/calls/group/Member.ts @@ -33,6 +33,8 @@ import type {ILogItem} from "../../../logging/types"; import type {BaseObservableValue} from "../../../observable/value"; import type {Clock, Timeout} from "../../../platform/web/dom/Clock"; +export type MemberUpdateEmitter = (participant: Member, params?: any) => void; + export type Options = Omit & { confId: string, ownUserId: string, @@ -41,7 +43,7 @@ export type Options = Omit, log: ILogItem) => Promise, - emitUpdate: (participant: Member, params?: any) => void, + emitUpdate: MemberUpdateEmitter, clock: Clock } @@ -105,8 +107,9 @@ class MemberConnection { export class Member { private connection?: MemberConnection; private expireTimeout?: Timeout; + private emitMemberUpdate: MemberUpdateEmitter; private errorBoundary = new ErrorBoundary(err => { - this.options.emitUpdate(this, "error"); + this.emitMemberUpdate(this, "error"); if (this.connection) { // in case the error happens in code that does not log, // log it here to make sure it isn't swallowed @@ -120,6 +123,7 @@ export class Member { private options: Options, updateMemberLog: ILogItem ) { + this.emitMemberUpdate = this.options.emitUpdate; this._renewExpireTimeout(updateMemberLog); } @@ -144,7 +148,7 @@ export class Member { // add 10ms to make sure isExpired returns true this.expireTimeout = this.options.clock.createTimeout(expiresFromNow + 10); this.expireTimeout.elapsed().then( - () => { this.options.emitUpdate(this, "isExpired"); }, + () => { this.emitMemberUpdate(this, "isExpired"); }, (err) => { /* ignore abort error */ }, ); } @@ -285,7 +289,7 @@ export class Member { updateRoomMember(roomMember: RoomMember) { this.member = roomMember; // TODO: this emits an update during the writeSync phase, which we usually try to avoid - this.options.emitUpdate(this); + this.emitMemberUpdate(this); } /** @internal */ @@ -317,7 +321,7 @@ export class Member { }); } } - this.options.emitUpdate(this, params); + this.emitMemberUpdate(this, params); } /** @internal */ @@ -466,8 +470,8 @@ export class Member { this.connection = undefined; this.expireTimeout?.dispose(); this.expireTimeout = undefined; - // ensure the emitUpdate callback can't be called anymore - this.options.emitUpdate = () => {}; + // ensure the emitMemberUpdate callback can't be called anymore + this.emitMemberUpdate = () => {}; } }