diff --git a/src/domain/session/room/timeline/tiles/MessageTile.js b/src/domain/session/room/timeline/tiles/MessageTile.js index 5b3bb603..99513fbe 100644 --- a/src/domain/session/room/timeline/tiles/MessageTile.js +++ b/src/domain/session/room/timeline/tiles/MessageTile.js @@ -71,6 +71,10 @@ export class MessageTile extends SimpleTile { return this._isContinuation; } + get isUnverified() { + return this._entry.isUnverified; + } + _getContent() { return this._entry.content; } diff --git a/src/matrix/DeviceMessageHandler.js b/src/matrix/DeviceMessageHandler.js index d8698127..e0398256 100644 --- a/src/matrix/DeviceMessageHandler.js +++ b/src/matrix/DeviceMessageHandler.js @@ -41,13 +41,20 @@ export class DeviceMessageHandler { // we don't handle anything other for now } - async _writeDecryptedEvents(payloads, txn) { - const megOlmRoomKeysPayloads = payloads.filter(p => { - return p.event?.type === "m.room_key" && p.event.content?.algorithm === MEGOLM_ALGORITHM; + /** + * [_writeDecryptedEvents description] + * @param {Array} olmResults + * @param {[type]} txn [description] + * @return {[type]} [description] + */ + async _writeDecryptedEvents(olmResults, txn) { + const megOlmRoomKeysResults = olmResults.filter(r => { + return r.event?.type === "m.room_key" && r.event.content?.algorithm === MEGOLM_ALGORITHM; }); let roomKeys; - if (megOlmRoomKeysPayloads.length) { - roomKeys = await this._megolmDecryption.addRoomKeys(megOlmRoomKeysPayloads, txn); + if (megOlmRoomKeysResults.length) { + console.log("new room keys", megOlmRoomKeysResults); + roomKeys = await this._megolmDecryption.addRoomKeys(megOlmRoomKeysResults, txn); } return {roomKeys}; } @@ -84,7 +91,7 @@ export class DeviceMessageHandler { ]); let changes; try { - changes = await this._writeDecryptedEvents(decryptChanges.payloads, txn); + changes = await this._writeDecryptedEvents(decryptChanges.results, txn); decryptChanges.write(txn); txn.session.remove(PENDING_ENCRYPTED_EVENTS); } catch (err) { diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 7917baf4..9b64115f 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -89,7 +89,7 @@ export class SessionContainer { let sessionInfo; try { const hsApi = new HomeServerApi({homeServer, request: this._request, createTimeout: this._clock.createTimeout}); - const loginData = await hsApi.passwordLogin(username, password).response(); + const loginData = await hsApi.passwordLogin(username, password, "Hydrogen").response(); const sessionId = this.createNewSessionId(); sessionInfo = { id: sessionId, diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index 09ed1824..4198618a 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -135,6 +135,7 @@ export class Sync { storeNames.userIdentities, storeNames.inboundGroupSessions, storeNames.groupSessionDecryptions, + storeNames.deviceIdentities, ]); const roomChanges = []; let sessionChanges; diff --git a/src/matrix/e2ee/DecryptionResult.js b/src/matrix/e2ee/DecryptionResult.js new file mode 100644 index 00000000..c109e689 --- /dev/null +++ b/src/matrix/e2ee/DecryptionResult.js @@ -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. +*/ + + +/** + * @property {object} event the plaintext event (type and content property) + * @property {string} senderCurve25519Key the curve25519 sender key of the olm event + * @property {string} claimedEd25519Key The ed25519 fingerprint key retrieved from the decryption payload. + * The sender of the olm event claims this is the ed25519 fingerprint key + * that matches the curve25519 sender key. + * The caller needs to check if this key does indeed match the senderKey + * for a device with a valid signature returned from /keys/query, + * see DeviceTracker + */ + + + +export class DecryptionResult { + constructor(event, senderCurve25519Key, claimedKeys) { + this.event = event; + this.senderCurve25519Key = senderCurve25519Key; + this.claimedEd25519Key = claimedKeys.ed25519; + this._device = null; + this._roomTracked = true; + } + + setDevice(device) { + this._device = device; + } + + setRoomNotTrackedYet() { + this._roomTracked = false; + } + + get isVerified() { + if (this._device) { + const comesFromDevice = this._device.ed25519Key === this.claimedEd25519Key; + return comesFromDevice; + } + return false; + } + + get isUnverified() { + if (this._device) { + return !this.isVerified; + } else if (this.isVerificationUnknown) { + return false; + } else { + return true; + } + } + + get isVerificationUnknown() { + // verification is unknown if we haven't yet fetched the devices for the room + return !this._device && !this._roomTracked; + } +} diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 84da2f37..6b6f3894 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -26,8 +26,8 @@ function deviceKeysAsDeviceIdentity(deviceSection) { return { userId, deviceId, - ed25519Key: deviceSection.keys?.[`ed25519:${deviceId}`], - curve25519Key: deviceSection.keys?.[`curve25519:${deviceId}`], + ed25519Key: deviceSection.keys[`ed25519:${deviceId}`], + curve25519Key: deviceSection.keys[`curve25519:${deviceId}`], algorithms: deviceSection.algorithms, displayName: deviceSection.unsigned?.device_display_name, }; @@ -200,6 +200,11 @@ export class DeviceTracker { 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; + } // don't store our own device if (userId === this._ownUserId && deviceId === this._ownDeviceId) { return false; @@ -270,4 +275,8 @@ export class DeviceTracker { }); return devices; } + + async getDeviceByCurve25519Key(curve25519Key, txn) { + return await txn.deviceIdentities.getByCurve25519Key(curve25519Key); + } } diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 729e1bee..7d540784 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -34,29 +34,47 @@ export class RoomEncryption { this._megolmSyncCache = this._megolmDecryption.createSessionCache(); // not `event_id`, but an internal event id passed in to the decrypt methods this._eventIdsByMissingSession = new Map(); + this._senderDeviceCache = new Map(); } notifyTimelineClosed() { // empty the backfill cache when closing the timeline this._megolmBackfillCache.dispose(); this._megolmBackfillCache = this._megolmDecryption.createSessionCache(); + this._senderDeviceCache = new Map(); // purge the sender device cache } async writeMemberChanges(memberChanges, txn) { return await this._deviceTracker.writeMemberChanges(this._room, memberChanges, txn); } - async decrypt(event, isSync, retryData, txn) { + async decrypt(event, isSync, isTimelineOpen, retryData, txn) { if (event.content?.algorithm !== MEGOLM_ALGORITHM) { throw new Error("Unsupported algorithm: " + event.content?.algorithm); } let sessionCache = isSync ? this._megolmSyncCache : this._megolmBackfillCache; - const payload = await this._megolmDecryption.decrypt( + const result = await this._megolmDecryption.decrypt( this._room.id, event, sessionCache, txn); - if (!payload) { + if (!result) { this._addMissingSessionEvent(event, isSync, retryData); } - return payload; + if (result && isTimelineOpen) { + await this._verifyDecryptionResult(result, txn); + } + return result; + } + + async _verifyDecryptionResult(result, txn) { + let device = this._senderDeviceCache.get(result.senderCurve25519Key); + if (!device) { + device = await this._deviceTracker.getDeviceByCurve25519Key(result.senderCurve25519Key, txn); + this._senderDeviceCache.set(result.senderCurve25519Key, device); + } + if (device) { + result.setDevice(device); + } else if (!this._room.isTrackingMembers) { + result.setRoomNotTrackedYet(); + } } _addMissingSessionEvent(event, isSync, data) { diff --git a/src/matrix/e2ee/megolm/Decryption.js b/src/matrix/e2ee/megolm/Decryption.js index 79a58ffa..bd3665b3 100644 --- a/src/matrix/e2ee/megolm/Decryption.js +++ b/src/matrix/e2ee/megolm/Decryption.js @@ -15,6 +15,7 @@ limitations under the License. */ import {DecryptionError} from "../common.js"; +import {DecryptionResult} from "../DecryptionResult.js"; const CACHE_MAX_SIZE = 10; @@ -28,6 +29,14 @@ export class Decryption { return new SessionCache(); } + /** + * [decrypt description] + * @param {[type]} roomId [description] + * @param {[type]} event [description] + * @param {[type]} sessionCache [description] + * @param {[type]} txn [description] + * @return {DecryptionResult?} the decrypted event result, or undefined if the session id is not known. + */ async decrypt(roomId, event, sessionCache, txn) { const senderKey = event.content?.["sender_key"]; const sessionId = event.content?.["session_id"]; @@ -41,8 +50,13 @@ export class Decryption { throw new DecryptionError("MEGOLM_INVALID_EVENT", event); } - let session = sessionCache.get(roomId, senderKey, sessionId); - if (!session) { + let session; + let claimedKeys; + const cacheEntry = sessionCache.get(roomId, senderKey, sessionId); + if (cacheEntry) { + session = cacheEntry.session; + claimedKeys = cacheEntry.claimedKeys; + } else { const sessionEntry = await txn.inboundGroupSessions.get(roomId, senderKey, sessionId); if (sessionEntry) { session = new this._olm.InboundGroupSession(); @@ -52,7 +66,8 @@ export class Decryption { session.free(); throw err; } - sessionCache.add(roomId, senderKey, session); + claimedKeys = sessionEntry.claimedKeys; + sessionCache.add(roomId, senderKey, session, claimedKeys); } } if (!session) { @@ -70,8 +85,7 @@ export class Decryption { {encryptedRoomId: payload.room_id, eventRoomId: roomId}); } await this._handleReplayAttack(roomId, sessionId, messageIndex, event, txn); - // TODO: verify event came from said senderKey - return payload; + return new DecryptionResult(payload, senderKey, claimedKeys); } async _handleReplayAttack(roomId, sessionId, messageIndex, event, txn) { @@ -82,7 +96,11 @@ export class Decryption { // the one with the newest timestamp should be the attack const decryptedEventIsBad = decryption.timestamp < timestamp; const badEventId = decryptedEventIsBad ? eventId : decryption.eventId; - throw new DecryptionError("MEGOLM_REPLAYED_INDEX", event, {badEventId, otherEventId: decryption.eventId}); + throw new DecryptionError("MEGOLM_REPLAYED_INDEX", event, { + messageIndex, + badEventId, + otherEventId: decryption.eventId + }); } if (!decryption) { txn.groupSessionDecryptions.set({ @@ -95,9 +113,19 @@ export class Decryption { } } - async addRoomKeys(payloads, txn) { + /** + * @type {MegolmInboundSessionDescription} + * @property {string} senderKey the sender key of the session + * @property {string} sessionId the session identifier + * + * Adds room keys as inbound group sessions + * @param {Array} decryptionResults an array of m.room_key decryption results. + * @param {[type]} txn a storage transaction with read/write on inboundGroupSessions + * @return {Promise>} an array with the newly added sessions + */ + async addRoomKeys(decryptionResults, txn) { const newSessions = []; - for (const {senderKey, event} of payloads) { + for (const {senderCurve25519Key: senderKey, event, claimedEd25519Key} of decryptionResults) { const roomId = event.content?.["room_id"]; const sessionId = event.content?.["session_id"]; const sessionKey = event.content?.["session_key"]; @@ -122,7 +150,7 @@ export class Decryption { senderKey, sessionId, session: session.pickle(this._pickleKey), - claimedKeys: event.keys, + claimedKeys: {ed25519: claimedEd25519Key}, }; txn.inboundGroupSessions.set(sessionEntry); newSessions.push(sessionEntry); @@ -142,6 +170,17 @@ class SessionCache { this._sessions = []; } + /** + * @type {CacheEntry} + * @property {InboundGroupSession} session the unpickled session + * @property {Object} claimedKeys an object with the claimed ed25519 key + * + * + * @param {string} roomId + * @param {string} senderKey + * @param {string} sessionId + * @return {CacheEntry?} + */ get(roomId, senderKey, sessionId) { const idx = this._sessions.findIndex(s => { return s.roomId === roomId && @@ -155,13 +194,13 @@ class SessionCache { this._sessions.splice(idx, 1); this._sessions.unshift(entry); } - return entry.session; + return entry; } } - add(roomId, senderKey, session) { + add(roomId, senderKey, session, claimedKeys) { // add new at top - this._sessions.unshift({roomId, senderKey, session}); + this._sessions.unshift({roomId, senderKey, session, claimedKeys}); if (this._sessions.length > CACHE_MAX_SIZE) { // free sessions we're about to remove for (let i = CACHE_MAX_SIZE; i < this._sessions.length; i += 1) { diff --git a/src/matrix/e2ee/megolm/Encryption.js b/src/matrix/e2ee/megolm/Encryption.js index 9849ce55..d3c613f6 100644 --- a/src/matrix/e2ee/megolm/Encryption.js +++ b/src/matrix/e2ee/megolm/Encryption.js @@ -26,6 +26,14 @@ export class Encryption { this._ownDeviceId = ownDeviceId; } + /** + * Encrypts a message with megolm + * @param {string} roomId + * @param {string} type event type to encrypt + * @param {string} content content to encrypt + * @param {object} encryptionParams the content of the m.room.encryption event + * @return {Promise} + */ async encrypt(roomId, type, content, encryptionParams) { let session = new this._olm.OutboundGroupSession(); try { @@ -145,6 +153,14 @@ export class Encryption { } } +/** + * @property {object?} roomKeyMessage if encrypting this message + * created a new outbound session, + * this contains the content of the m.room_key message + * that should be sent out over olm. + * @property {object} content the encrypted message as the content of + * the m.room.encrypted event that should be sent out + */ class EncryptionResult { constructor(content, roomKeyMessage) { this.content = content; diff --git a/src/matrix/e2ee/olm/Decryption.js b/src/matrix/e2ee/olm/Decryption.js index dde9522c..b3f47cd6 100644 --- a/src/matrix/e2ee/olm/Decryption.js +++ b/src/matrix/e2ee/olm/Decryption.js @@ -17,6 +17,7 @@ limitations under the License. import {DecryptionError} from "../common.js"; import {groupBy} from "../../../utils/groupBy.js"; import {Session} from "./Session.js"; +import {DecryptionResult} from "../DecryptionResult.js"; const SESSION_LIMIT_PER_SENDER_KEY = 4; @@ -50,6 +51,13 @@ export class Decryption { // and also can avoid side-effects before all can be stored this way // // doing it one by one would be possible, but we would lose the opportunity for parallelization + // + + /** + * [decryptAll description] + * @param {[type]} events + * @return {Promise} [description] + */ async decryptAll(events) { const eventsPerSenderKey = groupBy(events, event => event.content?.["sender_key"]); const timestamp = this._now(); @@ -61,15 +69,16 @@ export class Decryption { try { const readSessionsTxn = await this._storage.readTxn([this._storage.storeNames.olmSessions]); // decrypt events for different sender keys in parallel - const results = await Promise.all(Array.from(eventsPerSenderKey.entries()).map(([senderKey, events]) => { + const senderKeyOperations = await Promise.all(Array.from(eventsPerSenderKey.entries()).map(([senderKey, events]) => { return this._decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn); })); - const payloads = results.reduce((all, r) => all.concat(r.payloads), []); - const errors = results.reduce((all, r) => all.concat(r.errors), []); - const senderKeyDecryptions = results.map(r => r.senderKeyDecryption); - return new DecryptionChanges(senderKeyDecryptions, payloads, errors, this._account, locks); + const results = senderKeyOperations.reduce((all, r) => all.concat(r.results), []); + const errors = senderKeyOperations.reduce((all, r) => all.concat(r.errors), []); + const senderKeyDecryptions = senderKeyOperations.map(r => r.senderKeyDecryption); + return new DecryptionChanges(senderKeyDecryptions, results, errors, this._account, locks); } catch (err) { // make sure the locks are release if something throws + // otherwise they will be released in DecryptionChanges after having written for (const lock of locks) { lock.release(); } @@ -80,18 +89,18 @@ export class Decryption { async _decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn) { const sessions = await this._getSessions(senderKey, readSessionsTxn); const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this._olm, timestamp); - const payloads = []; + const results = []; const errors = []; // events for a single senderKey need to be decrypted one by one for (const event of events) { try { - const payload = this._decryptForSenderKey(senderKeyDecryption, event, timestamp); - payloads.push(payload); + const result = this._decryptForSenderKey(senderKeyDecryption, event, timestamp); + results.push(result); } catch (err) { errors.push(err); } } - return {payloads, errors, senderKeyDecryption}; + return {results, errors, senderKeyDecryption}; } _decryptForSenderKey(senderKeyDecryption, event, timestamp) { @@ -118,7 +127,7 @@ export class Decryption { throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, err}); } this._validatePayload(payload, event); - return {event: payload, senderKey}; + return new DecryptionResult(payload, senderKey, payload.keys); } else { throw new DecryptionError("OLM_NO_MATCHING_SESSION", event, {knownSessionIds: senderKeyDecryption.sessions.map(s => s.id)}); @@ -182,9 +191,9 @@ export class Decryption { if (!payload.content) { throw new DecryptionError("missing content on payload", event, {payload}); } - // TODO: how important is it to verify the message? - // we should look at payload.keys.ed25519 for that... and compare it to the key we have fetched - // from /keys/query, which we might not have done yet at this point. + if (typeof payload.keys?.ed25519 !== "string") { + throw new DecryptionError("Missing or invalid claimed ed25519 key on payload", event, {payload}); + } } } @@ -252,11 +261,15 @@ class SenderKeyDecryption { } } +/** + * @property {Array} results + * @property {Array} errors see DecryptionError.event to retrieve the event that failed to decrypt. + */ class DecryptionChanges { - constructor(senderKeyDecryptions, payloads, errors, account, locks) { + constructor(senderKeyDecryptions, results, errors, account, locks) { this._senderKeyDecryptions = senderKeyDecryptions; this._account = account; - this.payloads = payloads; + this.results = results; this.errors = errors; this._locks = locks; } diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index b1a89634..649a7462 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -141,14 +141,15 @@ export class HomeServerApi { {}, {}, options); } - passwordLogin(username, password, options = null) { + passwordLogin(username, password, initialDeviceDisplayName, options = null) { return this._post("/login", null, { "type": "m.login.password", "identifier": { "type": "m.id.user", "user": username }, - "password": password + "password": password, + "initial_device_display_name": initialDeviceDisplayName }, options); } diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 4ec6e43c..3942f1c5 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -26,7 +26,6 @@ import {fetchOrLoadMembers} from "./members/load.js"; import {MemberList} from "./members/MemberList.js"; import {Heroes} from "./members/Heroes.js"; import {EventEntry} from "./timeline/entries/EventEntry.js"; -import {EventKey} from "./timeline/EventKey.js"; export class Room extends EventEmitter { constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user, createRoomEncryption}) { @@ -58,13 +57,14 @@ export class Room extends EventEmitter { this._storage.storeNames.timelineEvents, this._storage.storeNames.inboundGroupSessions, this._storage.storeNames.groupSessionDecryptions, + this._storage.storeNames.deviceIdentities, ]); try { for (const retryEntry of retryEntries) { const {data: eventKey} = retryEntry; let entry = this._timeline?.findEntry(eventKey); if (!entry) { - const storageEntry = await txn.timelineEvents.get(this._roomId, eventKey.fragmentId, eventKey.entryIndex); + const storageEntry = await txn.timelineEvents.get(this._roomId, eventKey); if (storageEntry) { entry = new EventEntry(storageEntry, this._fragmentIdComparer); } @@ -101,12 +101,10 @@ export class Room extends EventEmitter { async _decryptEntry(entry, txn, isSync) { if (entry.eventType === "m.room.encrypted") { try { - const {fragmentId, entryIndex} = entry; - const key = new EventKey(fragmentId, entryIndex); - const decryptedEvent = await this._roomEncryption.decrypt( - entry.event, isSync, key, txn); - if (decryptedEvent) { - entry.replaceWithDecrypted(decryptedEvent); + const decryptionResult = await this._roomEncryption.decrypt( + entry.event, isSync, !!this._timeline, entry.asEventKey(), txn); + if (decryptionResult) { + entry.setDecryptionResult(decryptionResult); } } catch (err) { console.warn("event decryption error", err, entry.event); @@ -283,6 +281,7 @@ export class Room extends EventEmitter { stores = stores.concat([ this._storage.storeNames.inboundGroupSessions, this._storage.storeNames.groupSessionDecryptions, + this._storage.storeNames.deviceIdentities, ]); } const txn = await this._storage.readWriteTxn(stores); diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index d6d2f335..8c7029d4 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -22,7 +22,7 @@ export class EventEntry extends BaseEntry { super(fragmentIdComparer); this._eventEntry = eventEntry; this._decryptionError = null; - this._decryptedEvent = null; + this._decryptionResult = null; } get event() { @@ -38,15 +38,16 @@ export class EventEntry extends BaseEntry { } get content() { - return this._decryptedEvent?.content || this._eventEntry.event.content; + return this._decryptionResult?.event?.content || this._eventEntry.event.content; } get prevContent() { + // doesn't look at _decryptionResult because state events are not encrypted return getPrevContentFromStateEvent(this._eventEntry.event); } get eventType() { - return this._decryptedEvent?.type || this._eventEntry.event.type; + return this._decryptionResult?.event?.type || this._eventEntry.event.type; } get stateKey() { @@ -73,8 +74,20 @@ export class EventEntry extends BaseEntry { return this._eventEntry.event.event_id; } - replaceWithDecrypted(event) { - this._decryptedEvent = event; + setDecryptionResult(result) { + this._decryptionResult = result; + } + + get isEncrypted() { + return this._eventEntry.event.type === "m.room.encrypted"; + } + + get isVerified() { + return this.isEncrypted && this._decryptionResult?.isVerified; + } + + get isUnverified() { + return this.isEncrypted && this._decryptionResult?.isUnverified; } setDecryptionError(err) { diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js index 6b3ab23e..4446eaf1 100644 --- a/src/matrix/room/timeline/persistence/TimelineReader.js +++ b/src/matrix/room/timeline/persistence/TimelineReader.js @@ -38,6 +38,7 @@ export class TimelineReader { this._storage.storeNames.timelineFragments, this._storage.storeNames.inboundGroupSessions, this._storage.storeNames.groupSessionDecryptions, + this._storage.storeNames.deviceIdentities, ]); } else { diff --git a/src/matrix/storage/idb/schema.js b/src/matrix/storage/idb/schema.js index 63458916..f1817c24 100644 --- a/src/matrix/storage/idb/schema.js +++ b/src/matrix/storage/idb/schema.js @@ -14,6 +14,7 @@ export const schema = [ createInboundGroupSessionsStore, createOutboundGroupSessionsStore, createGroupSessionDecryptions, + addSenderKeyIndexToDeviceStore ]; // TODO: how to deal with git merge conflicts of this array? @@ -94,3 +95,9 @@ function createOutboundGroupSessionsStore(db) { function createGroupSessionDecryptions(db) { db.createObjectStore("groupSessionDecryptions", {keyPath: "key"}); } + +//v9 +function addSenderKeyIndexToDeviceStore(db, txn) { + const deviceIdentities = txn.objectStore("deviceIdentities"); + deviceIdentities.createIndex("byCurve25519Key", "curve25519Key", {unique: true}); +} diff --git a/src/matrix/storage/idb/stores/DeviceIdentityStore.js b/src/matrix/storage/idb/stores/DeviceIdentityStore.js index aec337fc..d1bf7eaa 100644 --- a/src/matrix/storage/idb/stores/DeviceIdentityStore.js +++ b/src/matrix/storage/idb/stores/DeviceIdentityStore.js @@ -38,4 +38,8 @@ export class DeviceIdentityStore { deviceIdentity.key = encodeKey(deviceIdentity.userId, deviceIdentity.deviceId); return this._store.put(deviceIdentity); } + + getByCurve25519Key(curve25519Key) { + return this._store.index("byCurve25519Key").get(curve25519Key); + } } diff --git a/src/ui/web/css/themes/element/theme.css b/src/ui/web/css/themes/element/theme.css index bf81bcd0..bdcd599f 100644 --- a/src/ui/web/css/themes/element/theme.css +++ b/src/ui/web/css/themes/element/theme.css @@ -373,6 +373,10 @@ ul.Timeline > li.continuation time { color: #ccc; } +.TextMessageView.unverified .message-container { + color: #ff4b55; +} + .message-container p { margin: 3px 0; line-height: 2.2rem; diff --git a/src/ui/web/session/room/timeline/common.js b/src/ui/web/session/room/timeline/common.js index 36ccb624..00751f4c 100644 --- a/src/ui/web/session/room/timeline/common.js +++ b/src/ui/web/session/room/timeline/common.js @@ -22,6 +22,7 @@ export function renderMessage(t, vm, children) { "TextMessageView": true, own: vm.isOwn, pending: vm.isPending, + unverified: vm.isUnverified, continuation: vm => vm.isContinuation, };