Skip to content

Commit dbb4828

Browse files
authored
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.
1 parent 414ac9d commit dbb4828

File tree

6 files changed

+210
-36
lines changed

6 files changed

+210
-36
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
],
5151
"dependencies": {
5252
"@babel/runtime": "^7.12.5",
53-
"@matrix-org/matrix-sdk-crypto-wasm": "^8.0.0",
53+
"@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
5454
"@matrix-org/olm": "3.2.15",
5555
"another-json": "^0.2.0",
5656
"bs58": "^6.0.0",

spec/integ/crypto/crypto.spec.ts

+82
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import { SecretStorageKeyDescription } from "../../../src/secret-storage";
8282
import {
8383
CrossSigningKey,
8484
CryptoCallbacks,
85+
CryptoMode,
8586
DecryptionFailureCode,
8687
EventShieldColour,
8788
EventShieldReason,
@@ -746,6 +747,87 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
746747
);
747748
});
748749

750+
newBackendOnly(
751+
"fails with an error when cross-signed sender is required but sender is not cross-signed",
752+
async () => {
753+
// This tests that a message will not be decrypted if the sender
754+
// is not sufficiently trusted according to the selected crypto
755+
// mode.
756+
//
757+
// This test is almost the same as the "Alice receives a megolm
758+
// message" test, with the main difference that we set the
759+
// crypto mode.
760+
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
761+
762+
// Start by using Invisible crypto mode
763+
aliceClient.getCrypto()!.setCryptoMode(CryptoMode.Invisible);
764+
765+
await startClientAndAwaitFirstSync();
766+
767+
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
768+
const groupSession = new Olm.OutboundGroupSession();
769+
groupSession.create();
770+
771+
// make the room_key event
772+
const roomKeyEncrypted = encryptGroupSessionKey({
773+
recipient: aliceClient.getUserId()!,
774+
recipientCurve25519Key: keyReceiver.getDeviceKey(),
775+
recipientEd25519Key: keyReceiver.getSigningKey(),
776+
olmAccount: testOlmAccount,
777+
p2pSession: p2pSession,
778+
groupSession: groupSession,
779+
room_id: ROOM_ID,
780+
});
781+
782+
// encrypt a message with the group session
783+
const messageEncrypted = encryptMegolmEvent({
784+
senderKey: testSenderKey,
785+
groupSession: groupSession,
786+
room_id: ROOM_ID,
787+
});
788+
789+
// Alice gets both the events in a single sync
790+
const syncResponse = {
791+
next_batch: 1,
792+
to_device: {
793+
events: [roomKeyEncrypted],
794+
},
795+
rooms: {
796+
join: {
797+
[ROOM_ID]: { timeline: { events: [messageEncrypted] } },
798+
},
799+
},
800+
};
801+
802+
syncResponder.sendOrQueueSyncResponse(syncResponse);
803+
await syncPromise(aliceClient);
804+
805+
const room = aliceClient.getRoom(ROOM_ID)!;
806+
const event = room.getLiveTimeline().getEvents()[0];
807+
expect(event.isEncrypted()).toBe(true);
808+
809+
// it probably won't be decrypted yet, because it takes a while to process the olm keys
810+
const decryptedEvent = await testUtils.awaitDecryption(event);
811+
// It will error as an unknown device because we haven't fetched
812+
// the sender's device keys.
813+
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
814+
815+
// Next, try decrypting in transition mode, which should also
816+
// fail for the same reason
817+
aliceClient.getCrypto()!.setCryptoMode(CryptoMode.Transition);
818+
819+
await event.attemptDecryption(aliceClient["cryptoBackend"]!);
820+
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
821+
822+
// Decrypting in legacy mode should succeed since it doesn't
823+
// care about device trust.
824+
aliceClient.getCrypto()!.setCryptoMode(CryptoMode.Legacy);
825+
826+
await event.attemptDecryption(aliceClient["cryptoBackend"]!);
827+
expect(decryptedEvent.decryptionFailureReason).toEqual(null);
828+
},
829+
);
830+
749831
it("Decryption fails with Unable to decrypt for other errors", async () => {
750832
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
751833
await startClientAndAwaitFirstSync();

src/crypto-api/index.ts

+57
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ export interface CryptoApi {
4040
*/
4141
globalBlacklistUnverifiedDevices: boolean;
4242

43+
/**
44+
* The cryptography mode to use.
45+
*
46+
* @see CryptoMode
47+
*/
48+
setCryptoMode(cryptoMode: CryptoMode): void;
49+
4350
/**
4451
* Return the current version of the crypto module.
4552
* For example: `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`.
@@ -589,6 +596,24 @@ export enum DecryptionFailureCode {
589596
*/
590597
HISTORICAL_MESSAGE_USER_NOT_JOINED = "HISTORICAL_MESSAGE_USER_NOT_JOINED",
591598

599+
/**
600+
* The sender's identity is not verified, but was previously verified.
601+
*/
602+
SENDER_IDENTITY_PREVIOUSLY_VERIFIED = "SENDER_IDENTITY_PREVIOUSLY_VERIFIED",
603+
604+
/**
605+
* The sender device is not cross-signed. This will only be used if the
606+
* crypto mode is set to `CryptoMode.Invisible` or `CryptoMode.Transition`.
607+
*/
608+
UNSIGNED_SENDER_DEVICE = "UNSIGNED_SENDER_DEVICE",
609+
610+
/**
611+
* We weren't able to link the message back to any known device. This will
612+
* only be used if the crypto mode is set to `CryptoMode.Invisible` or
613+
* `CryptoMode.Transition`.
614+
*/
615+
UNKNOWN_SENDER_DEVICE = "UNKNOWN_SENDER_DEVICE",
616+
592617
/** Unknown or unclassified error. */
593618
UNKNOWN_ERROR = "UNKNOWN_ERROR",
594619

@@ -632,6 +657,38 @@ export enum DecryptionFailureCode {
632657
UNKNOWN_ENCRYPTION_ALGORITHM = "UNKNOWN_ENCRYPTION_ALGORITHM",
633658
}
634659

660+
/**
661+
* The cryptography mode. Affects how messages are encrypted and decrypted.
662+
* Only supported by Rust crypto.
663+
*/
664+
export enum CryptoMode {
665+
/**
666+
* Message encryption keys are shared with all devices in the room, except for
667+
* blacklisted devices, or unverified devices if
668+
* `globalBlacklistUnverifiedDevices` is set. Events from all senders are
669+
* decrypted.
670+
*/
671+
Legacy,
672+
673+
/**
674+
* Events are encrypted as with `Legacy` mode, but encryption will throw an error if a
675+
* verified user has an unsigned device, or if a verified user replaces
676+
* their identity. Events are decrypted only if they come from cross-signed
677+
* devices, or devices that existed before the Rust crypto SDK started
678+
* tracking device trust: other events will result in a decryption failure. (To access the failure
679+
* reason, see {@link MatrixEvent.decryptionFailureReason}.)
680+
*/
681+
Transition,
682+
683+
/**
684+
* Message encryption keys are only shared with devices that have been cross-signed by their owner.
685+
* Encryption will throw an error if a verified user replaces their identity. Events are
686+
* decrypted only if they come from a cross-signed device other events will result in a decryption
687+
* failure. (To access the failure reason, see {@link MatrixEvent.decryptionFailureReason}.)
688+
*/
689+
Invisible,
690+
}
691+
635692
/**
636693
* Options object for `CryptoApi.bootstrapCrossSigning`.
637694
*/

src/crypto/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import {
8888
BootstrapCrossSigningOpts,
8989
CrossSigningKeyInfo,
9090
CrossSigningStatus,
91+
CryptoMode,
9192
decodeRecoveryKey,
9293
DecryptionFailureCode,
9394
DeviceVerificationStatus,
@@ -648,6 +649,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
648649
this.backupManager.checkAndStart();
649650
}
650651

652+
/**
653+
* Implementation of {@link Crypto.CryptoApi#setCryptoMode}.
654+
*/
655+
public setCryptoMode(cryptoMode: CryptoMode): void {
656+
throw new Error("Not supported");
657+
}
658+
651659
/**
652660
* Implementation of {@link Crypto.CryptoApi#getVersion}.
653661
*/

src/rust-crypto/rust-crypto.ts

+55-3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
CrossSigningStatus,
4646
CryptoApi,
4747
CryptoCallbacks,
48+
CryptoMode,
4849
Curve25519AuthData,
4950
DecryptionFailureCode,
5051
DeviceVerificationStatus,
@@ -106,6 +107,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
106107
private readonly RECOVERY_KEY_DERIVATION_ITERATIONS = 500000;
107108

108109
private _trustCrossSignedDevices = true;
110+
private cryptoMode = CryptoMode.Legacy;
109111

110112
/** whether {@link stop} has been called */
111113
private stopped = false;
@@ -257,7 +259,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
257259
// through decryptEvent and hence get rid of this case.
258260
throw new Error("to-device event was not decrypted in preprocessToDeviceMessages");
259261
}
260-
return await this.eventDecryptor.attemptEventDecryption(event);
262+
return await this.eventDecryptor.attemptEventDecryption(event, this.cryptoMode);
261263
}
262264

263265
/**
@@ -367,6 +369,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
367369
return `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`;
368370
}
369371

372+
/**
373+
* Implementation of {@link Crypto.CryptoApi#setCryptoMode}.
374+
*/
375+
public setCryptoMode(cryptoMode: CryptoMode): void {
376+
this.cryptoMode = cryptoMode;
377+
}
378+
370379
/**
371380
* Implementation of {@link CryptoApi#isEncryptionEnabledInRoom}.
372381
*/
@@ -1742,18 +1751,31 @@ class EventDecryptor {
17421751
private readonly perSessionBackupDownloader: PerSessionKeyBackupDownloader,
17431752
) {}
17441753

1745-
public async attemptEventDecryption(event: MatrixEvent): Promise<IEventDecryptionResult> {
1754+
public async attemptEventDecryption(event: MatrixEvent, cryptoMode: CryptoMode): Promise<IEventDecryptionResult> {
17461755
// add the event to the pending list *before* attempting to decrypt.
17471756
// then, if the key turns up while decryption is in progress (and
17481757
// decryption fails), we will schedule a retry.
17491758
// (fixes https://github.com/vector-im/element-web/issues/5001)
17501759
this.addEventToPendingList(event);
17511760

1761+
let trustRequirement;
1762+
switch (cryptoMode) {
1763+
case CryptoMode.Legacy:
1764+
trustRequirement = RustSdkCryptoJs.TrustRequirement.Untrusted;
1765+
break;
1766+
case CryptoMode.Transition:
1767+
trustRequirement = RustSdkCryptoJs.TrustRequirement.CrossSignedOrLegacy;
1768+
break;
1769+
case CryptoMode.Invisible:
1770+
trustRequirement = RustSdkCryptoJs.TrustRequirement.CrossSigned;
1771+
break;
1772+
}
1773+
17521774
try {
17531775
const res = (await this.olmMachine.decryptRoomEvent(
17541776
stringifyEvent(event),
17551777
new RustSdkCryptoJs.RoomId(event.getRoomId()!),
1756-
new RustSdkCryptoJs.DecryptionSettings(RustSdkCryptoJs.TrustRequirement.Untrusted),
1778+
new RustSdkCryptoJs.DecryptionSettings(trustRequirement),
17571779
)) as RustSdkCryptoJs.DecryptedRoomEvent;
17581780

17591781
// Success. We can remove the event from the pending list, if
@@ -1861,6 +1883,36 @@ class EventDecryptor {
18611883
errorDetails,
18621884
);
18631885

1886+
case RustSdkCryptoJs.DecryptionErrorCode.SenderIdentityPreviouslyVerified:
1887+
// We're refusing to decrypt due to not trusting the sender,
1888+
// rather than failing to decrypt due to lack of keys, so we
1889+
// don't need to keep it on the pending list.
1890+
this.removeEventFromPendingList(event);
1891+
throw new DecryptionError(
1892+
DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED,
1893+
"The sender identity is unverified, but was previously verified.",
1894+
);
1895+
1896+
case RustSdkCryptoJs.DecryptionErrorCode.UnknownSenderDevice:
1897+
// We're refusing to decrypt due to not trusting the sender,
1898+
// rather than failing to decrypt due to lack of keys, so we
1899+
// don't need to keep it on the pending list.
1900+
this.removeEventFromPendingList(event);
1901+
throw new DecryptionError(
1902+
DecryptionFailureCode.UNKNOWN_SENDER_DEVICE,
1903+
"The sender device is not known.",
1904+
);
1905+
1906+
case RustSdkCryptoJs.DecryptionErrorCode.UnsignedSenderDevice:
1907+
// We're refusing to decrypt due to not trusting the sender,
1908+
// rather than failing to decrypt due to lack of keys, so we
1909+
// don't need to keep it on the pending list.
1910+
this.removeEventFromPendingList(event);
1911+
throw new DecryptionError(
1912+
DecryptionFailureCode.UNSIGNED_SENDER_DEVICE,
1913+
"The sender identity is not cross-signed.",
1914+
);
1915+
18641916
// We don't map MismatchedIdentityKeys for now, as there is no equivalent in legacy.
18651917
// Just put it on the `UNKNOWN_ERROR` bucket.
18661918
default:

yarn.lock

+7-32
Original file line numberDiff line numberDiff line change
@@ -1453,10 +1453,10 @@
14531453
"@jridgewell/resolve-uri" "^3.1.0"
14541454
"@jridgewell/sourcemap-codec" "^1.4.14"
14551455

1456-
"@matrix-org/matrix-sdk-crypto-wasm@^8.0.0":
1457-
version "8.0.0"
1458-
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-8.0.0.tgz#6ddc0e63538e821a2efbc5c1a2f0fa0f71d489ff"
1459-
integrity sha512-s0q3O2dK8b6hOJ+SZFz+s/IiMabmVsNue6r17sTwbrRD8liBkCrpjYnxoMYvtC01GggJ9TZLQbeqpt8hQSPHAg==
1456+
"@matrix-org/matrix-sdk-crypto-wasm@^9.0.0":
1457+
version "9.0.0"
1458+
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.0.0.tgz#293fe8fcb9bc4d577c5f6cf2cbffa151c6e11329"
1459+
integrity sha512-dz4dkYXj6BeOQuw52XQj8dMuhi85pSFhfFeFlNRAO7JdRPhE9CHBrfK8knkZV5Zux5vvf3Ub4E7myoLeJgZoEw==
14601460

14611461
"@matrix-org/[email protected]":
14621462
version "3.2.15"
@@ -5831,16 +5831,7 @@ string-length@^4.0.1:
58315831
char-regex "^1.0.2"
58325832
strip-ansi "^6.0.0"
58335833

5834-
"string-width-cjs@npm:string-width@^4.2.0":
5835-
version "4.2.3"
5836-
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
5837-
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
5838-
dependencies:
5839-
emoji-regex "^8.0.0"
5840-
is-fullwidth-code-point "^3.0.0"
5841-
strip-ansi "^6.0.1"
5842-
5843-
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
5834+
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
58445835
version "4.2.3"
58455836
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
58465837
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -5895,14 +5886,7 @@ string.prototype.trimstart@^1.0.8:
58955886
define-properties "^1.2.1"
58965887
es-object-atoms "^1.0.0"
58975888

5898-
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
5899-
version "6.0.1"
5900-
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
5901-
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
5902-
dependencies:
5903-
ansi-regex "^5.0.1"
5904-
5905-
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
5889+
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
59065890
version "6.0.1"
59075891
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
59085892
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -6434,16 +6418,7 @@ word-wrap@^1.2.5:
64346418
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
64356419
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
64366420

6437-
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
6438-
version "7.0.0"
6439-
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
6440-
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
6441-
dependencies:
6442-
ansi-styles "^4.0.0"
6443-
string-width "^4.1.0"
6444-
strip-ansi "^6.0.0"
6445-
6446-
wrap-ansi@^7.0.0:
6421+
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
64476422
version "7.0.0"
64486423
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
64496424
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==

0 commit comments

Comments
 (0)