CVE-2022-39250: Improper Authentication (CWE-287)
value: Matrix JavaScript SDK is the Matrix Client-Server software development kit (SDK) for JavaScript. Prior to version 19.7.0, an attacker cooperating with a malicious homeserver could interfere with the verification flow between two users, injecting its own cross-signing user identity in place of one of the users’ identities. This would lead to the other device trusting/verifying the user identity under the control of the homeserver instead of the intended one. The vulnerability is a bug in the matrix-js-sdk, caused by checking and signing user identities and devices in two separate steps, and inadequately fixing the keys to be signed between those steps. Even though the attack is partly made possible due to the design decision of treating cross-signing user identities as Matrix devices on the server side (with their device ID set to the public part of the user identity key), no other examined implementations were vulnerable. Starting with version 19.7.0, the matrix-js-sdk has been modified to double check that the key signed is the one that was verified instead of just referencing the key by ID. An additional check has been made to report an error when one of the device ID matches a cross-signing key. As this attack requires coordination between a malicious homeserver and an attacker, those who trust their homeservers do not need a particular workaround.
CWE description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
CVSS v3 base score: 7.5
Published: 2022-09-29T13:15Z
Repository: https://github.com/matrix-org/matrix-js-sdk
Language: TypeScript
Files changed: spec/integ/matrix-client-crypto.spec.ts, spec/integ/matrix-client-syncing.spec.ts, spec/integ/megolm-integ.spec.ts, spec/unit/content-helpers.spec.ts, spec/unit/crypto.spec.ts, spec/unit/crypto/algorithms/megolm.spec.ts, spec/unit/crypto/backup.spec.ts, spec/unit/crypto/secrets.spec.ts, spec/unit/crypto/verification/sas.spec.ts, spec/unit/models/beacon.spec.ts, src/@types/crypto.ts, src/NamespacedValue.ts, src/client.ts, src/content-helpers.ts, src/crypto/OlmDevice.ts, src/crypto/SecretStorage.ts, src/crypto/algorithms/megolm.ts, src/crypto/algorithms/olm.ts, src/crypto/backup.ts, src/crypto/index.ts, src/crypto/olmlib.ts, src/crypto/store/base.ts, src/crypto/store/indexeddb-crypto-store-backend.ts, src/crypto/store/indexeddb-crypto-store.ts, src/crypto/store/memory-crypto-store.ts, src/crypto/verification/Base.ts, src/event-mapper.ts, src/models/beacon.ts, src/models/event.ts, src/sync.ts
Commit date: 2022-09-28 13:55:15 +0100
Commit message: Resolve multiple CVEs

CVE-2022-39249
CVE-2022-39250
CVE-2022-39251
CVE-2022-39236

