From 94d3ffa18d980462d785ba10c6fa893b689da8a9 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 19 Nov 2021 21:26:33 +0100 Subject: [PATCH] infrastructure for indexeddb cryptostore --- crates/matrix-sdk-base/Cargo.toml | 5 +- crates/matrix-sdk-base/src/lib.rs | 7 + crates/matrix-sdk-crypto/Cargo.toml | 17 +- .../src/file_encryption/key_export.rs | 2 +- .../src/gossiping/machine.rs | 3 + .../src/identities/device.rs | 3 + .../src/identities/manager.rs | 2 + .../matrix-sdk-crypto/src/identities/user.rs | 3 + crates/matrix-sdk-crypto/src/lib.rs | 7 +- crates/matrix-sdk-crypto/src/machine.rs | 58 +- crates/matrix-sdk-crypto/src/olm/account.rs | 6 +- .../src/olm/group_sessions/mod.rs | 2 +- crates/matrix-sdk-crypto/src/olm/mod.rs | 27 +- .../matrix-sdk-crypto/src/olm/signing/mod.rs | 2 + .../src/session_manager/group_sessions.rs | 6 +- .../src/session_manager/sessions.rs | 2 + crates/matrix-sdk-crypto/src/store/caches.rs | 12 +- .../matrix-sdk-crypto/src/store/indexeddb.rs | 1323 +++++++++++++++++ .../src/store/memorystore.rs | 14 +- crates/matrix-sdk-crypto/src/store/mod.rs | 27 + .../matrix-sdk-crypto/src/store/pickle_key.rs | 2 +- crates/matrix-sdk-crypto/src/store/sled.rs | 2 +- .../src/verification/machine.rs | 8 +- .../matrix-sdk-crypto/src/verification/mod.rs | 3 + .../src/verification/requests.rs | 3 + .../src/verification/sas/helpers.rs | 2 +- .../src/verification/sas/mod.rs | 5 +- .../src/verification/sas/sas_state.rs | 22 +- 28 files changed, 1500 insertions(+), 75 deletions(-) create mode 100644 crates/matrix-sdk-crypto/src/store/indexeddb.rs diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index e70a00d5e..eef9557c7 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -19,10 +19,13 @@ rustdoc-args = ["--cfg", "feature=\"docs\""] default = [] encryption = ["matrix-sdk-crypto"] qrcode = ["matrix-sdk-crypto/qrcode"] + sled_state_store = ["sled", "pbkdf2", "hmac", "sha2", "rand", "chacha20poly1305"] -indexeddb_state_store = ["indexed_db_futures", "wasm-bindgen", "pbkdf2", "hmac", "sha2", "rand", "chacha20poly1305"] sled_cryptostore = ["matrix-sdk-crypto/sled_cryptostore"] +indexeddb_state_store = ["indexed_db_futures", "wasm-bindgen", "pbkdf2", "hmac", "sha2", "rand", "chacha20poly1305"] +indexeddb_cryptostore = ["matrix-sdk-crypto/indexeddb_cryptostore"] + docs = ["encryption", "sled_cryptostore"] [dependencies] diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 653ae768c..f5f94ad81 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -28,9 +28,16 @@ #[cfg(all(feature = "sled_state_store", feature = "indexeddb_state_store"))] compile_error!("sled_state_store and indexeddb_state_store are mutually exclusive and cannot be enabled together"); + +#[cfg(all(feature = "indexeddb_state_store", not(target_arch = "wasm32")))] +compile_error!("indexeddb_state_store only works for wasm32 target"); + + #[cfg(all(feature = "sled_cryptostore", feature = "indexeddb_state_store"))] compile_error!("sled_cryptostore and indexeddb_state_store are mutually exclusive and cannot be enabled together"); + + pub use matrix_sdk_common::*; pub use crate::{ diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index c565e01f9..39dcb3495 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -20,6 +20,7 @@ default = [] qrcode = ["matrix-qrcode"] sled_cryptostore = ["sled"] docs = ["sled_cryptostore"] +indexeddb_cryptostore = ["indexed_db_futures", "wasm-bindgen"] [dependencies] aes = { version = "0.7.4", features = ["ctr"] } @@ -44,21 +45,31 @@ thiserror = "1.0.25" tracing = "0.1.26" zeroize = { version = "1.3.0", features = ["zeroize_derive"] } +## Feature indexeddb-state-store +indexed_db_futures = { version = "0.2.0", optional = true } +wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true } + [dev-dependencies] -criterion = { version = "0.3.4", features = ["async", "async_tokio", "html_reports"] } futures = { version = "0.3.15", default-features = false } http = "0.2.4" indoc = "1.0.3" matches = "0.1.8" matrix-sdk-test = { version = "0.4.0", path = "../matrix-sdk-test" } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] proptest = "1.0.0" -serde_json = "1.0.64" -tempfile = "3.2.0" +criterion = { version = "0.3.4", features = ["async", "async_tokio", "html_reports"] } tokio = { version = "1.7.1", default-features = false, features = ["rt-multi-thread", "macros"] } +tempfile = "3.2.0" [target.'cfg(target_os = "linux")'.dev-dependencies] +criterion = { version = "0.3.4", features = ["async", "html_reports"] } pprof = { version = "0.5.0", features = ["flamegraph", "criterion"] } +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = "0.3.24" + + [[bench]] name = "crypto_bench" harness = false diff --git a/crates/matrix-sdk-crypto/src/file_encryption/key_export.rs b/crates/matrix-sdk-crypto/src/file_encryption/key_export.rs index 1318fff2a..7b4c60989 100644 --- a/crates/matrix-sdk-crypto/src/file_encryption/key_export.rs +++ b/crates/matrix-sdk-crypto/src/file_encryption/key_export.rs @@ -233,7 +233,7 @@ fn decrypt_helper(ciphertext: &str, passphrase: &str) -> Result OlmResult { - let (_, session) = self - .group_session_manager - .create_outbound_group_session(room_id, EncryptionSettings::default()) - .await?; + // #[cfg(test)] + // pub(crate) async fn create_inbound_session( + // &self, + // room_id: &RoomId, + // ) -> OlmResult { + // let (_, session) = self + // .group_session_manager + // .create_outbound_group_session(room_id, EncryptionSettings::default()) + // .await?; - Ok(session) - } + // Ok(session) + // } /// Encrypt a room message for the given room. /// @@ -1459,6 +1459,10 @@ impl OlmMachine { pub(crate) mod test { static USER_ID: &str = "@bob:example.org"; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; + use matrix_sdk_test::async_test; + use std::{ collections::BTreeMap, convert::{TryFrom, TryInto}, @@ -1616,13 +1620,13 @@ pub(crate) mod test { (alice, bob) } - #[tokio::test] + #[async_test] async fn create_olm_machine() { let machine = OlmMachine::new(&user_id(), &alice_device_id()); assert!(machine.should_upload_keys().await); } - #[tokio::test] + #[async_test] async fn receive_keys_upload_response() { let machine = OlmMachine::new(&user_id(), &alice_device_id()); let mut response = keys_upload_response(); @@ -1646,7 +1650,7 @@ pub(crate) mod test { assert!(!machine.should_upload_keys().await); } - #[tokio::test] + #[async_test] async fn generate_one_time_keys() { let machine = OlmMachine::new(&user_id(), &alice_device_id()); @@ -1663,7 +1667,7 @@ pub(crate) mod test { assert!(machine.account.generate_one_time_keys().await.is_err()); } - #[tokio::test] + #[async_test] async fn test_device_key_signing() { let machine = OlmMachine::new(&user_id(), &alice_device_id()); @@ -1681,7 +1685,7 @@ pub(crate) mod test { assert!(ret.is_ok()); } - #[tokio::test] + #[async_test] async fn tests_session_invalidation() { let machine = OlmMachine::new(&user_id(), &alice_device_id()); let room_id = room_id!("!test:example.org"); @@ -1698,7 +1702,7 @@ pub(crate) mod test { .invalidated()); } - #[tokio::test] + #[async_test] async fn test_invalid_signature() { let machine = OlmMachine::new(&user_id(), &alice_device_id()); @@ -1714,7 +1718,7 @@ pub(crate) mod test { assert!(ret.is_err()); } - #[tokio::test] + #[async_test] async fn test_one_time_key_signing() { let machine = OlmMachine::new(&user_id(), &alice_device_id()); machine.account.inner.update_uploaded_key_count(49); @@ -1735,7 +1739,7 @@ pub(crate) mod test { assert!(ret.is_ok()); } - #[tokio::test] + #[async_test] async fn test_keys_for_upload() { let machine = OlmMachine::new(&user_id(), &alice_device_id()); machine.account.inner.update_uploaded_key_count(0); @@ -1776,7 +1780,7 @@ pub(crate) mod test { assert!(ret.is_none()); } - #[tokio::test] + #[async_test] async fn test_keys_query() { let (machine, _) = get_prepared_machine().await; let response = keys_query_response(); @@ -1793,7 +1797,7 @@ pub(crate) mod test { assert_eq!(device.device_id(), alice_device_id); } - #[tokio::test] + #[async_test] async fn test_missing_sessions_calculation() { let (machine, _) = get_machine_after_query().await; @@ -1808,7 +1812,7 @@ pub(crate) mod test { assert!(user_sessions.contains_key(&alice_device)); } - #[tokio::test] + #[async_test] async fn test_session_creation() { let (alice_machine, bob_machine, one_time_keys) = get_machine_pair().await; @@ -1836,7 +1840,7 @@ pub(crate) mod test { assert!(!session.lock().await.is_empty()) } - #[tokio::test] + #[async_test] async fn test_olm_encryption() { let (alice, bob) = get_machine_pair_with_session().await; @@ -1860,7 +1864,7 @@ pub(crate) mod test { } } - #[tokio::test] + #[async_test] async fn test_room_key_sharing() { let (alice, bob) = get_machine_pair_with_session().await; @@ -1911,7 +1915,7 @@ pub(crate) mod test { assert!(session.unwrap().is_some()); } - #[tokio::test] + #[async_test] async fn test_megolm_encryption() { let (alice, bob) = get_machine_pair_with_setup_sessions().await; let room_id = room_id!("!test:example.org"); @@ -1971,7 +1975,7 @@ pub(crate) mod test { } } - #[tokio::test] + #[async_test] #[cfg(feature = "sled_cryptostore")] async fn test_machine_with_default_store() { use tempfile::tempdir; @@ -2009,7 +2013,7 @@ pub(crate) mod test { assert_eq!(ed25519_key, machine.identity_keys().ed25519()); } - #[tokio::test] + #[async_test] async fn interactive_verification() { let (alice, bob) = get_machine_pair_with_setup_sessions().await; diff --git a/crates/matrix-sdk-crypto/src/olm/account.rs b/crates/matrix-sdk-crypto/src/olm/account.rs index b20150d01..21ed2c606 100644 --- a/crates/matrix-sdk-crypto/src/olm/account.rs +++ b/crates/matrix-sdk-crypto/src/olm/account.rs @@ -1141,6 +1141,8 @@ impl PartialEq for ReadOnlyAccount { mod test { use std::collections::BTreeSet; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; use matrix_sdk_test::async_test; use ruma::{identifiers::DeviceIdBox, user_id, DeviceKeyId, UserId}; @@ -1156,7 +1158,7 @@ mod test { } #[async_test] - async fn one_time_key_creation() -> Result<()> { + async fn one_time_key_creation() { let account = ReadOnlyAccount::new(&user_id(), &device_id()); let one_time_keys = account @@ -1194,7 +1196,5 @@ mod test { let fourth_device_key_ids: BTreeSet<&DeviceKeyId> = fourth_one_time_keys.keys().collect(); assert_ne!(device_key_ids, fourth_device_key_ids); - - Ok(()) } } diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs index b3fb829a1..23328c342 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs @@ -135,7 +135,7 @@ mod test { use super::EncryptionSettings; use crate::{MegolmError, ReadOnlyAccount}; - #[tokio::test] + #[async_test] #[cfg(target_os = "linux")] async fn expiration() -> Result<(), MegolmError> { let settings = EncryptionSettings { rotation_period_msgs: 1, ..Default::default() }; diff --git a/crates/matrix-sdk-crypto/src/olm/mod.rs b/crates/matrix-sdk-crypto/src/olm/mod.rs index 577f56479..fbae29102 100644 --- a/crates/matrix-sdk-crypto/src/olm/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/mod.rs @@ -59,6 +59,9 @@ where #[cfg(test)] pub(crate) mod test { + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; + use matrix_sdk_test::async_test; use std::{collections::BTreeMap, convert::TryInto}; use matches::assert_matches; @@ -129,7 +132,7 @@ pub(crate) mod test { assert!(account.shared()); } - #[tokio::test] + #[async_test] async fn one_time_keys_creation() { let account = ReadOnlyAccount::new(&alice_id(), &alice_device_id()); let one_time_keys = account.one_time_keys().await; @@ -153,7 +156,7 @@ pub(crate) mod test { assert!(one_time_keys.curve25519().is_empty()); } - #[tokio::test] + #[async_test] async fn session_creation() { let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id()); let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id()); @@ -194,7 +197,7 @@ pub(crate) mod test { assert_eq!(plaintext, decyrpted); } - #[tokio::test] + #[async_test] async fn group_session_creation() { let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id()); let room_id = room_id!("!test:localhost"); @@ -225,8 +228,8 @@ pub(crate) mod test { assert_eq!(plaintext, inbound.decrypt_helper(ciphertext).await.unwrap().0); } - #[tokio::test] - async fn edit_decryption() -> Result<(), MegolmError> { + #[async_test] + async fn edit_decryption() { let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id()); let room_id = room_id!("!test:localhost"); let event_id = event_id!("$1234adfad:asdf"); @@ -250,19 +253,19 @@ pub(crate) mod test { &room_id, outbound.session_key().await, None, - )?; + ).unwrap(); assert_eq!(0, inbound.first_known_index()); assert_eq!(outbound.session_id(), inbound.session_id()); let encrypted_content = - outbound.encrypt(serde_json::to_value(content)?, "m.room.message").await; + outbound.encrypt(serde_json::to_value(content).unwrap(), "m.room.message").await; let event = json!({ "sender": alice.user_id(), "event_id": event_id, - "origin_server_ts": 0, + "origin_server_ts": 0u64, "room_id": room_id, "type": "m.room.encrypted", "content": encrypted_content, @@ -278,20 +281,18 @@ pub(crate) mod test { panic!("Invalid event type") }; - let decrypted = inbound.decrypt(&event).await?.0; + let decrypted = inbound.decrypt(&event).await.unwrap().0; if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(e)) = - decrypted.deserialize()? + decrypted.deserialize().unwrap() { assert_matches!(e.content.relates_to, Some(Relation::Replacement(_))); } else { panic!("Invalid event type") } - - Ok(()) } - #[tokio::test] + #[async_test] async fn group_session_export() { let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id()); let room_id = room_id!("!test:localhost"); diff --git a/crates/matrix-sdk-crypto/src/olm/signing/mod.rs b/crates/matrix-sdk-crypto/src/olm/signing/mod.rs index 84db14e35..8b045d13c 100644 --- a/crates/matrix-sdk-crypto/src/olm/signing/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/signing/mod.rs @@ -645,6 +645,8 @@ impl PrivateCrossSigningIdentity { mod test { use std::sync::Arc; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; use matrix_sdk_test::async_test; use ruma::{user_id, UserId}; diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions.rs index c6620008c..0fcf1a11d 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions.rs @@ -583,6 +583,10 @@ mod test { }; use serde_json::Value; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; + use matrix_sdk_test::async_test; + use crate::{EncryptionSettings, OlmMachine}; fn alice_id() -> UserId { @@ -622,7 +626,7 @@ mod test { machine } - #[tokio::test] + #[async_test] async fn test_sharing() { let machine = machine().await; let room_id = room_id!("!test:localhost"); diff --git a/crates/matrix-sdk-crypto/src/session_manager/sessions.rs b/crates/matrix-sdk-crypto/src/session_manager/sessions.rs index 66578c4e2..9c2f741f9 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/sessions.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/sessions.rs @@ -337,6 +337,8 @@ mod test { use dashmap::DashMap; use matrix_sdk_common::locks::Mutex; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; use matrix_sdk_test::async_test; use ruma::{ api::client::r0::keys::claim_keys::Response as KeyClaimResponse, user_id, DeviceIdBox, diff --git a/crates/matrix-sdk-crypto/src/store/caches.rs b/crates/matrix-sdk-crypto/src/store/caches.rs index 558bbf8d3..4b9c631f0 100644 --- a/crates/matrix-sdk-crypto/src/store/caches.rs +++ b/crates/matrix-sdk-crypto/src/store/caches.rs @@ -189,7 +189,11 @@ mod test { store::caches::{DeviceStore, GroupSessionStore, SessionStore}, }; - #[tokio::test] + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; + use matrix_sdk_test::async_test; + + #[async_test] async fn test_session_store() { let (_, session) = get_account_and_session().await; @@ -206,7 +210,7 @@ mod test { assert_eq!(&session, loaded_session); } - #[tokio::test] + #[async_test] async fn test_session_store_bulk_storing() { let (_, session) = get_account_and_session().await; @@ -221,7 +225,7 @@ mod test { assert_eq!(&session, loaded_session); } - #[tokio::test] + #[async_test] async fn test_group_session_store() { let (account, _) = get_account_and_session().await; let room_id = room_id!("!test:localhost"); @@ -250,7 +254,7 @@ mod test { assert_eq!(inbound, loaded_session); } - #[tokio::test] + #[async_test] async fn test_device_store() { let device = get_device(); let store = DeviceStore::new(); diff --git a/crates/matrix-sdk-crypto/src/store/indexeddb.rs b/crates/matrix-sdk-crypto/src/store/indexeddb.rs new file mode 100644 index 000000000..eb97a2b77 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/store/indexeddb.rs @@ -0,0 +1,1323 @@ +// Copyright 2020 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. + +#![allow(dead_code)] + +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, RwLock}, + convert::{TryFrom, TryInto}, +}; +use wasm_bindgen::JsValue; + +use dashmap::DashSet; +use matrix_sdk_common::{async_trait, locks::Mutex, uuid}; +use olm_rs::{account::IdentityKeys, PicklingMode}; +use ruma::{ + events::{room_key_request::RequestedKeyInfo, secret::request::SecretName}, + DeviceId, DeviceIdBox, RoomId, UserId, +}; +use tracing::trace; +use uuid::Uuid; + +use super::{ + caches::SessionStore, Changes, CryptoStore, CryptoStoreError, InboundGroupSession, PickleKey, + ReadOnlyAccount, Result, Session, EncryptedPickleKey, +}; +use crate::{ + gossiping::{GossipRequest, SecretInfo}, + identities::{ReadOnlyDevice, ReadOnlyUserIdentities}, + olm::{OutboundGroupSession, PickledInboundGroupSession, PrivateCrossSigningIdentity}, +}; +use indexed_db_futures::{prelude::*, web_sys::IdbKeyRange}; + +/// This needs to be 32 bytes long since AES-GCM requires it, otherwise we will +/// panic once we try to pickle a Signing object. +const DEFAULT_PICKLE: &str = "DEFAULT_PICKLE_PASSPHRASE_123456"; + +trait EncodeKey { + const SEPARATOR: u8 = 0xff; + fn encode(&self) -> Vec; +} + +impl EncodeKey for Uuid { + fn encode(&self) -> Vec { + self.as_u128().to_be_bytes().to_vec() + } +} + +impl EncodeKey for SecretName { + fn encode(&self) -> Vec { + [self.as_ref().as_bytes(), &[Self::SEPARATOR]].concat() + } +} + +impl EncodeKey for SecretInfo { + fn encode(&self) -> Vec { + match self { + SecretInfo::KeyRequest(k) => k.encode(), + SecretInfo::SecretRequest(s) => s.encode(), + } + } +} + +impl EncodeKey for &RequestedKeyInfo { + fn encode(&self) -> Vec { + [ + self.room_id.as_bytes(), + &[Self::SEPARATOR], + self.sender_key.as_bytes(), + &[Self::SEPARATOR], + self.algorithm.as_ref().as_bytes(), + &[Self::SEPARATOR], + self.session_id.as_bytes(), + &[Self::SEPARATOR], + ] + .concat() + } +} + +impl EncodeKey for &UserId { + fn encode(&self) -> Vec { + self.as_str().encode() + } +} + +impl EncodeKey for &RoomId { + fn encode(&self) -> Vec { + self.as_str().encode() + } +} + +impl EncodeKey for &str { + fn encode(&self) -> Vec { + [self.as_bytes(), &[Self::SEPARATOR]].concat() + } +} + +impl EncodeKey for (&str, &str) { + fn encode(&self) -> Vec { + [self.0.as_bytes(), &[Self::SEPARATOR], self.1.as_bytes(), &[Self::SEPARATOR]].concat() + } +} + +impl EncodeKey for (&str, &str, &str) { + fn encode(&self) -> Vec { + [ + self.0.as_bytes(), + &[Self::SEPARATOR], + self.1.as_bytes(), + &[Self::SEPARATOR], + self.2.as_bytes(), + &[Self::SEPARATOR], + ] + .concat() + } +} + +#[derive(Clone, Debug)] +pub struct AccountInfo { + user_id: Arc, + device_id: Arc, + identity_keys: Arc, +} + + +#[allow(non_snake_case)] +mod KEYS { + + // STORES + pub const ACCOUNT: &'static str = "account"; + pub const PRIVATE_IDENTITY: &'static str = "private_identity"; + + pub const SESSION: &'static str = "session"; + pub const INBOUND_GROUP_SESSIONS: &'static str = "inbound_group_sessions"; + + pub const OUTBOUND_GROUP_SESSIONS: &'static str = "outbound_group_sessions"; + + pub const TRACKED_USERS: &'static str = "tracked_users"; + pub const OLM_HASHES: &'static str = "olm_hashes"; + + pub const DEVICES: &'static str = "devices"; + pub const IDENTITIES: &'static str = "identities"; + + pub const OUTGOING_SECRET_REQUESTS: &'static str = "outgoing_secret_requests"; + pub const UNSENT_SECRET_REQUESTS: &'static str = "unsent_secret_requests"; + pub const SECRET_REQUESTS_BY_INFO: &'static str = "secret_requests_by_info"; + + // KEYS + pub const PICKLE_KEY: &'static str = "pickle_key"; +} + +/// An in-memory only store that will forget all the E2EE key once it's dropped. +pub struct IndexeddbStore { + account_info: Arc>>, + name: String, + pub(crate) inner: IdbDatabase, + pickle_key: Arc, + + session_cache: SessionStore, + tracked_users_cache: Arc>, + users_for_key_query_cache: Arc>, +} + +impl std::fmt::Debug for IndexeddbStore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IndexeddbStore").field("name", &self.name).finish() + } +} + +fn make_range(key: String) -> Result { + IdbKeyRange::bound( + &JsValue::from_str(&format!("{}:", key)), + &JsValue::from_str(&format!("{};", key)), + ) + .map_err(|e| { + CryptoStoreError::IndexedDatabase { + code: 0, + name: "IdbKeyRangeMakeError".to_owned(), + message: e.as_string().unwrap_or(format!("Creating key range for {:} failed", key)), + } + }) +} + +impl IndexeddbStore { + + async fn open_helper(prefix: String, pickle_key: Option) -> Result { + let name = format!("{:0}::matrix-sdk-crypto", prefix); + + let pickle_key = pickle_key.unwrap_or_else(|| + PickleKey::try_from(DEFAULT_PICKLE.as_bytes().to_vec()) + .expect("Default Pickle always works. qed") + ); + + // Open my_db v1 + let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, 1.0)?; + db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { + if evt.old_version() < 1.0 { + // migrating to version 1 + let db = evt.db(); + + db.create_object_store(KEYS::ACCOUNT)?; + db.create_object_store(KEYS::SESSION)?; + + db.create_object_store(KEYS::PRIVATE_IDENTITY)?; + db.create_object_store(KEYS::INBOUND_GROUP_SESSIONS)?; + db.create_object_store(KEYS::TRACKED_USERS)?; + db.create_object_store(KEYS::OLM_HASHES)?; + db.create_object_store(KEYS::DEVICES)?; + + db.create_object_store(KEYS::IDENTITIES)?; + db.create_object_store(KEYS::OUTGOING_SECRET_REQUESTS)?; + db.create_object_store(KEYS::UNSENT_SECRET_REQUESTS)?; + db.create_object_store(KEYS::SECRET_REQUESTS_BY_INFO)?; + } + Ok(()) + })); + + let db: IdbDatabase = db_req.into_future().await?; + let session_cache = SessionStore::new(); + + Ok(Self { + name, + session_cache, + pickle_key: pickle_key.into(), + inner: db, + account_info: RwLock::new(None).into(), + tracked_users_cache: DashSet::new().into(), + users_for_key_query_cache: DashSet::new().into(), + }) + } + + pub async fn open() -> Result { + IndexeddbStore::open_helper("crypto".to_owned(), None).await + } + + pub async fn open_with_passphrase(prefix: String, passphrase: &str) -> Result { + let name = format!("{:0}::matrix-sdk-crypto-meta", prefix); + + let mut db_req: OpenDbRequest = IdbDatabase::open_u32(&name, 1)?; + db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { + if evt.old_version() < 1.0 { + // migrating to version 1 + let db = evt.db(); + + db.create_object_store("matrix-sdk-crypto")?; + } + Ok(()) + })); + + let db: IdbDatabase = db_req.into_future().await?; + + let tx: IdbTransaction = + db.transaction_on_one_with_mode(&name, IdbTransactionMode::Readwrite)?; + let ob = tx.object_store("matrix-sdk-crypto")?; + + let store_key: Option = ob + .get(&JsValue::from_str(KEYS::PICKLE_KEY))? + .await? + .map(|k| k.into_serde()) + .transpose()?; + + let pickle_key = match store_key { + Some(key) => PickleKey::from_encrypted(passphrase, key) + .map_err(|_| CryptoStoreError::UnpicklingError)?, + None => { + let key = PickleKey::new(); + let encrypted = key.encrypt(passphrase); + ob.put_key_val(&JsValue::from_str(KEYS::PICKLE_KEY), &JsValue::from_serde(&encrypted)?)?; + tx.await.into_result()?; + key + } + }; + + IndexeddbStore::open_helper(prefix, Some(pickle_key)).await + } + + pub async fn open_with_name(name: String) -> Result { + IndexeddbStore::open_helper(name, None).await + } + + fn get_account_info(&self) -> Option { + self.account_info.read().unwrap().clone() + } + + + fn get_pickle_mode(&self) -> PicklingMode { + self.pickle_key.pickle_mode() + } + + fn get_pickle_key(&self) -> &[u8] { + self.pickle_key.key() + } + + async fn load_tracked_users(&self) -> Result<()> { + todo!() + // for value in self.tracked_users.iter() { + // let (user, dirty) = value?; + // let user = UserId::try_from(String::from_utf8_lossy(&user).to_string())?; + // let dirty = dirty.get(0).map(|d| *d == 1).unwrap_or(true); + + // self.tracked_users_cache.insert(user.clone()); + + // if dirty { + // self.users_for_key_query_cache.insert(user); + // } + // } + + // Ok(()) + } + + async fn load_outbound_group_session( + &self, + room_id: &RoomId, + ) -> Result> { + todo!() + // let account_info = self.get_account_info().ok_or(CryptoStoreError::AccountUnset)?; + + // self.outbound_group_sessions + // .get(room_id.encode())? + // .map(|p| serde_json::from_slice(&p).map_err(CryptoStoreError::Serialization)) + // .transpose()? + // .map(|p| { + // OutboundGroupSession::from_pickle( + // account_info.device_id, + // account_info.identity_keys, + // p, + // self.get_pickle_mode(), + // ) + // .map_err(CryptoStoreError::OlmGroupSession) + // }) + // .transpose() + } + + async fn save_changes(&self, changes: Changes) -> Result<()> { + todo!() + // let account_pickle = if let Some(a) = changes.account { + // Some(a.pickle(self.get_pickle_mode()).await) + // } else { + // None + // }; + + // let private_identity_pickle = if let Some(i) = changes.private_identity { + // Some(i.pickle(self.get_pickle_key()).await?) + // } else { + // None + // }; + + // let device_changes = changes.devices; + // let mut session_changes = HashMap::new(); + + // for session in changes.sessions { + // let sender_key = session.sender_key(); + // let session_id = session.session_id(); + + // let pickle = session.pickle(self.get_pickle_mode()).await; + // let key = (sender_key, session_id).encode(); + + // self.session_cache.add(session).await; + // session_changes.insert(key, pickle); + // } + + // let mut inbound_session_changes = HashMap::new(); + + // for session in changes.inbound_group_sessions { + // let room_id = session.room_id(); + // let sender_key = session.sender_key(); + // let session_id = session.session_id(); + // let key = (room_id.as_str(), sender_key, session_id).encode(); + // let pickle = session.pickle(self.get_pickle_mode()).await; + + // inbound_session_changes.insert(key, pickle); + // } + + // let mut outbound_session_changes = HashMap::new(); + + // for session in changes.outbound_group_sessions { + // let room_id = session.room_id(); + // let pickle = session.pickle(self.get_pickle_mode()).await; + + // outbound_session_changes.insert(room_id.clone(), pickle); + // } + + // let identity_changes = changes.identities; + // let olm_hashes = changes.message_hashes; + // let key_requests = changes.key_requests; + + // let ret: Result<(), TransactionError> = ( + // &self.account, + // &self.private_identity, + // &self.devices, + // &self.identities, + // &self.sessions, + // &self.inbound_group_sessions, + // &self.outbound_group_sessions, + // &self.olm_hashes, + // &self.outgoing_secret_requests, + // &self.unsent_secret_requests, + // &self.secret_requests_by_info, + // ) + // .transaction( + // |( + // account, + // private_identity, + // devices, + // identities, + // sessions, + // inbound_sessions, + // outbound_sessions, + // hashes, + // outgoing_secret_requests, + // unsent_secret_requests, + // secret_requests_by_info, + // )| { + // if let Some(a) = &account_pickle { + // account.insert( + // "account".encode(), + // serde_json::to_vec(a).map_err(ConflictableTransactionError::Abort)?, + // )?; + // } + + // if let Some(i) = &private_identity_pickle { + // private_identity.insert( + // "identity".encode(), + // serde_json::to_vec(&i).map_err(ConflictableTransactionError::Abort)?, + // )?; + // } + + // for device in device_changes.new.iter().chain(&device_changes.changed) { + // let key = (device.user_id().as_str(), device.device_id().as_str()).encode(); + // let device = serde_json::to_vec(&device) + // .map_err(ConflictableTransactionError::Abort)?; + // devices.insert(key, device)?; + // } + + // for device in &device_changes.deleted { + // let key = (device.user_id().as_str(), device.device_id().as_str()).encode(); + // devices.remove(key)?; + // } + + // for identity in identity_changes.changed.iter().chain(&identity_changes.new) { + // identities.insert( + // identity.user_id().encode(), + // serde_json::to_vec(&identity) + // .map_err(ConflictableTransactionError::Abort)?, + // )?; + // } + + // for (key, session) in &session_changes { + // sessions.insert( + // key.as_slice(), + // serde_json::to_vec(&session) + // .map_err(ConflictableTransactionError::Abort)?, + // )?; + // } + + // for (key, session) in &inbound_session_changes { + // inbound_sessions.insert( + // key.as_slice(), + // serde_json::to_vec(&session) + // .map_err(ConflictableTransactionError::Abort)?, + // )?; + // } + + // for (key, session) in &outbound_session_changes { + // outbound_sessions.insert( + // key.encode(), + // serde_json::to_vec(&session) + // .map_err(ConflictableTransactionError::Abort)?, + // )?; + // } + + // for hash in &olm_hashes { + // hashes.insert( + // serde_json::to_vec(&hash) + // .map_err(ConflictableTransactionError::Abort)?, + // &[0], + // )?; + // } + + // for key_request in &key_requests { + // secret_requests_by_info.insert( + // (&key_request.info).encode(), + // key_request.request_id.encode(), + // )?; + + // let key_request_id = key_request.request_id.encode(); + + // if key_request.sent_out { + // unsent_secret_requests.remove(key_request_id.clone())?; + // outgoing_secret_requests.insert( + // key_request_id, + // serde_json::to_vec(&key_request) + // .map_err(ConflictableTransactionError::Abort)?, + // )?; + // } else { + // outgoing_secret_requests.remove(key_request_id.clone())?; + // unsent_secret_requests.insert( + // key_request_id, + // serde_json::to_vec(&key_request) + // .map_err(ConflictableTransactionError::Abort)?, + // )?; + // } + // } + + // Ok(()) + // }, + // ); + + // ret?; + // self.inner.flush_async().await?; + + // Ok(()) + } + + async fn get_outgoing_key_request_helper(&self, id: &[u8]) -> Result> { + todo!() + // let request = self + // .outgoing_secret_requests + // .get(id)? + // .map(|r| serde_json::from_slice(&r)) + // .transpose()?; + + // let request = if request.is_none() { + // self.unsent_secret_requests.get(id)?.map(|r| serde_json::from_slice(&r)).transpose()? + // } else { + // request + // }; + + // Ok(request) + } +} + +#[async_trait(?Send)] +impl CryptoStore for IndexeddbStore { + async fn load_account(&self) -> Result> { + todo!() + // if let Some(pickle) = self.account.get("account".encode())? { + // let pickle = serde_json::from_slice(&pickle)?; + + // self.load_tracked_users().await?; + + // let account = ReadOnlyAccount::from_pickle(pickle, self.get_pickle_mode())?; + + // let account_info = AccountInfo { + // user_id: account.user_id.clone(), + // device_id: account.device_id.clone(), + // identity_keys: account.identity_keys.clone(), + // }; + + // *self.account_info.write().unwrap() = Some(account_info); + + // Ok(Some(account)) + // } else { + // Ok(None) + // } + } + + async fn save_account(&self, account: ReadOnlyAccount) -> Result<()> { + todo!() + // let account_info = AccountInfo { + // user_id: account.user_id.clone(), + // device_id: account.device_id.clone(), + // identity_keys: account.identity_keys.clone(), + // }; + + // *self.account_info.write().unwrap() = Some(account_info); + + // let changes = Changes { account: Some(account), ..Default::default() }; + + // self.save_changes(changes).await + } + + async fn load_identity(&self) -> Result> { + todo!() + // if let Some(i) = self.private_identity.get("identity".encode())? { + // let pickle = serde_json::from_slice(&i)?; + // Ok(Some( + // PrivateCrossSigningIdentity::from_pickle(pickle, self.get_pickle_key()) + // .await + // .map_err(|_| CryptoStoreError::UnpicklingError)?, + // )) + // } else { + // Ok(None) + // } + } + + async fn save_changes(&self, changes: Changes) -> Result<()> { + self.save_changes(changes).await + } + + async fn get_sessions(&self, sender_key: &str) -> Result>>>> { + todo!() + // let account_info = self.get_account_info().ok_or(CryptoStoreError::AccountUnset)?; + + // if self.session_cache.get(sender_key).is_none() { + // let sessions: Result> = self + // .sessions + // .scan_prefix(sender_key.encode()) + // .map(|s| serde_json::from_slice(&s?.1).map_err(CryptoStoreError::Serialization)) + // .map(|p| { + // Session::from_pickle( + // account_info.user_id.clone(), + // account_info.device_id.clone(), + // account_info.identity_keys.clone(), + // p?, + // self.get_pickle_mode(), + // ) + // .map_err(CryptoStoreError::SessionUnpickling) + // }) + // .collect(); + + // self.session_cache.set_for_sender(sender_key, sessions?); + // } + + // Ok(self.session_cache.get(sender_key)) + } + + async fn get_inbound_group_session( + &self, + room_id: &RoomId, + sender_key: &str, + session_id: &str, + ) -> Result> { + todo!() + // let key = (room_id.as_str(), sender_key, session_id).encode(); + // let pickle = self.inbound_group_sessions.get(&key)?.map(|p| serde_json::from_slice(&p)); + + // if let Some(pickle) = pickle { + // Ok(Some(InboundGroupSession::from_pickle(pickle?, self.get_pickle_mode())?)) + // } else { + // Ok(None) + // } + } + + async fn get_inbound_group_sessions(&self) -> Result> { + todo!() + // let pickles: Result> = self + // .inbound_group_sessions + // .iter() + // .map(|p| serde_json::from_slice(&p?.1).map_err(CryptoStoreError::Serialization)) + // .collect(); + + // Ok(pickles? + // .into_iter() + // .filter_map(|p| InboundGroupSession::from_pickle(p, self.get_pickle_mode()).ok()) + // .collect()) + } + + async fn get_outbound_group_sessions( + &self, + room_id: &RoomId, + ) -> Result> { + self.load_outbound_group_session(room_id).await + } + + fn is_user_tracked(&self, user_id: &UserId) -> bool { + self.tracked_users_cache.contains(user_id) + } + + fn has_users_for_key_query(&self) -> bool { + !self.users_for_key_query_cache.is_empty() + } + + fn tracked_users(&self) -> HashSet { + self.tracked_users_cache.to_owned().iter().map(|u| u.clone()).collect() + } + + fn users_for_key_query(&self) -> HashSet { + self.users_for_key_query_cache.iter().map(|u| u.clone()).collect() + } + + async fn update_tracked_user(&self, user: &UserId, dirty: bool) -> Result { + todo!() + // let already_added = self.tracked_users_cache.insert(user.clone()); + + // if dirty { + // self.users_for_key_query_cache.insert(user.clone()); + // } else { + // self.users_for_key_query_cache.remove(user); + // } + + // self.tracked_users.insert(user.as_str(), &[dirty as u8])?; + + // Ok(already_added) + } + + async fn get_device( + &self, + user_id: &UserId, + device_id: &DeviceId, + ) -> Result> { + todo!() + // let key = (user_id.as_str(), device_id.as_str()).encode(); + // Ok(self.devices.get(key)?.map(|d| serde_json::from_slice(&d)).transpose()?) + } + + async fn get_user_devices( + &self, + user_id: &UserId, + ) -> Result> { + todo!() + // self.devices + // .scan_prefix(user_id.encode()) + // .map(|d| serde_json::from_slice(&d?.1).map_err(CryptoStoreError::Serialization)) + // .map(|d| { + // let d: ReadOnlyDevice = d?; + // Ok((d.device_id().to_owned(), d)) + // }) + // .collect() + } + + async fn get_user_identity(&self, user_id: &UserId) -> Result> { + todo!() + // Ok(self + // .identities + // .get(user_id.encode())? + // .map(|i| serde_json::from_slice(&i)) + // .transpose()?) + } + + async fn is_message_known(&self, message_hash: &crate::olm::OlmMessageHash) -> Result { + todo!() + // Ok(self.olm_hashes.contains_key(serde_json::to_vec(message_hash)?)?) + } + + async fn get_outgoing_secret_requests( + &self, + request_id: Uuid, + ) -> Result> { + todo!() + // let request_id = request_id.encode(); + + // self.get_outgoing_key_request_helper(&request_id).await + } + + async fn get_secret_request_by_info( + &self, + key_info: &SecretInfo, + ) -> Result> { + todo!() + // let id = self.secret_requests_by_info.get(key_info.encode())?; + + // if let Some(id) = id { + // self.get_outgoing_key_request_helper(&id).await + // } else { + // Ok(None) + // } + } + + async fn get_unsent_secret_requests(&self) -> Result> { + todo!() + // let requests: Result> = self + // .unsent_secret_requests + // .iter() + // .map(|i| serde_json::from_slice(&i?.1).map_err(CryptoStoreError::from)) + // .collect(); + + // requests + } + + async fn delete_outgoing_secret_requests(&self, request_id: Uuid) -> Result<()> { + todo!() + // let ret: Result<(), TransactionError> = ( + // &self.outgoing_secret_requests, + // &self.unsent_secret_requests, + // &self.secret_requests_by_info, + // ) + // .transaction( + // |(outgoing_key_requests, unsent_key_requests, key_requests_by_info)| { + // let sent_request: Option = outgoing_key_requests + // .remove(request_id.encode())? + // .map(|r| serde_json::from_slice(&r)) + // .transpose() + // .map_err(ConflictableTransactionError::Abort)?; + + // let unsent_request: Option = unsent_key_requests + // .remove(request_id.encode())? + // .map(|r| serde_json::from_slice(&r)) + // .transpose() + // .map_err(ConflictableTransactionError::Abort)?; + + // if let Some(request) = sent_request { + // key_requests_by_info.remove((&request.info).encode())?; + // } + + // if let Some(request) = unsent_request { + // key_requests_by_info.remove((&request.info).encode())?; + // } + + // Ok(()) + // }, + // ); + + // ret?; + // self.inner.flush_async().await?; + + // Ok(()) + } +} + +#[cfg(test)] +mod test { + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + use wasm_bindgen_test::wasm_bindgen_test; + use std::collections::BTreeMap; + + use matrix_sdk_common::uuid::Uuid; + use matrix_sdk_test::async_test; + use olm_rs::outbound_group_session::OlmOutboundGroupSession; + use ruma::{ + encryption::SignedKey, events::room_key_request::RequestedKeyInfo, room_id, user_id, + DeviceId, EventEncryptionAlgorithm, UserId, + }; + + use super::{CryptoStore, GossipRequest, IndexeddbStore}; + use crate::{ + gossiping::SecretInfo, + identities::{ + device::test::get_device, + user::test::{get_other_identity, get_own_identity}, + }, + olm::{ + GroupSessionKey, InboundGroupSession, OlmMessageHash, PrivateCrossSigningIdentity, + ReadOnlyAccount, Session, + }, + store::{Changes, DeviceChanges, IdentityChanges}, + }; + + fn alice_id() -> UserId { + user_id!("@alice:example.org") + } + + fn alice_device_id() -> Box { + "ALICEDEVICE".into() + } + + fn bob_id() -> UserId { + user_id!("@bob:example.org") + } + + fn bob_device_id() -> Box { + "BOBDEVICE".into() + } + + async fn get_store(name: String, passphrase: Option<&str>) -> IndexeddbStore { + match passphrase { + Some(pass) => IndexeddbStore::open_with_passphrase(name, pass) + .await + .expect("Can't create a passphrase protected store"), + None => IndexeddbStore::open_with_name(name) + .await + .expect("Can't create store without passphrase"), + } + + } + + async fn get_loaded_store(name: String) -> (ReadOnlyAccount, IndexeddbStore) { + let store = get_store(name, None).await; + let account = get_account(); + store.save_account(account.clone()).await.expect("Can't save account"); + + (account, store) + } + + fn get_account() -> ReadOnlyAccount { + ReadOnlyAccount::new(&alice_id(), &alice_device_id()) + } + + async fn get_account_and_session() -> (ReadOnlyAccount, Session) { + let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id()); + let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id()); + + bob.generate_one_time_keys_helper(1).await; + let one_time_key = + bob.one_time_keys().await.curve25519().iter().next().unwrap().1.to_owned(); + let one_time_key = SignedKey::new(one_time_key, BTreeMap::new()); + let sender_key = bob.identity_keys().curve25519().to_owned(); + let session = + alice.create_outbound_session_helper(&sender_key, &one_time_key).await.unwrap(); + + (alice, session) + } + + #[async_test] + async fn save_account() { + let store = get_store("save_account".to_owned(), None).await; + assert!(store.load_account().await.unwrap().is_none()); + let account = get_account(); + + store.save_account(account).await.expect("Can't save account"); + } + + #[async_test] + async fn load_account() { + let store = get_store("load_account".to_owned(), None).await; + let account = get_account(); + + store.save_account(account.clone()).await.expect("Can't save account"); + + let loaded_account = store.load_account().await.expect("Can't load account"); + let loaded_account = loaded_account.unwrap(); + + assert_eq!(account, loaded_account); + } + + #[async_test] + async fn load_account_with_passphrase() { + let store = get_store("load_account_with_passphrase".to_owned(), Some("secret_passphrase")).await; + let account = get_account(); + + store.save_account(account.clone()).await.expect("Can't save account"); + + let loaded_account = store.load_account().await.expect("Can't load account"); + let loaded_account = loaded_account.unwrap(); + + assert_eq!(account, loaded_account); + } + + #[async_test] + async fn save_and_share_account() { + let store = get_store("save_and_share_account".to_owned(), None).await; + let account = get_account(); + + store.save_account(account.clone()).await.expect("Can't save account"); + + account.mark_as_shared(); + account.update_uploaded_key_count(50); + + store.save_account(account.clone()).await.expect("Can't save account"); + + let loaded_account = store.load_account().await.expect("Can't load account"); + let loaded_account = loaded_account.unwrap(); + + assert_eq!(account, loaded_account); + assert_eq!(account.uploaded_key_count(), loaded_account.uploaded_key_count()); + } + + #[async_test] + async fn load_sessions() { + let store = get_store("load_sessions".to_owned(), None).await; + let (account, session) = get_account_and_session().await; + store.save_account(account.clone()).await.expect("Can't save account"); + + let changes = Changes { sessions: vec![session.clone()], ..Default::default() }; + + store.save_changes(changes).await.unwrap(); + + let sessions = + store.get_sessions(&session.sender_key).await.expect("Can't load sessions").unwrap(); + let loaded_session = sessions.lock().await.get(0).cloned().unwrap(); + + assert_eq!(&session, &loaded_session); + } + + #[async_test] + async fn add_and_save_session() { + let store_name = "add_and_save_session".to_owned(); + let store = get_store(store_name.clone(), None).await; + let (account, session) = get_account_and_session().await; + let sender_key = session.sender_key.to_owned(); + let session_id = session.session_id().to_owned(); + + store.save_account(account.clone()).await.expect("Can't save account"); + + let changes = Changes { sessions: vec![session.clone()], ..Default::default() }; + store.save_changes(changes).await.unwrap(); + + let sessions = store.get_sessions(&sender_key).await.unwrap().unwrap(); + let sessions_lock = sessions.lock().await; + let session = &sessions_lock[0]; + + assert_eq!(session_id, session.session_id()); + + drop(store); + + let store = IndexeddbStore::open_with_name(store_name) + .await + .expect("Can't create store"); + + let loaded_account = store.load_account().await.unwrap().unwrap(); + assert_eq!(account, loaded_account); + + let sessions = store.get_sessions(&sender_key).await.unwrap().unwrap(); + let sessions_lock = sessions.lock().await; + let session = &sessions_lock[0]; + + assert_eq!(session_id, session.session_id()); + } + + #[async_test] + async fn save_inbound_group_session() { + let (account, store) = get_loaded_store("save_inbound_group_session".to_owned()).await; + + let identity_keys = account.identity_keys(); + let outbound_session = OlmOutboundGroupSession::new(); + let session = InboundGroupSession::new( + identity_keys.curve25519(), + identity_keys.ed25519(), + &room_id!("!test:localhost"), + GroupSessionKey(outbound_session.session_key()), + None, + ) + .expect("Can't create session"); + + let changes = Changes { inbound_group_sessions: vec![session], ..Default::default() }; + + store.save_changes(changes).await.expect("Can't save group session"); + } + + #[async_test] + async fn load_inbound_group_session() { + let dir = "load_inbound_group_session".to_owned(); + let (account, store) = get_loaded_store(dir.clone()).await; + + let identity_keys = account.identity_keys(); + let outbound_session = OlmOutboundGroupSession::new(); + let session = InboundGroupSession::new( + identity_keys.curve25519(), + identity_keys.ed25519(), + &room_id!("!test:localhost"), + GroupSessionKey(outbound_session.session_key()), + None, + ) + .expect("Can't create session"); + + let mut export = session.export().await; + + export.forwarding_curve25519_key_chain = vec!["some_chain".to_owned()]; + + let session = InboundGroupSession::from_export(export).unwrap(); + + let changes = + Changes { inbound_group_sessions: vec![session.clone()], ..Default::default() }; + + store.save_changes(changes).await.expect("Can't save group session"); + + drop(store); + + let store = IndexeddbStore::open_with_name(dir).await.expect("Can't create store"); + + store.load_account().await.unwrap(); + + let loaded_session = store + .get_inbound_group_session(&session.room_id, &session.sender_key, session.session_id()) + .await + .unwrap() + .unwrap(); + assert_eq!(session, loaded_session); + let export = loaded_session.export().await; + assert!(!export.forwarding_curve25519_key_chain.is_empty()) + } + + #[async_test] + async fn test_tracked_users() { + let dir = "test_tracked_users".to_owned(); + let (_account, store) = get_loaded_store(dir.clone()).await; + let device = get_device(); + + assert!(store.update_tracked_user(device.user_id(), false).await.unwrap()); + assert!(!store.update_tracked_user(device.user_id(), false).await.unwrap()); + + assert!(store.is_user_tracked(device.user_id())); + assert!(!store.users_for_key_query().contains(device.user_id())); + assert!(!store.update_tracked_user(device.user_id(), true).await.unwrap()); + assert!(store.users_for_key_query().contains(device.user_id())); + drop(store); + + let store = IndexeddbStore::open_with_name(dir.clone()).await.expect("Can't create store"); + + store.load_account().await.unwrap(); + + assert!(store.is_user_tracked(device.user_id())); + assert!(store.users_for_key_query().contains(device.user_id())); + + store.update_tracked_user(device.user_id(), false).await.unwrap(); + assert!(!store.users_for_key_query().contains(device.user_id())); + drop(store); + + let store = IndexeddbStore::open_with_name(dir).await.expect("Can't create store"); + + store.load_account().await.unwrap(); + + assert!(!store.users_for_key_query().contains(device.user_id())); + } + + #[async_test] + async fn device_saving() { + let dir = "device_saving".to_owned(); + let (_account, store) = get_loaded_store(dir.clone()).await; + let device = get_device(); + + let changes = Changes { + devices: DeviceChanges { changed: vec![device.clone()], ..Default::default() }, + ..Default::default() + }; + + store.save_changes(changes).await.unwrap(); + + drop(store); + + let store = IndexeddbStore::open_with_name(dir).await.expect("Can't create store"); + + store.load_account().await.unwrap(); + + let loaded_device = + store.get_device(device.user_id(), device.device_id()).await.unwrap().unwrap(); + + assert_eq!(device, loaded_device); + + for algorithm in loaded_device.algorithms() { + assert!(device.algorithms().contains(algorithm)); + } + assert_eq!(device.algorithms().len(), loaded_device.algorithms().len()); + assert_eq!(device.keys(), loaded_device.keys()); + + let user_devices = store.get_user_devices(device.user_id()).await.unwrap(); + assert_eq!(&**user_devices.keys().next().unwrap(), device.device_id()); + assert_eq!(user_devices.values().next().unwrap(), &device); + } + + #[async_test] + async fn device_deleting() { + let dir = "device_deleting".to_owned(); + let (_account, store) = get_loaded_store(dir.clone()).await; + let device = get_device(); + + let changes = Changes { + devices: DeviceChanges { changed: vec![device.clone()], ..Default::default() }, + ..Default::default() + }; + + store.save_changes(changes).await.unwrap(); + + let changes = Changes { + devices: DeviceChanges { deleted: vec![device.clone()], ..Default::default() }, + ..Default::default() + }; + + store.save_changes(changes).await.unwrap(); + drop(store); + + let store = IndexeddbStore::open_with_name(dir).await.expect("Can't create store"); + + store.load_account().await.unwrap(); + + let loaded_device = store.get_device(device.user_id(), device.device_id()).await.unwrap(); + + assert!(loaded_device.is_none()); + } + + #[async_test] + async fn user_saving() { + let dir = "user_saving".to_owned(); + + let user_id = user_id!("@example:localhost"); + let device_id: &DeviceId = "WSKKLTJZCL".into(); + + let store = IndexeddbStore::open_with_name(dir.clone()).await.expect("Can't create store"); + + let account = ReadOnlyAccount::new(&user_id, device_id); + + store.save_account(account.clone()).await.expect("Can't save account"); + + let own_identity = get_own_identity(); + + let changes = Changes { + identities: IdentityChanges { + changed: vec![own_identity.clone().into()], + ..Default::default() + }, + ..Default::default() + }; + + store.save_changes(changes).await.expect("Can't save identity"); + + drop(store); + + let store = IndexeddbStore::open_with_name(dir).await.expect("Can't create store"); + + store.load_account().await.unwrap(); + + let loaded_user = store.get_user_identity(own_identity.user_id()).await.unwrap().unwrap(); + + assert_eq!(loaded_user.master_key(), own_identity.master_key()); + assert_eq!(loaded_user.self_signing_key(), own_identity.self_signing_key()); + assert_eq!(loaded_user, own_identity.clone().into()); + + let other_identity = get_other_identity(); + + let changes = Changes { + identities: IdentityChanges { + changed: vec![other_identity.clone().into()], + ..Default::default() + }, + ..Default::default() + }; + + store.save_changes(changes).await.unwrap(); + + let loaded_user = store.get_user_identity(other_identity.user_id()).await.unwrap().unwrap(); + + assert_eq!(loaded_user.master_key(), other_identity.master_key()); + assert_eq!(loaded_user.self_signing_key(), other_identity.self_signing_key()); + assert_eq!(loaded_user, other_identity.into()); + + own_identity.mark_as_verified(); + + let changes = Changes { + identities: IdentityChanges { + changed: vec![own_identity.into()], + ..Default::default() + }, + ..Default::default() + }; + + store.save_changes(changes).await.unwrap(); + let loaded_user = store.get_user_identity(&user_id).await.unwrap().unwrap(); + assert!(loaded_user.own().unwrap().is_verified()) + } + + #[async_test] + async fn private_identity_saving() { + let dir = "private_identity_saving".to_owned(); + let (_, store) = get_loaded_store(dir).await; + assert!(store.load_identity().await.unwrap().is_none()); + let identity = PrivateCrossSigningIdentity::new(alice_id()).await; + + let changes = Changes { private_identity: Some(identity.clone()), ..Default::default() }; + + store.save_changes(changes).await.unwrap(); + let loaded_identity = store.load_identity().await.unwrap().unwrap(); + assert_eq!(identity.user_id(), loaded_identity.user_id()); + } + + #[async_test] + async fn olm_hash_saving() { + let dir = "olm_hash_saving".to_owned(); + let (_, store) = get_loaded_store(dir).await; + + let hash = + OlmMessageHash { sender_key: "test_sender".to_owned(), hash: "test_hash".to_owned() }; + + let mut changes = Changes::default(); + changes.message_hashes.push(hash.clone()); + + assert!(!store.is_message_known(&hash).await.unwrap()); + store.save_changes(changes).await.unwrap(); + assert!(store.is_message_known(&hash).await.unwrap()); + } + + #[async_test] + async fn key_request_saving() { + let dir = "key_request_saving".to_owned(); + let (account, store) = get_loaded_store(dir).await; + + let id = Uuid::new_v4(); + let info: SecretInfo = RequestedKeyInfo::new( + EventEncryptionAlgorithm::MegolmV1AesSha2, + room_id!("!test:localhost"), + "test_sender_key".to_string(), + "test_session_id".to_string(), + ) + .into(); + + let request = GossipRequest { + request_recipient: account.user_id().to_owned(), + request_id: id, + info: info.clone(), + sent_out: false, + }; + + assert!(store.get_outgoing_secret_requests(id).await.unwrap().is_none()); + + let mut changes = Changes::default(); + changes.key_requests.push(request.clone()); + store.save_changes(changes).await.unwrap(); + + let request = Some(request); + + let stored_request = store.get_outgoing_secret_requests(id).await.unwrap(); + assert_eq!(request, stored_request); + + let stored_request = store.get_secret_request_by_info(&info).await.unwrap(); + assert_eq!(request, stored_request); + assert!(!store.get_unsent_secret_requests().await.unwrap().is_empty()); + + let request = GossipRequest { + request_recipient: account.user_id().to_owned(), + request_id: id, + info: info.clone(), + sent_out: true, + }; + + let mut changes = Changes::default(); + changes.key_requests.push(request.clone()); + store.save_changes(changes).await.unwrap(); + + assert!(store.get_unsent_secret_requests().await.unwrap().is_empty()); + let stored_request = store.get_outgoing_secret_requests(id).await.unwrap(); + assert_eq!(Some(request), stored_request); + + store.delete_outgoing_secret_requests(id).await.unwrap(); + + let stored_request = store.get_outgoing_secret_requests(id).await.unwrap(); + assert_eq!(None, stored_request); + + let stored_request = store.get_secret_request_by_info(&info).await.unwrap(); + assert_eq!(None, stored_request); + assert!(store.get_unsent_secret_requests().await.unwrap().is_empty()); + } +} diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index 69c9ec9fd..ebbd1689d 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -277,13 +277,17 @@ impl CryptoStore for MemoryStore { mod test { use ruma::room_id; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; + use matrix_sdk_test::async_test; + use crate::{ identities::device::test::get_device, olm::{test::get_account_and_session, InboundGroupSession, OlmMessageHash}, store::{memorystore::MemoryStore, Changes, CryptoStore}, }; - #[tokio::test] + #[async_test] async fn test_session_store() { let (account, session) = get_account_and_session().await; let store = MemoryStore::new(); @@ -301,7 +305,7 @@ mod test { assert_eq!(&session, loaded_session); } - #[tokio::test] + #[async_test] async fn test_group_session_store() { let (account, _) = get_account_and_session().await; let room_id = room_id!("!test:localhost"); @@ -328,7 +332,7 @@ mod test { assert_eq!(inbound, loaded_session); } - #[tokio::test] + #[async_test] async fn test_device_store() { let device = get_device(); let store = MemoryStore::new(); @@ -353,7 +357,7 @@ mod test { assert!(store.get_device(device.user_id(), device.device_id()).await.unwrap().is_none()); } - #[tokio::test] + #[async_test] async fn test_tracked_users() { let device = get_device(); let store = MemoryStore::new(); @@ -364,7 +368,7 @@ mod test { assert!(store.is_user_tracked(device.user_id())); } - #[tokio::test] + #[async_test] async fn test_message_hash() { let store = MemoryStore::new(); diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index da22ff544..0faac01d8 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -42,6 +42,8 @@ mod memorystore; mod pickle_key; #[cfg(feature = "sled_cryptostore")] pub(crate) mod sled; +#[cfg(feature = "indexeddb_cryptostore")] +pub(crate) mod indexeddb; use std::{ collections::{HashMap, HashSet}, @@ -67,6 +69,11 @@ use zeroize::Zeroize; #[cfg(feature = "sled_cryptostore")] pub use self::sled::SledStore; +#[cfg(feature = "indexeddb_cryptostore")] +pub use self::indexeddb::IndexeddbStore; +#[cfg(feature = "indexeddb_cryptostore")] +use indexed_db_futures::web_sys::DomException; + use crate::{ error::SessionUnpicklingError, identities::{ @@ -520,6 +527,18 @@ pub enum CryptoStoreError { #[error(transparent)] Database(#[from] sled::Error), + /// Error in the internal database + #[cfg(feature = "indexeddb_cryptostore")] + #[error("IndexDB error: {name} ({code}): {message}")] + IndexedDatabase { + /// DomException code + code: u16, + /// Specific name of the DomException + name: String, + /// Message given to the DomException + message: String, + }, + /// An IO error occurred. #[error(transparent)] Io(#[from] IoError), @@ -553,6 +572,14 @@ pub enum CryptoStoreError { Serialization(#[from] SerdeError), } + +#[cfg(feature = "indexeddb_cryptostore")] +impl From for CryptoStoreError { + fn from(frm: DomException) -> CryptoStoreError { + CryptoStoreError::IndexedDatabase { name: frm.name(), message: frm.message(), code: frm.code() } + } +} + /// Trait abstracting a store that the `OlmMachine` uses to store cryptographic /// keys. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/crates/matrix-sdk-crypto/src/store/pickle_key.rs b/crates/matrix-sdk-crypto/src/store/pickle_key.rs index 03d85f5e2..32b41ee15 100644 --- a/crates/matrix-sdk-crypto/src/store/pickle_key.rs +++ b/crates/matrix-sdk-crypto/src/store/pickle_key.rs @@ -74,7 +74,7 @@ pub struct EncryptedPickleKey { /// Olm uses AES256 to encrypt accounts, sessions, inbound group sessions. We /// also implement our own pickling for the cross-signing types using /// AES256-GCM so the key sizes match. -#[derive(Debug, Zeroize, PartialEq)] +#[derive(Debug, Zeroize, Serialize, Deserialize, PartialEq)] pub struct PickleKey { aes256_key: Vec, } diff --git a/crates/matrix-sdk-crypto/src/store/sled.rs b/crates/matrix-sdk-crypto/src/store/sled.rs index fdfdd6175..0e299b949 100644 --- a/crates/matrix-sdk-crypto/src/store/sled.rs +++ b/crates/matrix-sdk-crypto/src/store/sled.rs @@ -136,7 +136,7 @@ pub struct AccountInfo { identity_keys: Arc, } -/// An in-memory only store that will forget all the E2EE key once it's dropped. +/// Storing crypto with sled #[derive(Clone)] pub struct SledStore { account_info: Arc>>, diff --git a/crates/matrix-sdk-crypto/src/verification/machine.rs b/crates/matrix-sdk-crypto/src/verification/machine.rs index 0d22659fd..14ecd73e8 100644 --- a/crates/matrix-sdk-crypto/src/verification/machine.rs +++ b/crates/matrix-sdk-crypto/src/verification/machine.rs @@ -523,6 +523,10 @@ mod test { }; use matrix_sdk_common::locks::Mutex; + + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; + use matrix_sdk_test::async_test; use ruma::{DeviceId, UserId}; use super::{Sas, VerificationMachine}; @@ -596,7 +600,7 @@ mod test { let _ = VerificationMachine::new(alice, identity, Arc::new(store)); } - #[tokio::test] + #[async_test] async fn full_flow() { let (alice_machine, bob) = setup_verification_machine().await; @@ -643,7 +647,7 @@ mod test { } #[cfg(target_os = "linux")] - #[tokio::test] + #[async_test] async fn timing_out() { let (alice_machine, bob) = setup_verification_machine().await; let alice = alice_machine.get_sas(bob.user_id(), bob.flow_id().as_str()).unwrap(); diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index 0dc8f3887..3979c5938 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -667,6 +667,9 @@ impl IdentitiesBeingVerified { #[cfg(test)] pub(crate) mod test { + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; + use matrix_sdk_test::async_test; use std::convert::TryInto; use ruma::{ diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index b6d27f882..7bb5b6d8f 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -1266,6 +1266,9 @@ struct Done {} #[cfg(test)] mod test { + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; + use std::convert::{TryFrom, TryInto}; use matrix_sdk_test::async_test; diff --git a/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs b/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs index eadaa60fe..f085c0646 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs @@ -540,7 +540,7 @@ fn bytes_to_decimal(bytes: Vec) -> (u16, u16, u16) { (first + 1000, second + 1000, third + 1000) } -#[cfg(test)] +#[cfg(all(test, not(target_arch = "wasm32")))] mod test { use proptest::prelude::*; use ruma::events::key::verification::start::ToDeviceKeyVerificationStartEventContent; diff --git a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs index b3c128056..073571945 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs @@ -556,6 +556,9 @@ impl AcceptSettings { #[cfg(test)] mod test { use std::{convert::TryFrom, sync::Arc}; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; + use matrix_sdk_test::async_test; use ruma::{DeviceId, UserId}; @@ -586,7 +589,7 @@ mod test { "BOBDEVCIE".into() } - #[tokio::test] + #[async_test] async fn sas_wrapper_full() { let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id()); let alice_device = ReadOnlyDevice::from_account(&alice).await; diff --git a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs index 2e6de5f4f..9902bc603 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs @@ -1137,6 +1137,10 @@ impl SasState { mod test { use std::convert::TryFrom; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::wasm_bindgen_test; + use matrix_sdk_test::async_test; + use ruma::{ events::key::verification::{ accept::{AcceptMethod, ToDeviceKeyVerificationAcceptEventContent}, @@ -1195,12 +1199,12 @@ mod test { (alice_sas, bob_sas) } - #[tokio::test] + #[async_test] async fn create_sas() { let (_, _) = get_sas_pair().await; } - #[tokio::test] + #[async_test] async fn sas_accept() { let (alice, bob) = get_sas_pair().await; let content = bob.as_content(); @@ -1209,7 +1213,7 @@ mod test { alice.into_accepted(bob.user_id(), &content).unwrap(); } - #[tokio::test] + #[async_test] async fn sas_key_share() { let (alice, bob) = get_sas_pair().await; @@ -1231,7 +1235,7 @@ mod test { assert_eq!(alice.get_emoji(), bob.get_emoji()); } - #[tokio::test] + #[async_test] async fn sas_full() { let (alice, bob) = get_sas_pair().await; @@ -1272,7 +1276,7 @@ mod test { assert!(alice.verified_devices().contains(&alice.other_device())); } - #[tokio::test] + #[async_test] async fn sas_invalid_commitment() { let (alice, bob) = get_sas_pair().await; @@ -1301,7 +1305,7 @@ mod test { .expect_err("Didn't cancel on invalid commitment"); } - #[tokio::test] + #[async_test] async fn sas_invalid_sender() { let (alice, bob) = get_sas_pair().await; @@ -1311,7 +1315,7 @@ mod test { alice.into_accepted(&sender, &content).expect_err("Didn't cancel on a invalid sender"); } - #[tokio::test] + #[async_test] async fn sas_unknown_sas_method() { let (alice, bob) = get_sas_pair().await; @@ -1332,7 +1336,7 @@ mod test { .expect_err("Didn't cancel on an invalid SAS method"); } - #[tokio::test] + #[async_test] async fn sas_unknown_method() { let (alice, bob) = get_sas_pair().await; @@ -1351,7 +1355,7 @@ mod test { .expect_err("Didn't cancel on an unknown SAS method"); } - #[tokio::test] + #[async_test] async fn sas_from_start_unknown_method() { let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id()); let alice_device = ReadOnlyDevice::from_account(&alice).await;