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: {} });
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
await aliTestClient.start();
|
await aliTestClient.start();
|
||||||
await bobTestClient.start();
|
await bobTestClient.start();
|
||||||
|
bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
await firstSync(aliTestClient);
|
await firstSync(aliTestClient);
|
||||||
await aliEnablesEncryption();
|
await aliEnablesEncryption();
|
||||||
await aliSendsFirstMessage();
|
await aliSendsFirstMessage();
|
||||||
@@ -504,6 +505,7 @@ describe("MatrixClient crypto", () => {
|
|||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
await aliTestClient.start();
|
await aliTestClient.start();
|
||||||
await bobTestClient.start();
|
await bobTestClient.start();
|
||||||
|
bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
await firstSync(aliTestClient);
|
await firstSync(aliTestClient);
|
||||||
await aliEnablesEncryption();
|
await aliEnablesEncryption();
|
||||||
await aliSendsFirstMessage();
|
await aliSendsFirstMessage();
|
||||||
@@ -567,6 +569,7 @@ describe("MatrixClient crypto", () => {
|
|||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
await aliTestClient.start();
|
await aliTestClient.start();
|
||||||
await bobTestClient.start();
|
await bobTestClient.start();
|
||||||
|
bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
await firstSync(aliTestClient);
|
await firstSync(aliTestClient);
|
||||||
await aliEnablesEncryption();
|
await aliEnablesEncryption();
|
||||||
await aliSendsFirstMessage();
|
await aliSendsFirstMessage();
|
||||||
@@ -584,6 +587,9 @@ describe("MatrixClient crypto", () => {
|
|||||||
await firstSync(bobTestClient);
|
await firstSync(bobTestClient);
|
||||||
await aliEnablesEncryption();
|
await aliEnablesEncryption();
|
||||||
await aliSendsFirstMessage();
|
await aliSendsFirstMessage();
|
||||||
|
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
|
200, {},
|
||||||
|
);
|
||||||
await bobRecvMessage();
|
await bobRecvMessage();
|
||||||
await bobEnablesEncryption();
|
await bobEnablesEncryption();
|
||||||
const ciphertext = await bobSendsReplyMessage();
|
const ciphertext = await bobSendsReplyMessage();
|
||||||
|
@@ -87,6 +87,8 @@ describe("MatrixClient syncing", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||||
|
await client.initCrypto();
|
||||||
|
|
||||||
const roomId = "!cycles:example.org";
|
const roomId = "!cycles:example.org";
|
||||||
|
|
||||||
// First sync: an invite
|
// First sync: an invite
|
||||||
|
@@ -29,8 +29,11 @@ import {
|
|||||||
IDownloadKeyResult,
|
IDownloadKeyResult,
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
MatrixEventEvent,
|
MatrixEventEvent,
|
||||||
|
IndexedDBCryptoStore,
|
||||||
|
Room,
|
||||||
} from "../../src/matrix";
|
} from "../../src/matrix";
|
||||||
import { IDeviceKeys } from "../../src/crypto/dehydration";
|
import { IDeviceKeys } from "../../src/crypto/dehydration";
|
||||||
|
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||||
|
|
||||||
const ROOM_ID = "!room:id";
|
const ROOM_ID = "!room:id";
|
||||||
|
|
||||||
@@ -280,10 +283,13 @@ describe("megolm", () => {
|
|||||||
|
|
||||||
it("Alice receives a megolm message", async () => {
|
it("Alice receives a megolm message", async () => {
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
// make the room_key event
|
// make the room_key event
|
||||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||||
senderKey: testSenderKey,
|
senderKey: testSenderKey,
|
||||||
@@ -326,10 +332,13 @@ describe("megolm", () => {
|
|||||||
it("Alice receives a megolm message before the session keys", async () => {
|
it("Alice receives a megolm message before the session keys", async () => {
|
||||||
// https://github.com/vector-im/element-web/issues/2273
|
// https://github.com/vector-im/element-web/issues/2273
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
// make the room_key event, but don't send it yet
|
// make the room_key event, but don't send it yet
|
||||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||||
senderKey: testSenderKey,
|
senderKey: testSenderKey,
|
||||||
@@ -383,10 +392,13 @@ describe("megolm", () => {
|
|||||||
|
|
||||||
it("Alice gets a second room_key message", async () => {
|
it("Alice gets a second room_key message", async () => {
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
// make the room_key event
|
// make the room_key event
|
||||||
const roomKeyEncrypted1 = encryptGroupSessionKey({
|
const roomKeyEncrypted1 = encryptGroupSessionKey({
|
||||||
senderKey: testSenderKey,
|
senderKey: testSenderKey,
|
||||||
@@ -468,6 +480,9 @@ describe("megolm", () => {
|
|||||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
);
|
);
|
||||||
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
|
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
|
||||||
@@ -541,13 +556,16 @@ describe("megolm", () => {
|
|||||||
|
|
||||||
logger.log('Forcing alice to download our device keys');
|
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(
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
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');
|
logger.log('Telling alice to block our device');
|
||||||
@@ -592,6 +610,9 @@ describe("megolm", () => {
|
|||||||
|
|
||||||
logger.log("Fetching bob's devices and marking known");
|
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(
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
);
|
);
|
||||||
@@ -786,6 +807,10 @@ describe("megolm", () => {
|
|||||||
logger.log('Forcing alice to download our device keys');
|
logger.log('Forcing alice to download our device keys');
|
||||||
const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||||
|
|
||||||
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
|
);
|
||||||
|
|
||||||
// so will this.
|
// so will this.
|
||||||
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
|
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -805,9 +830,12 @@ describe("megolm", () => {
|
|||||||
it("Alice exports megolm keys and imports them to a new device", async () => {
|
it("Alice exports megolm keys and imports them to a new device", async () => {
|
||||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
|
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
// establish an olm session with alice
|
// establish an olm session with alice
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
@@ -855,6 +883,8 @@ describe("megolm", () => {
|
|||||||
await aliceTestClient.client.importRoomKeys(exported);
|
await aliceTestClient.client.importRoomKeys(exported);
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
const syncResponse = {
|
const syncResponse = {
|
||||||
next_batch: 1,
|
next_batch: 1,
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -927,10 +957,13 @@ describe("megolm", () => {
|
|||||||
|
|
||||||
it("Alice can decrypt a message with falsey content", async () => {
|
it("Alice can decrypt a message with falsey content", async () => {
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
// make the room_key event
|
// make the room_key event
|
||||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||||
senderKey: testSenderKey,
|
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",
|
"should successfully decrypt bundled redaction events that don't include a room_id in their /sync data",
|
||||||
async () => {
|
async () => {
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
// make the room_key event
|
// make the room_key event
|
||||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||||
senderKey: testSenderKey,
|
senderKey: testSenderKey,
|
||||||
@@ -1045,4 +1081,283 @@ describe("megolm", () => {
|
|||||||
expect(redactionEvent.content.reason).toEqual("redaction test");
|
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,
|
makeBeaconContent,
|
||||||
makeBeaconInfoContent,
|
makeBeaconInfoContent,
|
||||||
makeTopicContent,
|
makeTopicContent,
|
||||||
|
parseBeaconContent,
|
||||||
parseTopicContent,
|
parseTopicContent,
|
||||||
} from "../../src/content-helpers";
|
} 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', () => {
|
describe('Topic content helpers', () => {
|
||||||
|
@@ -15,6 +15,8 @@ import { CRYPTO_ENABLED } from "../../src/client";
|
|||||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||||
import { logger } from '../../src/logger';
|
import { logger } from '../../src/logger';
|
||||||
import { MemoryStore } from "../../src";
|
import { MemoryStore } from "../../src";
|
||||||
|
import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||||
|
import { RoomMember } from '../../src/models/room-member';
|
||||||
import { IStore } from '../../src/store';
|
import { IStore } from '../../src/store';
|
||||||
|
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
@@ -40,20 +42,52 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
|
|||||||
type: "m.forwarded_room_key",
|
type: "m.forwarded_room_key",
|
||||||
sender: client.getUserId(),
|
sender: client.getUserId(),
|
||||||
content: {
|
content: {
|
||||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||||
room_id: roomId,
|
"room_id": roomId,
|
||||||
sender_key: eventContent.sender_key,
|
"sender_key": eventContent.sender_key,
|
||||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||||
session_id: eventContent.session_id,
|
"session_id": eventContent.session_id,
|
||||||
session_key: key.key,
|
"session_key": key.key,
|
||||||
chain_index: key.chain_index,
|
"chain_index": key.chain_index,
|
||||||
forwarding_curve25519_key_chain:
|
"forwarding_curve25519_key_chain": key.forwarding_curve_key_chain,
|
||||||
key.forwarding_curve_key_chain,
|
"org.matrix.msc3061.shared_history": true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// make onRoomKeyEvent think this was an encrypted event
|
// make onRoomKeyEvent think this was an encrypted event
|
||||||
// @ts-ignore private property
|
// @ts-ignore private property
|
||||||
ksEvent.senderCurve25519Key = "akey";
|
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;
|
return ksEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +129,7 @@ describe("Crypto", function() {
|
|||||||
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||||
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
|
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
|
||||||
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
||||||
event.isKeySourceUntrusted = () => false;
|
event.isKeySourceUntrusted = () => true;
|
||||||
event.getClaimedEd25519Key =
|
event.getClaimedEd25519Key =
|
||||||
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||||
|
|
||||||
@@ -233,6 +267,7 @@ describe("Crypto", function() {
|
|||||||
describe('Key requests', function() {
|
describe('Key requests', function() {
|
||||||
let aliceClient: MatrixClient;
|
let aliceClient: MatrixClient;
|
||||||
let bobClient: MatrixClient;
|
let bobClient: MatrixClient;
|
||||||
|
let claraClient: MatrixClient;
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
aliceClient = (new TestClient(
|
aliceClient = (new TestClient(
|
||||||
@@ -241,22 +276,35 @@ describe("Crypto", function() {
|
|||||||
bobClient = (new TestClient(
|
bobClient = (new TestClient(
|
||||||
"@bob:example.com", "bobdevice",
|
"@bob:example.com", "bobdevice",
|
||||||
)).client;
|
)).client;
|
||||||
|
claraClient = (new TestClient(
|
||||||
|
"@clara:example.com", "claradevice",
|
||||||
|
)).client;
|
||||||
await aliceClient.initCrypto();
|
await aliceClient.initCrypto();
|
||||||
await bobClient.initCrypto();
|
await bobClient.initCrypto();
|
||||||
|
await claraClient.initCrypto();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function() {
|
afterEach(async function() {
|
||||||
aliceClient.stopClient();
|
aliceClient.stopClient();
|
||||||
bobClient.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 = {
|
const encryptionCfg = {
|
||||||
"algorithm": "m.megolm.v1.aes-sha2",
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
};
|
};
|
||||||
const roomId = "!someroom";
|
const roomId = "!someroom";
|
||||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||||
const bobRoom = new Room(roomId, bobClient, "@bob: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);
|
aliceClient.store.storeRoom(aliceRoom);
|
||||||
bobClient.store.storeRoom(bobRoom);
|
bobClient.store.storeRoom(bobRoom);
|
||||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
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(
|
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||||
);
|
);
|
||||||
@@ -314,6 +365,8 @@ describe("Crypto", function() {
|
|||||||
// the first message can't be decrypted yet, but the second one
|
// the first message can't be decrypted yet, but the second one
|
||||||
// can
|
// can
|
||||||
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
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 bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||||
await decryptEventsPromise;
|
await decryptEventsPromise;
|
||||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||||
@@ -340,8 +393,24 @@ describe("Crypto", function() {
|
|||||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||||
await decryptEventPromise;
|
await decryptEventPromise;
|
||||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||||
|
expect(events[0].isKeySourceUntrusted()).toBeTruthy();
|
||||||
await sleep(1);
|
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();
|
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -383,6 +452,9 @@ describe("Crypto", function() {
|
|||||||
// decryption keys yet
|
// decryption keys yet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const device = new DeviceInfo(aliceClient.deviceId);
|
||||||
|
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||||
|
|
||||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||||
);
|
);
|
||||||
@@ -462,6 +534,420 @@ describe("Crypto", function() {
|
|||||||
expect(aliceSendToDevice).toBeCalledTimes(3);
|
expect(aliceSendToDevice).toBeCalledTimes(3);
|
||||||
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
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() {
|
describe('Secret storage', function() {
|
||||||
|
@@ -110,6 +110,12 @@ describe("MegolmDecryption", function() {
|
|||||||
senderCurve25519Key: "SENDER_CURVE25519",
|
senderCurve25519Key: "SENDER_CURVE25519",
|
||||||
claimedEd25519Key: "SENDER_ED25519",
|
claimedEd25519Key: "SENDER_ED25519",
|
||||||
};
|
};
|
||||||
|
event.getWireType = () => "m.room.encrypted";
|
||||||
|
event.getWireContent = () => {
|
||||||
|
return {
|
||||||
|
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const mockCrypto = {
|
const mockCrypto = {
|
||||||
decryptEvent: function() {
|
decryptEvent: function() {
|
||||||
|
@@ -214,6 +214,12 @@ describe("MegolmBackup", function() {
|
|||||||
const event = new MatrixEvent({
|
const event = new MatrixEvent({
|
||||||
type: 'm.room.encrypted',
|
type: 'm.room.encrypted',
|
||||||
});
|
});
|
||||||
|
event.getWireType = () => "m.room.encrypted";
|
||||||
|
event.getWireContent = () => {
|
||||||
|
return {
|
||||||
|
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||||
|
};
|
||||||
|
};
|
||||||
const decryptedData = {
|
const decryptedData = {
|
||||||
clearEvent: {
|
clearEvent: {
|
||||||
type: 'm.room_key',
|
type: 'm.room_key',
|
||||||
|
@@ -26,6 +26,7 @@ import { logger } from '../../../src/logger';
|
|||||||
import * as utils from "../../../src/utils";
|
import * as utils from "../../../src/utils";
|
||||||
import { ICreateClientOpts } from '../../../src/client';
|
import { ICreateClientOpts } from '../../../src/client';
|
||||||
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
|
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
|
||||||
|
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
@@ -257,6 +258,7 @@ describe("Secrets", function() {
|
|||||||
"ed25519:VAX": vaxDevice.deviceEd25519Key,
|
"ed25519:VAX": vaxDevice.deviceEd25519Key,
|
||||||
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
|
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
|
||||||
},
|
},
|
||||||
|
verified: DeviceInfo.DeviceVerification.VERIFIED,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||||
@@ -280,10 +282,12 @@ describe("Secrets", function() {
|
|||||||
Object.values(otks)[0],
|
Object.values(otks)[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
const request = await secretStorage.request("foo", ["VAX"]);
|
osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const secret = await request.promise;
|
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();
|
osborne2.stop();
|
||||||
vax.stop();
|
vax.stop();
|
||||||
clearTestClientTimeouts();
|
clearTestClientTimeouts();
|
||||||
|
@@ -464,7 +464,7 @@ describe("SAS verification", function() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
alice.client.setDeviceVerified = jest.fn();
|
alice.client.crypto.setDeviceVerification = jest.fn();
|
||||||
alice.client.getDeviceEd25519Key = () => {
|
alice.client.getDeviceEd25519Key = () => {
|
||||||
return "alice+base64+ed25519+key";
|
return "alice+base64+ed25519+key";
|
||||||
};
|
};
|
||||||
@@ -482,7 +482,7 @@ describe("SAS verification", function() {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
bob.client.setDeviceVerified = jest.fn();
|
bob.client.crypto.setDeviceVerification = jest.fn();
|
||||||
bob.client.getStoredDevice = () => {
|
bob.client.getStoredDevice = () => {
|
||||||
return DeviceInfo.fromStorage(
|
return DeviceInfo.fromStorage(
|
||||||
{
|
{
|
||||||
@@ -565,10 +565,24 @@ describe("SAS verification", function() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// make sure Alice and Bob verified each other
|
// make sure Alice and Bob verified each other
|
||||||
expect(alice.client.setDeviceVerified)
|
expect(alice.client.crypto.setDeviceVerification)
|
||||||
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
|
.toHaveBeenCalledWith(
|
||||||
expect(bob.client.setDeviceVerified)
|
bob.client.getUserId(),
|
||||||
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
|
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,
|
BeaconEvent,
|
||||||
} from "../../../src/models/beacon";
|
} from "../../../src/models/beacon";
|
||||||
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
|
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
|
||||||
|
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
@@ -431,6 +432,27 @@ describe('Beacon', () => {
|
|||||||
expect(emitSpy).not.toHaveBeenCalled();
|
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', () => {
|
describe('when beacon is live with a start timestamp is in the future', () => {
|
||||||
it('ignores locations before the beacon start timestamp', () => {
|
it('ignores locations before the beacon start timestamp', () => {
|
||||||
const startTimestamp = now + 60000;
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
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
|
* 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.
|
* 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
|
// 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.
|
// 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;
|
let val: T;
|
||||||
if (this.name) {
|
if (this.name) {
|
||||||
val = obj?.[this.name];
|
val = obj?.[this.name];
|
||||||
|
@@ -5287,6 +5287,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @param {object} [options]
|
* @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.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.decrypt decrypt event proactively
|
||||||
|
* @param {boolean} options.toDevice the event is a to_device event
|
||||||
* @return {Function}
|
* @return {Function}
|
||||||
*/
|
*/
|
||||||
public getEventMapper(options?: MapperOpts): EventMapper {
|
public getEventMapper(options?: MapperOpts): EventMapper {
|
||||||
|
@@ -292,16 +292,17 @@ export const makeBeaconContent: MakeBeaconContent = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type BeaconLocationState = MLocationContent & {
|
export type BeaconLocationState = MLocationContent & {
|
||||||
timestamp: number;
|
uri?: string; // override from MLocationContent to allow optionals
|
||||||
|
timestamp?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
|
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);
|
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description,
|
description: location?.description,
|
||||||
uri,
|
uri: location?.uri,
|
||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -23,6 +23,7 @@ import * as algorithms from './algorithms';
|
|||||||
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
|
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
|
||||||
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
|
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
|
||||||
import { IMegolmSessionData } from "./index";
|
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
|
// 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.
|
// reasonable approximation to the biggest plaintext we can encrypt.
|
||||||
@@ -122,6 +123,7 @@ interface IInboundGroupSessionKey {
|
|||||||
forwarding_curve25519_key_chain: string[];
|
forwarding_curve25519_key_chain: string[];
|
||||||
sender_claimed_ed25519_key: string;
|
sender_claimed_ed25519_key: string;
|
||||||
shared_history: boolean;
|
shared_history: boolean;
|
||||||
|
untrusted: boolean;
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
@@ -1101,7 +1103,7 @@ export class OlmDevice {
|
|||||||
sessionKey: string,
|
sessionKey: string,
|
||||||
keysClaimed: Record<string, string>,
|
keysClaimed: Record<string, string>,
|
||||||
exportFormat: boolean,
|
exportFormat: boolean,
|
||||||
extraSessionData: Record<string, any> = {},
|
extraSessionData: OlmGroupSessionExtraData = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.cryptoStore.doTxn(
|
await this.cryptoStore.doTxn(
|
||||||
'readwrite', [
|
'readwrite', [
|
||||||
@@ -1133,18 +1135,43 @@ export class OlmDevice {
|
|||||||
"Update for megolm session "
|
"Update for megolm session "
|
||||||
+ senderKey + "/" + sessionId,
|
+ senderKey + "/" + sessionId,
|
||||||
);
|
);
|
||||||
if (existingSession.first_known_index()
|
if (existingSession.first_known_index() <= session.first_known_index()) {
|
||||||
<= session.first_known_index()
|
if (!existingSessionData.untrusted || extraSessionData.untrusted) {
|
||||||
&& !(existingSession.first_known_index() == session.first_known_index()
|
// existing session has less-than-or-equal index
|
||||||
&& !extraSessionData.untrusted
|
// (i.e. can decrypt at least as much), and the
|
||||||
&& existingSessionData.untrusted)) {
|
// new session's trust does not win over the old
|
||||||
// existing session has lower index (i.e. can
|
// session's trust, so keep it
|
||||||
// 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}`);
|
logger.log(`Keeping existing megolm session ${sessionId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (existingSession.first_known_index() < session.first_known_index()) {
|
||||||
|
// We want to upgrade the existing session's trust,
|
||||||
|
// but we can't just use the new session because we'll
|
||||||
|
// lose the lower index. Check that the sessions connect
|
||||||
|
// properly, and then manually set the existing session
|
||||||
|
// as trusted.
|
||||||
|
if (
|
||||||
|
existingSession.export_session(session.first_known_index())
|
||||||
|
=== session.export_session(session.first_known_index())
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
"Upgrading trust of existing megolm session " +
|
||||||
|
sessionId + " based on newly-received trusted session",
|
||||||
|
);
|
||||||
|
existingSessionData.untrusted = false;
|
||||||
|
this.cryptoStore.storeEndToEndInboundGroupSession(
|
||||||
|
senderKey, sessionId, existingSessionData, txn,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
"Newly-received megolm session " + sessionId +
|
||||||
|
" does not match existing session! Keeping existing session",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If the sessions have the same index, go ahead and store the new trusted one.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -1427,13 +1454,23 @@ export class OlmDevice {
|
|||||||
const claimedKeys = sessionData.keysClaimed || {};
|
const claimedKeys = sessionData.keysClaimed || {};
|
||||||
const senderEd25519Key = claimedKeys.ed25519 || null;
|
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 = {
|
result = {
|
||||||
"chain_index": chainIndex,
|
"chain_index": chainIndex,
|
||||||
"key": exportedSession,
|
"key": exportedSession,
|
||||||
"forwarding_curve25519_key_chain":
|
"forwarding_curve25519_key_chain": forwardingKeyChain,
|
||||||
sessionData.forwardingCurve25519KeyChain || [],
|
|
||||||
"sender_claimed_ed25519_key": senderEd25519Key,
|
"sender_claimed_ed25519_key": senderEd25519Key,
|
||||||
"shared_history": sessionData.sharedHistory || false,
|
"shared_history": sessionData.sharedHistory || false,
|
||||||
|
"untrusted": untrusted,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@@ -539,7 +539,23 @@ export class SecretStorage {
|
|||||||
// because someone could be trying to send us bogus data
|
// because someone could be trying to send us bogus data
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!olmlib.isOlmEncrypted(event)) {
|
||||||
|
logger.error("secret event not properly encrypted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const content = event.getContent();
|
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);
|
logger.log("got secret share for request", content.request_id);
|
||||||
const requestControl = this.requests.get(content.request_id);
|
const requestControl = this.requests.get(content.request_id);
|
||||||
if (requestControl) {
|
if (requestControl) {
|
||||||
@@ -559,6 +575,14 @@ export class SecretStorage {
|
|||||||
logger.log("unsolicited secret share from device", deviceInfo.deviceId);
|
logger.log("unsolicited secret share from device", deviceInfo.deviceId);
|
||||||
return;
|
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(
|
logger.log(
|
||||||
`Successfully received secret ${requestControl.name} ` +
|
`Successfully received secret ${requestControl.name} ` +
|
||||||
|
@@ -35,8 +35,10 @@ import { Room } from '../../models/room';
|
|||||||
import { DeviceInfo } from "../deviceinfo";
|
import { DeviceInfo } from "../deviceinfo";
|
||||||
import { IOlmSessionResult } from "../olmlib";
|
import { IOlmSessionResult } from "../olmlib";
|
||||||
import { DeviceInfoMap } from "../DeviceList";
|
import { DeviceInfoMap } from "../DeviceList";
|
||||||
import { MatrixEvent } from "../..";
|
import { MatrixEvent } from "../../models/event";
|
||||||
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
||||||
|
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
|
||||||
|
import { OlmGroupSessionExtraData } from "../../@types/crypto";
|
||||||
|
|
||||||
// determine whether the key can be shared with invitees
|
// determine whether the key can be shared with invitees
|
||||||
export function isRoomSharedHistory(room: Room): boolean {
|
export function isRoomSharedHistory(room: Room): boolean {
|
||||||
@@ -1189,8 +1191,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
* {@link module:crypto/algorithms/DecryptionAlgorithm}
|
* {@link module:crypto/algorithms/DecryptionAlgorithm}
|
||||||
*/
|
*/
|
||||||
class MegolmDecryption extends DecryptionAlgorithm {
|
class MegolmDecryption extends DecryptionAlgorithm {
|
||||||
// events which we couldn't decrypt due to unknown sessions / indexes: map from
|
// events which we couldn't decrypt due to unknown sessions /
|
||||||
// senderKey|sessionId to Set of MatrixEvents
|
// 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>>>();
|
private pendingEvents = new Map<string, Map<string, Set<MatrixEvent>>>();
|
||||||
|
|
||||||
// this gets stubbed out by the unit tests.
|
// 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
|
// Success. We can remove the event from the pending list, if
|
||||||
// already happened.
|
// 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);
|
this.removeEventFromPendingList(event);
|
||||||
|
}
|
||||||
|
|
||||||
const payload = JSON.parse(res.result);
|
const payload = JSON.parse(res.result);
|
||||||
|
|
||||||
@@ -1391,6 +1398,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
let exportFormat = false;
|
let exportFormat = false;
|
||||||
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
|
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
|
||||||
|
|
||||||
|
const extraSessionData: OlmGroupSessionExtraData = {};
|
||||||
|
|
||||||
if (!content.room_id ||
|
if (!content.room_id ||
|
||||||
!content.session_key ||
|
!content.session_key ||
|
||||||
!content.session_id ||
|
!content.session_id ||
|
||||||
@@ -1400,12 +1409,58 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!senderKey) {
|
if (!olmlib.isOlmEncrypted(event)) {
|
||||||
logger.error("key event has no sender key (not encrypted?)");
|
logger.error("key event not properly encrypted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (content["org.matrix.msc3061.shared_history"]) {
|
||||||
|
extraSessionData.sharedHistory = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.getType() == "m.forwarded_room_key") {
|
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;
|
exportFormat = true;
|
||||||
forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ?
|
forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ?
|
||||||
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");
|
logger.error("forwarded_room_key event is missing sender_key field");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
senderKey = content.sender_key;
|
|
||||||
|
|
||||||
const ed25519Key = content.sender_claimed_ed25519_key;
|
const ed25519Key = content.sender_claimed_ed25519_key;
|
||||||
if (!ed25519Key) {
|
if (!ed25519Key) {
|
||||||
@@ -1431,11 +1485,45 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
keysClaimed = {
|
keysClaimed = {
|
||||||
ed25519: ed25519Key,
|
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 {
|
} else {
|
||||||
keysClaimed = event.getKeysClaimed();
|
keysClaimed = event.getKeysClaimed();
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraSessionData: any = {};
|
|
||||||
if (content["org.matrix.msc3061.shared_history"]) {
|
if (content["org.matrix.msc3061.shared_history"]) {
|
||||||
extraSessionData.sharedHistory = true;
|
extraSessionData.sharedHistory = true;
|
||||||
}
|
}
|
||||||
@@ -1453,7 +1541,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// have another go at decrypting events sent with this session.
|
// 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.
|
// cancel any outstanding room key requests for this session.
|
||||||
// Only do this if we managed to decrypt every message in the
|
// Only do this if we managed to decrypt every message in the
|
||||||
// session, because if we didn't, we leave the other key
|
// session, because if we didn't, we leave the other key
|
||||||
@@ -1668,7 +1756,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
session: IMegolmSessionData,
|
session: IMegolmSessionData,
|
||||||
opts: { untrusted?: boolean, source?: string } = {},
|
opts: { untrusted?: boolean, source?: string } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const extraSessionData: any = {};
|
const extraSessionData: OlmGroupSessionExtraData = {};
|
||||||
if (opts.untrusted || session.untrusted) {
|
if (opts.untrusted || session.untrusted) {
|
||||||
extraSessionData.untrusted = true;
|
extraSessionData.untrusted = true;
|
||||||
}
|
}
|
||||||
@@ -1696,7 +1784,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// have another go at decrypting events sent with this session.
|
// 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
|
* @private
|
||||||
* @param {String} senderKey
|
* @param {String} senderKey
|
||||||
* @param {String} sessionId
|
* @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);
|
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||||
if (!senderPendingEvents) {
|
if (!senderPendingEvents) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1725,13 +1815,14 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
|
|
||||||
await Promise.all([...pending].map(async (ev) => {
|
await Promise.all([...pending].map(async (ev) => {
|
||||||
try {
|
try {
|
||||||
await ev.attemptDecryption(this.crypto, { isRetry: true });
|
await ev.attemptDecryption(this.crypto, { isRetry: true, keyTrusted });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// don't die if something goes wrong
|
// 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);
|
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
|
// check that the original sender matches what the homeserver told us, to
|
||||||
// avoid people masquerading as others.
|
// avoid people masquerading as others.
|
||||||
// (this check is also provided via the sender's embedded ed25519 key,
|
// (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;
|
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
|
* @param {?boolean} known whether to mark that the user has been made aware of
|
||||||
* the existence of this device. Null to leave unchanged
|
* 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
|
* @return {Promise<module:crypto/deviceinfo>} updated DeviceInfo
|
||||||
*/
|
*/
|
||||||
public async setDeviceVerification(
|
public async setDeviceVerification(
|
||||||
@@ -2113,6 +2117,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
verified?: boolean,
|
verified?: boolean,
|
||||||
blocked?: boolean,
|
blocked?: boolean,
|
||||||
known?: boolean,
|
known?: boolean,
|
||||||
|
keys?: Record<string, string>,
|
||||||
): Promise<DeviceInfo | CrossSigningInfo> {
|
): Promise<DeviceInfo | CrossSigningInfo> {
|
||||||
// get rid of any `undefined`s here so we can just check
|
// get rid of any `undefined`s here so we can just check
|
||||||
// for null rather than null or undefined
|
// for null rather than null or undefined
|
||||||
@@ -2131,6 +2136,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
if (!verified) {
|
if (!verified) {
|
||||||
throw new Error("Cannot set a cross-signing key as unverified");
|
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) {
|
if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
|
||||||
this.storeTrustedSelfKeys(xsk.keys);
|
this.storeTrustedSelfKeys(xsk.keys);
|
||||||
@@ -2191,6 +2200,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
let verificationStatus = dev.verified;
|
let verificationStatus = dev.verified;
|
||||||
|
|
||||||
if (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;
|
verificationStatus = DeviceVerification.VERIFIED;
|
||||||
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
|
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
|
||||||
verificationStatus = DeviceVerification.UNVERIFIED;
|
verificationStatus = DeviceVerification.UNVERIFIED;
|
||||||
@@ -2400,13 +2416,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
return null;
|
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()) {
|
if (event.isKeySourceUntrusted()) {
|
||||||
// we got the key for this event from a source that we consider untrusted
|
// we got the key for this event from a source that we consider untrusted
|
||||||
return null;
|
return null;
|
||||||
@@ -2478,8 +2487,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
}
|
}
|
||||||
ret.encrypted = true;
|
ret.encrypted = true;
|
||||||
|
|
||||||
const forwardingChain = event.getForwardingCurve25519KeyChain();
|
if (event.isKeySourceUntrusted()) {
|
||||||
if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
|
|
||||||
// we got the key this event from somewhere else
|
// we got the key this event from somewhere else
|
||||||
// TODO: check if we can trust the forwarders.
|
// TODO: check if we can trust the forwarders.
|
||||||
ret.authenticated = false;
|
ret.authenticated = false;
|
||||||
|
@@ -30,6 +30,8 @@ import { logger } from '../logger';
|
|||||||
import { IOneTimeKey } from "./dehydration";
|
import { IOneTimeKey } from "./dehydration";
|
||||||
import { IClaimOTKsResult, MatrixClient } from "../client";
|
import { IClaimOTKsResult, MatrixClient } from "../client";
|
||||||
import { ISignatures } from "../@types/signed";
|
import { ISignatures } from "../@types/signed";
|
||||||
|
import { MatrixEvent } from "../models/event";
|
||||||
|
import { EventType } from "../@types/event";
|
||||||
|
|
||||||
enum Algorithm {
|
enum Algorithm {
|
||||||
Olm = "m.olm.v1.curve25519-aes-sha2",
|
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.
|
* Encode a typed array of uint8 as base64.
|
||||||
* @param {Uint8Array} uint8Array The data to encode.
|
* @param {Uint8Array} uint8Array The data to encode.
|
||||||
|
@@ -25,6 +25,7 @@ import { ICrossSigningInfo } from "../CrossSigning";
|
|||||||
import { PrefixedLogger } from "../../logger";
|
import { PrefixedLogger } from "../../logger";
|
||||||
import { InboundGroupSessionData } from "../OlmDevice";
|
import { InboundGroupSessionData } from "../OlmDevice";
|
||||||
import { IEncryptedPayload } from "../aes";
|
import { IEncryptedPayload } from "../aes";
|
||||||
|
import { MatrixEvent } from "../../models/event";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal module. Definitions for storage for the crypto module
|
* Internal module. Definitions for storage for the crypto module
|
||||||
@@ -127,6 +128,8 @@ export interface CryptoStore {
|
|||||||
roomId: string,
|
roomId: string,
|
||||||
txn?: unknown,
|
txn?: unknown,
|
||||||
): Promise<[senderKey: string, sessionId: string][]>;
|
): Promise<[senderKey: string, sessionId: string][]>;
|
||||||
|
addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
|
||||||
|
takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
|
||||||
|
|
||||||
// Session key backups
|
// Session key backups
|
||||||
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
|
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
|
||||||
@@ -203,3 +206,12 @@ export interface OutgoingRoomKeyRequest {
|
|||||||
requestBody: IRoomKeyRequestBody;
|
requestBody: IRoomKeyRequestBody;
|
||||||
state: RoomKeyRequestState;
|
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,
|
IWithheld,
|
||||||
Mode,
|
Mode,
|
||||||
OutgoingRoomKeyRequest,
|
OutgoingRoomKeyRequest,
|
||||||
|
ParkedSharedHistory,
|
||||||
} from "./base";
|
} from "./base";
|
||||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||||
import { ICrossSigningKey } from "../../client";
|
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>(
|
public doTxn<T>(
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
stores: string | string[],
|
stores: string | string[],
|
||||||
@@ -958,6 +1002,11 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
|
|||||||
keyPath: ["roomId"],
|
keyPath: ["roomId"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 11) {
|
||||||
|
db.createObjectStore("parked_shared_history", {
|
||||||
|
keyPath: ["roomId"],
|
||||||
|
});
|
||||||
|
}
|
||||||
// Expand as needed.
|
// Expand as needed.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -29,6 +29,7 @@ import {
|
|||||||
IWithheld,
|
IWithheld,
|
||||||
Mode,
|
Mode,
|
||||||
OutgoingRoomKeyRequest,
|
OutgoingRoomKeyRequest,
|
||||||
|
ParkedSharedHistory,
|
||||||
} from "./base";
|
} from "./base";
|
||||||
import { IRoomKeyRequestBody } from "../index";
|
import { IRoomKeyRequestBody } from "../index";
|
||||||
import { ICrossSigningKey } from "../../client";
|
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 = 'inbound_group_sessions';
|
||||||
public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld';
|
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_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_DEVICE_DATA = 'device_data';
|
||||||
public static STORE_ROOMS = 'rooms';
|
public static STORE_ROOMS = 'rooms';
|
||||||
public static STORE_BACKUP = 'sessions_needing_backup';
|
public static STORE_BACKUP = 'sessions_needing_backup';
|
||||||
@@ -669,6 +671,27 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
|||||||
return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
|
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
|
* Perform a transaction on the crypto store. Any store methods
|
||||||
* that require a transaction (txn) object to be passed in may
|
* that require a transaction (txn) object to be passed in may
|
||||||
|
@@ -25,6 +25,7 @@ import {
|
|||||||
IWithheld,
|
IWithheld,
|
||||||
Mode,
|
Mode,
|
||||||
OutgoingRoomKeyRequest,
|
OutgoingRoomKeyRequest,
|
||||||
|
ParkedSharedHistory,
|
||||||
} from "./base";
|
} from "./base";
|
||||||
import { IRoomKeyRequestBody } from "../index";
|
import { IRoomKeyRequestBody } from "../index";
|
||||||
import { ICrossSigningKey } from "../../client";
|
import { ICrossSigningKey } from "../../client";
|
||||||
@@ -58,6 +59,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
|||||||
private rooms: { [roomId: string]: IRoomEncryption } = {};
|
private rooms: { [roomId: string]: IRoomEncryption } = {};
|
||||||
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
|
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
|
||||||
private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
|
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.
|
* Ensure the database exists and is up-to-date.
|
||||||
@@ -526,6 +528,18 @@ export class MemoryCryptoStore implements CryptoStore {
|
|||||||
return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
|
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
|
// Session key backups
|
||||||
|
|
||||||
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
|
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) {
|
if (this.doVerification && !this.started) {
|
||||||
this.started = true;
|
this.started = true;
|
||||||
this.resetTimer(); // restart the timeout
|
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;
|
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
|
// 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
|
// not know about all of them, so keep track of the keys that we know
|
||||||
// about, and ignore the rest
|
// about, and ignore the rest
|
||||||
const verifiedDevices = [];
|
const verifiedDevices: [string, string, string][] = [];
|
||||||
|
|
||||||
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
||||||
const deviceId = keyId.split(':', 2)[1];
|
const deviceId = keyId.split(':', 2)[1];
|
||||||
const device = this.baseApis.getStoredDevice(userId, deviceId);
|
const device = this.baseApis.getStoredDevice(userId, deviceId);
|
||||||
if (device) {
|
if (device) {
|
||||||
verifier(keyId, device, keyInfo);
|
verifier(keyId, device, keyInfo);
|
||||||
verifiedDevices.push(deviceId);
|
verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
|
||||||
} else {
|
} else {
|
||||||
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
|
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
|
||||||
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
|
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
|
||||||
@@ -326,7 +332,7 @@ export class VerificationBase<
|
|||||||
[keyId]: deviceId,
|
[keyId]: deviceId,
|
||||||
},
|
},
|
||||||
}, deviceId), keyInfo);
|
}, deviceId), keyInfo);
|
||||||
verifiedDevices.push(deviceId);
|
verifiedDevices.push([deviceId, keyId, deviceId]);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`verification: Could not find device ${deviceId} to verify`,
|
`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
|
// 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
|
// to upload each signature in a separate API call which is silly because the
|
||||||
// API supports as many signatures as you like.
|
// API supports as many signatures as you like.
|
||||||
for (const deviceId of verifiedDevices) {
|
for (const [deviceId, keyId, key] of verifiedDevices) {
|
||||||
await this.baseApis.setDeviceVerified(userId, deviceId);
|
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 {
|
export interface MapperOpts {
|
||||||
preventReEmit?: boolean;
|
preventReEmit?: boolean;
|
||||||
decrypt?: boolean;
|
decrypt?: boolean;
|
||||||
|
toDevice?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
|
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;
|
const decrypt = options.decrypt !== false;
|
||||||
|
|
||||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||||
|
if (options.toDevice) {
|
||||||
|
delete plainOldJsObject.room_id;
|
||||||
|
}
|
||||||
|
|
||||||
const room = client.getRoom(plainOldJsObject.room_id);
|
const room = client.getRoom(plainOldJsObject.room_id);
|
||||||
|
|
||||||
let event: MatrixEvent;
|
let event: MatrixEvent;
|
||||||
|
@@ -15,7 +15,6 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { MBeaconEventContent } from "../@types/beacon";
|
import { MBeaconEventContent } from "../@types/beacon";
|
||||||
import { M_TIMESTAMP } from "../@types/location";
|
|
||||||
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
|
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
|
||||||
import { MatrixEvent } from "../matrix";
|
import { MatrixEvent } from "../matrix";
|
||||||
import { sortEventsByLatestContentTimestamp } from "../utils";
|
import { sortEventsByLatestContentTimestamp } from "../utils";
|
||||||
@@ -161,7 +160,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
|
|||||||
|
|
||||||
const validLocationEvents = beaconLocationEvents.filter(event => {
|
const validLocationEvents = beaconLocationEvents.filter(event => {
|
||||||
const content = event.getContent<MBeaconEventContent>();
|
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 (
|
return (
|
||||||
// only include positions that were taken inside the beacon's live period
|
// only include positions that were taken inside the beacon's live period
|
||||||
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&
|
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&
|
||||||
|
@@ -151,6 +151,7 @@ interface IKeyRequestRecipient {
|
|||||||
export interface IDecryptOptions {
|
export interface IDecryptOptions {
|
||||||
emit?: boolean;
|
emit?: boolean;
|
||||||
isRetry?: 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");
|
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.
|
// we may want to just ignore this? let's start with rejecting it.
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempt to decrypt event which has already been decrypted",
|
"Attempt to decrypt event which has already been decrypted",
|
||||||
|
37
src/sync.ts
37
src/sync.ts
@@ -1109,7 +1109,20 @@ export class SyncApi {
|
|||||||
if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) {
|
if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) {
|
||||||
const cancelledKeyVerificationTxns = [];
|
const cancelledKeyVerificationTxns = [];
|
||||||
data.to_device.events
|
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
|
.map((toDeviceEvent) => { // map is a cheap inline forEach
|
||||||
// We want to flag m.key.verification.start events as cancelled
|
// We want to flag m.key.verification.start events as cancelled
|
||||||
// if there's an accompanying m.key.verification.cancel event, so
|
// 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);
|
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
|
||||||
|
|
||||||
await this.processRoomEvents(room, stateEvents);
|
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) {
|
if (inviteObj.isBrandNewRoom) {
|
||||||
room.recalculate();
|
room.recalculate();
|
||||||
client.store.storeRoom(room);
|
client.store.storeRoom(room);
|
||||||
@@ -1288,7 +1319,11 @@ export class SyncApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache);
|
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,
|
// set summary after processing events,
|
||||||
// because it will trigger a name calculation
|
// because it will trigger a name calculation
|
||||||
|
Reference in New Issue
Block a user