VYPR
High severityNVD Advisory· Published Sep 28, 2022· Updated Apr 23, 2025

Matrix Javascript SDK vulnerable to impersonation via forwarded Megolm sessions

CVE-2022-39249

Description

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

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
matrix-js-sdknpm
< 19.7.019.7.0

Affected products

1

Patches

1
a587d7c36026

Resolve multiple CVEs

https://github.com/matrix-org/matrix-js-sdkRiotRobotSep 28, 2022via ghsa
30 files changed · +1375 79
  • spec/integ/matrix-client-crypto.spec.ts+6 0 modified
    @@ -494,6 +494,7 @@ describe("MatrixClient crypto", () => {
             aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
             await aliTestClient.start();
             await bobTestClient.start();
    +        bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
             await firstSync(aliTestClient);
             await aliEnablesEncryption();
             await aliSendsFirstMessage();
    @@ -504,6 +505,7 @@ describe("MatrixClient crypto", () => {
             aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
             await aliTestClient.start();
             await bobTestClient.start();
    +        bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
             await firstSync(aliTestClient);
             await aliEnablesEncryption();
             await aliSendsFirstMessage();
    @@ -567,6 +569,7 @@ describe("MatrixClient crypto", () => {
             aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
             await aliTestClient.start();
             await bobTestClient.start();
    +        bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
             await firstSync(aliTestClient);
             await aliEnablesEncryption();
             await aliSendsFirstMessage();
    @@ -584,6 +587,9 @@ describe("MatrixClient crypto", () => {
             await firstSync(bobTestClient);
             await aliEnablesEncryption();
             await aliSendsFirstMessage();
    +        bobTestClient.httpBackend.when('POST', '/keys/query').respond(
    +            200, {},
    +        );
             await bobRecvMessage();
             await bobEnablesEncryption();
             const ciphertext = await bobSendsReplyMessage();
    
  • spec/integ/matrix-client-syncing.spec.ts+2 0 modified
    @@ -87,6 +87,8 @@ describe("MatrixClient syncing", () => {
             });
     
             it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
    +            await client.initCrypto();
    +
                 const roomId = "!cycles:example.org";
     
                 // First sync: an invite
    
  • spec/integ/megolm-integ.spec.ts+316 1 modified
    @@ -29,8 +29,11 @@ import {
         IDownloadKeyResult,
         MatrixEvent,
         MatrixEventEvent,
    +    IndexedDBCryptoStore,
    +    Room,
     } from "../../src/matrix";
     import { IDeviceKeys } from "../../src/crypto/dehydration";
    +import { DeviceInfo } from "../../src/crypto/deviceinfo";
     
     const ROOM_ID = "!room:id";
     
    @@ -280,10 +283,13 @@ describe("megolm", () => {
     
         it("Alice receives a megolm message", async () => {
             await aliceTestClient.start();
    +        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
             const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
             const groupSession = new Olm.OutboundGroupSession();
             groupSession.create();
     
    +        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
    +
             // make the room_key event
             const roomKeyEncrypted = encryptGroupSessionKey({
                 senderKey: testSenderKey,
    @@ -326,10 +332,13 @@ describe("megolm", () => {
         it("Alice receives a megolm message before the session keys", async () => {
             // https://github.com/vector-im/element-web/issues/2273
             await aliceTestClient.start();
    +        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
             const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
             const groupSession = new Olm.OutboundGroupSession();
             groupSession.create();
     
    +        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
    +
             // make the room_key event, but don't send it yet
             const roomKeyEncrypted = encryptGroupSessionKey({
                 senderKey: testSenderKey,
    @@ -383,10 +392,13 @@ describe("megolm", () => {
     
         it("Alice gets a second room_key message", async () => {
             await aliceTestClient.start();
    +        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
             const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
             const groupSession = new Olm.OutboundGroupSession();
             groupSession.create();
     
    +        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
    +
             // make the room_key event
             const roomKeyEncrypted1 = encryptGroupSessionKey({
                 senderKey: testSenderKey,
    @@ -468,6 +480,9 @@ describe("megolm", () => {
             aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
                 200, getTestKeysQueryResponse('@bob:xyz'),
             );
    +        aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
    +            200, getTestKeysQueryResponse('@bob:xyz'),
    +        );
     
             await Promise.all([
                 aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
    @@ -541,13 +556,16 @@ describe("megolm", () => {
     
             logger.log('Forcing alice to download our device keys');
     
    +        aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
    +            200, getTestKeysQueryResponse('@bob:xyz'),
    +        );
             aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
                 200, getTestKeysQueryResponse('@bob:xyz'),
             );
     
             await Promise.all([
                 aliceTestClient.client.downloadKeys(['@bob:xyz']),
    -            aliceTestClient.httpBackend.flush('/keys/query', 1),
    +            aliceTestClient.httpBackend.flush('/keys/query', 2),
             ]);
     
             logger.log('Telling alice to block our device');
    @@ -592,6 +610,9 @@ describe("megolm", () => {
     
             logger.log("Fetching bob's devices and marking known");
     
    +        aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
    +            200, getTestKeysQueryResponse('@bob:xyz'),
    +        );
             aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
                 200, getTestKeysQueryResponse('@bob:xyz'),
             );
    @@ -786,6 +807,10 @@ describe("megolm", () => {
             logger.log('Forcing alice to download our device keys');
             const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
     
    +        aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
    +            200, getTestKeysQueryResponse('@bob:xyz'),
    +        );
    +
             // so will this.
             const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
                 .then(() => {
    @@ -805,9 +830,12 @@ describe("megolm", () => {
         it("Alice exports megolm keys and imports them to a new device", async () => {
             aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
             await aliceTestClient.start();
    +        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
             // establish an olm session with alice
             const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
     
    +        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
    +
             const groupSession = new Olm.OutboundGroupSession();
             groupSession.create();
     
    @@ -855,6 +883,8 @@ describe("megolm", () => {
             await aliceTestClient.client.importRoomKeys(exported);
             await aliceTestClient.start();
     
    +        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
    +
             const syncResponse = {
                 next_batch: 1,
                 rooms: {
    @@ -927,10 +957,13 @@ describe("megolm", () => {
     
         it("Alice can decrypt a message with falsey content", async () => {
             await aliceTestClient.start();
    +        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
             const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
             const groupSession = new Olm.OutboundGroupSession();
             groupSession.create();
     
    +        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
    +
             // make the room_key event
             const roomKeyEncrypted = encryptGroupSessionKey({
                 senderKey: testSenderKey,
    @@ -985,10 +1018,13 @@ describe("megolm", () => {
             "should successfully decrypt bundled redaction events that don't include a room_id in their /sync data",
             async () => {
                 await aliceTestClient.start();
    +            aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
                 const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
                 const groupSession = new Olm.OutboundGroupSession();
                 groupSession.create();
     
    +            aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
    +
                 // make the room_key event
                 const roomKeyEncrypted = encryptGroupSessionKey({
                     senderKey: testSenderKey,
    @@ -1045,4 +1081,283 @@ describe("megolm", () => {
                 expect(redactionEvent.content.reason).toEqual("redaction test");
             },
         );
    +
    +    it("Alice receives shared history before being invited to a room by the sharer", async () => {
    +        const beccaTestClient = new TestClient(
    +            "@becca:localhost", "foobar", "bazquux",
    +        );
    +        await beccaTestClient.client.initCrypto();
    +
    +        await aliceTestClient.start();
    +        aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
    +        await beccaTestClient.start();
    +
    +        const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
    +        beccaTestClient.client.store.storeRoom(beccaRoom);
    +        await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
    +
    +        const event = new MatrixEvent({
    +            type: "m.room.message",
    +            sender: "@becca:localhost",
    +            room_id: ROOM_ID,
    +            event_id: "$1",
    +            content: {
    +                msgtype: "m.text",
    +                body: "test message",
    +            },
    +        });
    +
    +        await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom);
    +        // remove keys from the event
    +        // @ts-ignore private properties
    +        event.clearEvent = undefined;
    +        // @ts-ignore private properties
    +        event.senderCurve25519Key = null;
    +        // @ts-ignore private properties
    +        event.claimedEd25519Key = null;
    +
    +        const device = new DeviceInfo(beccaTestClient.client.deviceId);
    +        aliceTestClient.client.crypto.deviceList.getDeviceByIdentityKey = () => device;
    +        aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId();
    +
    +        // Create an olm session for Becca and Alice's devices
    +        const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
    +        const aliceOtkId = Object.keys(aliceOtks)[0];
    +        const aliceOtk = aliceOtks[aliceOtkId];
    +        const p2pSession = new global.Olm.Session();
    +        await beccaTestClient.client.crypto.cryptoStore.doTxn(
    +            'readonly',
    +            [IndexedDBCryptoStore.STORE_ACCOUNT],
    +            (txn) => {
    +                beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => {
    +                    const account = new global.Olm.Account();
    +                    try {
    +                        account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount);
    +                        p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
    +                    } finally {
    +                        account.free();
    +                    }
    +                });
    +            },
    +        );
    +
    +        const content = event.getWireContent();
    +        const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey(
    +            ROOM_ID,
    +            content.sender_key,
    +            content.session_id,
    +        );
    +        const encryptedForwardedKey = encryptOlmEvent({
    +            sender: "@becca:localhost",
    +            senderKey: beccaTestClient.getDeviceKey(),
    +            recipient: aliceTestClient,
    +            p2pSession: p2pSession,
    +            plaincontent: {
    +                "algorithm": 'm.megolm.v1.aes-sha2',
    +                "room_id": ROOM_ID,
    +                "sender_key": content.sender_key,
    +                "sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
    +                "session_id": content.session_id,
    +                "session_key": groupSessionKey.key,
    +                "chain_index": groupSessionKey.chain_index,
    +                "forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
    +                "org.matrix.msc3061.shared_history": true,
    +            },
    +            plaintype: 'm.forwarded_room_key',
    +        });
    +
    +        // Alice receives shared history
    +        aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
    +            next_batch: 1,
    +            to_device: { events: [encryptedForwardedKey] },
    +        });
    +        await aliceTestClient.flushSync();
    +
    +        // Alice is invited to the room by Becca
    +        aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
    +            next_batch: 2,
    +            rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
    +                {
    +                    sender: '@becca:localhost',
    +                    type: 'm.room.encryption',
    +                    state_key: '',
    +                    content: {
    +                        algorithm: 'm.megolm.v1.aes-sha2',
    +                    },
    +                },
    +                {
    +                    sender: '@becca:localhost',
    +                    type: 'm.room.member',
    +                    state_key: '@alice:localhost',
    +                    content: {
    +                        membership: 'invite',
    +                    },
    +                },
    +            ] } } } },
    +        });
    +        await aliceTestClient.flushSync();
    +
    +        // Alice has joined the room
    +        aliceTestClient.httpBackend.when("GET", "/sync").respond(
    +            200, getSyncResponse(["@alice:localhost", "@becca:localhost"]),
    +        );
    +        await aliceTestClient.flushSync();
    +
    +        aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
    +            next_batch: 4,
    +            rooms: {
    +                join: {
    +                    [ROOM_ID]: { timeline: { events: [event.event] } },
    +                },
    +            },
    +        });
    +        await aliceTestClient.flushSync();
    +
    +        const room = aliceTestClient.client.getRoom(ROOM_ID);
    +        const roomEvent = room.getLiveTimeline().getEvents()[0];
    +        expect(roomEvent.isEncrypted()).toBe(true);
    +        const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
    +        expect(decryptedEvent.getContent().body).toEqual('test message');
    +
    +        await beccaTestClient.stop();
    +    });
    +
    +    it("Alice receives shared history before being invited to a room by someone else", async () => {
    +        const beccaTestClient = new TestClient(
    +            "@becca:localhost", "foobar", "bazquux",
    +        );
    +        await beccaTestClient.client.initCrypto();
    +
    +        await aliceTestClient.start();
    +        await beccaTestClient.start();
    +
    +        const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
    +        beccaTestClient.client.store.storeRoom(beccaRoom);
    +        await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
    +
    +        const event = new MatrixEvent({
    +            type: "m.room.message",
    +            sender: "@becca:localhost",
    +            room_id: ROOM_ID,
    +            event_id: "$1",
    +            content: {
    +                msgtype: "m.text",
    +                body: "test message",
    +            },
    +        });
    +
    +        await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom);
    +        // remove keys from the event
    +        // @ts-ignore private properties
    +        event.clearEvent = undefined;
    +        // @ts-ignore private properties
    +        event.senderCurve25519Key = null;
    +        // @ts-ignore private properties
    +        event.claimedEd25519Key = null;
    +
    +        const device = new DeviceInfo(beccaTestClient.client.deviceId);
    +        aliceTestClient.client.crypto.deviceList.getDeviceByIdentityKey = () => device;
    +
    +        // Create an olm session for Becca and Alice's devices
    +        const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
    +        const aliceOtkId = Object.keys(aliceOtks)[0];
    +        const aliceOtk = aliceOtks[aliceOtkId];
    +        const p2pSession = new global.Olm.Session();
    +        await beccaTestClient.client.crypto.cryptoStore.doTxn(
    +            'readonly',
    +            [IndexedDBCryptoStore.STORE_ACCOUNT],
    +            (txn) => {
    +                beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => {
    +                    const account = new global.Olm.Account();
    +                    try {
    +                        account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount);
    +                        p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
    +                    } finally {
    +                        account.free();
    +                    }
    +                });
    +            },
    +        );
    +
    +        const content = event.getWireContent();
    +        const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey(
    +            ROOM_ID,
    +            content.sender_key,
    +            content.session_id,
    +        );
    +        const encryptedForwardedKey = encryptOlmEvent({
    +            sender: "@becca:localhost",
    +            senderKey: beccaTestClient.getDeviceKey(),
    +            recipient: aliceTestClient,
    +            p2pSession: p2pSession,
    +            plaincontent: {
    +                "algorithm": 'm.megolm.v1.aes-sha2',
    +                "room_id": ROOM_ID,
    +                "sender_key": content.sender_key,
    +                "sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
    +                "session_id": content.session_id,
    +                "session_key": groupSessionKey.key,
    +                "chain_index": groupSessionKey.chain_index,
    +                "forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
    +                "org.matrix.msc3061.shared_history": true,
    +            },
    +            plaintype: 'm.forwarded_room_key',
    +        });
    +
    +        // Alice receives forwarded history from Becca
    +        aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
    +            next_batch: 1,
    +            to_device: { events: [encryptedForwardedKey] },
    +        });
    +        await aliceTestClient.flushSync();
    +
    +        // Alice is invited to the room by Charlie
    +        aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
    +            next_batch: 2,
    +            rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
    +                {
    +                    sender: '@becca:localhost',
    +                    type: 'm.room.encryption',
    +                    state_key: '',
    +                    content: {
    +                        algorithm: 'm.megolm.v1.aes-sha2',
    +                    },
    +                },
    +                {
    +                    sender: '@charlie:localhost',
    +                    type: 'm.room.member',
    +                    state_key: '@alice:localhost',
    +                    content: {
    +                        membership: 'invite',
    +                    },
    +                },
    +            ] } } } },
    +        });
    +        await aliceTestClient.flushSync();
    +
    +        // Alice has joined the room
    +        aliceTestClient.httpBackend.when("GET", "/sync").respond(
    +            200, getSyncResponse(["@alice:localhost", "@becca:localhost", "@charlie:localhost"]),
    +        );
    +        await aliceTestClient.flushSync();
    +
    +        aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
    +            next_batch: 4,
    +            rooms: {
    +                join: {
    +                    [ROOM_ID]: { timeline: { events: [event.event] } },
    +                },
    +            },
    +        });
    +        await aliceTestClient.flushSync();
    +
    +        // Decryption should fail, because Alice hasn't received any keys she can trust
    +        const room = aliceTestClient.client.getRoom(ROOM_ID);
    +        const roomEvent = room.getLiveTimeline().getEvents()[0];
    +        expect(roomEvent.isEncrypted()).toBe(true);
    +        const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
    +        expect(decryptedEvent.isDecryptionFailure()).toBe(true);
    +
    +        await beccaTestClient.stop();
    +    });
     });
    
  • spec/unit/content-helpers.spec.ts+61 0 modified
    @@ -22,6 +22,7 @@ import {
         makeBeaconContent,
         makeBeaconInfoContent,
         makeTopicContent,
    +    parseBeaconContent,
         parseTopicContent,
     } from "../../src/content-helpers";
     
    @@ -127,6 +128,66 @@ describe('Beacon content helpers', () => {
                 });
             });
         });
    +
    +    describe("parseBeaconContent()", () => {
    +        it("should not explode when parsing an invalid beacon", () => {
    +            // deliberate cast to simulate wire content being invalid
    +            const result = parseBeaconContent({} as any);
    +            expect(result).toEqual({
    +                description: undefined,
    +                uri: undefined,
    +                timestamp: undefined,
    +            });
    +        });
    +
    +        it("should parse unstable values", () => {
    +            const uri = "urigoeshere";
    +            const description = "descriptiongoeshere";
    +            const timestamp = 1234;
    +            const result = parseBeaconContent({
    +                "org.matrix.msc3488.location": {
    +                    uri,
    +                    description,
    +                },
    +                "org.matrix.msc3488.ts": timestamp,
    +
    +                // relationship not used - just here to satisfy types
    +                "m.relates_to": {
    +                    rel_type: "m.reference",
    +                    event_id: "$unused",
    +                },
    +            });
    +            expect(result).toEqual({
    +                description,
    +                uri,
    +                timestamp,
    +            });
    +        });
    +
    +        it("should parse stable values", () => {
    +            const uri = "urigoeshere";
    +            const description = "descriptiongoeshere";
    +            const timestamp = 1234;
    +            const result = parseBeaconContent({
    +                "m.location": {
    +                    uri,
    +                    description,
    +                },
    +                "m.ts": timestamp,
    +
    +                // relationship not used - just here to satisfy types
    +                "m.relates_to": {
    +                    rel_type: "m.reference",
    +                    event_id: "$unused",
    +                },
    +            });
    +            expect(result).toEqual({
    +                description,
    +                uri,
    +                timestamp,
    +            });
    +        });
    +    });
     });
     
     describe('Topic content helpers', () => {
    
  • spec/unit/crypto/algorithms/megolm.spec.ts+6 0 modified
    @@ -110,6 +110,12 @@ describe("MegolmDecryption", function() {
                     senderCurve25519Key: "SENDER_CURVE25519",
                     claimedEd25519Key: "SENDER_ED25519",
                 };
    +            event.getWireType = () => "m.room.encrypted";
    +            event.getWireContent = () => {
    +                return {
    +                    algorithm: "m.olm.v1.curve25519-aes-sha2",
    +                };
    +            };
     
                 const mockCrypto = {
                     decryptEvent: function() {
    
  • spec/unit/crypto/backup.spec.ts+6 0 modified
    @@ -214,6 +214,12 @@ describe("MegolmBackup", function() {
                 const event = new MatrixEvent({
                     type: 'm.room.encrypted',
                 });
    +            event.getWireType = () => "m.room.encrypted";
    +            event.getWireContent = () => {
    +                return {
    +                    algorithm: "m.olm.v1.curve25519-aes-sha2",
    +                };
    +            };
                 const decryptedData = {
                     clearEvent: {
                         type: 'm.room_key',
    
  • spec/unit/crypto/secrets.spec.ts+6 2 modified
    @@ -26,6 +26,7 @@ import { logger } from '../../../src/logger';
     import * as utils from "../../../src/utils";
     import { ICreateClientOpts } from '../../../src/client';
     import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
    +import { DeviceInfo } from '../../../src/crypto/deviceinfo';
     
     try {
         // eslint-disable-next-line @typescript-eslint/no-var-requires
    @@ -257,6 +258,7 @@ describe("Secrets", function() {
                         "ed25519:VAX": vaxDevice.deviceEd25519Key,
                         "curve25519:VAX": vaxDevice.deviceCurve25519Key,
                     },
    +                verified: DeviceInfo.DeviceVerification.VERIFIED,
                 },
             });
             vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
    @@ -280,10 +282,12 @@ describe("Secrets", function() {
                 Object.values(otks)[0],
             );
     
    +        osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
    +        osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
    +
             const request = await secretStorage.request("foo", ["VAX"]);
    -        const secret = await request.promise;
    +        await request.promise; // return value not used
     
    -        expect(secret).toBe("bar");
             osborne2.stop();
             vax.stop();
             clearTestClientTimeouts();
    
  • spec/unit/crypto.spec.ts+498 12 modified
    @@ -15,6 +15,8 @@ import { CRYPTO_ENABLED } from "../../src/client";
     import { DeviceInfo } from "../../src/crypto/deviceinfo";
     import { logger } from '../../src/logger';
     import { MemoryStore } from "../../src";
    +import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager';
    +import { RoomMember } from '../../src/models/room-member';
     import { IStore } from '../../src/store';
     
     const Olm = global.Olm;
    @@ -40,20 +42,52 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
             type: "m.forwarded_room_key",
             sender: client.getUserId(),
             content: {
    -            algorithm: olmlib.MEGOLM_ALGORITHM,
    -            room_id: roomId,
    -            sender_key: eventContent.sender_key,
    -            sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
    -            session_id: eventContent.session_id,
    -            session_key: key.key,
    -            chain_index: key.chain_index,
    -            forwarding_curve25519_key_chain:
    -            key.forwarding_curve_key_chain,
    +            "algorithm": olmlib.MEGOLM_ALGORITHM,
    +            "room_id": roomId,
    +            "sender_key": eventContent.sender_key,
    +            "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
    +            "session_id": eventContent.session_id,
    +            "session_key": key.key,
    +            "chain_index": key.chain_index,
    +            "forwarding_curve25519_key_chain": key.forwarding_curve_key_chain,
    +            "org.matrix.msc3061.shared_history": true,
             },
         });
         // make onRoomKeyEvent think this was an encrypted event
         // @ts-ignore private property
         ksEvent.senderCurve25519Key = "akey";
    +    ksEvent.getWireType = () => "m.room.encrypted";
    +    ksEvent.getWireContent = () => {
    +        return {
    +            algorithm: "m.olm.v1.curve25519-aes-sha2",
    +        };
    +    };
    +    return ksEvent;
    +}
    +
    +function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent {
    +    const roomId = event.getRoomId();
    +    const eventContent = event.getWireContent();
    +    const key = client.crypto.olmDevice.getOutboundGroupSessionKey(eventContent.session_id);
    +    const ksEvent = new MatrixEvent({
    +        type: "m.room_key",
    +        sender: client.getUserId(),
    +        content: {
    +            "algorithm": olmlib.MEGOLM_ALGORITHM,
    +            "room_id": roomId,
    +            "session_id": eventContent.session_id,
    +            "session_key": key.key,
    +        },
    +    });
    +    // make onRoomKeyEvent think this was an encrypted event
    +    // @ts-ignore private property
    +    ksEvent.senderCurve25519Key = event.getSenderKey();
    +    ksEvent.getWireType = () => "m.room.encrypted";
    +    ksEvent.getWireContent = () => {
    +        return {
    +            algorithm: "m.olm.v1.curve25519-aes-sha2",
    +        };
    +    };
         return ksEvent;
     }
     
    @@ -95,7 +129,7 @@ describe("Crypto", function() {
                 event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
                 event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
                 event.getForwardingCurve25519KeyChain = () => ["not empty"];
    -            event.isKeySourceUntrusted = () => false;
    +            event.isKeySourceUntrusted = () => true;
                 event.getClaimedEd25519Key =
                     () => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
     
    @@ -233,6 +267,7 @@ describe("Crypto", function() {
         describe('Key requests', function() {
             let aliceClient: MatrixClient;
             let bobClient: MatrixClient;
    +        let claraClient: MatrixClient;
     
             beforeEach(async function() {
                 aliceClient = (new TestClient(
    @@ -241,22 +276,35 @@ describe("Crypto", function() {
                 bobClient = (new TestClient(
                     "@bob:example.com", "bobdevice",
                 )).client;
    +            claraClient = (new TestClient(
    +                "@clara:example.com", "claradevice",
    +            )).client;
                 await aliceClient.initCrypto();
                 await bobClient.initCrypto();
    +            await claraClient.initCrypto();
             });
     
             afterEach(async function() {
                 aliceClient.stopClient();
                 bobClient.stopClient();
    +            claraClient.stopClient();
             });
     
    -        it("does not cancel keyshare requests if some messages are not decrypted", async function() {
    +        it("does not cancel keyshare requests until all messages are decrypted with trusted keys", async function() {
                 const encryptionCfg = {
                     "algorithm": "m.megolm.v1.aes-sha2",
                 };
                 const roomId = "!someroom";
                 const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
                 const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
    +            // Make Bob invited by Alice so Bob will accept Alice's forwarded keys
    +            bobRoom.currentState.setStateEvents([new MatrixEvent({
    +                type: "m.room.member",
    +                sender: "@alice:example.com",
    +                room_id: roomId,
    +                content: { membership: "invite" },
    +                state_key: "@bob:example.com",
    +            })]);
                 aliceClient.store.storeRoom(aliceRoom);
                 bobClient.store.storeRoom(bobRoom);
                 await aliceClient.setRoomEncryption(roomId, encryptionCfg);
    @@ -302,6 +350,9 @@ describe("Crypto", function() {
                     }
                 }));
     
    +            const device = new DeviceInfo(aliceClient.deviceId);
    +            bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
    +
                 const bobDecryptor = bobClient.crypto.getRoomDecryptor(
                     roomId, olmlib.MEGOLM_ALGORITHM,
                 );
    @@ -314,6 +365,8 @@ describe("Crypto", function() {
                 // the first message can't be decrypted yet, but the second one
                 // can
                 let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
    +            bobClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
    +            bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
                 await bobDecryptor.onRoomKeyEvent(ksEvent);
                 await decryptEventsPromise;
                 expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
    @@ -340,8 +393,24 @@ describe("Crypto", function() {
                 await bobDecryptor.onRoomKeyEvent(ksEvent);
                 await decryptEventPromise;
                 expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
    +            expect(events[0].isKeySourceUntrusted()).toBeTruthy();
    +            await sleep(1);
    +            // the room key request should still be there, since we've
    +            // decrypted everything with an untrusted key
    +            expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined();
    +
    +            // Now share a trusted room key event so Bob will re-decrypt the messages.
    +            // Bob will backfill trust when they receive a trusted session with a higher
    +            // index that connects to an untrusted session with a lower index.
    +            const roomKeyEvent = roomKeyEventForEvent(aliceClient, events[1]);
    +            const trustedDecryptEventPromise = awaitEvent(events[0], "Event.decrypted");
    +            await bobDecryptor.onRoomKeyEvent(roomKeyEvent);
    +            await trustedDecryptEventPromise;
    +            expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
    +            expect(events[0].isKeySourceUntrusted()).toBeFalsy();
                 await sleep(1);
    -            // the room key request should be gone since we've now decrypted everything
    +            // now the room key request should be gone, since there's
    +            // no better key to wait for
                 expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
             });
     
    @@ -383,6 +452,9 @@ describe("Crypto", function() {
                     // decryption keys yet
                 }
     
    +            const device = new DeviceInfo(aliceClient.deviceId);
    +            bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
    +
                 const bobDecryptor = bobClient.crypto.getRoomDecryptor(
                     roomId, olmlib.MEGOLM_ALGORITHM,
                 );
    @@ -462,6 +534,420 @@ describe("Crypto", function() {
                 expect(aliceSendToDevice).toBeCalledTimes(3);
                 expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
             });
    +
    +        it("should accept forwarded keys which it requested", async function() {
    +            const encryptionCfg = {
    +                "algorithm": "m.megolm.v1.aes-sha2",
    +            };
    +            const roomId = "!someroom";
    +            const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
    +            const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
    +            aliceClient.store.storeRoom(aliceRoom);
    +            bobClient.store.storeRoom(bobRoom);
    +            await aliceClient.setRoomEncryption(roomId, encryptionCfg);
    +            await bobClient.setRoomEncryption(roomId, encryptionCfg);
    +            const events = [
    +                new MatrixEvent({
    +                    type: "m.room.message",
    +                    sender: "@alice:example.com",
    +                    room_id: roomId,
    +                    event_id: "$1",
    +                    content: {
    +                        msgtype: "m.text",
    +                        body: "1",
    +                    },
    +                }),
    +                new MatrixEvent({
    +                    type: "m.room.message",
    +                    sender: "@alice:example.com",
    +                    room_id: roomId,
    +                    event_id: "$2",
    +                    content: {
    +                        msgtype: "m.text",
    +                        body: "2",
    +                    },
    +                }),
    +            ];
    +            await Promise.all(events.map(async (event) => {
    +                // alice encrypts each event, and then bob tries to decrypt
    +                // them without any keys, so that they'll be in pending
    +                await aliceClient.crypto.encryptEvent(event, aliceRoom);
    +                // remove keys from the event
    +                // @ts-ignore private properties
    +                event.clearEvent = undefined;
    +                // @ts-ignore private properties
    +                event.senderCurve25519Key = null;
    +                // @ts-ignore private properties
    +                event.claimedEd25519Key = null;
    +                try {
    +                    await bobClient.crypto.decryptEvent(event);
    +                } catch (e) {
    +                    // we expect this to fail because we don't have the
    +                    // decryption keys yet
    +                }
    +            }));
    +
    +            const device = new DeviceInfo(aliceClient.deviceId);
    +            bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
    +            bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
    +
    +            const cryptoStore = bobClient.crypto.cryptoStore;
    +            const eventContent = events[0].getWireContent();
    +            const senderKey = eventContent.sender_key;
    +            const sessionId = eventContent.session_id;
    +            const roomKeyRequestBody = {
    +                algorithm: olmlib.MEGOLM_ALGORITHM,
    +                room_id: roomId,
    +                sender_key: senderKey,
    +                session_id: sessionId,
    +            };
    +            const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
    +            expect(outgoingReq).toBeDefined();
    +            await cryptoStore.updateOutgoingRoomKeyRequest(
    +                outgoingReq.requestId, RoomKeyRequestState.Unsent,
    +                { state: RoomKeyRequestState.Sent },
    +            );
    +
    +            const bobDecryptor = bobClient.crypto.getRoomDecryptor(
    +                roomId, olmlib.MEGOLM_ALGORITHM,
    +            );
    +
    +            const decryptEventsPromise = Promise.all(events.map((ev) => {
    +                return awaitEvent(ev, "Event.decrypted");
    +            }));
    +            const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
    +            await bobDecryptor.onRoomKeyEvent(ksEvent);
    +            const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
    +                roomId,
    +                events[0].getWireContent().sender_key,
    +                events[0].getWireContent().session_id,
    +            );
    +            expect(key).not.toBeNull();
    +            await decryptEventsPromise;
    +            expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
    +            expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
    +        });
    +
    +        it("should accept forwarded keys from the user who invited it to the room", async function() {
    +            const encryptionCfg = {
    +                "algorithm": "m.megolm.v1.aes-sha2",
    +            };
    +            const roomId = "!someroom";
    +            const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
    +            const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
    +            const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
    +            // Make Bob invited by Clara
    +            bobRoom.currentState.setStateEvents([new MatrixEvent({
    +                type: "m.room.member",
    +                sender: "@clara:example.com",
    +                room_id: roomId,
    +                content: { membership: "invite" },
    +                state_key: "@bob:example.com",
    +            })]);
    +            aliceClient.store.storeRoom(aliceRoom);
    +            bobClient.store.storeRoom(bobRoom);
    +            claraClient.store.storeRoom(claraRoom);
    +            await aliceClient.setRoomEncryption(roomId, encryptionCfg);
    +            await bobClient.setRoomEncryption(roomId, encryptionCfg);
    +            await claraClient.setRoomEncryption(roomId, encryptionCfg);
    +            const events = [
    +                new MatrixEvent({
    +                    type: "m.room.message",
    +                    sender: "@alice:example.com",
    +                    room_id: roomId,
    +                    event_id: "$1",
    +                    content: {
    +                        msgtype: "m.text",
    +                        body: "1",
    +                    },
    +                }),
    +                new MatrixEvent({
    +                    type: "m.room.message",
    +                    sender: "@alice:example.com",
    +                    room_id: roomId,
    +                    event_id: "$2",
    +                    content: {
    +                        msgtype: "m.text",
    +                        body: "2",
    +                    },
    +                }),
    +            ];
    +            await Promise.all(events.map(async (event) => {
    +                // alice encrypts each event, and then bob tries to decrypt
    +                // them without any keys, so that they'll be in pending
    +                await aliceClient.crypto.encryptEvent(event, aliceRoom);
    +                // remove keys from the event
    +                // @ts-ignore private properties
    +                event.clearEvent = undefined;
    +                // @ts-ignore private properties
    +                event.senderCurve25519Key = null;
    +                // @ts-ignore private properties
    +                event.claimedEd25519Key = null;
    +                try {
    +                    await bobClient.crypto.decryptEvent(event);
    +                } catch (e) {
    +                    // we expect this to fail because we don't have the
    +                    // decryption keys yet
    +                }
    +            }));
    +
    +            const device = new DeviceInfo(claraClient.deviceId);
    +            bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
    +            bobClient.crypto.deviceList.getUserByIdentityKey = () => "@clara:example.com";
    +
    +            const bobDecryptor = bobClient.crypto.getRoomDecryptor(
    +                roomId, olmlib.MEGOLM_ALGORITHM,
    +            );
    +
    +            const decryptEventsPromise = Promise.all(events.map((ev) => {
    +                return awaitEvent(ev, "Event.decrypted");
    +            }));
    +            const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
    +            ksEvent.event.sender = claraClient.getUserId(),
    +            ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
    +            await bobDecryptor.onRoomKeyEvent(ksEvent);
    +            const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
    +                roomId,
    +                events[0].getWireContent().sender_key,
    +                events[0].getWireContent().session_id,
    +            );
    +            expect(key).not.toBeNull();
    +            await decryptEventsPromise;
    +            expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
    +            expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
    +        });
    +
    +        it("should accept forwarded keys from one of its own user's other devices", async function() {
    +            const encryptionCfg = {
    +                "algorithm": "m.megolm.v1.aes-sha2",
    +            };
    +            const roomId = "!someroom";
    +            const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
    +            const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
    +            aliceClient.store.storeRoom(aliceRoom);
    +            bobClient.store.storeRoom(bobRoom);
    +            await aliceClient.setRoomEncryption(roomId, encryptionCfg);
    +            await bobClient.setRoomEncryption(roomId, encryptionCfg);
    +            const events = [
    +                new MatrixEvent({
    +                    type: "m.room.message",
    +                    sender: "@alice:example.com",
    +                    room_id: roomId,
    +                    event_id: "$1",
    +                    content: {
    +                        msgtype: "m.text",
    +                        body: "1",
    +                    },
    +                }),
    +                new MatrixEvent({
    +                    type: "m.room.message",
    +                    sender: "@alice:example.com",
    +                    room_id: roomId,
    +                    event_id: "$2",
    +                    content: {
    +                        msgtype: "m.text",
    +                        body: "2",
    +                    },
    +                }),
    +            ];
    +            await Promise.all(events.map(async (event) => {
    +                // alice encrypts each event, and then bob tries to decrypt
    +                // them without any keys, so that they'll be in pending
    +                await aliceClient.crypto.encryptEvent(event, aliceRoom);
    +                // remove keys from the event
    +                // @ts-ignore private properties
    +                event.clearEvent = undefined;
    +                // @ts-ignore private properties
    +                event.senderCurve25519Key = null;
    +                // @ts-ignore private properties
    +                event.claimedEd25519Key = null;
    +                try {
    +                    await bobClient.crypto.decryptEvent(event);
    +                } catch (e) {
    +                    // we expect this to fail because we don't have the
    +                    // decryption keys yet
    +                }
    +            }));
    +
    +            const device = new DeviceInfo(claraClient.deviceId);
    +            device.verified = DeviceInfo.DeviceVerification.VERIFIED;
    +            bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
    +            bobClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:example.com";
    +
    +            const bobDecryptor = bobClient.crypto.getRoomDecryptor(
    +                roomId, olmlib.MEGOLM_ALGORITHM,
    +            );
    +
    +            const decryptEventsPromise = Promise.all(events.map((ev) => {
    +                return awaitEvent(ev, "Event.decrypted");
    +            }));
    +            const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
    +            ksEvent.event.sender = bobClient.getUserId(),
    +            ksEvent.sender = new RoomMember(roomId, bobClient.getUserId());
    +            await bobDecryptor.onRoomKeyEvent(ksEvent);
    +            const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
    +                roomId,
    +                events[0].getWireContent().sender_key,
    +                events[0].getWireContent().session_id,
    +            );
    +            expect(key).not.toBeNull();
    +            await decryptEventsPromise;
    +            expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
    +            expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
    +        });
    +
    +        it("should not accept unexpected forwarded keys for a room it's in", async function() {
    +            const encryptionCfg = {
    +                "algorithm": "m.megolm.v1.aes-sha2",
    +            };
    +            const roomId = "!someroom";
    +            const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
    +            const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
    +            const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
    +            aliceClient.store.storeRoom(aliceRoom);
    +            bobClient.store.storeRoom(bobRoom);
    +            claraClient.store.storeRoom(claraRoom);
    +            await aliceClient.setRoomEncryption(roomId, encryptionCfg);
    +            await bobClient.setRoomEncryption(roomId, encryptionCfg);
    +            await claraClient.setRoomEncryption(roomId, encryptionCfg);
    +            const events = [
    +                new MatrixEvent({
    +                    type: "m.room.message",
    +                    sender: "@alice:example.com",
    +                    room_id: roomId,
    +                    event_id: "$1",
    +                    content: {
    +                        msgtype: "m.text",
    +                        body: "1",
    +                    },
    +                }),
    +                new MatrixEvent({
    +                    type: "m.room.message",
    +                    sender: "@alice:example.com",
    +                    room_id: roomId,
    +                    event_id: "$2",
    +                    content: {
    +                        msgtype: "m.text",
    +                        body: "2",
    +                    },
    +                }),
    +            ];
    +            await Promise.all(events.map(async (event) => {
    +                // alice encrypts each event, and then bob tries to decrypt
    +                // them without any keys, so that they'll be in pending
    +                await aliceClient.crypto.encryptEvent(event, aliceRoom);
    +                // remove keys from the event
    +                // @ts-ignore private properties
    +                event.clearEvent = undefined;
    +                // @ts-ignore private properties
    +                event.senderCurve25519Key = null;
    +                // @ts-ignore private properties
    +                event.claimedEd25519Key = null;
    +                try {
    +                    await bobClient.crypto.decryptEvent(event);
    +                } catch (e) {
    +                    // we expect this to fail because we don't have the
    +                    // decryption keys yet
    +                }
    +            }));
    +
    +            const device = new DeviceInfo(claraClient.deviceId);
    +            bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
    +            bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
    +
    +            const bobDecryptor = bobClient.crypto.getRoomDecryptor(
    +                roomId, olmlib.MEGOLM_ALGORITHM,
    +            );
    +
    +            const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
    +            ksEvent.event.sender = claraClient.getUserId(),
    +            ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
    +            await bobDecryptor.onRoomKeyEvent(ksEvent);
    +            const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
    +                roomId,
    +                events[0].getWireContent().sender_key,
    +                events[0].getWireContent().session_id,
    +            );
    +            expect(key).toBeNull();
    +        });
    +
    +        it("should park forwarded keys for a room it's not in", async function() {
    +            const encryptionCfg = {
    +                "algorithm": "m.megolm.v1.aes-sha2",
    +            };
    +            const roomId = "!someroom";
    +            const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
    +            aliceClient.store.storeRoom(aliceRoom);
    +            await aliceClient.setRoomEncryption(roomId, encryptionCfg);
    +            const events = [
    +                new MatrixEvent({
    +                    type: "m.room.message",
    +                    sender: "@alice:example.com",
    +                    room_id: roomId,
    +                    event_id: "$1",
    +                    content: {
    +                        msgtype: "m.text",
    +                        body: "1",
    +                    },
    +                }),
    +                new MatrixEvent({
    +                    type: "m.room.message",
    +                    sender: "@alice:example.com",
    +                    room_id: roomId,
    +                    event_id: "$2",
    +                    content: {
    +                        msgtype: "m.text",
    +                        body: "2",
    +                    },
    +                }),
    +            ];
    +            await Promise.all(events.map(async (event) => {
    +                // alice encrypts each event, and then bob tries to decrypt
    +                // them without any keys, so that they'll be in pending
    +                await aliceClient.crypto.encryptEvent(event, aliceRoom);
    +                // remove keys from the event
    +                // @ts-ignore private properties
    +                event.clearEvent = undefined;
    +                // @ts-ignore private properties
    +                event.senderCurve25519Key = null;
    +                // @ts-ignore private properties
    +                event.claimedEd25519Key = null;
    +            }));
    +
    +            const device = new DeviceInfo(aliceClient.deviceId);
    +            bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
    +            bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
    +
    +            const bobDecryptor = bobClient.crypto.getRoomDecryptor(
    +                roomId, olmlib.MEGOLM_ALGORITHM,
    +            );
    +
    +            const content = events[0].getWireContent();
    +
    +            const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
    +            await bobDecryptor.onRoomKeyEvent(ksEvent);
    +            const bobKey = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
    +                roomId,
    +                content.sender_key,
    +                content.session_id,
    +            );
    +            expect(bobKey).toBeNull();
    +
    +            const aliceKey = await aliceClient.crypto.olmDevice.getInboundGroupSessionKey(
    +                roomId,
    +                content.sender_key,
    +                content.session_id,
    +            );
    +            const parked = await bobClient.crypto.cryptoStore.takeParkedSharedHistory(roomId);
    +            expect(parked).toEqual([{
    +                senderId: aliceClient.getUserId(),
    +                senderKey: content.sender_key,
    +                sessionId: content.session_id,
    +                sessionKey: aliceKey.key,
    +                keysClaimed: { ed25519: aliceKey.sender_claimed_ed25519_key },
    +                forwardingCurve25519KeyChain: ["akey"],
    +            }]);
    +        });
         });
     
         describe('Secret storage', function() {
    
  • spec/unit/crypto/verification/sas.spec.ts+20 6 modified
    @@ -464,7 +464,7 @@ describe("SAS verification", function() {
                     },
                 );
     
    -            alice.client.setDeviceVerified = jest.fn();
    +            alice.client.crypto.setDeviceVerification = jest.fn();
                 alice.client.getDeviceEd25519Key = () => {
                     return "alice+base64+ed25519+key";
                 };
    @@ -482,7 +482,7 @@ describe("SAS verification", function() {
                     return Promise.resolve();
                 };
     
    -            bob.client.setDeviceVerified = jest.fn();
    +            bob.client.crypto.setDeviceVerification = jest.fn();
                 bob.client.getStoredDevice = () => {
                     return DeviceInfo.fromStorage(
                         {
    @@ -565,10 +565,24 @@ describe("SAS verification", function() {
                 ]);
     
                 // make sure Alice and Bob verified each other
    -            expect(alice.client.setDeviceVerified)
    -                .toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
    -            expect(bob.client.setDeviceVerified)
    -                .toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
    +            expect(alice.client.crypto.setDeviceVerification)
    +                .toHaveBeenCalledWith(
    +                    bob.client.getUserId(),
    +                    bob.client.deviceId,
    +                    true,
    +                    null,
    +                    null,
    +                    { "ed25519:Dynabook": "bob+base64+ed25519+key" },
    +            );
    +            expect(bob.client.crypto.setDeviceVerification)
    +                .toHaveBeenCalledWith(
    +                    alice.client.getUserId(),
    +                    alice.client.deviceId,
    +                    true,
    +                    null,
    +                    null,
    +                    { "ed25519:Osborne2": "alice+base64+ed25519+key" },
    +            );
             });
         });
     });
    
  • spec/unit/models/beacon.spec.ts+22 0 modified
    @@ -22,6 +22,7 @@ import {
         BeaconEvent,
     } from "../../../src/models/beacon";
     import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
    +import { REFERENCE_RELATION } from "matrix-events-sdk";
     
     jest.useFakeTimers();
     
    @@ -431,6 +432,27 @@ describe('Beacon', () => {
                     expect(emitSpy).not.toHaveBeenCalled();
                 });
     
    +            it("should ignore invalid beacon events", () => {
    +                const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
    +                const emitSpy = jest.spyOn(beacon, 'emit');
    +
    +                const ev = new MatrixEvent({
    +                    type: M_BEACON_INFO.name,
    +                    sender: userId,
    +                    room_id: roomId,
    +                    content: {
    +                        "m.relates_to": {
    +                            rel_type: REFERENCE_RELATION.name,
    +                            event_id: beacon.beaconInfoId,
    +                        },
    +                    },
    +                });
    +                beacon.addLocations([ev]);
    +
    +                expect(beacon.latestLocationEvent).toBeFalsy();
    +                expect(emitSpy).not.toHaveBeenCalled();
    +            });
    +
                 describe('when beacon is live with a start timestamp is in the future', () => {
                     it('ignores locations before the beacon start timestamp', () => {
                         const startTimestamp = now + 60000;
    
  • src/client.ts+1 0 modified
    @@ -5287,6 +5287,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
          * @param {object} [options]
          * @param {boolean} options.preventReEmit don't re-emit events emitted on an event mapped by this mapper on the client
          * @param {boolean} options.decrypt decrypt event proactively
    +     * @param {boolean} options.toDevice the event is a to_device event
          * @return {Function}
          */
         public getEventMapper(options?: MapperOpts): EventMapper {
    
  • src/content-helpers.ts+5 4 modified
    @@ -292,16 +292,17 @@ export const makeBeaconContent: MakeBeaconContent = (
     });
     
     export type BeaconLocationState = MLocationContent & {
    -    timestamp: number;
    +    uri?: string; // override from MLocationContent to allow optionals
    +    timestamp?: number;
     };
     
     export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
    -    const { description, uri } = M_LOCATION.findIn<MLocationContent>(content);
    +    const location = M_LOCATION.findIn<MLocationContent>(content);
         const timestamp = M_TIMESTAMP.findIn<number>(content);
     
         return {
    -        description,
    -        uri,
    +        description: location?.description,
    +        uri: location?.uri,
             timestamp,
         };
     };
    
  • src/crypto/algorithms/megolm.ts+108 17 modified
    @@ -35,8 +35,10 @@ import { Room } from '../../models/room';
     import { DeviceInfo } from "../deviceinfo";
     import { IOlmSessionResult } from "../olmlib";
     import { DeviceInfoMap } from "../DeviceList";
    -import { MatrixEvent } from "../..";
    +import { MatrixEvent } from "../../models/event";
     import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
    +import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
    +import { OlmGroupSessionExtraData } from "../../@types/crypto";
     
     // determine whether the key can be shared with invitees
     export function isRoomSharedHistory(room: Room): boolean {
    @@ -1189,8 +1191,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
      *     {@link module:crypto/algorithms/DecryptionAlgorithm}
      */
     class MegolmDecryption extends DecryptionAlgorithm {
    -    // events which we couldn't decrypt due to unknown sessions / indexes: map from
    -    // senderKey|sessionId to Set of MatrixEvents
    +    // events which we couldn't decrypt due to unknown sessions /
    +    // indexes, or which we could only decrypt with untrusted keys:
    +    // map from senderKey|sessionId to Set of MatrixEvents
         private pendingEvents = new Map<string, Map<string, Set<MatrixEvent>>>();
     
         // this gets stubbed out by the unit tests.
    @@ -1294,9 +1297,13 @@ class MegolmDecryption extends DecryptionAlgorithm {
                 );
             }
     
    -        // success. We can remove the event from the pending list, if that hasn't
    -        // already happened.
    -        this.removeEventFromPendingList(event);
    +        // Success. We can remove the event from the pending list, if
    +        // that hasn't already happened. However, if the event was
    +        // decrypted with an untrusted key, leave it on the pending
    +        // list so it will be retried if we find a trusted key later.
    +        if (!res.untrusted) {
    +            this.removeEventFromPendingList(event);
    +        }
     
             const payload = JSON.parse(res.result);
     
    @@ -1391,6 +1398,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
             let exportFormat = false;
             let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
     
    +        const extraSessionData: OlmGroupSessionExtraData = {};
    +
             if (!content.room_id ||
                 !content.session_key ||
                 !content.session_id ||
    @@ -1400,12 +1409,58 @@ class MegolmDecryption extends DecryptionAlgorithm {
                 return;
             }
     
    -        if (!senderKey) {
    -            logger.error("key event has no sender key (not encrypted?)");
    +        if (!olmlib.isOlmEncrypted(event)) {
    +            logger.error("key event not properly encrypted");
                 return;
             }
     
    +        if (content["org.matrix.msc3061.shared_history"]) {
    +            extraSessionData.sharedHistory = true;
    +        }
    +
             if (event.getType() == "m.forwarded_room_key") {
    +            const deviceInfo = this.crypto.deviceList.getDeviceByIdentityKey(
    +                olmlib.OLM_ALGORITHM,
    +                senderKey,
    +            );
    +            const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
    +                olmlib.OLM_ALGORITHM,
    +                senderKey,
    +            );
    +            if (senderKeyUser !== event.getSender()) {
    +                logger.error("sending device does not belong to the user it claims to be from");
    +                return;
    +            }
    +            const outgoingRequests = deviceInfo ? await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(
    +                event.getSender(), deviceInfo.deviceId, [RoomKeyRequestState.Sent],
    +            ) : [];
    +            const weRequested = outgoingRequests.some((req) => (
    +                req.requestBody.room_id === content.room_id && req.requestBody.session_id === content.session_id
    +            ));
    +            const room = this.baseApis.getRoom(content.room_id);
    +            const memberEvent = room?.getMember(this.userId)?.events.member;
    +            const fromInviter = memberEvent?.getSender() === event.getSender() ||
    +                (memberEvent?.getUnsigned()?.prev_sender === event.getSender() &&
    +                    memberEvent?.getPrevContent()?.membership === "invite");
    +            const fromUs = event.getSender() === this.baseApis.getUserId();
    +
    +            if (!weRequested) {
    +                // If someone sends us an unsolicited key and it's not
    +                // shared history, ignore it
    +                if (!extraSessionData.sharedHistory) {
    +                    logger.log("forwarded key not shared history - ignoring");
    +                    return;
    +                }
    +
    +                // If someone sends us an unsolicited key for a room
    +                // we're already in, and they're not one of our other
    +                // devices or the one who invited us, ignore it
    +                if (room && !fromInviter && !fromUs) {
    +                    logger.log("forwarded key not from inviter or from us - ignoring");
    +                    return;
    +                }
    +            }
    +
                 exportFormat = true;
                 forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ?
                     content.forwarding_curve25519_key_chain : [];
    @@ -1418,7 +1473,6 @@ class MegolmDecryption extends DecryptionAlgorithm {
                     logger.error("forwarded_room_key event is missing sender_key field");
                     return;
                 }
    -            senderKey = content.sender_key;
     
                 const ed25519Key = content.sender_claimed_ed25519_key;
                 if (!ed25519Key) {
    @@ -1431,11 +1485,45 @@ class MegolmDecryption extends DecryptionAlgorithm {
                 keysClaimed = {
                     ed25519: ed25519Key,
                 };
    +
    +            // If this is a key for a room we're not in, don't load it
    +            // yet, just park it in case *this sender* invites us to
    +            // that room later
    +            if (!room) {
    +                const parkedData = {
    +                    senderId: event.getSender(),
    +                    senderKey: content.sender_key,
    +                    sessionId: content.session_id,
    +                    sessionKey: content.session_key,
    +                    keysClaimed,
    +                    forwardingCurve25519KeyChain: forwardingKeyChain,
    +                };
    +                await this.crypto.cryptoStore.doTxn(
    +                    'readwrite',
    +                    ['parked_shared_history'],
    +                    (txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id, parkedData, txn),
    +                    logger.withPrefix("[addParkedSharedHistory]"),
    +                );
    +                return;
    +            }
    +
    +            const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
    +            const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice);
    +
    +            if (fromUs && !deviceTrust.isVerified()) {
    +                return;
    +            }
    +
    +            // forwarded keys are always untrusted
    +            extraSessionData.untrusted = true;
    +
    +            // replace the sender key with the sender key of the session
    +            // creator for storage
    +            senderKey = content.sender_key;
             } else {
                 keysClaimed = event.getKeysClaimed();
             }
     
    -        const extraSessionData: any = {};
             if (content["org.matrix.msc3061.shared_history"]) {
                 extraSessionData.sharedHistory = true;
             }
    @@ -1453,7 +1541,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
                 );
     
                 // have another go at decrypting events sent with this session.
    -            if (await this.retryDecryption(senderKey, content.session_id)) {
    +            if (await this.retryDecryption(senderKey, content.session_id, !extraSessionData.untrusted)) {
                     // cancel any outstanding room key requests for this session.
                     // Only do this if we managed to decrypt every message in the
                     // session, because if we didn't, we leave the other key
    @@ -1668,7 +1756,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
             session: IMegolmSessionData,
             opts: { untrusted?: boolean, source?: string } = {},
         ): Promise<void> {
    -        const extraSessionData: any = {};
    +        const extraSessionData: OlmGroupSessionExtraData = {};
             if (opts.untrusted || session.untrusted) {
                 extraSessionData.untrusted = true;
             }
    @@ -1696,7 +1784,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
                     });
                 }
                 // have another go at decrypting events sent with this session.
    -            this.retryDecryption(session.sender_key, session.session_id);
    +            this.retryDecryption(session.sender_key, session.session_id, !extraSessionData.untrusted);
             });
         }
     
    @@ -1707,10 +1795,12 @@ class MegolmDecryption extends DecryptionAlgorithm {
          * @private
          * @param {String} senderKey
          * @param {String} sessionId
    +     * @param {Boolean} keyTrusted
          *
    -     * @return {Boolean} whether all messages were successfully decrypted
    +     * @return {Boolean} whether all messages were successfully
    +     *     decrypted with trusted keys
          */
    -    private async retryDecryption(senderKey: string, sessionId: string): Promise<boolean> {
    +    private async retryDecryption(senderKey: string, sessionId: string, keyTrusted?: boolean): Promise<boolean> {
             const senderPendingEvents = this.pendingEvents.get(senderKey);
             if (!senderPendingEvents) {
                 return true;
    @@ -1725,13 +1815,14 @@ class MegolmDecryption extends DecryptionAlgorithm {
     
             await Promise.all([...pending].map(async (ev) => {
                 try {
    -                await ev.attemptDecryption(this.crypto, { isRetry: true });
    +                await ev.attemptDecryption(this.crypto, { isRetry: true, keyTrusted });
                 } catch (e) {
                     // don't die if something goes wrong
                 }
             }));
     
    -        // If decrypted successfully, they'll have been removed from pendingEvents
    +        // If decrypted successfully with trusted keys, they'll have
    +        // been removed from pendingEvents
             return !this.pendingEvents.get(senderKey)?.has(sessionId);
         }
     
    
  • src/crypto/algorithms/olm.ts+20 0 modified
    @@ -222,6 +222,26 @@ class OlmDecryption extends DecryptionAlgorithm {
                 );
             }
     
    +        // check that the device that encrypted the event belongs to the user
    +        // that the event claims it's from.  We need to make sure that our
    +        // device list is up-to-date.  If the device is unknown, we can only
    +        // assume that the device logged out.  Some event handlers, such as
    +        // secret sharing, may be more strict and reject events that come from
    +        // unknown devices.
    +        await this.crypto.deviceList.downloadKeys([event.getSender()], false);
    +        const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(
    +            olmlib.OLM_ALGORITHM,
    +            deviceKey,
    +        );
    +        if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined) {
    +            throw new DecryptionError(
    +                "OLM_BAD_SENDER",
    +                "Message claimed to be from " + event.getSender(), {
    +                    real_sender: senderKeyUser,
    +                },
    +            );
    +        }
    +
             // check that the original sender matches what the homeserver told us, to
             // avoid people masquerading as others.
             // (this check is also provided via the sender's embedded ed25519 key,
    
  • src/crypto/backup.ts+0 1 modified
    @@ -431,7 +431,6 @@ export class BackupManager {
                     )
                 );
             });
    -        ret.usable = ret.usable || ret.trusted_locally;
             return ret;
         }
     
    
  • src/crypto/index.ts+17 9 modified
    @@ -2105,6 +2105,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
          * @param {?boolean} known whether to mark that the user has been made aware of
          *      the existence of this device. Null to leave unchanged
          *
    +     * @param {?Record<string, any>} keys The list of keys that was present
    +     * during the device verification. This will be double checked with the list
    +     * of keys the given device has currently.
    +     *
          * @return {Promise<module:crypto/deviceinfo>} updated DeviceInfo
          */
         public async setDeviceVerification(
    @@ -2113,6 +2117,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
             verified?: boolean,
             blocked?: boolean,
             known?: boolean,
    +        keys?: Record<string, string>,
         ): Promise<DeviceInfo | CrossSigningInfo> {
             // get rid of any `undefined`s here so we can just check
             // for null rather than null or undefined
    @@ -2131,6 +2136,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
                 if (!verified) {
                     throw new Error("Cannot set a cross-signing key as unverified");
                 }
    +            const gotKeyId = keys ? Object.values(keys)[0] : null;
    +            if (keys && (Object.values(keys).length !== 1 || gotKeyId !== xsk.getId())) {
    +                throw new Error(`Key did not match expected value: expected ${xsk.getId()}, got ${gotKeyId}`);
    +            }
     
                 if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
                     this.storeTrustedSelfKeys(xsk.keys);
    @@ -2191,6 +2200,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
             let verificationStatus = dev.verified;
     
             if (verified) {
    +            if (keys) {
    +                for (const [keyId, key] of Object.entries(keys)) {
    +                    if (dev.keys[keyId] !== key) {
    +                        throw new Error(`Key did not match expected value: expected ${key}, got ${dev.keys[keyId]}`);
    +                    }
    +                }
    +            }
                 verificationStatus = DeviceVerification.VERIFIED;
             } else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
                 verificationStatus = DeviceVerification.UNVERIFIED;
    @@ -2400,13 +2416,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
                 return null;
             }
     
    -        const forwardingChain = event.getForwardingCurve25519KeyChain();
    -        if (forwardingChain.length > 0) {
    -            // we got the key this event from somewhere else
    -            // TODO: check if we can trust the forwarders.
    -            return null;
    -        }
    -
             if (event.isKeySourceUntrusted()) {
                 // we got the key for this event from a source that we consider untrusted
                 return null;
    @@ -2478,8 +2487,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
             }
             ret.encrypted = true;
     
    -        const forwardingChain = event.getForwardingCurve25519KeyChain();
    -        if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
    +        if (event.isKeySourceUntrusted()) {
                 // we got the key this event from somewhere else
                 // TODO: check if we can trust the forwarders.
                 ret.authenticated = false;
    
  • src/crypto/OlmDevice.ts+51 14 modified
    @@ -23,6 +23,7 @@ import * as algorithms from './algorithms';
     import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
     import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
     import { IMegolmSessionData } from "./index";
    +import { OlmGroupSessionExtraData } from "../@types/crypto";
     
     // The maximum size of an event is 65K, and we base64 the content, so this is a
     // reasonable approximation to the biggest plaintext we can encrypt.
    @@ -122,6 +123,7 @@ interface IInboundGroupSessionKey {
         forwarding_curve25519_key_chain: string[];
         sender_claimed_ed25519_key: string;
         shared_history: boolean;
    +    untrusted: boolean;
     }
     /* eslint-enable camelcase */
     
    @@ -1101,7 +1103,7 @@ export class OlmDevice {
             sessionKey: string,
             keysClaimed: Record<string, string>,
             exportFormat: boolean,
    -        extraSessionData: Record<string, any> = {},
    +        extraSessionData: OlmGroupSessionExtraData = {},
         ): Promise<void> {
             await this.cryptoStore.doTxn(
                 'readwrite', [
    @@ -1133,17 +1135,42 @@ export class OlmDevice {
                                         "Update for megolm session "
                                         + senderKey + "/" + sessionId,
                                     );
    -                                if (existingSession.first_known_index()
    -                                    <= session.first_known_index()
    -                                    && !(existingSession.first_known_index() == session.first_known_index()
    -                                        && !extraSessionData.untrusted
    -                                        && existingSessionData.untrusted)) {
    -                                    // existing session has lower index (i.e. can
    -                                    // decrypt more), or they have the same index and
    -                                    // the new sessions trust does not win over the old
    -                                    // sessions trust, so keep it
    -                                    logger.log(`Keeping existing megolm session ${sessionId}`);
    -                                    return;
    +                                if (existingSession.first_known_index() <= session.first_known_index()) {
    +                                    if (!existingSessionData.untrusted || extraSessionData.untrusted) {
    +                                        // existing session has less-than-or-equal index
    +                                        // (i.e. can decrypt at least as much), and the
    +                                        // new session's trust does not win over the old
    +                                        // session's trust, so keep it
    +                                        logger.log(`Keeping existing megolm session ${sessionId}`);
    +                                        return;
    +                                    }
    +                                    if (existingSession.first_known_index() < session.first_known_index()) {
    +                                        // We want to upgrade the existing session's trust,
    +                                        // but we can't just use the new session because we'll
    +                                        // lose the lower index. Check that the sessions connect
    +                                        // properly, and then manually set the existing session
    +                                        // as trusted.
    +                                        if (
    +                                            existingSession.export_session(session.first_known_index())
    +                                            === session.export_session(session.first_known_index())
    +                                        ) {
    +                                            logger.info(
    +                                                "Upgrading trust of existing megolm session " +
    +                                                sessionId + " based on newly-received trusted session",
    +                                            );
    +                                            existingSessionData.untrusted = false;
    +                                            this.cryptoStore.storeEndToEndInboundGroupSession(
    +                                                senderKey, sessionId, existingSessionData, txn,
    +                                            );
    +                                        } else {
    +                                            logger.warn(
    +                                                "Newly-received megolm session " + sessionId +
    +                                                " does not match existing session! Keeping existing session",
    +                                            );
    +                                        }
    +                                        return;
    +                                    }
    +                                    // If the sessions have the same index, go ahead and store the new trusted one.
                                     }
                                 }
     
    @@ -1427,13 +1454,23 @@ export class OlmDevice {
                             const claimedKeys = sessionData.keysClaimed || {};
                             const senderEd25519Key = claimedKeys.ed25519 || null;
     
    +                        const forwardingKeyChain = sessionData.forwardingCurve25519KeyChain || [];
    +                        // older forwarded keys didn't set the "untrusted"
    +                        // property, but can be identified by having a
    +                        // non-empty forwarding key chain.  These keys should
    +                        // be marked as untrusted since we don't know that they
    +                        // can be trusted
    +                        const untrusted = "untrusted" in sessionData
    +                            ? sessionData.untrusted
    +                            : forwardingKeyChain.length > 0;
    +
                             result = {
                                 "chain_index": chainIndex,
                                 "key": exportedSession,
    -                            "forwarding_curve25519_key_chain":
    -                                sessionData.forwardingCurve25519KeyChain || [],
    +                            "forwarding_curve25519_key_chain": forwardingKeyChain,
                                 "sender_claimed_ed25519_key": senderEd25519Key,
                                 "shared_history": sessionData.sharedHistory || false,
    +                            "untrusted": untrusted,
                             };
                         },
                     );
    
  • src/crypto/olmlib.ts+18 0 modified
    @@ -30,6 +30,8 @@ import { logger } from '../logger';
     import { IOneTimeKey } from "./dehydration";
     import { IClaimOTKsResult, MatrixClient } from "../client";
     import { ISignatures } from "../@types/signed";
    +import { MatrixEvent } from "../models/event";
    +import { EventType } from "../@types/event";
     
     enum Algorithm {
         Olm = "m.olm.v1.curve25519-aes-sha2",
    @@ -554,6 +556,22 @@ export function pkVerify(obj: IObject, pubKey: string, userId: string) {
         }
     }
     
    +/**
    + * Check that an event was encrypted using olm.
    + */
    +export function isOlmEncrypted(event: MatrixEvent): boolean {
    +    if (!event.getSenderKey()) {
    +        logger.error("Event has no sender key (not encrypted?)");
    +        return false;
    +    }
    +    if (event.getWireType() !== EventType.RoomMessageEncrypted ||
    +        !(["m.olm.v1.curve25519-aes-sha2"].includes(event.getWireContent().algorithm))) {
    +        logger.error("Event was not encrypted using an appropriate algorithm");
    +        return false;
    +    }
    +    return true;
    +}
    +
     /**
      * Encode a typed array of uint8 as base64.
      * @param {Uint8Array} uint8Array The data to encode.
    
  • src/crypto/SecretStorage.ts+24 0 modified
    @@ -539,7 +539,23 @@ export class SecretStorage {
                 // because someone could be trying to send us bogus data
                 return;
             }
    +
    +        if (!olmlib.isOlmEncrypted(event)) {
    +            logger.error("secret event not properly encrypted");
    +            return;
    +        }
    +
             const content = event.getContent();
    +
    +        const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
    +            olmlib.OLM_ALGORITHM,
    +            content.sender_key,
    +        );
    +        if (senderKeyUser !== event.getSender()) {
    +            logger.error("sending device does not belong to the user it claims to be from");
    +            return;
    +        }
    +
             logger.log("got secret share for request", content.request_id);
             const requestControl = this.requests.get(content.request_id);
             if (requestControl) {
    @@ -559,6 +575,14 @@ export class SecretStorage {
                     logger.log("unsolicited secret share from device", deviceInfo.deviceId);
                     return;
                 }
    +            // unsure that the sender is trusted.  In theory, this check is
    +            // unnecessary since we only accept secret shares from devices that
    +            // we requested from, but it doesn't hurt.
    +            const deviceTrust = this.baseApis.crypto.checkDeviceInfoTrust(event.getSender(), deviceInfo);
    +            if (!deviceTrust.isVerified()) {
    +                logger.log("secret share from unverified device");
    +                return;
    +            }
     
                 logger.log(
                     `Successfully received secret ${requestControl.name} ` +
    
  • src/crypto/store/base.ts+12 0 modified
    @@ -25,6 +25,7 @@ import { ICrossSigningInfo } from "../CrossSigning";
     import { PrefixedLogger } from "../../logger";
     import { InboundGroupSessionData } from "../OlmDevice";
     import { IEncryptedPayload } from "../aes";
    +import { MatrixEvent } from "../../models/event";
     
     /**
      * Internal module. Definitions for storage for the crypto module
    @@ -127,6 +128,8 @@ export interface CryptoStore {
             roomId: string,
             txn?: unknown,
         ): Promise<[senderKey: string, sessionId: string][]>;
    +    addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
    +    takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
     
         // Session key backups
         doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
    @@ -203,3 +206,12 @@ export interface OutgoingRoomKeyRequest {
         requestBody: IRoomKeyRequestBody;
         state: RoomKeyRequestState;
     }
    +
    +export interface ParkedSharedHistory {
    +    senderId: string;
    +    senderKey: string;
    +    sessionId: string;
    +    sessionKey: string;
    +    keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; // XXX: Less type dependence on MatrixEvent
    +    forwardingCurve25519KeyChain: string[];
    +}
    
  • src/crypto/store/indexeddb-crypto-store-backend.ts+49 0 modified
    @@ -25,6 +25,7 @@ import {
         IWithheld,
         Mode,
         OutgoingRoomKeyRequest,
    +    ParkedSharedHistory,
     } from "./base";
     import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
     import { ICrossSigningKey } from "../../client";
    @@ -873,6 +874,49 @@ export class Backend implements CryptoStore {
             });
         }
     
    +    public addParkedSharedHistory(
    +        roomId: string,
    +        parkedData: ParkedSharedHistory,
    +        txn?: IDBTransaction,
    +    ): void {
    +        if (!txn) {
    +            txn = this.db.transaction(
    +                "parked_shared_history", "readwrite",
    +            );
    +        }
    +        const objectStore = txn.objectStore("parked_shared_history");
    +        const req = objectStore.get([roomId]);
    +        req.onsuccess = () => {
    +            const { parked } = req.result || { parked: [] };
    +            parked.push(parkedData);
    +            objectStore.put({ roomId, parked });
    +        };
    +    }
    +
    +    public takeParkedSharedHistory(
    +        roomId: string,
    +        txn?: IDBTransaction,
    +    ): Promise<ParkedSharedHistory[]> {
    +        if (!txn) {
    +            txn = this.db.transaction(
    +                "parked_shared_history", "readwrite",
    +            );
    +        }
    +        const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId);
    +        return new Promise((resolve, reject) => {
    +            cursorReq.onsuccess = () => {
    +                const cursor = cursorReq.result;
    +                if (!cursor) {
    +                    resolve([]);
    +                }
    +                const data = cursor.value;
    +                cursor.delete();
    +                resolve(data);
    +            };
    +            cursorReq.onerror = reject;
    +        });
    +    }
    +
         public doTxn<T>(
             mode: Mode,
             stores: string | string[],
    @@ -958,6 +1002,11 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
                 keyPath: ["roomId"],
             });
         }
    +    if (oldVersion < 11) {
    +        db.createObjectStore("parked_shared_history", {
    +            keyPath: ["roomId"],
    +        });
    +    }
         // Expand as needed.
     }
     
    
  • src/crypto/store/indexeddb-crypto-store.ts+23 0 modified
    @@ -29,6 +29,7 @@ import {
         IWithheld,
         Mode,
         OutgoingRoomKeyRequest,
    +    ParkedSharedHistory,
     } from "./base";
     import { IRoomKeyRequestBody } from "../index";
     import { ICrossSigningKey } from "../../client";
    @@ -55,6 +56,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
         public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
         public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld';
         public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions';
    +    public static STORE_PARKED_SHARED_HISTORY = 'parked_shared_history';
         public static STORE_DEVICE_DATA = 'device_data';
         public static STORE_ROOMS = 'rooms';
         public static STORE_BACKUP = 'sessions_needing_backup';
    @@ -669,6 +671,27 @@ export class IndexedDBCryptoStore implements CryptoStore {
             return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
         }
     
    +    /**
    +     * Park a shared-history group session for a room we may be invited to later.
    +     */
    +    public addParkedSharedHistory(
    +        roomId: string,
    +        parkedData: ParkedSharedHistory,
    +        txn?: IDBTransaction,
    +    ): void {
    +        this.backend.addParkedSharedHistory(roomId, parkedData, txn);
    +    }
    +
    +    /**
    +     * Pop out all shared-history group sessions for a room.
    +     */
    +    public takeParkedSharedHistory(
    +        roomId: string,
    +        txn?: IDBTransaction,
    +    ): Promise<ParkedSharedHistory[]> {
    +        return this.backend.takeParkedSharedHistory(roomId, txn);
    +    }
    +
         /**
          * Perform a transaction on the crypto store. Any store methods
          * that require a transaction (txn) object to be passed in may
    
  • src/crypto/store/memory-crypto-store.ts+14 0 modified
    @@ -25,6 +25,7 @@ import {
         IWithheld,
         Mode,
         OutgoingRoomKeyRequest,
    +    ParkedSharedHistory,
     } from "./base";
     import { IRoomKeyRequestBody } from "../index";
     import { ICrossSigningKey } from "../../client";
    @@ -58,6 +59,7 @@ export class MemoryCryptoStore implements CryptoStore {
         private rooms: { [roomId: string]: IRoomEncryption } = {};
         private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
         private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
    +    private parkedSharedHistory = new Map<string, ParkedSharedHistory[]>(); // keyed by room ID
     
         /**
          * Ensure the database exists and is up-to-date.
    @@ -526,6 +528,18 @@ export class MemoryCryptoStore implements CryptoStore {
             return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
         }
     
    +    public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory): void {
    +        const parked = this.parkedSharedHistory.get(roomId) ?? [];
    +        parked.push(parkedData);
    +        this.parkedSharedHistory.set(roomId, parked);
    +    }
    +
    +    public takeParkedSharedHistory(roomId: string): Promise<ParkedSharedHistory[]> {
    +        const parked = this.parkedSharedHistory.get(roomId) ?? [];
    +        this.parkedSharedHistory.delete(roomId);
    +        return Promise.resolve(parked);
    +    }
    +
         // Session key backups
     
         public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
    
  • src/crypto/verification/Base.ts+19 6 modified
    @@ -299,7 +299,13 @@ export class VerificationBase<
             if (this.doVerification && !this.started) {
                 this.started = true;
                 this.resetTimer(); // restart the timeout
    -            Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
    +            new Promise<void>((resolve, reject) => {
    +                const crossSignId = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(this.userId)?.getId();
    +                if (crossSignId === this.deviceId) {
    +                    reject(new Error("Device ID is the same as the cross-signing ID"));
    +                }
    +                resolve();
    +            }).then(() => this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
             }
             return this.promise;
         }
    @@ -310,14 +316,14 @@ export class VerificationBase<
             // we try to verify all the keys that we're told about, but we might
             // not know about all of them, so keep track of the keys that we know
             // about, and ignore the rest
    -        const verifiedDevices = [];
    +        const verifiedDevices: [string, string, string][] = [];
     
             for (const [keyId, keyInfo] of Object.entries(keys)) {
                 const deviceId = keyId.split(':', 2)[1];
                 const device = this.baseApis.getStoredDevice(userId, deviceId);
                 if (device) {
                     verifier(keyId, device, keyInfo);
    -                verifiedDevices.push(deviceId);
    +                verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
                 } else {
                     const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
                     if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
    @@ -326,7 +332,7 @@ export class VerificationBase<
                                 [keyId]: deviceId,
                             },
                         }, deviceId), keyInfo);
    -                    verifiedDevices.push(deviceId);
    +                    verifiedDevices.push([deviceId, keyId, deviceId]);
                     } else {
                         logger.warn(
                             `verification: Could not find device ${deviceId} to verify`,
    @@ -348,8 +354,15 @@ export class VerificationBase<
             // TODO: There should probably be a batch version of this, otherwise it's going
             // to upload each signature in a separate API call which is silly because the
             // API supports as many signatures as you like.
    -        for (const deviceId of verifiedDevices) {
    -            await this.baseApis.setDeviceVerified(userId, deviceId);
    +        for (const [deviceId, keyId, key] of verifiedDevices) {
    +            await this.baseApis.crypto.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key });
    +        }
    +
    +        // if one of the user's own devices is being marked as verified / unverified,
    +        // check the key backup status, since whether or not we use this depends on
    +        // whether it has a signature from a verified device
    +        if (userId == this.baseApis.credentials.userId) {
    +            await this.baseApis.checkKeyBackup();
             }
         }
     
    
  • src/event-mapper.ts+5 0 modified
    @@ -22,13 +22,18 @@ 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;
    
  • src/models/beacon.ts+3 2 modified
    @@ -15,7 +15,6 @@ limitations under the License.
     */
     
     import { MBeaconEventContent } from "../@types/beacon";
    -import { M_TIMESTAMP } from "../@types/location";
     import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
     import { MatrixEvent } from "../matrix";
     import { sortEventsByLatestContentTimestamp } from "../utils";
    @@ -161,7 +160,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
     
             const validLocationEvents = beaconLocationEvents.filter(event => {
                 const content = event.getContent<MBeaconEventContent>();
    -            const timestamp = M_TIMESTAMP.findIn<number>(content);
    +            const parsed = parseBeaconContent(content);
    +            if (!parsed.uri || !parsed.timestamp) return false; // we won't be able to process these
    +            const { timestamp } = parsed;
                 return (
                     // only include positions that were taken inside the beacon's live period
                     isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&
    
  • src/models/event.ts+2 1 modified
    @@ -151,6 +151,7 @@ interface IKeyRequestRecipient {
     export interface IDecryptOptions {
         emit?: boolean;
         isRetry?: boolean;
    +    keyTrusted?: boolean;
     }
     
     /**
    @@ -695,7 +696,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
                 throw new Error("Attempt to decrypt event which isn't encrypted");
             }
     
    -        if (this.clearEvent && !this.isDecryptionFailure()) {
    +        if (this.clearEvent && !this.isDecryptionFailure() && !(this.isKeySourceUntrusted() && options.keyTrusted)) {
                 // we may want to just ignore this? let's start with rejecting it.
                 throw new Error(
                     "Attempt to decrypt event which has already been decrypted",
    
  • src/NamespacedValue.ts+4 2 modified
    @@ -1,5 +1,5 @@
     /*
    -Copyright 2021 The Matrix.org Foundation C.I.C.
    +Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
     
     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
    @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
     limitations under the License.
     */
     
    +import { Optional } from "matrix-events-sdk/lib/types";
    +
     /**
      * Represents a simple Matrix namespaced value. This will assume that if a stable prefix
      * is provided that the stable prefix should be used when representing the identifier.
    @@ -54,7 +56,7 @@ export class NamespacedValue<S extends string, U extends string> {
     
         // this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
         // so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
    -    public findIn<T>(obj: any): T {
    +    public findIn<T>(obj: any): Optional<T> {
             let val: T;
             if (this.name) {
                 val = obj?.[this.name];
    
  • src/sync.ts+37 2 modified
    @@ -1109,7 +1109,20 @@ export class SyncApi {
             if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) {
                 const cancelledKeyVerificationTxns = [];
                 data.to_device.events
    -                .map(client.getEventMapper())
    +                .filter((eventJSON) => {
    +                    if (
    +                        eventJSON.type === EventType.RoomMessageEncrypted &&
    +                        !(["m.olm.v1.curve25519-aes-sha2"].includes(eventJSON.content?.algorithm))
    +                    ) {
    +                        logger.log(
    +                            'Ignoring invalid encrypted to-device event from ' + eventJSON.sender,
    +                        );
    +                        return false;
    +                    }
    +
    +                    return true;
    +                })
    +                .map(client.getEventMapper({ toDevice: true }))
                     .map((toDeviceEvent) => { // map is a cheap inline forEach
                         // We want to flag m.key.verification.start events as cancelled
                         // if there's an accompanying m.key.verification.cancel event, so
    @@ -1185,6 +1198,24 @@ export class SyncApi {
                 const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
     
                 await this.processRoomEvents(room, stateEvents);
    +
    +            const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId())?.getSender();
    +            const parkedHistory = await client.crypto.cryptoStore.takeParkedSharedHistory(room.roomId);
    +            for (const parked of parkedHistory) {
    +                if (parked.senderId === inviter) {
    +                    await this.client.crypto.olmDevice.addInboundGroupSession(
    +                        room.roomId,
    +                        parked.senderKey,
    +                        parked.forwardingCurve25519KeyChain,
    +                        parked.sessionId,
    +                        parked.sessionKey,
    +                        parked.keysClaimed,
    +                        true,
    +                        { sharedHistory: true, untrusted: true },
    +                    );
    +                }
    +            }
    +
                 if (inviteObj.isBrandNewRoom) {
                     room.recalculate();
                     client.store.storeRoom(room);
    @@ -1288,7 +1319,11 @@ export class SyncApi {
                     }
                 }
     
    -            await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache);
    +            try {
    +                await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache);
    +            } catch (e) {
    +                logger.error(`Failed to process events on room ${room.roomId}:`, e);
    +            }
     
                 // set summary after processing events,
                 // because it will trigger a name calculation
    
  • src/@types/crypto.ts+20 0 added
    @@ -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;
    +};
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.