first commit

This commit is contained in:
Myk
2025-07-31 23:47:20 +03:00
commit 2186b278a0
5149 changed files with 537218 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
/// <reference types="node" />
import { MatrixClient } from "../MatrixClient";
import { IMegolmEncrypted, IOlmEncrypted, IToDeviceMessage, OTKAlgorithm, OTKCounts, Signatures } from "../models/Crypto";
import { EncryptedRoomEvent } from "../models/events/EncryptedRoomEvent";
import { RoomEvent } from "../models/events/RoomEvent";
import { EncryptedFile } from "../models/events/MessageEvent";
import { IKeyBackupInfoRetrieved } from "../models/KeyBackup";
/**
* Manages encryption for a MatrixClient. Get an instance from a MatrixClient directly
* rather than creating one manually.
* @category Encryption
*/
export declare class CryptoClient {
private client;
private ready;
private deviceId;
private deviceEd25519;
private deviceCurve25519;
private roomTracker;
private engine;
constructor(client: MatrixClient);
private get storage();
/**
* The device ID for the MatrixClient.
*/
get clientDeviceId(): string;
/**
* The device's Ed25519 identity
*/
get clientDeviceEd25519(): string;
/**
* Whether or not the crypto client is ready to be used. If not ready, prepare() should be called.
* @see prepare
*/
get isReady(): boolean;
/**
* Prepares the crypto client for usage.
* @param {string[]} roomIds The room IDs the MatrixClient is joined to.
*/
prepare(): Promise<void>;
/**
* Handles a room event.
* @internal
* @param roomId The room ID.
* @param event The event.
*/
onRoomEvent(roomId: string, event: any): Promise<void>;
/**
* Handles a room join.
* @internal
* @param roomId The room ID.
*/
onRoomJoin(roomId: string): Promise<void>;
/**
* Exports a set of keys for a given session.
* @param roomId The room ID for the session.
* @param sessionId The session ID.
* @returns An array of session keys.
*/
exportRoomKeysForSession(roomId: string, sessionId: string): Promise<import("../models/KeyBackup").IMegolmSessionDataExport[]>;
/**
* Checks if a room is encrypted.
* @param {string} roomId The room ID to check.
* @returns {Promise<boolean>} Resolves to true if encrypted, false otherwise.
*/
isRoomEncrypted(roomId: string): Promise<boolean>;
/**
* Updates the client's sync-related data.
* @param {Array.<IToDeviceMessage<IOlmEncrypted>>} toDeviceMessages The to-device messages received.
* @param {OTKCounts} otkCounts The current OTK counts.
* @param {OTKAlgorithm[]} unusedFallbackKeyAlgs The unused fallback key algorithms.
* @param {string[]} changedDeviceLists The user IDs which had device list changes.
* @param {string[]} leftDeviceLists The user IDs which the server believes we no longer need to track.
* @returns {Promise<void>} Resolves when complete.
*/
updateSyncData(toDeviceMessages: IToDeviceMessage<IOlmEncrypted>[], otkCounts: OTKCounts, unusedFallbackKeyAlgs: OTKAlgorithm[], changedDeviceLists: string[], leftDeviceLists: string[]): Promise<void>;
/**
* Signs an object using the device keys.
* @param {object} obj The object to sign.
* @returns {Promise<Signatures>} The signatures for the object.
*/
sign(obj: object): Promise<Signatures>;
/**
* Encrypts the details of a room event, returning an encrypted payload to be sent in an
* `m.room.encrypted` event to the room. If needed, this function will send decryption keys
* to the appropriate devices in the room (this happens when the Megolm session rotates or
* gets created).
* @param {string} roomId The room ID to encrypt within. If the room is not encrypted, an
* error is thrown.
* @param {string} eventType The event type being encrypted.
* @param {any} content The event content being encrypted.
* @returns {Promise<IMegolmEncrypted>} Resolves to the encrypted content for an `m.room.encrypted` event.
*/
encryptRoomEvent(roomId: string, eventType: string, content: any): Promise<IMegolmEncrypted>;
/**
* Decrypts a room event. Currently only supports Megolm-encrypted events (default for this SDK).
* @param {EncryptedRoomEvent} event The encrypted event.
* @param {string} roomId The room ID where the event was sent.
* @returns {Promise<RoomEvent<unknown>>} Resolves to a decrypted room event, or rejects/throws with
* an error if the event is undecryptable.
*/
decryptRoomEvent(event: EncryptedRoomEvent, roomId: string): Promise<RoomEvent<unknown>>;
/**
* Encrypts a file for uploading in a room, returning the encrypted data and information
* to include in a message event (except media URL) for sending.
* @param {Buffer} file The file to encrypt.
* @returns {{buffer: Buffer, file: Omit<EncryptedFile, "url">}} Resolves to the encrypted
* contents and file information.
*/
encryptMedia(file: Buffer): Promise<{
buffer: Buffer;
file: Omit<EncryptedFile, "url">;
}>;
/**
* Decrypts a previously-uploaded encrypted file, validating the fields along the way.
* @param {EncryptedFile} file The file to decrypt.
* @returns {Promise<Buffer>} Resolves to the decrypted file contents.
*/
decryptMedia(file: EncryptedFile): Promise<Buffer>;
/**
* Enable backing up of room keys.
* @param {IKeyBackupInfoRetrieved} info The configuration for key backup behaviour,
* as returned by {@link MatrixClient#getKeyBackupVersion}.
* @returns {Promise<void>} Resolves once backups have been enabled.
*/
enableKeyBackup(info: IKeyBackupInfoRetrieved): Promise<void>;
/**
* Disable backing up of room keys.
*/
disableKeyBackup(): Promise<void>;
private readonly onToDeviceMessage;
}

