From 1503acb30a6e2eeda82018d8a065b65cb6f5fac5 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 13 Sep 2023 11:08:26 +0200 Subject: [PATCH] rust backup restore support (#3709) * Refactor key backup recovery to prepare for rust * rust backup restore support * Move export out of old crypto to api with re-export * extract base64 utility * add tests for base64 util * more efficient regex * fix typo --- spec/integ/crypto/megolm-backup.spec.ts | 11 +- .../test-data/generate-test-data.py | 109 +++++++++++++++++- spec/test-utils/test-data/index.ts | 7 +- spec/unit/crypto/base64.spec.ts | 52 +++++++++ src/common-crypto/CryptoBackend.ts | 6 +- src/common-crypto/base64.ts | 46 ++++++++ src/crypto-api/keybackup.ts | 19 +++ src/crypto/index.ts | 2 +- src/crypto/keybackup.ts | 23 +--- src/crypto/recoverykey.ts | 7 +- src/rust-crypto/backup.ts | 58 +++++++++- src/rust-crypto/rust-crypto.ts | 24 +++- 12 files changed, 318 insertions(+), 46 deletions(-) create mode 100644 spec/unit/crypto/base64.spec.ts create mode 100644 src/common-crypto/base64.ts diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index f8ed24826..ba5a23745 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -18,7 +18,6 @@ import fetchMock from "fetch-mock-jest"; import "fake-indexeddb/auto"; import { IDBFactory } from "fake-indexeddb"; -import { IKeyBackupSession } from "../../../src/crypto/keybackup"; import { createClient, CryptoEvent, ICreateClientOpts, IEvent, MatrixClient, TypedEventEmitter } from "../../../src"; import { SyncResponder } from "../../test-utils/SyncResponder"; import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; @@ -26,7 +25,7 @@ import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; import { mockInitialApiRequests } from "../../test-utils/mockEndpoints"; import { awaitDecryption, CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils"; import * as testData from "../../test-utils/test-data"; -import { KeyBackupInfo } from "../../../src/crypto-api/keybackup"; +import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup"; import { IKeyBackup } from "../../../src/crypto/backup"; const ROOM_ID = "!ROOM:ID"; @@ -52,7 +51,7 @@ const ENCRYPTED_EVENT: Partial = { origin_server_ts: 1507753886000, }; -const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = { +const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = { first_message_index: 0, forwarded_count: 0, is_verified: false, @@ -230,7 +229,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe }); describe("recover from backup", () => { - oldBackendOnly("can restore from backup (Curve25519 version)", async function () { + it("can restore from backup (Curve25519 version)", async function () { fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA); aliceClient = await initTestClient(); @@ -275,7 +274,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe await awaitKeyCached; }); - oldBackendOnly("recover specific session from backup", async function () { + it("recover specific session from backup", async function () { fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA); aliceClient = await initTestClient(); @@ -303,7 +302,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe expect(result.imported).toStrictEqual(1); }); - oldBackendOnly("Fails on bad recovery key", async function () { + it("Fails on bad recovery key", async function () { fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA); aliceClient = await initTestClient(); diff --git a/spec/test-utils/test-data/generate-test-data.py b/spec/test-utils/test-data/generate-test-data.py index 17cc086f4..f56a49149 100755 --- a/spec/test-utils/test-data/generate-test-data.py +++ b/spec/test-utils/test-data/generate-test-data.py @@ -81,8 +81,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"; +import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup"; /* eslint-disable comma-dangle */ @@ -192,6 +191,7 @@ 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"]) @@ -253,7 +253,7 @@ export const {prefix}MEGOLM_SESSION_DATA: IMegolmSessionData = { }; /** 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)}; +export const {prefix}CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {json.dumps(backed_up_room_key, indent=4)}; """ @@ -383,6 +383,7 @@ def build_exported_megolm_key() -> dict: return megolm_export + def encrypt_megolm_key_for_backup(session_data: dict, backup_public_key: x25519.X25519PublicKey) -> dict: """ @@ -447,6 +448,108 @@ def encrypt_megolm_key_for_backup(session_data: dict, backup_public_key: x25519. return encrypted_key +def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519PrivateKey, curve_key: x25519.X25519PrivateKey) -> tuple[dict, dict]: + """ + Encrypts an event using the given key in session export format. + Will not do any ratcheting, just encrypt at index 0. + """ + + clear_event = { + "type": "m.room.message", + "room_id": "!room:id", + "sender": "@alice:localhost", + "content": { + "msgtype": "m.text", + "body": "Hello world" + } + } + + session_key: str = exported_key["session_key"] + + # Get the megolm R0 from the export format + decoded = base64.b64decode(session_key.encode("ascii")) + r0 = decoded[5:133] + + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=80, + salt=bytes(32), + info=b"MEGOLM_KEYS", + ) + + raw_key = hkdf.derive(r0) + aes_key = raw_key[:32] + mac = raw_key[32:64] + aes_iv = raw_key[64:80] + + payload_json = { + "room_id": clear_event["room_id"], + "type": clear_event["type"], + "content": clear_event["content"] + } + + payload_string = encode_canonical_json(payload_json) + + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(aes_iv)) + encryptor = cipher.encryptor() + padder = padding.PKCS7(128).padder() + + padded_data = padder.update(payload_string) + padded_data += padder.finalize() + + ct = encryptor.update(padded_data) + encryptor.finalize() + + # 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. + message = bytearray() + message += b'\x03' + # int tag for index + message += b'\x08' + # index is 0 + message += b'\x00' + message += b'\x12' + # probably works only for short messages + message += len(ct).to_bytes(1, 'big') + # encrypted data + message += ct + + h = hmac.HMAC(mac, hashes.SHA256()) + h.update(message) + signature = h.finalize() + mac = signature[:8] + + message += mac + + # Finally, the authenticated message is signed using the Ed25519 keypair; + # the 64 byte signature is appended to the message + signature = ed_key.sign(bytes(message)) + + message += signature + + cipher_text = encode_base64(message) + + encrypted_payload = { + "algorithm" : "m.megolm.v1.aes-sha2", + "sender_key" : encode_base64(curve_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)), + "ciphertext" : cipher_text, + "session_id" : exported_key["session_id"], + "device_id" : "TEST_DEVICE" + } + + encrypted_event = { + "type": "m.room.encrypted", + "room_id": "!room:id", + "sender": "@alice:localhost", + "content": encrypted_payload, + "event_id": "$event1", + "origin_server_ts": 1507753886000, + } + + return clear_event, encrypted_event + + def export_recovery_key(key_b64: str) -> str: """ Export a private recovery key as a recovery key that can be presented to users. diff --git a/spec/test-utils/test-data/index.ts b/spec/test-utils/test-data/index.ts index 5cdce44ba..8cc3e4903 100644 --- a/spec/test-utils/test-data/index.ts +++ b/spec/test-utils/test-data/index.ts @@ -5,8 +5,7 @@ import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto"; import { IDownloadKeyResult } from "../../../src"; -import { KeyBackupInfo } from "../../../src/crypto-api"; -import { IKeyBackupSession } from "../../../src/crypto/keybackup"; +import { KeyBackupSession, KeyBackupInfo } from "../../../src/crypto-api/keybackup"; /* eslint-disable comma-dangle */ @@ -179,7 +178,7 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = { }; /** 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 = { +export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = { "first_message_index": 1, "forwarded_count": 0, "is_verified": false, @@ -359,7 +358,7 @@ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = { }; /** 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 = { +export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = { "first_message_index": 1, "forwarded_count": 0, "is_verified": false, diff --git a/spec/unit/crypto/base64.spec.ts b/spec/unit/crypto/base64.spec.ts new file mode 100644 index 000000000..0fac4b454 --- /dev/null +++ b/spec/unit/crypto/base64.spec.ts @@ -0,0 +1,52 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { TextEncoder, TextDecoder } from "util"; + +import { decodeBase64, encodeBase64, encodeUnpaddedBase64 } from "../../../src/common-crypto/base64"; + +describe("Crypto Base64 encoding", () => { + it("Should decode properly encoded data", async () => { + const toEncode = "encoding hello world"; + const encoded = encodeBase64(new TextEncoder().encode(toEncode)); + const decoded = new TextDecoder().decode(decodeBase64(encoded)); + + expect(decoded).toStrictEqual(toEncode); + }); + + it("Encode unpadded should not have padding", async () => { + const toEncode = "encoding hello world"; + const data = new TextEncoder().encode(toEncode); + + const paddedEncoded = encodeBase64(data); + const unpaddedEncoded = encodeUnpaddedBase64(data); + + expect(paddedEncoded).not.toEqual(unpaddedEncoded); + + const padding = paddedEncoded.charAt(paddedEncoded.length - 1); + expect(padding).toStrictEqual("="); + }); + + it("Decode should be indifferent to padding", async () => { + const withPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ="; + const withoutPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ"; + + const decodedPad = decodeBase64(withPadding); + const decodedNoPad = decodeBase64(withoutPadding); + + expect(decodedPad).toStrictEqual(decodedNoPad); + }); +}); diff --git a/src/common-crypto/CryptoBackend.ts b/src/common-crypto/CryptoBackend.ts index e4894c7d6..6ec6c94dd 100644 --- a/src/common-crypto/CryptoBackend.ts +++ b/src/common-crypto/CryptoBackend.ts @@ -20,7 +20,7 @@ import { Room } from "../models/room"; import { CryptoApi } from "../crypto-api"; import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning"; import { IEncryptedEventInfo } from "../crypto/api"; -import { IKeyBackupInfo, IKeyBackupSession } from "../crypto/keybackup"; +import { KeyBackupInfo, KeyBackupSession } from "../crypto-api/keybackup"; import { IMegolmSessionData } from "../@types/crypto"; /** @@ -107,7 +107,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi { * @param backupInfo - The backup information * @param privKey - The private decryption key. */ - getBackupDecryptor(backupInfo: IKeyBackupInfo, privKey: ArrayLike): Promise; + getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike): Promise; } /** The methods which crypto implementations should expose to the Sync api @@ -245,7 +245,7 @@ export interface BackupDecryptor { * * @returns An array of decrypted `IMegolmSessionData` */ - decryptSessions(ciphertexts: Record): Promise; + decryptSessions(ciphertexts: Record): Promise; /** * Free any resources held by this decryptor. diff --git a/src/common-crypto/base64.ts b/src/common-crypto/base64.ts new file mode 100644 index 000000000..ca0476d9e --- /dev/null +++ b/src/common-crypto/base64.ts @@ -0,0 +1,46 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Base64 encoding and decoding utility for crypo. + */ + +/** + * Encode a typed array of uint8 as base64. + * @param uint8Array - The data to encode. + * @returns The base64. + */ +export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string { + return Buffer.from(uint8Array).toString("base64"); +} + +/** + * Encode a typed array of uint8 as unpadded base64. + * @param uint8Array - The data to encode. + * @returns The unpadded base64. + */ +export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string { + return encodeBase64(uint8Array).replace(/={1,2}$/, ""); +} + +/** + * Decode a base64 string to a typed array of uint8. + * @param base64 - The base64 to decode. + * @returns The decoded data. + */ +export function decodeBase64(base64: string): Uint8Array { + return Buffer.from(base64, "base64"); +} diff --git a/src/crypto-api/keybackup.ts b/src/crypto-api/keybackup.ts index 9187d8964..edb4908ec 100644 --- a/src/crypto-api/keybackup.ts +++ b/src/crypto-api/keybackup.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { ISigned } from "../@types/signed"; +import { IEncryptedPayload } from "../crypto/aes"; export interface Curve25519AuthData { public_key: string; @@ -68,3 +69,21 @@ export interface KeyBackupCheck { backupInfo: KeyBackupInfo; trustInfo: BackupTrustInfo; } + +export interface Curve25519SessionData { + ciphertext: string; + ephemeral: string; + mac: string; +} + +/* eslint-disable camelcase */ +export interface KeyBackupSession { + first_message_index: number; + forwarded_count: number; + is_verified: boolean; + session_data: T; +} + +export interface KeyBackupRoomSessions { + [sessionId: string]: KeyBackupSession; +} diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 24c65e5b0..dddc458c9 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -1848,7 +1848,7 @@ export class Crypto extends TypedEventEmitter): Promise { + public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike): Promise { if (!(privKey instanceof Uint8Array)) { throw new Error(`getBackupDecryptor expects Uint8Array`); } diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts index f15ab7343..24c73b85c 100644 --- a/src/crypto/keybackup.ts +++ b/src/crypto/keybackup.ts @@ -14,31 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IEncryptedPayload } from "./aes"; - -export interface Curve25519SessionData { - ciphertext: string; - ephemeral: string; - mac: string; -} - -/* eslint-disable camelcase */ -export interface IKeyBackupSession { - first_message_index: number; - forwarded_count: number; - is_verified: boolean; - session_data: T; -} - -export interface IKeyBackupRoomSessions { - [sessionId: string]: IKeyBackupSession; -} - // Export for backward compatibility export type { Curve25519AuthData as ICurve25519AuthData, Aes256AuthData as IAes256AuthData, KeyBackupInfo as IKeyBackupInfo, + Curve25519SessionData, + KeyBackupSession as IKeyBackupSession, + KeyBackupRoomSessions as IKeyBackupRoomSessions, } from "../crypto-api/keybackup"; /* eslint-enable camelcase */ diff --git a/src/crypto/recoverykey.ts b/src/crypto/recoverykey.ts index 4107b76f5..8c6a7e33e 100644 --- a/src/crypto/recoverykey.ts +++ b/src/crypto/recoverykey.ts @@ -19,6 +19,7 @@ import * as bs58 from "bs58"; // picked arbitrarily but to try & avoid clashing with any bitcoin ones // (which are also base58 encoded, but bitcoin's involve a lot more hashing) const OLM_RECOVERY_KEY_PREFIX = [0x8b, 0x01]; +const KEY_SIZE = 32; export function encodeRecoveryKey(key: ArrayLike): string | undefined { const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1); @@ -52,11 +53,9 @@ export function decodeRecoveryKey(recoveryKey: string): Uint8Array { } } - if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1) { + if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE + 1) { throw new Error("Incorrect length"); } - return Uint8Array.from( - result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH), - ); + return Uint8Array.from(result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE)); } diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index ac4183805..f115fee02 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -17,14 +17,23 @@ limitations under the License. import { OlmMachine, SignatureVerification } from "@matrix-org/matrix-sdk-crypto-wasm"; import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm"; -import { BackupTrustInfo, Curve25519AuthData, KeyBackupCheck, KeyBackupInfo } from "../crypto-api/keybackup"; +import { + BackupTrustInfo, + Curve25519AuthData, + KeyBackupCheck, + KeyBackupInfo, + KeyBackupSession, + Curve25519SessionData, +} from "../crypto-api/keybackup"; import { logger } from "../logger"; import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api"; -import { CryptoEvent } from "../crypto"; +import { CryptoEvent, IMegolmSessionData } from "../crypto"; import { TypedEventEmitter } from "../models/typed-event-emitter"; import { encodeUri } from "../utils"; import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; import { sleep } from "../utils"; +import { BackupDecryptor } from "../common-crypto/CryptoBackend"; +import { IEncryptedPayload } from "../crypto/aes"; /** Authentification of the backup info, depends on algorithm */ type AuthData = KeyBackupInfo["auth_data"]; @@ -370,6 +379,51 @@ export class RustBackupManager extends TypedEventEmitter>, + ): Promise { + const keys: IMegolmSessionData[] = []; + for (const [sessionId, sessionData] of Object.entries(ciphertexts)) { + try { + const decrypted = JSON.parse( + await this.decryptionKey.decryptV1( + sessionData.session_data.ephemeral, + sessionData.session_data.mac, + sessionData.session_data.ciphertext, + ), + ); + decrypted.session_id = sessionId; + keys.push(decrypted); + } catch (e) { + logger.log("Failed to decrypt megolm session from backup", e, sessionData); + } + } + return keys; + } + + /** + * Implements {@link BackupDecryptor#free} + */ + public free(): void { + this.decryptionKey.free(); + } +} + export type RustBackupCryptoEvents = | CryptoEvent.KeyBackupStatus | CryptoEvent.KeyBackupSessionsRemaining diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 4ea271a6c..71f126703 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -38,6 +38,7 @@ import { CrossSigningKeyInfo, CrossSigningStatus, CryptoCallbacks, + Curve25519AuthData, DeviceVerificationStatus, EventEncryptionInfo, EventShieldColour, @@ -62,11 +63,12 @@ import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentif import { EventType } from "../@types/event"; import { CryptoEvent } from "../crypto"; import { TypedEventEmitter } from "../models/typed-event-emitter"; -import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } from "./backup"; +import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupDecryptor, RustBackupManager } from "./backup"; import { TypedReEmitter } from "../ReEmitter"; import { randomString } from "../randomstring"; import { ClientStoppedError } from "../errors"; import { ISignatures } from "../@types/signed"; +import { encodeBase64 } from "../common-crypto/base64"; const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"]; @@ -941,7 +943,7 @@ export class RustCrypto extends TypedEventEmitter { - const base64Key = Buffer.from(key).toString("base64"); + const base64Key = encodeBase64(key); // TODO get version from backupManager await this.olmMachine.saveBackupDecryptionKey(RustSdkCryptoJs.BackupDecryptionKey.fromBase64(base64Key), ""); @@ -1027,7 +1029,23 @@ export class RustCrypto extends TypedEventEmitter): Promise { - throw new Error("Stub not yet implemented"); + if (backupInfo.algorithm != "m.megolm_backup.v1.curve25519-aes-sha2") { + throw new Error(`getBackupDecryptor Unsupported algorithm ${backupInfo.algorithm}`); + } + + const authData = backupInfo.auth_data; + + if (!(privKey instanceof Uint8Array)) { + throw new Error(`getBackupDecryptor expects Uint8Array`); + } + + const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(privKey)); + + if (authData.public_key != backupDecryptionKey.megolmV1PublicKey.publicKeyBase64) { + throw new Error(`getBackupDecryptor key mismatch error`); + } + + return new RustBackupDecryptor(backupDecryptionKey); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////