273 lines
12 KiB
JavaScript
273 lines
12 KiB
JavaScript
"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 {
|
|
constructor(client) {
|
|
this.client = client;
|
|
this.ready = false;
|
|
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;
|
|
}
|
|
/**
|
|
* 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(roomIds) {
|
|
await this.roomTracker.prepare(roomIds);
|
|
if (this.ready)
|
|
return; // stop re-preparing here
|
|
const storedDeviceId = await this.client.cryptoStore.getDeviceId();
|
|
if (storedDeviceId) {
|
|
this.deviceId = storedDeviceId;
|
|
}
|
|
else {
|
|
const deviceId = (await this.client.getWhoAmI())['device_id'];
|
|
if (!deviceId) {
|
|
throw new Error("Encryption not possible: server not revealing device ID");
|
|
}
|
|
this.deviceId = deviceId;
|
|
await this.client.cryptoStore.setDeviceId(this.deviceId);
|
|
}
|
|
LogService_1.LogService.debug("CryptoClient", "Starting with device ID:", this.deviceId);
|
|
const machine = await matrix_sdk_crypto_nodejs_1.OlmMachine.initialize(new matrix_sdk_crypto_nodejs_1.UserId(await this.client.getUserId()), new matrix_sdk_crypto_nodejs_1.DeviceId(this.deviceId), this.storage.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();
|
|
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') {
|
|
const members = await this.client.getRoomMembers(roomId, null, ['join', 'invite']);
|
|
await this.engine.addTrackedUsers(members.map(e => e.membershipFor));
|
|
}
|
|
}
|
|
/**
|
|
* 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));
|
|
}
|
|
}
|
|
/**
|
|
* 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 = await this.engine.machine.receiveSyncChanges(deviceMessages, deviceLists, otkCounts, unusedFallbackKeyAlgs);
|
|
const decryptedToDeviceMessages = JSON.parse(syncResp);
|
|
if (Array.isArray(decryptedToDeviceMessages)) {
|
|
for (const msg of decryptedToDeviceMessages) {
|
|
this.client.emit("to_device.decrypted", msg);
|
|
}
|
|
}
|
|
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 = (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);
|
|
}
|
|
}
|
|
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);
|
|
//# sourceMappingURL=CryptoClient.js.map
|