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 01/20] 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 | 151 ++++++++++-------- 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, 221 insertions(+), 80 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 b669629e..5d991fcb 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,67 +329,63 @@ 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) { - return false; - } - const key = keyInfo.keys[keyIds[0]]; - keys.set(userId, key); - return keys; - }, 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)) { + 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; + } + } + return true; + } + /** * @return {Array<{userId, verifiedKeys: Array>} */ @@ -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 cc3bfff5..9c5fe66c 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 e1e34917..adebcdd6 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 7a8de420..532ffd1d 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 d88f535e..3d1e714f 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 00000000..a2fa9ecb --- /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 db480dd0..d3b6bc90 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 02/20] 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 d335336d..4fb48713 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 0238f0cf..b0dd1546 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 83ad7a1e..146a1ad3 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 5d991fcb..c8e9df09 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 b74dc710..47112892 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 9c5fe66c..63cb389d 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 ce56cca7..f433a7d1 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 5fd1f25b..0b552387 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 5302dad8..164854ad 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 4ea25389..9931be83 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 b2c9dafb..4e908aa2 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 2ce8b5dd..1174d09d 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 adebcdd6..bf9ce39b 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 532ffd1d..4c76608c 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 3d1e714f..200f4089 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 a2fa9ecb..dc5804ef 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 2936f079..897d6453 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 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 03/20] 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 | 152 ++++++++++++------ src/matrix/verification/common.ts | 20 +-- 6 files changed, 115 insertions(+), 82 deletions(-) diff --git a/src/matrix/e2ee/Account.js b/src/matrix/e2ee/Account.js index b0dd1546..8fa2db02 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 c8e9df09..2b2728e1 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 63cb389d..27078135 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 dc5804ef..32100aca 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 d3b6bc90..e58c89e8 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 => { - if (!this._isMasterKeyTrusted) { - log.set("mskNotTrusted", true); - return; - } - const deviceKey = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning(); - const signedDeviceKey = await this.signDeviceData(deviceKey); - const payload = { - [signedDeviceKey["user_id"]]: { - [signedDeviceKey["device_id"]]: signedDeviceKey - } - }; - const request = this.hsApi.uploadSignatures(payload, {log}); - await request.response(); - }); - } - - 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)); - pkSign(this.olm, data, seed, this.ownUserId, ""); - return data as T & { signatures: ISignatures }; - } - 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 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 = { + [keyToSign.user_id]: { + [getKeyEd25519Key(keyToSign)!]: keyToSign + } + }; + const request = this.hsApi.uploadSignatures(payload, {log}); + await request.response(); + return keyToSign; + }); + } + + 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 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; + } } -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 369b5618..de9b1b1b 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 04/20] 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 f6e7cad7..78b384ab 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 35f713f6..9aa0494d 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 2b2728e1..0464377a 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 47112892..bd0defac 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 e139e8c9..c2d56207 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 eb5f68d3..681344fe 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 b45ab6dd..24226e25 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 ca294460..72af718c 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 0f96f2fc..e1546b0b 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 0b552387..c4fee911 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 9931be83..7ab8a209 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 b87d7a88..47da3c03 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 62608683..8e1619ca 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 fd4c2245..02f3290e 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 200f4089..c8d260cd 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 9ae9bb7e..24b7099a 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 05/20] 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 32100aca..bbda15c0 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 06/20] 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 e58c89e8..a3bd5f47 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 07/20] 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 a3bd5f47..6430c0a7 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 08/20] 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 6430c0a7..67a5616a 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 09/20] 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 67a5616a..77b489d2 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 10/20] 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 b3c8278c..774f64af 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 4c617386..ca64e15a 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 5d2f9387..72bd9e37 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 11/20] 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 72bd9e37..caa8037f 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 12/20] 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 0464377a..bbfec900 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 13/20] 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 c8d260cd..ce825edb 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 14/20] 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 bbfec900..98221523 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 ce825edb..c4c4bd61 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 1c55baf0..76bb2080 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 15/20] 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 c4c4bd61..191d3c1d 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 16/20] 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 191d3c1d..fe7cd900 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 17/20] 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 fe7cd900..9b4d5547 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 18/20] 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 a68a80b3..6a886e3a 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 19/20] 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 77b489d2..f98c469a 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 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 20/20] 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 f98c469a..2a4d21f2 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";