Skip to content

Commit

Permalink
use crypto mode setting when decrypting
Browse files Browse the repository at this point in the history
  • Loading branch information
uhoreg committed Sep 17, 2024
1 parent 2a5b799 commit bab0343
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 4 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
68 changes: 68 additions & 0 deletions spec/integ/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import { SecretStorageKeyDescription } from "../../../src/secret-storage";
import {
CrossSigningKey,
CryptoCallbacks,
CryptoMode,
DecryptionFailureCode,
EventShieldColour,
EventShieldReason,
Expand Down Expand Up @@ -746,6 +747,73 @@ 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. We select invisible crypto, which requires sender
// devices to be cross-signed. Since the sender is not
// cross-signed, we should get a SENDER_UNTRUSTED error.
//
// 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: {} });
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);
},
);

it("Decryption fails with Unable to decrypt for other errors", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
Expand Down
15 changes: 15 additions & 0 deletions src/crypto-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,21 @@ 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 a mode that requires cross-signing of devices. FIXME: which ones?
*/
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 a mode that requires a known device.
*/
UNKNOWN_SENDER_DEVICE = "UNKNOWN_SENDER_DEVICE",

/** Unknown or unclassified error. */
UNKNOWN_ERROR = "UNKNOWN_ERROR",

Expand Down
49 changes: 46 additions & 3 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
// through decryptEvent and hence get rid of this case.
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);
}

/**
Expand Down Expand Up @@ -1740,18 +1740,31 @@ class EventDecryptor {
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.
// 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
Expand Down Expand Up @@ -1859,6 +1872,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:
Expand Down

0 comments on commit bab0343

Please sign in to comment.