mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 11:35:04 +01:00
Merge pull request #86 from vector-im/bwindels/verify-events
Verify events come from the device/fingerprint key they claim
This commit is contained in:
commit
7da4f5c9ce
@ -71,6 +71,10 @@ export class MessageTile extends SimpleTile {
|
|||||||
return this._isContinuation;
|
return this._isContinuation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isUnverified() {
|
||||||
|
return this._entry.isUnverified;
|
||||||
|
}
|
||||||
|
|
||||||
_getContent() {
|
_getContent() {
|
||||||
return this._entry.content;
|
return this._entry.content;
|
||||||
}
|
}
|
||||||
|
@ -41,13 +41,20 @@ export class DeviceMessageHandler {
|
|||||||
// we don't handle anything other for now
|
// we don't handle anything other for now
|
||||||
}
|
}
|
||||||
|
|
||||||
async _writeDecryptedEvents(payloads, txn) {
|
/**
|
||||||
const megOlmRoomKeysPayloads = payloads.filter(p => {
|
* [_writeDecryptedEvents description]
|
||||||
return p.event?.type === "m.room_key" && p.event.content?.algorithm === MEGOLM_ALGORITHM;
|
* @param {Array<DecryptionResult>} 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;
|
let roomKeys;
|
||||||
if (megOlmRoomKeysPayloads.length) {
|
if (megOlmRoomKeysResults.length) {
|
||||||
roomKeys = await this._megolmDecryption.addRoomKeys(megOlmRoomKeysPayloads, txn);
|
console.log("new room keys", megOlmRoomKeysResults);
|
||||||
|
roomKeys = await this._megolmDecryption.addRoomKeys(megOlmRoomKeysResults, txn);
|
||||||
}
|
}
|
||||||
return {roomKeys};
|
return {roomKeys};
|
||||||
}
|
}
|
||||||
@ -84,7 +91,7 @@ export class DeviceMessageHandler {
|
|||||||
]);
|
]);
|
||||||
let changes;
|
let changes;
|
||||||
try {
|
try {
|
||||||
changes = await this._writeDecryptedEvents(decryptChanges.payloads, txn);
|
changes = await this._writeDecryptedEvents(decryptChanges.results, txn);
|
||||||
decryptChanges.write(txn);
|
decryptChanges.write(txn);
|
||||||
txn.session.remove(PENDING_ENCRYPTED_EVENTS);
|
txn.session.remove(PENDING_ENCRYPTED_EVENTS);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -89,7 +89,7 @@ export class SessionContainer {
|
|||||||
let sessionInfo;
|
let sessionInfo;
|
||||||
try {
|
try {
|
||||||
const hsApi = new HomeServerApi({homeServer, request: this._request, createTimeout: this._clock.createTimeout});
|
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();
|
const sessionId = this.createNewSessionId();
|
||||||
sessionInfo = {
|
sessionInfo = {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
|
@ -135,6 +135,7 @@ export class Sync {
|
|||||||
storeNames.userIdentities,
|
storeNames.userIdentities,
|
||||||
storeNames.inboundGroupSessions,
|
storeNames.inboundGroupSessions,
|
||||||
storeNames.groupSessionDecryptions,
|
storeNames.groupSessionDecryptions,
|
||||||
|
storeNames.deviceIdentities,
|
||||||
]);
|
]);
|
||||||
const roomChanges = [];
|
const roomChanges = [];
|
||||||
let sessionChanges;
|
let sessionChanges;
|
||||||
|
70
src/matrix/e2ee/DecryptionResult.js
Normal file
70
src/matrix/e2ee/DecryptionResult.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -26,8 +26,8 @@ function deviceKeysAsDeviceIdentity(deviceSection) {
|
|||||||
return {
|
return {
|
||||||
userId,
|
userId,
|
||||||
deviceId,
|
deviceId,
|
||||||
ed25519Key: deviceSection.keys?.[`ed25519:${deviceId}`],
|
ed25519Key: deviceSection.keys[`ed25519:${deviceId}`],
|
||||||
curve25519Key: deviceSection.keys?.[`curve25519:${deviceId}`],
|
curve25519Key: deviceSection.keys[`curve25519:${deviceId}`],
|
||||||
algorithms: deviceSection.algorithms,
|
algorithms: deviceSection.algorithms,
|
||||||
displayName: deviceSection.unsigned?.device_display_name,
|
displayName: deviceSection.unsigned?.device_display_name,
|
||||||
};
|
};
|
||||||
@ -200,6 +200,11 @@ export class DeviceTracker {
|
|||||||
if (deviceIdOnKeys !== deviceId) {
|
if (deviceIdOnKeys !== deviceId) {
|
||||||
return false;
|
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
|
// don't store our own device
|
||||||
if (userId === this._ownUserId && deviceId === this._ownDeviceId) {
|
if (userId === this._ownUserId && deviceId === this._ownDeviceId) {
|
||||||
return false;
|
return false;
|
||||||
@ -270,4 +275,8 @@ export class DeviceTracker {
|
|||||||
});
|
});
|
||||||
return devices;
|
return devices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDeviceByCurve25519Key(curve25519Key, txn) {
|
||||||
|
return await txn.deviceIdentities.getByCurve25519Key(curve25519Key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,29 +34,47 @@ export class RoomEncryption {
|
|||||||
this._megolmSyncCache = this._megolmDecryption.createSessionCache();
|
this._megolmSyncCache = this._megolmDecryption.createSessionCache();
|
||||||
// not `event_id`, but an internal event id passed in to the decrypt methods
|
// not `event_id`, but an internal event id passed in to the decrypt methods
|
||||||
this._eventIdsByMissingSession = new Map();
|
this._eventIdsByMissingSession = new Map();
|
||||||
|
this._senderDeviceCache = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyTimelineClosed() {
|
notifyTimelineClosed() {
|
||||||
// empty the backfill cache when closing the timeline
|
// empty the backfill cache when closing the timeline
|
||||||
this._megolmBackfillCache.dispose();
|
this._megolmBackfillCache.dispose();
|
||||||
this._megolmBackfillCache = this._megolmDecryption.createSessionCache();
|
this._megolmBackfillCache = this._megolmDecryption.createSessionCache();
|
||||||
|
this._senderDeviceCache = new Map(); // purge the sender device cache
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeMemberChanges(memberChanges, txn) {
|
async writeMemberChanges(memberChanges, txn) {
|
||||||
return await this._deviceTracker.writeMemberChanges(this._room, 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) {
|
if (event.content?.algorithm !== MEGOLM_ALGORITHM) {
|
||||||
throw new Error("Unsupported algorithm: " + event.content?.algorithm);
|
throw new Error("Unsupported algorithm: " + event.content?.algorithm);
|
||||||
}
|
}
|
||||||
let sessionCache = isSync ? this._megolmSyncCache : this._megolmBackfillCache;
|
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);
|
this._room.id, event, sessionCache, txn);
|
||||||
if (!payload) {
|
if (!result) {
|
||||||
this._addMissingSessionEvent(event, isSync, retryData);
|
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) {
|
_addMissingSessionEvent(event, isSync, data) {
|
||||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {DecryptionError} from "../common.js";
|
import {DecryptionError} from "../common.js";
|
||||||
|
import {DecryptionResult} from "../DecryptionResult.js";
|
||||||
|
|
||||||
const CACHE_MAX_SIZE = 10;
|
const CACHE_MAX_SIZE = 10;
|
||||||
|
|
||||||
@ -28,6 +29,14 @@ export class Decryption {
|
|||||||
return new SessionCache();
|
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) {
|
async decrypt(roomId, event, sessionCache, txn) {
|
||||||
const senderKey = event.content?.["sender_key"];
|
const senderKey = event.content?.["sender_key"];
|
||||||
const sessionId = event.content?.["session_id"];
|
const sessionId = event.content?.["session_id"];
|
||||||
@ -41,8 +50,13 @@ export class Decryption {
|
|||||||
throw new DecryptionError("MEGOLM_INVALID_EVENT", event);
|
throw new DecryptionError("MEGOLM_INVALID_EVENT", event);
|
||||||
}
|
}
|
||||||
|
|
||||||
let session = sessionCache.get(roomId, senderKey, sessionId);
|
let session;
|
||||||
if (!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);
|
const sessionEntry = await txn.inboundGroupSessions.get(roomId, senderKey, sessionId);
|
||||||
if (sessionEntry) {
|
if (sessionEntry) {
|
||||||
session = new this._olm.InboundGroupSession();
|
session = new this._olm.InboundGroupSession();
|
||||||
@ -52,7 +66,8 @@ export class Decryption {
|
|||||||
session.free();
|
session.free();
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
sessionCache.add(roomId, senderKey, session);
|
claimedKeys = sessionEntry.claimedKeys;
|
||||||
|
sessionCache.add(roomId, senderKey, session, claimedKeys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@ -70,8 +85,7 @@ export class Decryption {
|
|||||||
{encryptedRoomId: payload.room_id, eventRoomId: roomId});
|
{encryptedRoomId: payload.room_id, eventRoomId: roomId});
|
||||||
}
|
}
|
||||||
await this._handleReplayAttack(roomId, sessionId, messageIndex, event, txn);
|
await this._handleReplayAttack(roomId, sessionId, messageIndex, event, txn);
|
||||||
// TODO: verify event came from said senderKey
|
return new DecryptionResult(payload, senderKey, claimedKeys);
|
||||||
return payload;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _handleReplayAttack(roomId, sessionId, messageIndex, event, txn) {
|
async _handleReplayAttack(roomId, sessionId, messageIndex, event, txn) {
|
||||||
@ -82,7 +96,11 @@ export class Decryption {
|
|||||||
// the one with the newest timestamp should be the attack
|
// the one with the newest timestamp should be the attack
|
||||||
const decryptedEventIsBad = decryption.timestamp < timestamp;
|
const decryptedEventIsBad = decryption.timestamp < timestamp;
|
||||||
const badEventId = decryptedEventIsBad ? eventId : decryption.eventId;
|
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) {
|
if (!decryption) {
|
||||||
txn.groupSessionDecryptions.set({
|
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<OlmDecryptionResult>} decryptionResults an array of m.room_key decryption results.
|
||||||
|
* @param {[type]} txn a storage transaction with read/write on inboundGroupSessions
|
||||||
|
* @return {Promise<Array<MegolmInboundSessionDescription>>} an array with the newly added sessions
|
||||||
|
*/
|
||||||
|
async addRoomKeys(decryptionResults, txn) {
|
||||||
const newSessions = [];
|
const newSessions = [];
|
||||||
for (const {senderKey, event} of payloads) {
|
for (const {senderCurve25519Key: senderKey, event, claimedEd25519Key} of decryptionResults) {
|
||||||
const roomId = event.content?.["room_id"];
|
const roomId = event.content?.["room_id"];
|
||||||
const sessionId = event.content?.["session_id"];
|
const sessionId = event.content?.["session_id"];
|
||||||
const sessionKey = event.content?.["session_key"];
|
const sessionKey = event.content?.["session_key"];
|
||||||
@ -122,7 +150,7 @@ export class Decryption {
|
|||||||
senderKey,
|
senderKey,
|
||||||
sessionId,
|
sessionId,
|
||||||
session: session.pickle(this._pickleKey),
|
session: session.pickle(this._pickleKey),
|
||||||
claimedKeys: event.keys,
|
claimedKeys: {ed25519: claimedEd25519Key},
|
||||||
};
|
};
|
||||||
txn.inboundGroupSessions.set(sessionEntry);
|
txn.inboundGroupSessions.set(sessionEntry);
|
||||||
newSessions.push(sessionEntry);
|
newSessions.push(sessionEntry);
|
||||||
@ -142,6 +170,17 @@ class SessionCache {
|
|||||||
this._sessions = [];
|
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) {
|
get(roomId, senderKey, sessionId) {
|
||||||
const idx = this._sessions.findIndex(s => {
|
const idx = this._sessions.findIndex(s => {
|
||||||
return s.roomId === roomId &&
|
return s.roomId === roomId &&
|
||||||
@ -155,13 +194,13 @@ class SessionCache {
|
|||||||
this._sessions.splice(idx, 1);
|
this._sessions.splice(idx, 1);
|
||||||
this._sessions.unshift(entry);
|
this._sessions.unshift(entry);
|
||||||
}
|
}
|
||||||
return entry.session;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add(roomId, senderKey, session) {
|
add(roomId, senderKey, session, claimedKeys) {
|
||||||
// add new at top
|
// add new at top
|
||||||
this._sessions.unshift({roomId, senderKey, session});
|
this._sessions.unshift({roomId, senderKey, session, claimedKeys});
|
||||||
if (this._sessions.length > CACHE_MAX_SIZE) {
|
if (this._sessions.length > CACHE_MAX_SIZE) {
|
||||||
// free sessions we're about to remove
|
// free sessions we're about to remove
|
||||||
for (let i = CACHE_MAX_SIZE; i < this._sessions.length; i += 1) {
|
for (let i = CACHE_MAX_SIZE; i < this._sessions.length; i += 1) {
|
||||||
|
@ -26,6 +26,14 @@ export class Encryption {
|
|||||||
this._ownDeviceId = ownDeviceId;
|
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<EncryptionResult>}
|
||||||
|
*/
|
||||||
async encrypt(roomId, type, content, encryptionParams) {
|
async encrypt(roomId, type, content, encryptionParams) {
|
||||||
let session = new this._olm.OutboundGroupSession();
|
let session = new this._olm.OutboundGroupSession();
|
||||||
try {
|
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 {
|
class EncryptionResult {
|
||||||
constructor(content, roomKeyMessage) {
|
constructor(content, roomKeyMessage) {
|
||||||
this.content = content;
|
this.content = content;
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
import {DecryptionError} from "../common.js";
|
import {DecryptionError} from "../common.js";
|
||||||
import {groupBy} from "../../../utils/groupBy.js";
|
import {groupBy} from "../../../utils/groupBy.js";
|
||||||
import {Session} from "./Session.js";
|
import {Session} from "./Session.js";
|
||||||
|
import {DecryptionResult} from "../DecryptionResult.js";
|
||||||
|
|
||||||
const SESSION_LIMIT_PER_SENDER_KEY = 4;
|
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
|
// 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
|
// doing it one by one would be possible, but we would lose the opportunity for parallelization
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [decryptAll description]
|
||||||
|
* @param {[type]} events
|
||||||
|
* @return {Promise<DecryptionChanges>} [description]
|
||||||
|
*/
|
||||||
async decryptAll(events) {
|
async decryptAll(events) {
|
||||||
const eventsPerSenderKey = groupBy(events, event => event.content?.["sender_key"]);
|
const eventsPerSenderKey = groupBy(events, event => event.content?.["sender_key"]);
|
||||||
const timestamp = this._now();
|
const timestamp = this._now();
|
||||||
@ -61,15 +69,16 @@ export class Decryption {
|
|||||||
try {
|
try {
|
||||||
const readSessionsTxn = await this._storage.readTxn([this._storage.storeNames.olmSessions]);
|
const readSessionsTxn = await this._storage.readTxn([this._storage.storeNames.olmSessions]);
|
||||||
// decrypt events for different sender keys in parallel
|
// 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);
|
return this._decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn);
|
||||||
}));
|
}));
|
||||||
const payloads = results.reduce((all, r) => all.concat(r.payloads), []);
|
const results = senderKeyOperations.reduce((all, r) => all.concat(r.results), []);
|
||||||
const errors = results.reduce((all, r) => all.concat(r.errors), []);
|
const errors = senderKeyOperations.reduce((all, r) => all.concat(r.errors), []);
|
||||||
const senderKeyDecryptions = results.map(r => r.senderKeyDecryption);
|
const senderKeyDecryptions = senderKeyOperations.map(r => r.senderKeyDecryption);
|
||||||
return new DecryptionChanges(senderKeyDecryptions, payloads, errors, this._account, locks);
|
return new DecryptionChanges(senderKeyDecryptions, results, errors, this._account, locks);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// make sure the locks are release if something throws
|
// make sure the locks are release if something throws
|
||||||
|
// otherwise they will be released in DecryptionChanges after having written
|
||||||
for (const lock of locks) {
|
for (const lock of locks) {
|
||||||
lock.release();
|
lock.release();
|
||||||
}
|
}
|
||||||
@ -80,18 +89,18 @@ export class Decryption {
|
|||||||
async _decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn) {
|
async _decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn) {
|
||||||
const sessions = await this._getSessions(senderKey, readSessionsTxn);
|
const sessions = await this._getSessions(senderKey, readSessionsTxn);
|
||||||
const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this._olm, timestamp);
|
const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this._olm, timestamp);
|
||||||
const payloads = [];
|
const results = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
// events for a single senderKey need to be decrypted one by one
|
// events for a single senderKey need to be decrypted one by one
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
try {
|
try {
|
||||||
const payload = this._decryptForSenderKey(senderKeyDecryption, event, timestamp);
|
const result = this._decryptForSenderKey(senderKeyDecryption, event, timestamp);
|
||||||
payloads.push(payload);
|
results.push(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors.push(err);
|
errors.push(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {payloads, errors, senderKeyDecryption};
|
return {results, errors, senderKeyDecryption};
|
||||||
}
|
}
|
||||||
|
|
||||||
_decryptForSenderKey(senderKeyDecryption, event, timestamp) {
|
_decryptForSenderKey(senderKeyDecryption, event, timestamp) {
|
||||||
@ -118,7 +127,7 @@ export class Decryption {
|
|||||||
throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, err});
|
throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, err});
|
||||||
}
|
}
|
||||||
this._validatePayload(payload, event);
|
this._validatePayload(payload, event);
|
||||||
return {event: payload, senderKey};
|
return new DecryptionResult(payload, senderKey, payload.keys);
|
||||||
} else {
|
} else {
|
||||||
throw new DecryptionError("OLM_NO_MATCHING_SESSION", event,
|
throw new DecryptionError("OLM_NO_MATCHING_SESSION", event,
|
||||||
{knownSessionIds: senderKeyDecryption.sessions.map(s => s.id)});
|
{knownSessionIds: senderKeyDecryption.sessions.map(s => s.id)});
|
||||||
@ -182,9 +191,9 @@ export class Decryption {
|
|||||||
if (!payload.content) {
|
if (!payload.content) {
|
||||||
throw new DecryptionError("missing content on payload", event, {payload});
|
throw new DecryptionError("missing content on payload", event, {payload});
|
||||||
}
|
}
|
||||||
// TODO: how important is it to verify the message?
|
if (typeof payload.keys?.ed25519 !== "string") {
|
||||||
// we should look at payload.keys.ed25519 for that... and compare it to the key we have fetched
|
throw new DecryptionError("Missing or invalid claimed ed25519 key on payload", event, {payload});
|
||||||
// from /keys/query, which we might not have done yet at this point.
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,11 +261,15 @@ class SenderKeyDecryption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property {Array<DecryptionResult>} results
|
||||||
|
* @property {Array<DecryptionError>} errors see DecryptionError.event to retrieve the event that failed to decrypt.
|
||||||
|
*/
|
||||||
class DecryptionChanges {
|
class DecryptionChanges {
|
||||||
constructor(senderKeyDecryptions, payloads, errors, account, locks) {
|
constructor(senderKeyDecryptions, results, errors, account, locks) {
|
||||||
this._senderKeyDecryptions = senderKeyDecryptions;
|
this._senderKeyDecryptions = senderKeyDecryptions;
|
||||||
this._account = account;
|
this._account = account;
|
||||||
this.payloads = payloads;
|
this.results = results;
|
||||||
this.errors = errors;
|
this.errors = errors;
|
||||||
this._locks = locks;
|
this._locks = locks;
|
||||||
}
|
}
|
||||||
|
@ -141,14 +141,15 @@ export class HomeServerApi {
|
|||||||
{}, {}, options);
|
{}, {}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
passwordLogin(username, password, options = null) {
|
passwordLogin(username, password, initialDeviceDisplayName, options = null) {
|
||||||
return this._post("/login", null, {
|
return this._post("/login", null, {
|
||||||
"type": "m.login.password",
|
"type": "m.login.password",
|
||||||
"identifier": {
|
"identifier": {
|
||||||
"type": "m.id.user",
|
"type": "m.id.user",
|
||||||
"user": username
|
"user": username
|
||||||
},
|
},
|
||||||
"password": password
|
"password": password,
|
||||||
|
"initial_device_display_name": initialDeviceDisplayName
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,6 @@ import {fetchOrLoadMembers} from "./members/load.js";
|
|||||||
import {MemberList} from "./members/MemberList.js";
|
import {MemberList} from "./members/MemberList.js";
|
||||||
import {Heroes} from "./members/Heroes.js";
|
import {Heroes} from "./members/Heroes.js";
|
||||||
import {EventEntry} from "./timeline/entries/EventEntry.js";
|
import {EventEntry} from "./timeline/entries/EventEntry.js";
|
||||||
import {EventKey} from "./timeline/EventKey.js";
|
|
||||||
|
|
||||||
export class Room extends EventEmitter {
|
export class Room extends EventEmitter {
|
||||||
constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user, createRoomEncryption}) {
|
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.timelineEvents,
|
||||||
this._storage.storeNames.inboundGroupSessions,
|
this._storage.storeNames.inboundGroupSessions,
|
||||||
this._storage.storeNames.groupSessionDecryptions,
|
this._storage.storeNames.groupSessionDecryptions,
|
||||||
|
this._storage.storeNames.deviceIdentities,
|
||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
for (const retryEntry of retryEntries) {
|
for (const retryEntry of retryEntries) {
|
||||||
const {data: eventKey} = retryEntry;
|
const {data: eventKey} = retryEntry;
|
||||||
let entry = this._timeline?.findEntry(eventKey);
|
let entry = this._timeline?.findEntry(eventKey);
|
||||||
if (!entry) {
|
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) {
|
if (storageEntry) {
|
||||||
entry = new EventEntry(storageEntry, this._fragmentIdComparer);
|
entry = new EventEntry(storageEntry, this._fragmentIdComparer);
|
||||||
}
|
}
|
||||||
@ -101,12 +101,10 @@ export class Room extends EventEmitter {
|
|||||||
async _decryptEntry(entry, txn, isSync) {
|
async _decryptEntry(entry, txn, isSync) {
|
||||||
if (entry.eventType === "m.room.encrypted") {
|
if (entry.eventType === "m.room.encrypted") {
|
||||||
try {
|
try {
|
||||||
const {fragmentId, entryIndex} = entry;
|
const decryptionResult = await this._roomEncryption.decrypt(
|
||||||
const key = new EventKey(fragmentId, entryIndex);
|
entry.event, isSync, !!this._timeline, entry.asEventKey(), txn);
|
||||||
const decryptedEvent = await this._roomEncryption.decrypt(
|
if (decryptionResult) {
|
||||||
entry.event, isSync, key, txn);
|
entry.setDecryptionResult(decryptionResult);
|
||||||
if (decryptedEvent) {
|
|
||||||
entry.replaceWithDecrypted(decryptedEvent);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("event decryption error", err, entry.event);
|
console.warn("event decryption error", err, entry.event);
|
||||||
@ -283,6 +281,7 @@ export class Room extends EventEmitter {
|
|||||||
stores = stores.concat([
|
stores = stores.concat([
|
||||||
this._storage.storeNames.inboundGroupSessions,
|
this._storage.storeNames.inboundGroupSessions,
|
||||||
this._storage.storeNames.groupSessionDecryptions,
|
this._storage.storeNames.groupSessionDecryptions,
|
||||||
|
this._storage.storeNames.deviceIdentities,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
const txn = await this._storage.readWriteTxn(stores);
|
const txn = await this._storage.readWriteTxn(stores);
|
||||||
|
@ -22,7 +22,7 @@ export class EventEntry extends BaseEntry {
|
|||||||
super(fragmentIdComparer);
|
super(fragmentIdComparer);
|
||||||
this._eventEntry = eventEntry;
|
this._eventEntry = eventEntry;
|
||||||
this._decryptionError = null;
|
this._decryptionError = null;
|
||||||
this._decryptedEvent = null;
|
this._decryptionResult = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get event() {
|
get event() {
|
||||||
@ -38,15 +38,16 @@ export class EventEntry extends BaseEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get content() {
|
get content() {
|
||||||
return this._decryptedEvent?.content || this._eventEntry.event.content;
|
return this._decryptionResult?.event?.content || this._eventEntry.event.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
get prevContent() {
|
get prevContent() {
|
||||||
|
// doesn't look at _decryptionResult because state events are not encrypted
|
||||||
return getPrevContentFromStateEvent(this._eventEntry.event);
|
return getPrevContentFromStateEvent(this._eventEntry.event);
|
||||||
}
|
}
|
||||||
|
|
||||||
get eventType() {
|
get eventType() {
|
||||||
return this._decryptedEvent?.type || this._eventEntry.event.type;
|
return this._decryptionResult?.event?.type || this._eventEntry.event.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
get stateKey() {
|
get stateKey() {
|
||||||
@ -73,8 +74,20 @@ export class EventEntry extends BaseEntry {
|
|||||||
return this._eventEntry.event.event_id;
|
return this._eventEntry.event.event_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceWithDecrypted(event) {
|
setDecryptionResult(result) {
|
||||||
this._decryptedEvent = event;
|
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) {
|
setDecryptionError(err) {
|
||||||
|
@ -38,6 +38,7 @@ export class TimelineReader {
|
|||||||
this._storage.storeNames.timelineFragments,
|
this._storage.storeNames.timelineFragments,
|
||||||
this._storage.storeNames.inboundGroupSessions,
|
this._storage.storeNames.inboundGroupSessions,
|
||||||
this._storage.storeNames.groupSessionDecryptions,
|
this._storage.storeNames.groupSessionDecryptions,
|
||||||
|
this._storage.storeNames.deviceIdentities,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -14,6 +14,7 @@ export const schema = [
|
|||||||
createInboundGroupSessionsStore,
|
createInboundGroupSessionsStore,
|
||||||
createOutboundGroupSessionsStore,
|
createOutboundGroupSessionsStore,
|
||||||
createGroupSessionDecryptions,
|
createGroupSessionDecryptions,
|
||||||
|
addSenderKeyIndexToDeviceStore
|
||||||
];
|
];
|
||||||
// TODO: how to deal with git merge conflicts of this array?
|
// TODO: how to deal with git merge conflicts of this array?
|
||||||
|
|
||||||
@ -94,3 +95,9 @@ function createOutboundGroupSessionsStore(db) {
|
|||||||
function createGroupSessionDecryptions(db) {
|
function createGroupSessionDecryptions(db) {
|
||||||
db.createObjectStore("groupSessionDecryptions", {keyPath: "key"});
|
db.createObjectStore("groupSessionDecryptions", {keyPath: "key"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//v9
|
||||||
|
function addSenderKeyIndexToDeviceStore(db, txn) {
|
||||||
|
const deviceIdentities = txn.objectStore("deviceIdentities");
|
||||||
|
deviceIdentities.createIndex("byCurve25519Key", "curve25519Key", {unique: true});
|
||||||
|
}
|
||||||
|
@ -38,4 +38,8 @@ export class DeviceIdentityStore {
|
|||||||
deviceIdentity.key = encodeKey(deviceIdentity.userId, deviceIdentity.deviceId);
|
deviceIdentity.key = encodeKey(deviceIdentity.userId, deviceIdentity.deviceId);
|
||||||
return this._store.put(deviceIdentity);
|
return this._store.put(deviceIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getByCurve25519Key(curve25519Key) {
|
||||||
|
return this._store.index("byCurve25519Key").get(curve25519Key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -373,6 +373,10 @@ ul.Timeline > li.continuation time {
|
|||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.TextMessageView.unverified .message-container {
|
||||||
|
color: #ff4b55;
|
||||||
|
}
|
||||||
|
|
||||||
.message-container p {
|
.message-container p {
|
||||||
margin: 3px 0;
|
margin: 3px 0;
|
||||||
line-height: 2.2rem;
|
line-height: 2.2rem;
|
||||||
|
@ -22,6 +22,7 @@ export function renderMessage(t, vm, children) {
|
|||||||
"TextMessageView": true,
|
"TextMessageView": true,
|
||||||
own: vm.isOwn,
|
own: vm.isOwn,
|
||||||
pending: vm.isPending,
|
pending: vm.isPending,
|
||||||
|
unverified: vm.isUnverified,
|
||||||
continuation: vm => vm.isContinuation,
|
continuation: vm => vm.isContinuation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user