forked from mystiq/hydrogen-web
WIP to make megolm session in memory once you start sending
deciced against merging this though as it increases the chance of corrupting the outbound megolm session in case you have multiple tabs open on the same session we don't officially support that usecase, and even try to automatically log the user out, but I'm still not 100% sure if I'm comfortable with introducing more breakage if this does happen (no service worker, ...) so parking this work here for now. I started working on this as part of sending out megolm keys when you start typing.
This commit is contained in:
parent
5d12aef6db
commit
a2bc242c6b
3 changed files with 152 additions and 111 deletions
|
@ -123,7 +123,6 @@ export class Session {
|
||||||
account: this._e2eeAccount,
|
account: this._e2eeAccount,
|
||||||
pickleKey: PICKLE_KEY,
|
pickleKey: PICKLE_KEY,
|
||||||
olm: this._olm,
|
olm: this._olm,
|
||||||
storage: this._storage,
|
|
||||||
now: this._platform.clock.now,
|
now: this._platform.clock.now,
|
||||||
ownDeviceId: this._sessionInfo.deviceId,
|
ownDeviceId: this._sessionInfo.deviceId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,6 +36,7 @@ export class RoomEncryption {
|
||||||
this._deviceTracker = deviceTracker;
|
this._deviceTracker = deviceTracker;
|
||||||
this._olmEncryption = olmEncryption;
|
this._olmEncryption = olmEncryption;
|
||||||
this._megolmEncryption = megolmEncryption;
|
this._megolmEncryption = megolmEncryption;
|
||||||
|
this._megolmRoomEncryption = null;
|
||||||
this._megolmDecryption = megolmDecryption;
|
this._megolmDecryption = megolmDecryption;
|
||||||
// content of the m.room.encryption event
|
// content of the m.room.encryption event
|
||||||
this._encryptionParams = encryptionParams;
|
this._encryptionParams = encryptionParams;
|
||||||
|
@ -253,6 +254,8 @@ export class RoomEncryption {
|
||||||
this._storage.storeNames.outboundGroupSessions,
|
this._storage.storeNames.outboundGroupSessions,
|
||||||
this._storage.storeNames.inboundGroupSessions,
|
this._storage.storeNames.inboundGroupSessions,
|
||||||
]);
|
]);
|
||||||
|
await this._deviceTracker.trackRoom(this._room);
|
||||||
|
|
||||||
let roomKeyMessage;
|
let roomKeyMessage;
|
||||||
try {
|
try {
|
||||||
roomKeyMessage = await this._megolmEncryption.ensureOutboundSession(this._room.id, this._encryptionParams, txn);
|
roomKeyMessage = await this._megolmEncryption.ensureOutboundSession(this._room.id, this._encryptionParams, txn);
|
||||||
|
@ -268,10 +271,16 @@ export class RoomEncryption {
|
||||||
|
|
||||||
async encrypt(type, content, hsApi) {
|
async encrypt(type, content, hsApi) {
|
||||||
await this._deviceTracker.trackRoom(this._room);
|
await this._deviceTracker.trackRoom(this._room);
|
||||||
const megolmResult = await this._megolmEncryption.encrypt(this._room.id, type, content, this._encryptionParams);
|
const txn = this._storage.readWriteTxn([
|
||||||
|
this._storage.storeNames.operations,
|
||||||
|
this._storage.storeNames.outboundGroupSessions,
|
||||||
|
this._storage.storeNames.inboundGroupSessions,
|
||||||
|
]);
|
||||||
|
await this._ensureMegolmEncryption(txn);
|
||||||
|
const megolmResult = this._megolmRoomEncryption.encrypt(type, content, txn);
|
||||||
if (megolmResult.roomKeyMessage) {
|
if (megolmResult.roomKeyMessage) {
|
||||||
// TODO: should we await this??
|
// TODO: should we await this??
|
||||||
this._shareNewRoomKey(megolmResult.roomKeyMessage, hsApi);
|
this._shareNewRoomKey(megolmResult.roomKeyMessage, hsApi, txn);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: ENCRYPTED_TYPE,
|
type: ENCRYPTED_TYPE,
|
||||||
|
@ -288,12 +297,11 @@ export class RoomEncryption {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _shareNewRoomKey(roomKeyMessage, hsApi, txn = null) {
|
async _shareNewRoomKey(roomKeyMessage, hsApi, writeOpTxn) {
|
||||||
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()));
|
||||||
|
|
||||||
// store operation for room key share, in case we don't finish here
|
// store operation for room key share, in case we don't finish here
|
||||||
const writeOpTxn = txn || this._storage.readWriteTxn([this._storage.storeNames.operations]);
|
|
||||||
let operationId;
|
let operationId;
|
||||||
try {
|
try {
|
||||||
operationId = this._writeRoomKeyShareOperation(roomKeyMessage, userIds, writeOpTxn);
|
operationId = this._writeRoomKeyShareOperation(roomKeyMessage, userIds, writeOpTxn);
|
||||||
|
@ -322,8 +330,8 @@ export class RoomEncryption {
|
||||||
|
|
||||||
async _addShareRoomKeyOperationForNewMembers(memberChangesArray, txn) {
|
async _addShareRoomKeyOperationForNewMembers(memberChangesArray, txn) {
|
||||||
const userIds = memberChangesArray.filter(m => m.hasJoined).map(m => m.userId);
|
const userIds = memberChangesArray.filter(m => m.hasJoined).map(m => m.userId);
|
||||||
const roomKeyMessage = await this._megolmEncryption.createRoomKeyMessage(
|
await this._ensureMegolmEncryption(txn);
|
||||||
this._room.id, txn);
|
const roomKeyMessage = this._megolmRoomEncryption.createRoomKeyMessage(txn);
|
||||||
if (roomKeyMessage) {
|
if (roomKeyMessage) {
|
||||||
this._writeRoomKeyShareOperation(roomKeyMessage, userIds, txn);
|
this._writeRoomKeyShareOperation(roomKeyMessage, userIds, txn);
|
||||||
}
|
}
|
||||||
|
@ -394,10 +402,23 @@ export class RoomEncryption {
|
||||||
await hsApi.sendToDevice(type, payload, txnId).response();
|
await hsApi.sendToDevice(type, payload, txnId).response();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _ensureMegolmEncryption(txn) {
|
||||||
|
if (!this._megolmRoomEncryption) {
|
||||||
|
this._megolmRoomEncryption = await this._megolmEncryption.openRoomEncryption(
|
||||||
|
this._room.id,
|
||||||
|
this._encryptionParams,
|
||||||
|
txn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this._disposed = true;
|
this._disposed = true;
|
||||||
this._megolmBackfillCache.dispose();
|
this._megolmBackfillCache.dispose();
|
||||||
this._megolmSyncCache.dispose();
|
this._megolmSyncCache.dispose();
|
||||||
|
if (this._megolmRoomEncryption) {
|
||||||
|
this._megolmRoomEncryption.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,164 +17,186 @@ limitations under the License.
|
||||||
import {MEGOLM_ALGORITHM} from "../common.js";
|
import {MEGOLM_ALGORITHM} from "../common.js";
|
||||||
|
|
||||||
export class Encryption {
|
export class Encryption {
|
||||||
constructor({pickleKey, olm, account, storage, now, ownDeviceId}) {
|
constructor({pickleKey, olm, account, now, ownDeviceId}) {
|
||||||
this._pickleKey = pickleKey;
|
this._pickleKey = pickleKey;
|
||||||
this._olm = olm;
|
this._olm = olm;
|
||||||
this._account = account;
|
this._account = account;
|
||||||
this._storage = storage;
|
|
||||||
this._now = now;
|
this._now = now;
|
||||||
this._ownDeviceId = ownDeviceId;
|
this._ownDeviceId = ownDeviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
discardOutboundSession(roomId, txn) {
|
async openRoomEncryption(roomId, encryptionParams, txn) {
|
||||||
txn.outboundGroupSessions.remove(roomId);
|
const sessionEntry = await txn.outboundGroupSessions.get(roomId);
|
||||||
}
|
let session = null;
|
||||||
|
|
||||||
async createRoomKeyMessage(roomId, txn) {
|
|
||||||
let sessionEntry = await txn.outboundGroupSessions.get(roomId);
|
|
||||||
if (sessionEntry) {
|
|
||||||
const session = new this._olm.OutboundGroupSession();
|
|
||||||
try {
|
|
||||||
session.unpickle(this._pickleKey, sessionEntry.session);
|
|
||||||
return this._createRoomKeyMessage(session, roomId);
|
|
||||||
} finally {
|
|
||||||
session.free();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async ensureOutboundSession(roomId, encryptionParams, txn) {
|
|
||||||
let session = new this._olm.OutboundGroupSession();
|
|
||||||
try {
|
|
||||||
let sessionEntry = await txn.outboundGroupSessions.get(roomId);
|
|
||||||
const roomKeyMessage = this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn);
|
|
||||||
if (roomKeyMessage) {
|
|
||||||
this._writeSession(sessionEntry, session, roomId, txn);
|
|
||||||
return roomKeyMessage;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
session.free();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn) {
|
|
||||||
if (sessionEntry) {
|
if (sessionEntry) {
|
||||||
|
session = new this._olm.OutboundGroupSession();
|
||||||
session.unpickle(this._pickleKey, sessionEntry.session);
|
session.unpickle(this._pickleKey, sessionEntry.session);
|
||||||
}
|
}
|
||||||
if (!sessionEntry || this._needsToRotate(session, sessionEntry.createdAt, encryptionParams)) {
|
return new RoomEncryption({
|
||||||
// in the case of rotating, recreate a session as we already unpickled into it
|
pickleKey: this._pickleKey,
|
||||||
if (sessionEntry) {
|
olm: this._olm,
|
||||||
session.free();
|
account: this._account,
|
||||||
session = new this._olm.OutboundGroupSession();
|
now: this._now,
|
||||||
}
|
ownDeviceId: this._ownDeviceId,
|
||||||
session.create();
|
sessionEntry,
|
||||||
const roomKeyMessage = this._createRoomKeyMessage(session, roomId);
|
session,
|
||||||
this._storeAsInboundSession(session, roomId, txn);
|
roomId,
|
||||||
return roomKeyMessage;
|
encryptionParams
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoomEncryption {
|
||||||
|
constructor({pickleKey, olm, account, now, roomId, encryptionParams, sessionEntry, session, ownDeviceId}) {
|
||||||
|
this._pickleKey = pickleKey;
|
||||||
|
this._olm = olm;
|
||||||
|
this._account = account;
|
||||||
|
this._now = now;
|
||||||
|
this._roomId = roomId;
|
||||||
|
this._encryptionParams = encryptionParams;
|
||||||
|
this._ownDeviceId = ownDeviceId;
|
||||||
|
this._sessionEntry = sessionEntry;
|
||||||
|
this._session = session;
|
||||||
}
|
}
|
||||||
|
|
||||||
_writeSession(sessionEntry, session, roomId, txn) {
|
/**
|
||||||
txn.outboundGroupSessions.set({
|
* Discards the outbound session, if any.
|
||||||
roomId,
|
* @param {Transaction} txn a storage transaction with readwrite access to outboundGroupSessions and inboundGroupSessions stores
|
||||||
session: session.pickle(this._pickleKey),
|
*/
|
||||||
createdAt: sessionEntry?.createdAt || this._now(),
|
discardOutboundSession(txn) {
|
||||||
});
|
txn.outboundGroupSessions.remove(this._roomId);
|
||||||
|
if (this._session) {
|
||||||
|
this._session.free();
|
||||||
|
}
|
||||||
|
this._session = null;
|
||||||
|
this._sessionEntry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an outbound session if non exists already
|
||||||
|
* @param {Transaction} txn a storage transaction with readwrite access to outboundGroupSessions and inboundGroupSessions stores
|
||||||
|
* @return {boolean} true if a session has been created. Call `createRoomKeyMessage` to share the new session.
|
||||||
|
*/
|
||||||
|
ensureOutboundSession(txn) {
|
||||||
|
if (this._readOrCreateSession(txn)) {
|
||||||
|
this._writeSession(txn);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts a message with megolm
|
* Encrypts a message with megolm
|
||||||
* @param {string} roomId
|
|
||||||
* @param {string} type event type to encrypt
|
* @param {string} type event type to encrypt
|
||||||
* @param {string} content content to encrypt
|
* @param {string} content content to encrypt
|
||||||
* @param {object} encryptionParams the content of the m.room.encryption event
|
* @param {Transaction} txn a storage transaction with readwrite access to outboundGroupSessions and inboundGroupSessions stores
|
||||||
* @return {Promise<EncryptionResult>}
|
* @return {Promise<EncryptionResult>}
|
||||||
*/
|
*/
|
||||||
async encrypt(roomId, type, content, encryptionParams) {
|
encrypt(type, content, txn) {
|
||||||
let session = new this._olm.OutboundGroupSession();
|
let roomKeyMessage;
|
||||||
try {
|
if (this._readOrCreateSession(txn)) {
|
||||||
const txn = this._storage.readWriteTxn([
|
// important to create the room key message before encrypting
|
||||||
this._storage.storeNames.inboundGroupSessions,
|
// so the message index isn't advanced yet
|
||||||
this._storage.storeNames.outboundGroupSessions,
|
roomKeyMessage = this.createRoomKeyMessage();
|
||||||
]);
|
|
||||||
let roomKeyMessage;
|
|
||||||
let encryptedContent;
|
|
||||||
try {
|
|
||||||
let sessionEntry = await txn.outboundGroupSessions.get(roomId);
|
|
||||||
roomKeyMessage = this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn);
|
|
||||||
encryptedContent = this._encryptContent(roomId, session, type, content);
|
|
||||||
this._writeSession(sessionEntry, session, roomId, txn);
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
txn.abort();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
await txn.complete();
|
|
||||||
return new EncryptionResult(encryptedContent, roomKeyMessage);
|
|
||||||
} finally {
|
|
||||||
if (session) {
|
|
||||||
session.free();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const encryptedContent = this._encryptContent(type, content);
|
||||||
|
this._writeSession(txn);
|
||||||
|
return new EncryptionResult(encryptedContent, roomKeyMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
_needsToRotate(session, createdAt, encryptionParams) {
|
needsNewSession() {
|
||||||
|
if (!this._session) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
let rotationPeriodMs = 604800000; // default
|
let rotationPeriodMs = 604800000; // default
|
||||||
if (Number.isSafeInteger(encryptionParams?.rotation_period_ms)) {
|
if (Number.isSafeInteger(this._encryptionParams?.rotation_period_ms)) {
|
||||||
rotationPeriodMs = encryptionParams?.rotation_period_ms;
|
rotationPeriodMs = this._encryptionParams?.rotation_period_ms;
|
||||||
}
|
}
|
||||||
let rotationPeriodMsgs = 100; // default
|
let rotationPeriodMsgs = 100; // default
|
||||||
if (Number.isSafeInteger(encryptionParams?.rotation_period_msgs)) {
|
if (Number.isSafeInteger(this._encryptionParams?.rotation_period_msgs)) {
|
||||||
rotationPeriodMsgs = encryptionParams?.rotation_period_msgs;
|
rotationPeriodMsgs = this._encryptionParams?.rotation_period_msgs;
|
||||||
}
|
}
|
||||||
|
// assume this is a new session if sessionEntry hasn't been created/written yet
|
||||||
if (this._now() > (createdAt + rotationPeriodMs)) {
|
if (this._sessionEntry && this._now() > (this._sessionEntry.createdAt + rotationPeriodMs)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (session.message_index() >= rotationPeriodMsgs) {
|
if (this._session.message_index() >= rotationPeriodMsgs) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_encryptContent(roomId, session, type, content) {
|
createRoomKeyMessage() {
|
||||||
|
if (!this._session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
room_id: this._roomId,
|
||||||
|
session_id: this._session.session_id(),
|
||||||
|
session_key: this._session.session_key(),
|
||||||
|
algorithm: MEGOLM_ALGORITHM,
|
||||||
|
// chain_index is ignored by element-web if not all clients
|
||||||
|
// but let's send it anyway, as element-web does so
|
||||||
|
chain_index: this._session.message_index()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this._session) {
|
||||||
|
this._session.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_encryptContent(type, content) {
|
||||||
const plaintext = JSON.stringify({
|
const plaintext = JSON.stringify({
|
||||||
room_id: roomId,
|
room_id: this._roomId,
|
||||||
type,
|
type,
|
||||||
content
|
content
|
||||||
});
|
});
|
||||||
const ciphertext = session.encrypt(plaintext);
|
const ciphertext = this._session.encrypt(plaintext);
|
||||||
|
|
||||||
const encryptedContent = {
|
const encryptedContent = {
|
||||||
algorithm: MEGOLM_ALGORITHM,
|
algorithm: MEGOLM_ALGORITHM,
|
||||||
sender_key: this._account.identityKeys.curve25519,
|
sender_key: this._account.identityKeys.curve25519,
|
||||||
ciphertext,
|
ciphertext,
|
||||||
session_id: session.session_id(),
|
session_id: this._session.session_id(),
|
||||||
device_id: this._ownDeviceId
|
device_id: this._ownDeviceId
|
||||||
};
|
};
|
||||||
|
|
||||||
return encryptedContent;
|
return encryptedContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createRoomKeyMessage(session, roomId) {
|
|
||||||
return {
|
_readOrCreateSession(txn) {
|
||||||
room_id: roomId,
|
if (this.needsNewSession()) {
|
||||||
session_id: session.session_id(),
|
if (this._session) {
|
||||||
session_key: session.session_key(),
|
this._session.free();
|
||||||
algorithm: MEGOLM_ALGORITHM,
|
this._session = new this._olm.OutboundGroupSession();
|
||||||
// chain_index is ignored by element-web if not all clients
|
}
|
||||||
// but let's send it anyway, as element-web does so
|
this._session.create();
|
||||||
chain_index: session.message_index()
|
this._storeAsInboundSession(txn);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_storeAsInboundSession(outboundSession, roomId, txn) {
|
_writeSession(txn) {
|
||||||
|
this._sessionEntry = {
|
||||||
|
roomId: this._roomId,
|
||||||
|
session: this._session.pickle(this._pickleKey),
|
||||||
|
createdAt: this._sessionEntry?.createdAt || this._now(),
|
||||||
|
};
|
||||||
|
txn.outboundGroupSessions.set(this._sessionEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
_storeAsInboundSession(txn) {
|
||||||
const {identityKeys} = this._account;
|
const {identityKeys} = this._account;
|
||||||
const claimedKeys = {ed25519: identityKeys.ed25519};
|
const claimedKeys = {ed25519: identityKeys.ed25519};
|
||||||
const session = new this._olm.InboundGroupSession();
|
const session = new this._olm.InboundGroupSession();
|
||||||
try {
|
try {
|
||||||
session.create(outboundSession.session_key());
|
session.create(this._session.session_key());
|
||||||
const sessionEntry = {
|
const sessionEntry = {
|
||||||
roomId,
|
roomId: this._roomId,
|
||||||
senderKey: identityKeys.curve25519,
|
senderKey: identityKeys.curve25519,
|
||||||
sessionId: session.session_id(),
|
sessionId: session.session_id(),
|
||||||
session: session.pickle(this._pickleKey),
|
session: session.pickle(this._pickleKey),
|
||||||
|
@ -187,7 +209,6 @@ export class Encryption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property {object?} roomKeyMessage if encrypting this message
|
* @property {object?} roomKeyMessage if encrypting this message
|
||||||
* created a new outbound session,
|
* created a new outbound session,
|
||||||
|
|
Loading…
Reference in a new issue