1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Add crypto mode setting for invisible crypto, and apply it to decrypting events (#4407)

Adds a global "crypto mode" setting to the crypto API (only works with Rust crypto), and changes the decryption settings based on that.
This commit is contained in:
Hubert Chathi
2024-09-18 07:53:07 -04:00
committed by GitHub
parent 414ac9d8cc
commit dbb4828eda
6 changed files with 210 additions and 36 deletions

View File

@ -50,7 +50,7 @@
], ],
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^8.0.0", "@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
"@matrix-org/olm": "3.2.15", "@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0", "another-json": "^0.2.0",
"bs58": "^6.0.0", "bs58": "^6.0.0",

View File

@ -82,6 +82,7 @@ import { SecretStorageKeyDescription } from "../../../src/secret-storage";
import { import {
CrossSigningKey, CrossSigningKey,
CryptoCallbacks, CryptoCallbacks,
CryptoMode,
DecryptionFailureCode, DecryptionFailureCode,
EventShieldColour, EventShieldColour,
EventShieldReason, EventShieldReason,
@ -746,6 +747,87 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
); );
}); });
newBackendOnly(
"fails with an error when cross-signed sender is required but sender is not cross-signed",
async () => {
// This tests that a message will not be decrypted if the sender
// is not sufficiently trusted according to the selected crypto
// mode.
//
// This test is almost the same as the "Alice receives a megolm
// message" test, with the main difference that we set the
// crypto mode.
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
// Start by using Invisible crypto mode
aliceClient.getCrypto()!.setCryptoMode(CryptoMode.Invisible);
await startClientAndAwaitFirstSync();
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
olmAccount: testOlmAccount,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
const messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {
[ROOM_ID]: { timeline: { events: [messageEncrypted] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
// it probably won't be decrypted yet, because it takes a while to process the olm keys
const decryptedEvent = 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: {} });
await startClientAndAwaitFirstSync(); await startClientAndAwaitFirstSync();

View File

@ -40,6 +40,13 @@ export interface CryptoApi {
*/ */
globalBlacklistUnverifiedDevices: boolean; globalBlacklistUnverifiedDevices: boolean;
/**
* The cryptography mode to use.
*
* @see CryptoMode
*/
setCryptoMode(cryptoMode: CryptoMode): void;
/** /**
* Return the current version of the crypto module. * Return the current version of the crypto module.
* For example: `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`. * For example: `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`.
@ -589,6 +596,24 @@ export enum DecryptionFailureCode {
*/ */
HISTORICAL_MESSAGE_USER_NOT_JOINED = "HISTORICAL_MESSAGE_USER_NOT_JOINED", HISTORICAL_MESSAGE_USER_NOT_JOINED = "HISTORICAL_MESSAGE_USER_NOT_JOINED",
/**
* The sender's identity is not verified, but was previously verified.
*/
SENDER_IDENTITY_PREVIOUSLY_VERIFIED = "SENDER_IDENTITY_PREVIOUSLY_VERIFIED",
/**
* The sender device is not cross-signed. This will only be used if the
* crypto mode is set to `CryptoMode.Invisible` or `CryptoMode.Transition`.
*/
UNSIGNED_SENDER_DEVICE = "UNSIGNED_SENDER_DEVICE",
/**
* We weren't able to link the message back to any known device. This will
* only be used if the crypto mode is set to `CryptoMode.Invisible` or
* `CryptoMode.Transition`.
*/
UNKNOWN_SENDER_DEVICE = "UNKNOWN_SENDER_DEVICE",
/** Unknown or unclassified error. */ /** Unknown or unclassified error. */
UNKNOWN_ERROR = "UNKNOWN_ERROR", UNKNOWN_ERROR = "UNKNOWN_ERROR",
@ -632,6 +657,38 @@ export enum DecryptionFailureCode {
UNKNOWN_ENCRYPTION_ALGORITHM = "UNKNOWN_ENCRYPTION_ALGORITHM", UNKNOWN_ENCRYPTION_ALGORITHM = "UNKNOWN_ENCRYPTION_ALGORITHM",
} }
/**
* The cryptography mode. Affects how messages are encrypted and decrypted.
* Only supported by Rust crypto.
*/
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,
}
/** /**
* Options object for `CryptoApi.bootstrapCrossSigning`. * Options object for `CryptoApi.bootstrapCrossSigning`.
*/ */

View File

@ -88,6 +88,7 @@ import {
BootstrapCrossSigningOpts, BootstrapCrossSigningOpts,
CrossSigningKeyInfo, CrossSigningKeyInfo,
CrossSigningStatus, CrossSigningStatus,
CryptoMode,
decodeRecoveryKey, decodeRecoveryKey,
DecryptionFailureCode, DecryptionFailureCode,
DeviceVerificationStatus, DeviceVerificationStatus,
@ -648,6 +649,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
this.backupManager.checkAndStart(); this.backupManager.checkAndStart();
} }
/**
* Implementation of {@link Crypto.CryptoApi#setCryptoMode}.
*/
public setCryptoMode(cryptoMode: CryptoMode): void {
throw new Error("Not supported");
}
/** /**
* Implementation of {@link Crypto.CryptoApi#getVersion}. * Implementation of {@link Crypto.CryptoApi#getVersion}.
*/ */

View File

@ -45,6 +45,7 @@ import {
CrossSigningStatus, CrossSigningStatus,
CryptoApi, CryptoApi,
CryptoCallbacks, CryptoCallbacks,
CryptoMode,
Curve25519AuthData, Curve25519AuthData,
DecryptionFailureCode, DecryptionFailureCode,
DeviceVerificationStatus, DeviceVerificationStatus,
@ -106,6 +107,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;
/** whether {@link stop} has been called */ /** whether {@link stop} has been called */
private stopped = false; private stopped = false;
@ -257,7 +259,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); return await this.eventDecryptor.attemptEventDecryption(event, this.cryptoMode);
} }
/** /**
@ -367,6 +369,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
return `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`; return `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`;
} }
/**
* Implementation of {@link Crypto.CryptoApi#setCryptoMode}.
*/
public setCryptoMode(cryptoMode: CryptoMode): void {
this.cryptoMode = cryptoMode;
}
/** /**
* Implementation of {@link CryptoApi#isEncryptionEnabledInRoom}. * Implementation of {@link CryptoApi#isEncryptionEnabledInRoom}.
*/ */
@ -1742,18 +1751,31 @@ class EventDecryptor {
private readonly perSessionBackupDownloader: PerSessionKeyBackupDownloader, private readonly perSessionBackupDownloader: PerSessionKeyBackupDownloader,
) {} ) {}
public async attemptEventDecryption(event: MatrixEvent): Promise<IEventDecryptionResult> { public async attemptEventDecryption(event: MatrixEvent, cryptoMode: CryptoMode): 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.
// (fixes https://github.com/vector-im/element-web/issues/5001) // (fixes https://github.com/vector-im/element-web/issues/5001)
this.addEventToPendingList(event); this.addEventToPendingList(event);
let trustRequirement;
switch (cryptoMode) {
case CryptoMode.Legacy:
trustRequirement = RustSdkCryptoJs.TrustRequirement.Untrusted;
break;
case CryptoMode.Transition:
trustRequirement = RustSdkCryptoJs.TrustRequirement.CrossSignedOrLegacy;
break;
case CryptoMode.Invisible:
trustRequirement = RustSdkCryptoJs.TrustRequirement.CrossSigned;
break;
}
try { try {
const res = (await this.olmMachine.decryptRoomEvent( const res = (await this.olmMachine.decryptRoomEvent(
stringifyEvent(event), stringifyEvent(event),
new RustSdkCryptoJs.RoomId(event.getRoomId()!), new RustSdkCryptoJs.RoomId(event.getRoomId()!),
new RustSdkCryptoJs.DecryptionSettings(RustSdkCryptoJs.TrustRequirement.Untrusted), new RustSdkCryptoJs.DecryptionSettings(trustRequirement),
)) as RustSdkCryptoJs.DecryptedRoomEvent; )) as RustSdkCryptoJs.DecryptedRoomEvent;
// Success. We can remove the event from the pending list, if // Success. We can remove the event from the pending list, if
@ -1861,6 +1883,36 @@ class EventDecryptor {
errorDetails, errorDetails,
); );
case RustSdkCryptoJs.DecryptionErrorCode.SenderIdentityPreviouslyVerified:
// We're refusing to decrypt due to not trusting the sender,
// rather than failing to decrypt due to lack of keys, so we
// don't need to keep it on the pending list.
this.removeEventFromPendingList(event);
throw new DecryptionError(
DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED,
"The sender identity is unverified, but was previously verified.",
);
case RustSdkCryptoJs.DecryptionErrorCode.UnknownSenderDevice:
// We're refusing to decrypt due to not trusting the sender,
// rather than failing to decrypt due to lack of keys, so we
// don't need to keep it on the pending list.
this.removeEventFromPendingList(event);
throw new DecryptionError(
DecryptionFailureCode.UNKNOWN_SENDER_DEVICE,
"The sender device is not known.",
);
case RustSdkCryptoJs.DecryptionErrorCode.UnsignedSenderDevice:
// We're refusing to decrypt due to not trusting the sender,
// rather than failing to decrypt due to lack of keys, so we
// don't need to keep it on the pending list.
this.removeEventFromPendingList(event);
throw new DecryptionError(
DecryptionFailureCode.UNSIGNED_SENDER_DEVICE,
"The sender identity is not cross-signed.",
);
// We don't map MismatchedIdentityKeys for now, as there is no equivalent in legacy. // We don't map MismatchedIdentityKeys for now, as there is no equivalent in legacy.
// Just put it on the `UNKNOWN_ERROR` bucket. // Just put it on the `UNKNOWN_ERROR` bucket.
default: default:

View File

@ -1453,10 +1453,10 @@
"@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@jridgewell/sourcemap-codec" "^1.4.14"
"@matrix-org/matrix-sdk-crypto-wasm@^8.0.0": "@matrix-org/matrix-sdk-crypto-wasm@^9.0.0":
version "8.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-8.0.0.tgz#6ddc0e63538e821a2efbc5c1a2f0fa0f71d489ff" resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.0.0.tgz#293fe8fcb9bc4d577c5f6cf2cbffa151c6e11329"
integrity sha512-s0q3O2dK8b6hOJ+SZFz+s/IiMabmVsNue6r17sTwbrRD8liBkCrpjYnxoMYvtC01GggJ9TZLQbeqpt8hQSPHAg== integrity sha512-dz4dkYXj6BeOQuw52XQj8dMuhi85pSFhfFeFlNRAO7JdRPhE9CHBrfK8knkZV5Zux5vvf3Ub4E7myoLeJgZoEw==
"@matrix-org/olm@3.2.15": "@matrix-org/olm@3.2.15":
version "3.2.15" version "3.2.15"
@ -5831,16 +5831,7 @@ string-length@^4.0.1:
char-regex "^1.0.2" char-regex "^1.0.2"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0": "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -5895,14 +5886,7 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1" define-properties "^1.2.1"
es-object-atoms "^1.0.0" es-object-atoms "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -6434,16 +6418,7 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==