Merge pull request #191 from vector-im/bwindels/preshare-megolmsessions

Share megolm session once you start typing
This commit is contained in:
Bruno Windels 2020-11-10 14:00:57 +00:00 committed by GitHub
commit a812d07d53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 143 additions and 47 deletions

View File

@ -188,8 +188,14 @@ class ComposerViewModel extends ViewModel {
return !this._isEmpty; return !this._isEmpty;
} }
setInput(text) { async setInput(text) {
const wasEmpty = this._isEmpty;
this._isEmpty = text.length === 0; this._isEmpty = text.length === 0;
if (wasEmpty && !this._isEmpty) {
this._roomVM._room.ensureMessageKeyIsShared();
}
if (wasEmpty !== this._isEmpty) {
this.emitChange("canSend"); this.emitChange("canSend");
} }
} }
}

View File

@ -149,10 +149,14 @@ export class Account {
} }
} }
createOutboundOlmSession(theirIdentityKey, theirOneTimeKey) { async createOutboundOlmSession(theirIdentityKey, theirOneTimeKey) {
const newSession = new this._olm.Session(); const newSession = new this._olm.Session();
try { try {
if (this._olmWorker) {
await this._olmWorker.createOutboundOlmSession(this._account, newSession, theirIdentityKey, theirOneTimeKey);
} else {
newSession.create_outbound(this._account, theirIdentityKey, theirOneTimeKey); newSession.create_outbound(this._account, theirIdentityKey, theirOneTimeKey);
}
return newSession; return newSession;
} catch (err) { } catch (err) {
newSession.free(); newSession.free();

View File

@ -37,6 +37,12 @@ export class OlmWorker {
account.unpickle("", pickle); account.unpickle("", pickle);
} }
async createOutboundSession(account, newSession, theirIdentityKey, theirOneTimeKey) {
const accountPickle = account.pickle("");
const sessionPickle = await this._workerPool.send({type: "olm_create_outbound", accountPickle, theirIdentityKey, theirOneTimeKey}).response();
newSession.unpickle("", sessionPickle);
}
dispose() { dispose() {
this._workerPool.dispose(); this._workerPool.dispose();
} }

View File

@ -20,6 +20,10 @@ import {mergeMap} from "../../utils/mergeMap.js";
import {makeTxnId} from "../common.js"; import {makeTxnId} from "../common.js";
const ENCRYPTED_TYPE = "m.room.encrypted"; const ENCRYPTED_TYPE = "m.room.encrypted";
// how often ensureMessageKeyIsShared can check if it needs to
// create a new outbound session
// note that encrypt could still create a new session
const MIN_PRESHARE_INTERVAL = 60 * 1000; // 1min
function encodeMissingSessionKey(senderKey, sessionId) { function encodeMissingSessionKey(senderKey, sessionId) {
return `${senderKey}|${sessionId}`; return `${senderKey}|${sessionId}`;
@ -55,6 +59,7 @@ export class RoomEncryption {
this._clock = clock; this._clock = clock;
this._disposed = false; this._disposed = false;
this._isFlushingRoomKeyShares = false; this._isFlushingRoomKeyShares = false;
this._lastKeyPreShareTime = null;
} }
async enableSessionBackup(sessionBackup) { async enableSessionBackup(sessionBackup) {
@ -244,14 +249,23 @@ export class RoomEncryption {
} }
return matches; return matches;
}
/** shares the encryption key for the next message if needed */
async ensureMessageKeyIsShared(hsApi) {
if (this._lastKeyPreShareTime?.measure() < MIN_PRESHARE_INTERVAL) {
return;
}
this._lastKeyPreShareTime = this._clock.createMeasure();
const roomKeyMessage = await this._megolmEncryption.ensureOutboundSession(this._room.id, this._encryptionParams);
if (roomKeyMessage) {
await this._shareNewRoomKey(roomKeyMessage, hsApi);
}
} }
async encrypt(type, content, hsApi) { async encrypt(type, content, hsApi) {
await this._deviceTracker.trackRoom(this._room);
const megolmResult = await this._megolmEncryption.encrypt(this._room.id, type, content, this._encryptionParams); const megolmResult = await this._megolmEncryption.encrypt(this._room.id, type, content, this._encryptionParams);
if (megolmResult.roomKeyMessage) { if (megolmResult.roomKeyMessage) {
// TODO: should we await this??
this._shareNewRoomKey(megolmResult.roomKeyMessage, hsApi); this._shareNewRoomKey(megolmResult.roomKeyMessage, hsApi);
} }
return { return {
@ -270,6 +284,7 @@ export class RoomEncryption {
} }
async _shareNewRoomKey(roomKeyMessage, hsApi) { async _shareNewRoomKey(roomKeyMessage, hsApi) {
await this._deviceTracker.trackRoom(this._room);
const devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi); const devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi);
const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set())); const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set()));

View File

@ -43,6 +43,56 @@ export class Encryption {
} }
} }
async ensureOutboundSession(roomId, encryptionParams) {
let session = new this._olm.OutboundGroupSession();
try {
const txn = this._storage.readWriteTxn([
this._storage.storeNames.inboundGroupSessions,
this._storage.storeNames.outboundGroupSessions,
]);
let roomKeyMessage;
try {
let sessionEntry = await txn.outboundGroupSessions.get(roomId);
roomKeyMessage = this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn);
if (roomKeyMessage) {
this._writeSession(sessionEntry, session, roomId, txn);
}
} catch (err) {
txn.abort();
throw err;
}
await txn.complete();
return roomKeyMessage;
} finally {
session.free();
}
}
_readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn) {
if (sessionEntry) {
session.unpickle(this._pickleKey, sessionEntry.session);
}
if (!sessionEntry || this._needsToRotate(session, sessionEntry.createdAt, encryptionParams)) {
// in the case of rotating, recreate a session as we already unpickled into it
if (sessionEntry) {
session.free();
session = new this._olm.OutboundGroupSession();
}
session.create();
const roomKeyMessage = this._createRoomKeyMessage(session, roomId);
this._storeAsInboundSession(session, roomId, txn);
return roomKeyMessage;
}
}
_writeSession(sessionEntry, session, roomId, txn) {
txn.outboundGroupSessions.set({
roomId,
session: session.pickle(this._pickleKey),
createdAt: sessionEntry?.createdAt || this._now(),
});
}
/** /**
* Encrypts a message with megolm * Encrypts a message with megolm
* @param {string} roomId * @param {string} roomId
@ -61,28 +111,10 @@ export class Encryption {
let roomKeyMessage; let roomKeyMessage;
let encryptedContent; let encryptedContent;
try { try {
// TODO: we could consider keeping the session in memory for the current room
let sessionEntry = await txn.outboundGroupSessions.get(roomId); let sessionEntry = await txn.outboundGroupSessions.get(roomId);
if (sessionEntry) { roomKeyMessage = this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn);
session.unpickle(this._pickleKey, sessionEntry.session);
}
if (!sessionEntry || this._needsToRotate(session, sessionEntry.createdAt, encryptionParams)) {
// in the case of rotating, recreate a session as we already unpickled into it
if (sessionEntry) {
session.free();
session = new this._olm.OutboundGroupSession();
}
session.create();
roomKeyMessage = this._createRoomKeyMessage(session, roomId);
this._storeAsInboundSession(session, roomId, txn);
// TODO: we could tell the Decryption here that we have a new session so it can add it to its cache
}
encryptedContent = this._encryptContent(roomId, session, type, content); encryptedContent = this._encryptContent(roomId, session, type, content);
txn.outboundGroupSessions.set({ this._writeSession(sessionEntry, session, roomId, txn);
roomId,
session: session.pickle(this._pickleKey),
createdAt: sessionEntry?.createdAt || this._now(),
});
} catch (err) { } catch (err) {
txn.abort(); txn.abort();

View File

@ -154,7 +154,7 @@ export class Encryption {
try { try {
for (const target of newEncryptionTargets) { for (const target of newEncryptionTargets) {
const {device, oneTimeKey} = target; const {device, oneTimeKey} = target;
target.session = this._account.createOutboundOlmSession(device.curve25519Key, oneTimeKey); target.session = await this._account.createOutboundOlmSession(device.curve25519Key, oneTimeKey);
} }
this._storeSessions(newEncryptionTargets, timestamp); this._storeSessions(newEncryptionTargets, timestamp);
} catch (err) { } catch (err) {

View File

@ -354,6 +354,10 @@ export class Room extends EventEmitter {
return this._sendQueue.enqueueEvent(eventType, content); return this._sendQueue.enqueueEvent(eventType, content);
} }
async ensureMessageKeyIsShared() {
return this._roomEncryption?.ensureMessageKeyIsShared(this._hsApi);
}
/** @public */ /** @public */
async loadMemberList() { async loadMemberList() {
if (this._memberList) { if (this._memberList) {

View File

@ -44,3 +44,8 @@ body.hydrogen {
.hidden { .hidden {
display: none !important; display: none !important;
} }
/* hide clear buttons in IE */
input::-ms-clear {
display: none;
}

View File

@ -116,14 +116,13 @@ class MessageHandler {
_megolmDecrypt(sessionKey, ciphertext) { _megolmDecrypt(sessionKey, ciphertext) {
return this._toMessage(() => { return this._toMessage(() => {
let session; const session = new this._olm.InboundGroupSession();
try { try {
session = new this._olm.InboundGroupSession();
session.import_session(sessionKey); session.import_session(sessionKey);
// returns object with plaintext and message_index // returns object with plaintext and message_index
return session.decrypt(ciphertext); return session.decrypt(ciphertext);
} finally { } finally {
session?.free(); session.free();
} }
}); });
} }
@ -132,10 +131,29 @@ class MessageHandler {
return this._toMessage(() => { return this._toMessage(() => {
this._feedRandomValues(randomValues); this._feedRandomValues(randomValues);
const account = new this._olm.Account(); const account = new this._olm.Account();
try {
account.create(); account.create();
account.generate_one_time_keys(otkAmount); account.generate_one_time_keys(otkAmount);
this._checkRandomValuesUsed(); this._checkRandomValuesUsed();
return account.pickle(""); return account.pickle("");
} finally {
account.free();
}
});
}
_olmCreateOutbound(accountPickle, theirIdentityKey, theirOneTimeKey) {
return this._toMessage(() => {
const account = new this._olm.Account();
const newSession = new this._olm.Session();
try {
account.unpickle("", accountPickle);
newSession.create_outbound(account, newSession, theirIdentityKey, theirOneTimeKey);
return newSession.pickle("");
} finally {
account.free();
newSession.free();
}
}); });
} }
@ -149,6 +167,8 @@ class MessageHandler {
this._sendReply(message, this._megolmDecrypt(message.sessionKey, message.ciphertext)); this._sendReply(message, this._megolmDecrypt(message.sessionKey, message.ciphertext));
} else if (type === "olm_create_account_otks") { } else if (type === "olm_create_account_otks") {
this._sendReply(message, this._olmCreateAccountAndOTKs(message.randomValues, message.otkAmount)); this._sendReply(message, this._olmCreateAccountAndOTKs(message.randomValues, message.otkAmount));
} else if (type === "olm_create_outbound") {
this._sendReply(message, this._olmCreateOutbound(message.accountPickle, message.theirIdentityKey, message.theirOneTimeKey));
} }
} }
} }

