From c7827d971cc8faef0f7d6aa063b60a3cc83f9f8e Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 12 Sep 2023 13:19:35 +0200 Subject: [PATCH] Refactor key backup recovery to prepare for rust (#3708) * Refactor key backup recovery to prepare for rust * code review * quick doc format * code review fix --- spec/integ/crypto/megolm-backup.spec.ts | 110 +++++++++++++++++ .../test-data/generate-test-data.py | 111 +++++++++++++++++- spec/test-utils/test-data/index.ts | 101 ++++++++++------ src/client.ts | 29 ++--- src/common-crypto/CryptoBackend.ts | 41 +++++++ src/crypto/backup.ts | 30 +++++ src/crypto/index.ts | 28 ++++- src/rust-crypto/rust-crypto.ts | 9 +- 8 files changed, 398 insertions(+), 61 deletions(-) diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index 302bc1fe2..f8ed24826 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -229,6 +229,116 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe expect(event.getContent()).toEqual("testytest"); }); + describe("recover from backup", () => { + oldBackendOnly("can restore from backup (Curve25519 version)", async function () { + fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA); + + aliceClient = await initTestClient(); + const aliceCrypto = aliceClient.getCrypto()!; + await aliceClient.startClient(); + + // tell Alice to trust the dummy device that signed the backup + await waitForDeviceList(); + await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID); + + const fullBackup = { + rooms: { + [ROOM_ID]: { + sessions: { + [testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA, + }, + }, + }, + }; + + fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup); + + const check = await aliceCrypto.checkKeyBackupAndEnable(); + + let onKeyCached: () => void; + const awaitKeyCached = new Promise((resolve) => { + onKeyCached = resolve; + }); + + const result = await aliceClient.restoreKeyBackupWithRecoveryKey( + testData.BACKUP_DECRYPTION_KEY_BASE58, + undefined, + undefined, + check!.backupInfo!, + { + cacheCompleteCallback: () => onKeyCached(), + }, + ); + + expect(result.imported).toStrictEqual(1); + + await awaitKeyCached; + }); + + oldBackendOnly("recover specific session from backup", async function () { + fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA); + + aliceClient = await initTestClient(); + const aliceCrypto = aliceClient.getCrypto()!; + await aliceClient.startClient(); + + // tell Alice to trust the dummy device that signed the backup + await waitForDeviceList(); + await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID); + + fetchMock.get( + "express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", + testData.CURVE25519_KEY_BACKUP_DATA, + ); + + const check = await aliceCrypto.checkKeyBackupAndEnable(); + + const result = await aliceClient.restoreKeyBackupWithRecoveryKey( + testData.BACKUP_DECRYPTION_KEY_BASE58, + ROOM_ID, + testData.MEGOLM_SESSION_DATA.session_id, + check!.backupInfo!, + ); + + expect(result.imported).toStrictEqual(1); + }); + + oldBackendOnly("Fails on bad recovery key", async function () { + fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA); + + aliceClient = await initTestClient(); + const aliceCrypto = aliceClient.getCrypto()!; + await aliceClient.startClient(); + + // tell Alice to trust the dummy device that signed the backup + await waitForDeviceList(); + await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID); + + const fullBackup = { + rooms: { + [ROOM_ID]: { + sessions: { + [testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA, + }, + }, + }, + }; + + fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup); + + const check = await aliceCrypto.checkKeyBackupAndEnable(); + + await expect( + aliceClient.restoreKeyBackupWithRecoveryKey( + "EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD", + undefined, + undefined, + check!.backupInfo!, + ), + ).rejects.toThrow(); + }); + }); + describe("backupLoop", () => { it("Alice should upload known keys when backup is enabled", async function () { // 404 means that there is no active backup diff --git a/spec/test-utils/test-data/generate-test-data.py b/spec/test-utils/test-data/generate-test-data.py index d0846e021..17cc086f4 100755 --- a/spec/test-utils/test-data/generate-test-data.py +++ b/spec/test-utils/test-data/generate-test-data.py @@ -26,10 +26,15 @@ python -m venv env import base64 import json +import base58 from canonicaljson import encode_canonical_json from cryptography.hazmat.primitives.asymmetric import ed25519, x25519 from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat +from cryptography.hazmat.primitives import hashes, padding, hmac +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from random import randbytes, seed ALICE_DATA = { @@ -77,6 +82,7 @@ def main() -> None: import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto"; import {{ IDownloadKeyResult }} from "../../../src"; import {{ KeyBackupInfo }} from "../../../src/crypto-api"; +import {{ IKeyBackupSession }} from "../../../src/crypto/keybackup"; /* eslint-disable comma-dangle */ @@ -186,6 +192,10 @@ 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()) + + backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"]) + return f"""\ export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}"; export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}"; @@ -220,9 +230,15 @@ export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial json.dumps(build_cross_signing_keys_data(user_data), indent=4) }; +/** Signed OTKs, returned by `POST /keys/claim` */ +export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) }; + /** base64-encoded backup decryption (private) key */ export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }"; +/** Backup decryption key in export format */ +export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }"; + /** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */ export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) }; @@ -236,8 +252,8 @@ export const {prefix}MEGOLM_SESSION_DATA: IMegolmSessionData = { json.dumps(additional_exported_room_key, indent=4) }; -/** Signed OTKs, returned by `POST /keys/claim` */ -export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, 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: IKeyBackupSession = {json.dumps(backed_up_room_key, indent=4)}; """ @@ -367,6 +383,97 @@ def build_exported_megolm_key() -> dict: return megolm_export +def encrypt_megolm_key_for_backup(session_data: dict, backup_public_key: x25519.X25519PublicKey) -> dict: + + """ + Encrypts an exported megolm key for key backup, using the m.megolm_backup.v1.curve25519-aes-sha2 algorithm. + """ + data = encode_canonical_json(session_data) + + # Generate an ephemeral curve25519 key, and perform an ECDH with the ephemeral key + # and the backup’s public key to generate a shared secret. + # The public half of the ephemeral key, encoded using unpadded base64, + # becomes the ephemeral property of the session_data. + ephemeral_keypair = x25519.X25519PrivateKey.from_private_bytes(randbytes(32)) + shared_secret = ephemeral_keypair.exchange(backup_public_key) + ephemeral = encode_base64(ephemeral_keypair.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)) + + # Using the shared secret, generate 80 bytes by performing an HKDF using SHA-256 as the hash, + # with a salt of 32 bytes of 0, and with the empty string as the info. + # The first 32 bytes are used as the AES key, the next 32 bytes are used as the MAC key, + # and the last 16 bytes are used as the AES initialization vector. + salt = bytes(32) + info = b"" + + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=80, + salt=salt, + info=info, + ) + + raw_key = hkdf.derive(shared_secret) + aes_key = raw_key[:32] + mac = raw_key[32:64] + iv = raw_key[64:80] + + # Stringify the JSON object, and encrypt it using AES-CBC-256 with PKCS#7 padding. + # This encrypted data, encoded using unpadded base64, becomes the ciphertext property of the session_data. + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv)) + encryptor = cipher.encryptor() + padder = padding.PKCS7(128).padder() + padded_data = padder.update(data) + padder.finalize() + ct = encryptor.update(padded_data) + encryptor.finalize() + cipher_text = encode_base64(ct) + + # Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above. + # The first 8 bytes of the resulting MAC are base64-encoded, and become the mac property of the session_data. + h = hmac.HMAC(mac, hashes.SHA256()) + # h.update(ct) + signature = h.finalize() + mac = encode_base64(signature[:8]) + + encrypted_key = { + "first_message_index": 1, + "forwarded_count": 0, + "is_verified": False, + "session_data": { + "ciphertext": cipher_text, + "ephemeral": ephemeral, + "mac": mac + } + + } + + return encrypted_key + +def export_recovery_key(key_b64: str) -> str: + """ + Export a private recovery key as a recovery key that can be presented to users. + As per spec https://spec.matrix.org/v1.8/client-server-api/#recovery-key + """ + private_key_bytes = base64.b64decode(key_b64) + + # The 256-bit curve25519 private key is prepended by the bytes 0x8B and 0x01 + export_bytes = bytearray() + export_bytes += b'\x8b' + export_bytes += b'\x01' + + export_bytes += private_key_bytes + + # All the bytes in the string above, including the two header bytes, + # are XORed together to form a parity byte. This parity byte is appended to the byte string. + parity_byte = 0 #b'\x8b' ^ b'\x01' + [parity_byte := parity_byte ^ x for x in export_bytes] + + export_bytes += parity_byte.to_bytes(1, 'big') + + # The byte string is encoded using base58 + recovery_key = base58.b58encode(export_bytes).decode('utf-8') + + split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)] + return ' '.join(split) + if __name__ == "__main__": main() diff --git a/spec/test-utils/test-data/index.ts b/spec/test-utils/test-data/index.ts index 640962d11..5cdce44ba 100644 --- a/spec/test-utils/test-data/index.ts +++ b/spec/test-utils/test-data/index.ts @@ -6,6 +6,7 @@ import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto"; import { IDownloadKeyResult } from "../../../src"; import { KeyBackupInfo } from "../../../src/crypto-api"; +import { IKeyBackupSession } from "../../../src/crypto/keybackup"; /* eslint-disable comma-dangle */ @@ -102,9 +103,28 @@ export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial = { } }; +/** Signed OTKs, returned by `POST /keys/claim` */ +export const ONE_TIME_KEYS = { + "@alice:localhost": { + "test_device": { + "signed_curve25519:AAAAHQ": { + "key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw", + "signatures": { + "@alice:localhost": { + "ed25519:test_device": "25djC6Rk6gIgFBMVawY9X9LnY8XMMziey6lKqL8Q5Bbp7T1vw9uk0RE7eKO2a/jNLcYroO2xRztGhBrKz5sOCQ" + } + } + } + } + } +}; + /** base64-encoded backup decryption (private) key */ export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo="; +/** Backup decryption key in export format */ +export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"; + /** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */ export const SIGNED_BACKUP_DATA: KeyBackupInfo = { "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", @@ -158,19 +178,15 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = { "forwarding_curve25519_key_chain": [] }; -/** Signed OTKs, returned by `POST /keys/claim` */ -export const ONE_TIME_KEYS = { - "@alice:localhost": { - "test_device": { - "signed_curve25519:AAAAHQ": { - "key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw", - "signatures": { - "@alice:localhost": { - "ed25519:test_device": "25djC6Rk6gIgFBMVawY9X9LnY8XMMziey6lKqL8Q5Bbp7T1vw9uk0RE7eKO2a/jNLcYroO2xRztGhBrKz5sOCQ" - } - } - } - } +/** The key from MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/ +export const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = { + "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", + "ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU", + "mac": "OibmACbORhI" } }; @@ -267,9 +283,28 @@ export const BOB_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial = { } }; +/** Signed OTKs, returned by `POST /keys/claim` */ +export const BOB_ONE_TIME_KEYS = { + "@bob:xyz": { + "bob_device": { + "signed_curve25519:AAAAHQ": { + "key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw", + "signatures": { + "@bob:xyz": { + "ed25519:bob_device": "dlZc9VA/hP980Mxvu9qwi0qJx8VK7sADGOM48CE01YM7K/Mbty9lis/QjtQAWqDg371QyynVRjEzt9qj7eSFCg" + } + } + } + } + } +}; + /** base64-encoded backup decryption (private) key */ export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo="; +/** Backup decryption key in export format */ +export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR"; + /** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */ export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = { "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", @@ -290,10 +325,10 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [ "algorithm": "m.megolm.v1.aes-sha2", "room_id": "!roomA:example.org", "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", - "session_id": "X9SK5JceUdvUr9gZkmdfS78qmKztoL60oORJ9JA5XD8", - "session_key": "AQAAAADbfOdUj/ec5bK4xVEhhRw+jfd1FD4uA0MLy7NyVHugZxyXNZUx5YEof4H9YyjBretviZreMSXqflbgYKz257rkKu7MPeKFf7zmln2GxX0F/p++GOnvpY1FqOglhfRQi3tqiyOa7SL4f7TuERDTOpMqlWhIfTKQnqy0AyF2vpDi5V/UiuSXHlHb1K/YGZJnX0u/Kpis7aC+tKDkSfSQOVw/", + "session_id": "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0", + "session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt", "sender_claimed_keys": { - "ed25519": "ZG6lrfATe+958wN1xaGf3dKG/CThEfkmNdp1jcu4zok" + "ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA" }, "forwarding_curve25519_key_chain": [] }, @@ -301,10 +336,10 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [ "algorithm": "m.megolm.v1.aes-sha2", "room_id": "!roomA:example.org", "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", - "session_id": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA", - "session_key": "AQAAAACv0khqPrQ91MmWCgm0RTzfpn65AGCrRnAKLxGJdfSfECNZ8gyj34FZLwi+F+xC6ibFddcbLXW0mzR6PnTnHF3VHM4g/h+2rcxtlix8fySpIwFzaXViba7cOSy/b+dHTMZB40iA7F4y7AdTdHLv4N1XUj3puU/KVUIKf9/lEDLqyReD+39WdEY24mTIB5NcQQhtyguPzYPT5sSyeIUNd7Bw", + "session_id": "+07YOpSgdZ1X9le3n3NMByw0V1B0H0Djnbm76jgmWoo", + "session_key": "AQAAAAAjWfIMo9+BWS8IvhfsQuomxXXXGy11tJs0ej505xxd1RzOIP4ftq3MbZYsfH8kqSMBc2l1Ym2u3Dksv2/nR0zGQeNIgOxeMuwHU3Ry7+DdV1I96blPylVCCn/f5RAy6smKoaeylptPdXgVXmw3YBBUVYpHpm+xCIUUp9foAdb8hftO2DqUoHWdV/ZXt59zTAcsNFdQdB9A4525u+o4JlqK", "sender_claimed_keys": { - "ed25519": "HxUKnGfeUu0fF3cLyCFSDXYtVCQHy/+33q9RkzKfsiU" + "ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM" }, "forwarding_curve25519_key_chain": [] } @@ -315,27 +350,23 @@ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = { "algorithm": "m.megolm.v1.aes-sha2", "room_id": "!roomA:example.org", "sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo", - "session_id": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM", - "session_key": "AQAAAACvcoGk7mOY59fOqZaxFUiTCBRV1Ia94KBjAZx6kgdgBtkkvs50z8od8/Nc9ncK2UsEiXNvCTTp2dlN3du+Rx0/m7vet2ZOEEp2oYDjHMLLFmwd1gtlGuWYPdXA6Y1+9Yyph0/EDVfS+zd3XvbL0QgbyL43+yQnFNHKlxVJX1eiKTrGTHQtYEOZz6/i/bbk+sV7GhSZFT5IMT9hXsRxdf0D", + "session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc", + "session_key": "AQAAAADZJL7OdM/KHfPzXPZ3CtlLBIlzbwk06dnZTd3bvkcdP5u73rdmThBKdqGA4xzCyxZsHdYLZRrlmD3VwOmNfvWMqYdPxA1X0vs3d172y9EIG8i+N/skJxTRypcVSV9XoinBNIWr/gkyepuAKiQqemlc8J5amD9OkmbVkmnrxP1uyYMsMnQayCXCVpLQv4nN7bpymTNV2ZtTKWImLXW4/EaX", "sender_claimed_keys": { - "ed25519": "dV0TIhhkToXpL+gZLo+zXDHJfw7MWYxpg80cynIQDv0" + "ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64" }, "forwarding_curve25519_key_chain": [] }; -/** Signed OTKs, returned by `POST /keys/claim` */ -export const BOB_ONE_TIME_KEYS = { - "@bob:xyz": { - "bob_device": { - "signed_curve25519:AAAAHQ": { - "key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw", - "signatures": { - "@bob:xyz": { - "ed25519:bob_device": "dlZc9VA/hP980Mxvu9qwi0qJx8VK7sADGOM48CE01YM7K/Mbty9lis/QjtQAWqDg371QyynVRjEzt9qj7eSFCg" - } - } - } - } +/** 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: IKeyBackupSession = { + "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", + "ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU", + "mac": "lEfHlqfJQwU" } }; diff --git a/src/client.ts b/src/client.ts index 2ef39c8b1..cb6ce2dda 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3471,9 +3471,6 @@ export class MatrixClient extends TypedEventEmitter { - return privKey; - }); + const backupDecryptor = await this.cryptoBackend.getBackupDecryptor(backupInfo, privKey); - const untrusted = algorithm.untrusted; + const untrusted = !backupDecryptor.sourceTrusted; try { - // If the pubkey computed from the private data we've been given - // doesn't match the one in the auth_data, the user has entered - // a different recovery key / the wrong passphrase. - if (!(await algorithm.keyMatches(privKey))) { - return Promise.reject(new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY })); - } - if (!(privKey instanceof Uint8Array)) { // eslint-disable-next-line @typescript-eslint/no-base-to-string throw new Error(`restoreKeyBackup expects Uint8Array, got ${privKey}`); @@ -3842,7 +3830,7 @@ export class MatrixClient extends TypedEventEmitter; + + /** + * Get a backup decryptor capable of decrypting megolm session data encrypted with the given backup information. + * @param backupInfo - The backup information + * @param privKey - The private decryption key. + */ + getBackupDecryptor(backupInfo: IKeyBackupInfo, privKey: ArrayLike): Promise; } /** The methods which crypto implementations should expose to the Sync api @@ -213,3 +222,35 @@ export interface EventDecryptionResult { */ encryptedDisabledForUnverifiedDevices?: boolean; } + +/** + * Responsible for decrypting megolm session data retrieved from a remote backup. + * The result of {@link CryptoBackend#getBackupDecryptor}. + */ +export interface BackupDecryptor { + /** + * Whether keys retrieved from this backup can be trusted. + * + * Depending on the backup algorithm, keys retrieved from the backup can be trusted or not. + * If false, keys retrieved from the backup must be considered unsafe (authenticity cannot be guaranteed). + * It could be by design (deniability) or for some technical reason (eg asymmetric encryption). + */ + readonly sourceTrusted: boolean; + + /** + * + * Decrypt megolm session data retrieved from backup. + * + * @param ciphertexts - a Record of sessionId to session data. + * + * @returns An array of decrypted `IMegolmSessionData` + */ + decryptSessions(ciphertexts: Record): Promise; + + /** + * Free any resources held by this decryptor. + * + * Should be called once the decryptor is no longer needed. + */ + free(): void; +} diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 5ebf85f4f..10c6cc2d8 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -41,6 +41,7 @@ import { CryptoEvent } from "./index"; import { crypto } from "./crypto"; import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api"; import { BackupTrustInfo } from "../crypto-api/keybackup"; +import { BackupDecryptor } from "../common-crypto/CryptoBackend"; const KEY_BACKUP_KEYS_PER_REQUEST = 200; const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms @@ -878,3 +879,32 @@ export function backupTrustInfoFromLegacyTrustInfo(trustInfo: TrustInfo): Backup matchesDecryptionKey: trustInfo.trusted_locally ?? false, }; } + +/** + * Implementation of {@link BackupDecryptor} for the libolm crypto backend. + */ +export class LibOlmBackupDecryptor implements BackupDecryptor { + private algorithm: BackupAlgorithm; + public readonly sourceTrusted: boolean; + + public constructor(algorithm: BackupAlgorithm) { + this.algorithm = algorithm; + this.sourceTrusted = !algorithm.untrusted; + } + + /** + * Implements {@link BackupDecryptor#free} + */ + public free(): void { + this.algorithm.free(); + } + + /** + * Implements {@link BackupDecryptor#decryptSessions} + */ + public async decryptSessions( + sessions: Record>, + ): Promise { + return await this.algorithm.decryptSessions(sessions); + } +} diff --git a/src/crypto/index.ts b/src/crypto/index.ts index b435912fd..24c65e5b0 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -50,7 +50,7 @@ import { IllegalMethod } from "./verification/IllegalMethod"; import { KeySignatureUploadError } from "../errors"; import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes"; import { DehydrationManager } from "./dehydration"; -import { BackupManager, backupTrustInfoFromLegacyTrustInfo } from "./backup"; +import { BackupManager, LibOlmBackupDecryptor, backupTrustInfoFromLegacyTrustInfo } from "./backup"; import { IStore } from "../store"; import { Room, RoomEvent } from "../models/room"; import { RoomMember, RoomMemberEvent } from "../models/room-member"; @@ -73,7 +73,7 @@ import { TypedEventEmitter } from "../models/typed-event-emitter"; import { IDeviceLists, ISyncResponse, IToDeviceEvent } from "../sync-accumulator"; import { ISignatures } from "../@types/signed"; import { IMessage } from "./algorithms/olm"; -import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; +import { BackupDecryptor, CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; import { RoomState, RoomStateEvent } from "../models/room-state"; import { MapWithDefault, recursiveMapToObject } from "../utils"; import { @@ -101,7 +101,7 @@ import { } from "../crypto-api"; import { Device, DeviceMap } from "../models/device"; import { deviceInfoToDevice } from "./device-converter"; -import { ClientPrefix, Method } from "../http-api"; +import { ClientPrefix, MatrixError, Method } from "../http-api"; /* re-exports for backwards compatibility */ export type { @@ -1845,6 +1845,28 @@ export class Crypto extends TypedEventEmitter): Promise { + if (!(privKey instanceof Uint8Array)) { + throw new Error(`getBackupDecryptor expects Uint8Array`); + } + + const algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => { + return privKey; + }); + + // If the pubkey computed from the private data we've been given + // doesn't match the one in the auth_data, the user has entered + // a different recovery key / the wrong passphrase. + if (!(await algorithm.keyMatches(privKey))) { + return Promise.reject(new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY })); + } + + return new LibOlmBackupDecryptor(algorithm); + } + /** * Store a set of keys as our own, trusted, cross-signing keys. * diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 94def22a9..4ea271a6c 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -23,7 +23,7 @@ import type { IEncryptedEventInfo } from "../crypto/api"; import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event"; import { Room } from "../models/room"; import { RoomMember } from "../models/room-member"; -import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; +import { BackupDecryptor, CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; import { logger } from "../logger"; import { IHttpOpts, MatrixHttpApi, Method } from "../http-api"; import { RoomEncryptor } from "./RoomEncryptor"; @@ -1023,6 +1023,13 @@ export class RustCrypto extends TypedEventEmitter): Promise { + throw new Error("Stub not yet implemented"); + } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // SyncCryptoCallbacks implementation