You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-25 05:23:13 +03:00
Various changes to src/crypto files for correctness (#2137)
* make various changes for correctness * apply some review feedback * Address some review feedback * add some more correctness * refactor ensureOutboundSession to fit types better * change variable naming slightly to prevent confusion * some wording around exception-catching * Tidy test * Simplify * Add tests * Add more test coverage * Apply suggestions from code review Co-authored-by: Travis Ralston <travpc@gmail.com> * Update crypto.spec.js * Update spec/unit/crypto.spec.js Co-authored-by: Faye Duxovni <duxovni@duxovni.org> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Travis Ralston <travpc@gmail.com> Co-authored-by: Faye Duxovni <duxovni@duxovni.org>
This commit is contained in:
@@ -17,6 +17,43 @@ import { logger } from '../../src/logger';
|
|||||||
|
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
|
|
||||||
|
function awaitEvent(emitter, event) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
emitter.once(event, (result) => {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function keyshareEventForEvent(client, event, index) {
|
||||||
|
const roomId = event.getRoomId();
|
||||||
|
const eventContent = event.getWireContent();
|
||||||
|
const key = await client.crypto.olmDevice.getInboundGroupSessionKey(
|
||||||
|
roomId,
|
||||||
|
eventContent.sender_key,
|
||||||
|
eventContent.session_id,
|
||||||
|
index,
|
||||||
|
);
|
||||||
|
const ksEvent = new MatrixEvent({
|
||||||
|
type: "m.forwarded_room_key",
|
||||||
|
sender: client.getUserId(),
|
||||||
|
content: {
|
||||||
|
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||||
|
room_id: roomId,
|
||||||
|
sender_key: eventContent.sender_key,
|
||||||
|
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||||
|
session_id: eventContent.session_id,
|
||||||
|
session_key: key.key,
|
||||||
|
chain_index: key.chain_index,
|
||||||
|
forwarding_curve25519_key_chain:
|
||||||
|
key.forwarding_curve_key_chain,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// make onRoomKeyEvent think this was an encrypted event
|
||||||
|
ksEvent.senderCurve25519Key = "akey";
|
||||||
|
return ksEvent;
|
||||||
|
}
|
||||||
|
|
||||||
describe("Crypto", function() {
|
describe("Crypto", function() {
|
||||||
if (!CRYPTO_ENABLED) {
|
if (!CRYPTO_ENABLED) {
|
||||||
return;
|
return;
|
||||||
@@ -203,44 +240,7 @@ describe("Crypto", function() {
|
|||||||
bobClient.stopClient();
|
bobClient.stopClient();
|
||||||
});
|
});
|
||||||
|
|
||||||
it(
|
it("does not cancel keyshare requests if some messages are not decrypted", async function() {
|
||||||
"does not cancel keyshare requests if some messages are not decrypted",
|
|
||||||
async function() {
|
|
||||||
function awaitEvent(emitter, event) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
emitter.once(event, (result) => {
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function keyshareEventForEvent(event, index) {
|
|
||||||
const eventContent = event.getWireContent();
|
|
||||||
const key = await aliceClient.crypto.olmDevice
|
|
||||||
.getInboundGroupSessionKey(
|
|
||||||
roomId, eventContent.sender_key, eventContent.session_id,
|
|
||||||
index,
|
|
||||||
);
|
|
||||||
const ksEvent = new MatrixEvent({
|
|
||||||
type: "m.forwarded_room_key",
|
|
||||||
sender: "@alice:example.com",
|
|
||||||
content: {
|
|
||||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
|
||||||
room_id: roomId,
|
|
||||||
sender_key: eventContent.sender_key,
|
|
||||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
|
||||||
session_id: eventContent.session_id,
|
|
||||||
session_key: key.key,
|
|
||||||
chain_index: key.chain_index,
|
|
||||||
forwarding_curve25519_key_chain:
|
|
||||||
key.forwarding_curve_key_chain,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// make onRoomKeyEvent think this was an encrypted event
|
|
||||||
ksEvent.senderCurve25519Key = "akey";
|
|
||||||
return ksEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptionCfg = {
|
const encryptionCfg = {
|
||||||
"algorithm": "m.megolm.v1.aes-sha2",
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
};
|
};
|
||||||
@@ -299,7 +299,7 @@ describe("Crypto", function() {
|
|||||||
// keyshare the session key starting at the second message, so
|
// keyshare the session key starting at the second message, so
|
||||||
// the first message can't be decrypted yet, but the second one
|
// the first message can't be decrypted yet, but the second one
|
||||||
// can
|
// can
|
||||||
let ksEvent = await keyshareEventForEvent(events[1], 1);
|
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
||||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||||
await eventPromise;
|
await eventPromise;
|
||||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||||
@@ -317,23 +317,65 @@ describe("Crypto", function() {
|
|||||||
};
|
};
|
||||||
// the room key request should still be there, since we haven't
|
// the room key request should still be there, since we haven't
|
||||||
// decrypted everything
|
// decrypted everything
|
||||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined();
|
||||||
.toBeDefined();
|
|
||||||
|
|
||||||
// keyshare the session key starting at the first message, so
|
// keyshare the session key starting at the first message, so
|
||||||
// that it can now be decrypted
|
// that it can now be decrypted
|
||||||
eventPromise = awaitEvent(events[0], "Event.decrypted");
|
eventPromise = awaitEvent(events[0], "Event.decrypted");
|
||||||
ksEvent = await keyshareEventForEvent(events[0], 0);
|
ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||||
await eventPromise;
|
await eventPromise;
|
||||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||||
await sleep(1);
|
await sleep(1);
|
||||||
// the room key request should be gone since we've now decrypted everything
|
// the room key request should be gone since we've now decrypted everything
|
||||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
|
||||||
.toBeFalsy();
|
});
|
||||||
|
|
||||||
|
it("should error if a forwarded room key lacks a content.sender_key", async function() {
|
||||||
|
const encryptionCfg = {
|
||||||
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
|
};
|
||||||
|
const roomId = "!someroom";
|
||||||
|
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||||
|
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||||
|
aliceClient.store.storeRoom(aliceRoom);
|
||||||
|
bobClient.store.storeRoom(bobRoom);
|
||||||
|
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
const event = new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: "$1",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "1",
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
// alice encrypts each event, and then bob tries to decrypt
|
||||||
|
// them without any keys, so that they'll be in pending
|
||||||
|
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||||
|
event.clearEvent = undefined;
|
||||||
|
event.senderCurve25519Key = null;
|
||||||
|
event.claimedEd25519Key = null;
|
||||||
|
try {
|
||||||
|
await bobClient.crypto.decryptEvent(event);
|
||||||
|
} catch (e) {
|
||||||
|
// we expect this to fail because we don't have the
|
||||||
|
// decryption keys yet
|
||||||
|
}
|
||||||
|
|
||||||
|
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||||
|
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
|
||||||
|
ksEvent.getContent().sender_key = undefined; // test
|
||||||
|
bobClient.crypto.addInboundGroupSession = jest.fn();
|
||||||
|
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||||
|
expect(bobClient.crypto.addInboundGroupSession).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("creates a new keyshare request if we request a keyshare", async function() {
|
it("creates a new keyshare request if we request a keyshare", async function() {
|
||||||
// make sure that cancelAndResend... creates a new keyshare request
|
// make sure that cancelAndResend... creates a new keyshare request
|
||||||
// if there wasn't an already-existing one
|
// if there wasn't an already-existing one
|
||||||
|
|||||||
@@ -257,6 +257,8 @@ describe("MegolmDecryption", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("session reuse and key reshares", () => {
|
describe("session reuse and key reshares", () => {
|
||||||
|
const rotationPeriodMs = 999 * 24 * 60 * 60 * 1000; // 999 days, so we don't have to deal with it
|
||||||
|
|
||||||
let megolmEncryption;
|
let megolmEncryption;
|
||||||
let aliceDeviceInfo;
|
let aliceDeviceInfo;
|
||||||
let mockRoom;
|
let mockRoom;
|
||||||
@@ -318,7 +320,7 @@ describe("MegolmDecryption", function() {
|
|||||||
baseApis: mockBaseApis,
|
baseApis: mockBaseApis,
|
||||||
roomId: ROOM_ID,
|
roomId: ROOM_ID,
|
||||||
config: {
|
config: {
|
||||||
rotation_period_ms: 9999999999999,
|
rotation_period_ms: rotationPeriodMs,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mockRoom = {
|
mockRoom = {
|
||||||
@@ -329,6 +331,31 @@ describe("MegolmDecryption", function() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should use larger otkTimeout when preparing to encrypt room", async () => {
|
||||||
|
megolmEncryption.prepareToEncrypt(mockRoom);
|
||||||
|
await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||||
|
body: "Some text",
|
||||||
|
});
|
||||||
|
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||||
|
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 10000,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate a new session if this one needs rotation", async () => {
|
||||||
|
const session = await megolmEncryption.prepareNewSession(false);
|
||||||
|
session.creationTime -= rotationPeriodMs + 10000; // a smidge over the rotation time
|
||||||
|
// Inject expired session which needs rotation
|
||||||
|
megolmEncryption.setupPromise = Promise.resolve(session);
|
||||||
|
|
||||||
|
const prepareNewSessionSpy = jest.spyOn(megolmEncryption, "prepareNewSession");
|
||||||
|
await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||||
|
body: "Some text",
|
||||||
|
});
|
||||||
|
expect(prepareNewSessionSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("re-uses sessions for sequential messages", async function() {
|
it("re-uses sessions for sequential messages", async function() {
|
||||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||||
body: "Some text",
|
body: "Some text",
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export abstract class DecryptionAlgorithm {
|
|||||||
*
|
*
|
||||||
* @param {module:models/event.MatrixEvent} params event key event
|
* @param {module:models/event.MatrixEvent} params event key event
|
||||||
*/
|
*/
|
||||||
public onRoomKeyEvent(params: MatrixEvent): void {
|
public async onRoomKeyEvent(params: MatrixEvent): Promise<void> {
|
||||||
// ignore by default
|
// ignore by default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ class OutboundSessionInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +233,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
// are using, and which devices we have shared the keys with. It resolves
|
// are using, and which devices we have shared the keys with. It resolves
|
||||||
// with an OutboundSessionInfo (or undefined, for the first message in the
|
// with an OutboundSessionInfo (or undefined, for the first message in the
|
||||||
// room).
|
// room).
|
||||||
private setupPromise = Promise.resolve<OutboundSessionInfo>(undefined);
|
private setupPromise = Promise.resolve<OutboundSessionInfo | null>(null);
|
||||||
|
|
||||||
// Map of outbound sessions by sessions ID. Used if we need a particular
|
// Map of outbound sessions by sessions ID. Used if we need a particular
|
||||||
// session (the session we're currently using to send is always obtained
|
// session (the session we're currently using to send is always obtained
|
||||||
@@ -240,8 +242,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
|
|
||||||
private readonly sessionRotationPeriodMsgs: number;
|
private readonly sessionRotationPeriodMsgs: number;
|
||||||
private readonly sessionRotationPeriodMs: number;
|
private readonly sessionRotationPeriodMs: number;
|
||||||
private encryptionPreparation: Promise<void>;
|
private encryptionPreparation?: {
|
||||||
private encryptionPreparationMetadata: {
|
promise: Promise<void>;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,33 +272,59 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
blocked: IBlockedMap,
|
blocked: IBlockedMap,
|
||||||
singleOlmCreationPhase = false,
|
singleOlmCreationPhase = false,
|
||||||
): Promise<OutboundSessionInfo> {
|
): Promise<OutboundSessionInfo> {
|
||||||
let session: OutboundSessionInfo;
|
|
||||||
|
|
||||||
// takes the previous OutboundSessionInfo, and considers whether to create
|
// takes the previous OutboundSessionInfo, and considers whether to create
|
||||||
// a new one. Also shares the key with any (new) devices in the room.
|
// a new one. Also shares the key with any (new) devices in the room.
|
||||||
// Updates `session` to hold the final OutboundSessionInfo.
|
//
|
||||||
|
// Returns the successful session whether keyshare succeeds or not.
|
||||||
//
|
//
|
||||||
// returns a promise which resolves once the keyshare is successful.
|
// returns a promise which resolves once the keyshare is successful.
|
||||||
const prepareSession = async (oldSession: OutboundSessionInfo) => {
|
const setup = async (oldSession: OutboundSessionInfo | null): Promise<OutboundSessionInfo> => {
|
||||||
session = oldSession;
|
|
||||||
|
|
||||||
const sharedHistory = isRoomSharedHistory(room);
|
const sharedHistory = isRoomSharedHistory(room);
|
||||||
|
|
||||||
|
const session = await this.prepareSession(devicesInRoom, sharedHistory, oldSession);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.shareSession(devicesInRoom, sharedHistory, singleOlmCreationPhase, blocked, session);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Failed to ensure outbound session in ${this.roomId}`, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
// first wait for the previous share to complete
|
||||||
|
const prom = this.setupPromise.then(setup);
|
||||||
|
|
||||||
|
// Ensure any failures are logged for debugging
|
||||||
|
prom.catch(e => {
|
||||||
|
logger.error(`Failed to setup outbound session in ${this.roomId}`, e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// setupPromise resolves to `session` whether or not the share succeeds
|
||||||
|
this.setupPromise = prom;
|
||||||
|
|
||||||
|
// but we return a promise which only resolves if the share was successful.
|
||||||
|
return prom;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prepareSession(
|
||||||
|
devicesInRoom: DeviceInfoMap,
|
||||||
|
sharedHistory: boolean,
|
||||||
|
session: OutboundSessionInfo | null,
|
||||||
|
): Promise<OutboundSessionInfo> {
|
||||||
// history visibility changed
|
// history visibility changed
|
||||||
if (session && sharedHistory !== session.sharedHistory) {
|
if (session && sharedHistory !== session.sharedHistory) {
|
||||||
session = null;
|
session = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// need to make a brand new session?
|
// need to make a brand new session?
|
||||||
if (session && session.needsRotation(this.sessionRotationPeriodMsgs,
|
if (session?.needsRotation(this.sessionRotationPeriodMsgs, this.sessionRotationPeriodMs)) {
|
||||||
this.sessionRotationPeriodMs)
|
|
||||||
) {
|
|
||||||
logger.log("Starting new megolm session because we need to rotate.");
|
logger.log("Starting new megolm session because we need to rotate.");
|
||||||
session = null;
|
session = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine if we have shared with anyone we shouldn't have
|
// determine if we have shared with anyone we shouldn't have
|
||||||
if (session && session.sharedWithTooManyDevices(devicesInRoom)) {
|
if (session?.sharedWithTooManyDevices(devicesInRoom)) {
|
||||||
session = null;
|
session = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,6 +336,16 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
this.outboundSessions[session.sessionId] = session;
|
this.outboundSessions[session.sessionId] = session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async shareSession(
|
||||||
|
devicesInRoom: DeviceInfoMap,
|
||||||
|
sharedHistory: boolean,
|
||||||
|
singleOlmCreationPhase: boolean,
|
||||||
|
blocked: IBlockedMap,
|
||||||
|
session: OutboundSessionInfo,
|
||||||
|
) {
|
||||||
// now check if we need to share with any devices
|
// now check if we need to share with any devices
|
||||||
const shareMap: Record<string, DeviceInfo[]> = {};
|
const shareMap: Record<string, DeviceInfo[]> = {};
|
||||||
|
|
||||||
@@ -386,7 +424,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
for (const server of failedServers) {
|
for (const server of failedServers) {
|
||||||
failedServerMap.add(server);
|
failedServerMap.add(server);
|
||||||
}
|
}
|
||||||
const failedDevices = [];
|
const failedDevices: IOlmDevice[] = [];
|
||||||
for (const { userId, deviceInfo } of errorDevices) {
|
for (const { userId, deviceInfo } of errorDevices) {
|
||||||
const userHS = userId.slice(userId.indexOf(":") + 1);
|
const userHS = userId.slice(userId.indexOf(":") + 1);
|
||||||
if (failedServerMap.has(userHS)) {
|
if (failedServerMap.has(userHS)) {
|
||||||
@@ -437,26 +475,6 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
logger.debug(`Notified ${blockedCount} newly blocked devices in ${this.roomId}`, blockedMap);
|
logger.debug(`Notified ${blockedCount} newly blocked devices in ${this.roomId}`, blockedMap);
|
||||||
})(),
|
})(),
|
||||||
]);
|
]);
|
||||||
};
|
|
||||||
|
|
||||||
// helper which returns the session prepared by prepareSession
|
|
||||||
function returnSession() {
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
// first wait for the previous share to complete
|
|
||||||
const prom = this.setupPromise.then(prepareSession);
|
|
||||||
|
|
||||||
// Ensure any failures are logged for debugging
|
|
||||||
prom.catch(e => {
|
|
||||||
logger.error(`Failed to ensure outbound session in ${this.roomId}`, e);
|
|
||||||
});
|
|
||||||
|
|
||||||
// setupPromise resolves to `session` whether or not the share succeeds
|
|
||||||
this.setupPromise = prom.then(returnSession, returnSession);
|
|
||||||
|
|
||||||
// but we return a promise which only resolves if the share was successful.
|
|
||||||
return prom.then(returnSession);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -866,7 +884,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
logger.debug(`Ensuring Olm sessions for devices in ${this.roomId}`);
|
logger.debug(`Ensuring Olm sessions for devices in ${this.roomId}`);
|
||||||
const devicemap = await olmlib.ensureOlmSessionsForDevices(
|
const devicemap = await olmlib.ensureOlmSessionsForDevices(
|
||||||
this.olmDevice, this.baseApis, devicesByUser, false, otkTimeout, failedServers,
|
this.olmDevice, this.baseApis, devicesByUser, false, otkTimeout, failedServers,
|
||||||
logger.withPrefix(`[${this.roomId}]`),
|
logger.withPrefix?.(`[${this.roomId}]`),
|
||||||
);
|
);
|
||||||
logger.debug(`Ensured Olm sessions for devices in ${this.roomId}`);
|
logger.debug(`Ensured Olm sessions for devices in ${this.roomId}`);
|
||||||
|
|
||||||
@@ -1006,11 +1024,11 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
* @param {module:models/room} room the room the event is in
|
* @param {module:models/room} room the room the event is in
|
||||||
*/
|
*/
|
||||||
public prepareToEncrypt(room: Room): void {
|
public prepareToEncrypt(room: Room): void {
|
||||||
if (this.encryptionPreparation) {
|
if (this.encryptionPreparation != null) {
|
||||||
// We're already preparing something, so don't do anything else.
|
// We're already preparing something, so don't do anything else.
|
||||||
// FIXME: check if we need to restart
|
// FIXME: check if we need to restart
|
||||||
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
|
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
|
||||||
const elapsedTime = Date.now() - this.encryptionPreparationMetadata.startTime;
|
const elapsedTime = Date.now() - this.encryptionPreparation.startTime;
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Already started preparing to encrypt for ${this.roomId} ` +
|
`Already started preparing to encrypt for ${this.roomId} ` +
|
||||||
`${elapsedTime} ms ago, skipping`,
|
`${elapsedTime} ms ago, skipping`,
|
||||||
@@ -1020,10 +1038,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
|
|
||||||
logger.debug(`Preparing to encrypt events for ${this.roomId}`);
|
logger.debug(`Preparing to encrypt events for ${this.roomId}`);
|
||||||
|
|
||||||
this.encryptionPreparationMetadata = {
|
this.encryptionPreparation = {
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
};
|
promise: (async () => {
|
||||||
this.encryptionPreparation = (async () => {
|
|
||||||
try {
|
try {
|
||||||
logger.debug(`Getting devices in ${this.roomId}`);
|
logger.debug(`Getting devices in ${this.roomId}`);
|
||||||
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room);
|
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room);
|
||||||
@@ -1042,10 +1059,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Failed to prepare to encrypt events for ${this.roomId}`, e);
|
logger.error(`Failed to prepare to encrypt events for ${this.roomId}`, e);
|
||||||
} finally {
|
} finally {
|
||||||
delete this.encryptionPreparationMetadata;
|
|
||||||
delete this.encryptionPreparation;
|
delete this.encryptionPreparation;
|
||||||
}
|
}
|
||||||
})();
|
})(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1060,12 +1077,12 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
public async encryptMessage(room: Room, eventType: string, content: object): Promise<object> {
|
public async encryptMessage(room: Room, eventType: string, content: object): Promise<object> {
|
||||||
logger.log(`Starting to encrypt event for ${this.roomId}`);
|
logger.log(`Starting to encrypt event for ${this.roomId}`);
|
||||||
|
|
||||||
if (this.encryptionPreparation) {
|
if (this.encryptionPreparation != null) {
|
||||||
// If we started sending keys, wait for it to be done.
|
// If we started sending keys, wait for it to be done.
|
||||||
// FIXME: check if we need to cancel
|
// FIXME: check if we need to cancel
|
||||||
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
|
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
|
||||||
try {
|
try {
|
||||||
await this.encryptionPreparation;
|
await this.encryptionPreparation.promise;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore any errors -- if the preparation failed, we'll just
|
// ignore any errors -- if the preparation failed, we'll just
|
||||||
// restart everything here
|
// restart everything here
|
||||||
@@ -1405,7 +1422,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
if (!senderPendingEvents.has(sessionId)) {
|
if (!senderPendingEvents.has(sessionId)) {
|
||||||
senderPendingEvents.set(sessionId, new Set());
|
senderPendingEvents.set(sessionId, new Set());
|
||||||
}
|
}
|
||||||
senderPendingEvents.get(sessionId).add(event);
|
senderPendingEvents.get(sessionId)?.add(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1439,17 +1456,17 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
*
|
*
|
||||||
* @param {module:models/event.MatrixEvent} event key event
|
* @param {module:models/event.MatrixEvent} event key event
|
||||||
*/
|
*/
|
||||||
public onRoomKeyEvent(event: MatrixEvent): Promise<void> {
|
public async onRoomKeyEvent(event: MatrixEvent): Promise<void> {
|
||||||
const content = event.getContent();
|
const content = event.getContent<Partial<IMessage["content"]>>();
|
||||||
const sessionId = content.session_id;
|
|
||||||
let senderKey = event.getSenderKey();
|
let senderKey = event.getSenderKey();
|
||||||
let forwardingKeyChain = [];
|
let forwardingKeyChain: string[] = [];
|
||||||
let exportFormat = false;
|
let exportFormat = false;
|
||||||
let keysClaimed;
|
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
|
||||||
|
|
||||||
if (!content.room_id ||
|
if (!content.room_id ||
|
||||||
!sessionId ||
|
!content.session_key ||
|
||||||
!content.session_key
|
!content.session_id ||
|
||||||
|
!content.algorithm
|
||||||
) {
|
) {
|
||||||
logger.error("key event is missing fields");
|
logger.error("key event is missing fields");
|
||||||
return;
|
return;
|
||||||
@@ -1462,20 +1479,18 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
|
|
||||||
if (event.getType() == "m.forwarded_room_key") {
|
if (event.getType() == "m.forwarded_room_key") {
|
||||||
exportFormat = true;
|
exportFormat = true;
|
||||||
forwardingKeyChain = content.forwarding_curve25519_key_chain;
|
forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ?
|
||||||
if (!Array.isArray(forwardingKeyChain)) {
|
content.forwarding_curve25519_key_chain : [];
|
||||||
forwardingKeyChain = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy content before we modify it
|
// copy content before we modify it
|
||||||
forwardingKeyChain = forwardingKeyChain.slice();
|
forwardingKeyChain = forwardingKeyChain.slice();
|
||||||
forwardingKeyChain.push(senderKey);
|
forwardingKeyChain.push(senderKey);
|
||||||
|
|
||||||
senderKey = content.sender_key;
|
if (!content.sender_key) {
|
||||||
if (!senderKey) {
|
|
||||||
logger.error("forwarded_room_key event is missing sender_key field");
|
logger.error("forwarded_room_key event is missing sender_key field");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
senderKey = content.sender_key;
|
||||||
|
|
||||||
const ed25519Key = content.sender_claimed_ed25519_key;
|
const ed25519Key = content.sender_claimed_ed25519_key;
|
||||||
if (!ed25519Key) {
|
if (!ed25519Key) {
|
||||||
@@ -1496,20 +1511,26 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
if (content["org.matrix.msc3061.shared_history"]) {
|
if (content["org.matrix.msc3061.shared_history"]) {
|
||||||
extraSessionData.sharedHistory = true;
|
extraSessionData.sharedHistory = true;
|
||||||
}
|
}
|
||||||
return this.olmDevice.addInboundGroupSession(
|
|
||||||
content.room_id, senderKey, forwardingKeyChain, sessionId,
|
try {
|
||||||
content.session_key, keysClaimed,
|
await this.olmDevice.addInboundGroupSession(
|
||||||
exportFormat, extraSessionData,
|
content.room_id,
|
||||||
).then(() => {
|
senderKey,
|
||||||
|
forwardingKeyChain,
|
||||||
|
content.session_id,
|
||||||
|
content.session_key,
|
||||||
|
keysClaimed,
|
||||||
|
exportFormat,
|
||||||
|
extraSessionData,
|
||||||
|
);
|
||||||
|
|
||||||
// have another go at decrypting events sent with this session.
|
// have another go at decrypting events sent with this session.
|
||||||
this.retryDecryption(senderKey, sessionId)
|
if (await this.retryDecryption(senderKey, content.session_id)) {
|
||||||
.then((success) => {
|
|
||||||
// cancel any outstanding room key requests for this session.
|
// cancel any outstanding room key requests for this session.
|
||||||
// Only do this if we managed to decrypt every message in the
|
// Only do this if we managed to decrypt every message in the
|
||||||
// session, because if we didn't, we leave the other key
|
// session, because if we didn't, we leave the other key
|
||||||
// requests in the hopes that someone sends us a key that
|
// requests in the hopes that someone sends us a key that
|
||||||
// includes an earlier index.
|
// includes an earlier index.
|
||||||
if (success) {
|
|
||||||
this.crypto.cancelRoomKeyRequest({
|
this.crypto.cancelRoomKeyRequest({
|
||||||
algorithm: content.algorithm,
|
algorithm: content.algorithm,
|
||||||
room_id: content.room_id,
|
room_id: content.room_id,
|
||||||
@@ -1517,13 +1538,12 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
sender_key: senderKey,
|
sender_key: senderKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}).then(() => {
|
|
||||||
// don't wait for the keys to be backed up for the server
|
// don't wait for the keys to be backed up for the server
|
||||||
this.crypto.backupManager.backupGroupSession(senderKey, content.session_id);
|
await this.crypto.backupManager.backupGroupSession(senderKey, content.session_id);
|
||||||
}).catch((e) => {
|
} catch (e) {
|
||||||
logger.error(`Error handling m.room_key_event: ${e}`);
|
logger.error(`Error handling m.room_key_event: ${e}`);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1716,7 +1736,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
* @param {boolean} [opts.untrusted] whether the key should be considered as untrusted
|
* @param {boolean} [opts.untrusted] whether the key should be considered as untrusted
|
||||||
* @param {string} [opts.source] where the key came from
|
* @param {string} [opts.source] where the key came from
|
||||||
*/
|
*/
|
||||||
public importRoomKey(session: IMegolmSessionData, opts: any = {}): Promise<void> {
|
public importRoomKey(
|
||||||
|
session: IMegolmSessionData,
|
||||||
|
opts: { untrusted?: boolean, source?: string } = {},
|
||||||
|
): Promise<void> {
|
||||||
const extraSessionData: any = {};
|
const extraSessionData: any = {};
|
||||||
if (opts.untrusted || session.untrusted) {
|
if (opts.untrusted || session.untrusted) {
|
||||||
extraSessionData.untrusted = true;
|
extraSessionData.untrusted = true;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ interface IMessage {
|
|||||||
*/
|
*/
|
||||||
class OlmEncryption extends EncryptionAlgorithm {
|
class OlmEncryption extends EncryptionAlgorithm {
|
||||||
private sessionPrepared = false;
|
private sessionPrepared = false;
|
||||||
private prepPromise: Promise<void> = null;
|
private prepPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
@@ -117,11 +117,11 @@ class OlmEncryption extends EncryptionAlgorithm {
|
|||||||
ciphertext: {},
|
ciphertext: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const promises = [];
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < users.length; ++i) {
|
for (let i = 0; i < users.length; ++i) {
|
||||||
const userId = users[i];
|
const userId = users[i];
|
||||||
const devices = this.crypto.getStoredDevicesForUser(userId);
|
const devices = this.crypto.getStoredDevicesForUser(userId) || [];
|
||||||
|
|
||||||
for (let j = 0; j < devices.length; ++j) {
|
for (let j = 0; j < devices.length; ++j) {
|
||||||
const deviceInfo = devices[j];
|
const deviceInfo = devices[j];
|
||||||
@@ -240,7 +240,7 @@ class OlmDecryption extends DecryptionAlgorithm {
|
|||||||
throw new DecryptionError(
|
throw new DecryptionError(
|
||||||
"OLM_BAD_ROOM",
|
"OLM_BAD_ROOM",
|
||||||
"Message intended for room " + payload.room_id, {
|
"Message intended for room " + payload.room_id, {
|
||||||
reported_room: event.getRoomId(),
|
reported_room: event.getRoomId() || "ROOM_ID_UNDEFINED",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2590,7 +2590,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
// because it first stores in memory. We should await the promise only
|
// because it first stores in memory. We should await the promise only
|
||||||
// after all the in-memory state (roomEncryptors and _roomList) has been updated
|
// after all the in-memory state (roomEncryptors and _roomList) has been updated
|
||||||
// to avoid races when calling this method multiple times. Hence keep a hold of the promise.
|
// to avoid races when calling this method multiple times. Hence keep a hold of the promise.
|
||||||
let storeConfigPromise = null;
|
let storeConfigPromise: Promise<void> = null;
|
||||||
if (!existingConfig) {
|
if (!existingConfig) {
|
||||||
storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
|
storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export interface IOlmSessionResult {
|
|||||||
export async function encryptMessageForDevice(
|
export async function encryptMessageForDevice(
|
||||||
resultsObject: Record<string, string>,
|
resultsObject: Record<string, string>,
|
||||||
ourUserId: string,
|
ourUserId: string,
|
||||||
ourDeviceId: string,
|
ourDeviceId: string | undefined,
|
||||||
olmDevice: OlmDevice,
|
olmDevice: OlmDevice,
|
||||||
recipientUserId: string,
|
recipientUserId: string,
|
||||||
recipientDevice: DeviceInfo,
|
recipientDevice: DeviceInfo,
|
||||||
|
|||||||
Reference in New Issue
Block a user