1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-07 23:02:56 +03:00

Resolve multiple CVEs

CVE-2022-39249
CVE-2022-39250
CVE-2022-39251
CVE-2022-39236
This commit is contained in:
RiotRobot
2022-09-28 13:55:15 +01:00
parent b64a30f0ad
commit a587d7c360
30 changed files with 1376 additions and 80 deletions

View File

@@ -494,6 +494,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start(); await aliTestClient.start();
await bobTestClient.start(); await bobTestClient.start();
bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient); await firstSync(aliTestClient);
await aliEnablesEncryption(); await aliEnablesEncryption();
await aliSendsFirstMessage(); await aliSendsFirstMessage();
@@ -504,6 +505,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start(); await aliTestClient.start();
await bobTestClient.start(); await bobTestClient.start();
bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient); await firstSync(aliTestClient);
await aliEnablesEncryption(); await aliEnablesEncryption();
await aliSendsFirstMessage(); await aliSendsFirstMessage();
@@ -567,6 +569,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start(); await aliTestClient.start();
await bobTestClient.start(); await bobTestClient.start();
bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient); await firstSync(aliTestClient);
await aliEnablesEncryption(); await aliEnablesEncryption();
await aliSendsFirstMessage(); await aliSendsFirstMessage();
@@ -584,6 +587,9 @@ describe("MatrixClient crypto", () => {
await firstSync(bobTestClient); await firstSync(bobTestClient);
await aliEnablesEncryption(); await aliEnablesEncryption();
await aliSendsFirstMessage(); await aliSendsFirstMessage();
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {},
);
await bobRecvMessage(); await bobRecvMessage();
await bobEnablesEncryption(); await bobEnablesEncryption();
const ciphertext = await bobSendsReplyMessage(); const ciphertext = await bobSendsReplyMessage();

View File

@@ -87,6 +87,8 @@ describe("MatrixClient syncing", () => {
}); });
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => { it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
await client.initCrypto();
const roomId = "!cycles:example.org"; const roomId = "!cycles:example.org";
// First sync: an invite // First sync: an invite

View File

@@ -29,8 +29,11 @@ import {
IDownloadKeyResult, IDownloadKeyResult,
MatrixEvent, MatrixEvent,
MatrixEventEvent, MatrixEventEvent,
IndexedDBCryptoStore,
Room,
} from "../../src/matrix"; } from "../../src/matrix";
import { IDeviceKeys } from "../../src/crypto/dehydration"; import { IDeviceKeys } from "../../src/crypto/dehydration";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
const ROOM_ID = "!room:id"; const ROOM_ID = "!room:id";
@@ -280,10 +283,13 @@ describe("megolm", () => {
it("Alice receives a megolm message", async () => { it("Alice receives a megolm message", async () => {
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event // make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({ const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -326,10 +332,13 @@ describe("megolm", () => {
it("Alice receives a megolm message before the session keys", async () => { it("Alice receives a megolm message before the session keys", async () => {
// https://github.com/vector-im/element-web/issues/2273 // https://github.com/vector-im/element-web/issues/2273
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event, but don't send it yet // make the room_key event, but don't send it yet
const roomKeyEncrypted = encryptGroupSessionKey({ const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -383,10 +392,13 @@ describe("megolm", () => {
it("Alice gets a second room_key message", async () => { it("Alice gets a second room_key message", async () => {
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event // make the room_key event
const roomKeyEncrypted1 = encryptGroupSessionKey({ const roomKeyEncrypted1 = encryptGroupSessionKey({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -468,6 +480,9 @@ describe("megolm", () => {
aliceTestClient.httpBackend.when('POST', '/keys/query').respond( aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'), 200, getTestKeysQueryResponse('@bob:xyz'),
); );
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
await Promise.all([ await Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => { aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
@@ -541,13 +556,16 @@ describe("megolm", () => {
logger.log('Forcing alice to download our device keys'); logger.log('Forcing alice to download our device keys');
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
aliceTestClient.httpBackend.when('POST', '/keys/query').respond( aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'), 200, getTestKeysQueryResponse('@bob:xyz'),
); );
await Promise.all([ await Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']), aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1), aliceTestClient.httpBackend.flush('/keys/query', 2),
]); ]);
logger.log('Telling alice to block our device'); logger.log('Telling alice to block our device');
@@ -592,6 +610,9 @@ describe("megolm", () => {
logger.log("Fetching bob's devices and marking known"); logger.log("Fetching bob's devices and marking known");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
aliceTestClient.httpBackend.when('POST', '/keys/query').respond( aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'), 200, getTestKeysQueryResponse('@bob:xyz'),
); );
@@ -786,6 +807,10 @@ describe("megolm", () => {
logger.log('Forcing alice to download our device keys'); logger.log('Forcing alice to download our device keys');
const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']); const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
// so will this. // so will this.
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test') const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
.then(() => { .then(() => {
@@ -805,9 +830,12 @@ describe("megolm", () => {
it("Alice exports megolm keys and imports them to a new device", async () => { it("Alice exports megolm keys and imports them to a new device", async () => {
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} }); aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
// establish an olm session with alice // establish an olm session with alice
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
@@ -855,6 +883,8 @@ describe("megolm", () => {
await aliceTestClient.client.importRoomKeys(exported); await aliceTestClient.client.importRoomKeys(exported);
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
const syncResponse = { const syncResponse = {
next_batch: 1, next_batch: 1,
rooms: { rooms: {
@@ -927,10 +957,13 @@ describe("megolm", () => {
it("Alice can decrypt a message with falsey content", async () => { it("Alice can decrypt a message with falsey content", async () => {
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event // make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({ const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -985,10 +1018,13 @@ describe("megolm", () => {
"should successfully decrypt bundled redaction events that don't include a room_id in their /sync data", "should successfully decrypt bundled redaction events that don't include a room_id in their /sync data",
async () => { async () => {
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event // make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({ const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -1045,4 +1081,283 @@ describe("megolm", () => {
expect(redactionEvent.content.reason).toEqual("redaction test"); expect(redactionEvent.content.reason).toEqual("redaction test");
}, },
); );
it("Alice receives shared history before being invited to a room by the sharer", async () => {
const beccaTestClient = new TestClient(
"@becca:localhost", "foobar", "bazquux",
);
await beccaTestClient.client.initCrypto();
await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
await beccaTestClient.start();
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
beccaTestClient.client.store.storeRoom(beccaRoom);
await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
const event = new MatrixEvent({
type: "m.room.message",
sender: "@becca:localhost",
room_id: ROOM_ID,
event_id: "$1",
content: {
msgtype: "m.text",
body: "test message",
},
});
await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
event.claimedEd25519Key = null;
const device = new DeviceInfo(beccaTestClient.client.deviceId);
aliceTestClient.client.crypto.deviceList.getDeviceByIdentityKey = () => device;
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId();
// Create an olm session for Becca and Alice's devices
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
const aliceOtkId = Object.keys(aliceOtks)[0];
const aliceOtk = aliceOtks[aliceOtkId];
const p2pSession = new global.Olm.Session();
await beccaTestClient.client.crypto.cryptoStore.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => {
const account = new global.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount);
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
} finally {
account.free();
}
});
},
);
const content = event.getWireContent();
const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey(
ROOM_ID,
content.sender_key,
content.session_id,
);
const encryptedForwardedKey = encryptOlmEvent({
sender: "@becca:localhost",
senderKey: beccaTestClient.getDeviceKey(),
recipient: aliceTestClient,
p2pSession: p2pSession,
plaincontent: {
"algorithm": 'm.megolm.v1.aes-sha2',
"room_id": ROOM_ID,
"sender_key": content.sender_key,
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
"session_id": content.session_id,
"session_key": groupSessionKey.key,
"chain_index": groupSessionKey.chain_index,
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": true,
},
plaintype: 'm.forwarded_room_key',
});
// Alice receives shared history
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 1,
to_device: { events: [encryptedForwardedKey] },
});
await aliceTestClient.flushSync();
// Alice is invited to the room by Becca
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 2,
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
{
sender: '@becca:localhost',
type: 'm.room.encryption',
state_key: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
},
{
sender: '@becca:localhost',
type: 'm.room.member',
state_key: '@alice:localhost',
content: {
membership: 'invite',
},
},
] } } } },
});
await aliceTestClient.flushSync();
// Alice has joined the room
aliceTestClient.httpBackend.when("GET", "/sync").respond(
200, getSyncResponse(["@alice:localhost", "@becca:localhost"]),
);
await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 4,
rooms: {
join: {
[ROOM_ID]: { timeline: { events: [event.event] } },
},
},
});
await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID);
const roomEvent = room.getLiveTimeline().getEvents()[0];
expect(roomEvent.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
expect(decryptedEvent.getContent().body).toEqual('test message');
await beccaTestClient.stop();
});
it("Alice receives shared history before being invited to a room by someone else", async () => {
const beccaTestClient = new TestClient(
"@becca:localhost", "foobar", "bazquux",
);
await beccaTestClient.client.initCrypto();
await aliceTestClient.start();
await beccaTestClient.start();
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
beccaTestClient.client.store.storeRoom(beccaRoom);
await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
const event = new MatrixEvent({
type: "m.room.message",
sender: "@becca:localhost",
room_id: ROOM_ID,
event_id: "$1",
content: {
msgtype: "m.text",
body: "test message",
},
});
await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
event.claimedEd25519Key = null;
const device = new DeviceInfo(beccaTestClient.client.deviceId);
aliceTestClient.client.crypto.deviceList.getDeviceByIdentityKey = () => device;
// Create an olm session for Becca and Alice's devices
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
const aliceOtkId = Object.keys(aliceOtks)[0];
const aliceOtk = aliceOtks[aliceOtkId];
const p2pSession = new global.Olm.Session();
await beccaTestClient.client.crypto.cryptoStore.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => {
const account = new global.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount);
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
} finally {
account.free();
}
});
},
);
const content = event.getWireContent();
const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey(
ROOM_ID,
content.sender_key,
content.session_id,
);
const encryptedForwardedKey = encryptOlmEvent({
sender: "@becca:localhost",
senderKey: beccaTestClient.getDeviceKey(),
recipient: aliceTestClient,
p2pSession: p2pSession,
plaincontent: {
"algorithm": 'm.megolm.v1.aes-sha2',
"room_id": ROOM_ID,
"sender_key": content.sender_key,
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
"session_id": content.session_id,
"session_key": groupSessionKey.key,
"chain_index": groupSessionKey.chain_index,
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": true,
},
plaintype: 'm.forwarded_room_key',
});
// Alice receives forwarded history from Becca
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 1,
to_device: { events: [encryptedForwardedKey] },
});
await aliceTestClient.flushSync();
// Alice is invited to the room by Charlie
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 2,
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
{
sender: '@becca:localhost',
type: 'm.room.encryption',
state_key: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
},
{
sender: '@charlie:localhost',
type: 'm.room.member',
state_key: '@alice:localhost',
content: {
membership: 'invite',
},
},
] } } } },
});
await aliceTestClient.flushSync();
// Alice has joined the room
aliceTestClient.httpBackend.when("GET", "/sync").respond(
200, getSyncResponse(["@alice:localhost", "@becca:localhost", "@charlie:localhost"]),
);
await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 4,
rooms: {
join: {
[ROOM_ID]: { timeline: { events: [event.event] } },
},
},
});
await aliceTestClient.flushSync();
// Decryption should fail, because Alice hasn't received any keys she can trust
const room = aliceTestClient.client.getRoom(ROOM_ID);
const roomEvent = room.getLiveTimeline().getEvents()[0];
expect(roomEvent.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
expect(decryptedEvent.isDecryptionFailure()).toBe(true);
await beccaTestClient.stop();
});
}); });

View File

@@ -22,6 +22,7 @@ import {
makeBeaconContent, makeBeaconContent,
makeBeaconInfoContent, makeBeaconInfoContent,
makeTopicContent, makeTopicContent,
parseBeaconContent,
parseTopicContent, parseTopicContent,
} from "../../src/content-helpers"; } from "../../src/content-helpers";
@@ -127,6 +128,66 @@ describe('Beacon content helpers', () => {
}); });
}); });
}); });
describe("parseBeaconContent()", () => {
it("should not explode when parsing an invalid beacon", () => {
// deliberate cast to simulate wire content being invalid
const result = parseBeaconContent({} as any);
expect(result).toEqual({
description: undefined,
uri: undefined,
timestamp: undefined,
});
});
it("should parse unstable values", () => {
const uri = "urigoeshere";
const description = "descriptiongoeshere";
const timestamp = 1234;
const result = parseBeaconContent({
"org.matrix.msc3488.location": {
uri,
description,
},
"org.matrix.msc3488.ts": timestamp,
// relationship not used - just here to satisfy types
"m.relates_to": {
rel_type: "m.reference",
event_id: "$unused",
},
});
expect(result).toEqual({
description,
uri,
timestamp,
});
});
it("should parse stable values", () => {
const uri = "urigoeshere";
const description = "descriptiongoeshere";
const timestamp = 1234;
const result = parseBeaconContent({
"m.location": {
uri,
description,
},
"m.ts": timestamp,
// relationship not used - just here to satisfy types
"m.relates_to": {
rel_type: "m.reference",
event_id: "$unused",
},
});
expect(result).toEqual({
description,
uri,
timestamp,
});
});
});
}); });
describe('Topic content helpers', () => { describe('Topic content helpers', () => {

View File

@@ -15,6 +15,8 @@ import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo"; import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from '../../src/logger'; import { logger } from '../../src/logger';
import { MemoryStore } from "../../src"; import { MemoryStore } from "../../src";
import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager';
import { RoomMember } from '../../src/models/room-member';
import { IStore } from '../../src/store'; import { IStore } from '../../src/store';
const Olm = global.Olm; const Olm = global.Olm;
@@ -40,20 +42,52 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
type: "m.forwarded_room_key", type: "m.forwarded_room_key",
sender: client.getUserId(), sender: client.getUserId(),
content: { content: {
algorithm: olmlib.MEGOLM_ALGORITHM, "algorithm": olmlib.MEGOLM_ALGORITHM,
room_id: roomId, "room_id": roomId,
sender_key: eventContent.sender_key, "sender_key": eventContent.sender_key,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
session_id: eventContent.session_id, "session_id": eventContent.session_id,
session_key: key.key, "session_key": key.key,
chain_index: key.chain_index, "chain_index": key.chain_index,
forwarding_curve25519_key_chain: "forwarding_curve25519_key_chain": key.forwarding_curve_key_chain,
key.forwarding_curve_key_chain, "org.matrix.msc3061.shared_history": true,
}, },
}); });
// make onRoomKeyEvent think this was an encrypted event // make onRoomKeyEvent think this was an encrypted event
// @ts-ignore private property // @ts-ignore private property
ksEvent.senderCurve25519Key = "akey"; ksEvent.senderCurve25519Key = "akey";
ksEvent.getWireType = () => "m.room.encrypted";
ksEvent.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
return ksEvent;
}
function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent {
const roomId = event.getRoomId();
const eventContent = event.getWireContent();
const key = client.crypto.olmDevice.getOutboundGroupSessionKey(eventContent.session_id);
const ksEvent = new MatrixEvent({
type: "m.room_key",
sender: client.getUserId(),
content: {
"algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": roomId,
"session_id": eventContent.session_id,
"session_key": key.key,
},
});
// make onRoomKeyEvent think this was an encrypted event
// @ts-ignore private property
ksEvent.senderCurve25519Key = event.getSenderKey();
ksEvent.getWireType = () => "m.room.encrypted";
ksEvent.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
return ksEvent; return ksEvent;
} }
@@ -95,7 +129,7 @@ describe("Crypto", function() {
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI'; event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };}; event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
event.getForwardingCurve25519KeyChain = () => ["not empty"]; event.getForwardingCurve25519KeyChain = () => ["not empty"];
event.isKeySourceUntrusted = () => false; event.isKeySourceUntrusted = () => true;
event.getClaimedEd25519Key = event.getClaimedEd25519Key =
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; () => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
@@ -233,6 +267,7 @@ describe("Crypto", function() {
describe('Key requests', function() { describe('Key requests', function() {
let aliceClient: MatrixClient; let aliceClient: MatrixClient;
let bobClient: MatrixClient; let bobClient: MatrixClient;
let claraClient: MatrixClient;
beforeEach(async function() { beforeEach(async function() {
aliceClient = (new TestClient( aliceClient = (new TestClient(
@@ -241,22 +276,35 @@ describe("Crypto", function() {
bobClient = (new TestClient( bobClient = (new TestClient(
"@bob:example.com", "bobdevice", "@bob:example.com", "bobdevice",
)).client; )).client;
claraClient = (new TestClient(
"@clara:example.com", "claradevice",
)).client;
await aliceClient.initCrypto(); await aliceClient.initCrypto();
await bobClient.initCrypto(); await bobClient.initCrypto();
await claraClient.initCrypto();
}); });
afterEach(async function() { afterEach(async function() {
aliceClient.stopClient(); aliceClient.stopClient();
bobClient.stopClient(); bobClient.stopClient();
claraClient.stopClient();
}); });
it("does not cancel keyshare requests if some messages are not decrypted", async function() { it("does not cancel keyshare requests until all messages are decrypted with trusted keys", async function() {
const encryptionCfg = { const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
}; };
const roomId = "!someroom"; const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {}); const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
// Make Bob invited by Alice so Bob will accept Alice's forwarded keys
bobRoom.currentState.setStateEvents([new MatrixEvent({
type: "m.room.member",
sender: "@alice:example.com",
room_id: roomId,
content: { membership: "invite" },
state_key: "@bob:example.com",
})]);
aliceClient.store.storeRoom(aliceRoom); aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom); bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg); await aliceClient.setRoomEncryption(roomId, encryptionCfg);
@@ -302,6 +350,9 @@ describe("Crypto", function() {
} }
})); }));
const device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
const bobDecryptor = bobClient.crypto.getRoomDecryptor( const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
@@ -314,6 +365,8 @@ describe("Crypto", function() {
// 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(aliceClient, events[1], 1); let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
bobClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
await decryptEventsPromise; await decryptEventsPromise;
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted"); expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
@@ -340,8 +393,24 @@ describe("Crypto", function() {
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
await decryptEventPromise; await decryptEventPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted"); expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[0].isKeySourceUntrusted()).toBeTruthy();
await sleep(1); await sleep(1);
// the room key request should be gone since we've now decrypted everything // the room key request should still be there, since we've
// decrypted everything with an untrusted key
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined();
// Now share a trusted room key event so Bob will re-decrypt the messages.
// Bob will backfill trust when they receive a trusted session with a higher
// index that connects to an untrusted session with a lower index.
const roomKeyEvent = roomKeyEventForEvent(aliceClient, events[1]);
const trustedDecryptEventPromise = awaitEvent(events[0], "Event.decrypted");
await bobDecryptor.onRoomKeyEvent(roomKeyEvent);
await trustedDecryptEventPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[0].isKeySourceUntrusted()).toBeFalsy();
await sleep(1);
// now the room key request should be gone, since there's
// no better key to wait for
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy(); expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
}); });
@@ -383,6 +452,9 @@ describe("Crypto", function() {
// decryption keys yet // decryption keys yet
} }
const device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
const bobDecryptor = bobClient.crypto.getRoomDecryptor( const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
@@ -462,6 +534,420 @@ describe("Crypto", function() {
expect(aliceSendToDevice).toBeCalledTimes(3); expect(aliceSendToDevice).toBeCalledTimes(3);
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId); expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
}); });
it("should accept forwarded keys which it requested", 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 events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// 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);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
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 device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const cryptoStore = bobClient.crypto.cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: senderKey,
session_id: sessionId,
};
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
expect(outgoingReq).toBeDefined();
await cryptoStore.updateOutgoingRoomKeyRequest(
outgoingReq.requestId, RoomKeyRequestState.Unsent,
{ state: RoomKeyRequestState.Sent },
);
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const decryptEventsPromise = Promise.all(events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
);
expect(key).not.toBeNull();
await decryptEventsPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
});
it("should accept forwarded keys from the user who invited it to the room", 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", {});
const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
// Make Bob invited by Clara
bobRoom.currentState.setStateEvents([new MatrixEvent({
type: "m.room.member",
sender: "@clara:example.com",
room_id: roomId,
content: { membership: "invite" },
state_key: "@bob:example.com",
})]);
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
claraClient.store.storeRoom(claraRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
await claraClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// 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);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
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 device = new DeviceInfo(claraClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@clara:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const decryptEventsPromise = Promise.all(events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = claraClient.getUserId(),
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
);
expect(key).not.toBeNull();
await decryptEventsPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
});
it("should accept forwarded keys from one of its own user's other devices", 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 events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// 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);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
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 device = new DeviceInfo(claraClient.deviceId);
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const decryptEventsPromise = Promise.all(events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = bobClient.getUserId(),
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId());
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
);
expect(key).not.toBeNull();
await decryptEventsPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
});
it("should not accept unexpected forwarded keys for a room it's in", 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", {});
const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
claraClient.store.storeRoom(claraRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
await claraClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// 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);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
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 device = new DeviceInfo(claraClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = claraClient.getUserId(),
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
);
expect(key).toBeNull();
});
it("should park forwarded keys for a room it's not in", async function() {
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// 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);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
event.claimedEd25519Key = null;
}));
const device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const content = events[0].getWireContent();
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const bobKey = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
content.sender_key,
content.session_id,
);
expect(bobKey).toBeNull();
const aliceKey = await aliceClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
content.sender_key,
content.session_id,
);
const parked = await bobClient.crypto.cryptoStore.takeParkedSharedHistory(roomId);
expect(parked).toEqual([{
senderId: aliceClient.getUserId(),
senderKey: content.sender_key,
sessionId: content.session_id,
sessionKey: aliceKey.key,
keysClaimed: { ed25519: aliceKey.sender_claimed_ed25519_key },
forwardingCurve25519KeyChain: ["akey"],
}]);
});
}); });
describe('Secret storage', function() { describe('Secret storage', function() {

View File

@@ -110,6 +110,12 @@ describe("MegolmDecryption", function() {
senderCurve25519Key: "SENDER_CURVE25519", senderCurve25519Key: "SENDER_CURVE25519",
claimedEd25519Key: "SENDER_ED25519", claimedEd25519Key: "SENDER_ED25519",
}; };
event.getWireType = () => "m.room.encrypted";
event.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
const mockCrypto = { const mockCrypto = {
decryptEvent: function() { decryptEvent: function() {

View File

@@ -214,6 +214,12 @@ describe("MegolmBackup", function() {
const event = new MatrixEvent({ const event = new MatrixEvent({
type: 'm.room.encrypted', type: 'm.room.encrypted',
}); });
event.getWireType = () => "m.room.encrypted";
event.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
const decryptedData = { const decryptedData = {
clearEvent: { clearEvent: {
type: 'm.room_key', type: 'm.room_key',

View File

@@ -26,6 +26,7 @@ import { logger } from '../../../src/logger';
import * as utils from "../../../src/utils"; import * as utils from "../../../src/utils";
import { ICreateClientOpts } from '../../../src/client'; import { ICreateClientOpts } from '../../../src/client';
import { ISecretStorageKeyInfo } from '../../../src/crypto/api'; import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -257,6 +258,7 @@ describe("Secrets", function() {
"ed25519:VAX": vaxDevice.deviceEd25519Key, "ed25519:VAX": vaxDevice.deviceEd25519Key,
"curve25519:VAX": vaxDevice.deviceCurve25519Key, "curve25519:VAX": vaxDevice.deviceCurve25519Key,
}, },
verified: DeviceInfo.DeviceVerification.VERIFIED,
}, },
}); });
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
@@ -280,10 +282,12 @@ describe("Secrets", function() {
Object.values(otks)[0], Object.values(otks)[0],
); );
const request = await secretStorage.request("foo", ["VAX"]); osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
const secret = await request.promise; osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const request = await secretStorage.request("foo", ["VAX"]);
await request.promise; // return value not used
expect(secret).toBe("bar");
osborne2.stop(); osborne2.stop();
vax.stop(); vax.stop();
clearTestClientTimeouts(); clearTestClientTimeouts();

View File

@@ -464,7 +464,7 @@ describe("SAS verification", function() {
}, },
); );
alice.client.setDeviceVerified = jest.fn(); alice.client.crypto.setDeviceVerification = jest.fn();
alice.client.getDeviceEd25519Key = () => { alice.client.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key"; return "alice+base64+ed25519+key";
}; };
@@ -482,7 +482,7 @@ describe("SAS verification", function() {
return Promise.resolve(); return Promise.resolve();
}; };
bob.client.setDeviceVerified = jest.fn(); bob.client.crypto.setDeviceVerification = jest.fn();
bob.client.getStoredDevice = () => { bob.client.getStoredDevice = () => {
return DeviceInfo.fromStorage( return DeviceInfo.fromStorage(
{ {
@@ -565,10 +565,24 @@ describe("SAS verification", function() {
]); ]);
// make sure Alice and Bob verified each other // make sure Alice and Bob verified each other
expect(alice.client.setDeviceVerified) expect(alice.client.crypto.setDeviceVerification)
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId); .toHaveBeenCalledWith(
expect(bob.client.setDeviceVerified) bob.client.getUserId(),
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId); bob.client.deviceId,
true,
null,
null,
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
);
expect(bob.client.crypto.setDeviceVerification)
.toHaveBeenCalledWith(
alice.client.getUserId(),
alice.client.deviceId,
true,
null,
null,
{ "ed25519:Osborne2": "alice+base64+ed25519+key" },
);
}); });
}); });
}); });