Diff: diff --git a/spec/integ/matrix-client-crypto.spec.ts b/spec/integ/matrix-client-crypto.spec.ts
index fc0e30dd..d0f46b9e 100644
--- a/spec/integ/matrix-client-crypto.spec.ts
+++ b/spec/integ/matrix-client-crypto.spec.ts
@@ -492,20 +492,22 @@ describe("MatrixClient crypto", () => {
 
     it("Bob receives a message", async () => {
         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();
         await bobRecvMessage();
     });
 
     it("Bob receives a message with a bogus sender", async () => {
         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();
         const message = aliMessages.shift();
         const syncData = {
@@ -565,10 +567,11 @@ describe("MatrixClient crypto", () => {
 
     it("Bob receives two pre-key messages", async () => {
         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();
         await bobRecvMessage();
         await aliSendsMessage();
@@ -582,10 +585,13 @@ describe("MatrixClient crypto", () => {
         await bobTestClient.start();
         await firstSync(aliTestClient);
         await firstSync(bobTestClient);
         await aliEnablesEncryption();
         await aliSendsFirstMessage();
+        bobTestClient.httpBackend.when('POST', '/keys/query').respond(
+            200, {},
+        );
         await bobRecvMessage();
         await bobEnablesEncryption();
         const ciphertext = await bobSendsReplyMessage();
         expect(ciphertext.type).toEqual(1);
         await aliRecvMessage();
diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts
index 9f3fb988..a22e0a25 100644
--- a/spec/integ/matrix-client-syncing.spec.ts
+++ b/spec/integ/matrix-client-syncing.spec.ts
@@ -85,10 +85,12 @@ describe("MatrixClient syncing", () => {
                 done();
             });
         });
 
         it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
+            await client.initCrypto();
+
             const roomId = "!cycles:example.org";
 
             // First sync: an invite
             const inviteSyncRoomSection = {
                 invite: {
diff --git a/spec/integ/megolm-integ.spec.ts b/spec/integ/megolm-integ.spec.ts
index f69d2ebd..ae2771f3 100644
--- a/spec/integ/megolm-integ.spec.ts
+++ b/spec/integ/megolm-integ.spec.ts
@@ -27,12 +27,15 @@ import {
     IJoinedRoom,
     ISyncResponse,
     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";
 
 // start an Olm session with a given recipient
 async function createOlmSession(olmAccount: Olm.Account, recipientTestClient: TestClient): Promise<Olm.Session> {
@@ -278,14 +281,17 @@ describe("megolm", () => {
 
     afterEach(() => aliceTestClient.stop());
 
     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,
             recipient: aliceTestClient,
             p2pSession: p2pSession,
@@ -324,14 +330,17 @@ 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,
             recipient: aliceTestClient,
             p2pSession: p2pSession,
@@ -381,14 +390,17 @@ describe("megolm", () => {
         expect(decryptedEvent.getContent().body).toEqual('42');
     });
 
     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,
             recipient: aliceTestClient,
             p2pSession: p2pSession,
@@ -466,10 +478,13 @@ describe("megolm", () => {
 
         // start out with the device unknown - the send should be rejected.
         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(() => {
                 throw new Error("sendTextMessage failed on an unknown device");
             }, (e) => {
@@ -539,17 +554,20 @@ describe("megolm", () => {
 
         await aliceTestClient.flushSync();
 
         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');
         aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
 
@@ -590,10 +608,13 @@ describe("megolm", () => {
 
         await aliceTestClient.flushSync();
 
         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'),
         );
 
         await Promise.all([
@@ -784,10 +805,14 @@ describe("megolm", () => {
 
         // this will block
         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(() => {
                 throw new Error("sendTextMessage failed on an unknown device");
             }, (e) => {
@@ -803,13 +828,16 @@ 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();
 
         // make the room_key event
         const roomKeyEncrypted = encryptGroupSessionKey({
@@ -853,10 +881,12 @@ describe("megolm", () => {
         );
         await aliceTestClient.client.initCrypto();
         await aliceTestClient.client.importRoomKeys(exported);
         await aliceTestClient.start();
 
+        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
+
         const syncResponse = {
             next_batch: 1,
             rooms: {
                 join: { [ROOM_ID]: { timeline: { events: [messageEncrypted] } } },
             },
@@ -925,14 +955,17 @@ describe("megolm", () => {
         testClient.stop();
     });
 
     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,
             recipient: aliceTestClient,
             p2pSession: p2pSession,
@@ -983,14 +1016,17 @@ describe("megolm", () => {
 
     it(
         "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,
                 recipient: aliceTestClient,
                 p2pSession: p2pSession,
@@ -1043,6 +1079,285 @@ describe("megolm", () => {
             expect(event.getContent()).toEqual({});
             const redactionEvent: any = event.getRedactionEvent();
             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();
+    });
 });
diff --git a/spec/unit/content-helpers.spec.ts b/spec/unit/content-helpers.spec.ts
index ba431d9d..35971600 100644
--- a/spec/unit/content-helpers.spec.ts
+++ b/spec/unit/content-helpers.spec.ts
@@ -20,10 +20,11 @@ import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@
 import { M_TOPIC } from "../../src/@types/topic";
 import {
     makeBeaconContent,
     makeBeaconInfoContent,
     makeTopicContent,
+    parseBeaconContent,
     parseTopicContent,
 } from "../../src/content-helpers";
 
 describe('Beacon content helpers', () => {
     describe('makeBeaconInfoContent()', () => {
@@ -125,10 +126,70 @@ describe('Beacon content helpers', () => {
                     event_id: '$1234',
                 },
             });
         });
     });
+
+    describe("parseBeaconContent()", () => {
+        it("should not explode when parsing an invalid beacon", () => {
+            // deliberate cast to simulate wire content being invalid
+            const result = parseBeaconContent({} as any);
+            expect(result).toEqual({
+                description: undefined,
+                uri: undefined,
+                timestamp: undefined,
+            });
+        });
+
+        it("should parse unstable values", () => {
+            const uri = "urigoeshere";
+            const description = "descriptiongoeshere";
+            const timestamp = 1234;
+            const result = parseBeaconContent({
+                "org.matrix.msc3488.location": {
+                    uri,
+                    description,
+                },
+                "org.matrix.msc3488.ts": timestamp,
+
+                // relationship not used - just here to satisfy types
+                "m.relates_to": {
+                    rel_type: "m.reference",
+                    event_id: "$unused",
+                },
+            });
+            expect(result).toEqual({
+                description,
+                uri,
+                timestamp,
+            });
+        });
+
+        it("should parse stable values", () => {
+            const uri = "urigoeshere";
+            const description = "descriptiongoeshere";
+            const timestamp = 1234;
+            const result = parseBeaconContent({
+                "m.location": {
+                    uri,
+                    description,
+                },
+                "m.ts": timestamp,
+
+                // relationship not used - just here to satisfy types
+                "m.relates_to": {
+                    rel_type: "m.reference",
+                    event_id: "$unused",
+                },
+            });
+            expect(result).toEqual({
+                description,
+                uri,
+                timestamp,
+            });
+        });
+    });
 });
 
 describe('Topic content helpers', () => {
     describe('makeTopicContent()', () => {
         it('creates fully defined event content without html', () => {
diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts
index 93a5461b..6e46b3aa 100644
--- a/spec/unit/crypto.spec.ts
+++ b/spec/unit/crypto.spec.ts
@@ -13,10 +13,12 @@ import * as olmlib from "../../src/crypto/olmlib";
 import { sleep } from "../../src/utils";
 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;
 
 function awaitEvent(emitter, event) {
@@ -38,24 +40,56 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
     );
     const ksEvent = new 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;
 }
 
 describe("Crypto", function() {
     if (!CRYPTO_ENABLED) {
@@ -93,11 +127,11 @@ describe("Crypto", function() {
 
             // unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
             event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
             event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
             event.getForwardingCurve25519KeyChain = () => ["not empty"];
-            event.isKeySourceUntrusted = () => false;
+            event.isKeySourceUntrusted = () => true;
             event.getClaimedEd25519Key =
                 () => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
 
             encryptionInfo = client.getEventEncryptionInfo(event);
             expect(encryptionInfo.encrypted).toBeTruthy();
@@ -231,34 +265,48 @@ describe("Crypto", function() {
     });
 
     describe('Key requests', function() {
         let aliceClient: MatrixClient;
         let bobClient: MatrixClient;
+        let claraClient: MatrixClient;
 
         beforeEach(async function() {
             aliceClient = (new TestClient(
                 "@alice:example.com", "alicedevice",
             )).client;
             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);
             await bobClient.setRoomEncryption(roomId, encryptionCfg);
             const events = [
@@ -300,10 +348,13 @@ describe("Crypto", function() {
                     // 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;
+
             const bobDecryptor = bobClient.crypto.getRoomDecryptor(
                 roomId, olmlib.MEGOLM_ALGORITHM,
             );
 
             const decryptEventsPromise = Promise.all(events.map((ev) => {
@@ -312,10 +363,12 @@ describe("Crypto", function() {
 
             // keyshare the session key starting at the second message, so
             // 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");
             expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
 
@@ -338,12 +391,28 @@ describe("Crypto", function() {
             const decryptEventPromise = awaitEvent(events[0], "Event.decrypted");
             ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
             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();
         });
 
         it("should error if a forwarded room key lacks a content.sender_key", async function() {
             const encryptionCfg = {
@@ -381,10 +450,13 @@ describe("Crypto", function() {
             } 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;
+
             const bobDecryptor = bobClient.crypto.getRoomDecryptor(
                 roomId, olmlib.MEGOLM_ALGORITHM,
             );
 
             const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
@@ -460,10 +532,424 @@ describe("Crypto", function() {
             // the first call to sendToDevice will be the cancellation
             // the second call to sendToDevice will be the key request
             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() {
         it("creates secret storage even if there is no keyInfo", async function() {
             jest.spyOn(logger, 'log').mockImplementation(() => {});
diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts
index b9f16c74..f1bb6228 100644
--- a/spec/unit/crypto/algorithms/megolm.spec.ts
+++ b/spec/unit/crypto/algorithms/megolm.spec.ts
@@ -108,10 +108,16 @@ 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() {
                     return Promise.resolve(decryptedData);
                 },
diff --git a/spec/unit/crypto/backup.spec.ts b/spec/unit/crypto/backup.spec.ts
index 8e264740..acec47fc 100644
--- a/spec/unit/crypto/backup.spec.ts
+++ b/spec/unit/crypto/backup.spec.ts
@@ -212,10 +212,16 @@ describe("MegolmBackup", function() {
             // construct a fake decrypted key event via the use of a mocked
             // 'crypto' implementation.
             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',
                     content: {
                         algorithm: 'm.megolm.v1.aes-sha2',
diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts
index 8a832686..9c8b2538 100644
--- a/spec/unit/crypto/secrets.spec.ts
+++ b/spec/unit/crypto/secrets.spec.ts
@@ -24,10 +24,11 @@ import { encryptAES } from "../../../src/crypto/aes";
 import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
 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
     const crypto = require('crypto');
     utils.setCrypto(crypto);
@@ -255,10 +256,11 @@ describe("Secrets", function() {
                 algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
                 keys: {
                     "ed25519:VAX": vaxDevice.deviceEd25519Key,
                     "curve25519:VAX": vaxDevice.deviceCurve25519Key,
                 },
+                verified: DeviceInfo.DeviceVerification.VERIFIED,
             },
         });
         vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
             "Osborne2": {
                 algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
@@ -278,14 +280,16 @@ describe("Secrets", function() {
         await vax.client.crypto.olmDevice.createOutboundSession(
             osborne2Device.deviceCurve25519Key,
             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();
     });
 
diff --git a/spec/unit/crypto/verification/sas.spec.ts b/spec/unit/crypto/verification/sas.spec.ts
index f82b313f..fc0a0d9b 100644
--- a/spec/unit/crypto/verification/sas.spec.ts
+++ b/spec/unit/crypto/verification/sas.spec.ts
@@ -462,11 +462,11 @@ describe("SAS verification", function() {
                 {
                     verificationMethods: [verificationMethods.SAS],
                 },
             );
 
-            alice.client.setDeviceVerified = jest.fn();
+            alice.client.crypto.setDeviceVerification = jest.fn();
             alice.client.getDeviceEd25519Key = () => {
                 return "alice+base64+ed25519+key";
             };
             alice.client.getStoredDevice = () => {
                 return DeviceInfo.fromStorage(
@@ -480,11 +480,11 @@ describe("SAS verification", function() {
             };
             alice.client.downloadKeys = () => {
                 return Promise.resolve();
             };
 
-            bob.client.setDeviceVerified = jest.fn();
+            bob.client.crypto.setDeviceVerification = jest.fn();
             bob.client.getStoredDevice = () => {
                 return DeviceInfo.fromStorage(
                     {
                         keys: {
                             "ed25519:Osborne2": "alice+base64+ed25519+key",
@@ -563,12 +563,26 @@ describe("SAS verification", function() {
                 aliceVerifier.verify(),
                 bobPromise,
             ]);
 
             // 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" },
+            );
         });
     });
 });
diff --git a/spec/unit/models/beacon.spec.ts b/spec/unit/models/beacon.spec.ts
index c0de3591..49006c78 100644
--- a/spec/unit/models/beacon.spec.ts
+++ b/spec/unit/models/beacon.spec.ts
@@ -20,10 +20,11 @@ import {
     isTimestampInDuration,
     Beacon,
     BeaconEvent,
 } from "../../../src/models/beacon";
 import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
+import { REFERENCE_RELATION } from "matrix-events-sdk";
 
 jest.useFakeTimers();
 
 describe('Beacon', () => {
     describe('isTimestampInDuration()', () => {
@@ -429,10 +430,31 @@ describe('Beacon', () => {
 
                 expect(beacon.latestLocationState).toBeFalsy();
                 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;
                     const beacon = new Beacon(makeBeaconInfoEvent(
                         userId,
diff --git a/src/@types/crypto.ts b/src/@types/crypto.ts
new file mode 100644
index 00000000..3c46d993
--- /dev/null
+++ b/src/@types/crypto.ts
@@ -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;
+};
diff --git a/src/NamespacedValue.ts b/src/NamespacedValue.ts
index 794366f8..9b8b3d40 100644
--- a/src/NamespacedValue.ts
+++ b/src/NamespacedValue.ts
@@ -1,7 +1,7 @@
 /*
-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.
 You may obtain a copy of the License at
 
@@ -12,10 +12,12 @@ 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.
 */
 
+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.
  */
 export class NamespacedValue<S extends string, U extends string> {
@@ -52,11 +54,11 @@ export class NamespacedValue<S extends string, U extends string> {
         return this.name === val || this.altName === val;
     }
 
     // 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];
         }
         if (!val && this.altName) {
diff --git a/src/client.ts b/src/client.ts
index cf7c4508..07fbaece 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -5285,10 +5285,11 @@ 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 {
         return eventMapperFor(this, options || {});
     }
diff --git a/src/content-helpers.ts b/src/content-helpers.ts
index 82404ea6..c8c90dd6 100644
--- a/src/content-helpers.ts
+++ b/src/content-helpers.ts
@@ -290,18 +290,19 @@ export const makeBeaconContent: MakeBeaconContent = (
         event_id: beaconInfoEventId,
     },
 });
 
 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,
     };
 };
diff --git a/src/crypto/OlmDevice.ts b/src/crypto/OlmDevice.ts
index 0b2e616a..cca3b7db 100644
--- a/src/crypto/OlmDevice.ts
+++ b/src/crypto/OlmDevice.ts
@@ -21,10 +21,11 @@ import { logger } from '../logger';
 import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
 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.
 const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
 
@@ -120,10 +121,11 @@ interface IInboundGroupSessionKey {
     chain_index: number;
     key: string;
     forwarding_curve25519_key_chain: string[];
     sender_claimed_ed25519_key: string;
     shared_history: boolean;
+    untrusted: boolean;
 }
 /* eslint-enable camelcase */
 
 /**
  * Manages the olm cryptography functions. Each OlmDevice has a single
@@ -1099,11 +1101,11 @@ export class OlmDevice {
         forwardingCurve25519KeyChain: string[],
         sessionId: string,
         sessionKey: string,
         keysClaimed: Record<string, string>,
         exportFormat: boolean,
-        extraSessionData: Record<string, any> = {},
+        extraSessionData: OlmGroupSessionExtraData = {},
     ): Promise<void> {
         await this.cryptoStore.doTxn(
             'readwrite', [
                 IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
                 IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
@@ -1131,21 +1133,46 @@ export class OlmDevice {
                             if (existingSession) {
                                 logger.log(
                                     "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.
                                 }
                             }
 
                             logger.info(
                                 "Storing megolm session " + senderKey + "/" + sessionId +
@@ -1425,17 +1452,27 @@ export class OlmDevice {
                         const exportedSession = session.export_session(chainIndex);
 
                         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,
                         };
                     },
                 );
             },
             logger.withPrefix("[getInboundGroupSessionKey]"),
diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts
index 0eef2ee7..aeb0d459 100644
--- a/src/crypto/SecretStorage.ts
+++ b/src/crypto/SecretStorage.ts
@@ -537,11 +537,27 @@ export class SecretStorage {
         if (event.getSender() !== this.baseApis.getUserId()) {
             // we shouldn't be receiving secrets from anyone else, so ignore
             // 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) {
             // make sure that the device that sent it is one of the devices that
             // we requested from
@@ -557,10 +573,18 @@ export class SecretStorage {
             }
             if (!requestControl.devices.includes(deviceInfo.deviceId)) {
                 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} ` +
                 `from ${deviceInfo.deviceId}`,
             );
diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts
index 122164f4..2052ee96 100644
--- a/src/crypto/algorithms/megolm.ts
+++ b/src/crypto/algorithms/megolm.ts
@@ -33,12 +33,14 @@ import {
 import { IDecryptedGroupMessage, WITHHELD_MESSAGES } from '../OlmDevice';
 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 {
     const visibilityEvent = room?.currentState?.getStateEvents("m.room.history_visibility", "");
     // NOTE: if the room visibility is unset, it would normally default to
@@ -1187,12 +1189,13 @@ class MegolmEncryption extends EncryptionAlgorithm {
  *
  * @param {object} params parameters, as per
  *     {@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.
     private olmlib = olmlib;
 
@@ -1292,13 +1295,17 @@ class MegolmDecryption extends DecryptionAlgorithm {
                     session: content.sender_key + '|' + content.session_id,
                 },
             );
         }
 
-        // 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);
 
         // belt-and-braces check that the room id matches that indicated by the HS
         // (this is somewhat redundant, since the megolm session is scoped to the
@@ -1389,25 +1396,73 @@ class MegolmDecryption extends DecryptionAlgorithm {
         let senderKey = event.getSenderKey();
         let forwardingKeyChain: string[] = [];
         let exportFormat = false;
         let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
 
+        const extraSessionData: OlmGroupSessionExtraData = {};
+
         if (!content.room_id ||
             !content.session_key ||
             !content.session_id ||
             !content.algorithm
         ) {
             logger.error("key event is missing fields");
             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 : [];
 
             // copy content before we modify it
@@ -1416,11 +1471,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
 
             if (!content.sender_key) {
                 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) {
                 logger.error(
                     `forwarded_room_key_event is missing sender_claimed_ed25519_key field`,
@@ -1429,15 +1483,49 @@ 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;
         }
 
         try {
@@ -1451,11 +1539,11 @@ class MegolmDecryption extends DecryptionAlgorithm {
                 exportFormat,
                 extraSessionData,
             );
 
             // 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
                 // requests in the hopes that someone sends us a key that
                 // includes an earlier index.
@@ -1666,11 +1754,11 @@ class MegolmDecryption extends DecryptionAlgorithm {
      */
     public importRoomKey(
         session: IMegolmSessionData,
         opts: { untrusted?: boolean, source?: string } = {},
     ): Promise<void> {
-        const extraSessionData: any = {};
+        const extraSessionData: OlmGroupSessionExtraData = {};
         if (opts.untrusted || session.untrusted) {
             extraSessionData.untrusted = true;
         }
         if (session["org.matrix.msc3061.shared_history"]) {
             extraSessionData.sharedHistory = true;
@@ -1694,25 +1782,27 @@ class MegolmDecryption extends DecryptionAlgorithm {
                     // since it will have written it to the db and will retry.
                     logger.log("Failed to back up megolm session", e);
                 });
             }
             // 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);
         });
     }
 
     /**
      * Have another go at decrypting events after we receive a key. Resolves once
      * decryption has been re-attempted on all events.
      *
      * @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;
         }
 
@@ -1723,17 +1813,18 @@ class MegolmDecryption extends DecryptionAlgorithm {
 
         logger.debug("Retrying decryption on events", [...pending]);
 
         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);
     }
 
     public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
         const senderPendingEvents = this.pendingEvents.get(senderKey);
diff --git a/src/crypto/algorithms/olm.ts b/src/crypto/algorithms/olm.ts
index aec39d49..38b1c97b 100644
--- a/src/crypto/algorithms/olm.ts
+++ b/src/crypto/algorithms/olm.ts
@@ -220,10 +220,30 @@ class OlmDecryption extends DecryptionAlgorithm {
                     our_key: this.olmDevice.deviceEd25519Key,
                 },
             );
         }
 
+        // 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,
         // which is checked elsewhere).
         if (payload.sender != event.getSender()) {
diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts
index 2f038658..c6c4d66e 100644
--- a/src/crypto/backup.ts
+++ b/src/crypto/backup.ts
@@ -429,11 +429,10 @@ export class BackupManager {
                     (s.device && s.deviceTrust.isVerified()) ||
                         (s.crossSigningId)
                 )
             );
         });
-        ret.usable = ret.usable || ret.trusted_locally;
         return ret;
     }
 
     /**
      * Schedules sending all keys waiting to be sent to the backup, if not already
diff --git a/src/crypto/index.ts b/src/crypto/index.ts
index 65391648..7c0092bf 100644
--- a/src/crypto/index.ts
+++ b/src/crypto/index.ts
@@ -2103,18 +2103,23 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
      *      leave unchanged.
      *
      * @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(
         userId: string,
         deviceId: string,
         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
         if (verified === undefined) verified = null;
         if (blocked === undefined) blocked = null;
@@ -2129,10 +2134,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
                 throw new Error("Cannot set blocked or known for a cross-signing key");
             }
             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);
                 // This will cause our own user trust to change, so emit the event
                 this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
@@ -2189,10 +2198,17 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
 
         const dev = devices[deviceId];
         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;
         }
 
@@ -2398,17 +2414,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
 
         if (!senderKey || !algorithm) {
             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;
         }
 
@@ -2476,12 +2485,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
             ret.encrypted = false;
             return ret as IEncryptedEventInfo;
         }
         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;
         } else {
             ret.authenticated = true;
diff --git a/src/crypto/olmlib.ts b/src/crypto/olmlib.ts
index 9d447844..f108020b 100644
--- a/src/crypto/olmlib.ts
+++ b/src/crypto/olmlib.ts
@@ -28,10 +28,12 @@ import { OlmDevice } from "./OlmDevice";
 import { DeviceInfo } from "./deviceinfo";
 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",
     Megolm = "m.megolm.v1.aes-sha2",
     MegolmBackup = "m.megolm_backup.v1.curve25519-aes-sha2",
@@ -552,10 +554,26 @@ export function pkVerify(obj: IObject, pubKey: string, userId: string) {
         if (unsigned) obj.unsigned = unsigned;
         util.free();
     }
 }
 
+/**
+ * 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.
  * @return {string} The base64.
  */
diff --git a/src/crypto/store/base.ts b/src/crypto/store/base.ts
index 556854c7..6b938aad 100644
--- a/src/crypto/store/base.ts
+++ b/src/crypto/store/base.ts
@@ -23,10 +23,11 @@ import { IRoomEncryption } from "../RoomList";
 import { IDevice } from "../deviceinfo";
 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
  *
  * @module
@@ -125,10 +126,12 @@ export interface CryptoStore {
     addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string, txn?: unknown): void;
     getSharedHistoryInboundGroupSessions(
         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>;
 }
 
@@ -201,5 +204,14 @@ export interface OutgoingRoomKeyRequest {
     cancellationTxnId?: string;
     recipients: IRoomKeyRequestRecipient[];
     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[];
+}
diff --git a/src/crypto/store/indexeddb-crypto-store-backend.ts b/src/crypto/store/indexeddb-crypto-store-backend.ts
index 2611a5f2..a4d46b12 100644
--- a/src/crypto/store/indexeddb-crypto-store-backend.ts
+++ b/src/crypto/store/indexeddb-crypto-store-backend.ts
@@ -23,10 +23,11 @@ import {
     ISession,
     ISessionInfo,
     IWithheld,
     Mode,
     OutgoingRoomKeyRequest,
+    ParkedSharedHistory,
 } from "./base";
 import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
 import { ICrossSigningKey } from "../../client";
 import { IOlmDevice } from "../algorithms/megolm";
 import { IRoomEncryption } from "../RoomList";
@@ -871,10 +872,53 @@ export class Backend implements CryptoStore {
             };
             req.onerror = reject;
         });
     }
 
+    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[],
         func: (txn: IDBTransaction) => T,
         log: PrefixedLogger = logger,
@@ -956,10 +1000,15 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
     if (oldVersion < 10) {
         db.createObjectStore("shared_history_inbound_group_sessions", {
             keyPath: ["roomId"],
         });
     }
+    if (oldVersion < 11) {
+        db.createObjectStore("parked_shared_history", {
+            keyPath: ["roomId"],
+        });
+    }
     // Expand as needed.
 }
 
 function createDatabase(db: IDBDatabase): void {
     const outgoingRoomKeyRequestsStore =
diff --git a/src/crypto/store/indexeddb-crypto-store.ts b/src/crypto/store/indexeddb-crypto-store.ts
index ecc3d86c..21f704fd 100644
--- a/src/crypto/store/indexeddb-crypto-store.ts
+++ b/src/crypto/store/indexeddb-crypto-store.ts
@@ -27,10 +27,11 @@ import {
     ISession,
     ISessionInfo,
     IWithheld,
     Mode,
     OutgoingRoomKeyRequest,
+    ParkedSharedHistory,
 } from "./base";
 import { IRoomKeyRequestBody } from "../index";
 import { ICrossSigningKey } from "../../client";
 import { IOlmDevice } from "../algorithms/megolm";
 import { IRoomEncryption } from "../RoomList";
@@ -53,10 +54,11 @@ export class IndexedDBCryptoStore implements CryptoStore {
     public static STORE_ACCOUNT = 'account';
     public static STORE_SESSIONS = 'sessions';
     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';
 
     public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
@@ -667,10 +669,31 @@ export class IndexedDBCryptoStore implements CryptoStore {
         txn?: IDBTransaction,
     ): Promise<[senderKey: string, sessionId: string][]> {
         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
      * only be called within a callback of either this function or
      * one of the store functions operating on the same transaction.
diff --git a/src/crypto/store/memory-crypto-store.ts b/src/crypto/store/memory-crypto-store.ts
index f62f5225..a0195bb4 100644
--- a/src/crypto/store/memory-crypto-store.ts
+++ b/src/crypto/store/memory-crypto-store.ts
@@ -23,10 +23,11 @@ import {
     ISession,
     ISessionInfo,
     IWithheld,
     Mode,
     OutgoingRoomKeyRequest,
+    ParkedSharedHistory,
 } from "./base";
 import { IRoomKeyRequestBody } from "../index";
 import { ICrossSigningKey } from "../../client";
 import { IOlmDevice } from "../algorithms/megolm";
 import { IRoomEncryption } from "../RoomList";
@@ -56,10 +57,11 @@ export class MemoryCryptoStore implements CryptoStore {
     // Opaque device data object
     private deviceData: IDeviceData = null;
     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.
      *
      * This must be called before the store can be used.
@@ -524,10 +526,22 @@ export class MemoryCryptoStore implements CryptoStore {
 
     public getSharedHistoryInboundGroupSessions(roomId: string): Promise<[senderKey: string, sessionId: string][]> {
         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> {
         return Promise.resolve(func(null));
     }
diff --git a/src/crypto/verification/Base.ts b/src/crypto/verification/Base.ts
index 35100499..ddf38f8c 100644
--- a/src/crypto/verification/Base.ts
+++ b/src/crypto/verification/Base.ts
@@ -297,38 +297,44 @@ 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;
     }
 
     protected doVerification?: () => Promise<void>;
 
     protected async verifyKeys(userId: string, keys: Record<string, string>, verifier: KeyVerifier): Promise<void> {
         // 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) {
                     verifier(keyId, DeviceInfo.fromStorage({
                         keys: {
                             [keyId]: deviceId,
                         },
                     }, deviceId), keyInfo);
-                    verifiedDevices.push(deviceId);
+                    verifiedDevices.push([deviceId, keyId, deviceId]);
                 } else {
                     logger.warn(
                         `verification: Could not find device ${deviceId} to verify`,
                     );
                 }
@@ -346,12 +352,19 @@ export class VerificationBase<
             verifiedDevices,
         );
         // 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();
         }
     }
 
     public get events(): string[] | undefined {
         return undefined;
diff --git a/src/event-mapper.ts b/src/event-mapper.ts
index 40ef5a82..38ee4267 100644
--- a/src/event-mapper.ts
+++ b/src/event-mapper.ts
@@ -20,17 +20,22 @@ import { IEvent, MatrixEvent, MatrixEventEvent } from "./models/event";
 export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
 
 export interface MapperOpts {
     preventReEmit?: boolean;
     decrypt?: boolean;
+    toDevice?: boolean;
 }
 
 export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
     let preventReEmit = Boolean(options.preventReEmit);
     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;
         // If the event is already known to the room, let's re-use the model rather than duplicating.
         // We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour.
diff --git a/src/models/beacon.ts b/src/models/beacon.ts
index 25abf284..92e95079 100644
--- a/src/models/beacon.ts
+++ b/src/models/beacon.ts
@@ -13,11 +13,10 @@ 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.
 */
 
 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";
 import { TypedEventEmitter } from "./typed-event-emitter";
 
@@ -159,11 +158,13 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
             return;
         }
 
         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) &&
                 // ignore positions older than our current latest location
                 (!this.latestLocationState || timestamp > this.latestLocationState.timestamp)
diff --git a/src/models/event.ts b/src/models/event.ts
index c56bedd2..07113497 100644
--- a/src/models/event.ts
+++ b/src/models/event.ts
@@ -149,10 +149,11 @@ interface IKeyRequestRecipient {
 }
 
 export interface IDecryptOptions {
     emit?: boolean;
     isRetry?: boolean;
+    keyTrusted?: boolean;
 }
 
 /**
  * Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
  */
@@ -693,11 +694,11 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
         // start with a couple of sanity checks.
         if (!this.isEncrypted()) {
             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",
             );
         }
diff --git a/src/sync.ts b/src/sync.ts
index 396b5ba4..72a8e7f8 100644
--- a/src/sync.ts
+++ b/src/sync.ts
@@ -1107,11 +1107,24 @@ export class SyncApi {
 
         // handle to-device events
         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
                     // we pull out the transaction IDs from the cancellation events
                     // so we can flag the verification events as cancelled in the loop
@@ -1183,10 +1196,28 @@ export class SyncApi {
         await utils.promiseMapSeries(inviteRooms, async (inviteObj) => {
             const room = inviteObj.room;
             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);
                 client.emit(ClientEvent.Room, room);
             } else {
@@ -1286,11 +1317,15 @@ export class SyncApi {
                     // reset the timeline.
                     client.resetNotifTimelineSet();
                 }
             }
 
-            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
             // which needs the room state to be up to date
             if (joinObj.summary) {

Vulnerable code:             aliceTestClient.httpBackend.flush('/keys/query', 1),
            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,
            event.isKeySourceUntrusted = () => false;
        it("does not cancel keyshare requests if some messages are not decrypted", async function() {
            // the room key request should be gone since we've now decrypted everything
        const secret = await request.promise;
        expect(secret).toBe("bar");
            alice.client.setDeviceVerified = jest.fn();
            bob.client.setDeviceVerified = jest.fn();
            expect(alice.client.setDeviceVerified)
                .toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
            expect(bob.client.setDeviceVerified)
                .toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
Copyright 2021 The Matrix.org Foundation C.I.C.
    public findIn<T>(obj: any): T {
    timestamp: number;
    const { description, uri } = M_LOCATION.findIn<MLocationContent>(content);
        description,
        uri,
        extraSessionData: Record<string, any> = {},
                                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;
                            "forwarding_curve25519_key_chain":
                                sessionData.forwardingCurve25519KeyChain || [],
import { MatrixEvent } from "../..";
    // events which we couldn't decrypt due to unknown sessions / indexes: map from
    // senderKey|sessionId to Set of MatrixEvents
        // success. We can remove the event from the pending list, if that hasn't
        // already happened.
        this.removeEventFromPendingList(event);
        if (!senderKey) {
            logger.error("key event has no sender key (not encrypted?)");
            senderKey = content.sender_key;
        const extraSessionData: any = {};
            if (await this.retryDecryption(senderKey, content.session_id)) {
        const extraSessionData: any = {};
            this.retryDecryption(session.sender_key, session.session_id);
     * @return {Boolean} whether all messages were successfully decrypted
    private async retryDecryption(senderKey: string, sessionId: string): Promise<boolean> {
                await ev.attemptDecryption(this.crypto, { isRetry: true });
        // If decrypted successfully, they'll have been removed from pendingEvents
        ret.usable = ret.usable || ret.trusted_locally;
        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;
        }

        const forwardingChain = event.getForwardingCurve25519KeyChain();
        if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
            Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
        const verifiedDevices = [];
                verifiedDevices.push(deviceId);
                    verifiedDevices.push(deviceId);
        for (const deviceId of verifiedDevices) {
            await this.baseApis.setDeviceVerified(userId, deviceId);
import { M_TIMESTAMP } from "../@types/location";
            const timestamp = M_TIMESTAMP.findIn<number>(content);
        if (this.clearEvent && !this.isDecryptionFailure()) {
                .map(client.getEventMapper())
            await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache);
Fixed code:         bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
        bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
        bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
        bobTestClient.httpBackend.when('POST', '/keys/query').respond(
            200, {},
        );
            await client.initCrypto();

    IndexedDBCryptoStore,
    Room,
import { DeviceInfo } from "../../src/crypto/deviceinfo";
        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";

        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";

        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";

        aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
            200, getTestKeysQueryResponse('@bob:xyz'),
        );
        aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
            200, getTestKeysQueryResponse('@bob:xyz'),
        );
            aliceTestClient.httpBackend.flush('/keys/query', 2),
        aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
            200, getTestKeysQueryResponse('@bob:xyz'),
        );
        aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
            200, getTestKeysQueryResponse('@bob:xyz'),
        );

        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";

        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";

        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";

            aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
            aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";


    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();
    });
    parseBeaconContent,

    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,
            });
        });
    });
import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager';
import { RoomMember } from '../../src/models/room-member';
            "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,
    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",
        };
    };
            event.isKeySourceUntrusted = () => true;
        let claraClient: MatrixClient;
            claraClient = (new TestClient(
                "@clara:example.com", "claradevice",
            )).client;
            await claraClient.initCrypto();
            claraClient.stopClient();
        it("does not cancel keyshare requests until all messages are decrypted with trusted keys", async function() {
            // 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",
            })]);
            const device = new DeviceInfo(aliceClient.deviceId);
            bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;

            bobClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
            bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
            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();
            // now the room key request should be gone, since there's
            // no better key to wait for
            const device = new DeviceInfo(aliceClient.deviceId);
            bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;


        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"],
            }]);
        });
            event.getWireType = () => "m.room.encrypted";
            event.getWireContent = () => {
                return {
                    algorithm: "m.olm.v1.curve25519-aes-sha2",
                };
            };
            event.getWireType = () => "m.room.encrypted";
            event.getWireContent = () => {
                return {
                    algorithm: "m.olm.v1.curve25519-aes-sha2",
                };
            };
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
                verified: DeviceInfo.DeviceVerification.VERIFIED,
        osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
        osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";

        await request.promise; // return value not used
            alice.client.crypto.setDeviceVerification = jest.fn();
            bob.client.crypto.setDeviceVerification = jest.fn();
            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" },
            );
import { REFERENCE_RELATION } from "matrix-events-sdk";
            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();
            });

/*
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;
};
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
import { Optional } from "matrix-events-sdk/lib/types";

    public findIn<T>(obj: any): Optional<T> {
     * @param {boolean} options.toDevice the event is a to_device event
    uri?: string; // override from MLocationContent to allow optionals
    timestamp?: number;
    const location = M_LOCATION.findIn<MLocationContent>(content);
        description: location?.description,
        uri: location?.uri,
import { OlmGroupSessionExtraData } from "../@types/crypto";
    untrusted: boolean;
        extraSessionData: OlmGroupSessionExtraData = {},
                                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.
                        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;

                            "forwarding_curve25519_key_chain": forwardingKeyChain,
                            "untrusted": untrusted,

        if (!olmlib.isOlmEncrypted(event)) {
            logger.error("secret event not properly encrypted");
            return;
        }


        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;
        }

            // 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;
            }
import { MatrixEvent } from "../../models/event";
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
import { OlmGroupSessionExtraData } from "../../@types/crypto";
    // 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
        // 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 extraSessionData: OlmGroupSessionExtraData = {};

        if (!olmlib.isOlmEncrypted(event)) {
            logger.error("key event not properly encrypted");
        if (content["org.matrix.msc3061.shared_history"]) {
            extraSessionData.sharedHistory = true;
        }

            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;
                }
            }


            // 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;
            if (await this.retryDecryption(senderKey, content.session_id, !extraSessionData.untrusted)) {
        const extraSessionData: OlmGroupSessionExtraData = {};
            this.retryDecryption(session.sender_key, session.session_id, !extraSessionData.untrusted);
     * @param {Boolean} keyTrusted
     * @return {Boolean} whether all messages were successfully
     *     decrypted with trusted keys
    private async retryDecryption(senderKey: string, sessionId: string, keyTrusted?: boolean): Promise<boolean> {
                await ev.attemptDecryption(this.crypto, { isRetry: true, keyTrusted });
        // If decrypted successfully with trusted keys, they'll have
        // been removed from pendingEvents
        // 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,
                },
            );
        }

     * @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.
     *
        keys?: Record<string, string>,
            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 (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]}`);
                    }
                }
            }
        if (event.isKeySourceUntrusted()) {
import { MatrixEvent } from "../models/event";
import { EventType } from "../@types/event";
/**
 * 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;
}

import { MatrixEvent } from "../../models/event";
    addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
    takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;

export interface ParkedSharedHistory {
    senderId: string;
    senderKey: string;
    sessionId: string;
    sessionKey: string;
    keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; // XXX: Less type dependence on MatrixEvent
    forwardingCurve25519KeyChain: string[];
}
    ParkedSharedHistory,
    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;
        });
    }

    if (oldVersion < 11) {
        db.createObjectStore("parked_shared_history", {
            keyPath: ["roomId"],
        });
    }
    ParkedSharedHistory,
    public static STORE_PARKED_SHARED_HISTORY = 'parked_shared_history';
    /**
     * 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);
    }

    ParkedSharedHistory,
    private parkedSharedHistory = new Map<string, ParkedSharedHistory[]>(); // keyed by room ID
    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);
    }

            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));
        const verifiedDevices: [string, string, string][] = [];
                verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
                    verifiedDevices.push([deviceId, keyId, 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();
    toDevice?: boolean;
        if (options.toDevice) {
            delete plainOldJsObject.room_id;
        }

            const parsed = parseBeaconContent(content);
            if (!parsed.uri || !parsed.timestamp) return false; // we won't be able to process these
            const { timestamp } = parsed;
    keyTrusted?: boolean;
        if (this.clearEvent && !this.isDecryptionFailure() && !(this.isKeySourceUntrusted() && options.keyTrusted)) {
                .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 }))

            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 },
                    );
                }
            }

            try {
                await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache);
            } catch (e) {
                logger.error(`Failed to process events on room ${room.roomId}:`, e);
            }