@vector-im/matrix-bot-sdk
Version:
TypeScript/JavaScript SDK for Matrix bots and appservices
276 lines (246 loc) • 11.3 kB
text/typescript
import {
EncryptionSettings,
KeysClaimRequest,
OlmMachine,
RequestType,
RoomId,
UserId,
EncryptionAlgorithm as RustEncryptionAlgorithm,
HistoryVisibility,
KeysUploadRequest,
KeysQueryRequest,
ToDeviceRequest,
KeysBackupRequest,
} from "@matrix-org/matrix-sdk-crypto-nodejs";
import * as AsyncLock from "async-lock";
import { MatrixClient } from "../MatrixClient";
import { extractRequestError, LogService } from "../logging/LogService";
import { ICryptoRoomInformation } from "./ICryptoRoomInformation";
import { EncryptionAlgorithm } from "../models/Crypto";
import { EncryptionEvent } from "../models/events/EncryptionEvent";
import { ICurve25519AuthData, IKeyBackupInfoRetrieved, IMegolmSessionDataExport, KeyBackupEncryptionAlgorithm, KeyBackupVersion } from "../models/KeyBackup";
import { Membership } from "../models/events/MembershipEvent";
/**
* @internal
*/
export const SYNC_LOCK_NAME = "sync";
/**
* @internal
*/
export class RustEngine {
public readonly lock = new AsyncLock();
public readonly trackedUsersToAdd = new Set<string>();
public addTrackedUsersPromise: Promise<void>|undefined;
private keyBackupVersion: KeyBackupVersion|undefined;
private keyBackupWaiter = Promise.resolve();
private backupEnabled = false;
public isBackupEnabled() {
return this.backupEnabled;
}
public constructor(public readonly machine: OlmMachine, private client: MatrixClient) {
}
public async run() {
await this.runOnly(); // run everything, but with syntactic sugar
}
private async runOnly(...types: RequestType[]) {
// 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 RequestType.KeysUpload:
await this.processKeysUploadRequest(request);
break;
case RequestType.KeysQuery:
await this.processKeysQueryRequest(request);
break;
case RequestType.KeysClaim:
await this.processKeysClaimRequest(request);
break;
case RequestType.ToDevice:
await this.processToDeviceRequest(request as ToDeviceRequest);
break;
case RequestType.RoomMessage:
throw new Error("Bindings error: Sending room messages is not supported");
case RequestType.SignatureUpload:
throw new Error("Bindings error: Backup feature not possible");
case RequestType.KeysBackup:
await this.processKeysBackupRequest(request);
break;
default:
throw new Error("Bindings error: Unrecognized request type: " + request.type);
}
}
}
public async addTrackedUsers(userIds: string[]) {
// 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(SYNC_LOCK_NAME, async () => {
// Immediately clear this promise so that a new promise is queued up.
this.addTrackedUsersPromise = undefined;
const uids = new Array<UserId>(this.trackedUsersToAdd.size);
let idx = 0;
for (const u of this.trackedUsersToAdd.values()) {
uids[idx++] = new 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);
}
});
}
public async prepareEncrypt(roomId: string, roomInfo: ICryptoRoomInformation) {
let memberships: Membership[] = ["join", "invite"];
let historyVis = HistoryVisibility.Joined;
switch (roomInfo.historyVisibility) {
case "world_readable":
historyVis = HistoryVisibility.WorldReadable;
break;
case "invited":
historyVis = HistoryVisibility.Invited;
break;
case "shared":
historyVis = HistoryVisibility.Shared;
break;
case "joined":
default:
memberships = ["join"];
}
const members = new Set<UserId>();
for (const membership of memberships) {
try {
(await this.client.getRoomMembersByMembership(roomId, membership))
.map(u => new UserId(u.membershipFor))
.forEach(u => void members.add(u));
} catch (err) {
LogService.warn("RustEngine", `Failed to get room members for membership type "${membership}" in ${roomId}`, extractRequestError(err));
}
}
if (members.size === 0) {
return;
}
const membersArray = Array.from(members);
const encEv = new EncryptionEvent({
type: "m.room.encryption",
content: roomInfo,
});
const settings = new EncryptionSettings();
settings.algorithm = roomInfo.algorithm === EncryptionAlgorithm.MegolmV1AesSha2
? RustEncryptionAlgorithm.MegolmV1AesSha2
: undefined;
settings.historyVisibility = historyVis;
settings.rotationPeriod = BigInt(encEv.rotationPeriodMs);
settings.rotationPeriodMessages = BigInt(encEv.rotationPeriodMessages);
await this.lock.acquire(SYNC_LOCK_NAME, async () => {
await this.machine.updateTrackedUsers(membersArray); // just in case we missed some
await this.runOnly(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 RoomId(roomId), membersArray, settings);
for (const req of requests) {
await this.processToDeviceRequest(req);
}
// Back up keys asynchronously
void this.backupRoomKeysIfEnabled();
});
}
public enableKeyBackup(info: IKeyBackupInfoRetrieved): Promise<void> {
this.keyBackupWaiter = this.keyBackupWaiter.then(async () => {
if (this.backupEnabled) {
// Finish any pending backups before changing the backup version/pubkey
await this.actuallyDisableKeyBackup();
}
let publicKey: string;
switch (info.algorithm) {
case KeyBackupEncryptionAlgorithm.MegolmBackupV1Curve25519AesSha2:
publicKey = (info.auth_data as ICurve25519AuthData).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;
}
public disableKeyBackup(): Promise<void> {
this.keyBackupWaiter = this.keyBackupWaiter.then(async () => {
await this.actuallyDisableKeyBackup();
});
return this.keyBackupWaiter;
}
private async actuallyDisableKeyBackup(): Promise<void> {
await this.machine.disableBackup();
this.keyBackupVersion = undefined;
this.backupEnabled = false;
}
public backupRoomKeys(): Promise<void> {
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;
}
public async exportRoomKeysForSession(roomId: string, sessionId: string): Promise<IMegolmSessionDataExport[]> {
return JSON.parse(await this.machine.exportRoomKeysForSession(roomId, sessionId)) as IMegolmSessionDataExport[];
}
private backupRoomKeysIfEnabled(): Promise<void> {
this.keyBackupWaiter = this.keyBackupWaiter.then(async () => {
if (this.backupEnabled) {
await this.actuallyBackupRoomKeys();
}
});
return this.keyBackupWaiter;
}
private async actuallyBackupRoomKeys(): Promise<void> {
const request = await this.machine.backupRoomKeys();
if (request) {
await this.processKeysBackupRequest(request);
}
}
private async processKeysClaimRequest(request: KeysClaimRequest) {
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));
}
private async processKeysUploadRequest(request: KeysUploadRequest) {
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));
}
private async processKeysQueryRequest(request: KeysQueryRequest) {
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));
}
private async processToDeviceRequest(request: ToDeviceRequest) {
const resp = await this.client.sendToDevices(request.eventType, JSON.parse(request.body).messages);
await this.machine.markRequestAsSent(request.txnId, RequestType.ToDevice, JSON.stringify(resp));
}
private async processKeysBackupRequest(request: KeysBackupRequest) {
let resp: Awaited<ReturnType<MatrixClient["doRequest"]>>;
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));
}
}