1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-12-16 09:42:23 +03:00

Implement experimental encrypted state events. (#4994)

* feat: Implement experimental encrypted state events.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* fix: Add cast from StateEvents[K] to IContent.

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
This commit is contained in:
Skye Elliot
2025-09-24 12:44:17 +01:00
committed by GitHub
parent dbe441de33
commit a08a2737e1
17 changed files with 594 additions and 144 deletions

View File

@@ -425,6 +425,11 @@ export interface ICreateClientOpts {
*/
cryptoCallbacks?: CryptoCallbacks;
/**
* Enable encrypted state events.
*/
enableEncryptedStateEvents?: boolean;
/**
* Method to generate room names for empty rooms and rooms names based on membership.
* Defaults to a built-in English handler with basic pluralisation.
@@ -1205,6 +1210,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public http: MatrixHttpApi<IHttpOpts & { onlyData: true }>; // XXX: Intended private, used in code.
private cryptoBackend?: CryptoBackend; // one of crypto or rustCrypto
private readonly enableEncryptedStateEvents: boolean;
public cryptoCallbacks: CryptoCallbacks; // XXX: Intended private, used in code.
public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code.
public groupCallEventHandler?: GroupCallEventHandler;
@@ -1363,6 +1369,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.legacyCryptoStore = opts.cryptoStore;
this.verificationMethods = opts.verificationMethods;
this.cryptoCallbacks = opts.cryptoCallbacks || {};
this.enableEncryptedStateEvents = opts.enableEncryptedStateEvents ?? false;
this.forceTURN = opts.forceTURN || false;
this.iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize;
@@ -1979,6 +1986,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
legacyMigrationProgressListener: (progress: number, total: number): void => {
this.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, progress, total);
},
enableEncryptedStateEvents: this.enableEncryptedStateEvents,
});
rustCrypto.setSupportedVerificationMethods(this.verificationMethods);
@@ -6034,6 +6043,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns A decryption promise
*/
public decryptEventIfNeeded(event: MatrixEvent, options?: IDecryptOptions): Promise<void> {
if (event.isState() && !this.enableEncryptedStateEvents) {
return Promise.resolve();
}
if (event.shouldAttemptDecryption() && this.getCrypto()) {
event.attemptDecryption(this.cryptoBackend!, options);
}
@@ -6618,23 +6631,83 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: TODO
* @returns Rejects: with an error response.
*/
public sendStateEvent<K extends keyof StateEvents>(
public async sendStateEvent<K extends keyof StateEvents>(
roomId: string,
eventType: K,
content: StateEvents[K],
stateKey = "",
opts: IRequestOpts = {},
): Promise<ISendEventResponse> {
const room = this.getRoom(roomId);
const event = new MatrixEvent({
room_id: roomId,
type: eventType,
state_key: stateKey,
// Cast safety: StateEvents[K] is a stronger bound than IContent, which has [key: string]: any
content: content as IContent,
});
await this.encryptStateEventIfNeeded(event, room ?? undefined);
const pathParams = {
$roomId: roomId,
$eventType: eventType,
$stateKey: stateKey,
$eventType: event.getWireType(),
$stateKey: event.getWireStateKey(),
};
let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
if (stateKey !== undefined) {
path = utils.encodeUri(path + "/$stateKey", pathParams);
}
return this.http.authedRequest(Method.Put, path, undefined, content as Body, opts);
return this.http.authedRequest(Method.Put, path, undefined, event.getWireContent(), opts);
}
private async encryptStateEventIfNeeded(event: MatrixEvent, room?: Room): Promise<void> {
if (!this.enableEncryptedStateEvents) {
return;
}
// If the room is unknown, we cannot encrypt for it
if (!room) return;
if (!this.cryptoBackend && this.usingExternalCrypto) {
// The client has opted to allow sending messages to encrypted
// rooms even if the room is encrypted, and we haven't set up
// crypto. This is useful for users of matrix-org/pantalaimon
return;
}
if (!this.cryptoBackend) {
throw new Error("This room is configured to use encryption, but your client does not support encryption.");
}
// Check regular encryption conditions.
if (!(await this.shouldEncryptEventForRoom(event, room))) {
return;
}
// If the crypto impl thinks we shouldn't encrypt, then we shouldn't.
// Safety: we checked the crypto impl exists above.
if (!(await this.cryptoBackend!.isStateEncryptionEnabledInRoom(room.roomId))) {
return;
}
// Check if the event is excluded under MSC3414
if (
[
"m.room.create",
"m.room.member",
"m.room.join_rules",
"m.room.power_levels",
"m.room.third_party_invite",
"m.room.history_visibility",
"m.room.guest_access",
"m.room.encryption",
].includes(event.getType())
) {
return;
}
await this.cryptoBackend.encryptEvent(event, room);
}
/**