View File

@@ -22,6 +22,7 @@ import {
BeaconEvent, BeaconEvent,
} from "../../../src/models/beacon"; } from "../../../src/models/beacon";
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon"; import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
import { REFERENCE_RELATION } from "matrix-events-sdk";
jest.useFakeTimers(); jest.useFakeTimers();
@@ -431,6 +432,27 @@ describe('Beacon', () => {
expect(emitSpy).not.toHaveBeenCalled(); expect(emitSpy).not.toHaveBeenCalled();
}); });
it("should ignore invalid beacon events", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit');
const ev = new MatrixEvent({
type: M_BEACON_INFO.name,
sender: userId,
room_id: roomId,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: beacon.beaconInfoId,
},
},
});
beacon.addLocations([ev]);
expect(beacon.latestLocationEvent).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
describe('when beacon is live with a start timestamp is in the future', () => { describe('when beacon is live with a start timestamp is in the future', () => {
it('ignores locations before the beacon start timestamp', () => { it('ignores locations before the beacon start timestamp', () => {
const startTimestamp = now + 60000; const startTimestamp = now + 60000;

20
src/@types/crypto.ts Normal file
View File

@@ -0,0 +1,20 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export type OlmGroupSessionExtraData = {
untrusted?: boolean;
sharedHistory?: boolean;
};

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2021 The Matrix.org Foundation C.I.C. Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Optional } from "matrix-events-sdk/lib/types";
/** /**
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix * Represents a simple Matrix namespaced value. This will assume that if a stable prefix
* is provided that the stable prefix should be used when representing the identifier. * is provided that the stable prefix should be used when representing the identifier.
@@ -54,7 +56,7 @@ export class NamespacedValue<S extends string, U extends string> {
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class // this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace. // so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
public findIn<T>(obj: any): T { public findIn<T>(obj: any): Optional<T> {
let val: T; let val: T;
if (this.name) { if (this.name) {
val = obj?.[this.name]; val = obj?.[this.name];

View File

@@ -5287,6 +5287,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {object} [options] * @param {object} [options]
* @param {boolean} options.preventReEmit don't re-emit events emitted on an event mapped by this mapper on the client * @param {boolean} options.preventReEmit don't re-emit events emitted on an event mapped by this mapper on the client
* @param {boolean} options.decrypt decrypt event proactively * @param {boolean} options.decrypt decrypt event proactively
* @param {boolean} options.toDevice the event is a to_device event
* @return {Function} * @return {Function}
*/ */
public getEventMapper(options?: MapperOpts): EventMapper { public getEventMapper(options?: MapperOpts): EventMapper {

View File

@@ -292,16 +292,17 @@ export const makeBeaconContent: MakeBeaconContent = (
}); });
export type BeaconLocationState = MLocationContent & { export type BeaconLocationState = MLocationContent & {
timestamp: number; uri?: string; // override from MLocationContent to allow optionals
timestamp?: number;
}; };
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => { export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
const { description, uri } = M_LOCATION.findIn<MLocationContent>(content); const location = M_LOCATION.findIn<MLocationContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content); const timestamp = M_TIMESTAMP.findIn<number>(content);
return { return {
description, description: location?.description,
uri, uri: location?.uri,
timestamp, timestamp,
}; };
}; };

View File

@@ -23,6 +23,7 @@ import * as algorithms from './algorithms';
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base"; import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm"; import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
import { IMegolmSessionData } from "./index"; import { IMegolmSessionData } from "./index";
import { OlmGroupSessionExtraData } from "../@types/crypto";
// The maximum size of an event is 65K, and we base64 the content, so this is a // The maximum size of an event is 65K, and we base64 the content, so this is a
// reasonable approximation to the biggest plaintext we can encrypt. // reasonable approximation to the biggest plaintext we can encrypt.
@@ -122,6 +123,7 @@ interface IInboundGroupSessionKey {
forwarding_curve25519_key_chain: string[]; forwarding_curve25519_key_chain: string[];
sender_claimed_ed25519_key: string; sender_claimed_ed25519_key: string;
shared_history: boolean; shared_history: boolean;
untrusted: boolean;
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
@@ -1101,7 +1103,7 @@ export class OlmDevice {
sessionKey: string, sessionKey: string,
keysClaimed: Record<string, string>, keysClaimed: Record<string, string>,
exportFormat: boolean, exportFormat: boolean,
extraSessionData: Record<string, any> = {}, extraSessionData: OlmGroupSessionExtraData = {},
): Promise<void> { ): Promise<void> {
await this.cryptoStore.doTxn( await this.cryptoStore.doTxn(
'readwrite', [ 'readwrite', [
@@ -1133,17 +1135,42 @@ export class OlmDevice {
"Update for megolm session " "Update for megolm session "
+ senderKey + "/" + sessionId, + senderKey + "/" + sessionId,
); );
if (existingSession.first_known_index() if (existingSession.first_known_index() <= session.first_known_index()) {
<= session.first_known_index() if (!existingSessionData.untrusted || extraSessionData.untrusted) {
&& !(existingSession.first_known_index() == session.first_known_index() // existing session has less-than-or-equal index
&& !extraSessionData.untrusted // (i.e. can decrypt at least as much), and the
&& existingSessionData.untrusted)) { // new session's trust does not win over the old
// existing session has lower index (i.e. can // session's trust, so keep it
// decrypt more), or they have the same index and logger.log(`Keeping existing megolm session ${sessionId}`);
// the new sessions trust does not win over the old return;
// sessions trust, so keep it }
logger.log(`Keeping existing megolm session ${sessionId}`); if (existingSession.first_known_index() < session.first_known_index()) {
return; // We want to upgrade the existing session's trust,
// but we can't just use the new session because we'll
// lose the lower index. Check that the sessions connect
// properly, and then manually set the existing session
// as trusted.
if (
existingSession.export_session(session.first_known_index())
=== session.export_session(session.first_known_index())
) {
logger.info(
"Upgrading trust of existing megolm session " +
sessionId + " based on newly-received trusted session",
);
existingSessionData.untrusted = false;
this.cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, existingSessionData, txn,
);
} else {
logger.warn(
"Newly-received megolm session " + sessionId +
" does not match existing session! Keeping existing session",
);
}
return;
}
// If the sessions have the same index, go ahead and store the new trusted one.
} }
} }
@@ -1427,13 +1454,23 @@ export class OlmDevice {
const claimedKeys = sessionData.keysClaimed || {}; const claimedKeys = sessionData.keysClaimed || {};
const senderEd25519Key = claimedKeys.ed25519 || null; const senderEd25519Key = claimedKeys.ed25519 || null;
const forwardingKeyChain = sessionData.forwardingCurve25519KeyChain || [];
// older forwarded keys didn't set the "untrusted"
// property, but can be identified by having a
// non-empty forwarding key chain. These keys should
// be marked as untrusted since we don't know that they
// can be trusted
const untrusted = "untrusted" in sessionData
? sessionData.untrusted
: forwardingKeyChain.length > 0;
result = { result = {
"chain_index": chainIndex, "chain_index": chainIndex,
"key": exportedSession, "key": exportedSession,
"forwarding_curve25519_key_chain": "forwarding_curve25519_key_chain": forwardingKeyChain,
sessionData.forwardingCurve25519KeyChain || [],
"sender_claimed_ed25519_key": senderEd25519Key, "sender_claimed_ed25519_key": senderEd25519Key,
"shared_history": sessionData.sharedHistory || false, "shared_history": sessionData.sharedHistory || false,
"untrusted": untrusted,
}; };
}, },
); );

View File

@@ -539,7 +539,23 @@ export class SecretStorage {
// because someone could be trying to send us bogus data // because someone could be trying to send us bogus data
return; return;
} }
if (!olmlib.isOlmEncrypted(event)) {
logger.error("secret event not properly encrypted");
return;
}
const content = event.getContent(); const content = event.getContent();
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
content.sender_key,
);
if (senderKeyUser !== event.getSender()) {
logger.error("sending device does not belong to the user it claims to be from");
return;
}
logger.log("got secret share for request", content.request_id); logger.log("got secret share for request", content.request_id);
const requestControl = this.requests.get(content.request_id); const requestControl = this.requests.get(content.request_id);
if (requestControl) { if (requestControl) {
@@ -559,6 +575,14 @@ export class SecretStorage {
logger.log("unsolicited secret share from device", deviceInfo.deviceId); logger.log("unsolicited secret share from device", deviceInfo.deviceId);
return; return;
} }
// unsure that the sender is trusted. In theory, this check is
// unnecessary since we only accept secret shares from devices that
// we requested from, but it doesn't hurt.
const deviceTrust = this.baseApis.crypto.checkDeviceInfoTrust(event.getSender(), deviceInfo);
if (!deviceTrust.isVerified()) {
logger.log("secret share from unverified device");
return;
}
logger.log( logger.log(
`Successfully received secret ${requestControl.name} ` + `Successfully received secret ${requestControl.name} ` +

View File

@@ -35,8 +35,10 @@ import { Room } from '../../models/room';
import { DeviceInfo } from "../deviceinfo"; import { DeviceInfo } from "../deviceinfo";
import { IOlmSessionResult } from "../olmlib"; import { IOlmSessionResult } from "../olmlib";
import { DeviceInfoMap } from "../DeviceList"; import { DeviceInfoMap } from "../DeviceList";
import { MatrixEvent } from "../.."; import { MatrixEvent } from "../../models/event";
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index"; import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
import { OlmGroupSessionExtraData } from "../../@types/crypto";
// determine whether the key can be shared with invitees // determine whether the key can be shared with invitees
export function isRoomSharedHistory(room: Room): boolean { export function isRoomSharedHistory(room: Room): boolean {
@@ -1189,8 +1191,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
* {@link module:crypto/algorithms/DecryptionAlgorithm} * {@link module:crypto/algorithms/DecryptionAlgorithm}
*/ */
class MegolmDecryption extends DecryptionAlgorithm { class MegolmDecryption extends DecryptionAlgorithm {
// events which we couldn't decrypt due to unknown sessions / indexes: map from // events which we couldn't decrypt due to unknown sessions /
// senderKey|sessionId to Set of MatrixEvents // indexes, or which we could only decrypt with untrusted keys:
// map from senderKey|sessionId to Set of MatrixEvents
private pendingEvents = new Map<string, Map<string, Set<MatrixEvent>>>(); private pendingEvents = new Map<string, Map<string, Set<MatrixEvent>>>();
// this gets stubbed out by the unit tests. // this gets stubbed out by the unit tests.
@@ -1294,9 +1297,13 @@ class MegolmDecryption extends DecryptionAlgorithm {
); );
} }
// success. We can remove the event from the pending list, if that hasn't // Success. We can remove the event from the pending list, if
// already happened. // that hasn't already happened. However, if the event was
this.removeEventFromPendingList(event); // decrypted with an untrusted key, leave it on the pending
// list so it will be retried if we find a trusted key later.
if (!res.untrusted) {
this.removeEventFromPendingList(event);
}
const payload = JSON.parse(res.result); const payload = JSON.parse(res.result);
@@ -1391,6 +1398,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
let exportFormat = false; let exportFormat = false;
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
const extraSessionData: OlmGroupSessionExtraData = {};
if (!content.room_id || if (!content.room_id ||
!content.session_key || !content.session_key ||
!content.session_id || !content.session_id ||
@@ -1400,12 +1409,58 @@ class MegolmDecryption extends DecryptionAlgorithm {
return; return;
} }
if (!senderKey) { if (!olmlib.isOlmEncrypted(event)) {
logger.error("key event has no sender key (not encrypted?)"); logger.error("key event not properly encrypted");
return; return;
} }
if (content["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true;
}
if (event.getType() == "m.forwarded_room_key") { if (event.getType() == "m.forwarded_room_key") {
const deviceInfo = this.crypto.deviceList.getDeviceByIdentityKey(
olmlib.OLM_ALGORITHM,
senderKey,
);
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
senderKey,
);
if (senderKeyUser !== event.getSender()) {
logger.error("sending device does not belong to the user it claims to be from");
return;
}
const outgoingRequests = deviceInfo ? await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(
event.getSender(), deviceInfo.deviceId, [RoomKeyRequestState.Sent],
) : [];
const weRequested = outgoingRequests.some((req) => (
req.requestBody.room_id === content.room_id && req.requestBody.session_id === content.session_id
));
const room = this.baseApis.getRoom(content.room_id);
const memberEvent = room?.getMember(this.userId)?.events.member;
const fromInviter = memberEvent?.getSender() === event.getSender() ||
(memberEvent?.getUnsigned()?.prev_sender === event.getSender() &&
memberEvent?.getPrevContent()?.membership === "invite");
const fromUs = event.getSender() === this.baseApis.getUserId();
if (!weRequested) {
// If someone sends us an unsolicited key and it's not
// shared history, ignore it
if (!extraSessionData.sharedHistory) {
logger.log("forwarded key not shared history - ignoring");
return;
}
// If someone sends us an unsolicited key for a room
// we're already in, and they're not one of our other
// devices or the one who invited us, ignore it
if (room && !fromInviter && !fromUs) {
logger.log("forwarded key not from inviter or from us - ignoring");
return;
}
}
exportFormat = true; exportFormat = true;
forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ? forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ?
content.forwarding_curve25519_key_chain : []; content.forwarding_curve25519_key_chain : [];
@@ -1418,7 +1473,6 @@ class MegolmDecryption extends DecryptionAlgorithm {
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) {
@@ -1431,11 +1485,45 @@ class MegolmDecryption extends DecryptionAlgorithm {
keysClaimed = { keysClaimed = {
ed25519: ed25519Key, ed25519: ed25519Key,
}; };
// If this is a key for a room we're not in, don't load it
// yet, just park it in case *this sender* invites us to
// that room later
if (!room) {
const parkedData = {
senderId: event.getSender(),
senderKey: content.sender_key,
sessionId: content.session_id,
sessionKey: content.session_key,
keysClaimed,
forwardingCurve25519KeyChain: forwardingKeyChain,
};
await this.crypto.cryptoStore.doTxn(
'readwrite',
['parked_shared_history'],
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id, parkedData, txn),
logger.withPrefix("[addParkedSharedHistory]"),
);
return;
}
const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice);
if (fromUs && !deviceTrust.isVerified()) {
return;
}
// forwarded keys are always untrusted
extraSessionData.untrusted = true;
// replace the sender key with the sender key of the session
// creator for storage
senderKey = content.sender_key;
} else { } else {
keysClaimed = event.getKeysClaimed(); keysClaimed = event.getKeysClaimed();
} }
const extraSessionData: any = {};
if (content["org.matrix.msc3061.shared_history"]) { if (content["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true; extraSessionData.sharedHistory = true;
} }
@@ -1453,7 +1541,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
); );
// have another go at decrypting events sent with this session. // have another go at decrypting events sent with this session.
if (await this.retryDecryption(senderKey, content.session_id)) { if (await this.retryDecryption(senderKey, content.session_id, !extraSessionData.untrusted)) {
// 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
@@ -1668,7 +1756,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
session: IMegolmSessionData, session: IMegolmSessionData,
opts: { untrusted?: boolean, source?: string } = {}, opts: { untrusted?: boolean, source?: string } = {},
): Promise<void> { ): Promise<void> {
const extraSessionData: any = {}; const extraSessionData: OlmGroupSessionExtraData = {};
if (opts.untrusted || session.untrusted) { if (opts.untrusted || session.untrusted) {
extraSessionData.untrusted = true; extraSessionData.untrusted = true;
} }
@@ -1696,7 +1784,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
}); });
} }
// have another go at decrypting events sent with this session. // have another go at decrypting events sent with this session.
this.retryDecryption(session.sender_key, session.session_id); this.retryDecryption(session.sender_key, session.session_id, !extraSessionData.untrusted);
}); });
} }
@@ -1707,10 +1795,12 @@ class MegolmDecryption extends DecryptionAlgorithm {
* @private * @private
* @param {String} senderKey * @param {String} senderKey
* @param {String} sessionId * @param {String} sessionId
* @param {Boolean} keyTrusted
* *
* @return {Boolean} whether all messages were successfully decrypted * @return {Boolean} whether all messages were successfully
* decrypted with trusted keys
*/ */
private async retryDecryption(senderKey: string, sessionId: string): Promise<boolean> { private async retryDecryption(senderKey: string, sessionId: string, keyTrusted?: boolean): Promise<boolean> {
const senderPendingEvents = this.pendingEvents.get(senderKey); const senderPendingEvents = this.pendingEvents.get(senderKey);
if (!senderPendingEvents) { if (!senderPendingEvents) {
return true; return true;
@@ -1725,13 +1815,14 @@ class MegolmDecryption extends DecryptionAlgorithm {
await Promise.all([...pending].map(async (ev) => { await Promise.all([...pending].map(async (ev) => {
try { try {
await ev.attemptDecryption(this.crypto, { isRetry: true }); await ev.attemptDecryption(this.crypto, { isRetry: true, keyTrusted });
} catch (e) { } catch (e) {
// don't die if something goes wrong // don't die if something goes wrong
} }
})); }));
// If decrypted successfully, they'll have been removed from pendingEvents // If decrypted successfully with trusted keys, they'll have
// been removed from pendingEvents
return !this.pendingEvents.get(senderKey)?.has(sessionId); return !this.pendingEvents.get(senderKey)?.has(sessionId);
} }

View File

@@ -222,6 +222,26 @@ class OlmDecryption extends DecryptionAlgorithm {
); );
} }
// check that the device that encrypted the event belongs to the user
// that the event claims it's from. We need to make sure that our
// device list is up-to-date. If the device is unknown, we can only
// assume that the device logged out. Some event handlers, such as
// secret sharing, may be more strict and reject events that come from
// unknown devices.
await this.crypto.deviceList.downloadKeys([event.getSender()], false);
const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
deviceKey,
);
if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined) {
throw new DecryptionError(
"OLM_BAD_SENDER",
"Message claimed to be from " + event.getSender(), {
real_sender: senderKeyUser,
},
);
}
// check that the original sender matches what the homeserver told us, to // check that the original sender matches what the homeserver told us, to
// avoid people masquerading as others. // avoid people masquerading as others.
// (this check is also provided via the sender's embedded ed25519 key, // (this check is also provided via the sender's embedded ed25519 key,

