Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement getEncryptionInfoForEvent and deprecate getEventEncryptionInfo #3693

Merged
merged 4 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2846,6 +2846,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 @@ -257,6 +258,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 @@ -617,5 +628,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 @@ -2694,6 +2697,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
Loading