From cb06c51f0c042afe4d1199ffe2583f693ba5de2b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 19 Jul 2023 12:14:54 +0100 Subject: [PATCH] `matrix-sdk-crypto-wasm` bindings for KeyBackup Import Valere's changes from https://github.com/matrix-org/matrix-rust-sdk/pull/2196, excluding the fixes to MemoryStore. --- Cargo.toml | 8 +- src/backup_recovery_key.rs | 176 ++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/machine.rs | 155 +++++++++++++++++++++++++- src/types.rs | 117 +++++++++++++++++++- tests/backup_recovery_key.test.js | 69 ++++++++++++ tests/machine.test.js | 115 +++++++++++++++++++ 7 files changed, 638 insertions(+), 3 deletions(-) create mode 100644 src/backup_recovery_key.rs create mode 100644 tests/backup_recovery_key.test.js diff --git a/Cargo.toml b/Cargo.toml index 8c9583a..7fa3d31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,13 +38,19 @@ tracing = [] anyhow = { workspace = true } console_error_panic_hook = "0.1.7" futures-util = "0.3.27" +hmac = "0.12.1" http = { workspace = true } +pbkdf2 = "0.11.0" +rand = "0.8.5" js-sys = "0.3.49" matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] } matrix-sdk-indexeddb = { version = "0.2.0", path = "../../crates/matrix-sdk-indexeddb" } matrix-sdk-qrcode = { version = "0.4.0", path = "../../crates/matrix-sdk-qrcode", optional = true } ruma = { workspace = true, features = ["js", "rand"] } +serde = { workspace = true } serde_json = { workspace = true } +serde-wasm-bindgen = "0.5.0" +sha2 = "0.10.2" tracing = { workspace = true } tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] } vodozemac = { workspace = true, features = ["js"] } @@ -56,4 +62,4 @@ zeroize = { workspace = true } path = "../../crates/matrix-sdk-crypto" version = "0.6.0" default_features = false -features = ["js", "automatic-room-key-forwarding"] +features = ["js", "backups_v1", "automatic-room-key-forwarding"] diff --git a/src/backup_recovery_key.rs b/src/backup_recovery_key.rs new file mode 100644 index 0000000..5afa21a --- /dev/null +++ b/src/backup_recovery_key.rs @@ -0,0 +1,176 @@ +//! Megolm backup types + +use std::{collections::HashMap, iter, ops::DerefMut}; + +use hmac::Hmac; +use js_sys::{JsString, JSON}; +use matrix_sdk_crypto::{backups::MegolmV1BackupKey as InnerMegolmV1BackupKey, store::RecoveryKey}; +use pbkdf2::pbkdf2; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use serde_wasm_bindgen; +use sha2::Sha512; +use wasm_bindgen::prelude::*; +use zeroize::Zeroize; + +/// The private part of the backup key, the one used for recovery. +#[derive(Debug)] +#[wasm_bindgen] +pub struct BackupRecoveryKey { + pub(crate) inner: RecoveryKey, + pub(crate) passphrase_info: Option, +} + +/// Struct containing info about the way the backup key got derived from a +/// passphrase. +#[derive(Debug, Clone)] +#[wasm_bindgen] +pub struct PassphraseInfo { + /// The salt that was used during key derivation. + #[wasm_bindgen(getter_with_clone)] + pub private_key_salt: JsString, + /// The number of PBKDF rounds that were used for key derivation. + pub private_key_iterations: i32, +} + +/// The public part of the backup key. +#[derive(Debug, Clone)] +#[wasm_bindgen] +pub struct MegolmV1BackupKey { + inner: InnerMegolmV1BackupKey, + passphrase_info: Option, +} + +#[wasm_bindgen] +impl MegolmV1BackupKey { + /// The actual base64 encoded public key. + #[wasm_bindgen(getter, js_name = "publicKeyBase64")] + pub fn public_key(&self) -> JsString { + self.inner.to_base64().into() + } + + /// The passphrase info, if the key was derived from one. + #[wasm_bindgen(getter, js_name = "passphraseInfo")] + pub fn passphrase_info(&self) -> Option { + self.passphrase_info.clone() + } + + /// Get the full name of the backup algorithm this backup key supports. + #[wasm_bindgen(getter, js_name = "algorithm")] + pub fn backup_algorithm(&self) -> JsString { + self.inner.backup_algorithm().into() + } + + /// Signatures that have signed our backup key. + /// map of userId to map of deviceOrKeyId to signature + #[wasm_bindgen(getter, js_name = "signatures")] + pub fn signatures(&self) -> JsValue { + let signatures: HashMap> = self + .inner + .signatures() + .into_iter() + .map(|(k, v)| (k.to_string(), v.into_iter().map(|(k, v)| (k.to_string(), v)).collect())) + .collect(); + + serde_wasm_bindgen::to_value(&signatures).unwrap() + } +} + +impl BackupRecoveryKey { + const KEY_SIZE: usize = 32; + const SALT_SIZE: usize = 32; + const PBKDF_ROUNDS: i32 = 500_000; +} + +#[wasm_bindgen] +impl BackupRecoveryKey { + /// Create a new random [`BackupRecoveryKey`]. + #[wasm_bindgen(js_name = "createRandomKey")] + pub fn create_random_key() -> BackupRecoveryKey { + BackupRecoveryKey { + inner: RecoveryKey::new() + .expect("Can't gather enough randomness to create a recovery key"), + passphrase_info: None, + } + } + + /// Try to create a [`BackupRecoveryKey`] from a base 64 encoded string. + #[wasm_bindgen(js_name = "fromBase64")] + pub fn from_base64(key: String) -> Result { + Ok(Self { inner: RecoveryKey::from_base64(&key)?, passphrase_info: None }) + } + + /// Try to create a [`BackupRecoveryKey`] from a base 58 encoded string. + #[wasm_bindgen(js_name = "fromBase58")] + pub fn from_base58(key: String) -> Result { + Ok(Self { inner: RecoveryKey::from_base58(&key)?, passphrase_info: None }) + } + + /// Create a new [`BackupRecoveryKey`] from the given passphrase. + #[wasm_bindgen(js_name = "newFromPassphrase")] + pub fn new_from_passphrase(passphrase: String) -> BackupRecoveryKey { + let mut rng = thread_rng(); + let salt: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .take(Self::SALT_SIZE) + .collect(); + + BackupRecoveryKey::from_passphrase(passphrase, salt, Self::PBKDF_ROUNDS) + } + + /// Restore a [`BackupRecoveryKey`] from the given passphrase. + #[wasm_bindgen(js_name = "fromPassphrase")] + pub fn from_passphrase(passphrase: String, salt: String, rounds: i32) -> Self { + let mut key = Box::new([0u8; Self::KEY_SIZE]); + let rounds = rounds as u32; + + pbkdf2::>(passphrase.as_bytes(), salt.as_bytes(), rounds, key.deref_mut()); + + let recovery_key = RecoveryKey::from_bytes(&key); + + key.zeroize(); + + Self { + inner: recovery_key, + passphrase_info: Some(PassphraseInfo { + private_key_salt: salt.into(), + private_key_iterations: rounds as i32, + }), + } + } + + /// Convert the recovery key to a base 58 encoded string. + #[wasm_bindgen(js_name = "toBase58")] + pub fn to_base58(&self) -> JsString { + self.inner.to_base58().into() + } + + /// Convert the recovery key to a base 64 encoded string. + #[wasm_bindgen(js_name = "toBase64")] + pub fn to_base64(&self) -> JsString { + self.inner.to_base64().into() + } + + /// Get the public part of the backup key. + #[wasm_bindgen(getter, js_name = "megolmV1PublicKey")] + pub fn megolm_v1_public_key(&self) -> MegolmV1BackupKey { + let public_key = self.inner.megolm_v1_public_key(); + + MegolmV1BackupKey { inner: public_key, passphrase_info: self.passphrase_info.clone() } + } + + /// Try to decrypt a message that was encrypted using the public part of the + /// backup key. + #[wasm_bindgen(js_name = "decryptV1")] + pub fn decrypt_v1( + &self, + ephemeral_key: String, + mac: String, + ciphertext: String, + ) -> Result { + self.inner + .decrypt_v1(&ephemeral_key, &mac, &ciphertext) + .map_err(|e| e.into()) + .map(|r| JSON::parse(&r).unwrap()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 4038eca..39ef940 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ #![allow(clippy::drop_non_drop)] pub mod attachment; +pub mod backup_recovery_key; pub mod device; pub mod encryption; pub mod events; diff --git a/src/machine.rs b/src/machine.rs index 959ed39..1e92cf1 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -4,8 +4,10 @@ use std::{collections::BTreeMap, ops::Deref, time::Duration}; use futures_util::StreamExt; use js_sys::{Array, Function, Map, Promise, Set}; +use matrix_sdk_crypto::{backups::MegolmV1BackupKey, store::RecoveryKey, types::RoomKeyBackupInfo}; use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt}; use serde_json::{json, Value as JsonValue}; +use serde_wasm_bindgen; use tracing::warn; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::{spawn_local, JsFuture}; @@ -20,7 +22,9 @@ use crate::{ responses::{self, response_from_string}, store, store::RoomKeyInfo, - sync_events, types, verification, vodozemac, + sync_events, + types::{self, BackupKeys, RoomKeyCounts, SignatureVerification}, + verification, vodozemac, }; /// State machine implementation of the Olm/Megolm encryption protocol @@ -763,6 +767,155 @@ impl OlmMachine { })) } + /// Store the recovery key in the crypto store. + /// + /// This is useful if the client wants to support gossiping of the backup + /// key. + #[wasm_bindgen(js_name = "saveBackupRecoveryKey")] + pub fn save_recovery_key( + &self, + recovery_key_base_58: String, + version: String, + ) -> Result { + let key = RecoveryKey::from_base58(&recovery_key_base_58)?; + + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + me.backup_machine().save_recovery_key(Some(key), Some(version)).await?; + Ok(JsValue::NULL) + })) + } + + /// Get the backup keys we have saved in our crypto store. + /// Returns a json object {recoveryKeyBase58: "", backupVersion: ""} + #[wasm_bindgen(js_name = "getBackupKeys")] + pub fn get_backup_keys(&self) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { + let inner = me.backup_machine().get_backup_keys().await?; + let backup_keys = BackupKeys { + recovery_key: inner.recovery_key.map(|k| k.to_base58()), + backup_version: inner.backup_version, + }; + Ok(serde_wasm_bindgen::to_value(&backup_keys).unwrap()) + }) + } + + /// Check if the given backup has been verified by us or by another of our + /// devices that we trust. + /// + /// The `backup_info` should be a JSON object with the following + /// format: + /// + /// ```json + /// { + /// "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", + /// "auth_data": { + /// "public_key":"XjhWTCjW7l59pbfx9tlCBQolfnIQWARoKOzjTOPSlWM", + /// "signatures": {} + /// } + /// } + /// ``` + /// Returns a SignatureVerification object + #[wasm_bindgen(js_name = "verifyBackup")] + pub fn verify_backup(&self, backup_info: JsValue) -> Result { + let backup_info: RoomKeyBackupInfo = serde_wasm_bindgen::from_value(backup_info)?; + + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + let result = me.backup_machine().verify_backup(backup_info, false).await?; + Ok(SignatureVerification { inner: result }) + })) + } + + /// Activate the given backup key to be used with the given backup version. + /// + /// **Warning**: The caller needs to make sure that the given `BackupKey` is + /// trusted, otherwise we might be encrypting room keys that a malicious + /// party could decrypt. + /// + /// The [`OlmMachine::verify_backup`] method can be used to so. + #[wasm_bindgen(js_name = "enableBackupV1")] + pub fn enable_backup_v1( + &self, + public_key_base_64: String, + version: String, + ) -> Result { + let backup_key = MegolmV1BackupKey::from_base64(&public_key_base_64)?; + backup_key.set_version(version); + + let me = self.inner.clone(); + + Ok(future_to_promise(async move { + me.backup_machine().enable_backup_v1(backup_key).await?; + Ok(JsValue::NULL) + })) + } + + /// Are we able to encrypt room keys. + /// + /// This returns true if we have an active `BackupKey` and backup version + /// registered with the state machine. + #[wasm_bindgen(js_name = "isBackupEnabled")] + pub fn backup_enabled(&self) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { + let enabled = me.backup_machine().enabled().await; + Ok(JsValue::from_bool(enabled)) + }) + } + + /// Disable and reset our backup state. + /// + /// This will remove any pending backup request, remove the backup key and + /// reset the backup state of each room key we have. + #[wasm_bindgen(js_name = "disabledBackup")] + pub fn disable_backup(&self) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { + me.backup_machine().disable_backup().await?; + Ok(JsValue::NULL) + }) + } + + /// Encrypt a batch of room keys and return a request that needs to be sent + /// out to backup the room keys. + /// This returns an optional `JsValue` representing a `KeysBackupRequest`. + #[wasm_bindgen(js_name = "backupRoomKeys")] + pub fn backup_room_keys(&self) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { + let request = me.backup_machine().backup().await?.map(OutgoingRequest); + + match request { + None => Ok(JsValue::NULL), + Some(r) => Ok(JsValue::try_from(r)?), + } + }) + } + + /// Get the number of backed up room keys and the total number of room keys. + /// Returns a {"total":1,"backedUp":0} json object + #[wasm_bindgen(js_name = "roomKeyCounts")] + pub fn room_key_counts(&self) -> Promise { + let me = self.inner.clone(); + future_to_promise(async move { + let inner = me.backup_machine().room_key_counts().await?; + let count = RoomKeyCounts { + total: inner.total.try_into()?, + backed_up: inner.backed_up.try_into()?, + }; + let js_value = serde_wasm_bindgen::to_value(&count).unwrap(); + Ok(js_value) + }) + } + /// Encrypt the list of exported room keys using the given passphrase. /// /// `exported_room_keys` is a list of sessions that should be encrypted diff --git a/src/types.rs b/src/types.rs index c42a40a..78dfa6e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,13 @@ //! Extra types, like `Signatures`. -use js_sys::Map; +use std::collections::BTreeMap; + +use js_sys::{Map, JSON}; +use matrix_sdk_crypto::backups::{ + SignatureState as InnerSignatureState, SignatureVerification as InnerSignatureVerification, +}; +use serde::{Deserialize, Serialize}; +use tracing::trace; use wasm_bindgen::prelude::*; use crate::{ @@ -84,6 +91,42 @@ impl Signatures { pub fn count(&self) -> usize { self.inner.signature_count() } + + /// Get the json with all signatures + #[wasm_bindgen(js_name = "asJSON")] + pub fn as_json(&self) -> JsValue { + trace!(?self.inner, "The signature"); + // can't use directly serde_wasm_bindgen as there is an issue with BTreeMap + // It's always returning an empty {} if I do: + // serde_wasm_bindgen::to_value(&self.inner).unwrap() + + // Keep it like that for now as it's working + let map: BTreeMap> = self + .inner + .clone() + .into_iter() + .map(|(u, sign)| { + ( + u.as_str().to_owned(), + sign.iter() + .map(|(device, maybe_sign)| { + ( + device.as_str().to_owned(), + match maybe_sign { + Ok(s) => s.to_base64(), + Err(e) => e.source.to_owned(), + }, + ) + }) + .collect(), + ) + }) + .collect(); + + let raw_string = serde_json::to_string(&map).unwrap(); + + JSON::parse(&raw_string).unwrap() + } } /// Represents a potentially decoded signature (but not a validated @@ -154,3 +197,75 @@ impl MaybeSignature { } } } + +/// The result of a signature verification of a signed JSON object. +#[derive(Debug)] +#[wasm_bindgen] +pub struct SignatureVerification { + pub(crate) inner: InnerSignatureVerification, +} + +/// The result of a signature check. +#[derive(Debug)] +#[wasm_bindgen] +pub enum SignatureState { + /// The signature is missing. + Missing = 0, + /// The signature is invalid. + Invalid = 1, + /// The signature is valid but the device or user identity that created the + /// signature is not trusted. + ValidButNotTrusted = 2, + /// The signature is valid and the device or user identity that created the + /// signature is trusted. + ValidAndTrusted = 3, +} + +impl From for SignatureState { + fn from(val: InnerSignatureState) -> Self { + match val { + InnerSignatureState::Missing => SignatureState::Missing, + InnerSignatureState::Invalid => SignatureState::Invalid, + InnerSignatureState::ValidButNotTrusted => SignatureState::ValidButNotTrusted, + InnerSignatureState::ValidAndTrusted => SignatureState::ValidAndTrusted, + } + } +} + +#[wasm_bindgen] +impl SignatureVerification { + /// Give the backup signature state from the current device. + /// See SignatureState for values + #[wasm_bindgen(getter, js_name = "deviceState")] + pub fn device_state(&self) -> SignatureState { + self.inner.device_signature.into() + } + + /// Give the backup signature state from the current user identity. + /// See SignatureState for values + #[wasm_bindgen(getter, js_name = "userState")] + pub fn user_state(&self) -> SignatureState { + self.inner.user_identity_signature.into() + } +} + +/// Struct holding the number of room keys we have. +#[derive(Debug, Serialize, Deserialize)] +pub struct RoomKeyCounts { + /// The total number of room keys. + pub total: i64, + /// The number of backed up room keys. + #[serde(rename = "backedUp")] + pub backed_up: i64, +} + +/// The backup recovery key has saved by sdk +#[derive(Debug, Serialize, Deserialize)] +pub struct BackupKeys { + /// The total number of room keys. + #[serde(rename = "recoveryKeyBase58")] + pub recovery_key: Option, + /// The number of backed up room keys. + #[serde(rename = "backupVersion")] + pub backup_version: Option, +} diff --git a/tests/backup_recovery_key.test.js b/tests/backup_recovery_key.test.js new file mode 100644 index 0000000..4ea979a --- /dev/null +++ b/tests/backup_recovery_key.test.js @@ -0,0 +1,69 @@ +const { BackupRecoveryKey } = require("../pkg/matrix_sdk_crypto_js"); + +const aMegolmKey = { + algorithm: "m.megolm.v1.aes-sha2", + sender_key: "wREG/hBdSspoqM9xPCEXd/4YwjpBFXlsobRkyDTo/Q8", + session_key: + "AQAAAABwCEYsl5BrvPW0N8HTYP11phC7LOzItQLS3Zen6j1j9qMydUHVDeuMLxwo5i3GYfLWGjJEjsCj0Q99TZMABnJBCFg9MheV8cNSBfj7mHSZr6NP8aUAAAOhsY+cJwPDHxcnU181nAEs0fovHnonZGXs6iB/K6sKfuRWUNvX50ORohgDT3TGl0gQFed1FQEtn2Q1qT35iTRfe81SGOnFJrOM", + sender_claimed_keys: { ed25519: "MnNLGwn4j9ArCvtgU6o1jG8TgJaEXQpDTxz7QU0h7GM" }, + forwarding_curve25519_key_chain: [], +}; + +const encryptedMegolm = { + first_message_index: 0, + forwarded_count: 0, + is_verified: false, + session_data: { + ephemeral: "HlLi76oV6wxHz3PCqE/bxJi6yF1HnYz5Dq3T+d/KpRw", + ciphertext: + "MuM8E3Yc6TSAvhVGb77rQ++jE6p9dRepx63/3YPD2wACKAppkZHeFrnTH6wJ/HSyrmzo7HfwqVl6tKNpfooSTHqUf6x1LHz+h4B/Id5ITO1WYt16AaI40LOnZqTkJZCfSPuE2oxalwEHnCS3biWybutcnrBFPR3LMtaeHvvkb+k3ny9l5ZpsU9G7vCm3XoeYkWfLekWXvDhbqWrylXD0+CNUuaQJ/S527TzLd4XKctqVjjO/cCH7q+9utt9WJAfK8LGaWT/mZ3AeWjf5kiqOpKKf5Cn4n5SSil5p/pvGYmjnURvZSEeQIzHgvunIBEPtzK/MYEPOXe/P5achNGlCx+5N19Ftyp9TFaTFlTWCTi0mpD7ePfCNISrwpozAz9HZc0OhA8+1aSc7rhYFIeAYXFU326NuFIFHI5pvpSxjzPQlOA+mavIKmiRAtjlLw11IVKTxgrdT4N8lXeMr4ndCSmvIkAzFMo1uZA4fzjiAdQJE4/2WeXFNNpvdfoYmX8Zl9CAYjpSO5HvpwkAbk4/iLEH3hDfCVUwDfMh05PdGLnxeRpiEFWSMSsJNp+OWAA+5JsF41BoRGrxoXXT+VKqlUDONd+O296Psu8Q+d8/S618", + mac: "GtMrurhDTwo", + }, +}; + +describe("BackupRecoveryKey", () => { + test("create from base64 string", () => { + const backupkey = BackupRecoveryKey.fromBase64("Ha9cklU/9NqFo9WKdVfGzmqUL/9wlkdxfEitbSIPVXw"); + + const decypted = backupkey.decryptV1( + encryptedMegolm.session_data.ephemeral, + encryptedMegolm.session_data.mac, + encryptedMegolm.session_data.ciphertext, + ); + + expect(decypted.algorithm).toStrictEqual(aMegolmKey.algorithm); + expect(decypted.sender_key).toStrictEqual(aMegolmKey.sender_key); + expect(decypted.session_key).toStrictEqual(aMegolmKey.session_key); + }); + + test("create export and import base58", () => { + const backupkey = BackupRecoveryKey.fromBase64("Ha9cklU/9NqFo9WKdVfGzmqUL/9wlkdxfEitbSIPVXw"); + const base58 = backupkey.toBase58(); + const imported = BackupRecoveryKey.fromBase58(base58); + + expect(backupkey.megolmV1PublicKey.publicKeyBase64).toStrictEqual(imported.megolmV1PublicKey.publicKeyBase64); + }); + + test("with passphrase", () => { + const recoveryKey = BackupRecoveryKey.newFromPassphrase("aSecretPhrase"); + + expect(recoveryKey.megolmV1PublicKey.passphraseInfo).toBeDefined(); + expect(recoveryKey.megolmV1PublicKey.passphraseInfo.private_key_iterations).toStrictEqual(500000); + }); + + test("errors", () => { + expect(() => { + BackupRecoveryKey.fromBase64("notBase64"); + }).toThrow(); + + const wrongKey = BackupRecoveryKey.newFromPassphrase("aSecretPhrase"); + + expect(() => { + wrongKey.decryptV1( + encryptedMegolm.session_data.ephemeral, + encryptedMegolm.session_data.mac, + encryptedMegolm.session_data.ciphertext, + ); + }).toThrow(); + }); +}); diff --git a/tests/machine.test.js b/tests/machine.test.js index 220d105..f019c2c 100644 --- a/tests/machine.test.js +++ b/tests/machine.test.js @@ -24,6 +24,8 @@ const { VerificationState, Versions, getVersions, + SignatureState, + BackupRecoveryKey, } = require("../pkg/matrix_sdk_crypto_js"); const { addMachineToMachine } = require("./helper"); require("fake-indexeddb/auto"); @@ -903,4 +905,117 @@ describe(OlmMachine.name, () => { }); }); }); + + describe("can manage key backups", () => { + test("test unknown signature", async () => { + let m = await machine(); + + let backupData = { + version: "2", + algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", + auth_data: { + public_key: "ddIQtIjfCzfR69I/imE7XiGsPPKA1KF74aclXsiWh08", + signatures: { + "@web:example.org": { + "ed25519:WVJSAIOBUZ": + "zzqyWl3ek5dSWKKeNPrpMFDQyu9ZlHrA2XpAaXtcSyo8BoZIu0K2flfT+N0YgVee2gmAZdLAribwgoCopvTeAg", + "ed25519:LHMKRoMYl7haWnst5Xo54DuRqjZ5h/Sk1lxc4heSEcI": + "YwRj5UqKrbMbAb/VK0Dwj4HspiOjSN64cM5SwFQ7HEcFiHp4gJmHtV90kl+12OLiE5JqRWvgzsx61hSXM/JDCA", + }, + }, + }, + etag: "0", + count: 0, + }; + + const state = await m.verifyBackup(backupData); + + expect(state.deviceState).toStrictEqual(SignatureState.Missing); + expect(state.userState).toStrictEqual(SignatureState.Missing); + }); + + test("test validate own signatures", async () => { + let m = await machine(); + let _ = m.bootstrapCrossSigning(true); + + let keyBackupKey = BackupRecoveryKey.createRandomKey(); + + let auth_data = { + public_key: keyBackupKey.megolmV1PublicKey.publicKeyBase64, + }; + + let canonical = JSON.stringify(auth_data); + + let signatures = (await m.sign(canonical)).asJSON(); + + let backupData = { + algorithm: keyBackupKey.megolmV1PublicKey.algorithm, + auth_data: { + signatures: signatures, + ...auth_data, + }, + }; + + const state = await m.verifyBackup(backupData); + + expect(state.deviceState).toStrictEqual(SignatureState.ValidAndTrusted); + expect(state.userState).toStrictEqual(SignatureState.ValidAndTrusted); + }); + + test("test backup keys", async () => { + let m = await machine(); + + await m.shareRoomKey(room, [new UserId("@bob:example.org")], new EncryptionSettings()); + + let counts = await m.roomKeyCounts(); + + expect(counts.total).toStrictEqual(1); + expect(counts.backedUp).toStrictEqual(0); + + let backupEnabled = await m.isBackupEnabled(); + expect(backupEnabled).toStrictEqual(false); + + let keyBackupKey = BackupRecoveryKey.createRandomKey(); + + await m.enableBackupV1(keyBackupKey.megolmV1PublicKey.publicKeyBase64, "1"); + + expect(await m.isBackupEnabled()).toStrictEqual(true); + + let outgoing = await m.backupRoomKeys(); + + expect(outgoing.id).toBeDefined(); + expect(outgoing.body).toBeDefined(); + expect(outgoing.type).toStrictEqual(RequestType.KeysBackup); + + let exportedKey = JSON.parse(outgoing.body); + + let sessions = exportedKey.rooms["!baz:matrix.org"].sessions; + let session_data = Object.values(sessions)[0].session_data; + + // should decrypt with the created key + let decrypted = keyBackupKey.decryptV1(session_data.ephemeral, session_data.mac, session_data.ciphertext); + expect(decrypted.algorithm).toStrictEqual("m.megolm.v1.aes-sha2"); + + // simulate key backed up + m.markRequestAsSent(outgoing.id, outgoing.type, '{"etag":"1","count":3}'); + + let newCounts = await m.roomKeyCounts(); + + expect(newCounts.total).toStrictEqual(1); + expect(newCounts.backedUp).toStrictEqual(1); + }); + + test("test save and get private key", async () => { + let m = await machine(); + + let keyBackupKey = BackupRecoveryKey.createRandomKey(); + + await m.saveBackupRecoveryKey(keyBackupKey.toBase58(), "3"); + + let savedKey = await m.getBackupKeys(); + + expect(savedKey.recoveryKeyBase58).toStrictEqual(keyBackupKey.toBase58()); + expect(savedKey.backupVersion).toStrictEqual("3"); + }); + }); });