Skip to content

Commit

Permalink
Implement getEncryptionInfoForEvent and deprecate `getEventEncrypti…
Browse files Browse the repository at this point in the history
…onInfo` (#3693)

* Implement `getEncryptionInfoForEvent` and deprecate `getEventEncryptionInfo`

* fix tsdoc

* fix tests

* Improve test coverage
  • Loading branch information
richvdh committed Sep 7, 2023
1 parent 0700e86 commit 7e691bf
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 3 deletions.
128 changes: 126 additions & 2 deletions spec/unit/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ import { sleep } from "../../src/utils";
import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from "../../src/logger";
import { MemoryStore } from "../../src";
import { DeviceVerification, MemoryStore } from "../../src";
import { RoomKeyRequestState } from "../../src/crypto/OutgoingRoomKeyRequestManager";
import { RoomMember } from "../../src/models/room-member";
import { IStore } from "../../src/store";
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;

Expand Down Expand Up @@ -111,13 +116,14 @@ describe("Crypto", 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;
await client.initCrypto();

// unencrypted event
const event = {
getId: () => "$event_id",
getSender: () => "@bob:example.com",
getSenderKey: () => null,
getWireContent: () => {
return {};
Expand All @@ -127,6 +133,8 @@ describe("Crypto", function () {
let encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeFalsy();

expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toBe(null);

// unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
event.getSenderKey = () => "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI";
event.getWireContent = () => {
Expand All @@ -141,6 +149,11 @@ describe("Crypto", function () {
expect(encryptionInfo.authenticated).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
event.getForwardingCurve25519KeyChain = () => [];
event.isKeySourceUntrusted = () => true;
Expand All @@ -155,6 +168,11 @@ describe("Crypto", function () {
expect(encryptionInfo.sender).toBeTruthy();
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
event.isKeySourceUntrusted = () => false;
device.keys["ed25519:FLIBBLE"] = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
Expand All @@ -165,9 +183,115 @@ describe("Crypto", function () {
expect(encryptionInfo.sender).toBeTruthy();
expect(encryptionInfo.mismatchedSender).toBeTruthy();

expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.MISMATCHED_SENDER_KEY,
});

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 () => {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
Expand Down
1 change: 1 addition & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2848,6 +2848,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param event - event to be checked
* @returns The event information.
* @deprecated Prefer {@link CryptoApi.getEncryptionInfoForEvent | `CryptoApi.getEncryptionInfoForEvent`}.
*/
public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo {
if (!this.cryptoBackend) {
Expand Down
4 changes: 4 additions & 0 deletions src/common-crypto/CryptoBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ export interface EventDecryptionResult {
* ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}.
*/
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;
/**
* The sender doesn't authorize the unverified devices to decrypt his messages
Expand Down
63 changes: 63 additions & 0 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescri
import { VerificationRequest } from "./crypto-api/verification";
import { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "./crypto-api/keybackup";
import { ISignatures } from "./@types/signed";
import { MatrixEvent } from "./models/event";

/**
* Public interface to the cryptography parts of the js-sdk
Expand Down Expand Up @@ -265,6 +266,16 @@ export interface CryptoApi {
*/
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
Expand Down Expand Up @@ -665,5 +676,57 @@ export interface GeneratedSecretStorageKey {
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/keybackup";
65 changes: 65 additions & 0 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ import {
BootstrapCrossSigningOpts,
CrossSigningStatus,
DeviceVerificationStatus,
EventEncryptionInfo,
EventShieldColour,
EventShieldReason,
ImportRoomKeysOpts,
KeyBackupCheck,
KeyBackupInfo,
Expand Down Expand Up @@ -2701,6 +2704,68 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
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
* that another one will be created next time an event is sent.
Expand Down
2 changes: 1 addition & 1 deletion src/models/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* signing the public curve25519 key with the ed25519 key.
*
* In general, applications should not use this method directly, but should
* instead use MatrixClient.getEventSenderDeviceInfo.
* instead use {@link CryptoApi#getEncryptionInfoForEvent}.
*/
public getClaimedEd25519Key(): string | null {
return this.claimedEd25519Key;
Expand Down
Loading

0 comments on commit 7e691bf

Please sign in to comment.