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 {