Skip to content

Various changes to src/crypto files for correctness #2137

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/crypto/algorithms/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export abstract class DecryptionAlgorithm {
*
* @param {module:models/event.MatrixEvent} params event key event
*/
public onRoomKeyEvent(params: MatrixEvent): void {
public async onRoomKeyEvent(params: MatrixEvent): Promise<void> {
// ignore by default
}

Expand Down
167 changes: 85 additions & 82 deletions src/crypto/algorithms/megolm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ class OutboundSessionInfo {
}
}
}

return false;
}
}

Expand All @@ -231,7 +233,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
// are using, and which devices we have shared the keys with. It resolves
// with an OutboundSessionInfo (or undefined, for the first message in the
// 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
// session (the session we're currently using to send is always obtained
Expand All @@ -240,8 +242,8 @@ class MegolmEncryption extends EncryptionAlgorithm {

private readonly sessionRotationPeriodMsgs: number;
private readonly sessionRotationPeriodMs: number;
private encryptionPreparation: Promise<void>;
private encryptionPreparationMetadata: {
private encryptionPreparation?: {
promise: Promise<void>;
startTime: number;
};

Expand Down Expand Up @@ -277,37 +279,37 @@ class MegolmEncryption extends EncryptionAlgorithm {
// Updates `session` to hold the final OutboundSessionInfo.
//
// returns a promise which resolves once the keyshare is successful.
const prepareSession = async (oldSession: OutboundSessionInfo) => {
session = oldSession;

const prepareSession = async (oldSession: OutboundSessionInfo | null) => {
const sharedHistory = isRoomSharedHistory(room);

// history visibility changed
if (session && sharedHistory !== session.sharedHistory) {
session = null;
if (oldSession != null && sharedHistory !== oldSession.sharedHistory) {
oldSession = null;
}

// need to make a brand new session?
if (session && session.needsRotation(this.sessionRotationPeriodMsgs,
if (oldSession != null && oldSession.needsRotation(this.sessionRotationPeriodMsgs,
this.sessionRotationPeriodMs)
) {
logger.log("Starting new megolm session because we need to rotate.");
session = null;
oldSession = null;
}

// determine if we have shared with anyone we shouldn't have
if (session && session.sharedWithTooManyDevices(devicesInRoom)) {
session = null;
if (oldSession != null && oldSession.sharedWithTooManyDevices(devicesInRoom)) {
oldSession = null;
}

if (!session) {
if (oldSession == null) {
logger.log(`Starting new megolm session for room ${this.roomId}`);
session = await this.prepareNewSession(sharedHistory);
logger.log(`Started new megolm session ${session.sessionId} ` +
oldSession = await this.prepareNewSession(sharedHistory);
logger.log(`Started new megolm session ${oldSession.sessionId} ` +
`for room ${this.roomId}`);
this.outboundSessions[session.sessionId] = session;
this.outboundSessions[oldSession.sessionId] = oldSession;
}

session = oldSession;

// now check if we need to share with any devices
const shareMap: Record<string, DeviceInfo[]> = {};

Expand Down Expand Up @@ -386,7 +388,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
for (const server of failedServers) {
failedServerMap.add(server);
}
const failedDevices = [];
const failedDevices: IOlmDevice[] = [];
for (const { userId, deviceInfo } of errorDevices) {
const userHS = userId.slice(userId.indexOf(":") + 1);
if (failedServerMap.has(userHS)) {
Expand Down Expand Up @@ -853,7 +855,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
logger.debug(`Ensuring Olm sessions for devices in ${this.roomId}`);
const devicemap = await olmlib.ensureOlmSessionsForDevices(
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}`);

Expand Down Expand Up @@ -993,11 +995,11 @@ class MegolmEncryption extends EncryptionAlgorithm {
* @param {module:models/room} room the room the event is in
*/
public prepareToEncrypt(room: Room): void {
if (this.encryptionPreparation) {
if (this.encryptionPreparation != null) {
// We're already preparing something, so don't do anything else.
// FIXME: check if we need to restart
// (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(
`Already started preparing to encrypt for ${this.roomId} ` +
`${elapsedTime} ms ago, skipping`,
Expand All @@ -1007,32 +1009,31 @@ class MegolmEncryption extends EncryptionAlgorithm {

logger.debug(`Preparing to encrypt events for ${this.roomId}`);

this.encryptionPreparationMetadata = {
this.encryptionPreparation = {
startTime: Date.now(),
};
this.encryptionPreparation = (async () => {
try {
logger.debug(`Getting devices in ${this.roomId}`);
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room);

if (this.crypto.getGlobalErrorOnUnknownDevices()) {
// Drop unknown devices for now. When the message gets sent, we'll
// throw an error, but we'll still be prepared to send to the known
// devices.
this.removeUnknownDevices(devicesInRoom);
}
promise: (async () => {
try {
logger.debug(`Getting devices in ${this.roomId}`);
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room);

if (this.crypto.getGlobalErrorOnUnknownDevices()) {
// Drop unknown devices for now. When the message gets sent, we'll
// throw an error, but we'll still be prepared to send to the known
// devices.
this.removeUnknownDevices(devicesInRoom);
}

logger.debug(`Ensuring outbound session in ${this.roomId}`);
await this.ensureOutboundSession(room, devicesInRoom, blocked, true);
logger.debug(`Ensuring outbound session in ${this.roomId}`);
await this.ensureOutboundSession(room, devicesInRoom, blocked, true);

logger.debug(`Ready to encrypt events for ${this.roomId}`);
} catch (e) {
logger.error(`Failed to prepare to encrypt events for ${this.roomId}`, e);
} finally {
delete this.encryptionPreparationMetadata;
delete this.encryptionPreparation;
}
})();
logger.debug(`Ready to encrypt events for ${this.roomId}`);
} catch (e) {
logger.error(`Failed to prepare to encrypt events for ${this.roomId}`, e);
} finally {
delete this.encryptionPreparation;
}
})(),
};
}

/**
Expand All @@ -1047,12 +1048,12 @@ class MegolmEncryption extends EncryptionAlgorithm {
public async encryptMessage(room: Room, eventType: string, content: object): Promise<object> {
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.
// FIXME: check if we need to cancel
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
try {
await this.encryptionPreparation;
await this.encryptionPreparation.promise;
} catch (e) {
// ignore any errors -- if the preparation failed, we'll just
// restart everything here
Expand Down Expand Up @@ -1390,6 +1391,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
if (!senderPendingEvents.has(sessionId)) {
senderPendingEvents.set(sessionId, new Set());
}
// @ts-ignore: TS isn't smart enough to figure out `has` + `set` above makes this non-null
senderPendingEvents.get(sessionId).add(event);
}

Expand Down Expand Up @@ -1424,17 +1426,17 @@ class MegolmDecryption extends DecryptionAlgorithm {
*
* @param {module:models/event.MatrixEvent} event key event
*/
public onRoomKeyEvent(event: MatrixEvent): Promise<void> {
const content = event.getContent();
const sessionId = content.session_id;
public async onRoomKeyEvent(event: MatrixEvent): Promise<void> {
const content: Partial<IMessage["content"]> = event.getContent();
let senderKey = event.getSenderKey();
let forwardingKeyChain = [];
let forwardingKeyChain: string[] = [];
let exportFormat = false;
let keysClaimed;

if (!content.room_id ||
!sessionId ||
!content.session_key
!content.session_key ||
!content.session_id ||
!content.algorithm
) {
logger.error("key event is missing fields");
return;
Expand All @@ -1447,19 +1449,17 @@ class MegolmDecryption extends DecryptionAlgorithm {

if (event.getType() == "m.forwarded_room_key") {
exportFormat = true;
forwardingKeyChain = content.forwarding_curve25519_key_chain;
if (!Array.isArray(forwardingKeyChain)) {
forwardingKeyChain = [];
}
forwardingKeyChain = content.forwarding_curve25519_key_chain ?? [];

// copy content before we modify it
forwardingKeyChain = forwardingKeyChain.slice();
forwardingKeyChain.push(senderKey);

senderKey = content.sender_key;
if (!senderKey) {
if (!content.sender_key) {
logger.error("forwarded_room_key event is missing sender_key field");
return;
} else {
senderKey = content.sender_key;
}

const ed25519Key = content.sender_claimed_ed25519_key;
Expand All @@ -1481,34 +1481,34 @@ class MegolmDecryption extends DecryptionAlgorithm {
if (content["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true;
}
return this.olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed,
exportFormat, extraSessionData,
).then(() => {

try {
await this.olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, content.session_id,
content.session_key, keysClaimed,
exportFormat, extraSessionData,
);

// have another go at decrypting events sent with this session.
this.retryDecryption(senderKey, sessionId)
.then((success) => {
// cancel any outstanding room key requests for this session.
// Only do this if we managed to decrypt every message in the
// session, because if we didn't, we leave the other key
// requests in the hopes that someone sends us a key that
// includes an earlier index.
if (success) {
this.crypto.cancelRoomKeyRequest({
algorithm: content.algorithm,
room_id: content.room_id,
session_id: content.session_id,
sender_key: senderKey,
});
}
if (await this.retryDecryption(senderKey, content.session_id)) {
// cancel any outstanding room key requests for this session.
// Only do this if we managed to decrypt every message in the
// session, because if we didn't, we leave the other key
// requests in the hopes that someone sends us a key that
// includes an earlier index.
this.crypto.cancelRoomKeyRequest({
algorithm: content.algorithm,
room_id: content.room_id,
session_id: content.session_id,
sender_key: senderKey,
});
}).then(() => {
}

// don't wait for the keys to be backed up for the server
this.crypto.backupManager.backupGroupSession(senderKey, content.session_id);
}).catch((e) => {
await this.crypto.backupManager.backupGroupSession(senderKey, content.session_id);
} catch (e) {
logger.error(`Error handling m.room_key_event: ${e}`);
});
}
}

/**
Expand Down Expand Up @@ -1701,7 +1701,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
* @param {boolean} [opts.untrusted] whether the key should be considered as untrusted
* @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 = {};
if (opts.untrusted || session.untrusted) {
extraSessionData.untrusted = true;
Expand Down
4 changes: 2 additions & 2 deletions src/crypto/algorithms/olm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ interface IMessage {
*/
class OlmEncryption extends EncryptionAlgorithm {
private sessionPrepared = false;
private prepPromise: Promise<void> = null;
private prepPromise: Promise<void> | null = null;

/**
* @private
Expand Down Expand Up @@ -116,7 +116,7 @@ class OlmEncryption extends EncryptionAlgorithm {
ciphertext: {},
};

const promises = [];
const promises: Promise<void>[] = [];

for (let i = 0; i < users.length; ++i) {
const userId = users[i];
Expand Down
2 changes: 1 addition & 1 deletion src/crypto/olmlib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export interface IOlmSessionResult {
export async function encryptMessageForDevice(
resultsObject: Record<string, string>,
ourUserId: string,
ourDeviceId: string,
ourDeviceId: string | undefined,
olmDevice: OlmDevice,
recipientUserId: string,
recipientDevice: DeviceInfo,
Expand Down