CVE-2022-39249 (published 2022-09-28T20:15Z)

Matrix Javascript SDK is the Matrix Client-Server SDK for JavaScript. Prior to version 19.7.0, an attacker cooperating with a malicious homeserver can construct messages appearing to have come from another person. Such messages will be marked with a grey shield on some platforms, but this may be missing in others. This attack is possible due to the matrix-js-sdk implementing a too permissive key forwarding strategy on the receiving end. Starting with version 19.7.0, the default policy for accepting key forwards has been made more strict in the matrix-js-sdk. matrix-js-sdk will now only accept forwarded keys in response to previously issued requests and only from own, verified devices. The SDK now sets a `trusted` flag on the decrypted message upon decryption, based on whether the key used to decrypt the message was received from a trusted source. Clients need to ensure that messages decrypted with a key with `trusted = false` are decorated appropriately, for example, by showing a warning for such messages. This attack requires coordination between a malicious homeserver and an attacker, and those who trust your homeservers do not need a workaround.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-287
cwe_name: Improper Authentication
description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
extended_description: 
            
			
         
         

Fix information:
repo_url: https://github.com/matrix-org/matrix-js-sdk
commit_details: repo_url: https://github.com/matrix-org/matrix-js-sdk
author: RiotRobot
author_date: 2022-09-28 13:55:15+01:00
msg: Resolve multiple CVEs

