You've already forked matrix-js-sdk
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:
@@ -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,
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
newBackendOnly(
|
describe("IsolationMode decryption tests", () => {
|
||||||
"fails with an error when cross-signed sender is required but sender is not cross-signed",
|
newBackendOnly(
|
||||||
async () => {
|
"OnlySigned mode - fails with an error when cross-signed sender is required but sender is not cross-signed",
|
||||||
|
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: {} });
|
||||||
|
|||||||
@@ -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
|
|
||||||
* verified user has an unsigned device, or if a verified user replaces
|
|
||||||
* their identity. Events are decrypted only if they come from cross-signed
|
|
||||||
* devices, or devices that existed before the Rust crypto SDK started
|
|
||||||
* tracking device trust: other events will result in a decryption failure. (To access the failure
|
|
||||||
* reason, see {@link MatrixEvent.decryptionFailureReason}.)
|
|
||||||
*/
|
|
||||||
Transition,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* 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}.)
|
|
||||||
*/
|
|
||||||
Invisible,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type of {@link DeviceIsolationMode}.
|
||||||
|
*
|
||||||
|
* Message encryption keys are shared with all devices in the room, except in case of
|
||||||
|
* verified user problems (see {@link errorOnVerifiedUserProblems}).
|
||||||
|
*
|
||||||
|
* Events from all senders are always decrypted (and should be decorated with message shields in case
|
||||||
|
* of authenticity warnings, see {@link EventEncryptionInfo}).
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
* Encryption will throw an error if a verified user replaces their identity.
|
||||||
|
*
|
||||||
|
* 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}.)
|
||||||
|
*/
|
||||||
|
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`.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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}.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user