View File

@ -20,7 +20,7 @@ export class Lock {
this._resolve = null; this._resolve = null;
} }
take() { tryTake() {
if (!this._promise) { if (!this._promise) {
this._promise = new Promise(resolve => { this._promise = new Promise(resolve => {
this._resolve = resolve; this._resolve = resolve;
@ -30,6 +30,12 @@ export class Lock {
return false; return false;
} }
async take() {
while(!this.tryTake()) {
await this.released();
}
}
get isTaken() { get isTaken() {
return !!this._promise; return !!this._promise;
} }
@ -52,25 +58,25 @@ export function tests() {
return { return {
"taking a lock twice returns false": assert => { "taking a lock twice returns false": assert => {
const lock = new Lock(); const lock = new Lock();
assert.equal(lock.take(), true); assert.equal(lock.tryTake(), true);
assert.equal(lock.isTaken, true); assert.equal(lock.isTaken, true);
assert.equal(lock.take(), false); assert.equal(lock.tryTake(), false);
}, },
"can take a released lock again": assert => { "can take a released lock again": assert => {
const lock = new Lock(); const lock = new Lock();
lock.take(); lock.tryTake();
lock.release(); lock.release();
assert.equal(lock.isTaken, false); assert.equal(lock.isTaken, false);
assert.equal(lock.take(), true); assert.equal(lock.tryTake(), true);
}, },
"2 waiting for lock, only first one gets it": async assert => { "2 waiting for lock, only first one gets it": async assert => {
const lock = new Lock(); const lock = new Lock();
lock.take(); lock.tryTake();
let first; let first;
lock.released().then(() => first = lock.take()); lock.released().then(() => first = lock.tryTake());
let second; let second;
lock.released().then(() => second = lock.take()); lock.released().then(() => second = lock.tryTake());
const promise = lock.released(); const promise = lock.released();
lock.release(); lock.release();
await promise; await promise;

View File

@ -24,12 +24,10 @@ export class LockMap {
async takeLock(key) { async takeLock(key) {
let lock = this._map.get(key); let lock = this._map.get(key);
if (lock) { if (lock) {
while (!lock.take()) { await lock.take();
await lock.released();
}
} else { } else {
lock = new Lock(); lock = new Lock();
lock.take(); lock.tryTake();
this._map.set(key, lock); this._map.set(key, lock);
} }
// don't leave old locks lying around // don't leave old locks lying around