CVE-2022-39249
CVE-2022-39250
CVE-2022-39251
CVE-2022-39236
repository: repo_url: https://github.com/matrix-org/matrix-js-sdk
repo_name: matrix-org/matrix-js-sdk
description: Matrix Client-Server SDK for JavaScript
repo_language: TypeScript
owner: matrix-org
file_changes: filename: matrix-client-crypto.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -494,6 +494,7 @@ describe("MatrixClient crypto", () => {
         aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
         await aliTestClient.start();
         await bobTestClient.start();
+        bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
         await firstSync(aliTestClient);
         await aliEnablesEncryption();
         await aliSendsFirstMessage();
@@ -504,6 +505,7 @@ describe("MatrixClient crypto", () => {
         aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
         await aliTestClient.start();
         await bobTestClient.start();
+        bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
         await firstSync(aliTestClient);
         await aliEnablesEncryption();
         await aliSendsFirstMessage();
@@ -567,6 +569,7 @@ describe("MatrixClient crypto", () => {
         aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
         await aliTestClient.start();
         await bobTestClient.start();
+        bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
         await firstSync(aliTestClient);
         await aliEnablesEncryption();
         await aliSendsFirstMessage();
@@ -584,6 +587,9 @@ describe("MatrixClient crypto", () => {
         await firstSync(bobTestClient);
         await aliEnablesEncryption();
         await aliSendsFirstMessage();
+        bobTestClient.httpBackend.when('POST', '/keys/query').respond(
+            200, {},
+        );
         await bobRecvMessage();
         await bobEnablesEncryption();
         const ciphertext = await bobSendsReplyMessage();

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

programming_language: TypeScript
filename: megolm-integ.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -29,8 +29,11 @@ import {
     IDownloadKeyResult,
     MatrixEvent,
     MatrixEventEvent,
+    IndexedDBCryptoStore,
+    Room,
 } from "../../src/matrix";
 import { IDeviceKeys } from "../../src/crypto/dehydration";
+import { DeviceInfo } from "../../src/crypto/deviceinfo";
 
 const ROOM_ID = "!room:id";
 
@@ -280,10 +283,13 @@ describe("megolm", () => {
 
     it("Alice receives a megolm message", async () => {
         await aliceTestClient.start();
+        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
         const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
         const groupSession = new Olm.OutboundGroupSession();
         groupSession.create();
 
+        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
+
         // make the room_key event
         const roomKeyEncrypted = encryptGroupSessionKey({
             senderKey: testSenderKey,
@@ -326,10 +332,13 @@ describe("megolm", () => {
     it("Alice receives a megolm message before the session keys", async () => {
         // https://github.com/vector-im/element-web/issues/2273
         await aliceTestClient.start();
+        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
         const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
         const groupSession = new Olm.OutboundGroupSession();
         groupSession.create();
 
+        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
+
         // make the room_key event, but don't send it yet
         const roomKeyEncrypted = encryptGroupSessionKey({
             senderKey: testSenderKey,
@@ -383,10 +392,13 @@ describe("megolm", () => {
 
     it("Alice gets a second room_key message", async () => {
         await aliceTestClient.start();
+        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
         const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
         const groupSession = new Olm.OutboundGroupSession();
         groupSession.create();
 
+        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
+
         // make the room_key event
         const roomKeyEncrypted1 = encryptGroupSessionKey({
             senderKey: testSenderKey,
@@ -468,6 +480,9 @@ describe("megolm", () => {
         aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
             200, getTestKeysQueryResponse('@bob:xyz'),
         );
+        aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
+            200, getTestKeysQueryResponse('@bob:xyz'),
+        );
 
         await Promise.all([
             aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
@@ -541,13 +556,16 @@ describe("megolm", () => {
 
         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(
             200, getTestKeysQueryResponse('@bob:xyz'),
         );
 
         await Promise.all([
             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');
@@ -592,6 +610,9 @@ describe("megolm", () => {
 
         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(
             200, getTestKeysQueryResponse('@bob:xyz'),
         );
@@ -786,6 +807,10 @@ describe("megolm", () => {
         logger.log('Forcing alice to download our device keys');
         const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
 
+        aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
+            200, getTestKeysQueryResponse('@bob:xyz'),
+        );
+
         // so will this.
         const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
             .then(() => {
@@ -805,9 +830,12 @@ describe("megolm", () => {
     it("Alice exports megolm keys and imports them to a new device", async () => {
         aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
         await aliceTestClient.start();
+        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
         // establish an olm session with alice
         const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
 
+        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
+
         const groupSession = new Olm.OutboundGroupSession();
         groupSession.create();
 
@@ -855,6 +883,8 @@ describe("megolm", () => {
         await aliceTestClient.client.importRoomKeys(exported);
         await aliceTestClient.start();
 
+        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
+
         const syncResponse = {
             next_batch: 1,
             rooms: {
@@ -927,10 +957,13 @@ describe("megolm", () => {
 
     it("Alice can decrypt a message with falsey content", async () => {
         await aliceTestClient.start();
+        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
         const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
         const groupSession = new Olm.OutboundGroupSession();
         groupSession.create();
 
+        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
+
         // make the room_key event
         const roomKeyEncrypted = encryptGroupSessionKey({
             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",
         async () => {
             await aliceTestClient.start();
+            aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
             const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
             const groupSession = new Olm.OutboundGroupSession();
             groupSession.create();
 
+            aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
+
             // make the room_key event
             const roomKeyEncrypted = encryptGroupSessionKey({
                 senderKey: testSenderKey,
@@ -1045,4 +1081,283 @@ describe("megolm", () => {
             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();
+    });
 });

programming_language: TypeScript
filename: content-helpers.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,7 @@ import {
     makeBeaconContent,
     makeBeaconInfoContent,
     makeTopicContent,
+    parseBeaconContent,
     parseTopicContent,
 } 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', () => {

programming_language: TypeScript
filename: crypto.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -15,6 +15,8 @@ import { CRYPTO_ENABLED } from "../../src/client";
 import { DeviceInfo } from "../../src/crypto/deviceinfo";
 import { logger } from '../../src/logger';
 import { MemoryStore } from "../../src";
+import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager';
+import { RoomMember } from '../../src/models/room-member';
 import { IStore } from '../../src/store';
 
 const Olm = global.Olm;
@@ -40,20 +42,52 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
         type: "m.forwarded_room_key",
         sender: client.getUserId(),
         content: {
-            algorithm: olmlib.MEGOLM_ALGORITHM,
-            room_id: roomId,
-            sender_key: eventContent.sender_key,
-            sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
-            session_id: eventContent.session_id,
-            session_key: key.key,
-            chain_index: key.chain_index,
-            forwarding_curve25519_key_chain:
-            key.forwarding_curve_key_chain,
+            "algorithm": olmlib.MEGOLM_ALGORITHM,
+            "room_id": roomId,
+            "sender_key": eventContent.sender_key,
+            "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
+            "session_id": eventContent.session_id,
+            "session_key": key.key,
+            "chain_index": key.chain_index,
+            "forwarding_curve25519_key_chain": key.forwarding_curve_key_chain,
+            "org.matrix.msc3061.shared_history": true,
         },
     });
     // make onRoomKeyEvent think this was an encrypted event
     // @ts-ignore private property
     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;
 }
 
@@ -95,7 +129,7 @@ describe("Crypto", function() {
             event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
             event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
             event.getForwardingCurve25519KeyChain = () => ["not empty"];
-            event.isKeySourceUntrusted = () => false;
+            event.isKeySourceUntrusted = () => true;
             event.getClaimedEd25519Key =
                 () => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
 
@@ -233,6 +267,7 @@ describe("Crypto", function() {
     describe('Key requests', function() {
         let aliceClient: MatrixClient;
         let bobClient: MatrixClient;
+        let claraClient: MatrixClient;
 
         beforeEach(async function() {
             aliceClient = (new TestClient(
@@ -241,22 +276,35 @@ describe("Crypto", function() {
             bobClient = (new TestClient(
                 "@bob:example.com", "bobdevice",
             )).client;
+            claraClient = (new TestClient(
+                "@clara:example.com", "claradevice",
+            )).client;
             await aliceClient.initCrypto();
             await bobClient.initCrypto();
+            await claraClient.initCrypto();
         });
 
         afterEach(async function() {
             aliceClient.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 = {
                 "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", {});
+            // 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);
             bobClient.store.storeRoom(bobRoom);
             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(
                 roomId, olmlib.MEGOLM_ALGORITHM,
             );
@@ -314,6 +365,8 @@ describe("Crypto", function() {
             // the first message can't be decrypted yet, but the second one
             // can
             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 decryptEventsPromise;
             expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
@@ -340,8 +393,24 @@ describe("Crypto", function() {
             await bobDecryptor.onRoomKeyEvent(ksEvent);
             await decryptEventPromise;
             expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
+            expect(events[0].isKeySourceUntrusted()).toBeTruthy();
+            await sleep(1);
+            // 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);
-            // the room key request should be gone since we've now decrypted everything
+            // now the room key request should be gone, since there's
+            // no better key to wait for
             expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
         });
 
@@ -383,6 +452,9 @@ describe("Crypto", function() {
                 // decryption keys yet
             }
 
+            const device = new DeviceInfo(aliceClient.deviceId);
+            bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
+
             const bobDecryptor = bobClient.crypto.getRoomDecryptor(
                 roomId, olmlib.MEGOLM_ALGORITHM,
             );
@@ -462,6 +534,420 @@ describe("Crypto", function() {
             expect(aliceSendToDevice).toBeCalledTimes(3);
             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() {

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

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

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

programming_language: TypeScript
filename: sas.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -464,7 +464,7 @@ describe("SAS verification", function() {
                 },
             );
 
-            alice.client.setDeviceVerified = jest.fn();
+            alice.client.crypto.setDeviceVerification = jest.fn();
             alice.client.getDeviceEd25519Key = () => {
                 return "alice+base64+ed25519+key";
             };
@@ -482,7 +482,7 @@ describe("SAS verification", function() {
                 return Promise.resolve();
             };
 
-            bob.client.setDeviceVerified = jest.fn();
+            bob.client.crypto.setDeviceVerification = jest.fn();
             bob.client.getStoredDevice = () => {
                 return DeviceInfo.fromStorage(
                     {
@@ -565,10 +565,24 @@ describe("SAS verification", function() {
             ]);
 
             // make sure Alice and Bob verified each other
-            expect(alice.client.setDeviceVerified)
-                .toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
-            expect(bob.client.setDeviceVerified)
-                .toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
+            expect(alice.client.crypto.setDeviceVerification)
+                .toHaveBeenCalledWith(
+                    bob.client.getUserId(),
+                    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" },
+            );
         });
     });
 });

programming_language: TypeScript
filename: beacon.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,7 @@ import {
     BeaconEvent,
 } from "../../../src/models/beacon";
 import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
+import { REFERENCE_RELATION } from "matrix-events-sdk";
 
 jest.useFakeTimers();
 
@@ -431,6 +432,27 @@ describe('Beacon', () => {
                 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', () => {
                 it('ignores locations before the beacon start timestamp', () => {
                     const startTimestamp = now + 60000;

programming_language: TypeScript
filename: crypto.ts
change_type: ModificationType.ADD
diff: @@ -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;
+};

programming_language: TypeScript
filename: NamespacedValue.ts
change_type: ModificationType.MODIFY
diff: @@ -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");
 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.
 */
 
+import { Optional } from "matrix-events-sdk/lib/types";
+
 /**
  * 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.
@@ -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
     // 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;
         if (this.name) {
             val = obj?.[this.name];

programming_language: TypeScript
filename: client.ts
change_type: ModificationType.MODIFY
diff: @@ -5287,6 +5287,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
      * @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.decrypt decrypt event proactively
+     * @param {boolean} options.toDevice the event is a to_device event
      * @return {Function}
      */
     public getEventMapper(options?: MapperOpts): EventMapper {

programming_language: TypeScript
filename: content-helpers.ts
change_type: ModificationType.MODIFY
diff: @@ -292,16 +292,17 @@ export const makeBeaconContent: MakeBeaconContent = (
 });
 
 export type BeaconLocationState = MLocationContent & {
-    timestamp: number;
+    uri?: string; // override from MLocationContent to allow optionals
+    timestamp?: number;
 };
 
 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);
 
     return {
-        description,
-        uri,
+        description: location?.description,
+        uri: location?.uri,
         timestamp,
     };
 };

programming_language: TypeScript
filename: OlmDevice.ts
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,7 @@ import * as algorithms from './algorithms';
 import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
 import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
 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
 // reasonable approximation to the biggest plaintext we can encrypt.
@@ -122,6 +123,7 @@ interface IInboundGroupSessionKey {
     forwarding_curve25519_key_chain: string[];
     sender_claimed_ed25519_key: string;
     shared_history: boolean;
+    untrusted: boolean;
 }
 /* eslint-enable camelcase */
 
@@ -1101,7 +1103,7 @@ export class OlmDevice {
         sessionKey: string,
         keysClaimed: Record<string, string>,
         exportFormat: boolean,
-        extraSessionData: Record<string, any> = {},
+        extraSessionData: OlmGroupSessionExtraData = {},
     ): Promise<void> {
         await this.cryptoStore.doTxn(
             'readwrite', [
@@ -1133,17 +1135,42 @@ export class OlmDevice {
                                     "Update for megolm session "
                                     + senderKey + "/" + sessionId,
                                 );
-                                if (existingSession.first_known_index()
-                                    <= session.first_known_index()
-                                    && !(existingSession.first_known_index() == session.first_known_index()
-                                        && !extraSessionData.untrusted
-                                        && existingSessionData.untrusted)) {
-                                    // existing session has lower index (i.e. can
-                                    // decrypt more), or they have the same index and
-                                    // the new sessions trust does not win over the old
-                                    // sessions trust, so keep it
-                                    logger.log(`Keeping existing megolm session ${sessionId}`);
-                                    return;
+                                if (existingSession.first_known_index() <= session.first_known_index()) {
+                                    if (!existingSessionData.untrusted || extraSessionData.untrusted) {
+                                        // existing session has less-than-or-equal index
+                                        // (i.e. can decrypt at least as much), and the
+                                        // new session's trust does not win over the old
+                                        // session's trust, so keep it
+                                        logger.log(`Keeping existing megolm session ${sessionId}`);
+                                        return;
+                                    }
+                                    if (existingSession.first_known_index() < session.first_known_index()) {
+                                        // 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 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 = {
                             "chain_index": chainIndex,
                             "key": exportedSession,
-                            "forwarding_curve25519_key_chain":
-                                sessionData.forwardingCurve25519KeyChain || [],
+                            "forwarding_curve25519_key_chain": forwardingKeyChain,
                             "sender_claimed_ed25519_key": senderEd25519Key,
                             "shared_history": sessionData.sharedHistory || false,
+                            "untrusted": untrusted,
                         };
                     },
                 );

programming_language: TypeScript
filename: SecretStorage.ts
change_type: ModificationType.MODIFY
diff: @@ -539,7 +539,23 @@ export class SecretStorage {
             // because someone could be trying to send us bogus data
             return;
         }
+
+        if (!olmlib.isOlmEncrypted(event)) {
+            logger.error("secret event not properly encrypted");
+            return;
+        }
+
         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);
         const requestControl = this.requests.get(content.request_id);
         if (requestControl) {
@@ -559,6 +575,14 @@ export class SecretStorage {
                 logger.log("unsolicited secret share from device", deviceInfo.deviceId);
                 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(
                 `Successfully received secret ${requestControl.name} ` +

programming_language: TypeScript
filename: megolm.ts
change_type: ModificationType.MODIFY
diff: @@ -35,8 +35,10 @@ import { Room } from '../../models/room';
 import { DeviceInfo } from "../deviceinfo";
 import { IOlmSessionResult } from "../olmlib";
 import { DeviceInfoMap } from "../DeviceList";
-import { MatrixEvent } from "../..";
+import { MatrixEvent } from "../../models/event";
 import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
+import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
+import { OlmGroupSessionExtraData } from "../../@types/crypto";
 
 // determine whether the key can be shared with invitees
 export function isRoomSharedHistory(room: Room): boolean {
@@ -1189,8 +1191,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
  *     {@link module:crypto/algorithms/DecryptionAlgorithm}
  */
 class MegolmDecryption extends DecryptionAlgorithm {
-    // events which we couldn't decrypt due to unknown sessions / indexes: map from
-    // senderKey|sessionId to Set of MatrixEvents
+    // events which we couldn't decrypt due to unknown sessions /
+    // 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>>>();
 
     // 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
-        // already happened.
-        this.removeEventFromPendingList(event);
+        // Success. We can remove the event from the pending list, if
+        // that hasn't already happened. However, if the event was
+        // 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);
 
@@ -1391,6 +1398,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
         let exportFormat = false;
         let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
 
+        const extraSessionData: OlmGroupSessionExtraData = {};
+
         if (!content.room_id ||
             !content.session_key ||
             !content.session_id ||
@@ -1400,12 +1409,58 @@ class MegolmDecryption extends DecryptionAlgorithm {
             return;
         }
 
-        if (!senderKey) {
-            logger.error("key event has no sender key (not encrypted?)");
+        if (!olmlib.isOlmEncrypted(event)) {
+            logger.error("key event not properly encrypted");
             return;
         }
 
+        if (content["org.matrix.msc3061.shared_history"]) {
+            extraSessionData.sharedHistory = true;
+        }
+
         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;
             forwardingKeyChain = Array.isArray(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");
                 return;
             }
-            senderKey = content.sender_key;
 
             const ed25519Key = content.sender_claimed_ed25519_key;
             if (!ed25519Key) {
@@ -1431,11 +1485,45 @@ class MegolmDecryption extends DecryptionAlgorithm {
             keysClaimed = {
                 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 {
             keysClaimed = event.getKeysClaimed();
         }
 
-        const extraSessionData: any = {};
         if (content["org.matrix.msc3061.shared_history"]) {
             extraSessionData.sharedHistory = true;
         }
@@ -1453,7 +1541,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
             );
 
             // 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.
                 // Only do this if we managed to decrypt every message in the
                 // session, because if we didn't, we leave the other key
@@ -1668,7 +1756,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
         session: IMegolmSessionData,
         opts: { untrusted?: boolean, source?: string } = {},
     ): Promise<void> {
-        const extraSessionData: any = {};
+        const extraSessionData: OlmGroupSessionExtraData = {};
         if (opts.untrusted || session.untrusted) {
             extraSessionData.untrusted = true;
         }
@@ -1696,7 +1784,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
                 });
             }
             // 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
      * @param {String} senderKey
      * @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);
         if (!senderPendingEvents) {
             return true;
@@ -1725,13 +1815,14 @@ class MegolmDecryption extends DecryptionAlgorithm {
 
         await Promise.all([...pending].map(async (ev) => {
             try {
-                await ev.attemptDecryption(this.crypto, { isRetry: true });
+                await ev.attemptDecryption(this.crypto, { isRetry: true, keyTrusted });
             } catch (e) {
                 // 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);
     }
 

programming_language: TypeScript
filename: olm.ts
change_type: ModificationType.MODIFY
diff: @@ -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
         // avoid people masquerading as others.
         // (this check is also provided via the sender's embedded ed25519 key,

programming_language: TypeScript
filename: backup.ts
change_type: ModificationType.MODIFY
diff: @@ -431,7 +431,6 @@ export class BackupManager {
                 )
             );
         });
-        ret.usable = ret.usable || ret.trusted_locally;
         return ret;
     }
 

programming_language: TypeScript
filename: index.ts
change_type: ModificationType.MODIFY
diff: @@ -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
      *      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
      */
     public async setDeviceVerification(
@@ -2113,6 +2117,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
         verified?: boolean,
         blocked?: boolean,
         known?: boolean,
+        keys?: Record<string, string>,
     ): Promise<DeviceInfo | CrossSigningInfo> {
         // get rid of any `undefined`s here so we can just check
         // for null rather than null or undefined
@@ -2131,6 +2136,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
             if (!verified) {
                 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) {
                 this.storeTrustedSelfKeys(xsk.keys);
@@ -2191,6 +2200,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
         let verificationStatus = dev.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;
         } else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
             verificationStatus = DeviceVerification.UNVERIFIED;
@@ -2400,13 +2416,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
             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()) {
             // we got the key for this event from a source that we consider untrusted
             return null;
@@ -2478,8 +2487,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
         }
         ret.encrypted = true;
 
-        const forwardingChain = event.getForwardingCurve25519KeyChain();
-        if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
+        if (event.isKeySourceUntrusted()) {
             // we got the key this event from somewhere else
             // TODO: check if we can trust the forwarders.
             ret.authenticated = false;

programming_language: TypeScript
filename: olmlib.ts
change_type: ModificationType.MODIFY
diff: @@ -30,6 +30,8 @@ import { logger } from '../logger';
 import { IOneTimeKey } from "./dehydration";
 import { IClaimOTKsResult, MatrixClient } from "../client";
 import { ISignatures } from "../@types/signed";
+import { MatrixEvent } from "../models/event";
+import { EventType } from "../@types/event";
 
 enum Algorithm {
     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.
  * @param {Uint8Array} uint8Array The data to encode.

programming_language: TypeScript
filename: base.ts
change_type: ModificationType.MODIFY
diff: @@ -25,6 +25,7 @@ import { ICrossSigningInfo } from "../CrossSigning";
 import { PrefixedLogger } from "../../logger";
 import { InboundGroupSessionData } from "../OlmDevice";
 import { IEncryptedPayload } from "../aes";
+import { MatrixEvent } from "../../models/event";
 
 /**
  * Internal module. Definitions for storage for the crypto module
@@ -127,6 +128,8 @@ export interface CryptoStore {
         roomId: string,
         txn?: unknown,
     ): Promise<[senderKey: string, sessionId: string][]>;
+    addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
+    takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
 
     // Session key backups
     doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
@@ -203,3 +206,12 @@ export interface OutgoingRoomKeyRequest {
     requestBody: IRoomKeyRequestBody;
     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[];
+}

programming_language: TypeScript
filename: indexeddb-crypto-store-backend.ts
change_type: ModificationType.MODIFY
diff: @@ -25,6 +25,7 @@ import {
     IWithheld,
     Mode,
     OutgoingRoomKeyRequest,
+    ParkedSharedHistory,
 } from "./base";
 import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
 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>(
         mode: Mode,
         stores: string | string[],
@@ -958,6 +1002,11 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
             keyPath: ["roomId"],
         });
     }
+    if (oldVersion < 11) {
+        db.createObjectStore("parked_shared_history", {
+            keyPath: ["roomId"],
+        });
+    }
     // Expand as needed.
 }
 

programming_language: TypeScript
filename: indexeddb-crypto-store.ts
change_type: ModificationType.MODIFY
diff: @@ -29,6 +29,7 @@ import {
     IWithheld,
     Mode,
     OutgoingRoomKeyRequest,
+    ParkedSharedHistory,
 } from "./base";
 import { IRoomKeyRequestBody } from "../index";
 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_WITHHELD = 'inbound_group_sessions_withheld';
     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_ROOMS = 'rooms';
     public static STORE_BACKUP = 'sessions_needing_backup';
@@ -669,6 +671,27 @@ export class IndexedDBCryptoStore implements CryptoStore {
         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
      * that require a transaction (txn) object to be passed in may

programming_language: TypeScript
filename: memory-crypto-store.ts
change_type: ModificationType.MODIFY
diff: @@ -25,6 +25,7 @@ import {
     IWithheld,
     Mode,
     OutgoingRoomKeyRequest,
+    ParkedSharedHistory,
 } from "./base";
 import { IRoomKeyRequestBody } from "../index";
 import { ICrossSigningKey } from "../../client";
@@ -58,6 +59,7 @@ export class MemoryCryptoStore implements CryptoStore {
     private rooms: { [roomId: string]: IRoomEncryption } = {};
     private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
     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.
@@ -526,6 +528,18 @@ export class MemoryCryptoStore implements CryptoStore {
         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
 
     public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {

programming_language: TypeScript
filename: Base.ts
change_type: ModificationType.MODIFY
diff: @@ -299,7 +299,13 @@ export class VerificationBase<
         if (this.doVerification && !this.started) {
             this.started = true;
             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;
     }
@@ -310,14 +316,14 @@ export class VerificationBase<
         // 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
         // about, and ignore the rest
-        const verifiedDevices = [];
+        const verifiedDevices: [string, string, string][] = [];
 
         for (const [keyId, keyInfo] of Object.entries(keys)) {
             const deviceId = keyId.split(':', 2)[1];
             const device = this.baseApis.getStoredDevice(userId, deviceId);
             if (device) {
                 verifier(keyId, device, keyInfo);
-                verifiedDevices.push(deviceId);
+                verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
             } else {
                 const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
                 if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
@@ -326,7 +332,7 @@ export class VerificationBase<
                             [keyId]: deviceId,
                         },
                     }, deviceId), keyInfo);
-                    verifiedDevices.push(deviceId);
+                    verifiedDevices.push([deviceId, keyId, deviceId]);
                 } else {
                     logger.warn(
                         `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
         // to upload each signature in a separate API call which is silly because the
         // API supports as many signatures as you like.
-        for (const deviceId of verifiedDevices) {
-            await this.baseApis.setDeviceVerified(userId, deviceId);
+        for (const [deviceId, keyId, key] of verifiedDevices) {
+            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();
         }
     }
 

programming_language: TypeScript
filename: event-mapper.ts
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,7 @@ export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
 export interface MapperOpts {
     preventReEmit?: boolean;
     decrypt?: boolean;
+    toDevice?: boolean;
 }
 
 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;
 
     function mapper(plainOldJsObject: Partial<IEvent>) {
+        if (options.toDevice) {
+            delete plainOldJsObject.room_id;
+        }
+
         const room = client.getRoom(plainOldJsObject.room_id);
 
         let event: MatrixEvent;

programming_language: TypeScript
filename: beacon.ts
change_type: ModificationType.MODIFY
diff: @@ -15,7 +15,6 @@ limitations under the License.
 */
 
 import { MBeaconEventContent } from "../@types/beacon";
-import { M_TIMESTAMP } from "../@types/location";
 import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
 import { MatrixEvent } from "../matrix";
 import { sortEventsByLatestContentTimestamp } from "../utils";
@@ -161,7 +160,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
 
         const validLocationEvents = beaconLocationEvents.filter(event => {
             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 (
                 // only include positions that were taken inside the beacon's live period
                 isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&

programming_language: TypeScript
filename: event.ts
change_type: ModificationType.MODIFY
diff: @@ -151,6 +151,7 @@ interface IKeyRequestRecipient {
 export interface IDecryptOptions {
     emit?: 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");
         }
 
-        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.
             throw new Error(
                 "Attempt to decrypt event which has already been decrypted",

programming_language: TypeScript
filename: sync.ts
change_type: ModificationType.MODIFY
diff: @@ -1109,7 +1109,20 @@ export class SyncApi {
         if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) {
             const cancelledKeyVerificationTxns = [];
             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
                     // We want to flag m.key.verification.start events as cancelled
                     // 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);
 
             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) {
                 room.recalculate();
                 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,
             // because it will trigger a name calculation

programming_language: TypeScript