View File

@@ -0,0 +1,334 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CryptoClient = void 0;
const matrix_sdk_crypto_nodejs_1 = require("@matrix-org/matrix-sdk-crypto-nodejs");
const LogService_1 = require("../logging/LogService");
const decorators_1 = require("./decorators");
const RoomTracker_1 = require("./RoomTracker");
const EncryptedRoomEvent_1 = require("../models/events/EncryptedRoomEvent");
const RoomEvent_1 = require("../models/events/RoomEvent");
const RustEngine_1 = require("./RustEngine");
const MembershipEvent_1 = require("../models/events/MembershipEvent");
/**
* Manages encryption for a MatrixClient. Get an instance from a MatrixClient directly
* rather than creating one manually.
* @category Encryption
*/
class CryptoClient {
client;
ready = false;
deviceId;
deviceEd25519;
deviceCurve25519;
roomTracker;
engine;
constructor(client) {
this.client = client;
this.roomTracker = new RoomTracker_1.RoomTracker(this.client);
}
get storage() {
return this.client.cryptoStore;
}
/**
* The device ID for the MatrixClient.
*/
get clientDeviceId() {
return this.deviceId;
}
/**
* The device's Ed25519 identity
*/
get clientDeviceEd25519() {
return this.deviceEd25519;
}
/**
* Whether or not the crypto client is ready to be used. If not ready, prepare() should be called.
* @see prepare
*/
get isReady() {
return this.ready;
}
/**
* Prepares the crypto client for usage.
* @param {string[]} roomIds The room IDs the MatrixClient is joined to.
*/
async prepare() {
if (this.ready)
return; // stop re-preparing here
const storedDeviceId = await this.client.cryptoStore.getDeviceId();
const { user_id: userId, device_id: deviceId } = (await this.client.getWhoAmI());
if (!deviceId) {
throw new Error("Encryption not possible: server not revealing device ID");
}
const storagePath = await this.storage.getMachineStoragePath(deviceId);
if (storedDeviceId !== deviceId) {
this.client.cryptoStore.setDeviceId(deviceId);
}
this.deviceId = deviceId;
LogService_1.LogService.info("CryptoClient", `Starting ${userId} with device ID:`, this.deviceId); // info so all bots know for debugging
const machine = await matrix_sdk_crypto_nodejs_1.OlmMachine.initialize(new matrix_sdk_crypto_nodejs_1.UserId(userId), new matrix_sdk_crypto_nodejs_1.DeviceId(this.deviceId), storagePath, "", this.storage.storageType);
this.engine = new RustEngine_1.RustEngine(machine, this.client);
await this.engine.run();
const identity = this.engine.machine.identityKeys;
this.deviceCurve25519 = identity.curve25519.toBase64();
this.deviceEd25519 = identity.ed25519.toBase64();
LogService_1.LogService.info("CryptoClient", `Running ${userId} with device Ed25519 identity:`, this.deviceEd25519); // info so all bots know for debugging
this.ready = true;
}
/**
* Handles a room event.
* @internal
* @param roomId The room ID.
* @param event The event.
*/
async onRoomEvent(roomId, event) {
await this.roomTracker.onRoomEvent(roomId, event);
if (typeof event['state_key'] !== 'string')
return;
if (event['type'] === 'm.room.member') {
const membership = new MembershipEvent_1.MembershipEvent(event);
if (membership.effectiveMembership !== 'join' && membership.effectiveMembership !== 'invite')
return;
await this.engine.addTrackedUsers([membership.membershipFor]);
}
else if (event['type'] === 'm.room.encryption') {
return this.client.getRoomMembers(roomId, null, ['join', 'invite']).then(members => this.engine.addTrackedUsers(members.map(e => e.membershipFor)), e => void LogService_1.LogService.warn("CryptoClient", `Unable to get members of room ${roomId}`));
}
}
/**
* Handles a room join.
* @internal
* @param roomId The room ID.
*/
async onRoomJoin(roomId) {
await this.roomTracker.onRoomJoin(roomId);
if (await this.isRoomEncrypted(roomId)) {
const members = await this.client.getRoomMembers(roomId, null, ['join', 'invite']);
await this.engine.addTrackedUsers(members.map(e => e.membershipFor));
}
}
/**
* Exports a set of keys for a given session.
* @param roomId The room ID for the session.
* @param sessionId The session ID.
* @returns An array of session keys.
*/
async exportRoomKeysForSession(roomId, sessionId) {
return this.engine.exportRoomKeysForSession(roomId, sessionId);
}
/**
* Checks if a room is encrypted.
* @param {string} roomId The room ID to check.
* @returns {Promise<boolean>} Resolves to true if encrypted, false otherwise.
*/
async isRoomEncrypted(roomId) {
const config = await this.roomTracker.getRoomCryptoConfig(roomId);
return !!config?.algorithm;
}
/**
* Updates the client's sync-related data.
* @param {Array.<IToDeviceMessage<IOlmEncrypted>>} toDeviceMessages The to-device messages received.
* @param {OTKCounts} otkCounts The current OTK counts.
* @param {OTKAlgorithm[]} unusedFallbackKeyAlgs The unused fallback key algorithms.
* @param {string[]} changedDeviceLists The user IDs which had device list changes.
* @param {string[]} leftDeviceLists The user IDs which the server believes we no longer need to track.
* @returns {Promise<void>} Resolves when complete.
*/
async updateSyncData(toDeviceMessages, otkCounts, unusedFallbackKeyAlgs, changedDeviceLists, leftDeviceLists) {
const deviceMessages = JSON.stringify(toDeviceMessages);
const deviceLists = new matrix_sdk_crypto_nodejs_1.DeviceLists(changedDeviceLists.map(u => new matrix_sdk_crypto_nodejs_1.UserId(u)), leftDeviceLists.map(u => new matrix_sdk_crypto_nodejs_1.UserId(u)));
await this.engine.lock.acquire(RustEngine_1.SYNC_LOCK_NAME, async () => {
const syncResp = JSON.parse(await this.engine.machine.receiveSyncChanges(deviceMessages, deviceLists, otkCounts, unusedFallbackKeyAlgs));
if (Array.isArray(syncResp) && syncResp.length === 2 && Array.isArray(syncResp[0])) {
for (const msg of syncResp[0]) {
this.client.emit("to_device.decrypted", msg);
}
}
else {
LogService_1.LogService.error("CryptoClient", "OlmMachine.receiveSyncChanges did not return an expected value of [to-device events, room key changes]");
}
await this.engine.run();
});
}
/**
* Signs an object using the device keys.
* @param {object} obj The object to sign.
* @returns {Promise<Signatures>} The signatures for the object.
*/
async sign(obj) {
obj = JSON.parse(JSON.stringify(obj));
const existingSignatures = obj['signatures'] || {};
delete obj['signatures'];
delete obj['unsigned'];
const container = await this.engine.machine.sign(JSON.stringify(obj));
const userSignature = container.get(new matrix_sdk_crypto_nodejs_1.UserId(await this.client.getUserId()));
const sig = {
[await this.client.getUserId()]: {},
};
for (const [key, maybeSignature] of Object.entries(userSignature)) {
if (maybeSignature.isValid) {
sig[await this.client.getUserId()][key] = maybeSignature.signature.toBase64();
}
}
return {
...sig,
...existingSignatures,
};
}
/**
* Encrypts the details of a room event, returning an encrypted payload to be sent in an
* `m.room.encrypted` event to the room. If needed, this function will send decryption keys
* to the appropriate devices in the room (this happens when the Megolm session rotates or
* gets created).
* @param {string} roomId The room ID to encrypt within. If the room is not encrypted, an
* error is thrown.
* @param {string} eventType The event type being encrypted.
* @param {any} content The event content being encrypted.
* @returns {Promise<IMegolmEncrypted>} Resolves to the encrypted content for an `m.room.encrypted` event.
*/
async encryptRoomEvent(roomId, eventType, content) {
if (!(await this.isRoomEncrypted(roomId))) {
throw new Error("Room is not encrypted");
}
await this.engine.prepareEncrypt(roomId, await this.roomTracker.getRoomCryptoConfig(roomId));
const encrypted = JSON.parse(await this.engine.machine.encryptRoomEvent(new matrix_sdk_crypto_nodejs_1.RoomId(roomId), eventType, JSON.stringify(content)));
await this.engine.run();
return encrypted;
}
/**
* Decrypts a room event. Currently only supports Megolm-encrypted events (default for this SDK).
* @param {EncryptedRoomEvent} event The encrypted event.
* @param {string} roomId The room ID where the event was sent.
* @returns {Promise<RoomEvent<unknown>>} Resolves to a decrypted room event, or rejects/throws with
* an error if the event is undecryptable.
*/
async decryptRoomEvent(event, roomId) {
const decrypted = await this.engine.machine.decryptRoomEvent(JSON.stringify(event.raw), new matrix_sdk_crypto_nodejs_1.RoomId(roomId));
const clearEvent = JSON.parse(decrypted.event);
return new RoomEvent_1.RoomEvent({
...event.raw,
type: clearEvent.type || "io.t2bot.unknown",
content: (typeof (clearEvent.content) === 'object') ? clearEvent.content : {},
});
}
/**
* Encrypts a file for uploading in a room, returning the encrypted data and information
* to include in a message event (except media URL) for sending.
* @param {Buffer} file The file to encrypt.
* @returns {{buffer: Buffer, file: Omit<EncryptedFile, "url">}} Resolves to the encrypted
* contents and file information.
*/
async encryptMedia(file) {
const encrypted = matrix_sdk_crypto_nodejs_1.Attachment.encrypt(file);
const info = JSON.parse(encrypted.mediaEncryptionInfo);
return {
buffer: Buffer.from(encrypted.encryptedData),
file: info,
};
}
/**
* Decrypts a previously-uploaded encrypted file, validating the fields along the way.
* @param {EncryptedFile} file The file to decrypt.
* @returns {Promise<Buffer>} Resolves to the decrypted file contents.
*/
async decryptMedia(file) {
const contents = this.client.contentScannerInstance ?
await this.client.contentScannerInstance.downloadEncryptedContent(file) :
(await this.client.downloadContent(file.url)).data;
const encrypted = new matrix_sdk_crypto_nodejs_1.EncryptedAttachment(contents, JSON.stringify(file));
const decrypted = matrix_sdk_crypto_nodejs_1.Attachment.decrypt(encrypted);
return Buffer.from(decrypted);
}
/**
* Enable backing up of room keys.
* @param {IKeyBackupInfoRetrieved} info The configuration for key backup behaviour,
* as returned by {@link MatrixClient#getKeyBackupVersion}.
* @returns {Promise<void>} Resolves once backups have been enabled.
*/
async enableKeyBackup(info) {
if (!this.engine.isBackupEnabled()) {
// Only add the listener if we didn't add it already
this.client.on("to_device.decrypted", this.onToDeviceMessage);
}
await this.engine.enableKeyBackup(info);
// Back up any pending keys now, but asynchronously
void this.engine.backupRoomKeys();
}
/**
* Disable backing up of room keys.
*/
async disableKeyBackup() {
await this.engine.disableKeyBackup();
this.client.removeListener("to_device.decrypted", this.onToDeviceMessage);
}
onToDeviceMessage = (msg) => {
if (msg.type === "m.room_key") {
this.engine.backupRoomKeys();
}
};
}
exports.CryptoClient = CryptoClient;
__decorate([
(0, decorators_1.requiresReady)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], CryptoClient.prototype, "isRoomEncrypted", null);
__decorate([
(0, decorators_1.requiresReady)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array, Object, Array, Array, Array]),
__metadata("design:returntype", Promise)
], CryptoClient.prototype, "updateSyncData", null);
__decorate([
(0, decorators_1.requiresReady)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], CryptoClient.prototype, "sign", null);
__decorate([
(0, decorators_1.requiresReady)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String, Object]),
__metadata("design:returntype", Promise)
], CryptoClient.prototype, "encryptRoomEvent", null);
__decorate([
(0, decorators_1.requiresReady)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [EncryptedRoomEvent_1.EncryptedRoomEvent, String]),
__metadata("design:returntype", Promise)
], CryptoClient.prototype, "decryptRoomEvent", null);
__decorate([
(0, decorators_1.requiresReady)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Buffer]),
__metadata("design:returntype", Promise)
], CryptoClient.prototype, "encryptMedia", null);
__decorate([
(0, decorators_1.requiresReady)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], CryptoClient.prototype, "decryptMedia", null);
__decorate([
(0, decorators_1.requiresReady)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], CryptoClient.prototype, "enableKeyBackup", null);
__decorate([
(0, decorators_1.requiresReady)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], CryptoClient.prototype, "disableKeyBackup", null);
//# sourceMappingURL=CryptoClient.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
import { EncryptionEventContent } from "../models/events/EncryptionEvent";
/**
* Information about a room for the purposes of crypto.
* @category Encryption
*/
export interface ICryptoRoomInformation extends Partial<EncryptionEventContent> {
historyVisibility?: string;
}

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=ICryptoRoomInformation.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICryptoRoomInformation.js","sourceRoot":"","sources":["../../src/e2ee/ICryptoRoomInformation.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,36 @@
import { MatrixClient } from "../MatrixClient";
import { ICryptoRoomInformation } from "./ICryptoRoomInformation";
/**
* Tracks room encryption status for a MatrixClient.
* @category Encryption
*/
export declare class RoomTracker {
private client;
constructor(client: MatrixClient);
/**
* Handles a room join
* @internal
* @param roomId The room ID.
*/
onRoomJoin(roomId: string): Promise<void>;
/**
* Handles a room event.
* @internal
* @param roomId The room ID.
* @param event The event.
*/
onRoomEvent(roomId: string, event: any): Promise<void>;
/**
* Queues a room check for the tracker. If the room needs an update to the store, an
* update will be made.
* @param {string} roomId The room ID to check.
*/
queueRoomCheck(roomId: string): Promise<void>;
/**
* Gets the room's crypto configuration, as known by the underlying store. If the room is
* not encrypted then this will return an empty object.
* @param {string} roomId The room ID to get the config for.
* @returns {Promise<ICryptoRoomInformation>} Resolves to the encryption config.
*/
getRoomCryptoConfig(roomId: string): Promise<ICryptoRoomInformation>;
}

