diff --git a/Cargo.lock b/Cargo.lock index 41326c2..7d06751 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,6 +341,15 @@ dependencies = [ "log", ] +[[package]] +name = "bs58" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.13.0" @@ -1187,13 +1196,16 @@ dependencies = [ "async-trait", "atomic", "base64 0.21.2", + "bs58", "byteorder", + "cbc", "cfg-if", "ctr", "dashmap", "eyeball", "futures-core", "futures-util", + "hkdf", "hmac", "itertools 0.11.0", "matrix-sdk-common", @@ -1217,6 +1229,7 @@ dependencies = [ name = "matrix-sdk-crypto-nodejs" version = "0.6.0" dependencies = [ + "hmac", "http", "matrix-sdk-common", "matrix-sdk-crypto", @@ -1224,7 +1237,11 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "pbkdf2", + "rand 0.8.5", + "serde", "serde_json", + "sha2 0.10.7", "tracing-subscriber", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index 2779444..c0af1e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,10 +26,15 @@ tracing = ["dep:tracing-subscriber"] [dependencies] matrix-sdk-common = { git = "https://github.com/matrix-org/matrix-rust-sdk", rev = "6e10eb9efb595f2f463022454d0af7d265dfb16b", features = ["js"] } matrix-sdk-sqlite = { git = "https://github.com/matrix-org/matrix-rust-sdk", rev = "6e10eb9efb595f2f463022454d0af7d265dfb16b", features = ["crypto-store"] } +hmac = "0.12.1" +http = "0.2.6" +pbkdf2 = "0.11.0" +rand = "0.8.5" napi = { version = "2.9.1", default-features = false, features = ["napi6", "tokio_rt"] } napi-derive = "2.9.1" +serde = "1.0.151" serde_json = "1.0.91" -http = "0.2.6" +sha2 = "0.10.2" tracing-subscriber = { version = "0.3", default-features = false, features = ["tracing-log", "time", "smallvec", "fmt", "env-filter"], optional = true } zeroize = "1.3.0" @@ -37,7 +42,7 @@ zeroize = "1.3.0" git = "https://github.com/matrix-org/matrix-rust-sdk" rev = "6e10eb9efb595f2f463022454d0af7d265dfb16b" default_features = false -features = ["js", "automatic-room-key-forwarding"] +features = ["js", "backups_v1", "automatic-room-key-forwarding"] [build-dependencies] napi-build = "2.0.0" diff --git a/src/backup_recovery_key.rs b/src/backup_recovery_key.rs new file mode 100644 index 0000000..5e055bd --- /dev/null +++ b/src/backup_recovery_key.rs @@ -0,0 +1,172 @@ +//! Megolm backup types + +use std::{collections::HashMap, iter, ops::DerefMut}; + +use hmac::Hmac; +use matrix_sdk_crypto::{backups::MegolmV1BackupKey as InnerMegolmV1BackupKey, store::RecoveryKey}; +use napi_derive::*; +use pbkdf2::pbkdf2; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use sha2::Sha512; +use zeroize::Zeroize; + +use crate::into_err; + +/// The private part of the backup key, the one used for recovery. +#[napi] +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. +#[napi] +#[derive(Clone)] +pub struct PassphraseInfo { + /// The salt that was used during key derivation. + #[napi(getter)] + pub private_key_salt: String, + /// The number of PBKDF rounds that were used for key derivation. + pub private_key_iterations: i32, +} + +/// The public part of the backup key. +#[napi] +#[derive(Clone)] +pub struct MegolmV1BackupKey { + inner: InnerMegolmV1BackupKey, + passphrase_info: Option, +} + +#[napi] +impl MegolmV1BackupKey { + /// The actual base64 encoded public key. + #[napi(getter, js_name = "publicKeyBase64")] + pub fn public_key(&self) -> String { + self.inner.to_base64().into() + } + + /// The passphrase info, if the key was derived from one. + #[napi(getter)] + pub fn passphrase_info(&self) -> Option { + self.passphrase_info.clone() + } + + /// Get the full name of the backup algorithm this backup key supports. + #[napi(getter, js_name = "algorithm")] + pub fn backup_algorithm(&self) -> String { + self.inner.backup_algorithm().into() + } + + /// Signatures that have signed our backup key. + /// map of userId to map of deviceOrKeyId to signature + #[napi(getter)] + pub fn signatures(&self) -> HashMap> { + self + .inner + .signatures() + .into_iter() + .map(|(k, v)| (k.to_string(), v.into_iter().map(|(k, v)| (k.to_string(), v)).collect())) + .collect() + } +} + +impl BackupRecoveryKey { + const KEY_SIZE: usize = 32; + const SALT_SIZE: usize = 32; + const PBKDF_ROUNDS: i32 = 500_000; +} + +#[napi] +impl BackupRecoveryKey { + /// Create a new random [`BackupRecoveryKey`]. + #[napi] + 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. + #[napi] + pub fn from_base64(key: String) -> napi::Result { + Ok(Self { inner: RecoveryKey::from_base64(&key).map_err(into_err)?, passphrase_info: None }) + } + + /// Try to create a [`BackupRecoveryKey`] from a base 58 encoded string. + #[napi] + pub fn from_base58(key: String) -> napi::Result { + Ok(Self { inner: RecoveryKey::from_base58(&key).map_err(into_err)?, passphrase_info: None }) + } + + /// Create a new [`BackupRecoveryKey`] from the given passphrase. + #[napi] + 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. + #[napi] + 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. + #[napi] + pub fn to_base58(&self) -> String { + self.inner.to_base58().into() + } + + /// Convert the recovery key to a base 64 encoded string. + #[napi] + pub fn to_base64(&self) -> String { + self.inner.to_base64().into() + } + + /// Get the public part of the backup key. + #[napi(getter)] + 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. + #[napi] + pub fn decrypt_v1( + &self, + ephemeral_key: String, + mac: String, + ciphertext: String, + ) -> napi::Result { + self.inner + .decrypt_v1(&ephemeral_key, &mac, &ciphertext) + .map_err(into_err) + } +} diff --git a/src/lib.rs b/src/lib.rs index 540204b..862476f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ use napi_derive::napi; pub mod attachment; +pub mod backup_recovery_key; pub mod encryption; mod errors; pub mod events; diff --git a/src/machine.rs b/src/machine.rs index e61e7aa..d449d29 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -8,6 +8,7 @@ use std::{ }; use matrix_sdk_common::ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt}; +use matrix_sdk_crypto::{backups::MegolmV1BackupKey, store::RecoveryKey, types::RoomKeyBackupInfo}; use napi::bindgen_prelude::{within_runtime_if_available, Either7, FromNapiValue, ToNapiValue}; use napi_derive::*; use serde_json::{value::RawValue, Value as JsonValue}; @@ -15,7 +16,7 @@ use zeroize::Zeroize; use crate::{ encryption, identifiers, into_err, olm, requests, responses, responses::response_from_string, - sync_events, types, vodozemac, + sync_events, types::{self, BackupKeys, RoomKeyCounts, SignatureVerification}, vodozemac, }; /// The value used by the `OlmMachine` JS class. @@ -453,6 +454,20 @@ impl OlmMachine { self.inner.cross_signing_status().await.into() } + /// Create a new cross signing identity and get the upload request + /// to push the new public keys to the server. + /// + /// Warning: This will delete any existing cross signing keys that + /// might exist on the server and thus will reset the trust + /// between all the devices. + /// + /// Uploading these keys will require user interactive auth. + #[napi] + pub async fn bootstrap_cross_signing(&self, reset: bool) -> napi::Result<()> { + self.inner.bootstrap_cross_signing(reset).await.map_err(into_err)?; + Ok(()) + } + /// Sign the given message using our device key and if available /// cross-signing master key. #[napi(strict)] @@ -460,6 +475,141 @@ impl OlmMachine { self.inner.sign(message.as_str()).await.into() } + /// Store the recovery key in the crypto store. + /// + /// This is useful if the client wants to support gossiping of the backup + /// key. + #[napi(strict, js_name = "saveBackupRecoveryKey")] + pub async fn save_recovery_key( + &self, + recovery_key_base_58: String, + version: String, + ) -> napi::Result<()> { + let key = RecoveryKey::from_base58(&recovery_key_base_58).map_err(into_err)?; + + self.inner.backup_machine().save_recovery_key(Some(key), Some(version)).await.map_err(into_err)?; + + Ok(()) + } + + /// Get the backup keys we have saved in our crypto store. + #[napi] + pub async fn get_backup_keys(&self) -> napi::Result { + let inner = self.inner.backup_machine().get_backup_keys().await.map_err(into_err)?; + Ok(BackupKeys { + recovery_key: inner.recovery_key.map(|k| k.to_base58()), + backup_version: inner.backup_version, + }) + } + + /// 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 stringified JSON object with the following + /// format: + /// + /// ```json + /// { + /// "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", + /// "auth_data": { + /// "public_key":"XjhWTCjW7l59pbfx9tlCBQolfnIQWARoKOzjTOPSlWM", + /// "signatures": {} + /// } + /// } + /// ``` + #[napi(strict)] + pub async fn verify_backup(&self, backup_info: String) -> napi::Result { + let backup_info: RoomKeyBackupInfo = serde_json::from_str(backup_info.as_str()).map_err(into_err)?; + + let result = self.inner.backup_machine().verify_backup(backup_info, false).await.map_err(into_err)?; + 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 do so. + #[napi(strict)] + pub async fn enable_backup_v1( + &self, + public_key_base_64: String, + version: String, + ) -> napi::Result<()> { + let backup_key = MegolmV1BackupKey::from_base64(&public_key_base_64).map_err(into_err)?; + backup_key.set_version(version); + + self.inner.backup_machine().enable_backup_v1(backup_key).await.map_err(into_err)?; + Ok(()) + } + + /// Are we able to encrypt room keys. + /// + /// This returns true if we have an active `BackupKey` and backup version + /// registered with the state machine. + #[napi(js_name = "isBackupEnabled")] + pub async fn backup_enabled(&self) -> bool { + self.inner.backup_machine().enabled().await + } + + /// 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. + #[napi] + pub async fn disable_backup(&self) -> napi::Result<()> { + self.inner.backup_machine().disable_backup().await.map_err(into_err)?; + Ok(()) + } + + /// Encrypt a batch of room keys and return a request that needs to be sent + /// out to backup the room keys. + #[napi] + pub async fn backup_room_keys( + &self, + ) -> napi::Result< + Option< + // TODO Reduce this to just requests::KeysBackupRequest if appropriate + Either7< + requests::KeysUploadRequest, + requests::KeysQueryRequest, + requests::KeysClaimRequest, + requests::ToDeviceRequest, + requests::SignatureUploadRequest, + requests::RoomMessageRequest, + requests::KeysBackupRequest, + >, + >, + > { + match self + .inner + .backup_machine() + .backup() + .await + .map_err(into_err)? + .map(requests::OutgoingRequest) + { + Some(r) => Ok(Some( + requests::OutgoingRequests::try_from(r) + .map_err(into_err)?, + )), + + None => Ok(None), + } + } + + /// Get the number of backed up room keys and the total number of room keys. + #[napi] + pub async fn room_key_counts(&self) -> napi::Result { + let inner = self.inner.backup_machine().room_key_counts().await.map_err(into_err)?; + Ok(RoomKeyCounts { + total: inner.total.try_into().map_err(into_err)?, + backed_up: inner.backed_up.try_into().map_err(into_err)?, + }) + } + /// Shut down the `OlmMachine`. /// /// The `OlmMachine` cannot be used after this method has been called, diff --git a/src/types.rs b/src/types.rs index 89d1027..5f9bbb8 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,11 @@ use std::collections::HashMap; +use matrix_sdk_crypto::backups::{ + SignatureState as InnerSignatureState, SignatureVerification as InnerSignatureVerification, +}; +use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; +use serde::{Deserialize, Serialize}; + use napi_derive::*; use crate::{ @@ -78,6 +84,12 @@ impl Signatures { pub fn count(&self) -> usize { self.inner.signature_count() } + + /// Get the json with all signatures + #[napi(strict, js_name = "asJSONString")] + pub fn as_json(&self) -> String { + serde_json::to_string(&self.inner).unwrap() + } } /// Represents a potentially decoded signature (but not a validated @@ -154,3 +166,76 @@ impl MaybeSignature { } } } + +/// The result of a signature verification of a signed JSON object. +#[napi] +pub struct SignatureVerification { + pub(crate) inner: InnerSignatureVerification, +} + +/// The result of a signature check. +#[napi] +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, + } + } +} + +#[napi] +impl SignatureVerification { + /// Give the backup signature state from the current device. + /// See SignatureState for values + #[napi(getter)] + 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 + #[napi(getter)] + 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)] +#[napi(object)] +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)] +#[napi(object)] +pub struct BackupKeys { + /// The total number of room keys. + #[serde(rename = "recoveryKeyBase58")] + #[napi(js_name = "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..002e561 --- /dev/null +++ b/tests/backup_recovery_key.test.js @@ -0,0 +1,71 @@ +const { BackupRecoveryKey } = require("../"); + +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 decrypted = JSON.parse( + backupkey.decryptV1( + encryptedMegolm.session_data.ephemeral, + encryptedMegolm.session_data.mac, + encryptedMegolm.session_data.ciphertext, + ), + ); + + expect(decrypted.algorithm).toStrictEqual(aMegolmKey.algorithm); + expect(decrypted.sender_key).toStrictEqual(aMegolmKey.sender_key); + expect(decrypted.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.privateKeyIterations).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 6c20248..ca2f914 100644 --- a/tests/machine.test.js +++ b/tests/machine.test.js @@ -8,16 +8,16 @@ const { RequestType, KeysUploadRequest, KeysQueryRequest, - KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, - VerificationState, CrossSigningStatus, MaybeSignature, ShieldColor, StoreType, Versions, getVersions, + SignatureState, + BackupRecoveryKey, } = require("../"); const path = require("path"); const os = require("os"); @@ -142,7 +142,7 @@ describe(OlmMachine.name, () => { expect(receiveSyncChanges).toEqual([[], []]); }); - test("can get the outgoing requests that need to be send out", async () => { + test("can get the outgoing requests that need to be sent out", async () => { const m = await machine(); const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); @@ -480,4 +480,119 @@ describe(OlmMachine.name, () => { expect(signatures.getSignature(user, new DeviceKeyId("world:foobar"))).toBeNull(); } }); + + 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(JSON.stringify(backupData)); + + expect(state.deviceState).toStrictEqual(SignatureState.Missing); + expect(state.userState).toStrictEqual(SignatureState.Missing); + }); + + test("test validate own signatures", async () => { + let m = await machine(); + m.bootstrapCrossSigning(true); + + let keyBackupKey = BackupRecoveryKey.createRandomKey(); + + let auth_data = { + public_key: keyBackupKey.megolmV1PublicKey.publicKeyBase64, + }; + + let canonical = JSON.stringify(auth_data); + + let signatures = JSON.parse((await m.sign(canonical)).asJSONString()); + + let backupData = { + algorithm: keyBackupKey.megolmV1PublicKey.algorithm, + auth_data: { + signatures: signatures, + ...auth_data, + }, + }; + + const state = await m.verifyBackup(JSON.stringify(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 = JSON.parse( + 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"); + }); + }); });