Skip to content

Commit eb058ed

Browse files
authored
Fix spurious "Decryption key withheld" messages (#3061)
When we receive an `m.unavailable` notification, do not show it as "Decryption key withheld".
1 parent 4847d78 commit eb058ed

File tree

2 files changed

+150
-60
lines changed

2 files changed

+150
-60
lines changed

spec/integ/megolm-integ.spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1702,4 +1702,79 @@ describe("megolm", () => {
17021702
await Promise.all([sendPromise, megolmMessagePromise, aliceTestClient.httpBackend.flush("/keys/query", 1)]);
17031703
});
17041704
});
1705+
1706+
describe("m.room_key.withheld handling", () => {
1707+
// TODO: there are a bunch more tests for this sort of thing in spec/unit/crypto/algorithms/megolm.spec.ts.
1708+
// They should be converted to integ tests and moved.
1709+
1710+
it("does not block decryption on an 'm.unavailable' report", async function () {
1711+
await aliceTestClient.start();
1712+
1713+
// there may be a key downloads for alice
1714+
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {});
1715+
aliceTestClient.httpBackend.flush("/keys/query", 1, 5000);
1716+
1717+
// encrypt a message with a group session.
1718+
const groupSession = new Olm.OutboundGroupSession();
1719+
groupSession.create();
1720+
const messageEncryptedEvent = encryptMegolmEvent({
1721+
senderKey: testSenderKey,
1722+
groupSession: groupSession,
1723+
room_id: ROOM_ID,
1724+
});
1725+
1726+
// Alice gets the room message, but not the key
1727+
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
1728+
next_batch: 1,
1729+
rooms: {
1730+
join: { [ROOM_ID]: { timeline: { events: [messageEncryptedEvent] } } },
1731+
},
1732+
});
1733+
await aliceTestClient.flushSync();
1734+
1735+
// alice will (eventually) send a room-key request
1736+
aliceTestClient.httpBackend.when("PUT", "/sendToDevice/m.room_key_request/").respond(200, {});
1737+
await aliceTestClient.httpBackend.flush("/sendToDevice/m.room_key_request/", 1, 1000);
1738+
1739+
// at this point, the message should be a decryption failure
1740+
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
1741+
const event = room.getLiveTimeline().getEvents()[0];
1742+
expect(event.isDecryptionFailure()).toBeTruthy();
1743+
1744+
// we want to wait for the message to be updated, so create a promise for it
1745+
const retryPromise = new Promise((resolve) => {
1746+
event.once(MatrixEventEvent.Decrypted, (ev) => {
1747+
resolve(ev);
1748+
});
1749+
});
1750+
1751+
// alice gets back a room-key-withheld notification
1752+
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
1753+
next_batch: 2,
1754+
to_device: {
1755+
events: [
1756+
{
1757+
type: "m.room_key.withheld",
1758+
sender: "@bob:example.com",
1759+
content: {
1760+
algorithm: "m.megolm.v1.aes-sha2",
1761+
room_id: ROOM_ID,
1762+
session_id: groupSession.session_id(),
1763+
sender_key: testSenderKey,
1764+
code: "m.unavailable",
1765+
reason: "",
1766+
},
1767+
},
1768+
],
1769+
},
1770+
});
1771+
await aliceTestClient.flushSync();
1772+
1773+
// the withheld notification should trigger a retry; wait for it
1774+
await retryPromise;
1775+
1776+
// finally: the message should still be a regular decryption failure, not a withheld notification.
1777+
expect(event.getContent().body).not.toContain("withheld");
1778+
});
1779+
});
17051780
});

src/crypto/algorithms/megolm.ts

Lines changed: 75 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,76 +1613,91 @@ export class MegolmDecryption extends DecryptionAlgorithm {
16131613
const senderKey = content.sender_key;
16141614

16151615
if (content.code === "m.no_olm") {
1616-
const sender = event.getSender()!;
1617-
this.prefixedLogger.warn(`${sender}:${senderKey} was unable to establish an olm session with us`);
1618-
// if the sender says that they haven't been able to establish an olm
1619-
// session, let's proactively establish one
1616+
await this.onNoOlmWithheldEvent(event);
1617+
} else if (content.code === "m.unavailable") {
1618+
// this simply means that the other device didn't have the key, which isn't very useful information. Don't
1619+
// record it in the storage
1620+
} else {
1621+
await this.olmDevice.addInboundGroupSessionWithheld(
1622+
content.room_id,
1623+
senderKey,
1624+
content.session_id,
1625+
content.code,
1626+
content.reason,
1627+
);
1628+
}
16201629

1621-
// Note: after we record that the olm session has had a problem, we
1622-
// trigger retrying decryption for all the messages from the sender's
1630+
// Having recorded the problem, retry decryption on any affected messages.
1631+
// It's unlikely we'll be able to decrypt sucessfully now, but this will
1632+
// update the error message.
1633+
//
1634+
if (content.session_id) {
1635+
await this.retryDecryption(senderKey, content.session_id);
1636+
} else {
1637+
// no_olm messages aren't specific to a given megolm session, so
1638+
// we trigger retrying decryption for all the messages from the sender's
16231639
// key, so that we can update the error message to indicate the olm
16241640
// session problem.
1641+
await this.retryDecryptionFromSender(senderKey);
1642+
}
1643+
}
16251644

