CVE-2022-39236 (published 2022-09-28T17:15Z)

Matrix Javascript SDK is the Matrix Client-Server SDK for JavaScript. Starting with version 17.1.0-rc.1, improperly formed beacon events can disrupt or impede the matrix-js-sdk from functioning properly, potentially impacting the consumer's ability to process data safely. Note that the matrix-js-sdk can appear to be operating normally but be excluding or corrupting runtime data presented to the consumer. This is patched in matrix-js-sdk v19.7.0. Redacting applicable events, waiting for the sync processor to store data, and restarting the client are possible workarounds. Alternatively, redacting the applicable events and clearing all storage will fix the further perceived issues. Downgrading to an unaffected version, noting that such a version may be subject to other vulnerabilities, will additionally resolve the issue.

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

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

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