forked from mystiq/hydrogen-web
implement room key sharing with operations store
This commit is contained in:
parent
b00865510f
commit
ab1fe711ad
3 changed files with 86 additions and 65 deletions
|
@ -226,7 +226,9 @@ export class Sync {
|
||||||
storeNames.groupSessionDecryptions,
|
storeNames.groupSessionDecryptions,
|
||||||
storeNames.deviceIdentities,
|
storeNames.deviceIdentities,
|
||||||
// to discard outbound session when somebody leaves a room
|
// to discard outbound session when somebody leaves a room
|
||||||
storeNames.outboundGroupSessions
|
// and to create room key messages when somebody leaves
|
||||||
|
storeNames.outboundGroupSessions,
|
||||||
|
storeNames.operations
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,13 +47,14 @@ export class RoomEncryption {
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeMemberChanges(memberChanges, txn) {
|
async writeMemberChanges(memberChanges, txn) {
|
||||||
for (const m of memberChanges.values()) {
|
const memberChangesArray = Array.from(memberChanges.values());
|
||||||
if (m.hasLeft) {
|
if (memberChangesArray.some(m => m.hasLeft)) {
|
||||||
this._megolmEncryption.discardOutboundSession(this._room.id, txn);
|
this._megolmEncryption.discardOutboundSession(this._room.id, txn);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
if (memberChangesArray.some(m => m.hasJoined)) {
|
||||||
|
await this._addShareRoomKeyOperationForNewMembers(memberChangesArray, txn);
|
||||||
}
|
}
|
||||||
return await this._deviceTracker.writeMemberChanges(this._room, memberChanges, txn);
|
await this._deviceTracker.writeMemberChanges(this._room, memberChanges, txn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this happens before entries exists, as they are created by the syncwriter
|
// this happens before entries exists, as they are created by the syncwriter
|
||||||
|
@ -146,16 +147,10 @@ export class RoomEncryption {
|
||||||
}
|
}
|
||||||
|
|
||||||
async encrypt(type, content, hsApi) {
|
async encrypt(type, content, hsApi) {
|
||||||
const megolmResult = await this._megolmEncryption.encrypt(this._room.id, type, content, this._encryptionParams);
|
|
||||||
// share the new megolm session if needed
|
|
||||||
if (megolmResult.roomKeyMessage) {
|
|
||||||
await this._deviceTracker.trackRoom(this._room);
|
await this._deviceTracker.trackRoom(this._room);
|
||||||
const devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi);
|
const megolmResult = await this._megolmEncryption.encrypt(this._room.id, type, content, this._encryptionParams);
|
||||||
await this._sendRoomKey(megolmResult.roomKeyMessage, devices, hsApi);
|
if (megolmResult.roomKeyMessage) {
|
||||||
// if we happen to rotate the session before we have sent newly joined members the room key
|
this._shareNewRoomKey(megolmResult.roomKeyMessage, hsApi);
|
||||||
// then mark those members as not needing the key anymore
|
|
||||||
const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set()));
|
|
||||||
await this._clearNeedsRoomKeyFlag(userIds);
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: ENCRYPTED_TYPE,
|
type: ENCRYPTED_TYPE,
|
||||||
|
@ -165,64 +160,87 @@ export class RoomEncryption {
|
||||||
|
|
||||||
needsToShareKeys(memberChanges) {
|
needsToShareKeys(memberChanges) {
|
||||||
for (const m of memberChanges.values()) {
|
for (const m of memberChanges.values()) {
|
||||||
if (m.member.needsRoomKey) {
|
if (m.hasJoined) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async shareRoomKeyToPendingMembers(hsApi) {
|
async _shareNewRoomKey(roomKeyMessage, hsApi) {
|
||||||
// sucks to call this for all encrypted rooms on startup?
|
const devices = await this._deviceTracker.devicesForTrackedRoom(this._room.id, hsApi);
|
||||||
const txn = await this._storage.readTxn([this._storage.storeNames.roomMembers]);
|
const userIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set()));
|
||||||
const pendingUserIds = await txn.roomMembers.getUserIdsNeedingRoomKey(this._room.id);
|
|
||||||
return await this._shareRoomKey(pendingUserIds, hsApi);
|
|
||||||
}
|
|
||||||
|
|
||||||
async shareRoomKeyForMemberChanges(memberChanges, hsApi) {
|
// store operation for room key share, in case we don't finish here
|
||||||
const pendingUserIds = [];
|
const writeOpTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]);
|
||||||
for (const m of memberChanges.values()) {
|
let operationId;
|
||||||
if (m.member.needsRoomKey) {
|
|
||||||
pendingUserIds.push(m.userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return await this._shareRoomKey(pendingUserIds, hsApi);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _shareRoomKey(userIds, hsApi) {
|
|
||||||
if (userIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const readRoomKeyTxn = await this._storage.readTxn([this._storage.storeNames.outboundGroupSessions]);
|
|
||||||
const roomKeyMessage = await this._megolmEncryption.createRoomKeyMessage(this._room.id, readRoomKeyTxn);
|
|
||||||
// no room key if we haven't created a session yet
|
|
||||||
// (or we removed it and will create a new one on the next send)
|
|
||||||
if (roomKeyMessage) {
|
|
||||||
const devices = await this._deviceTracker.devicesForRoomMembers(this._room.id, userIds, hsApi);
|
|
||||||
await this._sendRoomKey(roomKeyMessage, devices, hsApi);
|
|
||||||
const actuallySentUserIds = Array.from(devices.reduce((set, device) => set.add(device.userId), new Set()));
|
|
||||||
await this._clearNeedsRoomKeyFlag(actuallySentUserIds);
|
|
||||||
} else {
|
|
||||||
// we don't have a session yet, clear them all
|
|
||||||
await this._clearNeedsRoomKeyFlag(userIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _clearNeedsRoomKeyFlag(userIds) {
|
|
||||||
const txn = await this._storage.readWriteTxn([this._storage.storeNames.roomMembers]);
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(userIds.map(async userId => {
|
operationId = this._writeRoomKeyShareOperation(roomKeyMessage, userIds, writeOpTxn);
|
||||||
const memberData = await txn.roomMembers.get(this._room.id, userId);
|
|
||||||
if (memberData.needsRoomKey) {
|
|
||||||
memberData.needsRoomKey = false;
|
|
||||||
txn.roomMembers.set(memberData);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
txn.abort();
|
writeOpTxn.abort();
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
await txn.complete();
|
await writeOpTxn.complete();
|
||||||
|
// TODO: at this point we have the room key stored, and the rest is sort of optional
|
||||||
|
// it would be nice if we could signal SendQueue that any error from here on is non-fatal and
|
||||||
|
// return the encrypted payload.
|
||||||
|
|
||||||
|
// send the room key
|
||||||
|
await this._sendRoomKey(roomKeyMessage, devices, hsApi);
|
||||||
|
|
||||||
|
// remove the operation
|
||||||
|
const removeOpTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]);
|
||||||
|
try {
|
||||||
|
removeOpTxn.operations.remove(operationId);
|
||||||
|
} catch (err) {
|
||||||
|
removeOpTxn.abort();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await removeOpTxn.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _addShareRoomKeyOperationForNewMembers(memberChangesArray, txn) {
|
||||||
|
const userIds = memberChangesArray.filter(m => m.hasJoined).map(m => m.userId);
|
||||||
|
const roomKeyMessage = await this._megolmEncryption.createRoomKeyMessage(
|
||||||
|
this._room.id, txn);
|
||||||
|
if (roomKeyMessage) {
|
||||||
|
this._writeRoomKeyShareOperation(roomKeyMessage, userIds, txn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeRoomKeyShareOperation(roomKeyMessage, userIds, txn) {
|
||||||
|
const id = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString();
|
||||||
|
txn.operations.add({
|
||||||
|
id,
|
||||||
|
type: "share_room_key",
|
||||||
|
scope: this._room.id,
|
||||||
|
userIds,
|
||||||
|
roomKeyMessage,
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async flushPendingRoomKeyShares(hsApi, operations = null) {
|
||||||
|
if (!operations) {
|
||||||
|
const txn = await this._storage.readTxn([this._storage.storeNames.operations]);
|
||||||
|
operations = await txn.operations.getAllByTypeAndScope("share_room_key", this._room.id);
|
||||||
|
}
|
||||||
|
for (const operation of operations) {
|
||||||
|
// just to be sure
|
||||||
|
if (operation.type !== "share_room_key") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const devices = await this._deviceTracker.devicesForRoomMembers(this._room.id, operation.userIds, hsApi);
|
||||||
|
await this._sendRoomKey(operation.roomKeyMessage, devices, hsApi);
|
||||||
|
const removeTxn = await this._storage.readWriteTxn([this._storage.storeNames.operations]);
|
||||||
|
try {
|
||||||
|
removeTxn.operations.remove(operation.id);
|
||||||
|
} catch (err) {
|
||||||
|
removeTxn.abort();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await removeTxn.complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _sendRoomKey(roomKeyMessage, devices, hsApi) {
|
async _sendRoomKey(roomKeyMessage, devices, hsApi) {
|
||||||
|
|
|
@ -243,7 +243,8 @@ export class Room extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
needsAfterSyncCompleted({memberChanges}) {
|
needsAfterSyncCompleted({memberChanges}) {
|
||||||
return this._roomEncryption?.needsToShareKeys(memberChanges);
|
const result = this._roomEncryption?.needsToShareKeys(memberChanges);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -251,9 +252,9 @@ export class Room extends EventEmitter {
|
||||||
* Can be used to do longer running operations that resulted from the last sync,
|
* Can be used to do longer running operations that resulted from the last sync,
|
||||||
* like network operations.
|
* like network operations.
|
||||||
*/
|
*/
|
||||||
async afterSyncCompleted({memberChanges}) {
|
async afterSyncCompleted() {
|
||||||
if (this._roomEncryption) {
|
if (this._roomEncryption) {
|
||||||
await this._roomEncryption.shareRoomKeyForMemberChanges(memberChanges, this._hsApi);
|
await this._roomEncryption.flushPendingRoomKeyShares(this._hsApi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue