feat: complete Ancestor quote bot with production-ready Docker support

This commit is contained in:
unfunny
2026-09-13 14:14:38 -04:00
parent f2016da05a
commit fe7351a7dc
1145 changed files with 30890 additions and 97684 deletions

View File

@@ -1,4 +1,14 @@
import _defineProperty from "@babel/runtime/helpers/defineProperty";
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.RoomEncryptor = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _matrixSdkCryptoWasm = require("@matrix-org/matrix-sdk-crypto-wasm");
var _event = require("../@types/event");
var _logger = require("../logger");
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
@@ -15,52 +25,26 @@ 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, RoomId, UserId } from "@matrix-org/matrix-sdk-crypto-wasm";
import { EventType } from "../@types/event.js";
import { LogSpan } from "../logger.js";
import { HistoryVisibility } from "../@types/partials.js";
import { logDuration } from "../utils.js";
import { KnownMembership } from "../@types/membership.js";
import { DeviceIsolationModeKind } from "../crypto-api/index.js";
/**
* RoomEncryptor: responsible for encrypting messages to a given room
*
* @internal
*/
export class RoomEncryptor {
class RoomEncryptor {
/**
* @param prefixedLogger - A logger to use for log messages.
* @param olmMachine - The rust-sdk's OlmMachine
* @param keyClaimManager - Our KeyClaimManager, which manages the queue of one-time-key claim requests
* @param outgoingRequestManager - The OutgoingRequestManager, which manages the queue of outgoing requests.
* @param room - The room we want to encrypt for
* @param encryptionSettings - body of the m.room.encryption event currently in force in this room
*/
constructor(prefixedLogger, olmMachine, keyClaimManager, outgoingRequestManager, room, encryptionSettings) {
/** whether the room members have been loaded and tracked for the first time */
_defineProperty(this, "lazyLoadedMembersResolved", false);
/**
* Ensures that there is only one encryption operation at a time for that room.
*
* An encryption operation is either a {@link prepareForEncryption} or an {@link encryptEvent} call.
*/
_defineProperty(this, "currentEncryptionPromise", Promise.resolve());
this.prefixedLogger = prefixedLogger;
constructor(olmMachine, keyClaimManager, outgoingRequestProcessor, room, encryptionSettings) {
this.olmMachine = olmMachine;
this.keyClaimManager = keyClaimManager;
this.outgoingRequestManager = outgoingRequestManager;
this.outgoingRequestProcessor = outgoingRequestProcessor;
this.room = room;
this.encryptionSettings = encryptionSettings;
// start tracking devices for any users already known to be in this room.
// Do not load members here, would defeat lazy loading.
const members = room.getJoinedMembers();
// At this point just mark the known members as tracked, it might not be the full list of members
// because of lazy loading. This is fine, because we will get a member list update when sending a message for
// the first time, see `RoomEncryptor#ensureEncryptionSession`
this.olmMachine.updateTrackedUsers(members.map(u => new RustSdkCryptoJs.UserId(u.userId))).catch(e => this.prefixedLogger.error("Error initializing tracked users", e));
(0, _defineProperty2.default)(this, "prefixedLogger", void 0);
this.prefixedLogger = _logger.logger.withPrefix(`[${room.roomId} encryption]`);
}
/**
@@ -70,8 +54,7 @@ export class RoomEncryptor {
*/
onCryptoEvent(config) {
if (JSON.stringify(this.encryptionSettings) != JSON.stringify(config)) {
// This should currently be unreachable, since the Rust SDK will reject any attempts to change config.
throw new Error("Cannot reconfigure an active RoomEncryptor");
this.prefixedLogger.error(`Ignoring m.room.encryption event which requests a change of config`);
}
}
@@ -81,11 +64,11 @@ export class RoomEncryptor {
* @param member - new membership state
*/
onRoomMembership(member) {
if (member.membership == KnownMembership.Join || member.membership == KnownMembership.Invite && this.room.shouldEncryptForInvitedMembers()) {
this.prefixedLogger.debug(`${member.membership} event for ${member.userId}`);
if (member.membership == "join" || member.membership == "invite" && this.room.shouldEncryptForInvitedMembers()) {
// make sure we are tracking the deviceList for this user
this.olmMachine.updateTrackedUsers([new UserId(member.userId)]).catch(e => {
this.prefixedLogger.error("Unable to update tracked users", e);
});
this.prefixedLogger.debug(`starting to track devices for: ${member.userId}`);
this.olmMachine.updateTrackedUsers([new _matrixSdkCryptoWasm.UserId(member.userId)]);
}
// TODO: handle leaves (including our own)
@@ -96,197 +79,50 @@ export class RoomEncryptor {
*
* This ensures that we have a megolm session ready to use and that we have shared its key with all the devices
* in the room.
* @param globalBlacklistUnverifiedDevices - When `true`, and `deviceIsolationMode` is `AllDevicesIsolationMode`,
* will not send encrypted messages to unverified devices.
* Ignored when `deviceIsolationMode` is `OnlySignedDevicesIsolationMode`.
* @param deviceIsolationMode - The device isolation mode. See {@link DeviceIsolationMode}.
*/
async prepareForEncryption(globalBlacklistUnverifiedDevices, deviceIsolationMode) {
// We consider a prepareForEncryption as an encryption promise as it will potentially share keys
// even if it doesn't send an event.
// Usually this is called when the user starts typing, so we want to make sure we have keys ready when the
// message is finally sent.
// If `encryptEvent` is invoked before `prepareForEncryption` has completed, the `encryptEvent` call will wait for
// `prepareForEncryption` to complete before executing.
// The part where `encryptEvent` shares the room key will then usually be a no-op as it was already performed by `prepareForEncryption`.
await this.encryptEvent(null, globalBlacklistUnverifiedDevices, deviceIsolationMode);
}
/**
* Encrypt an event for this room, or prepare for encryption.
*
* This will ensure that we have a megolm session for this room, share it with the devices in the room, and
* then, if an event is provided, encrypt it using the session.
*
* @param event - Event to be encrypted, or null if only preparing for encryption (in which case we will pre-share the room key).
* @param globalBlacklistUnverifiedDevices - When `true`, and `deviceIsolationMode` is `AllDevicesIsolationMode`,
* will not send encrypted messages to unverified devices.
* Ignored when `deviceIsolationMode` is `OnlySignedDevicesIsolationMode`.
* @param deviceIsolationMode - The device isolation mode. See {@link DeviceIsolationMode}.
*/
encryptEvent(event, globalBlacklistUnverifiedDevices, deviceIsolationMode) {
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.catch(() => {
// Any errors in the previous call will have been reported already, so there is nothing to do here.
// we just throw away the error and start anew.
}).then(async () => {
await logDuration(logger, "ensureEncryptionSession", async () => {
await this.ensureEncryptionSession(logger, globalBlacklistUnverifiedDevices, deviceIsolationMode);
});
if (event) {
await logDuration(logger, "encryptEventInner", async () => {
await this.encryptEventInner(logger, event);
});
}
});
this.currentEncryptionPromise = prom;
return prom;
}
/**
* Prepare to encrypt events in this room.
*
* This ensures that we have a megolm session ready to use and that we have shared its key with all the devices
* in the room.
*
* @param logger - a place to write diagnostics to
* @param globalBlacklistUnverifiedDevices - When `true`, and `deviceIsolationMode` is `AllDevicesIsolationMode`,
* will not send encrypted messages to unverified devices.
* Ignored when `deviceIsolationMode` is `OnlySignedDevicesIsolationMode`.
* @param deviceIsolationMode - The device isolation mode. See {@link DeviceIsolationMode}.
*/
async ensureEncryptionSession(logger, globalBlacklistUnverifiedDevices, deviceIsolationMode) {
async ensureEncryptionSession() {
if (this.encryptionSettings.algorithm !== "m.megolm.v1.aes-sha2") {
throw new Error(`Cannot encrypt in ${this.room.roomId} for unsupported algorithm '${this.encryptionSettings.algorithm}'`);
}
logger.debug("Starting encryption");
const members = await this.room.getEncryptionTargetMembers();
this.prefixedLogger.debug(`Encrypting for users (shouldEncryptForInvitedMembers: ${this.room.shouldEncryptForInvitedMembers()}):`, members.map(u => `${u.userId} (${u.membership})`));
const userList = members.map(u => new _matrixSdkCryptoWasm.UserId(u.userId));
await this.keyClaimManager.ensureSessionsForUsers(userList);
this.prefixedLogger.debug("Sessions for users are ready; now sharing room key");
const rustEncryptionSettings = new _matrixSdkCryptoWasm.EncryptionSettings();
/* FIXME historyVisibility, rotation, etc */
// If this is the first time we are sending a message to the room, we may not yet have seen all the members
// (so the Crypto SDK might not have a device list for them). So, if this is the first time we are encrypting
// for this room, give the SDK the full list of members, to be on the safe side.
//
// This could end up being racy (if two calls to ensureEncryptionSession happen at the same time), but that's
// not a particular problem, since `OlmMachine.updateTrackedUsers` just adds any users that weren't already tracked.
if (!this.lazyLoadedMembersResolved) {
await logDuration(logger, "loadMembersIfNeeded: updateTrackedUsers", async () => {
await this.olmMachine.updateTrackedUsers(members.map(u => new RustSdkCryptoJs.UserId(u.userId)));
});
logger.debug(`Updated tracked users`);
this.lazyLoadedMembersResolved = true;
// Query keys in case we don't have them for newly tracked members.
// It's important after loading members for the first time, as likely most of them won't be
// known yet and will be unable to decrypt messages despite being in the room for long.
// This must be done before ensuring sessions. If not the devices of these users are not
// known yet and will not get the room key.
// We don't have API to only get the keys queries related to this member list, so we just
// process the pending requests from the olmMachine. (usually these are processed
// at the end of the sync, but we can't wait for that).
// XXX future improvement process only KeysQueryRequests for the users that have never been queried.
logger.debug(`Processing outgoing requests`);
await logDuration(logger, "doProcessOutgoingRequests", async () => {
await this.outgoingRequestManager.doProcessOutgoingRequests();
});
} else {
// If members are already loaded it's less critical to await on key queries.
// We might still want to trigger a processOutgoingRequests here.
// The call to `ensureSessionsForUsers` below will wait a bit on in-flight key queries we are
// interested in. If a sync handling happens in the meantime, and some new members are added to the room
// or have new devices it would give us a chance to query them before sending.
// It's less critical due to the racy nature of this process.
logger.debug(`Processing outgoing requests in background`);
this.outgoingRequestManager.doProcessOutgoingRequests();
}
logger.debug(`Encrypting for users (shouldEncryptForInvitedMembers: ${this.room.shouldEncryptForInvitedMembers()}):`, members.map(u => `${u.userId} (${u.membership})`));
const userList = members.map(u => new UserId(u.userId));
await logDuration(logger, "ensureSessionsForUsers", async () => {
await this.keyClaimManager.ensureSessionsForUsers(logger, userList);
});
const rustEncryptionSettings = new EncryptionSettings();
rustEncryptionSettings.historyVisibility = toRustHistoryVisibility(this.room.getHistoryVisibility());
// We only support megolm
rustEncryptionSettings.algorithm = EncryptionAlgorithm.MegolmV1AesSha2;
// We need to convert the rotation period from milliseconds to microseconds
// See https://spec.matrix.org/v1.8/client-server-api/#mroomencryption and
// https://matrix-org.github.io/matrix-rust-sdk-crypto-wasm/classes/EncryptionSettings.html#rotationPeriod
if (typeof this.encryptionSettings.rotation_period_ms === "number") {
rustEncryptionSettings.rotationPeriod = BigInt(this.encryptionSettings.rotation_period_ms * 1000);
}
if (typeof this.encryptionSettings.rotation_period_msgs === "number") {
rustEncryptionSettings.rotationPeriodMessages = BigInt(this.encryptionSettings.rotation_period_msgs);
}
switch (deviceIsolationMode.kind) {
case DeviceIsolationModeKind.AllDevicesIsolationMode:
{
// When this.room.getBlacklistUnverifiedDevices() === null, the global settings should be used
// See Room#getBlacklistUnverifiedDevices
const onlyAllowTrustedDevices = this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices;
rustEncryptionSettings.sharingStrategy = CollectStrategy.deviceBasedStrategy(onlyAllowTrustedDevices, deviceIsolationMode.errorOnVerifiedUserProblems);
}
break;
case DeviceIsolationModeKind.OnlySignedDevicesIsolationMode:
rustEncryptionSettings.sharingStrategy = CollectStrategy.identityBasedStrategy();
break;
}
await logDuration(logger, "shareRoomKey", async () => {
const shareMessages = await this.olmMachine.shareRoomKey(new RoomId(this.room.roomId),
// safe to pass without cloning, as it's not reused here (before or after)
userList, rustEncryptionSettings);
if (shareMessages) {
for (const m of shareMessages) {
await this.outgoingRequestManager.outgoingRequestProcessor.makeOutgoingRequest(m);
}
const shareMessages = await this.olmMachine.shareRoomKey(new _matrixSdkCryptoWasm.RoomId(this.room.roomId), userList, rustEncryptionSettings);
if (shareMessages) {
for (const m of shareMessages) {
await this.outgoingRequestProcessor.makeOutgoingRequest(m);
}
});
}
}
/**
* Discard any existing group session for this room
*/
async forceDiscardSession() {
const r = await this.olmMachine.invalidateGroupSession(new RoomId(this.room.roomId));
const r = await this.olmMachine.invalidateGroupSession(new _matrixSdkCryptoWasm.RoomId(this.room.roomId));
if (r) {
this.prefixedLogger.info("Discarded existing group session");
}
}
async encryptEventInner(logger, event) {
logger.debug("Encrypting actual message content");
const room = new RoomId(this.room.roomId);
const type = event.getType();
const content = JSON.stringify(event.getContent());
let encryptedContent;
if (event.isState()) {
encryptedContent = await this.olmMachine.encryptStateEvent(room, type,
// Safety: we've already checked above that this is a state event, so the state key must exist.
event.getStateKey(), content);
} else {
encryptedContent = await this.olmMachine.encryptRoomEvent(room, type, content);
}
event.makeEncrypted(EventType.RoomMessageEncrypted, JSON.parse(encryptedContent), this.olmMachine.identityKeys.curve25519.toBase64(), this.olmMachine.identityKeys.ed25519.toBase64());
logger.debug("Encrypted event successfully");
}
}
/**
* Convert a HistoryVisibility to a RustHistoryVisibility
* @param visibility - HistoryVisibility enum
* @returns a RustHistoryVisibility enum
*/
export function toRustHistoryVisibility(visibility) {
switch (visibility) {
case HistoryVisibility.Invited:
return RustHistoryVisibility.Invited;
case HistoryVisibility.Joined:
return RustHistoryVisibility.Joined;
case HistoryVisibility.Shared:
return RustHistoryVisibility.Shared;
case HistoryVisibility.WorldReadable:
return RustHistoryVisibility.WorldReadable;
/**
* Encrypt an event for this room
*
* This will ensure that we have a megolm session for this room, share it with the devices in the room, and
* then encrypt the event using the session.
*
* @param event - Event to be encrypted.
*/
async encryptEvent(event) {
await this.ensureEncryptionSession();
const encryptedContent = await this.olmMachine.encryptRoomEvent(new _matrixSdkCryptoWasm.RoomId(this.room.roomId), event.getType(), JSON.stringify(event.getContent()));
event.makeEncrypted(_event.EventType.RoomMessageEncrypted, JSON.parse(encryptedContent), this.olmMachine.identityKeys.curve25519.toBase64(), this.olmMachine.identityKeys.ed25519.toBase64());
}
}
exports.RoomEncryptor = RoomEncryptor;
//# sourceMappingURL=RoomEncryptor.js.map