1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Implement getEncryptionInfoForEvent and deprecate getEventEncryptionInfo (#3693)

* Implement `getEncryptionInfoForEvent` and deprecate `getEventEncryptionInfo`

* fix tsdoc

* fix tests

* Improve test coverage
This commit is contained in:
Richard van der Hoff
2023-09-07 10:39:10 +01:00
committed by GitHub
parent 0700e86f58
commit 7e691bf700
7 changed files with 277 additions and 3 deletions

View File

@@ -15,11 +15,16 @@ import { sleep } from "../../src/utils";
import { CRYPTO_ENABLED } from "../../src/client"; import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo"; import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from "../../src/logger"; import { logger } from "../../src/logger";
import { MemoryStore } from "../../src"; import { DeviceVerification, MemoryStore } from "../../src";
import { RoomKeyRequestState } from "../../src/crypto/OutgoingRoomKeyRequestManager"; import { RoomKeyRequestState } from "../../src/crypto/OutgoingRoomKeyRequestManager";
import { RoomMember } from "../../src/models/room-member"; import { RoomMember } from "../../src/models/room-member";
import { IStore } from "../../src/store"; import { IStore } from "../../src/store";
import { IRoomEncryption, RoomList } from "../../src/crypto/RoomList"; import { IRoomEncryption, RoomList } from "../../src/crypto/RoomList";
import { EventShieldColour, EventShieldReason } from "../../src/crypto-api";
import { UserTrustLevel } from "../../src/crypto/CrossSigning";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend";
import * as testData from "../test-utils/test-data";
const Olm = global.Olm; const Olm = global.Olm;
@@ -111,13 +116,14 @@ describe("Crypto", function () {
}); });
describe("encrypted events", function () { describe("encrypted events", function () {
it("provides encryption information", async function () { it("provides encryption information for events from unverified senders", async function () {
const client = new TestClient("@alice:example.com", "deviceid").client; const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto(); await client.initCrypto();
// unencrypted event // unencrypted event
const event = { const event = {
getId: () => "$event_id", getId: () => "$event_id",
getSender: () => "@bob:example.com",
getSenderKey: () => null, getSenderKey: () => null,
getWireContent: () => { getWireContent: () => {
return {}; return {};
@@ -127,6 +133,8 @@ describe("Crypto", function () {
let encryptionInfo = client.getEventEncryptionInfo(event); let encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeFalsy(); expect(encryptionInfo.encrypted).toBeFalsy();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toBe(null);
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted) // unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
event.getSenderKey = () => "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI"; event.getSenderKey = () => "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI";
event.getWireContent = () => { event.getWireContent = () => {
@@ -141,6 +149,11 @@ describe("Crypto", function () {
expect(encryptionInfo.authenticated).toBeFalsy(); expect(encryptionInfo.authenticated).toBeFalsy();
expect(encryptionInfo.sender).toBeFalsy(); expect(encryptionInfo.sender).toBeFalsy();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
});
// known sender, megolm key from backup // known sender, megolm key from backup
event.getForwardingCurve25519KeyChain = () => []; event.getForwardingCurve25519KeyChain = () => [];
event.isKeySourceUntrusted = () => true; event.isKeySourceUntrusted = () => true;
@@ -155,6 +168,11 @@ describe("Crypto", function () {
expect(encryptionInfo.sender).toBeTruthy(); expect(encryptionInfo.sender).toBeTruthy();
expect(encryptionInfo.mismatchedSender).toBeFalsy(); expect(encryptionInfo.mismatchedSender).toBeFalsy();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
});
// known sender, trusted megolm key, but bad ed25519key // known sender, trusted megolm key, but bad ed25519key
event.isKeySourceUntrusted = () => false; event.isKeySourceUntrusted = () => false;
device.keys["ed25519:FLIBBLE"] = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; device.keys["ed25519:FLIBBLE"] = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
@@ -165,9 +183,115 @@ describe("Crypto", function () {
expect(encryptionInfo.sender).toBeTruthy(); expect(encryptionInfo.sender).toBeTruthy();
expect(encryptionInfo.mismatchedSender).toBeTruthy(); expect(encryptionInfo.mismatchedSender).toBeTruthy();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.MISMATCHED_SENDER_KEY,
});
client.stopClient(); client.stopClient();
}); });
describe("provides encryption information for events from verified senders", function () {
const testDeviceId = testData.BOB_TEST_DEVICE_ID;
const testDevice = testData.BOB_SIGNED_TEST_DEVICE_DATA;
let client: MatrixClient;
beforeEach(async () => {
client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
// mock out the verification check
client.crypto!.checkUserTrust = (userId) => new UserTrustLevel(true, false, false);
});
afterEach(() => {
client.stopClient();
});
async function buildEncryptedEvent(
decryptionResult: Partial<EventDecryptionResult> = {},
): Promise<MatrixEvent> {
const mockCryptoBackend = {
decryptEvent: async (event: MatrixEvent): Promise<EventDecryptionResult> => {
return {
claimedEd25519Key: testDevice.keys["ed25519:" + testDeviceId],
clearEvent: {
room_id: "!room_id",
type: "m.room.message",
content: { body: "test" },
},
forwardingCurve25519KeyChain: [],
senderCurve25519Key: testDevice.keys["curve25519:" + testDeviceId],
...decryptionResult,
};
},
} as unknown as CryptoBackend;
const event = new MatrixEvent({
event_id: "$event_id",
sender: testData.BOB_TEST_USER_ID,
type: "m.room.encrypted",
content: { algorithm: "m.megolm.v1.aes-sha2" },
});
await event.attemptDecryption(mockCryptoBackend);
return event;
}
it("unknown device", async () => {
const event = await buildEncryptedEvent();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.UNKNOWN_DEVICE,
});
});
it("known but unsigned device", async () => {
client.crypto!.deviceList.storeDevicesForUser(testData.BOB_TEST_USER_ID, {
[testDeviceId]: {
keys: testDevice.keys,
algorithms: testDevice.algorithms,
verified: DeviceVerification.Unverified,
known: true,
},
});
const event = await buildEncryptedEvent();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNVERIFIED_IDENTITY,
});
});
describe("known and verified device", () => {
beforeEach(() => {
client.crypto!.deviceList.storeDevicesForUser(testData.BOB_TEST_USER_ID, {
[testDeviceId]: {
keys: testDevice.keys,
algorithms: testDevice.algorithms,
verified: DeviceVerification.Verified,
known: true,
},
});
});
it("regular key", async () => {
const event = await buildEncryptedEvent();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.NONE,
shieldReason: null,
});
});
it("unauthenticated key", async () => {
const event = await buildEncryptedEvent({ untrusted: true });
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
});
});
});
});
it("doesn't throw an error when attempting to decrypt a redacted event", async () => { it("doesn't throw an error when attempting to decrypt a redacted event", async () => {
const client = new TestClient("@alice:example.com", "deviceid").client; const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto(); await client.initCrypto();

View File

@@ -2848,6 +2848,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* *
* @param event - event to be checked * @param event - event to be checked
* @returns The event information. * @returns The event information.
* @deprecated Prefer {@link CryptoApi.getEncryptionInfoForEvent | `CryptoApi.getEncryptionInfoForEvent`}.
*/ */
public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo { public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo {
if (!this.cryptoBackend) { if (!this.cryptoBackend) {

View File

@@ -203,6 +203,10 @@ export interface EventDecryptionResult {
* ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}. * ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}.
*/ */
claimedEd25519Key?: string; claimedEd25519Key?: string;
/**
* Whether the keys for this event have been received via an unauthenticated source (eg via key forwards, or
* restored from backup)
*/
untrusted?: boolean; untrusted?: boolean;
/** /**
* The sender doesn't authorize the unverified devices to decrypt his messages * The sender doesn't authorize the unverified devices to decrypt his messages

View File

@@ -22,6 +22,7 @@ import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescri
import { VerificationRequest } from "./crypto-api/verification"; import { VerificationRequest } from "./crypto-api/verification";
import { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "./crypto-api/keybackup"; import { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "./crypto-api/keybackup";
import { ISignatures } from "./@types/signed"; import { ISignatures } from "./@types/signed";
import { MatrixEvent } from "./models/event";
/** /**
* Public interface to the cryptography parts of the js-sdk * Public interface to the cryptography parts of the js-sdk
@@ -265,6 +266,16 @@ export interface CryptoApi {
*/ */
createRecoveryKeyFromPassphrase(password?: string): Promise<GeneratedSecretStorageKey>; createRecoveryKeyFromPassphrase(password?: string): Promise<GeneratedSecretStorageKey>;
/**
* Get information about the encryption of the given event.
*
* @param event - the event to get information for
*
* @returns `null` if the event is not encrypted, or has not (yet) been successfully decrypted. Otherwise, an
* object with information about the encryption of the event.
*/
getEncryptionInfoForEvent(event: MatrixEvent): Promise<EventEncryptionInfo | null>;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// Device/User verification // Device/User verification
@@ -665,5 +676,57 @@ export interface GeneratedSecretStorageKey {
encodedPrivateKey?: string; encodedPrivateKey?: string;
} }
/**
* Result type of {@link CryptoApi#getEncryptionInfoForEvent}.
*/
export interface EventEncryptionInfo {
/** "Shield" to be shown next to this event representing its verification status */
shieldColour: EventShieldColour;
/**
* `null` if `shieldColour` is `EventShieldColour.NONE`; otherwise a reason code for the shield in `shieldColour`.
*/
shieldReason: EventShieldReason | null;
}
/**
* Types of shield to be shown for {@link EventEncryptionInfo#shieldColour}.
*/
export enum EventShieldColour {
NONE,
GREY,
RED,
}
/**
* Reason codes for {@link EventEncryptionInfo#shieldReason}.
*/
export enum EventShieldReason {
/** An unknown reason from the crypto library (if you see this, it is a bug in matrix-js-sdk). */
UNKNOWN,
/** "Encrypted by an unverified user." */
UNVERIFIED_IDENTITY,
/** "Encrypted by a device not verified by its owner." */
UNSIGNED_DEVICE,
/** "Encrypted by an unknown or deleted device." */
UNKNOWN_DEVICE,
/**
* "The authenticity of this encrypted message can't be guaranteed on this device."
*
* ie: the key has been forwarded, or retrieved from an insecure backup.
*/
AUTHENTICITY_NOT_GUARANTEED,
/**
* The (deprecated) sender_key field in the event does not match the Ed25519 key of the device that sent us the
* decryption keys.
*/
MISMATCHED_SENDER_KEY,
}
export * from "./crypto-api/verification"; export * from "./crypto-api/verification";
export * from "./crypto-api/keybackup"; export * from "./crypto-api/keybackup";

View File

@@ -91,6 +91,9 @@ import {
BootstrapCrossSigningOpts, BootstrapCrossSigningOpts,
CrossSigningStatus, CrossSigningStatus,
DeviceVerificationStatus, DeviceVerificationStatus,
EventEncryptionInfo,
EventShieldColour,
EventShieldReason,
ImportRoomKeysOpts, ImportRoomKeysOpts,
KeyBackupCheck, KeyBackupCheck,
KeyBackupInfo, KeyBackupInfo,
@@ -2701,6 +2704,68 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return ret as IEncryptedEventInfo; return ret as IEncryptedEventInfo;
} }
/**
* Implementation of {@link CryptoApi.getEncryptionInfoForEvent}.
*/
public async getEncryptionInfoForEvent(event: MatrixEvent): Promise<EventEncryptionInfo | null> {
const encryptionInfo = this.getEventEncryptionInfo(event);
if (!encryptionInfo.encrypted) {
return null;
}
const senderId = event.getSender();
if (!senderId || encryptionInfo.mismatchedSender) {
// something definitely wrong is going on here
return {
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.MISMATCHED_SENDER_KEY,
};
}
const userTrust = this.checkUserTrust(senderId);
if (!userTrust.isCrossSigningVerified()) {
// If the message is unauthenticated, then display a grey
// shield, otherwise if the user isn't cross-signed then
// nothing's needed
if (!encryptionInfo.authenticated) {
return {
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
};
} else {
return { shieldColour: EventShieldColour.NONE, shieldReason: null };
}
}
const eventSenderTrust =
senderId &&
encryptionInfo.sender &&
(await this.getDeviceVerificationStatus(senderId, encryptionInfo.sender.deviceId));
if (!eventSenderTrust) {
return {
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.UNKNOWN_DEVICE,
};
}
if (!eventSenderTrust.isVerified()) {
return {
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNVERIFIED_IDENTITY,
};
}
if (!encryptionInfo.authenticated) {
return {
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
};
}
return { shieldColour: EventShieldColour.NONE, shieldReason: null };
}
/** /**
* Forces the current outbound group session to be discarded such * Forces the current outbound group session to be discarded such
* that another one will be created next time an event is sent. * that another one will be created next time an event is sent.

View File

@@ -1026,7 +1026,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* signing the public curve25519 key with the ed25519 key. * signing the public curve25519 key with the ed25519 key.
* *
* In general, applications should not use this method directly, but should * In general, applications should not use this method directly, but should
* instead use MatrixClient.getEventSenderDeviceInfo. * instead use {@link CryptoApi#getEncryptionInfoForEvent}.
*/ */
public getClaimedEd25519Key(): string | null { public getClaimedEd25519Key(): string | null {
return this.claimedEd25519Key; return this.claimedEd25519Key;

View File

@@ -39,6 +39,8 @@ import {
CrossSigningStatus, CrossSigningStatus,
CryptoCallbacks, CryptoCallbacks,
DeviceVerificationStatus, DeviceVerificationStatus,
EventEncryptionInfo,
EventShieldColour,
GeneratedSecretStorageKey, GeneratedSecretStorageKey,
ImportRoomKeyProgressData, ImportRoomKeyProgressData,
ImportRoomKeysOpts, ImportRoomKeysOpts,
@@ -208,6 +210,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
return await this.eventDecryptor.attemptEventDecryption(event); return await this.eventDecryptor.attemptEventDecryption(event);
} }
/**
* Implementation of (deprecated) {@link MatrixClient#getEventEncryptionInfo}.
*
* @param event - event to inspect
*/
public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo { public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo {
// TODO: make this work properly. Or better, replace it. // TODO: make this work properly. Or better, replace it.
@@ -732,6 +739,16 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
}; };
} }
/**
* Implementation of {@link CryptoApi.getEncryptionInfoForEvent}.
*/
public async getEncryptionInfoForEvent(event: MatrixEvent): Promise<EventEncryptionInfo | null> {
return {
shieldColour: EventShieldColour.NONE,
shieldReason: null,
};
}
/** /**
* Returns to-device verification requests that are already in progress for the given user id. * Returns to-device verification requests that are already in progress for the given user id.
* *