View File

@@ -0,0 +1,94 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RoomTracker = void 0;
const MatrixError_1 = require("../models/MatrixError");
// noinspection ES6RedundantAwait
/**
* Tracks room encryption status for a MatrixClient.
* @category Encryption
*/
class RoomTracker {
client;
constructor(client) {
this.client = client;
}
/**
* Handles a room join
* @internal
* @param roomId The room ID.
*/
async onRoomJoin(roomId) {
await this.queueRoomCheck(roomId);
}
/**
* Handles a room event.
* @internal
* @param roomId The room ID.
* @param event The event.
*/
async onRoomEvent(roomId, event) {
if (event['state_key'] !== '')
return; // we don't care about anything else
if (event['type'] === 'm.room.encryption' || event['type'] === 'm.room.history_visibility') {
await this.queueRoomCheck(roomId);
}
}
/**
* Queues a room check for the tracker. If the room needs an update to the store, an
* update will be made.
* @param {string} roomId The room ID to check.
*/
async queueRoomCheck(roomId) {
const config = await this.client.cryptoStore.getRoom(roomId);
if (config) {
if (config.algorithm !== undefined) {
return; // assume no change to encryption config
}
}
let encEvent;
try {
encEvent = await this.client.getRoomStateEvent(roomId, "m.room.encryption", "");
encEvent.algorithm = encEvent.algorithm ?? 'UNKNOWN';
}
catch (e) {
if (e instanceof MatrixError_1.MatrixError && e.errcode === "M_NOT_FOUND") {
encEvent = {};
}
else {
return; // Other failures should not be cached.
}
}
// Pick out the history visibility setting too
let historyVisibility;
try {
const ev = await this.client.getRoomStateEvent(roomId, "m.room.history_visibility", "");
historyVisibility = ev.history_visibility;
}
catch (e) {
// ignore - we'll just treat history visibility as normal
}
await this.client.cryptoStore.storeRoom(roomId, {
...encEvent,
historyVisibility,
});
}
/**
* Gets the room's crypto configuration, as known by the underlying store. If the room is
* not encrypted then this will return an empty object.
* @param {string} roomId The room ID to get the config for.
* @returns {Promise<ICryptoRoomInformation>} Resolves to the encryption config.
*/
async getRoomCryptoConfig(roomId) {
let config = await this.client.cryptoStore.getRoom(roomId);
if (!config) {
await this.queueRoomCheck(roomId);
config = await this.client.cryptoStore.getRoom(roomId);
}
if (!config) {
return {};
}
return config;
}
}
exports.RoomTracker = RoomTracker;
//# sourceMappingURL=RoomTracker.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"RoomTracker.js","sourceRoot":"","sources":["../../src/e2ee/RoomTracker.ts"],"names":[],"mappings":";;;AACA,uDAAoD;AAIpD,iCAAiC;AACjC;;;GAGG;AACH,MAAa,WAAW;IACO;IAA3B,YAA2B,MAAoB;QAApB,WAAM,GAAN,MAAM,CAAc;IAC/C,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,UAAU,CAAC,MAAc;QAClC,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,WAAW,CAAC,MAAc,EAAE,KAAU;QAC/C,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE;YAAE,OAAO,CAAC,oCAAoC;QAC3E,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,mBAAmB,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,2BAA2B,EAAE;YACxF,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;SACrC;IACL,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,cAAc,CAAC,MAAc;QACtC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC7D,IAAI,MAAM,EAAE;YACR,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,EAAE;gBAChC,OAAO,CAAC,wCAAwC;aACnD;SACJ;QAED,IAAI,QAAyC,CAAC;QAC9C,IAAI;YACA,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,MAAM,EAAE,mBAAmB,EAAE,EAAE,CAAC,CAAC;YAChF,QAAQ,CAAC,SAAS,GAAG,QAAQ,CAAC,SAAS,IAAI,SAAS,CAAC;SACxD;QAAC,OAAO,CAAC,EAAE;YACR,IAAI,CAAC,YAAY,yBAAW,IAAI,CAAC,CAAC,OAAO,KAAK,aAAa,EAAE;gBACzD,QAAQ,GAAG,EAAE,CAAC;aACjB;iBAAM;gBACH,OAAO,CAAC,uCAAuC;aAClD;SACJ;QAED,8CAA8C;QAC9C,IAAI,iBAAyB,CAAC;QAC9B,IAAI;YACA,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,MAAM,EAAE,2BAA2B,EAAE,EAAE,CAAC,CAAC;YACxF,iBAAiB,GAAG,EAAE,CAAC,kBAAkB,CAAC;SAC7C;QAAC,OAAO,CAAC,EAAE;YACR,yDAAyD;SAC5D;QAED,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,MAAM,EAAE;YAC5C,GAAG,QAAQ;YACX,iBAAiB;SACpB,CAAC,CAAC;IACP,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,mBAAmB,CAAC,MAAc;QAC3C,IAAI,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC3D,IAAI,CAAC,MAAM,EAAE;YACT,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;YAClC,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;SAC1D;QACD,IAAI,CAAC,MAAM,EAAE;YACT,OAAO,EAAE,CAAC;SACb;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AAnFD,kCAmFC"}

