You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-07 23:02:56 +03:00
Resolve multiple CVEs
CVE-2022-39249 CVE-2022-39250 CVE-2022-39251 CVE-2022-39236
This commit is contained in:
@@ -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();
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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 be gone since we've now decrypted everything
|
||||
// the room key request should still be there, since we've
|
||||
// decrypted everything with an untrusted key
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined();
|
||||
|
||||
// Now share a trusted room key event so Bob will re-decrypt the messages.
|
||||
// Bob will backfill trust when they receive a trusted session with a higher
|
||||
// index that connects to an untrusted session with a lower index.
|
||||
const roomKeyEvent = roomKeyEventForEvent(aliceClient, events[1]);
|
||||
const trustedDecryptEventPromise = awaitEvent(events[0], "Event.decrypted");
|
||||
await bobDecryptor.onRoomKeyEvent(roomKeyEvent);
|
||||
await trustedDecryptEventPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[0].isKeySourceUntrusted()).toBeFalsy();
|
||||
await sleep(1);
|
||||
// now the room key request should be gone, since there's
|
||||
// no better key to wait for
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
@@ -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() {
|
||||
|
@@ -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',
|
||||
|
@@ -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],
|
||||
);
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
const secret = await request.promise;
|
||||
osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
await request.promise; // return value not used
|
||||
|
||||
expect(secret).toBe("bar");
|
||||
osborne2.stop();
|
||||
vax.stop();
|
||||
clearTestClientTimeouts();
|
||||
|
@@ -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" },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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;
|
||||
|
20
src/@types/crypto.ts
Normal file
20
src/@types/crypto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export type OlmGroupSessionExtraData = {
|
||||
untrusted?: boolean;
|
||||
sharedHistory?: boolean;
|
||||
};
|
@@ -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];
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
};
|
||||
};
|
||||
|
@@ -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,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
@@ -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} ` +
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -431,7 +431,6 @@ export class BackupManager {
|
||||
)
|
||||
);
|
||||
});
|
||||
ret.usable = ret.usable || ret.trusted_locally;
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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.
|
||||
|
@@ -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[];
|
||||
}
|
||||
|
@@ -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.
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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> {
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -22,6 +22,7 @@ export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
|
||||
export interface MapperOpts {
|
||||
preventReEmit?: boolean;
|
||||
decrypt?: boolean;
|
||||
toDevice?: boolean;
|
||||
}
|
||||
|
||||
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
|
||||
@@ -29,6 +30,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
const decrypt = options.decrypt !== false;
|
||||
|
||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||
if (options.toDevice) {
|
||||
delete plainOldJsObject.room_id;
|
||||
}
|
||||
|
||||
const room = client.getRoom(plainOldJsObject.room_id);
|
||||
|
||||
let event: MatrixEvent;
|
||||
|
@@ -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) &&
|
||||
|
@@ -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",
|
||||
|
39
src/sync.ts
39
src/sync.ts
@@ -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
|
||||
|
Reference in New Issue
Block a user