diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index b27f4102b..f6123b14f 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -57,6 +57,7 @@ import { RoomStateEvent, } from "../../../src/matrix"; import { DeviceInfo } from "../../../src/crypto/deviceinfo"; +import * as testData from "../../test-utils/test-data"; import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder"; import { escapeRegExp } from "../../../src/utils"; @@ -70,6 +71,7 @@ import { import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage"; import { CrossSigningKey, CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; +import { DecryptionError } from "../../../src/crypto/algorithms"; afterEach(() => { // reset fake-indexeddb after each test, to make sure we don't leak connections @@ -629,6 +631,107 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(decryptedEvent.getContent().body).toEqual("42"); }); + describe("Unable to decrypt error codes", function () { + it("Encryption fails with expected UISI error", async () => { + expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await startClientAndAwaitFirstSync(); + + const awaitUISI = new Promise((resolve) => { + aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => { + const error = err as DecryptionError; + if (error.code == "MEGOLM_UNKNOWN_INBOUND_SESSION_ID") { + resolve(); + } + }); + }); + + // Alice gets both the events in a single sync + const syncResponse = { + next_batch: 1, + rooms: { + join: { + [testData.TEST_ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } }, + }, + }, + }; + + syncResponder.sendOrQueueSyncResponse(syncResponse); + await syncPromise(aliceClient); + + await awaitUISI; + }); + + it("Encryption fails with expected Unknown Index error", async () => { + expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await startClientAndAwaitFirstSync(); + + const awaitUnknownIndex = new Promise((resolve) => { + aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => { + const error = err as DecryptionError; + if (error.code == "OLM_UNKNOWN_MESSAGE_INDEX") { + resolve(); + } + }); + }); + + await aliceClient.getCrypto()!.importRoomKeys([testData.RATCHTED_MEGOLM_SESSION_DATA]); + + // Alice gets both the events in a single sync + const syncResponse = { + next_batch: 1, + rooms: { + join: { + [testData.TEST_ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } }, + }, + }, + }; + + syncResponder.sendOrQueueSyncResponse(syncResponse); + await syncPromise(aliceClient); + + await awaitUnknownIndex; + }); + + it("Encryption fails with Unable to decrypt for other errors", async () => { + expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await startClientAndAwaitFirstSync(); + + await aliceClient.getCrypto()!.importRoomKeys([testData.MEGOLM_SESSION_DATA]); + + const awaitDecryptionError = new Promise((resolve) => { + aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => { + const error = err as DecryptionError; + // rust and libolm can't have an exact 1:1 mapping for all errors, + // but some errors are part of API and should match + if ( + error.code != "MEGOLM_UNKNOWN_INBOUND_SESSION_ID" && + error.code != "OLM_UNKNOWN_MESSAGE_INDEX" + ) { + resolve(); + } + }); + }); + + const malformedEvent: Partial = JSON.parse(JSON.stringify(testData.ENCRYPTED_EVENT)); + malformedEvent.content!.ciphertext = "AwgAEnAkBmciEAyhh1j6DCk29UXJ7kv/kvayUNfuNT0iAioLxcXjFX"; + + // Alice gets both the events in a single sync + const syncResponse = { + next_batch: 1, + rooms: { + join: { + [testData.TEST_ROOM_ID]: { timeline: { events: [malformedEvent] } }, + }, + }, + }; + + syncResponder.sendOrQueueSyncResponse(syncResponse); + await syncPromise(aliceClient); + + await awaitDecryptionError; + }); + }); + it("Alice receives a megolm message before the session keys", async () => { expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); diff --git a/spec/test-utils/test-data/generate-test-data.py b/spec/test-utils/test-data/generate-test-data.py index f56a49149..92a797dc9 100755 --- a/spec/test-utils/test-data/generate-test-data.py +++ b/spec/test-utils/test-data/generate-test-data.py @@ -43,6 +43,8 @@ ALICE_DATA = { "TEST_ROOM_ID": "!room:id", # any 32-byte string can be an ed25519 private key. "TEST_DEVICE_PRIVATE_KEY_BYTES": b"deadbeefdeadbeefdeadbeefdeadbeef", + # any 32-byte string can be an curve25519 private key. + "TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"deadmuledeadmuledeadmuledeadmule", "MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"doyouspeakwhaaaaaaaaaaaaaaaaaale", "USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"useruseruseruseruseruseruseruser", @@ -60,6 +62,8 @@ BOB_DATA = { "TEST_ROOM_ID": "!room:id", # any 32-byte string can be an ed25519 private key. "TEST_DEVICE_PRIVATE_KEY_BYTES": b"Deadbeefdeadbeefdeadbeefdeadbeef", + # any 32-byte string can be an curve25519 private key. + "TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"Deadmuledeadmuledeadmuledeadmule", "MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale", "USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser", @@ -80,7 +84,7 @@ def main() -> None: */ import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto"; -import {{ IDownloadKeyResult }} from "../../../src"; +import {{ IDownloadKeyResult, IEvent }} from "../../../src"; import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup"; /* eslint-disable comma-dangle */ @@ -102,6 +106,11 @@ def build_test_data(user_data, prefix = "") -> str: private_key = ed25519.Ed25519PrivateKey.from_private_bytes( user_data["TEST_DEVICE_PRIVATE_KEY_BYTES"] ) + + device_curve_key = x25519.X25519PrivateKey.from_private_bytes( + user_data["TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES"] + ) + b64_public_key = encode_base64( private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) ) @@ -169,9 +178,10 @@ def build_test_data(user_data, prefix = "") -> str: user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": sig} } - set_of_exported_room_keys = [build_exported_megolm_key(), build_exported_megolm_key()] + set_of_exported_room_keys = [build_exported_megolm_key(device_curve_key)[0], build_exported_megolm_key(device_curve_key)[0]] - additional_exported_room_key = build_exported_megolm_key() + additional_exported_room_key, additional_exported_ed_key = build_exported_megolm_key(device_curve_key) + ratcheted_exported_room_key = symetric_ratchet_step_of_megolm_key(additional_exported_room_key, additional_exported_ed_key) otk_to_sign = { "key": user_data['OTK'] @@ -194,6 +204,8 @@ def build_test_data(user_data, prefix = "") -> str: backed_up_room_key = encrypt_megolm_key_for_backup(additional_exported_room_key, backup_decryption_key.public_key()) + clear_event, encrypted_event = generate_encrypted_event_content(additional_exported_room_key, additional_exported_ed_key, device_curve_key) + backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"]) return f"""\ @@ -252,8 +264,19 @@ export const {prefix}MEGOLM_SESSION_DATA: IMegolmSessionData = { json.dumps(additional_exported_room_key, indent=4) }; +/** A ratcheted version of {prefix}MEGOLM_SESSION_DATA */ +export const {prefix}RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = { + json.dumps(ratcheted_exported_room_key, indent=4) +}; + /** The key from {prefix}MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/ export const {prefix}CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {json.dumps(backed_up_room_key, indent=4)}; + +/** A test clear event */ +export const {prefix}CLEAR_EVENT: Partial = {json.dumps(clear_event, indent=4)}; + +/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */ +export const {prefix}ENCRYPTED_EVENT: Partial = {json.dumps(encrypted_event, indent=4)}; """ @@ -350,10 +373,11 @@ def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str: return signature_base64 -def build_exported_megolm_key() -> dict: +def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tuple[dict, ed25519.Ed25519PrivateKey]: """ Creates an exported megolm room key, as per https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-export-format that can be imported via importRoomKeys API. + Returns the exported key, the matching privat edKey (needed to encrypt) """ index = 0 private_key = ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)) @@ -369,8 +393,10 @@ def build_exported_megolm_key() -> dict: megolm_export = { "algorithm": "m.megolm.v1.aes-sha2", - "room_id": "!roomA:example.org", - "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", + "room_id": "!room:id", + "sender_key": encode_base64( + device_curve_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + ), "session_id": encode_base64( private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) ), @@ -381,6 +407,53 @@ def build_exported_megolm_key() -> dict: "forwarding_curve25519_key_chain": [], } + return megolm_export, private_key + +def symetric_ratchet_step_of_megolm_key(previous: dict , megolm_private_key: ed25519.Ed25519PrivateKey) -> dict: + + """ + Very simple ratchet step from 0 to 1 + Used to generate a ratcheted key to test unknown message index. + """ + session_key: str = previous["session_key"] + + # Get the megolm R0 from the export format + decoded = base64.b64decode(session_key.encode("ascii")) + ri = decoded[5:133] + + ri0 = ri[0:32] + ri1 = ri[32:64] + ri2 = ri[64:96] + ri3 = ri[96:128] + + h = hmac.HMAC(ri3, hashes.SHA256()) + h.update(b'x\03') + ri1_3 = h.finalize() + + index = 1 + private_key = megolm_private_key + + # exported key, start with version byte + exported_key = bytearray(b'\x01') + exported_key += index.to_bytes(4, 'big') + exported_key += ri0 + exported_key += ri1 + exported_key += ri2 + exported_key += ri1_3 + # KPub + exported_key += private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + + + megolm_export = { + "algorithm": "m.megolm.v1.aes-sha2", + "room_id": "!room:id", + "sender_key": previous["sender_key"], + "session_id": previous["session_id"], + "session_key": encode_base64(exported_key), + "sender_claimed_keys": previous["sender_claimed_keys"], + "forwarding_curve25519_key_chain": [], + } + return megolm_export @@ -499,7 +572,7 @@ def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519 ct = encryptor.update(padded_data) + encryptor.finalize() - # The ratchet index i, and the cipher-text​, are then packed + # The ratchet index i, and the cipher-text, are then packed # into a message as described in Message format. Then the entire message # (including the version bytes and all payload bytes) are passed through # HMAC-SHA-256. The first 8 bytes of the MAC are appended to the message. diff --git a/spec/test-utils/test-data/index.ts b/spec/test-utils/test-data/index.ts index 8cc3e4903..44245b4a6 100644 --- a/spec/test-utils/test-data/index.ts +++ b/spec/test-utils/test-data/index.ts @@ -4,7 +4,7 @@ */ import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto"; -import { IDownloadKeyResult } from "../../../src"; +import { IDownloadKeyResult, IEvent } from "../../../src"; import { KeyBackupSession, KeyBackupInfo } from "../../../src/crypto-api/keybackup"; /* eslint-disable comma-dangle */ @@ -142,8 +142,8 @@ export const SIGNED_BACKUP_DATA: KeyBackupInfo = { export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [ { "algorithm": "m.megolm.v1.aes-sha2", - "room_id": "!roomA:example.org", - "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", + "room_id": "!room:id", + "sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc", "session_id": "FYOoKQSwe4d9jhTZ/LQCZFJINjPEqZ7Or4Z08reP92M", "session_key": "AQAAAABZ0jXQOprFfXe41tIFmAtHxflJp4O2hM/vzQQpOazOCFeWSoW5P3Z9Q+voU3eXehMwyP8/hm/Q8xLP6/PmJdy+71se/17kdFwcDGgLxBWfa4ODM9zlI4EjKbNqmiii5loJ7rBhA/XXaw80m0hfU6zTDX/KrO55J0Pt4vJ0LDa3LBWDqCkEsHuHfY4U2fy0AmRSSDYzxKmezq+GdPK3j/dj", "sender_claimed_keys": { @@ -153,8 +153,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [ }, { "algorithm": "m.megolm.v1.aes-sha2", - "room_id": "!roomA:example.org", - "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", + "room_id": "!room:id", + "sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc", "session_id": "mPYSGA2l1tOQiipEDEVYhDSdTSFh2lDW1qpGKYZRxTc", "session_key": "AQAAAAAHwgkB49BTPAEGTCK6degxUIbl8GPG2ugPRYhNtOpNic63u11+baXFfjDw5fmVfD1gJXpQQjGsqrIYioxrB1xzl7mfb942UHhYdaMQZowpp1fSpJVsxR5TddUU2EWifYD9EQsoz8mY1zqoazm4vUP4v9yxaTcUBj2c6HMJCY0gCJj2EhgNpdbTkIoqRAxFWIQ0nU0hYdpQ1taqRimGUcU3", "sender_claimed_keys": { @@ -167,8 +167,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [ /** An exported megolm session */ export const MEGOLM_SESSION_DATA: IMegolmSessionData = { "algorithm": "m.megolm.v1.aes-sha2", - "room_id": "!roomA:example.org", - "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", + "room_id": "!room:id", + "sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc", "session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co", "session_key": "AQAAAABXGO+Z9jlQJhIL6ByhXrv2BwCIxkhh7MXpKLsYmXkJcWrQlirmXmD79ga1zo+I4DCtEZzyGSpDWXBC6G7ez3H4gDMBam1RE3Jm5tc+oTlIri32UkYgSL0kBkcEnttqmIXBlK8tAfJo3cJnlh7F4ltEOAqrdME6dU0zXTkqXmURqYqXSOmbP+w8xUxIYgNohmjA3x5CGApXql0+i/uXtf3K", "sender_claimed_keys": { @@ -177,18 +177,58 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = { "forwarding_curve25519_key_chain": [] }; +/** A ratcheted version of MEGOLM_SESSION_DATA */ +export const RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = { + "algorithm": "m.megolm.v1.aes-sha2", + "room_id": "!room:id", + "sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc", + "session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co", + "session_key": "AQAAAAFXGO+Z9jlQJhIL6ByhXrv2BwCIxkhh7MXpKLsYmXkJcWrQlirmXmD79ga1zo+I4DCtEZzyGSpDWXBC6G7ez3H4gDMBam1RE3Jm5tc+oTlIri32UkYgSL0kBkcEnttqmIUWvpwC7by/yg231+gyzu9lDHAU4ivCj48pt7WGiORWmIqXSOmbP+w8xUxIYgNohmjA3x5CGApXql0+i/uXtf3K", + "sender_claimed_keys": { + "ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI" + }, + "forwarding_curve25519_key_chain": [] +}; + /** The key from MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/ export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = { "first_message_index": 1, "forwarded_count": 0, "is_verified": false, "session_data": { - "ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//KzlqinE6iAfrQYHmoOTes2uOsHgvbE7M9bIyAE1/FxzIgZuzmbJ1Fp/qjgKeTeonuMujLpnezUOYzDYqbF8hyLdm+SkrVWISde/wmVfpl1TI56ItCKmz0uB4fjUMWUV0sgp+3JLAUIqIqmwb7VVZooox94ze2VuspJMu9mF6MoQs5BEk3bFi9nbdgGwGh0pFYjEwE62+xLjo9x/gdVkIy8xlBmCFjBdvWjBjtvc8NfboWKxZ1tmpPCt6IiCTwOY4kyQdmK6U+sjh3tvs7qLOX5ETThOAqwKsY9VeXbeCtWF2h/m6ZXFQpxsf/vLgpTe0djDsGpWgLY+dywAelfz6O69EiHAxti9XxEGO0WLBDs0rmyhPN03IeLa+b8Gp2RMRdJ4prjKr0m7CVTUXEHbw6LtN0SWvEnc6iBI0vCEs1EocJAFfXsiwzAAgPDmaaDrCEk1dOoYj2eXoy4tbJx3+/FXFKe+RVdmiuv1nsVAQxPq78Tx6UtIz7ihHI0tzaZV+/7PUY065b6kevDBST+SZWERwqFZdz28TWsHu6Kxjr5k2N4sahs8a4frobip6C7PhXJFiq3n7lMx1IjfRrN0HxzpMqBzhkmiat9JR6nWtGjvOvvw", + "ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//Kzlq2o5CssPsw+88gRehkAsPg9Zp5G9sL9to6giltvTWTbsaQpmvv3HLmBOYSFIxvyZrOT/Ffqu325f0IEsKcyV2BdIkw8Ob9Xt+VWoe4MYEGG6y1T8W125zeFgKWI4Ow76uput64H9zZjIo+Cc+hCTO9Ea4EnosSjizCotevkNck7C/zGgfhBikiohROb6SbaZgxicSsEDZ+f7brnri9yP3iXS3PMDHHpa1+XzG2VOG/Y9OQZpkPq+pbLrCC+NWJeJPslDAK5i+RURwzjnPmaHKCRHTq86CwhFyiCDf61MGwCY3xjrmBJg44BCdxWqCx0YJvwsvVqqnl4vTieUfrwThNPsQ81aVkDHvlmrgrTt8icDa8jTJhu34jem+pbRSEM5aJikV4B+zYiLz+dH/v6UpYA2eG8ReOvwpPXp6CAcIlplRPpWbMBeLFVcPkT4KAXTp9exFpB4on4pf8OsaDomlt4qAA0rhAZmhPWPKcU/A0Tz4gyMu54OivVtw1SPj+5Iq+YDQ8jB6Po3ApzMf6fwF9x/FjevbboFB05X2Jr0NrbFqXMOUwXHMgDAGiIWX8+gkmmbaiNWqg2etjN94pobQSGZelb18XGN7kuwMk+Zwk7A", "ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU", "mac": "OibmACbORhI" } }; +/** A test clear event */ +export const CLEAR_EVENT: Partial = { + "type": "m.room.message", + "room_id": "!room:id", + "sender": "@alice:localhost", + "content": { + "msgtype": "m.text", + "body": "Hello world" + } +}; + +/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */ +export const ENCRYPTED_EVENT: Partial = { + "type": "m.room.encrypted", + "room_id": "!room:id", + "sender": "@alice:localhost", + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc", + "ciphertext": "AwgAEnAkBmciEAyhh1j6DCk29UXJ7kv/kvayUNfuNT0iAioLxcXjFXOZ5ho3jF1/wrytlt0Lb298uMM67OxdVMi+/mMfYpwlvi07P9cIH6CMSj8tyhYoWl0SrKY6tkPf5GWOlRSRRKbziXa96FHXvnA3V2FCAIGtAe3G4ei5RPbhkmKAFBLAen33/D6MjJVqU8Ojr5vTkgls5eyirarlVpsmnH06alDaxO8avrU0NL+Vsw26xvlUQgEMOnUJ", + "session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co", + "device_id": "TEST_DEVICE" + }, + "event_id": "$event1", + "origin_server_ts": 1507753886000 +}; + // Bob data export const BOB_TEST_USER_ID = "@bob:xyz"; @@ -322,8 +362,8 @@ export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = { export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [ { "algorithm": "m.megolm.v1.aes-sha2", - "room_id": "!roomA:example.org", - "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", + "room_id": "!room:id", + "sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw", "session_id": "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0", "session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt", "sender_claimed_keys": { @@ -333,8 +373,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [ }, { "algorithm": "m.megolm.v1.aes-sha2", - "room_id": "!roomA:example.org", - "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", + "room_id": "!room:id", + "sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw", "session_id": "+07YOpSgdZ1X9le3n3NMByw0V1B0H0Djnbm76jgmWoo", "session_key": "AQAAAAAjWfIMo9+BWS8IvhfsQuomxXXXGy11tJs0ej505xxd1RzOIP4ftq3MbZYsfH8kqSMBc2l1Ym2u3Dksv2/nR0zGQeNIgOxeMuwHU3Ry7+DdV1I96blPylVCCn/f5RAy6smKoaeylptPdXgVXmw3YBBUVYpHpm+xCIUUp9foAdb8hftO2DqUoHWdV/ZXt59zTAcsNFdQdB9A4525u+o4JlqK", "sender_claimed_keys": { @@ -347,8 +387,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [ /** An exported megolm session */ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = { "algorithm": "m.megolm.v1.aes-sha2", - "room_id": "!roomA:example.org", - "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", + "room_id": "!room:id", + "sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw", "session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc", "session_key": "AQAAAADZJL7OdM/KHfPzXPZ3CtlLBIlzbwk06dnZTd3bvkcdP5u73rdmThBKdqGA4xzCyxZsHdYLZRrlmD3VwOmNfvWMqYdPxA1X0vs3d172y9EIG8i+N/skJxTRypcVSV9XoinBNIWr/gkyepuAKiQqemlc8J5amD9OkmbVkmnrxP1uyYMsMnQayCXCVpLQv4nN7bpymTNV2ZtTKWImLXW4/EaX", "sender_claimed_keys": { @@ -357,15 +397,55 @@ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = { "forwarding_curve25519_key_chain": [] }; +/** A ratcheted version of BOB_MEGOLM_SESSION_DATA */ +export const BOB_RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = { + "algorithm": "m.megolm.v1.aes-sha2", + "room_id": "!room:id", + "sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw", + "session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc", + "session_key": "AQAAAAHZJL7OdM/KHfPzXPZ3CtlLBIlzbwk06dnZTd3bvkcdP5u73rdmThBKdqGA4xzCyxZsHdYLZRrlmD3VwOmNfvWMqYdPxA1X0vs3d172y9EIG8i+N/skJxTRypcVSV9Xoil2JdGx9oPqR0dFVh661Aqs86rJRbQ4IeRiuEm35VMxboMsMnQayCXCVpLQv4nN7bpymTNV2ZtTKWImLXW4/EaX", + "sender_claimed_keys": { + "ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64" + }, + "forwarding_curve25519_key_chain": [] +}; + /** The key from BOB_MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/ export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = { "first_message_index": 1, "forwarded_count": 0, "is_verified": false, "session_data": { - "ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAMW+71BJqIPQW910vV7SX3JkR91kuuiDqlFT1VHLMP9/MNsvCfeZz1Q+0/LqkJo1KTbrrjKxqOq5/e7RQequOEXHu0a658fKaK/tk0eLvO2xUTxxBCgtuuLpeysOykf4jqp3yV89+h+h3o4zsWGyA2cLIBC/9ofqiqsoF+l1smzA+sQ3ArLBM5B2wc7b4Ir7jZUzCpnOK6O6nQA3vwOpwy3AqpeCKbsuXBPLkb9QXnVH9UzNbuL1Go7M6S3zH2BlAQrUhKSj4yI9tQC3XL3Y3IuAJRUX2bIXzYn9DryL+zwz9aULGN1ECZ4pMHm9jlPUYgMDYmSA2rRCZPWXcSeqmV8aB2qR3k+gcCFXNGbKy73bdN87GsPsYPI7RCkgtHGyW2tpz0JpP1CQmtP3wXNBKIJKIgcxGQ3YDehGV7XlFDDUYBZyYGazXqU/fdt21gc4Jvg1xVKsj83woVmGDo+2WzPrpIJ79NJUovC8jJ+UA3QamY28Df6UaRuS3+vvgCCW9e3T5+h5hR7BQayZ9r9Ykv7h7NTxQYjsYwxfZK+OULJPFNYbcB+vc6msLAsqocPhNQTazOe4TfsSAAuaHa/2FfGGMyaWCQY5DI3tk3Pagr87L8lBhwXll9usXKslvc+sJw", + "ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAMW+71BJqIPQW910vV7SX3IcGylnoUcS3doVkJZiprXytXMP89AKcgv5Dj7mS2ZdvNGE+Atro74bzZ5yot5BrE0ZE5SjoUBPLaLMMu9HopLIV+qx01Rc3F0wmkocSPo51N0nv6wvO5Cst0FiOGHDK6r1pFlgDEJLmBkOyC4e8oMVbKTJzsSQVbJ8tJ37xuhI+T5P0ZlmiqKDqYRp8uh50w+txLEixYhEUunFgCTt1DAmiS9pLNYhLyl1ggwuQjzZe+AV6timbRxNJy18/AEcPomJw7z/pxYIiNLHRKOC13Wp8kGWx9cOgfMQ5KmBuLS8psGiLTBkfWPLOfNYqjbeqAR+OGZQoS6hUjbBYU7QuFa4FOYBHkNB2UqNsdsMb9qB/qs7QGTSb8Lok5YjW1c81BUpmIyKvuqnKma0MZskrpTYGQD2eJDABFCZwLFm+LgDyUTeSiV5xguYztLrHOk8LHKo9M8dIZgoBjeFVJxyjbcXKsVS3aQkMXKCrRlKLqhZTws/ZJwVfW9DbktZ9dT+tRZQvI7tjJofojcLX61AGJDnqUf5+2Gv1tEnmUI953gIzc8NlcFabPOsDsZEODt7MdOCTPT3w29umyhKbCsslpb64LoS/AB2QRPRCgkJS7snRA", "ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU", "mac": "lEfHlqfJQwU" } }; +/** A test clear event */ +export const BOB_CLEAR_EVENT: Partial = { + "type": "m.room.message", + "room_id": "!room:id", + "sender": "@alice:localhost", + "content": { + "msgtype": "m.text", + "body": "Hello world" + } +}; + +/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */ +export const BOB_ENCRYPTED_EVENT: Partial = { + "type": "m.room.encrypted", + "room_id": "!room:id", + "sender": "@alice:localhost", + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw", + "ciphertext": "AwgAEnA/mEqZm2lSrhoG11OpDqsohGSBJWsudbuoItLlivmpFZQHrKMbE6z/dhCTwUi76vwfRCtf4tyPMD845cqZH1nL0bowq3/awyzZ8Q263Y3WrLfkUTFBU6oPF/IULUFZZuw6kLdfd5g5+uigvqUhFFpICoj7KNHznv4sFNssd00/WgJquZ6PRt6e1v6ANFNiZPAwghIL+kBc6pb8i6MUWt9JnXilJhTqFDHdXiY4qkaKBWbwebC26PYM", + "session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc", + "device_id": "TEST_DEVICE" + }, + "event_id": "$event1", + "origin_server_ts": 1507753886000 +}; + diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 71f126703..92eb88301 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -69,6 +69,7 @@ import { randomString } from "../randomstring"; import { ClientStoppedError } from "../errors"; import { ISignatures } from "../@types/signed"; import { encodeBase64 } from "../common-crypto/base64"; +import { DecryptionError } from "../crypto/algorithms"; const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"]; @@ -1392,28 +1393,69 @@ class EventDecryptor { // (fixes https://github.com/vector-im/element-web/issues/5001) this.addEventToPendingList(event); - const res = (await this.olmMachine.decryptRoomEvent( - JSON.stringify({ - event_id: event.getId(), - type: event.getWireType(), - sender: event.getSender(), - state_key: event.getStateKey(), - content: event.getWireContent(), - origin_server_ts: event.getTs(), - }), - new RustSdkCryptoJs.RoomId(event.getRoomId()!), - )) as RustSdkCryptoJs.DecryptedRoomEvent; + try { + const res = (await this.olmMachine.decryptRoomEvent( + JSON.stringify({ + event_id: event.getId(), + type: event.getWireType(), + sender: event.getSender(), + state_key: event.getStateKey(), + content: event.getWireContent(), + origin_server_ts: event.getTs(), + }), + new RustSdkCryptoJs.RoomId(event.getRoomId()!), + )) as RustSdkCryptoJs.DecryptedRoomEvent; - // Success. We can remove the event from the pending list, if - // that hasn't already happened. - this.removeEventFromPendingList(event); + // Success. We can remove the event from the pending list, if + // that hasn't already happened. + this.removeEventFromPendingList(event); - return { - clearEvent: JSON.parse(res.event), - claimedEd25519Key: res.senderClaimedEd25519Key, - senderCurve25519Key: res.senderCurve25519Key, - forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain, - }; + return { + clearEvent: JSON.parse(res.event), + claimedEd25519Key: res.senderClaimedEd25519Key, + senderCurve25519Key: res.senderCurve25519Key, + forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain, + }; + } catch (err) { + // We need to map back to regular decryption errors (used for analytics for example) + // The DecryptionErrors are used by react-sdk so is implicitly part of API, but poorly typed + if (err instanceof RustSdkCryptoJs.MegolmDecryptionError) { + const content = event.getWireContent(); + let jsError; + switch (err.code) { + case RustSdkCryptoJs.DecryptionErrorCode.MissingRoomKey: { + jsError = new DecryptionError( + "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", + "The sender's device has not sent us the keys for this message.", + { + session: content.sender_key + "|" + content.session_id, + }, + ); + break; + } + case RustSdkCryptoJs.DecryptionErrorCode.UnknownMessageIndex: { + jsError = new DecryptionError( + "OLM_UNKNOWN_MESSAGE_INDEX", + "The sender's device has not sent us the keys for this message at this index.", + { + session: content.sender_key + "|" + content.session_id, + }, + ); + break; + } + // We don't map MismatchedIdentityKeys for now, as there is no equivalent in legacy. + // Just put it on the `UNABLE_TO_DECRYPT` bucket. + default: { + jsError = new DecryptionError("UNABLE_TO_DECRYPT", err.description, { + session: content.sender_key + "|" + content.session_id, + }); + break; + } + } + throw jsError; + } + throw new DecryptionError("UNABLE_TO_DECRYPT", "Unknown error"); + } } /**