View File

@@ -0,0 +1,40 @@
import { OlmMachine } from "@matrix-org/matrix-sdk-crypto-nodejs";
import * as AsyncLock from "async-lock";
import { MatrixClient } from "../MatrixClient";
import { ICryptoRoomInformation } from "./ICryptoRoomInformation";
import { IKeyBackupInfoRetrieved, IMegolmSessionDataExport } from "../models/KeyBackup";
/**
* @internal
*/
export declare const SYNC_LOCK_NAME = "sync";
/**
* @internal
*/
export declare class RustEngine {
readonly machine: OlmMachine;
private client;
readonly lock: AsyncLock;
readonly trackedUsersToAdd: Set<string>;
addTrackedUsersPromise: Promise<void> | undefined;
private keyBackupVersion;
private keyBackupWaiter;
private backupEnabled;
isBackupEnabled(): boolean;
constructor(machine: OlmMachine, client: MatrixClient);
run(): Promise<void>;
private runOnly;
addTrackedUsers(userIds: string[]): Promise<void>;
prepareEncrypt(roomId: string, roomInfo: ICryptoRoomInformation): Promise<void>;
enableKeyBackup(info: IKeyBackupInfoRetrieved): Promise<void>;
disableKeyBackup(): Promise<void>;
private actuallyDisableKeyBackup;
backupRoomKeys(): Promise<void>;
exportRoomKeysForSession(roomId: string, sessionId: string): Promise<IMegolmSessionDataExport[]>;
private backupRoomKeysIfEnabled;
private actuallyBackupRoomKeys;
private processKeysClaimRequest;
private processKeysUploadRequest;
private processKeysQueryRequest;
private processToDeviceRequest;
private processKeysBackupRequest;
}

