From dbb4828eda223a60d3ad4597dcfd18ca1f476a34 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 18 Sep 2024 07:53:07 -0400 Subject: [PATCH] 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. --- package.json | 2 +- spec/integ/crypto/crypto.spec.ts | 82 ++++++++++++++++++++++++++++++++ src/crypto-api/index.ts | 57 ++++++++++++++++++++++ src/crypto/index.ts | 8 ++++ src/rust-crypto/rust-crypto.ts | 58 ++++++++++++++++++++-- yarn.lock | 39 +++------------ 6 files changed, 210 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index b9132f43e2b..c2684552ed1 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ], "dependencies": { "@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", "another-json": "^0.2.0", "bs58": "^6.0.0", diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index c1a665e2c06..41b52dd53cb 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -82,6 +82,7 @@ import { SecretStorageKeyDescription } from "../../../src/secret-storage"; import { CrossSigningKey, CryptoCallbacks, + CryptoMode, DecryptionFailureCode, EventShieldColour, 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 () => { expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 0faf89a7677..35e4a689762 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -40,6 +40,13 @@ export interface CryptoApi { */ globalBlacklistUnverifiedDevices: boolean; + /** + * The cryptography mode to use. + * + * @see CryptoMode + */ + setCryptoMode(cryptoMode: CryptoMode): void; + /** * Return the current version of the crypto module. * 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", + /** + * 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_ERROR = "UNKNOWN_ERROR", @@ -632,6 +657,38 @@ export enum DecryptionFailureCode { 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`. */ diff --git a/src/crypto/index.ts b/src/crypto/index.ts index d72e41a6e3d..90638aea1a0 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -88,6 +88,7 @@ import { BootstrapCrossSigningOpts, CrossSigningKeyInfo, CrossSigningStatus, + CryptoMode, decodeRecoveryKey, DecryptionFailureCode, DeviceVerificationStatus, @@ -648,6 +649,13 @@ export class Crypto extends TypedEventEmitter { + public async attemptEventDecryption(event: MatrixEvent, cryptoMode: CryptoMode): Promise { // add the event to the pending list *before* attempting to decrypt. // then, if the key turns up while decryption is in progress (and // decryption fails), we will schedule a retry. // (fixes https://github.com/vector-im/element-web/issues/5001) 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 { const res = (await this.olmMachine.decryptRoomEvent( stringifyEvent(event), new RustSdkCryptoJs.RoomId(event.getRoomId()!), - new RustSdkCryptoJs.DecryptionSettings(RustSdkCryptoJs.TrustRequirement.Untrusted), + new RustSdkCryptoJs.DecryptionSettings(trustRequirement), )) as RustSdkCryptoJs.DecryptedRoomEvent; // Success. We can remove the event from the pending list, if @@ -1861,6 +1883,36 @@ class EventDecryptor { 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. // Just put it on the `UNKNOWN_ERROR` bucket. default: diff --git a/yarn.lock b/yarn.lock index 83f6c7b31ea..d32a10c688f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1453,10 +1453,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@matrix-org/matrix-sdk-crypto-wasm@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-8.0.0.tgz#6ddc0e63538e821a2efbc5c1a2f0fa0f71d489ff" - integrity sha512-s0q3O2dK8b6hOJ+SZFz+s/IiMabmVsNue6r17sTwbrRD8liBkCrpjYnxoMYvtC01GggJ9TZLQbeqpt8hQSPHAg== +"@matrix-org/matrix-sdk-crypto-wasm@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.0.0.tgz#293fe8fcb9bc4d577c5f6cf2cbffa151c6e11329" + integrity sha512-dz4dkYXj6BeOQuw52XQj8dMuhi85pSFhfFeFlNRAO7JdRPhE9CHBrfK8knkZV5Zux5vvf3Ub4E7myoLeJgZoEw== "@matrix-org/olm@3.2.15": version "3.2.15" @@ -5831,16 +5831,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - 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: +"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== @@ -5895,14 +5886,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm: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: +"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== @@ -6434,16 +6418,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm: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: +"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==