View File

@@ -431,7 +431,6 @@ export class BackupManager {
) )
); );
}); });
ret.usable = ret.usable || ret.trusted_locally;
return ret; return ret;
} }

View File

@@ -2105,6 +2105,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @param {?boolean} known whether to mark that the user has been made aware of * @param {?boolean} known whether to mark that the user has been made aware of
* the existence of this device. Null to leave unchanged * the existence of this device. Null to leave unchanged
* *
* @param {?Record<string, any>} keys The list of keys that was present
* during the device verification. This will be double checked with the list
* of keys the given device has currently.
*
* @return {Promise<module:crypto/deviceinfo>} updated DeviceInfo * @return {Promise<module:crypto/deviceinfo>} updated DeviceInfo
*/ */
public async setDeviceVerification( public async setDeviceVerification(
@@ -2113,6 +2117,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
verified?: boolean, verified?: boolean,
blocked?: boolean, blocked?: boolean,
known?: boolean, known?: boolean,
keys?: Record<string, string>,
): Promise<DeviceInfo | CrossSigningInfo> { ): Promise<DeviceInfo | CrossSigningInfo> {
// get rid of any `undefined`s here so we can just check // get rid of any `undefined`s here so we can just check
// for null rather than null or undefined // for null rather than null or undefined
@@ -2131,6 +2136,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
if (!verified) { if (!verified) {
throw new Error("Cannot set a cross-signing key as unverified"); throw new Error("Cannot set a cross-signing key as unverified");
} }
const gotKeyId = keys ? Object.values(keys)[0] : null;
if (keys && (Object.values(keys).length !== 1 || gotKeyId !== xsk.getId())) {
throw new Error(`Key did not match expected value: expected ${xsk.getId()}, got ${gotKeyId}`);
}
if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) { if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
this.storeTrustedSelfKeys(xsk.keys); this.storeTrustedSelfKeys(xsk.keys);
@@ -2191,6 +2200,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
let verificationStatus = dev.verified; let verificationStatus = dev.verified;
if (verified) { if (verified) {
if (keys) {
for (const [keyId, key] of Object.entries(keys)) {
if (dev.keys[keyId] !== key) {
throw new Error(`Key did not match expected value: expected ${key}, got ${dev.keys[keyId]}`);
}
}
}
verificationStatus = DeviceVerification.VERIFIED; verificationStatus = DeviceVerification.VERIFIED;
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) { } else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
verificationStatus = DeviceVerification.UNVERIFIED; verificationStatus = DeviceVerification.UNVERIFIED;
@@ -2400,13 +2416,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return null; return null;
} }
const forwardingChain = event.getForwardingCurve25519KeyChain();
if (forwardingChain.length > 0) {
// we got the key this event from somewhere else
// TODO: check if we can trust the forwarders.
return null;
}
if (event.isKeySourceUntrusted()) { if (event.isKeySourceUntrusted()) {
// we got the key for this event from a source that we consider untrusted // we got the key for this event from a source that we consider untrusted
return null; return null;
@@ -2478,8 +2487,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
} }
ret.encrypted = true; ret.encrypted = true;
const forwardingChain = event.getForwardingCurve25519KeyChain(); if (event.isKeySourceUntrusted()) {
if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
// we got the key this event from somewhere else // we got the key this event from somewhere else
// TODO: check if we can trust the forwarders. // TODO: check if we can trust the forwarders.
ret.authenticated = false; ret.authenticated = false;

View File

@@ -30,6 +30,8 @@ import { logger } from '../logger';
import { IOneTimeKey } from "./dehydration"; import { IOneTimeKey } from "./dehydration";
import { IClaimOTKsResult, MatrixClient } from "../client"; import { IClaimOTKsResult, MatrixClient } from "../client";
import { ISignatures } from "../@types/signed"; import { ISignatures } from "../@types/signed";
import { MatrixEvent } from "../models/event";
import { EventType } from "../@types/event";
enum Algorithm { enum Algorithm {
Olm = "m.olm.v1.curve25519-aes-sha2", Olm = "m.olm.v1.curve25519-aes-sha2",
@@ -554,6 +556,22 @@ export function pkVerify(obj: IObject, pubKey: string, userId: string) {
} }
} }
/**
* Check that an event was encrypted using olm.
*/
export function isOlmEncrypted(event: MatrixEvent): boolean {
if (!event.getSenderKey()) {
logger.error("Event has no sender key (not encrypted?)");
return false;
}
if (event.getWireType() !== EventType.RoomMessageEncrypted ||
!(["m.olm.v1.curve25519-aes-sha2"].includes(event.getWireContent().algorithm))) {
logger.error("Event was not encrypted using an appropriate algorithm");
return false;
}
return true;
}
/** /**
* Encode a typed array of uint8 as base64. * Encode a typed array of uint8 as base64.
* @param {Uint8Array} uint8Array The data to encode. * @param {Uint8Array} uint8Array The data to encode.

View File

@@ -25,6 +25,7 @@ import { ICrossSigningInfo } from "../CrossSigning";
import { PrefixedLogger } from "../../logger"; import { PrefixedLogger } from "../../logger";
import { InboundGroupSessionData } from "../OlmDevice"; import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes"; import { IEncryptedPayload } from "../aes";
import { MatrixEvent } from "../../models/event";
/** /**
* Internal module. Definitions for storage for the crypto module * Internal module. Definitions for storage for the crypto module
@@ -127,6 +128,8 @@ export interface CryptoStore {
roomId: string, roomId: string,
txn?: unknown, txn?: unknown,
): Promise<[senderKey: string, sessionId: string][]>; ): Promise<[senderKey: string, sessionId: string][]>;
addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
// Session key backups // Session key backups
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>; doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
@@ -203,3 +206,12 @@ export interface OutgoingRoomKeyRequest {
requestBody: IRoomKeyRequestBody; requestBody: IRoomKeyRequestBody;
state: RoomKeyRequestState; state: RoomKeyRequestState;
} }
export interface ParkedSharedHistory {
senderId: string;
senderKey: string;
sessionId: string;
sessionKey: string;
keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; // XXX: Less type dependence on MatrixEvent
forwardingCurve25519KeyChain: string[];
}

View File

@@ -25,6 +25,7 @@ import {
IWithheld, IWithheld,
Mode, Mode,
OutgoingRoomKeyRequest, OutgoingRoomKeyRequest,
ParkedSharedHistory,
} from "./base"; } from "./base";
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index"; import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
import { ICrossSigningKey } from "../../client"; import { ICrossSigningKey } from "../../client";
@@ -873,6 +874,49 @@ export class Backend implements CryptoStore {
}); });
} }
public addParkedSharedHistory(
roomId: string,
parkedData: ParkedSharedHistory,
txn?: IDBTransaction,
): void {
if (!txn) {
txn = this.db.transaction(
"parked_shared_history", "readwrite",
);
}
const objectStore = txn.objectStore("parked_shared_history");
const req = objectStore.get([roomId]);
req.onsuccess = () => {
const { parked } = req.result || { parked: [] };
parked.push(parkedData);
objectStore.put({ roomId, parked });
};
}
public takeParkedSharedHistory(
roomId: string,
txn?: IDBTransaction,
): Promise<ParkedSharedHistory[]> {
if (!txn) {
txn = this.db.transaction(
"parked_shared_history", "readwrite",
);
}
const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId);
return new Promise((resolve, reject) => {
cursorReq.onsuccess = () => {
const cursor = cursorReq.result;
if (!cursor) {
resolve([]);
}
const data = cursor.value;
cursor.delete();
resolve(data);
};
cursorReq.onerror = reject;
});
}
public doTxn<T>( public doTxn<T>(
mode: Mode, mode: Mode,
stores: string | string[], stores: string | string[],
@@ -958,6 +1002,11 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
keyPath: ["roomId"], keyPath: ["roomId"],
}); });
} }
if (oldVersion < 11) {
db.createObjectStore("parked_shared_history", {
keyPath: ["roomId"],
});
}
// Expand as needed. // Expand as needed.
} }

View File

@@ -29,6 +29,7 @@ import {
IWithheld, IWithheld,
Mode, Mode,
OutgoingRoomKeyRequest, OutgoingRoomKeyRequest,
ParkedSharedHistory,
} from "./base"; } from "./base";
import { IRoomKeyRequestBody } from "../index"; import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client"; import { ICrossSigningKey } from "../../client";
@@ -55,6 +56,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld'; public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld';
public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions'; public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions';
public static STORE_PARKED_SHARED_HISTORY = 'parked_shared_history';
public static STORE_DEVICE_DATA = 'device_data'; public static STORE_DEVICE_DATA = 'device_data';
public static STORE_ROOMS = 'rooms'; public static STORE_ROOMS = 'rooms';
public static STORE_BACKUP = 'sessions_needing_backup'; public static STORE_BACKUP = 'sessions_needing_backup';
@@ -669,6 +671,27 @@ export class IndexedDBCryptoStore implements CryptoStore {
return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn); return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
} }
/**
* Park a shared-history group session for a room we may be invited to later.
*/
public addParkedSharedHistory(
roomId: string,
parkedData: ParkedSharedHistory,
txn?: IDBTransaction,
): void {
this.backend.addParkedSharedHistory(roomId, parkedData, txn);
}
/**
* Pop out all shared-history group sessions for a room.
*/
public takeParkedSharedHistory(
roomId: string,
txn?: IDBTransaction,
): Promise<ParkedSharedHistory[]> {
return this.backend.takeParkedSharedHistory(roomId, txn);
}
/** /**
* Perform a transaction on the crypto store. Any store methods * Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may * that require a transaction (txn) object to be passed in may

View File

@@ -25,6 +25,7 @@ import {
IWithheld, IWithheld,
Mode, Mode,
OutgoingRoomKeyRequest, OutgoingRoomKeyRequest,
ParkedSharedHistory,
} from "./base"; } from "./base";
import { IRoomKeyRequestBody } from "../index"; import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client"; import { ICrossSigningKey } from "../../client";
@@ -58,6 +59,7 @@ export class MemoryCryptoStore implements CryptoStore {
private rooms: { [roomId: string]: IRoomEncryption } = {}; private rooms: { [roomId: string]: IRoomEncryption } = {};
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {}; private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {}; private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
private parkedSharedHistory = new Map<string, ParkedSharedHistory[]>(); // keyed by room ID
/** /**
* Ensure the database exists and is up-to-date. * Ensure the database exists and is up-to-date.
@@ -526,6 +528,18 @@ export class MemoryCryptoStore implements CryptoStore {
return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []); return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
} }
public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory): void {
const parked = this.parkedSharedHistory.get(roomId) ?? [];
parked.push(parkedData);
this.parkedSharedHistory.set(roomId, parked);
}
public takeParkedSharedHistory(roomId: string): Promise<ParkedSharedHistory[]> {
const parked = this.parkedSharedHistory.get(roomId) ?? [];
this.parkedSharedHistory.delete(roomId);
return Promise.resolve(parked);
}
// Session key backups // Session key backups
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> { public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {

View File

@@ -299,7 +299,13 @@ export class VerificationBase<
if (this.doVerification && !this.started) { if (this.doVerification && !this.started) {
this.started = true; this.started = true;
this.resetTimer(); // restart the timeout this.resetTimer(); // restart the timeout
Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this)); new Promise<void>((resolve, reject) => {
const crossSignId = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(this.userId)?.getId();
if (crossSignId === this.deviceId) {
reject(new Error("Device ID is the same as the cross-signing ID"));
}
resolve();
}).then(() => this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
} }
return this.promise; return this.promise;
} }
@@ -310,14 +316,14 @@ export class VerificationBase<
// we try to verify all the keys that we're told about, but we might // we try to verify all the keys that we're told about, but we might
// not know about all of them, so keep track of the keys that we know // not know about all of them, so keep track of the keys that we know
// about, and ignore the rest // about, and ignore the rest
const verifiedDevices = []; const verifiedDevices: [string, string, string][] = [];
for (const [keyId, keyInfo] of Object.entries(keys)) { for (const [keyId, keyInfo] of Object.entries(keys)) {
const deviceId = keyId.split(':', 2)[1]; const deviceId = keyId.split(':', 2)[1];
const device = this.baseApis.getStoredDevice(userId, deviceId); const device = this.baseApis.getStoredDevice(userId, deviceId);
if (device) { if (device) {
verifier(keyId, device, keyInfo); verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId); verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
} else { } else {
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId); const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
@@ -326,7 +332,7 @@ export class VerificationBase<
[keyId]: deviceId, [keyId]: deviceId,
}, },
}, deviceId), keyInfo); }, deviceId), keyInfo);
verifiedDevices.push(deviceId); verifiedDevices.push([deviceId, keyId, deviceId]);
} else { } else {
logger.warn( logger.warn(
`verification: Could not find device ${deviceId} to verify`, `verification: Could not find device ${deviceId} to verify`,
@@ -348,8 +354,15 @@ export class VerificationBase<
// TODO: There should probably be a batch version of this, otherwise it's going // TODO: There should probably be a batch version of this, otherwise it's going
// to upload each signature in a separate API call which is silly because the // to upload each signature in a separate API call which is silly because the
// API supports as many signatures as you like. // API supports as many signatures as you like.
for (const deviceId of verifiedDevices) { for (const [deviceId, keyId, key] of verifiedDevices) {
await this.baseApis.setDeviceVerified(userId, deviceId); await this.baseApis.crypto.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key });
}
// if one of the user's own devices is being marked as verified / unverified,
// check the key backup status, since whether or not we use this depends on
// whether it has a signature from a verified device
if (userId == this.baseApis.credentials.userId) {
await this.baseApis.checkKeyBackup();
} }
} }

View File

@@ -22,6 +22,7 @@ export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
export interface MapperOpts { export interface MapperOpts {
preventReEmit?: boolean; preventReEmit?: boolean;
decrypt?: boolean; decrypt?: boolean;
toDevice?: boolean;
} }
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper { export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
@@ -29,6 +30,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
const decrypt = options.decrypt !== false; const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject: Partial<IEvent>) { function mapper(plainOldJsObject: Partial<IEvent>) {
if (options.toDevice) {
delete plainOldJsObject.room_id;
}
const room = client.getRoom(plainOldJsObject.room_id); const room = client.getRoom(plainOldJsObject.room_id);
let event: MatrixEvent; let event: MatrixEvent;

View File

@@ -15,7 +15,6 @@ limitations under the License.
*/ */
import { MBeaconEventContent } from "../@types/beacon"; import { MBeaconEventContent } from "../@types/beacon";
import { M_TIMESTAMP } from "../@types/location";
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers"; import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
import { MatrixEvent } from "../matrix"; import { MatrixEvent } from "../matrix";
import { sortEventsByLatestContentTimestamp } from "../utils"; import { sortEventsByLatestContentTimestamp } from "../utils";
@@ -161,7 +160,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
const validLocationEvents = beaconLocationEvents.filter(event => { const validLocationEvents = beaconLocationEvents.filter(event => {
const content = event.getContent<MBeaconEventContent>(); const content = event.getContent<MBeaconEventContent>();
const timestamp = M_TIMESTAMP.findIn<number>(content); const parsed = parseBeaconContent(content);
if (!parsed.uri || !parsed.timestamp) return false; // we won't be able to process these
const { timestamp } = parsed;
return ( return (
// only include positions that were taken inside the beacon's live period // only include positions that were taken inside the beacon's live period
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) && isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&

View File

@@ -151,6 +151,7 @@ interface IKeyRequestRecipient {
export interface IDecryptOptions { export interface IDecryptOptions {
emit?: boolean; emit?: boolean;
isRetry?: boolean; isRetry?: boolean;
keyTrusted?: boolean;
} }
/** /**
@@ -695,7 +696,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
throw new Error("Attempt to decrypt event which isn't encrypted"); throw new Error("Attempt to decrypt event which isn't encrypted");
} }
if (this.clearEvent && !this.isDecryptionFailure()) { if (this.clearEvent && !this.isDecryptionFailure() && !(this.isKeySourceUntrusted() && options.keyTrusted)) {
// we may want to just ignore this? let's start with rejecting it. // we may want to just ignore this? let's start with rejecting it.
throw new Error( throw new Error(
"Attempt to decrypt event which has already been decrypted", "Attempt to decrypt event which has already been decrypted",

View File

@@ -1109,7 +1109,20 @@ export class SyncApi {
if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) { if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) {
const cancelledKeyVerificationTxns = []; const cancelledKeyVerificationTxns = [];
data.to_device.events data.to_device.events
.map(client.getEventMapper()) .filter((eventJSON) => {
if (
eventJSON.type === EventType.RoomMessageEncrypted &&
!(["m.olm.v1.curve25519-aes-sha2"].includes(eventJSON.content?.algorithm))
) {
logger.log(
'Ignoring invalid encrypted to-device event from ' + eventJSON.sender,
);
return false;
}
return true;
})
.map(client.getEventMapper({ toDevice: true }))
.map((toDeviceEvent) => { // map is a cheap inline forEach .map((toDeviceEvent) => { // map is a cheap inline forEach
// We want to flag m.key.verification.start events as cancelled // We want to flag m.key.verification.start events as cancelled
// if there's an accompanying m.key.verification.cancel event, so // if there's an accompanying m.key.verification.cancel event, so
@@ -1185,6 +1198,24 @@ export class SyncApi {
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room); const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
await this.processRoomEvents(room, stateEvents); await this.processRoomEvents(room, stateEvents);
const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId())?.getSender();
const parkedHistory = await client.crypto.cryptoStore.takeParkedSharedHistory(room.roomId);
for (const parked of parkedHistory) {
if (parked.senderId === inviter) {
await this.client.crypto.olmDevice.addInboundGroupSession(
room.roomId,
parked.senderKey,
parked.forwardingCurve25519KeyChain,
parked.sessionId,
parked.sessionKey,
parked.keysClaimed,
true,
{ sharedHistory: true, untrusted: true },
);
}
}
if (inviteObj.isBrandNewRoom) { if (inviteObj.isBrandNewRoom) {
room.recalculate(); room.recalculate();
client.store.storeRoom(room); client.store.storeRoom(room);
@@ -1288,7 +1319,11 @@ export class SyncApi {
} }
} }
await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache); try {
await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache);
} catch (e) {
logger.error(`Failed to process events on room ${room.roomId}:`, e);
}
// set summary after processing events, // set summary after processing events,
// because it will trigger a name calculation // because it will trigger a name calculation