View File

@@ -0,0 +1,242 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RustEngine = exports.SYNC_LOCK_NAME = void 0;
const matrix_sdk_crypto_nodejs_1 = require("@matrix-org/matrix-sdk-crypto-nodejs");
const AsyncLock = require("async-lock");
const LogService_1 = require("../logging/LogService");
const Crypto_1 = require("../models/Crypto");
const EncryptionEvent_1 = require("../models/events/EncryptionEvent");
const KeyBackup_1 = require("../models/KeyBackup");
/**
* @internal
*/
exports.SYNC_LOCK_NAME = "sync";
/**
* @internal
*/
class RustEngine {
machine;
client;
lock = new AsyncLock();
trackedUsersToAdd = new Set();
addTrackedUsersPromise;
keyBackupVersion;
keyBackupWaiter = Promise.resolve();
backupEnabled = false;
isBackupEnabled() {
return this.backupEnabled;
}
constructor(machine, client) {
this.machine = machine;
this.client = client;
}
async run() {
await this.runOnly(); // run everything, but with syntactic sugar
}
async runOnly(...types) {
// Note: we should not be running this until it runs out, so cache the value into a variable
const requests = await this.machine.outgoingRequests();
for (const request of requests) {
if (types.length && !types.includes(request.type))
continue;
switch (request.type) {
case 0 /* RequestType.KeysUpload */:
await this.processKeysUploadRequest(request);
break;
case 1 /* RequestType.KeysQuery */:
await this.processKeysQueryRequest(request);
break;
case 2 /* RequestType.KeysClaim */:
await this.processKeysClaimRequest(request);
break;
case 3 /* RequestType.ToDevice */:
await this.processToDeviceRequest(request);
break;
case 5 /* RequestType.RoomMessage */:
throw new Error("Bindings error: Sending room messages is not supported");
case 4 /* RequestType.SignatureUpload */:
throw new Error("Bindings error: Backup feature not possible");
case 6 /* RequestType.KeysBackup */:
await this.processKeysBackupRequest(request);
break;
default:
throw new Error("Bindings error: Unrecognized request type: " + request.type);
}
}
}
async addTrackedUsers(userIds) {
// Add the new set of users to the pool
userIds.forEach(uId => this.trackedUsersToAdd.add(uId));
if (this.addTrackedUsersPromise) {
// If we have a pending promise, don't create another lock requirement.
return;
}
return this.addTrackedUsersPromise = this.lock.acquire(exports.SYNC_LOCK_NAME, async () => {
// Immediately clear this promise so that a new promise is queued up.
this.addTrackedUsersPromise = undefined;
const uids = new Array(this.trackedUsersToAdd.size);
let idx = 0;
for (const u of this.trackedUsersToAdd.values()) {
uids[idx++] = new matrix_sdk_crypto_nodejs_1.UserId(u);
}
// Clear the existing pool
this.trackedUsersToAdd.clear();
await this.machine.updateTrackedUsers(uids);
const keysClaim = await this.machine.getMissingSessions(uids);
if (keysClaim) {
await this.processKeysClaimRequest(keysClaim);
}
});
}
async prepareEncrypt(roomId, roomInfo) {
let memberships = ["join", "invite"];
let historyVis = 1 /* HistoryVisibility.Joined */;
switch (roomInfo.historyVisibility) {
case "world_readable":
historyVis = 3 /* HistoryVisibility.WorldReadable */;
break;
case "invited":
historyVis = 0 /* HistoryVisibility.Invited */;
break;
case "shared":
historyVis = 2 /* HistoryVisibility.Shared */;
break;
case "joined":
default:
memberships = ["join"];
}
const members = new Set();
for (const membership of memberships) {
try {
(await this.client.getRoomMembersByMembership(roomId, membership))
.map(u => new matrix_sdk_crypto_nodejs_1.UserId(u.membershipFor))
.forEach(u => void members.add(u));
}
catch (err) {
LogService_1.LogService.warn("RustEngine", `Failed to get room members for membership type "${membership}" in ${roomId}`, (0, LogService_1.extractRequestError)(err));
}
}
if (members.size === 0) {
return;
}
const membersArray = Array.from(members);
const encEv = new EncryptionEvent_1.EncryptionEvent({
type: "m.room.encryption",
content: roomInfo,
});
const settings = new matrix_sdk_crypto_nodejs_1.EncryptionSettings();
settings.algorithm = roomInfo.algorithm === Crypto_1.EncryptionAlgorithm.MegolmV1AesSha2
? 1 /* RustEncryptionAlgorithm.MegolmV1AesSha2 */
: undefined;
settings.historyVisibility = historyVis;
settings.rotationPeriod = BigInt(encEv.rotationPeriodMs);
settings.rotationPeriodMessages = BigInt(encEv.rotationPeriodMessages);
await this.lock.acquire(exports.SYNC_LOCK_NAME, async () => {
await this.machine.updateTrackedUsers(membersArray); // just in case we missed some
await this.runOnly(1 /* RequestType.KeysQuery */);
const keysClaim = await this.machine.getMissingSessions(membersArray);
if (keysClaim) {
await this.processKeysClaimRequest(keysClaim);
}
});
await this.lock.acquire(roomId, async () => {
const requests = await this.machine.shareRoomKey(new matrix_sdk_crypto_nodejs_1.RoomId(roomId), membersArray, settings);
for (const req of requests) {
await this.processToDeviceRequest(req);
}
// Back up keys asynchronously
void this.backupRoomKeysIfEnabled();
});
}
enableKeyBackup(info) {
this.keyBackupWaiter = this.keyBackupWaiter.then(async () => {
if (this.backupEnabled) {
// Finish any pending backups before changing the backup version/pubkey
await this.actuallyDisableKeyBackup();
}
let publicKey;
switch (info.algorithm) {
case KeyBackup_1.KeyBackupEncryptionAlgorithm.MegolmBackupV1Curve25519AesSha2:
publicKey = info.auth_data.public_key;
break;
default:
throw new Error("Key backup error: cannot enable backups with unsupported backup algorithm " + info.algorithm);
}
await this.machine.enableBackupV1(publicKey, info.version);
this.keyBackupVersion = info.version;
this.backupEnabled = true;
});
return this.keyBackupWaiter;
}
disableKeyBackup() {
this.keyBackupWaiter = this.keyBackupWaiter.then(async () => {
await this.actuallyDisableKeyBackup();
});
return this.keyBackupWaiter;
}
async actuallyDisableKeyBackup() {
await this.machine.disableBackup();
this.keyBackupVersion = undefined;
this.backupEnabled = false;
}
backupRoomKeys() {
this.keyBackupWaiter = this.keyBackupWaiter.then(async () => {
if (!this.backupEnabled) {
throw new Error("Key backup error: attempted to create a backup before having enabled backups");
}
await this.actuallyBackupRoomKeys();
});
return this.keyBackupWaiter;
}
async exportRoomKeysForSession(roomId, sessionId) {
return JSON.parse(await this.machine.exportRoomKeysForSession(roomId, sessionId));
}
backupRoomKeysIfEnabled() {
this.keyBackupWaiter = this.keyBackupWaiter.then(async () => {
if (this.backupEnabled) {
await this.actuallyBackupRoomKeys();
}
});
return this.keyBackupWaiter;
}
async actuallyBackupRoomKeys() {
const request = await this.machine.backupRoomKeys();
if (request) {
await this.processKeysBackupRequest(request);
}
}
async processKeysClaimRequest(request) {
const resp = await this.client.doRequest("POST", "/_matrix/client/v3/keys/claim", null, JSON.parse(request.body));
await this.machine.markRequestAsSent(request.id, request.type, JSON.stringify(resp));
}
async processKeysUploadRequest(request) {
const body = JSON.parse(request.body);
// delete body["one_time_keys"]; // use this to test MSC3983
const resp = await this.client.doRequest("POST", "/_matrix/client/v3/keys/upload", null, body);
await this.machine.markRequestAsSent(request.id, request.type, JSON.stringify(resp));
}
async processKeysQueryRequest(request) {
const resp = await this.client.doRequest("POST", "/_matrix/client/v3/keys/query", null, JSON.parse(request.body));
await this.machine.markRequestAsSent(request.id, request.type, JSON.stringify(resp));
}
async processToDeviceRequest(request) {
const resp = await this.client.sendToDevices(request.eventType, JSON.parse(request.body).messages);
await this.machine.markRequestAsSent(request.txnId, 3 /* RequestType.ToDevice */, JSON.stringify(resp));
}
async processKeysBackupRequest(request) {
let resp;
try {
if (!this.keyBackupVersion) {
throw new Error("Key backup version missing");
}
resp = await this.client.doRequest("PUT", "/_matrix/client/v3/room_keys/keys", { version: this.keyBackupVersion }, JSON.parse(request.body));
}
catch (e) {
this.client.emit("crypto.failed_backup", e);
return;
}
await this.machine.markRequestAsSent(request.id, request.type, JSON.stringify(resp));
}
}
exports.RustEngine = RustEngine;
//# sourceMappingURL=RustEngine.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
/**
* Flags a MatrixClient function as needing end-to-end encryption enabled.
* @category Encryption
*/
export declare function requiresCrypto(): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
/**
* Flags a CryptoClient function as needing the CryptoClient to be ready.
* @category Encryption
*/
export declare function requiresReady(): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;

