Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 858db67778 | |||
| e230abee45 | |||
| 1476ffbd15 | |||
| 62f0a65472 | |||
| 2ef7ae7661 | |||
| 61c0a49971 | |||
| 2172f28888 | |||
| 2e9b34e0c3 | |||
| 5a782b7377 | |||
| 54bc807056 | |||
| 9e07710d80 | |||
| e9ed91d800 | |||
| 88ba4fad71 | |||
| 21b3471453 | |||
| 0ada9803ab | |||
| 1744f0e97b | |||
| fd0c4a7f56 | |||
| 615f7f9e72 | |||
| 77259e81c9 | |||
| 2193cd9d1c | |||
| 6d28154dcd | |||
| 83d447adfe | |||
| 73c9f4e322 | |||
| e6fa4cdb3c | |||
| a04653a72c | |||
| 5f9341f39c | |||
| 906946c419 | |||
| 4397b9d640 |
@@ -15,7 +15,7 @@ concurrency:
|
||||
jobs:
|
||||
cypress:
|
||||
name: Cypress
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.75.0
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.76.0
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
|
||||
@@ -21,7 +21,7 @@ concurrency:
|
||||
jobs:
|
||||
build-element-web:
|
||||
name: Build element-web
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.75.0
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.76.0
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
|
||||
+19
-2
@@ -1,5 +1,22 @@
|
||||
Changes in [27.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.1.0-rc.1) (2023-07-27)
|
||||
============================================================================================================
|
||||
Changes in [27.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.2.0) (2023-08-15)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
|
||||
|
||||
## ✨ Features
|
||||
* Allow knocking rooms ([\#3647](https://github.com/matrix-org/matrix-js-sdk/pull/3647)). Contributed by @charlynguyen.
|
||||
* Bump pagination limit to account for threaded events ([\#3638](https://github.com/matrix-org/matrix-js-sdk/pull/3638)).
|
||||
* ElementR: Add `CryptoApi.findVerificationRequestDMInProgress` ([\#3601](https://github.com/matrix-org/matrix-js-sdk/pull/3601)). Contributed by @florianduros.
|
||||
* Export more into the public interface ([\#3614](https://github.com/matrix-org/matrix-js-sdk/pull/3614)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix wrong handling of encrypted rooms when loading them from sync accumulator ([\#3640](https://github.com/matrix-org/matrix-js-sdk/pull/3640)). Fixes vector-im/element-web#25803.
|
||||
* Skip processing thread roots and fetching threads list when support is disabled ([\#3642](https://github.com/matrix-org/matrix-js-sdk/pull/3642)).
|
||||
* Ensure we don't overinflate the total notification count ([\#3634](https://github.com/matrix-org/matrix-js-sdk/pull/3634)). Fixes vector-im/element-web#25803.
|
||||
|
||||
Changes in [27.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.1.0) (2023-08-01)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "27.1.0-rc.1",
|
||||
"version": "27.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -55,14 +55,14 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^1.1.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^1.2.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.3.1",
|
||||
"matrix-widget-api": "^1.5.0",
|
||||
"oidc-client-ts": "^2.2.4",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
@@ -101,7 +101,7 @@
|
||||
"browserify-swap": "^0.2.2",
|
||||
"debug": "^4.3.4",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.44.0",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
@@ -110,7 +110,7 @@
|
||||
"eslint-plugin-jsdoc": "^46.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^47.0.0",
|
||||
"eslint-plugin-unicorn": "^48.0.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
|
||||
@@ -20,22 +20,23 @@ import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { MockResponse, MockResponseFunction } from "fetch-mock";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import type { IDeviceKeys } from "../../../src/@types/crypto";
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import { TEST_ROOM_ID, TEST_ROOM_ID as ROOM_ID, TEST_USER_ID } from "../../test-utils/test-data";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
Category,
|
||||
createClient,
|
||||
IClaimOTKsResult,
|
||||
IContent,
|
||||
IDownloadKeyResult,
|
||||
IEvent,
|
||||
IJoinedRoom,
|
||||
IndexedDBCryptoStore,
|
||||
IStartClientOpts,
|
||||
ISyncResponse,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
Room,
|
||||
RoomMember,
|
||||
RoomStateEvent,
|
||||
IRoomEvent,
|
||||
} from "../../../src/matrix";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
@@ -53,8 +55,7 @@ import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
|
||||
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
import { CryptoCallbacks } from "../../../src/crypto-api";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -156,18 +157,23 @@ function encryptMegolmEvent(opts: {
|
||||
expect(opts.room_id).toBeTruthy();
|
||||
plaintext.room_id = opts.room_id;
|
||||
}
|
||||
return encryptMegolmEventRawPlainText({ senderKey: opts.senderKey, groupSession: opts.groupSession, plaintext });
|
||||
return encryptMegolmEventRawPlainText({
|
||||
senderKey: opts.senderKey,
|
||||
groupSession: opts.groupSession,
|
||||
plaintext,
|
||||
});
|
||||
}
|
||||
|
||||
function encryptMegolmEventRawPlainText(opts: {
|
||||
senderKey: string;
|
||||
groupSession: Olm.OutboundGroupSession;
|
||||
plaintext: Partial<IEvent>;
|
||||
origin_server_ts?: number;
|
||||
}): IEvent {
|
||||
return {
|
||||
event_id: "$test_megolm_event_" + Math.random(),
|
||||
sender: "@not_the_real_sender:example.com",
|
||||
origin_server_ts: 1672944778000,
|
||||
sender: opts.plaintext.sender ?? "@not_the_real_sender:example.com",
|
||||
origin_server_ts: opts.plaintext.origin_server_ts ?? 1672944778000,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: opts.groupSession.encrypt(JSON.stringify(opts.plaintext)),
|
||||
@@ -213,55 +219,6 @@ function encryptGroupSessionKey(opts: {
|
||||
});
|
||||
}
|
||||
|
||||
// get a /sync response which contains a single room (ROOM_ID), with the members given
|
||||
function getSyncResponse(roomMembers: string[]): ISyncResponse {
|
||||
const roomResponse: IJoinedRoom = {
|
||||
summary: {
|
||||
"m.heroes": [],
|
||||
"m.joined_member_count": roomMembers.length,
|
||||
"m.invited_member_count": roomMembers.length,
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEventCustom({
|
||||
sender: roomMembers[0],
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
prev_batch: "",
|
||||
},
|
||||
ephemeral: { events: [] },
|
||||
account_data: { events: [] },
|
||||
unread_notifications: {},
|
||||
};
|
||||
|
||||
for (let i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
testUtils.mkMembershipCustom({
|
||||
membership: "join",
|
||||
sender: roomMembers[i],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
next_batch: "1",
|
||||
rooms: {
|
||||
join: { [ROOM_ID]: roomResponse },
|
||||
invite: {},
|
||||
leave: {},
|
||||
},
|
||||
account_data: { events: [] },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish an Olm Session with the test user
|
||||
*
|
||||
@@ -415,7 +372,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
|
||||
let keyReceiver: IE2EKeyReceiver;
|
||||
let keyReceiver: E2EKeyReceiver;
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let keyResponder: E2EKeyResponder;
|
||||
|
||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||
let syncResponder: ISyncResponder;
|
||||
@@ -586,6 +546,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
@@ -708,6 +672,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(event.isDecryptionFailure()).toBeFalsy();
|
||||
expect(event.getContent().body).toEqual("42");
|
||||
});
|
||||
|
||||
@@ -2411,4 +2376,193 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(selfSigningKey[secretStorageKey]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Incoming verification in a DM", () => {
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
|
||||
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||
keyResponder.addKeyReceiver(TEST_USER_ID, keyReceiver);
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
/**
|
||||
* Return a verification request event from Bob
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#mkeyverificationrequest
|
||||
*/
|
||||
function createVerificationRequestEvent(): IRoomEvent {
|
||||
return {
|
||||
content: {
|
||||
body: "Verification request from Bob to Alice",
|
||||
from_device: "BobDevice",
|
||||
methods: ["m.sas.v1"],
|
||||
msgtype: "m.key.verification.request",
|
||||
to: aliceClient.getUserId()!,
|
||||
},
|
||||
event_id: "$143273582443PhrSn:example.org",
|
||||
origin_server_ts: Date.now(),
|
||||
room_id: TEST_ROOM_ID,
|
||||
sender: "@bob:xyz",
|
||||
type: "m.room.message",
|
||||
unsigned: {
|
||||
age: 1234,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a to-device event
|
||||
* @param groupSession
|
||||
* @param p2pSession
|
||||
*/
|
||||
function createToDeviceEvent(groupSession: Olm.OutboundGroupSession, p2pSession: Olm.Session): Partial<IEvent> {
|
||||
return encryptGroupSessionKey({
|
||||
recipient: aliceClient.getUserId()!,
|
||||
recipientCurve25519Key: keyReceiver.getDeviceKey(),
|
||||
recipientEd25519Key: keyReceiver.getSigningKey(),
|
||||
olmAccount: testOlmAccount,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and encrypt a verification request event
|
||||
* @param groupSession
|
||||
*/
|
||||
function createEncryptedMessage(groupSession: Olm.OutboundGroupSession): IEvent {
|
||||
return encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
plaintext: createVerificationRequestEvent(),
|
||||
});
|
||||
}
|
||||
|
||||
newBackendOnly("Verification request from Bob to Alice", async () => {
|
||||
// Tell alice she is sharing a room with bob
|
||||
const syncResponse = getSyncResponse(["@bob:xyz"]);
|
||||
|
||||
// Add verification request from Bob to Alice in the DM between them
|
||||
syncResponse.rooms[Category.Join][TEST_ROOM_ID].timeline.events.push(createVerificationRequestEvent());
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
// Wait for the sync response to be processed
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
|
||||
// Expect to find the verification request received during the sync
|
||||
expect(request?.roomId).toBe(TEST_ROOM_ID);
|
||||
expect(request?.isSelfVerification).toBe(false);
|
||||
expect(request?.otherUserId).toBe("@bob:xyz");
|
||||
});
|
||||
|
||||
newBackendOnly("Verification request not found", async () => {
|
||||
// Tell alice she is sharing a room with bob
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
|
||||
// Wait for the sync response to be processed
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Expect to not find any verification request
|
||||
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
|
||||
expect(request).not.toBeDefined();
|
||||
});
|
||||
|
||||
newBackendOnly("Process encrypted verification request", async () => {
|
||||
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event, but don't send it yet
|
||||
const toDeviceEvent = createToDeviceEvent(groupSession, p2pSession);
|
||||
|
||||
// Add verification request from Bob to Alice in the DM between them
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
rooms: { join: { [ROOM_ID]: { timeline: { events: [createEncryptedMessage(groupSession)] } } } },
|
||||
});
|
||||
// Wait for the sync response to be processed
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
const matrixEvent = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
// wait for a first attempt at decryption: should fail
|
||||
await testUtils.awaitDecryption(matrixEvent);
|
||||
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
|
||||
// Send the Bob's keys
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 2,
|
||||
to_device: {
|
||||
events: [toDeviceEvent],
|
||||
},
|
||||
});
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Wait for the message to be decrypted
|
||||
await testUtils.awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });
|
||||
|
||||
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
|
||||
// Expect to find the verification request received during the sync
|
||||
expect(request?.roomId).toBe(TEST_ROOM_ID);
|
||||
expect(request?.isSelfVerification).toBe(false);
|
||||
expect(request?.otherUserId).toBe("@bob:xyz");
|
||||
});
|
||||
|
||||
newBackendOnly(
|
||||
"If Bob keys are not received in the 5mins after the verification request, the request is ignored",
|
||||
async () => {
|
||||
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event, but don't send it yet
|
||||
const toDeviceEvent = createToDeviceEvent(groupSession, p2pSession);
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
// Add verification request from Bob to Alice in the DM between them
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
rooms: { join: { [ROOM_ID]: { timeline: { events: [createEncryptedMessage(groupSession)] } } } },
|
||||
});
|
||||
// Wait for the sync response to be processed
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
const matrixEvent = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
// wait for a first attempt at decryption: should fail
|
||||
await testUtils.awaitDecryption(matrixEvent);
|
||||
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
|
||||
// Advance time by 5mins, the verification request should be ignored after that
|
||||
jest.advanceTimersByTime(5 * 60 * 1000);
|
||||
|
||||
// Send the Bob's keys
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 2,
|
||||
to_device: {
|
||||
events: [toDeviceEvent],
|
||||
},
|
||||
});
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Wait for the message to be decrypted
|
||||
await testUtils.awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });
|
||||
|
||||
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
|
||||
// the request should not be present
|
||||
expect(request).not.toBeDefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,17 +15,18 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { logger } from "../../../src/logger";
|
||||
import { decodeRecoveryKey } from "../../../src/crypto/recoverykey";
|
||||
import { IKeyBackupInfo, IKeyBackupSession } from "../../../src/crypto/keybackup";
|
||||
import { createClient, ICreateClientOpts, IEvent, MatrixClient } from "../../../src";
|
||||
import { MatrixEventEvent } from "../../../src/models/event";
|
||||
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
|
||||
import { createClient, CryptoEvent, ICreateClientOpts, IEvent, MatrixClient } from "../../../src";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
import { syncPromise } from "../../test-utils/test-utils";
|
||||
import { awaitDecryption, CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { KeyBackupInfo } from "../../../src/crypto-api/keybackup";
|
||||
|
||||
const ROOM_ID = "!ROOM:ID";
|
||||
|
||||
@@ -72,22 +73,21 @@ const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
|
||||
},
|
||||
};
|
||||
|
||||
const CURVE25519_BACKUP_INFO: IKeyBackupInfo = {
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: "1",
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
// Will be updated with correct value on the fly
|
||||
signatures: {},
|
||||
},
|
||||
};
|
||||
|
||||
const RECOVERY_KEY = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
const TEST_USER_ID = "@alice:localhost";
|
||||
const TEST_DEVICE_ID = "xzcvb";
|
||||
|
||||
describe("megolm key backups", function () {
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
|
||||
// Rust backend. Once we have full support in the rust sdk, it will go away.
|
||||
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
|
||||
|
||||
let aliceClient: MatrixClient;
|
||||
/** an object which intercepts `/sync` requests on the test homeserver */
|
||||
let syncResponder: SyncResponder;
|
||||
@@ -108,6 +108,7 @@ describe("megolm key backups", function () {
|
||||
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
|
||||
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
|
||||
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
|
||||
e2eKeyResponder.addDeviceKeys(testData.SIGNED_TEST_DEVICE_DATA);
|
||||
e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver);
|
||||
});
|
||||
|
||||
@@ -130,12 +131,12 @@ describe("megolm key backups", function () {
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
...opts,
|
||||
});
|
||||
await client.initCrypto();
|
||||
await initCrypto(client);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
it("Alice checks key backups when receiving a message she can't decrypt", async function () {
|
||||
oldBackendOnly("Alice checks key backups when receiving a message she can't decrypt", async function () {
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
@@ -150,35 +151,151 @@ describe("megolm key backups", function () {
|
||||
};
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", CURVE25519_KEY_BACKUP_DATA);
|
||||
|
||||
// mock for the outgoing key requests that will be sent
|
||||
fetchMock.put("express:/_matrix/client/r0/sendToDevice/m.room_key_request/:txid", {});
|
||||
|
||||
// We'll need to add a signature to the backup data, so take a copy to avoid mutating global state.
|
||||
const backupData = JSON.parse(JSON.stringify(CURVE25519_BACKUP_INFO));
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData);
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
await aliceClient.crypto!.signObject(backupData.auth_data);
|
||||
await aliceClient.crypto!.storeSessionBackupPrivateKey(decodeRecoveryKey(RECOVERY_KEY));
|
||||
await aliceClient.crypto!.backupManager!.checkAndStart();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"));
|
||||
|
||||
// start after saving the private key
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
|
||||
// XXX: should we automatically re-check after a device becomes verified?
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceClient.checkKeyBackup();
|
||||
|
||||
// Now, send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
await new Promise((resolve, reject) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
await awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(event.getContent()).toEqual("testytest");
|
||||
});
|
||||
|
||||
oldBackendOnly("getActiveSessionBackupVersion() should give correct result", async function () {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceClient.checkKeyBackup();
|
||||
|
||||
// At this point there is no backup
|
||||
let backupStatus: string | null;
|
||||
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
|
||||
expect(backupStatus).toBeNull();
|
||||
|
||||
// Serve a backup with no trusted signature
|
||||
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
delete unsignedBackup.auth_data.signatures;
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
const checked = await aliceClient.checkKeyBackup();
|
||||
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
|
||||
expect(checked?.trustInfo?.usable).toBeFalsy();
|
||||
|
||||
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
|
||||
expect(backupStatus).toBeNull();
|
||||
|
||||
// Add a valid signature to the backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
// check that signalling is working
|
||||
const backupPromise = new Promise<void>((resolve, reject) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||
if (enabled) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
expect(event.getContent()).toEqual("testytest");
|
||||
const validCheck = await aliceClient.checkKeyBackup();
|
||||
expect(validCheck?.trustInfo?.usable).toStrictEqual(true);
|
||||
|
||||
await backupPromise;
|
||||
|
||||
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
|
||||
expect(backupStatus).toStrictEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
});
|
||||
|
||||
describe("isKeyBackupTrusted", () => {
|
||||
it("does not trust a backup signed by an untrusted device", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// download the device list, to match the trusted case
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
|
||||
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
|
||||
expect(result).toEqual({ trusted: false, matchesDecryptionKey: false });
|
||||
});
|
||||
|
||||
it("trusts a backup signed by a trusted device", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
|
||||
expect(result).toEqual({ trusted: true, matchesDecryptionKey: false });
|
||||
});
|
||||
|
||||
it("recognises a backup which matches the decryption key", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
await aliceClient.startClient();
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
);
|
||||
|
||||
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
|
||||
expect(result).toEqual({ trusted: false, matchesDecryptionKey: true });
|
||||
});
|
||||
|
||||
it("is not fooled by a backup which matches the decryption key but uses a different algorithm", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
await aliceClient.startClient();
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
);
|
||||
|
||||
const backup: KeyBackupInfo = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
backup.algorithm = "m.megolm_backup.v1.aes-hmac-sha2";
|
||||
const result = await aliceCrypto.isKeyBackupTrusted(backup);
|
||||
expect(result).toEqual({ trusted: false, matchesDecryptionKey: false });
|
||||
});
|
||||
});
|
||||
|
||||
/** make sure that the client knows about the dummy device */
|
||||
async function waitForDeviceList(): Promise<void> {
|
||||
// Completing the initial sync will make the device list download outdated device lists (of which our own
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the dummy device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
expect(devices.get(TEST_USER_ID)!.keys()).toContain(TEST_DEVICE_ID);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,7 +124,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
describe("Outgoing verification requests for another device", () => {
|
||||
beforeEach(async () => {
|
||||
// pretend that we have another device, which we will verify
|
||||
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
});
|
||||
|
||||
// test with (1) the default verification method list, (2) a custom verification method list.
|
||||
@@ -626,7 +626,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
describe("cancellation", () => {
|
||||
beforeEach(async () => {
|
||||
// pretend that we have another device, which we will start verifying
|
||||
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
|
||||
aliceClient = await startTestClient();
|
||||
@@ -743,7 +743,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
describe("Incoming verification from another device", () => {
|
||||
beforeEach(async () => {
|
||||
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
aliceClient = await startTestClient();
|
||||
await waitForDeviceList();
|
||||
|
||||
@@ -207,7 +207,7 @@ function startClient(httpBackend: HttpBackend, client: MatrixClient) {
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
|
||||
|
||||
client.startClient();
|
||||
client.startClient({ threadSupport: true });
|
||||
|
||||
// set up a promise which will resolve once the client is initialised
|
||||
const prom = new Promise<void>((resolve) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Mocked } from "jest-mock";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Filter, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix";
|
||||
import { Filter, KnockRoomOpts, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { IFilterDefinition } from "../../src/filter";
|
||||
@@ -205,6 +205,84 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("knockRoom", function () {
|
||||
const roomId = "!some-room-id:example.org";
|
||||
const reason = "some reason";
|
||||
const viaServers = "example.com";
|
||||
|
||||
type TestCase = [string, KnockRoomOpts];
|
||||
const testCases: TestCase[] = [
|
||||
["should knock a room", {}],
|
||||
["should knock a room for a reason", { reason }],
|
||||
["should knock a room via given servers", { viaServers }],
|
||||
["should knock a room for a reason via given servers", { reason, viaServers }],
|
||||
];
|
||||
|
||||
it.each(testCases)("%s", async (_, opts) => {
|
||||
httpBackend
|
||||
.when("POST", "/knock/" + encodeURIComponent(roomId))
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual({ reason: opts.reason });
|
||||
expect(request.queryParams).toEqual({ server_name: opts.viaServers });
|
||||
})
|
||||
.respond(200, { room_id: roomId });
|
||||
|
||||
const prom = client.knockRoom(roomId, opts);
|
||||
await httpBackend.flushAllExpected();
|
||||
expect((await prom).room_id).toBe(roomId);
|
||||
});
|
||||
|
||||
it("should no-op if you've already knocked a room", function () {
|
||||
const room = new Room(roomId, client, userId);
|
||||
|
||||
client.fetchRoomEvent = () =>
|
||||
Promise.resolve({
|
||||
type: "test",
|
||||
content: {},
|
||||
});
|
||||
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: "knock",
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
store.storeRoom(room);
|
||||
client.knockRoom(roomId);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
describe("errors", function () {
|
||||
type TestCase = [number, { errcode: string; error?: string }, string];
|
||||
const testCases: TestCase[] = [
|
||||
[
|
||||
403,
|
||||
{ errcode: "M_FORBIDDEN", error: "You don't have permission to knock" },
|
||||
"[M_FORBIDDEN: MatrixError: [403] You don't have permission to knock]",
|
||||
],
|
||||
[
|
||||
500,
|
||||
{ errcode: "INTERNAL_SERVER_ERROR" },
|
||||
"[INTERNAL_SERVER_ERROR: MatrixError: [500] Unknown message]",
|
||||
],
|
||||
];
|
||||
|
||||
it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => {
|
||||
httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error });
|
||||
|
||||
const prom = client.knockRoom(roomId);
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
expect(prom).rejects.toMatchInlineSnapshot(snapshot),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFilter", function () {
|
||||
const filterId = "f1lt3r1d";
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
Room,
|
||||
IndexedDBStore,
|
||||
RelationType,
|
||||
EventType,
|
||||
} from "../../src";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
|
||||
@@ -1590,6 +1591,68 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should apply encrypted notification logic for events within the same sync blob", async () => {
|
||||
const roomId = "!room123:server";
|
||||
const syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
ephemeral: {
|
||||
events: [],
|
||||
},
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
room: roomId,
|
||||
event: true,
|
||||
skey: "",
|
||||
type: EventType.RoomEncryption,
|
||||
content: {},
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId,
|
||||
user: otherUserId,
|
||||
msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as ISyncResponse;
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("of a room", () => {
|
||||
|
||||
@@ -89,12 +89,10 @@ export class E2EKeyResponder {
|
||||
/**
|
||||
* Add a set of device keys for return by a future `/keys/query`, as if they had been `/upload`ed
|
||||
*
|
||||
* @param userId - user the keys belong to
|
||||
* @param deviceId - device the keys belong to
|
||||
* @param keys - device keys for this device.
|
||||
*/
|
||||
public addDeviceKeys(userId: string, deviceId: string, keys: IDeviceKeys) {
|
||||
this.deviceKeysByUserByDevice.getOrCreate(userId).set(deviceId, keys);
|
||||
public addDeviceKeys(keys: IDeviceKeys) {
|
||||
this.deviceKeysByUserByDevice.getOrCreate(keys.user_id).set(keys.device_id, keys);
|
||||
}
|
||||
|
||||
/** Add a set of cross-signing keys for return by a future `/keys/query`, as if they had been `/keys/device_signing/upload`ed
|
||||
|
||||
@@ -28,12 +28,13 @@ import base64
|
||||
import json
|
||||
|
||||
from canonicaljson import encode_canonical_json
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519, x25519
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
|
||||
# input data
|
||||
TEST_USER_ID = "@alice:localhost"
|
||||
TEST_DEVICE_ID = "test_device"
|
||||
TEST_ROOM_ID = "!room:id"
|
||||
# any 32-byte string can be an ed25519 private key.
|
||||
TEST_DEVICE_PRIVATE_KEY_BYTES = b"deadbeefdeadbeefdeadbeefdeadbeef"
|
||||
|
||||
@@ -41,6 +42,8 @@ MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"doyouspeakwhaaaaaaaaaaaaaaaaaale"
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"useruseruseruseruseruseruseruser"
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"selfselfselfselfselfselfselfself"
|
||||
|
||||
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
|
||||
B64_BACKUP_DECRYPTION_KEY = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo="
|
||||
|
||||
def main() -> None:
|
||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
@@ -71,29 +74,47 @@ def main() -> None:
|
||||
b64_master_public_key = encode_base64(
|
||||
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
b64_master_private_key = encode_base64(
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
)
|
||||
b64_master_private_key = encode_base64(MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES)
|
||||
|
||||
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
)
|
||||
b64_self_signing_public_key = encode_base64(
|
||||
self_signing_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
b64_self_signing_private_key = encode_base64(
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
self_signing_private_key.public_key().public_bytes(
|
||||
Encoding.Raw, PublicFormat.Raw
|
||||
)
|
||||
)
|
||||
b64_self_signing_private_key = encode_base64(SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES)
|
||||
|
||||
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
)
|
||||
b64_user_signing_public_key = encode_base64(
|
||||
user_signing_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
user_signing_private_key.public_key().public_bytes(
|
||||
Encoding.Raw, PublicFormat.Raw
|
||||
)
|
||||
)
|
||||
b64_user_signing_private_key = encode_base64(
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
b64_user_signing_private_key = encode_base64(USER_CROSS_SIGNING_PRIVATE_KEY_BYTES)
|
||||
|
||||
backup_decryption_key = x25519.X25519PrivateKey.from_private_bytes(
|
||||
base64.b64decode(B64_BACKUP_DECRYPTION_KEY)
|
||||
)
|
||||
b64_backup_public_key = encode_base64(
|
||||
backup_decryption_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
|
||||
backup_data = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": b64_backup_public_key,
|
||||
},
|
||||
}
|
||||
# sign with our device key
|
||||
sig = sign_json(backup_data["auth_data"], private_key)
|
||||
backup_data["auth_data"]["signatures"] = {
|
||||
TEST_USER_ID: {f"ed25519:{TEST_DEVICE_ID}": sig}
|
||||
}
|
||||
|
||||
print(
|
||||
f"""\
|
||||
@@ -104,11 +125,13 @@ def main() -> None:
|
||||
|
||||
import {{ IDeviceKeys }} from "../../../src/@types/crypto";
|
||||
import {{ IDownloadKeyResult }} from "../../../src";
|
||||
import {{ KeyBackupInfo }} from "../../../src/crypto-api";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
export const TEST_USER_ID = "{TEST_USER_ID}";
|
||||
export const TEST_DEVICE_ID = "{TEST_DEVICE_ID}";
|
||||
export const TEST_ROOM_ID = "{TEST_ROOM_ID}";
|
||||
|
||||
/** The base64-encoded public ed25519 key for this device */
|
||||
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}";
|
||||
@@ -138,6 +161,12 @@ export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_
|
||||
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
json.dumps(build_cross_signing_keys_data(), indent=4)
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "{ B64_BACKUP_DECRYPTION_KEY }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
""",
|
||||
end="",
|
||||
)
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
|
||||
import { IDeviceKeys } from "../../../src/@types/crypto";
|
||||
import { IDownloadKeyResult } from "../../../src";
|
||||
import { KeyBackupInfo } from "../../../src/crypto-api";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
export const TEST_USER_ID = "@alice:localhost";
|
||||
export const TEST_DEVICE_ID = "test_device";
|
||||
export const TEST_ROOM_ID = "!room:id";
|
||||
|
||||
/** The base64-encoded public ed25519 key for this device */
|
||||
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU";
|
||||
@@ -97,3 +99,20 @@ export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,9 +6,19 @@ import "../olm-loader";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType, RelationType } from "../../src";
|
||||
import {
|
||||
ClientEvent,
|
||||
EventType,
|
||||
IJoinedRoom,
|
||||
IPusher,
|
||||
ISyncResponse,
|
||||
MatrixClient,
|
||||
MsgType,
|
||||
RelationType,
|
||||
} from "../../src";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
import { TEST_ROOM_ID } from "./test-data";
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
@@ -39,6 +49,61 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sync response which contains a single room (by default TEST_ROOM_ID), with the members given
|
||||
* @param roomMembers
|
||||
* @param roomId
|
||||
*
|
||||
* @returns the sync response
|
||||
*/
|
||||
export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): ISyncResponse {
|
||||
const roomResponse: IJoinedRoom = {
|
||||
summary: {
|
||||
"m.heroes": [],
|
||||
"m.joined_member_count": roomMembers.length,
|
||||
"m.invited_member_count": roomMembers.length,
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
mkEventCustom({
|
||||
sender: roomMembers[0],
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
prev_batch: "",
|
||||
},
|
||||
ephemeral: { events: [] },
|
||||
account_data: { events: [] },
|
||||
unread_notifications: {},
|
||||
};
|
||||
|
||||
for (let i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
mkMembershipCustom({
|
||||
membership: "join",
|
||||
sender: roomMembers[i],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
next_batch: "1",
|
||||
rooms: {
|
||||
join: { [roomId]: roomResponse },
|
||||
invite: {},
|
||||
leave: {},
|
||||
},
|
||||
account_data: { events: [] },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spy for an object and automatically spy its methods.
|
||||
* @param constr - The class constructor (used with 'new')
|
||||
|
||||
@@ -23,6 +23,8 @@ import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../../src/rust-crypto";
|
||||
import {
|
||||
CryptoEvent,
|
||||
Device,
|
||||
DeviceVerification,
|
||||
HttpApiEvent,
|
||||
HttpApiEventHandlerMap,
|
||||
IHttpOpts,
|
||||
@@ -351,6 +353,60 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDeviceVerified", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
async function getTestDevice(): Promise<Device> {
|
||||
const devices = await rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]);
|
||||
return devices.get(testData.TEST_USER_ID)!.get(testData.TEST_DEVICE_ID)!;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
rustCrypto = await makeTestRustCrypto(
|
||||
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
|
||||
baseUrl: "http://server/",
|
||||
prefix: "",
|
||||
onlyData: true,
|
||||
}),
|
||||
testData.TEST_USER_ID,
|
||||
);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", {
|
||||
device_keys: {
|
||||
[testData.TEST_USER_ID]: {
|
||||
[testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA,
|
||||
},
|
||||
},
|
||||
});
|
||||
// call onSyncCompleted to kick off the outgoingRequestLoop and download the device list.
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
// before the call, the device should be unverified.
|
||||
const device = await getTestDevice();
|
||||
expect(device.verified).toEqual(DeviceVerification.Unverified);
|
||||
});
|
||||
|
||||
it("should throw an error for an unknown device", async () => {
|
||||
await expect(rustCrypto.setDeviceVerified(testData.TEST_USER_ID, "xxy")).rejects.toThrow("Unknown device");
|
||||
});
|
||||
|
||||
it("should mark an unverified device as verified", async () => {
|
||||
await rustCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
// and confirm that the device is now verified
|
||||
expect((await getTestDevice()).verified).toEqual(DeviceVerification.Verified);
|
||||
});
|
||||
|
||||
it("should mark a verified device as unverified", async () => {
|
||||
await rustCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
expect((await getTestDevice()).verified).toEqual(DeviceVerification.Verified);
|
||||
|
||||
await rustCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID, false);
|
||||
expect((await getTestDevice()).verified).toEqual(DeviceVerification.Unverified);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDeviceVerificationStatus", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
@@ -513,6 +569,22 @@ describe("RustCrypto", () => {
|
||||
expect(new TextDecoder().decode(fetched!)).toEqual(key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getActiveSessionBackupVersion", () => {
|
||||
it("returns null", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
expect(await rustCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("findVerificationRequestDMInProgress", () => {
|
||||
it("throws an error if the userId is not provided", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
expect(() => rustCrypto.findVerificationRequestDMInProgress(testData.TEST_ROOM_ID)).toThrow(
|
||||
"missing userId",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** build a basic RustCrypto instance for testing
|
||||
|
||||
@@ -45,6 +45,18 @@ export interface IJoinRoomOpts {
|
||||
viaServers?: string[];
|
||||
}
|
||||
|
||||
export interface KnockRoomOpts {
|
||||
/**
|
||||
* The reason for the knock.
|
||||
*/
|
||||
reason?: string;
|
||||
|
||||
/**
|
||||
* The server names to try and knock through in addition to those that are automatically chosen.
|
||||
*/
|
||||
viaServers?: string | string[];
|
||||
}
|
||||
|
||||
export interface IRedactOpts {
|
||||
reason?: string;
|
||||
/**
|
||||
|
||||
+55
-25
@@ -134,6 +134,7 @@ import {
|
||||
ITagsResponse,
|
||||
IStatusResponse,
|
||||
IAddThreePidBody,
|
||||
KnockRoomOpts,
|
||||
} from "./@types/requests";
|
||||
import {
|
||||
EventType,
|
||||
@@ -2243,6 +2244,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
// attach the event listeners needed by RustCrypto
|
||||
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
|
||||
this.on(ClientEvent.Event, (event) => {
|
||||
rustCrypto.onLiveEventFromSync(event);
|
||||
});
|
||||
|
||||
// re-emit the events emitted by the crypto impl
|
||||
this.reEmitter.reEmit(rustCrypto, [
|
||||
@@ -3282,6 +3286,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* @param info - key backup info dict from getKeyBackupVersion()
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.isKeyBackupTrusted | `CryptoApi.isKeyBackupTrusted`}.
|
||||
*/
|
||||
public isKeyBackupTrusted(info: IKeyBackupInfo): Promise<TrustInfo> {
|
||||
if (!this.crypto) {
|
||||
@@ -3294,6 +3300,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns true if the client is configured to back up keys to
|
||||
* the server, otherwise false. If we haven't completed a successful check
|
||||
* of key backup status yet, returns null.
|
||||
*
|
||||
* @deprecated Prefer direct access to {@link CryptoApi.getActiveSessionBackupVersion}:
|
||||
*
|
||||
* ```javascript
|
||||
* let enabled = (await client.getCrypto().getActiveSessionBackupVersion()) !== null;
|
||||
* ```
|
||||
*/
|
||||
public getKeyBackupEnabled(): boolean | null {
|
||||
if (!this.crypto) {
|
||||
@@ -3436,6 +3448,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// If we're currently backing up to this backup... stop.
|
||||
// (We start using it automatically in createKeyBackupVersion
|
||||
// so this is symmetrical).
|
||||
// TODO: convert this to use crypto.getActiveSessionBackupVersion. And actually check the version.
|
||||
if (this.crypto.backupManager.version) {
|
||||
this.crypto.backupManager.disableKeyBackup();
|
||||
}
|
||||
@@ -4146,6 +4159,34 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return syncRoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Knock a room. If you have already knocked the room, this will no-op.
|
||||
* @param roomIdOrAlias - The room ID or room alias to knock.
|
||||
* @param opts - Options when knocking the room.
|
||||
* @returns Promise which resolves: `{room_id: {string}}`
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public knockRoom(roomIdOrAlias: string, opts: KnockRoomOpts = {}): Promise<{ room_id: string }> {
|
||||
const room = this.getRoom(roomIdOrAlias);
|
||||
if (room?.hasMembershipState(this.credentials.userId!, "knock")) {
|
||||
return Promise.resolve({ room_id: room.roomId });
|
||||
}
|
||||
|
||||
const path = utils.encodeUri("/knock/$roomIdOrAlias", { $roomIdOrAlias: roomIdOrAlias });
|
||||
|
||||
const queryParams: Record<string, string | string[]> = {};
|
||||
if (opts.viaServers) {
|
||||
queryParams.server_name = opts.viaServers;
|
||||
}
|
||||
|
||||
const body: Record<string, string> = {};
|
||||
if (opts.reason) {
|
||||
body.reason = opts.reason;
|
||||
}
|
||||
|
||||
return this.http.authedRequest(Method.Post, path, queryParams, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend an event. Will also retry any to-device messages waiting to be sent.
|
||||
* @param event - The event to resend.
|
||||
@@ -9759,6 +9800,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param toStartOfTimeline - the direction
|
||||
*/
|
||||
public processThreadRoots(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
if (!this.supportsThreads()) return;
|
||||
room.processThreadRoots(threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
@@ -9849,19 +9891,8 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri
|
||||
const room = cli.getRoom(event.getRoomId());
|
||||
if (!room || !ourUserId || !eventId) return;
|
||||
|
||||
const oldActions = event.getPushActions();
|
||||
const actions = cli.getPushActionsForEvent(event, true);
|
||||
|
||||
const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
|
||||
|
||||
const currentHighlightCount = room.getUnreadCountForEventContext(NotificationCountType.Highlight, event);
|
||||
|
||||
// Ensure the unread counts are kept up to date if the event is encrypted
|
||||
// We also want to make sure that the notification count goes up if we already
|
||||
// have encrypted events to avoid other code from resetting 'highlight' to zero.
|
||||
const oldHighlight = !!oldActions?.tweaks?.highlight;
|
||||
const newHighlight = !!actions?.tweaks?.highlight;
|
||||
|
||||
let hasReadEvent;
|
||||
if (isThreadEvent) {
|
||||
const thread = room.getThread(event.threadRootId);
|
||||
@@ -9883,13 +9914,17 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHighlight !== newHighlight || currentHighlightCount > 0) {
|
||||
const actions = cli.getPushActionsForEvent(event, true);
|
||||
|
||||
// Ensure the unread counts are kept up to date if the event is encrypted
|
||||
// We also want to make sure that the notification count goes up if we already
|
||||
// have encrypted events to avoid other code from resetting 'highlight' to zero.
|
||||
const newHighlight = !!actions?.tweaks?.highlight;
|
||||
|
||||
if (newHighlight) {
|
||||
// TODO: Handle mentions received while the client is offline
|
||||
// See also https://github.com/vector-im/element-web/issues/9069
|
||||
let newCount = currentHighlightCount;
|
||||
if (newHighlight && !oldHighlight) newCount++;
|
||||
if (!newHighlight && oldHighlight) newCount--;
|
||||
|
||||
const newCount = room.getUnreadCountForEventContext(NotificationCountType.Highlight, event) + 1;
|
||||
if (isThreadEvent) {
|
||||
room.setThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Highlight, newCount);
|
||||
} else {
|
||||
@@ -9897,23 +9932,18 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri
|
||||
}
|
||||
}
|
||||
|
||||
// Total count is used to typically increment a room notification counter, but not loudly highlight it.
|
||||
const currentTotalCount = room.getUnreadCountForEventContext(NotificationCountType.Total, event);
|
||||
|
||||
// `notify` is used in practice for incrementing the total count
|
||||
const newNotify = !!actions?.notify;
|
||||
|
||||
// The room total count is NEVER incremented by the server for encrypted rooms. We basically ignore
|
||||
// the server here as it's always going to tell us to increment for encrypted events.
|
||||
if (newNotify) {
|
||||
// Total count is used to typically increment a room notification counter, but not loudly highlight it.
|
||||
const newCount = room.getUnreadCountForEventContext(NotificationCountType.Total, event) + 1;
|
||||
if (isThreadEvent) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
event.threadRootId,
|
||||
NotificationCountType.Total,
|
||||
currentTotalCount + 1,
|
||||
);
|
||||
room.setThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total, newCount);
|
||||
} else {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, currentTotalCount + 1);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, newCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+42
-1
@@ -20,7 +20,7 @@ import { DeviceMap } from "./models/device";
|
||||
import { UIAuthCallback } from "./interactive-auth";
|
||||
import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescription } from "./secret-storage";
|
||||
import { VerificationRequest } from "./crypto-api/verification";
|
||||
import { KeyBackupInfo } from "./crypto-api/keybackup";
|
||||
import { BackupTrustInfo, KeyBackupInfo } from "./crypto-api/keybackup";
|
||||
import { ISignatures } from "./@types/signed";
|
||||
|
||||
/**
|
||||
@@ -137,6 +137,22 @@ export interface CryptoApi {
|
||||
*/
|
||||
getDeviceVerificationStatus(userId: string, deviceId: string): Promise<DeviceVerificationStatus | null>;
|
||||
|
||||
/**
|
||||
* Mark the given device as locally verified.
|
||||
*
|
||||
* Marking a devices as locally verified has much the same effect as completing the verification dance, or receiving
|
||||
* a cross-signing signature for it.
|
||||
*
|
||||
* @param userId - owner of the device
|
||||
* @param deviceId - unique identifier for the device.
|
||||
* @param verified - whether to mark the device as verified. Defaults to 'true'.
|
||||
*
|
||||
* @throws an error if the device is unknown, or has not published any encryption keys.
|
||||
*
|
||||
* @remarks Fires {@link CryptoEvent#DeviceVerificationChanged}
|
||||
*/
|
||||
setDeviceVerified(userId: string, deviceId: string, verified?: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Checks whether cross signing:
|
||||
* - is enabled on this account and trusted by this device
|
||||
@@ -251,9 +267,20 @@ export interface CryptoApi {
|
||||
* @param roomId - the room to use for verification
|
||||
*
|
||||
* @returns the VerificationRequest that is in progress, if any
|
||||
* @deprecated prefer `userId` parameter variant.
|
||||
*/
|
||||
findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined;
|
||||
|
||||
/**
|
||||
* Finds a DM verification request that is already in progress for the given room and user.
|
||||
*
|
||||
* @param roomId - the room to use for verification.
|
||||
* @param userId - search for a verification request for the given user.
|
||||
*
|
||||
* @returns the VerificationRequest that is in progress, if any.
|
||||
*/
|
||||
findVerificationRequestDMInProgress(roomId: string, userId?: string): VerificationRequest | undefined;
|
||||
|
||||
/**
|
||||
* Send a verification request to our other devices.
|
||||
*
|
||||
@@ -309,6 +336,20 @@ export interface CryptoApi {
|
||||
* @param key - the backup decryption key
|
||||
*/
|
||||
storeSessionBackupPrivateKey(key: Uint8Array): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the current status of key backup.
|
||||
*
|
||||
* @returns If automatic key backups are enabled, the `version` of the active backup. Otherwise, `null`.
|
||||
*/
|
||||
getActiveSessionBackupVersion(): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* Determine if a key backup can be trusted.
|
||||
*
|
||||
* @param info - key backup info dict from {@link MatrixClient#getKeyBackupVersion}.
|
||||
*/
|
||||
isKeyBackupTrusted(info: KeyBackupInfo): Promise<BackupTrustInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,10 @@ export interface Aes256AuthData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra info of a recovery key
|
||||
* Information about a server-side key backup.
|
||||
*
|
||||
* Returned by [`GET /_matrix/client/v3/room_keys/version`](https://spec.matrix.org/v1.7/client-server-api/#get_matrixclientv3room_keysversion)
|
||||
* and hence {@link MatrixClient#getKeyBackupVersion}.
|
||||
*/
|
||||
export interface KeyBackupInfo {
|
||||
algorithm: string;
|
||||
@@ -40,3 +43,20 @@ export interface KeyBackupInfo {
|
||||
etag?: string;
|
||||
version?: string; // number contained within
|
||||
}
|
||||
|
||||
/**
|
||||
* Information on whether a given server-side backup is trusted.
|
||||
*/
|
||||
export interface BackupTrustInfo {
|
||||
/**
|
||||
* Is this backup trusted?
|
||||
*
|
||||
* True if, and only if, there is a valid signature on the backup from a trusted device.
|
||||
*/
|
||||
readonly trusted: boolean;
|
||||
|
||||
/**
|
||||
* True if this backup matches the stored decryption key.
|
||||
*/
|
||||
readonly matchesDecryptionKey: boolean;
|
||||
}
|
||||
|
||||
@@ -1119,7 +1119,7 @@ export class OlmDevice {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`Storing megolm session ${senderKey}|${sessionId} with first index ` +
|
||||
session.first_known_index(),
|
||||
);
|
||||
|
||||
@@ -40,6 +40,7 @@ import { UnstableValue } from "../NamespacedValue";
|
||||
import { CryptoEvent } from "./index";
|
||||
import { crypto } from "./crypto";
|
||||
import { HTTPError, MatrixError } from "../http-api";
|
||||
import { BackupTrustInfo } from "../crypto-api/keybackup";
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
|
||||
@@ -54,6 +55,7 @@ type SigInfo = {
|
||||
deviceTrust?: DeviceTrustLevel;
|
||||
};
|
||||
|
||||
/** @deprecated Prefer {@link BackupTrustInfo} */
|
||||
export type TrustInfo = {
|
||||
usable: boolean; // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
sigs: SigInfo[];
|
||||
@@ -829,3 +831,15 @@ export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
|
||||
};
|
||||
|
||||
export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;
|
||||
|
||||
/**
|
||||
* Map a legacy {@link TrustInfo} into a new-style {@link BackupTrustInfo}.
|
||||
*
|
||||
* @param trustInfo - trustInfo to convert
|
||||
*/
|
||||
export function backupTrustInfoFromLegacyTrustInfo(trustInfo: TrustInfo): BackupTrustInfo {
|
||||
return {
|
||||
trusted: trustInfo.usable,
|
||||
matchesDecryptionKey: trustInfo.trusted_locally ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface IDevice {
|
||||
|
||||
/**
|
||||
* Information about a user's device
|
||||
*
|
||||
* Superceded by {@link Device}.
|
||||
*/
|
||||
export class DeviceInfo {
|
||||
/**
|
||||
|
||||
+36
-3
@@ -50,7 +50,7 @@ import { IllegalMethod } from "./verification/IllegalMethod";
|
||||
import { KeySignatureUploadError } from "../errors";
|
||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
|
||||
import { DehydrationManager } from "./dehydration";
|
||||
import { BackupManager } from "./backup";
|
||||
import { BackupManager, backupTrustInfoFromLegacyTrustInfo } from "./backup";
|
||||
import { IStore } from "../store";
|
||||
import { Room, RoomEvent } from "../models/room";
|
||||
import { RoomMember, RoomMemberEvent } from "../models/room-member";
|
||||
@@ -87,10 +87,12 @@ import {
|
||||
} from "../secret-storage";
|
||||
import { ISecretRequest } from "./SecretSharing";
|
||||
import {
|
||||
BackupTrustInfo,
|
||||
BootstrapCrossSigningOpts,
|
||||
CrossSigningStatus,
|
||||
DeviceVerificationStatus,
|
||||
ImportRoomKeysOpts,
|
||||
KeyBackupInfo,
|
||||
VerificationRequest as CryptoApiVerificationRequest,
|
||||
} from "../crypto-api";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
@@ -1280,6 +1282,28 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status of key backup.
|
||||
*
|
||||
* Implementation of {@link CryptoApi.getActiveSessionBackupVersion}.
|
||||
*/
|
||||
public async getActiveSessionBackupVersion(): Promise<string | null> {
|
||||
if (this.backupManager.getKeyBackupEnabled()) {
|
||||
return this.backupManager.version ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a key backup can be trusted.
|
||||
*
|
||||
* Implementation of {@link Crypto.CryptoApi.isKeyBackupTrusted}.
|
||||
*/
|
||||
public async isKeyBackupTrusted(info: KeyBackupInfo): Promise<BackupTrustInfo> {
|
||||
const trustInfo = await this.backupManager.isKeyBackupTrusted(info);
|
||||
return backupTrustInfoFromLegacyTrustInfo(trustInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a given cross-signing private key matches a given public key.
|
||||
* This can be used by the getCrossSigningKey callback to verify that the
|
||||
@@ -2174,6 +2198,15 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return this.deviceList.saveIfDirty(delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given device as locally verified.
|
||||
*
|
||||
* Implementation of {@link CryptoApi#setDeviceVerified}.
|
||||
*/
|
||||
public async setDeviceVerified(userId: string, deviceId: string, verified = true): Promise<void> {
|
||||
await this.setDeviceVerification(userId, deviceId, verified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the blocked/verified state of the given device
|
||||
*
|
||||
@@ -2347,8 +2380,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return deviceObj;
|
||||
}
|
||||
|
||||
public findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined {
|
||||
return this.inRoomVerificationRequests.findRequestInProgress(roomId);
|
||||
public findVerificationRequestDMInProgress(roomId: string, userId?: string): VerificationRequest | undefined {
|
||||
return this.inRoomVerificationRequests.findRequestInProgress(roomId, userId);
|
||||
}
|
||||
|
||||
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
|
||||
|
||||
@@ -350,11 +350,11 @@ export class InRoomRequests implements IRequestsMap {
|
||||
}
|
||||
}
|
||||
|
||||
public findRequestInProgress(roomId: string): VerificationRequest | undefined {
|
||||
public findRequestInProgress(roomId: string, userId?: string): VerificationRequest | undefined {
|
||||
const requestsByTxnId = this.requestsByRoomId.get(roomId);
|
||||
if (requestsByTxnId) {
|
||||
for (const request of requestsByTxnId.values()) {
|
||||
if (request.pending) {
|
||||
if (request.pending && (userId === undefined || request.requestingUserId === userId)) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -41,6 +41,7 @@ export * from "./models/thread";
|
||||
export * from "./models/typed-event-emitter";
|
||||
export * from "./models/user";
|
||||
export * from "./models/device";
|
||||
export * from "./models/search-result";
|
||||
export * from "./scheduler";
|
||||
export * from "./filter";
|
||||
export * from "./timeline-window";
|
||||
@@ -52,33 +53,41 @@ export * from "./crypto/store/memory-crypto-store";
|
||||
export * from "./crypto/store/indexeddb-crypto-store";
|
||||
export type { OutgoingRoomKeyRequest } from "./crypto/store/base";
|
||||
export * from "./content-repo";
|
||||
export * from "./@types/uia";
|
||||
export * from "./@types/event";
|
||||
export * from "./@types/PushRules";
|
||||
export * from "./@types/partials";
|
||||
export * from "./@types/requests";
|
||||
export * from "./@types/search";
|
||||
export * from "./models/room-summary";
|
||||
export * from "./models/event-status";
|
||||
export * as ContentHelpers from "./content-helpers";
|
||||
export * as SecretStorage from "./secret-storage";
|
||||
export type { ICryptoCallbacks } from "./crypto"; // used to be located here
|
||||
export { createNewMatrixCall } from "./webrtc/call";
|
||||
export { createNewMatrixCall, CallEvent } from "./webrtc/call";
|
||||
export type { MatrixCall } from "./webrtc/call";
|
||||
export {
|
||||
GroupCall,
|
||||
GroupCallEvent,
|
||||
GroupCallIntent,
|
||||
GroupCallState,
|
||||
GroupCallType,
|
||||
GroupCallStatsReportEvent,
|
||||
} from "./webrtc/groupCall";
|
||||
export type { GroupCall } from "./webrtc/groupCall";
|
||||
export { CryptoEvent } from "./crypto";
|
||||
export { SyncState, SetPresence } from "./sync";
|
||||
export type { ISyncStateData as SyncStateData } from "./sync";
|
||||
export { SlidingSyncEvent } from "./sliding-sync";
|
||||
export { MediaHandlerEvent } from "./webrtc/mediaHandler";
|
||||
export { CallEvent } from "./webrtc/call";
|
||||
export { CallFeedEvent } from "./webrtc/callFeed";
|
||||
export { StatsReport } from "./webrtc/stats/statsReport";
|
||||
export { RelationsEvent } from "./models/relations";
|
||||
export { Relations, RelationsEvent } from "./models/relations";
|
||||
export { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
export { LocalStorageErrors } from "./store/local-storage-events-emitter";
|
||||
export { IdentityProviderBrand, SSOAction } from "./@types/auth";
|
||||
export type { ISSOFlow as SSOFlow, LoginFlow } from "./@types/auth";
|
||||
export type { IHierarchyRelation as HierarchyRelation, IHierarchyRoom as HierarchyRoom } from "./@types/spaces";
|
||||
export { LocationAssetType } from "./@types/location";
|
||||
|
||||
/**
|
||||
* Types supporting cryptography.
|
||||
|
||||
@@ -1877,6 +1877,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* Takes the given thread root events and creates threads for them.
|
||||
*/
|
||||
public processThreadRoots(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
if (!this.client.supportsThreads()) return;
|
||||
for (const rootEvent of events) {
|
||||
EventTimeline.setEventMetadata(rootEvent, this.currentState, toStartOfTimeline);
|
||||
if (!this.getThread(rootEvent.getId()!)) {
|
||||
@@ -2031,6 +2032,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @internal
|
||||
*/
|
||||
private async fetchRoomThreadList(filter?: ThreadFilterType): Promise<void> {
|
||||
if (!this.client.supportsThreads()) return;
|
||||
if (this.threadsTimelineSets.length === 0) return;
|
||||
|
||||
const timelineSet = filter === ThreadFilterType.My ? this.threadsTimelineSets[1] : this.threadsTimelineSets[0];
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2023 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.
|
||||
*/
|
||||
|
||||
import { OlmMachine, SignatureVerification } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { BackupTrustInfo, Curve25519AuthData, KeyBackupInfo } from "../crypto-api/keybackup";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class RustBackupManager {
|
||||
public constructor(private readonly olmMachine: OlmMachine) {}
|
||||
|
||||
/**
|
||||
* Get the backup version we are currently backing up to, if any
|
||||
*/
|
||||
public async getActiveBackupVersion(): Promise<string | null> {
|
||||
// TODO stub
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a key backup can be trusted.
|
||||
*
|
||||
* @param info - key backup info dict from {@link MatrixClient#getKeyBackupVersion}.
|
||||
*/
|
||||
public async isKeyBackupTrusted(info: KeyBackupInfo): Promise<BackupTrustInfo> {
|
||||
const signatureVerification: SignatureVerification = await this.olmMachine.verifyBackup(info);
|
||||
|
||||
const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys();
|
||||
const pubKeyForSavedPrivateKey = backupKeys?.decryptionKey?.megolmV1PublicKey;
|
||||
const backupMatchesSavedPrivateKey =
|
||||
info.algorithm === pubKeyForSavedPrivateKey?.algorithm &&
|
||||
(info.auth_data as Curve25519AuthData)?.public_key === pubKeyForSavedPrivateKey.publicKeyBase64;
|
||||
|
||||
return {
|
||||
matchesDecryptionKey: backupMatchesSavedPrivateKey,
|
||||
trusted: signatureVerification.trusted(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
|
||||
import type { IEncryptedEventInfo } from "../crypto/api";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||
@@ -31,17 +31,19 @@ import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProc
|
||||
import { KeyClaimManager } from "./KeyClaimManager";
|
||||
import { MapWithDefault } from "../utils";
|
||||
import {
|
||||
BackupTrustInfo,
|
||||
BootstrapCrossSigningOpts,
|
||||
CreateSecretStorageOpts,
|
||||
CrossSigningKey,
|
||||
CrossSigningKeyInfo,
|
||||
CrossSigningStatus,
|
||||
CryptoCallbacks,
|
||||
DeviceVerificationStatus,
|
||||
GeneratedSecretStorageKey,
|
||||
ImportRoomKeyProgressData,
|
||||
ImportRoomKeysOpts,
|
||||
KeyBackupInfo,
|
||||
VerificationRequest,
|
||||
CrossSigningKeyInfo,
|
||||
} from "../crypto-api";
|
||||
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
|
||||
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
|
||||
@@ -53,9 +55,10 @@ import { keyFromPassphrase } from "../crypto/key_passphrase";
|
||||
import { encodeRecoveryKey } from "../crypto/recoverykey";
|
||||
import { crypto } from "../crypto/crypto";
|
||||
import { RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification";
|
||||
import { EventType } from "../@types/event";
|
||||
import { EventType, MsgType } from "../@types/event";
|
||||
import { CryptoEvent } from "../crypto";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { RustBackupManager } from "./backup";
|
||||
|
||||
const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
|
||||
|
||||
@@ -82,6 +85,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
private outgoingRequestProcessor: OutgoingRequestProcessor;
|
||||
private crossSigningIdentity: CrossSigningIdentity;
|
||||
|
||||
public readonly backupManager: RustBackupManager;
|
||||
|
||||
public constructor(
|
||||
/** The `OlmMachine` from the underlying rust crypto sdk. */
|
||||
private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
|
||||
@@ -109,6 +114,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
|
||||
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
|
||||
this.eventDecryptor = new EventDecryptor(olmMachine);
|
||||
this.backupManager = new RustBackupManager(olmMachine);
|
||||
|
||||
// Fire if the cross signing keys are imported from the secret storage
|
||||
const onCrossSigningKeysImport = (): void => {
|
||||
@@ -377,6 +383,23 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
// events. Maybe we need to do the same?
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given device as locally verified.
|
||||
*
|
||||
* Implementation of {@link CryptoApi#setDeviceVerified}.
|
||||
*/
|
||||
public async setDeviceVerified(userId: string, deviceId: string, verified = true): Promise<void> {
|
||||
const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice(
|
||||
new RustSdkCryptoJs.UserId(userId),
|
||||
new RustSdkCryptoJs.DeviceId(deviceId),
|
||||
);
|
||||
|
||||
if (!device) {
|
||||
throw new Error(`Unknown device ${userId}|${deviceId}`);
|
||||
}
|
||||
await device.setLocalTrust(verified ? RustSdkCryptoJs.LocalTrust.Verified : RustSdkCryptoJs.LocalTrust.Unset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getDeviceVerificationStatus}.
|
||||
*/
|
||||
@@ -657,13 +680,28 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
* Implementation of {@link CryptoApi#findVerificationRequestDMInProgress}
|
||||
*
|
||||
* @param roomId - the room to use for verification
|
||||
* @param userId - search the verification request for the given user
|
||||
*
|
||||
* @returns the VerificationRequest that is in progress, if any
|
||||
*
|
||||
*/
|
||||
public findVerificationRequestDMInProgress(roomId: string): undefined {
|
||||
// TODO
|
||||
return;
|
||||
public findVerificationRequestDMInProgress(roomId: string, userId?: string): VerificationRequest | undefined {
|
||||
if (!userId) throw new Error("missing userId");
|
||||
|
||||
const requests: RustSdkCryptoJs.VerificationRequest[] = this.olmMachine.getVerificationRequests(
|
||||
new RustSdkCryptoJs.UserId(userId),
|
||||
);
|
||||
|
||||
// Search for the verification request for the given room id
|
||||
const request = requests.find((request) => request.roomId?.toString() === roomId);
|
||||
|
||||
if (request) {
|
||||
return new RustVerificationRequest(
|
||||
request,
|
||||
this.outgoingRequestProcessor,
|
||||
this._supportedVerificationMethods,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -745,8 +783,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
*/
|
||||
public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> {
|
||||
const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys();
|
||||
if (!backupKeys.decryptionKeyBase64) return null;
|
||||
return Buffer.from(backupKeys.decryptionKeyBase64, "base64");
|
||||
if (!backupKeys.decryptionKey) return null;
|
||||
return Buffer.from(backupKeys.decryptionKey.toBase64(), "base64");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -763,6 +801,24 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
await this.olmMachine.saveBackupDecryptionKey(RustSdkCryptoJs.BackupDecryptionKey.fromBase64(base64Key), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status of key backup.
|
||||
*
|
||||
* Implementation of {@link CryptoApi#getActiveSessionBackupVersion}.
|
||||
*/
|
||||
public async getActiveSessionBackupVersion(): Promise<string | null> {
|
||||
return await this.backupManager.getActiveBackupVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a key backup can be trusted.
|
||||
*
|
||||
* Implementation of {@link Crypto.CryptoApi.isKeyBackupTrusted}.
|
||||
*/
|
||||
public async isKeyBackupTrusted(info: KeyBackupInfo): Promise<BackupTrustInfo> {
|
||||
return await this.backupManager.isKeyBackupTrusted(info);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SyncCryptoCallbacks implementation
|
||||
@@ -977,6 +1033,73 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a live event received via /sync.
|
||||
* See {@link ClientEventHandlerMap#event}
|
||||
*
|
||||
* @param event - live event
|
||||
*/
|
||||
public async onLiveEventFromSync(event: MatrixEvent): Promise<void> {
|
||||
// Ignore state event
|
||||
if (event.isState()) return;
|
||||
|
||||
const processEvent = async (evt: MatrixEvent): Promise<void> => {
|
||||
// Process only key validation request
|
||||
if (
|
||||
evt.getType() === EventType.RoomMessage &&
|
||||
evt.getContent().msgtype === MsgType.KeyVerificationRequest
|
||||
) {
|
||||
await this.onKeyVerificationRequest(evt);
|
||||
}
|
||||
};
|
||||
|
||||
// If the event is encrypted of in failure, we wait for decryption
|
||||
if (event.isDecryptionFailure() || event.isEncrypted()) {
|
||||
// 5 mins
|
||||
const TIMEOUT_DELAY = 5 * 60 * 1000;
|
||||
|
||||
const timeoutId = setTimeout(() => event.off(MatrixEventEvent.Decrypted, onDecrypted), TIMEOUT_DELAY);
|
||||
|
||||
const onDecrypted = (decryptedEvent: MatrixEvent, error?: Error): void => {
|
||||
if (error) return;
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
event.off(MatrixEventEvent.Decrypted, onDecrypted);
|
||||
processEvent(decryptedEvent);
|
||||
};
|
||||
// After 5mins, we are not expecting the event to be decrypted
|
||||
|
||||
event.on(MatrixEventEvent.Decrypted, onDecrypted);
|
||||
} else {
|
||||
await processEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key verification request.
|
||||
*
|
||||
* @param event - a key validation request event.
|
||||
*/
|
||||
private async onKeyVerificationRequest(event: MatrixEvent): Promise<void> {
|
||||
const roomId = event.getRoomId();
|
||||
|
||||
if (!roomId) {
|
||||
throw new Error("missing roomId in the event");
|
||||
}
|
||||
|
||||
await this.olmMachine.receiveVerificationEvent(
|
||||
JSON.stringify({
|
||||
event_id: event.getId(),
|
||||
type: event.getType(),
|
||||
sender: event.getSender(),
|
||||
state_key: event.getStateKey(),
|
||||
content: event.getContent(),
|
||||
origin_server_ts: event.getTs(),
|
||||
}),
|
||||
new RustSdkCryptoJs.RoomId(roomId),
|
||||
);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Outgoing requests
|
||||
|
||||
+19
-3
@@ -28,8 +28,7 @@ import { Optional } from "matrix-events-sdk";
|
||||
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend";
|
||||
import { User, UserEvent } from "./models/user";
|
||||
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
||||
import { promiseMapSeries, defer, deepCopy } from "./utils";
|
||||
import { IDeferred, noUnsafeEventProps, unsafeProp } from "./utils";
|
||||
import { deepCopy, defer, IDeferred, noUnsafeEventProps, promiseMapSeries, unsafeProp } from "./utils";
|
||||
import { Filter } from "./filter";
|
||||
import { EventTimeline } from "./models/event-timeline";
|
||||
import { logger } from "./logger";
|
||||
@@ -1316,7 +1315,7 @@ export class SyncApi {
|
||||
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
|
||||
|
||||
const encrypted = client.isRoomEncrypted(room.roomId);
|
||||
const encrypted = this.isRoomEncrypted(room, stateEvents, events);
|
||||
// We store the server-provided value first so it's correct when any of the events fire.
|
||||
if (joinObj.unread_notifications) {
|
||||
/**
|
||||
@@ -1324,6 +1323,9 @@ export class SyncApi {
|
||||
* bother setting it here. We trust our calculations better than the
|
||||
* server's for this case, and therefore will assume that our non-zero
|
||||
* count is accurate.
|
||||
* XXX: this is known faulty as the push rule for `.m.room.encrypted` may be disabled so server
|
||||
* may issue notification counts of 0 which we wrongly trust.
|
||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/2654 would fix this
|
||||
*
|
||||
* @see import("./client").fixNotificationCountOnDecryption
|
||||
*/
|
||||
@@ -1721,6 +1723,20 @@ export class SyncApi {
|
||||
});
|
||||
}
|
||||
|
||||
private findEncryptionEvent(events?: MatrixEvent[]): MatrixEvent | undefined {
|
||||
return events?.find((e) => e.getType() === EventType.RoomEncryption && e.getStateKey() === "");
|
||||
}
|
||||
|
||||
// When processing the sync response we cannot rely on MatrixClient::isRoomEncrypted before we actually
|
||||
// inject the events into the room object, so we have to inspect the events themselves.
|
||||
private isRoomEncrypted(room: Room, stateEventList: MatrixEvent[], timelineEventList?: MatrixEvent[]): boolean {
|
||||
return (
|
||||
this.client.isRoomEncrypted(room.roomId) ||
|
||||
!!this.findEncryptionEvent(stateEventList) ||
|
||||
!!this.findEncryptionEvent(timelineEventList)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects events into a room's model.
|
||||
* @param stateEventList - A list of state events. This is the state
|
||||
|
||||
@@ -35,10 +35,13 @@ const debuglog = DEBUG ? logger.log.bind(logger) : function (): void {};
|
||||
|
||||
/**
|
||||
* the number of times we ask the server for more events before giving up
|
||||
* this is currently higher than it needs to be to workaround lack of a filter
|
||||
* for excluding thread responses from main timeline pagination which can cause
|
||||
* giving up incorrectly - https://github.com/matrix-org/matrix-spec-proposals/pull/3874
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
||||
const DEFAULT_PAGINATE_LOOP_LIMIT = 10;
|
||||
|
||||
interface IOpts {
|
||||
/**
|
||||
|
||||
@@ -132,7 +132,7 @@ export class GroupCallEventHandler {
|
||||
break;
|
||||
}
|
||||
|
||||
logger.info(`GroupCallEventHandler createGroupCallForRoom() processed room (roomId=${room.roomId})`);
|
||||
logger.debug(`GroupCallEventHandler createGroupCallForRoom() processed room (roomId=${room.roomId})`);
|
||||
this.getRoomDeferred(room.roomId).resolve!();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user