1626-
if (await this.olmDevice.getSessionIdForDevice(senderKey)) {
1627-
// a session has already been established, so we don't need to
1628-
// create a new one.
1629-
this.prefixedLogger.debug("New session already created. Not creating a new one.");
1630-
await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true);
1631-
this.retryDecryptionFromSender(senderKey);
1632-
return;
1633-
}
1634-
let device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey);
1645+
private async onNoOlmWithheldEvent(event: MatrixEvent): Promise<void> {
1646+
const content = event.getContent();
1647+
const senderKey = content.sender_key;
1648+
const sender = event.getSender()!;
1649+
this.prefixedLogger.warn(`${sender}:${senderKey} was unable to establish an olm session with us`);
1650+
// if the sender says that they haven't been able to establish an olm
1651+
// session, let's proactively establish one
1652+
1653+
if (await this.olmDevice.getSessionIdForDevice(senderKey)) {
1654+
// a session has already been established, so we don't need to
1655+
// create a new one.
1656+
this.prefixedLogger.debug("New session already created. Not creating a new one.");
1657+
await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true);
1658+
return;
1659+
}
1660+
let device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey);
1661+
if (!device) {
1662+
// if we don't know about the device, fetch the user's devices again
1663+
// and retry before giving up
1664+
await this.crypto.downloadKeys([sender], false);
1665+
device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey);
16351666
if (!device) {
1636-
// if we don't know about the device, fetch the user's devices again
1637-
// and retry before giving up
1638-
await this.crypto.downloadKeys([sender], false);
1639-
device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey);
1640-
if (!device) {
1641-
this.prefixedLogger.info(
1642-
"Couldn't find device for identity key " + senderKey + ": not establishing session",
1643-
);
1644-
await this.olmDevice.recordSessionProblem(senderKey, "no_olm", false);
1645-
this.retryDecryptionFromSender(senderKey);
1646-
return;
1647-
}
1667+
this.prefixedLogger.info(
1668+
"Couldn't find device for identity key " + senderKey + ": not establishing session",
1669+
);
1670+
await this.olmDevice.recordSessionProblem(senderKey, "no_olm", false);
1671+
return;
16481672
}
1673+
}
16491674

1650-
// XXX: switch this to use encryptAndSendToDevices() rather than duplicating it?
1675+
// XXX: switch this to use encryptAndSendToDevices() rather than duplicating it?
16511676

1652-
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, { [sender]: [device] }, false);
1653-
const encryptedContent: IEncryptedContent = {
1654-
algorithm: olmlib.OLM_ALGORITHM,
1655-
sender_key: this.olmDevice.deviceCurve25519Key!,
1656-
ciphertext: {},
1657-
[ToDeviceMessageId]: uuidv4(),
1658-
};
1659-
await olmlib.encryptMessageForDevice(
1660-
encryptedContent.ciphertext,
1661-
this.userId,
1662-
undefined,
1663-
this.olmDevice,
1664-
sender,
1665-
device,
1666-
{ type: "m.dummy" },
1667-
);
1677+
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, { [sender]: [device] }, false);
1678+
const encryptedContent: IEncryptedContent = {
1679+
algorithm: olmlib.OLM_ALGORITHM,
1680+
sender_key: this.olmDevice.deviceCurve25519Key!,
1681+
ciphertext: {},
1682+
[ToDeviceMessageId]: uuidv4(),
1683+
};
1684+
await olmlib.encryptMessageForDevice(
1685+
encryptedContent.ciphertext,
1686+
this.userId,
1687+
undefined,
1688+
this.olmDevice,
1689+
sender,
1690+
device,
1691+
{ type: "m.dummy" },
1692+
);
16681693

1669-
await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true);
1670-
this.retryDecryptionFromSender(senderKey);
1694+
await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true);
16711695

1672-
await this.baseApis.sendToDevice("m.room.encrypted", {
1673-
[sender]: {
1674-
[device.deviceId]: encryptedContent,
1675-
},
1676-
});
1677-
} else {
1678-
await this.olmDevice.addInboundGroupSessionWithheld(
1679-
content.room_id,
1680-
senderKey,
1681-
content.session_id,
1682-
content.code,
1683-
content.reason,
1684-
);
1685-
}
1696+
await this.baseApis.sendToDevice("m.room.encrypted", {
1697+
[sender]: {
1698+
[device.deviceId]: encryptedContent,
1699+
},
1700+
});
16861701
}
16871702

16881703
public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise<boolean> {

0 commit comments

Comments
 (0)