View File

@@ -0,0 +1,38 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requiresReady = exports.requiresCrypto = void 0;
/**
* Flags a MatrixClient function as needing end-to-end encryption enabled.
* @category Encryption
*/
function requiresCrypto() {
return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
const client = this; // eslint-disable-line @typescript-eslint/no-this-alias
if (!client.crypto) {
throw new Error("End-to-end encryption is not enabled");
}
return originalMethod.apply(this, args);
};
};
}
exports.requiresCrypto = requiresCrypto;
/**
* Flags a CryptoClient function as needing the CryptoClient to be ready.
* @category Encryption
*/
function requiresReady() {
return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
const crypto = this; // eslint-disable-line @typescript-eslint/no-this-alias
if (!crypto.isReady) {
throw new Error("End-to-end encryption has not initialized");
}
return originalMethod.apply(this, args);
};
};
}
exports.requiresReady = requiresReady;
//# sourceMappingURL=decorators.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"decorators.js","sourceRoot":"","sources":["../../src/e2ee/decorators.ts"],"names":[],"mappings":";;;AAGA;;;GAGG;AACH,SAAgB,cAAc;IAC1B,OAAO,UAAS,MAAW,EAAE,WAAmB,EAAE,UAA8B;QAC5E,MAAM,cAAc,GAAG,UAAU,CAAC,KAAK,CAAC;QACxC,UAAU,CAAC,KAAK,GAAG,UAAS,GAAG,IAAW;YACtC,MAAM,MAAM,GAAiB,IAAI,CAAC,CAAC,uDAAuD;YAC1F,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;gBAChB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;aAC3D;YAED,OAAO,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5C,CAAC,CAAC;IACN,CAAC,CAAC;AACN,CAAC;AAZD,wCAYC;AAED;;;GAGG;AACH,SAAgB,aAAa;IACzB,OAAO,UAAS,MAAW,EAAE,WAAmB,EAAE,UAA8B;QAC5E,MAAM,cAAc,GAAG,UAAU,CAAC,KAAK,CAAC;QACxC,UAAU,CAAC,KAAK,GAAG,UAAS,GAAG,IAAW;YACtC,MAAM,MAAM,GAAiB,IAAI,CAAC,CAAC,uDAAuD;YAC1F,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE;gBACjB,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;aAChE;YAED,OAAO,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5C,CAAC,CAAC;IACN,CAAC,CAAC;AACN,CAAC;AAZD,sCAYC"}