Compare commits
26 Commits
v34.1.0-rc.0
...
v34.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 0300d6343f | |||
| dc1cccfecc | |||
| d32f398345 | |||
| 0f08c00c07 | |||
| 6b261b98c9 | |||
| 99f157a0f1 | |||
| f9f6d81346 | |||
| 46604abe7b | |||
| 553758e713 | |||
| 509e64cfc1 | |||
| 60c2e9b3ed | |||
| cfb21fa80a | |||
| c3d7f4e730 | |||
| aa97beae44 | |||
| 6f63ff1711 | |||
| 30a26813ec | |||
| f17a4fedb9 | |||
| 94e393c9a6 | |||
| 53201688a6 | |||
| 996663bf64 | |||
| d6e4338a37 | |||
| b2665f2128 | |||
| af4b6bc126 | |||
| 565bb0ef7c | |||
| fe0edcd081 | |||
| 51544f25a7 |
@@ -316,6 +316,11 @@ jobs:
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.npm.outputs.id }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [integ, unit]
|
||||
node: ["lts/*", 21, 22]
|
||||
node: ["lts/*", 22]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -63,6 +63,16 @@ jobs:
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
# Dummy completion job to simplify branch protections
|
||||
jest-complete:
|
||||
name: Jest tests
|
||||
needs: jest
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
matrix-react-sdk:
|
||||
name: Downstream test matrix-react-sdk
|
||||
if: github.event_name == 'merge_group'
|
||||
|
||||
@@ -39,7 +39,7 @@ client.publicRooms(function (err, data) {
|
||||
```
|
||||
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
[the Node.js terminal app](examples/node) for a more complex example.
|
||||
[the Node.js terminal app](examples/node/README.md) for a more complex example.
|
||||
|
||||
To start the client:
|
||||
|
||||
|
||||
+4
-4
@@ -53,7 +53,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^6.0.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^7.0.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
@@ -106,7 +106,7 @@
|
||||
"eslint-plugin-tsdoc": "^0.3.0",
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "10.0.7",
|
||||
"fetch-mock": "10.1.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^29.0.0",
|
||||
@@ -117,8 +117,8 @@
|
||||
"lint-staged": "^15.0.2",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.3.2",
|
||||
"rimraf": "^5.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"rimraf": "^6.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.0",
|
||||
"typedoc-plugin-coverage": "^3.0.0",
|
||||
|
||||
@@ -2333,8 +2333,84 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
|
||||
describe("m.room_key.withheld handling", () => {
|
||||
// TODO: there are a bunch more tests for this sort of thing in spec/unit/crypto/algorithms/megolm.spec.ts.
|
||||
// They should be converted to integ tests and moved.
|
||||
describe.each([
|
||||
["m.blacklisted", "The sender has blocked you.", DecryptionFailureCode.MEGOLM_KEY_WITHHELD],
|
||||
[
|
||||
"m.unverified",
|
||||
"The sender has disabled encrypting to unverified devices.",
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
],
|
||||
])(
|
||||
"Decryption fails with withheld error if a withheld notice with code '%s' is received",
|
||||
(withheldCode, expectedMessage, expectedErrorCode) => {
|
||||
it.each(["before", "after"])("%s the event", async (when) => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
|
||||
let awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
|
||||
|
||||
// Send Alice an encrypted room event which looks like it was encrypted with a megolm session
|
||||
async function sendEncryptedEvent() {
|
||||
const event = {
|
||||
...testData.ENCRYPTED_EVENT,
|
||||
origin_server_ts: Date.now(),
|
||||
};
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: { join: { [ROOM_ID]: { timeline: { events: [event] } } } },
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
}
|
||||
|
||||
// Send Alice a withheld notice
|
||||
async function sendWithheldMessage() {
|
||||
const withheldMessage = {
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: ROOM_ID,
|
||||
sender_key: testData.ENCRYPTED_EVENT.content!.sender_key,
|
||||
session_id: testData.ENCRYPTED_EVENT.content!.session_id,
|
||||
code: withheldCode,
|
||||
reason: "zzz",
|
||||
},
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
to_device: { events: [withheldMessage] },
|
||||
});
|
||||
await syncPromise(aliceClient);
|
||||
}
|
||||
|
||||
if (when === "before") {
|
||||
await sendWithheldMessage();
|
||||
await sendEncryptedEvent();
|
||||
} else {
|
||||
await sendEncryptedEvent();
|
||||
// Make sure that the first attempt to decrypt has happened before the withheld arrives
|
||||
await awaitDecryption;
|
||||
awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
|
||||
await sendWithheldMessage();
|
||||
}
|
||||
|
||||
const ev = await awaitDecryption;
|
||||
expect(ev.getContent()).toEqual({
|
||||
body: `** Unable to decrypt: DecryptionError: ${expectedMessage} **`,
|
||||
msgtype: "m.bad.encrypted",
|
||||
});
|
||||
|
||||
expect(ev.decryptionFailureReason).toEqual(expectedErrorCode);
|
||||
|
||||
// `isEncryptedDisabledForUnverifiedDevices` should be true for `m.unverified` and false for other errors.
|
||||
expect(ev.isEncryptedDisabledForUnverifiedDevices).toEqual(withheldCode === "m.unverified");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
oldBackendOnly("does not block decryption on an 'm.unavailable' report", async function () {
|
||||
// there may be a key downloads for alice
|
||||
|
||||
@@ -412,7 +412,12 @@ describe("MatrixEvent", () => {
|
||||
const crypto = {
|
||||
decryptEvent: jest
|
||||
.fn()
|
||||
.mockRejectedValue("DecryptionError: The sender has disabled encrypting to unverified devices."),
|
||||
.mockRejectedValue(
|
||||
new DecryptionError(
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
"The sender has disabled encrypting to unverified devices.",
|
||||
),
|
||||
),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
@@ -95,6 +95,7 @@ describe("initRustCrypto", () => {
|
||||
deleteSecretsFromInbox: jest.fn(),
|
||||
registerReceiveSecretCallback: jest.fn(),
|
||||
registerDevicesUpdatedCallback: jest.fn(),
|
||||
registerRoomKeysWithheldCallback: jest.fn(),
|
||||
outgoingRequests: jest.fn(),
|
||||
isBackupEnabled: jest.fn().mockResolvedValue(false),
|
||||
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
|
||||
|
||||
@@ -434,7 +434,7 @@ describe("TimelineWindow", function () {
|
||||
});
|
||||
|
||||
function idsOf(events: Array<MatrixEvent>): Array<string> {
|
||||
return events.map((e) => (e ? e.getId() ?? "MISSING_ID" : "MISSING_EVENT"));
|
||||
return events.map((e) => (e ? (e.getId() ?? "MISSING_ID") : "MISSING_EVENT"));
|
||||
}
|
||||
|
||||
describe("removing events", () => {
|
||||
|
||||
@@ -105,7 +105,7 @@ const mockGetStateEvents =
|
||||
(events: MatrixEvent[] = FAKE_STATE_EVENTS as MatrixEvent[]) =>
|
||||
(type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
|
||||
if (type === EventType.GroupCallMemberPrefix) {
|
||||
return userId === undefined ? events : events.find((e) => e.getStateKey() === userId) ?? null;
|
||||
return userId === undefined ? events : (events.find((e) => e.getStateKey() === userId) ?? null);
|
||||
} else {
|
||||
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
|
||||
return userId === undefined ? [fakeEvent] : fakeEvent;
|
||||
|
||||
+6
-3
@@ -324,10 +324,13 @@ export interface ICreateClientOpts {
|
||||
localTimeoutMs?: number;
|
||||
|
||||
/**
|
||||
* Set to true to use
|
||||
* Authorization header instead of query param to send the access token to the server.
|
||||
* Set to false to send the access token to the server via a query parameter rather
|
||||
* than the Authorization HTTP header.
|
||||
*
|
||||
* Default false.
|
||||
* Note that as of v1.11 of the Matrix spec, sending the access token via a query
|
||||
* is deprecated.
|
||||
*
|
||||
* Default true.
|
||||
*/
|
||||
useAuthorizationHeader?: boolean;
|
||||
|
||||
|
||||
+13
-2
@@ -557,6 +557,12 @@ export enum DecryptionFailureCode {
|
||||
/** Message was encrypted with a Megolm session whose keys have not been shared with us. */
|
||||
MEGOLM_UNKNOWN_INBOUND_SESSION_ID = "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
|
||||
|
||||
/** A special case of {@link MEGOLM_UNKNOWN_INBOUND_SESSION_ID}: the sender has told us it is withholding the key. */
|
||||
MEGOLM_KEY_WITHHELD = "MEGOLM_KEY_WITHHELD",
|
||||
|
||||
/** A special case of {@link MEGOLM_KEY_WITHHELD}: the sender has told us it is withholding the key, because the current device is unverified. */
|
||||
MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE = "MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE",
|
||||
|
||||
/** Message was encrypted with a Megolm session which has been shared with us, but in a later ratchet state. */
|
||||
OLM_UNKNOWN_MESSAGE_INDEX = "OLM_UNKNOWN_MESSAGE_INDEX",
|
||||
|
||||
@@ -848,9 +854,14 @@ export interface CreateSecretStorageOpts {
|
||||
setupNewSecretStorage?: boolean;
|
||||
|
||||
/**
|
||||
* Function called to get the user's
|
||||
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
|
||||
* Function called to get the user's current key backup passphrase.
|
||||
*
|
||||
* Should return a promise that resolves with a Uint8Array
|
||||
* containing the key, or rejects if the key cannot be obtained.
|
||||
*
|
||||
* Only used when the client has existing key backup, but no secret storage.
|
||||
*
|
||||
* @deprecated Not used by the Rust crypto stack.
|
||||
*/
|
||||
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
+14
-14
@@ -1221,13 +1221,13 @@ export class OlmDevice {
|
||||
this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
|
||||
if (session === null || sessionData === null) {
|
||||
if (withheld) {
|
||||
error = new DecryptionError(
|
||||
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
calculateWithheldMessage(withheld),
|
||||
{
|
||||
session: senderKey + "|" + sessionId,
|
||||
},
|
||||
);
|
||||
const failureCode =
|
||||
withheld.code === "m.unverified"
|
||||
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
|
||||
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
|
||||
error = new DecryptionError(failureCode, calculateWithheldMessage(withheld), {
|
||||
session: senderKey + "|" + sessionId,
|
||||
});
|
||||
}
|
||||
result = null;
|
||||
return;
|
||||
@@ -1237,13 +1237,13 @@ export class OlmDevice {
|
||||
res = session.decrypt(body);
|
||||
} catch (e) {
|
||||
if ((<Error>e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX" && withheld) {
|
||||
error = new DecryptionError(
|
||||
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
calculateWithheldMessage(withheld),
|
||||
{
|
||||
session: senderKey + "|" + sessionId,
|
||||
},
|
||||
);
|
||||
const failureCode =
|
||||
withheld.code === "m.unverified"
|
||||
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
|
||||
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
|
||||
error = new DecryptionError(failureCode, calculateWithheldMessage(withheld), {
|
||||
session: senderKey + "|" + sessionId,
|
||||
});
|
||||
} else {
|
||||
error = <Error>e;
|
||||
}
|
||||
|
||||
+2
-2
@@ -258,7 +258,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
}
|
||||
|
||||
room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
|
||||
return { event_id: response.event_id };
|
||||
return { event_id: response.event_id! };
|
||||
}
|
||||
|
||||
public async sendStateEvent(
|
||||
@@ -267,7 +267,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
content: any,
|
||||
stateKey = "",
|
||||
): Promise<ISendEventResponse> {
|
||||
return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
|
||||
return (await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId)) as ISendEventResponse;
|
||||
}
|
||||
|
||||
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> {
|
||||
|
||||
@@ -160,26 +160,27 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
const callMemberships: CallMembership[] = [];
|
||||
for (const memberEvent of callMemberEvents) {
|
||||
const content = memberEvent.getContent();
|
||||
const eventKeysCount = Object.keys(content).length;
|
||||
// Dont even bother about empty events (saves us from costly type/"key in" checks in bigger rooms)
|
||||
if (eventKeysCount === 0) continue;
|
||||
|
||||
let membershipContents: any[] = [];
|
||||
|
||||
// We first decide if its a MSC4143 event (per device state key)
|
||||
if ("memberships" in content) {
|
||||
if (eventKeysCount > 1 && "focus_active" in content) {
|
||||
// We have a MSC4143 event membership event
|
||||
membershipContents.push(content);
|
||||
} else if (eventKeysCount === 1 && "memberships" in content) {
|
||||
// we have a legacy (one event for all devices) event
|
||||
if (!Array.isArray(content["memberships"])) {
|
||||
logger.warn(`Malformed member event from ${memberEvent.getSender()}: memberships is not an array`);
|
||||
continue;
|
||||
}
|
||||
membershipContents = content["memberships"];
|
||||
} else {
|
||||
// We have a MSC4143 event membership event
|
||||
if (Object.keys(content).length !== 0) {
|
||||
// We checked for empty content to not try to construct CallMembership's with {}.
|
||||
membershipContents.push(content);
|
||||
}
|
||||
}
|
||||
if (membershipContents.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (membershipContents.length === 0) continue;
|
||||
|
||||
for (const membershipData of membershipContents) {
|
||||
try {
|
||||
const membership = new CallMembership(memberEvent, membershipData);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
Copyright 2024 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 * from "./CallMembership";
|
||||
export * from "./focus";
|
||||
export * from "./LivekitFocus";
|
||||
export * from "./MatrixRTCSession";
|
||||
export * from "./MatrixRTCSessionManager";
|
||||
export * from "./types";
|
||||
+5
-12
@@ -43,7 +43,6 @@ import { MatrixError } from "../http-api";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { EventStatus } from "./event-status";
|
||||
import { CryptoBackend, DecryptionError } from "../common-crypto/CryptoBackend";
|
||||
import { WITHHELD_MESSAGES } from "../crypto/OlmDevice";
|
||||
import { IAnnotatedPushRule } from "../@types/PushRules";
|
||||
import { Room } from "./room";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
@@ -312,12 +311,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
private thread?: Thread;
|
||||
private threadId?: string;
|
||||
|
||||
/*
|
||||
* True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
|
||||
* the sender has disabled encrypting to unverified devices.
|
||||
*/
|
||||
private encryptedDisabledForUnverifiedDevices = false;
|
||||
|
||||
/* Set an approximate timestamp for the event relative the local clock.
|
||||
* This will inherently be approximate because it doesn't take into account
|
||||
* the time between the server putting the 'age' field on the event as it sent
|
||||
@@ -411,7 +404,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
// The fallback in these cases will be to use the origin_server_ts.
|
||||
// For EDUs, the origin_server_ts also is not defined so we use Date.now().
|
||||
const age = this.getAge();
|
||||
this.localTimestamp = age !== undefined ? Date.now() - age : this.getTs() ?? Date.now();
|
||||
this.localTimestamp = age !== undefined ? Date.now() - age : (this.getTs() ?? Date.now());
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
}
|
||||
|
||||
@@ -787,12 +780,14 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
return this._decryptionFailureReason;
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
|
||||
* the sender has disabled encrypting to unverified devices.
|
||||
*
|
||||
* @deprecated: Prefer `event.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE`.
|
||||
*/
|
||||
public get isEncryptedDisabledForUnverifiedDevices(): boolean {
|
||||
return this.isDecryptionFailure() && this.encryptedDisabledForUnverifiedDevices;
|
||||
return this.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE;
|
||||
}
|
||||
|
||||
public shouldAttemptDecryption(): boolean {
|
||||
@@ -982,7 +977,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
this.claimedEd25519Key = decryptionResult.claimedEd25519Key ?? null;
|
||||
this.forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
this.untrusted = decryptionResult.untrusted || false;
|
||||
this.encryptedDisabledForUnverifiedDevices = false;
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
@@ -1003,7 +997,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
this.claimedEd25519Key = null;
|
||||
this.forwardingCurve25519KeyChain = [];
|
||||
this.untrusted = false;
|
||||
this.encryptedDisabledForUnverifiedDevices = reason === `DecryptionError: ${WITHHELD_MESSAGES["m.unverified"]}`;
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -24,7 +24,7 @@ import {
|
||||
} from "./event-timeline-set";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import { compare, removeElement } from "../utils";
|
||||
import { removeElement } from "../utils";
|
||||
import { normalize, noUnsafeEventProps } from "../utils";
|
||||
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
|
||||
import { EventStatus } from "./event-status";
|
||||
@@ -3451,7 +3451,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
return true;
|
||||
});
|
||||
// make sure members have stable order
|
||||
otherMembers.sort((a, b) => compare(a.userId, b.userId));
|
||||
const collator = new Intl.Collator();
|
||||
otherMembers.sort((a, b) => collator.compare(a.userId, b.userId));
|
||||
// only 5 first members, immitate summaryHeroes
|
||||
otherMembers = otherMembers.slice(0, 5);
|
||||
otherNames = otherMembers.map((m) => m.name);
|
||||
|
||||
@@ -14,16 +14,17 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import {
|
||||
CollectStrategy,
|
||||
EncryptionAlgorithm,
|
||||
EncryptionSettings,
|
||||
HistoryVisibility as RustHistoryVisibility,
|
||||
OlmMachine,
|
||||
RoomId,
|
||||
UserId,
|
||||
HistoryVisibility as RustHistoryVisibility,
|
||||
ToDeviceRequest,
|
||||
UserId,
|
||||
} from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { EventType } from "../@types/event";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
@@ -142,7 +143,7 @@ export class RoomEncryptor {
|
||||
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
|
||||
*/
|
||||
public encryptEvent(event: MatrixEvent | null, globalBlacklistUnverifiedDevices: boolean): Promise<void> {
|
||||
const logger = new LogSpan(this.prefixedLogger, event ? event.getTxnId() ?? "" : "prepareForEncryption");
|
||||
const logger = new LogSpan(this.prefixedLogger, event ? (event.getTxnId() ?? "") : "prepareForEncryption");
|
||||
// Ensure order of encryption to avoid message ordering issues, as the scheduler only ensures
|
||||
// events order after they have been encrypted.
|
||||
const prom = this.currentEncryptionPromise
|
||||
@@ -252,8 +253,11 @@ export class RoomEncryptor {
|
||||
|
||||
// When this.room.getBlacklistUnverifiedDevices() === null, the global settings should be used
|
||||
// See Room#getBlacklistUnverifiedDevices
|
||||
rustEncryptionSettings.onlyAllowTrustedDevices =
|
||||
this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices;
|
||||
if (this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices) {
|
||||
rustEncryptionSettings.sharingStrategy = CollectStrategy.DeviceBasedStrategyOnlyTrustedDevices;
|
||||
} else {
|
||||
rustEncryptionSettings.sharingStrategy = CollectStrategy.DeviceBasedStrategyAllDevices;
|
||||
}
|
||||
|
||||
await logDuration(this.prefixedLogger, "shareRoomKey", async () => {
|
||||
const shareMessages: ToDeviceRequest[] = await this.olmMachine.shareRoomKey(
|
||||
|
||||
@@ -174,6 +174,9 @@ async function initOlmMachine(
|
||||
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
|
||||
rustCrypto.onRoomKeysUpdated(sessions),
|
||||
);
|
||||
await olmMachine.registerRoomKeysWithheldCallback((withheld: RustSdkCryptoJs.RoomKeyWithheldInfo[]) =>
|
||||
rustCrypto.onRoomKeysWithheld(withheld),
|
||||
);
|
||||
await olmMachine.registerUserIdentityUpdatedCallback((userId: RustSdkCryptoJs.UserId) =>
|
||||
rustCrypto.onUserIdentityUpdated(userId),
|
||||
);
|
||||
|
||||
@@ -1486,7 +1486,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
this.logger.debug(
|
||||
`Got update for session ${key.senderKey.toBase64()}|${key.sessionId} in ${key.roomId.toString()}`,
|
||||
);
|
||||
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key);
|
||||
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key.roomId.toString(), key.sessionId);
|
||||
if (pendingList.length === 0) return;
|
||||
|
||||
this.logger.debug(
|
||||
@@ -1507,6 +1507,37 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `OlmMachine.registerRoomKeyWithheldCallback`.
|
||||
*
|
||||
* Called by the rust sdk whenever we are told that a key has been withheld. We see if we had any events that
|
||||
* failed to decrypt for the given session, and update their status if so.
|
||||
*
|
||||
* @param withheld - Details of the withheld sessions.
|
||||
*/
|
||||
public async onRoomKeysWithheld(withheld: RustSdkCryptoJs.RoomKeyWithheldInfo[]): Promise<void> {
|
||||
for (const session of withheld) {
|
||||
this.logger.debug(`Got withheld message for session ${session.sessionId} in ${session.roomId.toString()}`);
|
||||
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(
|
||||
session.roomId.toString(),
|
||||
session.sessionId,
|
||||
);
|
||||
if (pendingList.length === 0) return;
|
||||
|
||||
// The easiest way to update the status of the event is to have another go at decrypting it.
|
||||
this.logger.debug(
|
||||
"Retrying decryption on events:",
|
||||
pendingList.map((e) => `${e.getId()}`),
|
||||
);
|
||||
|
||||
for (const ev of pendingList) {
|
||||
ev.attemptDecryption(this, { isRetry: true }).catch((_e) => {
|
||||
// It's somewhat expected that we still can't decrypt here.
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `OlmMachine.registerUserIdentityUpdatedCallback`
|
||||
*
|
||||
@@ -1683,7 +1714,7 @@ class EventDecryptor {
|
||||
/**
|
||||
* Events which we couldn't decrypt due to unknown sessions / indexes.
|
||||
*
|
||||
* Map from senderKey to sessionId to Set of MatrixEvents
|
||||
* Map from roomId to sessionId to Set of MatrixEvents
|
||||
*/
|
||||
private eventsPendingKey = new MapWithDefault<string, MapWithDefault<string, Set<MatrixEvent>>>(
|
||||
() => new MapWithDefault<string, Set<MatrixEvent>>(() => new Set()),
|
||||
@@ -1787,6 +1818,17 @@ class EventDecryptor {
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a withheld code, expose that.
|
||||
if (err.maybe_withheld) {
|
||||
// Unfortunately the Rust SDK API doesn't let us distinguish between different withheld cases, other than
|
||||
// by string-matching.
|
||||
const failureCode =
|
||||
err.maybe_withheld === "The sender has disabled encrypting to unverified devices."
|
||||
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
|
||||
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
|
||||
throw new DecryptionError(failureCode, err.maybe_withheld, errorDetails);
|
||||
}
|
||||
|
||||
switch (err.code) {
|
||||
case RustSdkCryptoJs.DecryptionErrorCode.MissingRoomKey:
|
||||
throw new DecryptionError(
|
||||
@@ -1832,30 +1874,27 @@ class EventDecryptor {
|
||||
* Look for events which are waiting for a given megolm session
|
||||
*
|
||||
* Returns a list of events which were encrypted by `session` and could not be decrypted
|
||||
*
|
||||
* @param session -
|
||||
*/
|
||||
public getEventsPendingRoomKey(session: RustSdkCryptoJs.RoomKeyInfo): MatrixEvent[] {
|
||||
const senderPendingEvents = this.eventsPendingKey.get(session.senderKey.toBase64());
|
||||
if (!senderPendingEvents) return [];
|
||||
public getEventsPendingRoomKey(roomId: string, sessionId: string): MatrixEvent[] {
|
||||
const roomPendingEvents = this.eventsPendingKey.get(roomId);
|
||||
if (!roomPendingEvents) return [];
|
||||
|
||||
const sessionPendingEvents = senderPendingEvents.get(session.sessionId);
|
||||
const sessionPendingEvents = roomPendingEvents.get(sessionId);
|
||||
if (!sessionPendingEvents) return [];
|
||||
|
||||
const roomId = session.roomId.toString();
|
||||
return [...sessionPendingEvents].filter((ev) => ev.getRoomId() === roomId);
|
||||
return [...sessionPendingEvents];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the list of those awaiting their session keys.
|
||||
*/
|
||||
private addEventToPendingList(event: MatrixEvent): void {
|
||||
const content = event.getWireContent();
|
||||
const senderKey = content.sender_key;
|
||||
const sessionId = content.session_id;
|
||||
const roomId = event.getRoomId();
|
||||
// We shouldn't have events without a room id here.
|
||||
if (!roomId) return;
|
||||
|
||||
const senderPendingEvents = this.eventsPendingKey.getOrCreate(senderKey);
|
||||
const sessionPendingEvents = senderPendingEvents.getOrCreate(sessionId);
|
||||
const roomPendingEvents = this.eventsPendingKey.getOrCreate(roomId);
|
||||
const sessionPendingEvents = roomPendingEvents.getOrCreate(event.getWireContent().session_id);
|
||||
sessionPendingEvents.add(event);
|
||||
}
|
||||
|
||||
@@ -1863,23 +1902,22 @@ class EventDecryptor {
|
||||
* Remove an event from the list of those awaiting their session keys.
|
||||
*/
|
||||
private removeEventFromPendingList(event: MatrixEvent): void {
|
||||
const content = event.getWireContent();
|
||||
const senderKey = content.sender_key;
|
||||
const sessionId = content.session_id;
|
||||
const roomId = event.getRoomId();
|
||||
if (!roomId) return;
|
||||
|
||||
const senderPendingEvents = this.eventsPendingKey.get(senderKey);
|
||||
if (!senderPendingEvents) return;
|
||||
const roomPendingEvents = this.eventsPendingKey.getOrCreate(roomId);
|
||||
if (!roomPendingEvents) return;
|
||||
|
||||
const sessionPendingEvents = senderPendingEvents.get(sessionId);
|
||||
const sessionPendingEvents = roomPendingEvents.get(event.getWireContent().session_id);
|
||||
if (!sessionPendingEvents) return;
|
||||
|
||||
sessionPendingEvents.delete(event);
|
||||
|
||||
// also clean up the higher-level maps if they are now empty
|
||||
if (sessionPendingEvents.size === 0) {
|
||||
senderPendingEvents.delete(sessionId);
|
||||
if (senderPendingEvents.size === 0) {
|
||||
this.eventsPendingKey.delete(senderKey);
|
||||
roomPendingEvents.delete(event.getWireContent().session_id);
|
||||
if (roomPendingEvents.size === 0) {
|
||||
this.eventsPendingKey.delete(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,7 +504,7 @@ export class SyncAccumulator {
|
||||
|
||||
currentData._timeline.push({
|
||||
event: transformedEvent,
|
||||
token: index === 0 ? data.timeline.prev_batch ?? null : null,
|
||||
token: index === 0 ? (data.timeline.prev_batch ?? null) : null,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+3
-1
@@ -1403,7 +1403,9 @@ export class SyncApi {
|
||||
if (limited) {
|
||||
room.resetLiveTimeline(
|
||||
joinObj.timeline.prev_batch,
|
||||
this.syncOpts.canResetEntireTimeline!(room.roomId) ? null : syncEventData.oldSyncToken ?? null,
|
||||
this.syncOpts.canResetEntireTimeline!(room.roomId)
|
||||
? null
|
||||
: (syncEventData.oldSyncToken ?? null),
|
||||
);
|
||||
|
||||
// We have to assume any gap in any timeline is
|
||||
|
||||
@@ -649,16 +649,6 @@ export function lexicographicCompare(a: string, b: string): number {
|
||||
}
|
||||
}
|
||||
|
||||
const collator = new Intl.Collator();
|
||||
/**
|
||||
* Performant language-sensitive string comparison
|
||||
* @param a - the first string to compare
|
||||
* @param b - the second string to compare
|
||||
*/
|
||||
export function compare(a: string, b: string): number {
|
||||
return collator.compare(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is similar to Object.assign() but it assigns recursively and
|
||||
* allows you to ignore nullish values from the source
|
||||
|
||||
Reference in New Issue
Block a user