mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 11:35:04 +01:00
Merge pull request #191 from vector-im/bwindels/preshare-megolmsessions
Share megolm session once you start typing
This commit is contained in:
commit
a812d07d53
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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()));
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -44,3 +44,8 @@ body.hydrogen {
|
|||||||
.hidden {
|
.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* hide clear buttons in IE */
|
||||||
|
input::-ms-clear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user