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

crypto: Replace cryptoMode with DeviceIsolationMode concept (#4429)

* crypto: Replace cryptoMode with DeviceIsolationMode concept

* use enum instead of string for the IsolationMode kind

* Code review - Cleaning, renaming

* review: unneeded @see in doc

* review: Rename IsolationMode with better names

* review: quick cleaning and doc
This commit is contained in:
Valere
2024-09-25 15:33:02 +02:00
committed by GitHub
parent 1a8ea3d685
commit 538717c23e
4 changed files with 107 additions and 76 deletions

View File

@@ -82,11 +82,13 @@ import { SecretStorageKeyDescription } from "../../../src/secret-storage";
import { import {
CrossSigningKey, CrossSigningKey,
CryptoCallbacks, CryptoCallbacks,
CryptoMode,
DecryptionFailureCode, DecryptionFailureCode,
DeviceIsolationMode,
EventShieldColour, EventShieldColour,
EventShieldReason, EventShieldReason,
KeyBackupInfo, KeyBackupInfo,
AllDevicesIsolationMode,
OnlySignedDevicesIsolationMode,
} from "../../../src/crypto-api"; } from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { IKeyBackup } from "../../../src/crypto/backup"; import { IKeyBackup } from "../../../src/crypto/backup";
@@ -747,9 +749,34 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
); );
}); });
describe("IsolationMode decryption tests", () => {
newBackendOnly( newBackendOnly(
"fails with an error when cross-signed sender is required but sender is not cross-signed", "OnlySigned mode - fails with an error when cross-signed sender is required but sender is not cross-signed",
async () => { async () => {
const decryptedEvent = await setUpTestAndDecrypt(new OnlySignedDevicesIsolationMode());
// It will error as an unknown device because we haven't fetched
// the sender's device keys.
expect(decryptedEvent.isDecryptionFailure()).toBe(true);
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
},
);
newBackendOnly(
"NoIsolation mode - Decrypts with warning when cross-signed sender is required but sender is not cross-signed",
async () => {
const decryptedEvent = await setUpTestAndDecrypt(new AllDevicesIsolationMode(false));
expect(decryptedEvent.isDecryptionFailure()).toBe(false);
expect(await aliceClient.getCrypto()!.getEncryptionInfoForEvent(decryptedEvent)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNKNOWN_DEVICE,
});
},
);
async function setUpTestAndDecrypt(isolationMode: DeviceIsolationMode): Promise<MatrixEvent> {
// This tests that a message will not be decrypted if the sender // This tests that a message will not be decrypted if the sender
// is not sufficiently trusted according to the selected crypto // is not sufficiently trusted according to the selected crypto
// mode. // mode.
@@ -760,7 +787,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
// Start by using Invisible crypto mode // Start by using Invisible crypto mode
aliceClient.getCrypto()!.setCryptoMode(CryptoMode.Invisible); aliceClient.getCrypto()!.setDeviceIsolationMode(isolationMode);
await startClientAndAwaitFirstSync(); await startClientAndAwaitFirstSync();
@@ -807,26 +834,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expect(event.isEncrypted()).toBe(true); expect(event.isEncrypted()).toBe(true);
// it probably won't be decrypted yet, because it takes a while to process the olm keys // it probably won't be decrypted yet, because it takes a while to process the olm keys
const decryptedEvent = await testUtils.awaitDecryption(event); return await testUtils.awaitDecryption(event);
// It will error as an unknown device because we haven't fetched }
// the sender's device keys. });
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
// Next, try decrypting in transition mode, which should also
// fail for the same reason
aliceClient.getCrypto()!.setCryptoMode(CryptoMode.Transition);
await event.attemptDecryption(aliceClient["cryptoBackend"]!);
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
// Decrypting in legacy mode should succeed since it doesn't
// care about device trust.
aliceClient.getCrypto()!.setCryptoMode(CryptoMode.Legacy);
await event.attemptDecryption(aliceClient["cryptoBackend"]!);
expect(decryptedEvent.decryptionFailureReason).toEqual(null);
},
);
it("Decryption fails with Unable to decrypt for other errors", async () => { it("Decryption fails with Unable to decrypt for other errors", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });

View File

@@ -41,11 +41,9 @@ export interface CryptoApi {
globalBlacklistUnverifiedDevices: boolean; globalBlacklistUnverifiedDevices: boolean;
/** /**
* The cryptography mode to use. * The {@link DeviceIsolationMode} mode to use.
*
* @see CryptoMode
*/ */
setCryptoMode(cryptoMode: CryptoMode): void; setDeviceIsolationMode(isolationMode: DeviceIsolationMode): void;
/** /**
* Return the current version of the crypto module. * Return the current version of the crypto module.
@@ -667,38 +665,59 @@ export enum DecryptionFailureCode {
UNKNOWN_ENCRYPTION_ALGORITHM = "UNKNOWN_ENCRYPTION_ALGORITHM", UNKNOWN_ENCRYPTION_ALGORITHM = "UNKNOWN_ENCRYPTION_ALGORITHM",
} }
/** /** Base {@link DeviceIsolationMode} kind. */
* The cryptography mode. Affects how messages are encrypted and decrypted. export enum DeviceIsolationModeKind {
* Only supported by Rust crypto. AllDevicesIsolationMode,
*/ OnlySignedDevicesIsolationMode,
export enum CryptoMode { }
/**
* Message encryption keys are shared with all devices in the room, except for
* blacklisted devices, or unverified devices if
* `globalBlacklistUnverifiedDevices` is set. Events from all senders are
* decrypted.
*/
Legacy,
/** /**
* Events are encrypted as with `Legacy` mode, but encryption will throw an error if a * A type of {@link DeviceIsolationMode}.
* verified user has an unsigned device, or if a verified user replaces *
* their identity. Events are decrypted only if they come from cross-signed * Message encryption keys are shared with all devices in the room, except in case of
* devices, or devices that existed before the Rust crypto SDK started * verified user problems (see {@link errorOnVerifiedUserProblems}).
* tracking device trust: other events will result in a decryption failure. (To access the failure *
* reason, see {@link MatrixEvent.decryptionFailureReason}.) * Events from all senders are always decrypted (and should be decorated with message shields in case
* of authenticity warnings, see {@link EventEncryptionInfo}).
*/ */
Transition, export class AllDevicesIsolationMode {
public readonly kind = DeviceIsolationModeKind.AllDevicesIsolationMode;
/** /**
*
* @param errorOnVerifiedUserProblems - Behavior when sharing keys to remote devices.
*
* If set to `true`, sharing keys will fail (i.e. message sending will fail) with an error if:
* - The user was previously verified but is not anymore, or:
* - A verified user has some unverified devices (not cross-signed).
*
* If `false`, the keys will be distributed as usual. In this case, the client UX should display
* warnings to inform the user about problematic devices/users, and stop them hitting this case.
*/
public constructor(public readonly errorOnVerifiedUserProblems: boolean) {}
}
/**
* A type of {@link DeviceIsolationMode}.
*
* Message encryption keys are only shared with devices that have been cross-signed by their owner. * Message encryption keys are only shared with devices that have been cross-signed by their owner.
* Encryption will throw an error if a verified user replaces their identity. Events are * Encryption will throw an error if a verified user replaces their identity.
* decrypted only if they come from a cross-signed device other events will result in a decryption *
* Events are decrypted only if they come from a cross-signed device. Other events will result in a decryption
* failure. (To access the failure reason, see {@link MatrixEvent.decryptionFailureReason}.) * failure. (To access the failure reason, see {@link MatrixEvent.decryptionFailureReason}.)
*/ */
Invisible, export class OnlySignedDevicesIsolationMode {
public readonly kind = DeviceIsolationModeKind.OnlySignedDevicesIsolationMode;
} }
/**
* DeviceIsolationMode represents the mode of device isolation used when encrypting or decrypting messages.
* It can be one of two types: {@link AllDevicesIsolationMode} or {@link OnlySignedDevicesIsolationMode}.
*
* Only supported by rust Crypto.
*/
export type DeviceIsolationMode = AllDevicesIsolationMode | OnlySignedDevicesIsolationMode;
/** /**
* Options object for `CryptoApi.bootstrapCrossSigning`. * Options object for `CryptoApi.bootstrapCrossSigning`.
*/ */

View File

@@ -88,9 +88,9 @@ import {
BootstrapCrossSigningOpts, BootstrapCrossSigningOpts,
CrossSigningKeyInfo, CrossSigningKeyInfo,
CrossSigningStatus, CrossSigningStatus,
CryptoMode,
decodeRecoveryKey, decodeRecoveryKey,
DecryptionFailureCode, DecryptionFailureCode,
DeviceIsolationMode,
DeviceVerificationStatus, DeviceVerificationStatus,
encodeRecoveryKey, encodeRecoveryKey,
EventEncryptionInfo, EventEncryptionInfo,
@@ -650,12 +650,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
} }
/** /**
* Implementation of {@link Crypto.CryptoApi#setCryptoMode}. * Implementation of {@link Crypto.CryptoApi#setDeviceIsolationMode}.
*/ */
public setCryptoMode(cryptoMode: CryptoMode): void { public setDeviceIsolationMode(isolationMode: DeviceIsolationMode): void {
throw new Error("Not supported"); throw new Error("Not supported");
} }
/** /**
* Implementation of {@link Crypto.CryptoApi#getVersion}. * Implementation of {@link Crypto.CryptoApi#getVersion}.
*/ */

View File

@@ -45,7 +45,6 @@ import {
CrossSigningStatus, CrossSigningStatus,
CryptoApi, CryptoApi,
CryptoCallbacks, CryptoCallbacks,
CryptoMode,
Curve25519AuthData, Curve25519AuthData,
DecryptionFailureCode, DecryptionFailureCode,
DeviceVerificationStatus, DeviceVerificationStatus,
@@ -61,6 +60,9 @@ import {
VerificationRequest, VerificationRequest,
encodeRecoveryKey, encodeRecoveryKey,
deriveRecoveryKeyFromPassphrase, deriveRecoveryKeyFromPassphrase,
DeviceIsolationMode,
AllDevicesIsolationMode,
DeviceIsolationModeKind,
} from "../crypto-api/index.ts"; } from "../crypto-api/index.ts";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts"; import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts"; import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
@@ -107,7 +109,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
private readonly RECOVERY_KEY_DERIVATION_ITERATIONS = 500000; private readonly RECOVERY_KEY_DERIVATION_ITERATIONS = 500000;
private _trustCrossSignedDevices = true; private _trustCrossSignedDevices = true;
private cryptoMode = CryptoMode.Legacy; private deviceIsolationMode: DeviceIsolationMode = new AllDevicesIsolationMode(false);
/** whether {@link stop} has been called */ /** whether {@link stop} has been called */
private stopped = false; private stopped = false;
@@ -259,7 +261,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
// through decryptEvent and hence get rid of this case. // through decryptEvent and hence get rid of this case.
throw new Error("to-device event was not decrypted in preprocessToDeviceMessages"); throw new Error("to-device event was not decrypted in preprocessToDeviceMessages");
} }
return await this.eventDecryptor.attemptEventDecryption(event, this.cryptoMode); return await this.eventDecryptor.attemptEventDecryption(event, this.deviceIsolationMode);
} }
/** /**
@@ -370,10 +372,10 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
} }
/** /**
* Implementation of {@link Crypto.CryptoApi#setCryptoMode}. * Implementation of {@link CryptoApi#setDeviceIsolationMode}.
*/ */
public setCryptoMode(cryptoMode: CryptoMode): void { public setDeviceIsolationMode(isolationMode: DeviceIsolationMode): void {
this.cryptoMode = cryptoMode; this.deviceIsolationMode = isolationMode;
} }
/** /**
@@ -1776,7 +1778,10 @@ class EventDecryptor {
private readonly perSessionBackupDownloader: PerSessionKeyBackupDownloader, private readonly perSessionBackupDownloader: PerSessionKeyBackupDownloader,
) {} ) {}
public async attemptEventDecryption(event: MatrixEvent, cryptoMode: CryptoMode): Promise<IEventDecryptionResult> { public async attemptEventDecryption(
event: MatrixEvent,
isolationMode: DeviceIsolationMode,
): Promise<IEventDecryptionResult> {
// add the event to the pending list *before* attempting to decrypt. // add the event to the pending list *before* attempting to decrypt.
// then, if the key turns up while decryption is in progress (and // then, if the key turns up while decryption is in progress (and
// decryption fails), we will schedule a retry. // decryption fails), we will schedule a retry.
@@ -1784,16 +1789,14 @@ class EventDecryptor {
this.addEventToPendingList(event); this.addEventToPendingList(event);
let trustRequirement; let trustRequirement;
switch (cryptoMode) {
case CryptoMode.Legacy: switch (isolationMode.kind) {
case DeviceIsolationModeKind.AllDevicesIsolationMode:
trustRequirement = RustSdkCryptoJs.TrustRequirement.Untrusted; trustRequirement = RustSdkCryptoJs.TrustRequirement.Untrusted;
break; break;
case CryptoMode.Transition: case DeviceIsolationModeKind.OnlySignedDevicesIsolationMode:
trustRequirement = RustSdkCryptoJs.TrustRequirement.CrossSignedOrLegacy; trustRequirement = RustSdkCryptoJs.TrustRequirement.CrossSignedOrLegacy;
break; break;
case CryptoMode.Invisible:
trustRequirement = RustSdkCryptoJs.TrustRequirement.CrossSigned;
break;
} }
try { try {