From a2bfa08e0984c4a083b20a41ef0968379dc3c003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 19 Aug 2020 09:23:03 +0200 Subject: [PATCH 01/24] crypto: Initial decryption method for key exports. --- matrix_sdk_crypto/Cargo.toml | 6 ++ matrix_sdk_crypto/src/key_export.rs | 85 +++++++++++++++++++++++++++++ matrix_sdk_crypto/src/lib.rs | 2 + 3 files changed, 93 insertions(+) create mode 100644 matrix_sdk_crypto/src/key_export.rs diff --git a/matrix_sdk_crypto/Cargo.toml b/matrix_sdk_crypto/Cargo.toml index e1ec78b2..08b544b2 100644 --- a/matrix_sdk_crypto/Cargo.toml +++ b/matrix_sdk_crypto/Cargo.toml @@ -37,6 +37,12 @@ thiserror = "1.0.20" tracing = "0.1.19" atomic = "0.5.0" dashmap = "3.11.10" +sha2 = "0.9.1" +aes-ctr = "0.4.0" +pbkdf2 = { version = "0.5.0", default-features = false } +hmac = "0.9.0" +base64 = "0.12.3" +byteorder = "1.3.4" [dependencies.tracing-futures] version = "0.2.4" diff --git a/matrix_sdk_crypto/src/key_export.rs b/matrix_sdk_crypto/src/key_export.rs new file mode 100644 index 00000000..e8492be6 --- /dev/null +++ b/matrix_sdk_crypto/src/key_export.rs @@ -0,0 +1,85 @@ +// 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. + +use std::io::{Cursor, Read, Seek, SeekFrom}; + +use base64::{decode_config, encode_config, DecodeError, STANDARD_NO_PAD}; +use byteorder::{BigEndian, ReadBytesExt}; + +use aes_ctr::{ + stream_cipher::{NewStreamCipher, SyncStreamCipher}, + Aes128Ctr, +}; +use hmac::{Hmac, Mac, NewMac}; +use pbkdf2::pbkdf2; +use sha2::{Sha256, Sha512}; + +const SALT_SIZE: usize = 16; +const IV_SIZE: usize = 16; +const MAC_SIZE: usize = 32; +const KEY_SIZE: usize = 16; + +pub fn decode(input: impl AsRef<[u8]>) -> Result, DecodeError> { + decode_config(input, STANDARD_NO_PAD) +} + +pub fn encode(input: impl AsRef<[u8]>) -> String { + encode_config(input, STANDARD_NO_PAD) +} + +pub fn decrypt(ciphertext: &str, passphrase: String) -> Result { + let decoded = decode(ciphertext)?; + + let mut decoded = Cursor::new(decoded); + + let mut salt = [0u8; SALT_SIZE]; + let mut iv = [0u8; IV_SIZE]; + let mut mac = [0u8; MAC_SIZE]; + let mut derived_keys = [0u8; KEY_SIZE * 2]; + + let version = decoded.read_u8().unwrap(); + + decoded.read_exact(&mut salt).unwrap(); + decoded.read_exact(&mut iv).unwrap(); + + let rounds = decoded.read_u32::().unwrap(); + let ciphertext_start = decoded.position() as usize; + + decoded.seek(SeekFrom::End(-32)).unwrap(); + let ciphertext_end = decoded.position() as usize; + + decoded.read_exact(&mut mac).unwrap(); + + let mut decoded = decoded.into_inner(); + let ciphertext = &decoded[0..ciphertext_end]; + + if version != 1u8 { + panic!("Unsupported version") + } + + pbkdf2::>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys); + let (key, hmac_key) = derived_keys.split_at(KEY_SIZE / 2); + + let mut hmac = Hmac::::new_varkey(hmac_key).unwrap(); + hmac.update(ciphertext); + hmac.verify(&mac).expect("MAC DOESN'T MATCH"); + + let mut ciphertext = &mut decoded[ciphertext_start..ciphertext_end]; + + let mut aes = Aes128Ctr::new_var(&key, &iv).expect("Can't create AES"); + + aes.apply_keystream(&mut ciphertext); + + Ok(String::from_utf8(ciphertext.to_owned()).expect("Invalid utf-8")) +} diff --git a/matrix_sdk_crypto/src/lib.rs b/matrix_sdk_crypto/src/lib.rs index 38630f32..bf05600f 100644 --- a/matrix_sdk_crypto/src/lib.rs +++ b/matrix_sdk_crypto/src/lib.rs @@ -29,6 +29,8 @@ mod device; mod error; +#[allow(dead_code)] +mod key_export; mod machine; pub mod memory_stores; pub mod olm; From 8dbc7c38e5e4d0b3db4bd41d18ba8fedf6a6a2f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 4 Sep 2020 16:34:19 +0200 Subject: [PATCH 02/24] crypto: Correctly split the 2 keys in the key export logic. --- matrix_sdk_crypto/Cargo.toml | 1 + matrix_sdk_crypto/src/key_export.rs | 67 ++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/matrix_sdk_crypto/Cargo.toml b/matrix_sdk_crypto/Cargo.toml index ae416460..495cfb6b 100644 --- a/matrix_sdk_crypto/Cargo.toml +++ b/matrix_sdk_crypto/Cargo.toml @@ -63,3 +63,4 @@ serde_json = "1.0.57" tempfile = "3.1.0" http = "0.2.1" matrix-sdk-test = { version = "0.1.0", path = "../matrix_sdk_test" } +indoc = "1.0.2" diff --git a/matrix_sdk_crypto/src/key_export.rs b/matrix_sdk_crypto/src/key_export.rs index e8492be6..5d6fc057 100644 --- a/matrix_sdk_crypto/src/key_export.rs +++ b/matrix_sdk_crypto/src/key_export.rs @@ -19,7 +19,7 @@ use byteorder::{BigEndian, ReadBytesExt}; use aes_ctr::{ stream_cipher::{NewStreamCipher, SyncStreamCipher}, - Aes128Ctr, + Aes256Ctr, }; use hmac::{Hmac, Mac, NewMac}; use pbkdf2::pbkdf2; @@ -28,7 +28,7 @@ use sha2::{Sha256, Sha512}; const SALT_SIZE: usize = 16; const IV_SIZE: usize = 16; const MAC_SIZE: usize = 32; -const KEY_SIZE: usize = 16; +const KEY_SIZE: usize = 32; pub fn decode(input: impl AsRef<[u8]>) -> Result, DecodeError> { decode_config(input, STANDARD_NO_PAD) @@ -38,7 +38,7 @@ pub fn encode(input: impl AsRef<[u8]>) -> String { encode_config(input, STANDARD_NO_PAD) } -pub fn decrypt(ciphertext: &str, passphrase: String) -> Result { +pub fn decrypt(ciphertext: &str, passphrase: &str) -> Result { let decoded = decode(ciphertext)?; let mut decoded = Cursor::new(decoded); @@ -49,7 +49,6 @@ pub fn decrypt(ciphertext: &str, passphrase: String) -> Result Result>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys); - let (key, hmac_key) = derived_keys.split_at(KEY_SIZE / 2); + let (key, hmac_key) = derived_keys.split_at(KEY_SIZE); let mut hmac = Hmac::::new_varkey(hmac_key).unwrap(); - hmac.update(ciphertext); + hmac.update(&decoded[0..ciphertext_end]); hmac.verify(&mac).expect("MAC DOESN'T MATCH"); let mut ciphertext = &mut decoded[ciphertext_start..ciphertext_end]; - - let mut aes = Aes128Ctr::new_var(&key, &iv).expect("Can't create AES"); - + let mut aes = Aes256Ctr::new_var(&key, &iv).expect("Can't create AES"); aes.apply_keystream(&mut ciphertext); Ok(String::from_utf8(ciphertext.to_owned()).expect("Invalid utf-8")) } + +#[cfg(test)] +mod test { + use indoc::indoc; + + use super::{decode, decrypt}; + + const PASSPHRASE: &str = "1234"; + + const TEST_EXPORT: &str = indoc! {" + -----BEGIN MEGOLM SESSION DATA----- + AfukAbxZSTcn8SRQG/evmxHHzVGkkH7vmE2s0wWwebk+AAehIMVgIRmYAIPoxhSB4mKrsmoTbZuP4urLaWYPwCi22JhW5yvhGWp92vArL9gnN+2rB6VjDjzgR/OHkuLc + Tv5bWKNXPubrdWQujLmzQHnSqxPmNxpEbiDBLFIjuw2gKt8m8KKZGWcOk5sdtBVA7N8pje9urQE8e+rOT7q5yx4yeydtmZC5fb38/5YOn3E8hfpSC6jpXS+3jo/0so36 + 4c4l25CkkKWc07Ayhg9OjsEmDuHYuOO13r/TXojlfkaagBh3v8ZZb3+eWE4CeTV7jwVYbEy44vSKgACwLGdNX0/4TfjgfWBvOjF50h6YnprVD+vhbrG4NLg/TpdqiJ8p + pbp6t12vUMGULQudooXGvcsCoga6p9gS+pfxn9yhONBPU+pBFo+1Fnq80ZN8ErjVv58n3hLpH7YbvFwBPLBAj2h7dCHtj92E14jSgUvg+vRAAI5TwcZfuQwzH5qL6+Zh + 1+pht9RsqplnbbdR32M3lypncAWgsUYtR/4wEBwlTSYCFW3GSm/Ow9XaWKF5RCZf9UTlOJ5veSkDZW61GCVLgsZj06Y+sji7IN7kGOBv4SfkYbhloLm0xPFCuQTbijqh + OCdzwH/lApCZflqBHLwAWsPuLrhgax99xs/QN9MIx8hh/pRc0pNNrBOgF1SJWQ/ChAuB7KbHcf4k1IFM0I4XH6u2GKpOxsQtulTulX3Sm+gCo0JBTO1DdYZPe7x0Enhl + dYojj6op4+HpJUw6Alh2g4SeCmZRcaux+0hwSCkuPRBX4fwgv+Qj0abgqATaLTGI6VXAP3ya4thaNNfEurj3+20b39VL6Bz8mW5g8aWo9DZ7/Ph6t042613wz9rKKrFv + ozKEqUX9Rbs01IknK2e8iakUKvzQjZ0ezs+XBy/0OKfyc+0KIj4vFvoTL9TePmOdBNNFQz91s21YDziE3fO8jJQLZGgf91ttCdtoouT2RAM1J7uIw0+4Ar0ytE2GiUgO + w/Zbo7M8CifIJ6CVM6F1dVdX7hj61lsbhzIV46xhscH6KszGioWD8zUaaXwt5b3Hrvsy4YbIRLhkVq1sZR/ETC+dqbSa/1cYngNwkoVspksK3JAaK1xVWGHFboXnxOtN + S+iWtIfWOIy9ExfA9+bz4y1s/VkruQAIIJJg/KH2bCsGMM+FHO3OaMOf4bvdnGH0SKpAk0WqVVZME6rTw5XqObqi5AF7Y+m8wyK87zdn3+n6JPBb7ROh/m4kuOcNvs9A + Gkhi8K4Q+/Ymeq6NNjkuvdKrlJxzdiwggCTA941158xnrs470QC0R/xDKfoZeFLWW6GhbwO6AuRy1vg4lkidrrD/zrczTMtOajhqL6CGARiufdbmByNVo1sybwTT/jOh + /divg/jY1RNY+IbEjOOWKnVcxrYURUFWjur4BgwkGBy9RovZRK6Co0gCXGwsdFrR9LG7jnGXiXm2fjGYDQghj8opC3wVtC7O0DQVrjDGRTZPOjoIgzvRomt7wbFosrww + Bw5RxiL5bA7bI9h1KuIPfR4+C2LjuRdnNSX0s+iQkz1cbwfGsa2pN1unzPwC0Gul6tciu2A32akfDQVzJrSCDahnoQXVZEEk0TdPY2oIZsEOmWZf94GQKr1IR7vJ37xj + iaYFKid+9F+ELlk2i6DQYDzXMacg55mWLuQkxC8MR5L9mPTtwoeVjbpbdzYctKdJ9KqeZTSh0qaSRnzuy3F7HnifwhkKI5JjyBQ5734gUmrikAfvtcBcpcd3ctX3+cwN + VwFxyQcwkTWNt4AQnzNVM3ZCxLEyMGqZ7XR3FQoxw8xTXJ4tSclI/g6vzKMVjHmuPRSMQj8PLhpICRnk5k50UmwS+rZTVqnHeJ+XF6JrEOYGgfOIVUUG5N+NCiBgvQKr + xa0ImLQXzJIdJqB2CLWIWytaqGSY4+Px2jSxQCIhl4PSBs5Ia1iBgZ3578Dp0DYz0caEzggDurLHrth1kVzC3zqm/D+uPBhF3iMOyp7gHuMOsCY1LKB2eyiizz1Hrhb4 + ZBGGo15dC0KMP9uhmDeaijoH7YO5wReD427KNf5Z4kIFrgP0ebGr3sObZm74yJBlfnR0kKhFsQN7b/WthZIna7F/zIQaPYncitJqMTIgHXptrhaCV+c7AARs1pw/cnOQ + bH5evUA2zibgqaAOselrgnxbIqG2LQk84184b7V4Lg0+ebAjtTEVGqAiVVhydA7OCXETWfrS5le0W1/DAtv80K6KBMCkS33rKHUdTPru2fNGmo+angp9lO/ANQ== + -----END MEGOLM SESSION DATA----- + "}; + + fn export_wihtout_headers() -> String { + TEST_EXPORT + .lines() + .filter(|l| !l.starts_with("-----")) + .collect() + } + + #[test] + fn test_decode() { + let export = export_wihtout_headers(); + assert!(decode(export).is_ok()); + } + + #[test] + fn test_decrypt() { + let export = export_wihtout_headers(); + let decrypted = decrypt(&export, PASSPHRASE).expect("Can't decrypt key export"); + } +} From f57447527d9227857db7a086527492d1bb8e2347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 4 Sep 2020 17:59:56 +0200 Subject: [PATCH 03/24] crypto: Initial logic for encrypting key exports. --- matrix_sdk_crypto/Cargo.toml | 3 +- matrix_sdk_crypto/src/key_export.rs | 54 +++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/matrix_sdk_crypto/Cargo.toml b/matrix_sdk_crypto/Cargo.toml index 495cfb6b..3ee88c84 100644 --- a/matrix_sdk_crypto/Cargo.toml +++ b/matrix_sdk_crypto/Cargo.toml @@ -25,7 +25,8 @@ async-trait = "0.1.40" matrix-sdk-common-macros = { version = "0.1.0", path = "../matrix_sdk_common_macros" } matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" } -olm-rs = { git = 'https://gitlab.gnome.org/jhaye/olm-rs/', features = ["serde"]} +olm-rs = { version = "0.6.0", features = ["serde"] } +getrandom = "0.1.14" serde = { version = "1.0.115", features = ["derive", "rc"] } serde_json = "1.0.57" cjson = "0.1.1" diff --git a/matrix_sdk_crypto/src/key_export.rs b/matrix_sdk_crypto/src/key_export.rs index 5d6fc057..d4b4045a 100644 --- a/matrix_sdk_crypto/src/key_export.rs +++ b/matrix_sdk_crypto/src/key_export.rs @@ -16,6 +16,7 @@ use std::io::{Cursor, Read, Seek, SeekFrom}; use base64::{decode_config, encode_config, DecodeError, STANDARD_NO_PAD}; use byteorder::{BigEndian, ReadBytesExt}; +use getrandom::getrandom; use aes_ctr::{ stream_cipher::{NewStreamCipher, SyncStreamCipher}, @@ -38,6 +39,42 @@ pub fn encode(input: impl AsRef<[u8]>) -> String { encode_config(input, STANDARD_NO_PAD) } +pub fn encrypt(mut plaintext: &mut [u8], passphrase: &str, rounds: u32) -> String { + let mut salt = [0u8; SALT_SIZE]; + let mut iv = [0u8; IV_SIZE]; + let mut derived_keys = [0u8; KEY_SIZE * 2]; + let version: u8 = 1; + + getrandom(&mut salt).expect("Can't generate randomness"); + getrandom(&mut iv).expect("Can't generate randomness"); + + let mut iv = u128::from_be_bytes(iv); + iv &= !(1 << 63); + + pbkdf2::>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys); + let (key, hmac_key) = derived_keys.split_at(KEY_SIZE); + + let mut aes = Aes256Ctr::new_var(&key, &iv.to_be_bytes()).expect("Can't create AES"); + + aes.apply_keystream(&mut plaintext); + + let mut payload: Vec = vec![]; + + payload.extend(&version.to_be_bytes()); + payload.extend(&salt); + payload.extend(&iv.to_be_bytes()); + payload.extend(&rounds.to_be_bytes()); + payload.extend_from_slice(&plaintext); + + let mut hmac = Hmac::::new_varkey(hmac_key).unwrap(); + hmac.update(&payload); + let mac = hmac.finalize(); + + payload.extend(mac.into_bytes()); + + encode(payload) +} + pub fn decrypt(ciphertext: &str, passphrase: &str) -> Result { let decoded = decode(ciphertext)?; @@ -84,7 +121,7 @@ pub fn decrypt(ciphertext: &str, passphrase: &str) -> Result Date: Mon, 7 Sep 2020 16:49:36 +0200 Subject: [PATCH 04/24] crypto: Identities add some methods to get the keys/signatures of the keys. --- matrix_sdk_crypto/src/identities/mod.rs | 2 +- matrix_sdk_crypto/src/identities/user.rs | 63 +++++++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/matrix_sdk_crypto/src/identities/mod.rs b/matrix_sdk_crypto/src/identities/mod.rs index 853447dc..c1941b50 100644 --- a/matrix_sdk_crypto/src/identities/mod.rs +++ b/matrix_sdk_crypto/src/identities/mod.rs @@ -41,7 +41,7 @@ //! Both identity sets need to reqularly fetched from the server using the //! `/keys/query` API call. pub(crate) mod device; -mod user; +pub(crate) mod user; pub use device::{Device, LocalTrust, ReadOnlyDevice, UserDevices}; pub use user::{ diff --git a/matrix_sdk_crypto/src/identities/user.rs b/matrix_sdk_crypto/src/identities/user.rs index 1444e7ac..d3a78979 100644 --- a/matrix_sdk_crypto/src/identities/user.rs +++ b/matrix_sdk_crypto/src/identities/user.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::{ + collections::{btree_map::Iter, BTreeMap}, convert::TryFrom, sync::{ atomic::{AtomicBool, Ordering}, @@ -24,7 +25,7 @@ use serde::{Deserialize, Serialize}; use serde_json::to_value; use matrix_sdk_common::{ - api::r0::keys::CrossSigningKey, + api::r0::keys::{CrossSigningKey, KeyUsage}, identifiers::{DeviceKeyId, UserId}, }; @@ -117,6 +118,21 @@ impl MasterPubkey { &self.0.user_id } + /// Get the keys map of containing the master keys. + pub fn keys(&self) -> &BTreeMap { + &self.0.keys + } + + /// Get the list of `KeyUsage` that is set for this key. + pub fn usage(&self) -> &[KeyUsage] { + &self.0.usage + } + + /// Get the signatures map of this cross signing key. + pub fn signatures(&self) -> &BTreeMap> { + &self.0.signatures + } + /// Get the master key with the given key id. /// /// # Arguments @@ -167,12 +183,26 @@ impl MasterPubkey { } } +impl<'a> IntoIterator for &'a MasterPubkey { + type Item = (&'a String, &'a String); + type IntoIter = Iter<'a, String, String>; + + fn into_iter(self) -> Self::IntoIter { + self.keys().iter() + } +} + impl UserSigningPubkey { /// Get the user id of the user signing key's owner. pub fn user_id(&self) -> &UserId { &self.0.user_id } + /// Get the keys map of containing the user signing keys. + pub fn keys(&self) -> &BTreeMap { + &self.0.keys + } + /// Check if the given master key is signed by this user signing key. /// /// # Arguments @@ -202,12 +232,26 @@ impl UserSigningPubkey { } } +impl<'a> IntoIterator for &'a UserSigningPubkey { + type Item = (&'a String, &'a String); + type IntoIter = Iter<'a, String, String>; + + fn into_iter(self) -> Self::IntoIter { + self.keys().iter() + } +} + impl SelfSigningPubkey { /// Get the user id of the self signing key's owner. pub fn user_id(&self) -> &UserId { &self.0.user_id } + /// Get the keys map of containing the self signing keys. + pub fn keys(&self) -> &BTreeMap { + &self.0.keys + } + /// Check if the given device is signed by this self signing key. /// /// # Arguments @@ -236,6 +280,15 @@ impl SelfSigningPubkey { } } +impl<'a> IntoIterator for &'a SelfSigningPubkey { + type Item = (&'a String, &'a String); + type IntoIter = Iter<'a, String, String>; + + fn into_iter(self) -> Self::IntoIter { + self.keys().iter() + } +} + /// Enum over the different user identity types we can have. #[derive(Debug, Clone)] pub enum UserIdentities { @@ -245,6 +298,12 @@ pub enum UserIdentities { Other(UserIdentity), } +impl From for UserIdentities { + fn from(identity: OwnUserIdentity) -> Self { + UserIdentities::Own(identity) + } +} + impl UserIdentities { /// The unique user id of this identity. pub fn user_id(&self) -> &UserId { @@ -504,7 +563,7 @@ impl OwnUserIdentity { } #[cfg(test)] -mod test { +pub(crate) mod test { use serde_json::json; use std::{convert::TryFrom, sync::Arc}; From 083cebe735577cd4687ac7e005996ef89a9674c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 7 Sep 2020 16:57:58 +0200 Subject: [PATCH 05/24] crypto: Initial WIP user identity storing logic. --- matrix_sdk_crypto/src/identities/user.rs | 4 + matrix_sdk_crypto/src/store/sqlite.rs | 263 ++++++++++++++++++++++- 2 files changed, 261 insertions(+), 6 deletions(-) diff --git a/matrix_sdk_crypto/src/identities/user.rs b/matrix_sdk_crypto/src/identities/user.rs index d3a78979..5893b7ad 100644 --- a/matrix_sdk_crypto/src/identities/user.rs +++ b/matrix_sdk_crypto/src/identities/user.rs @@ -756,6 +756,10 @@ pub(crate) mod test { OwnUserIdentity::new(master_key.into(), self_signing.into(), user_signing.into()).unwrap() } + pub(crate) fn get_own_identity() -> OwnUserIdentity { + own_identity(&own_key_query()) + } + #[test] fn own_identity_create() { let user_id = user_id!("@example:localhost"); diff --git a/matrix_sdk_crypto/src/store/sqlite.rs b/matrix_sdk_crypto/src/store/sqlite.rs index 50b0b5f4..f4619a59 100644 --- a/matrix_sdk_crypto/src/store/sqlite.rs +++ b/matrix_sdk_crypto/src/store/sqlite.rs @@ -21,8 +21,9 @@ use std::{ }; use async_trait::async_trait; -use dashmap::DashSet; +use dashmap::{DashMap, DashSet}; use matrix_sdk_common::{ + api::r0::keys::{CrossSigningKey, KeyUsage}, identifiers::{ DeviceId, DeviceKeyAlgorithm, DeviceKeyId, EventEncryptionAlgorithm, RoomId, UserId, }, @@ -135,8 +136,8 @@ impl SqliteStore { passphrase: Option>, ) -> Result { let url = SqliteStore::path_to_url(path.as_ref())?; - let connection = SqliteConnection::connect(url.as_ref()).await?; + let store = SqliteStore { user_id: Arc::new(user_id.to_owned()), device_id: Arc::new(device_id.into()), @@ -151,6 +152,7 @@ impl SqliteStore { users_for_key_query: Arc::new(DashSet::new()), }; store.create_tables().await?; + Ok(store) } @@ -310,6 +312,61 @@ impl SqliteStore { ) .await?; + connection + .execute( + r#" + CREATE TABLE IF NOT EXISTS users ( + "id" INTEGER NOT NULL PRIMARY KEY, + "account_id" INTEGER NOT NULL, + "user_id" TEXT NOT NULL, + FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") + ON DELETE CASCADE + UNIQUE(account_id,user_id) + ); + + CREATE INDEX IF NOT EXISTS "users_account_id" ON "users" ("account_id"); + "#, + ) + .await?; + + connection + .execute( + r#" + CREATE TABLE IF NOT EXISTS user_keys ( + "id" INTEGER NOT NULL PRIMARY KEY, + "key" TEXT NOT NULL, + "key_id" TEXT NOT NULL, + "key_type" TEXT NOT NULL, + "usage" TEXT NOT NULL, + "user_id" INTEGER NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE + UNIQUE(user_id, key_id, key_type) + ); + + CREATE INDEX IF NOT EXISTS "user_keys_user_id" ON "users" ("user_id"); + "#, + ) + .await?; + + connection + .execute( + r#" + CREATE TABLE IF NOT EXISTS user_key_signatures ( + "id" INTEGER NOT NULL PRIMARY KEY, + "user_id" TEXT NOT NULL, + "key_id" INTEGER NOT NULL, + "signature" TEXT NOT NULL, + "user_key" INTEGER NOT NULL, + FOREIGN KEY ("user_key") REFERENCES "user_keys" ("id") + ON DELETE CASCADE + UNIQUE(user_id, key_id, user_key) + ); + + CREATE INDEX IF NOT EXISTS "user_keys_device_id" ON "device_keys" ("device_id"); + "#, + ) + .await?; + Ok(()) } @@ -670,6 +727,159 @@ impl SqliteStore { None => PicklingMode::Unencrypted, } } + + async fn load_user(&self, user_id: &UserId) -> Result> { + let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; + let mut connection = self.connection.lock().await; + + let row: Option<(i64,)> = + query_as("SELECT id FROM users WHERE account_id = ? and user_id = ?") + .bind(account_id) + .bind(user_id.as_str()) + .fetch_optional(&mut *connection) + .await?; + + let user_row_id = if let Some(row) = row { + row.0 + } else { + return Ok(None); + }; + + let key_rows: Vec<(i64, String, String, String)> = query_as( + "SELECT id, key_id, key, usage FROM user_keys WHERE user_id = ? and key_type = ?", + ) + .bind(user_row_id) + .bind("master_key") + .fetch_all(&mut *connection) + .await?; + + let mut keys = BTreeMap::new(); + let mut signatures = BTreeMap::new(); + let mut key_usage = HashSet::new(); + + for row in key_rows { + let key_row_id = row.0; + let key_id = row.1; + let key = row.2; + let usage: Vec = serde_json::from_str(&row.3)?; + + keys.insert(key_id, key); + key_usage.extend(usage); + + let mut signature_rows: Vec<(String, String, String)> = query_as( + "SELECT user_id, key_id, signature, FROM user_key_signatures WHERE user_key = ?", + ) + .bind(user_row_id) + .bind("master_key") + .fetch_all(&mut *connection) + .await?; + + for row in signature_rows.drain(..) { + let user_id = if let Ok(u) = UserId::try_from(row.0) { + u + } else { + continue; + }; + + let key_id = row.1; + let signature = row.2; + + signatures + .entry(user_id) + .or_insert_with(BTreeMap::new) + .insert(key_id, signature); + } + } + + let usage: Vec = key_usage + .iter() + .filter_map(|u| serde_json::from_str(u).ok()) + .collect(); + + let key = CrossSigningKey { + user_id: user_id.to_owned(), + usage, + keys, + signatures, + }; + + Ok(None) + } + + async fn save_user_helper(&self, user: &UserIdentities) -> Result<()> { + let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; + + let mut connection = self.connection.lock().await; + + query( + "INSERT OR IGNORE INTO users ( + account_id, user_id + ) VALUES (?1, ?2) + ", + ) + .bind(account_id) + .bind(user.user_id().as_str()) + .execute(&mut *connection) + .await?; + + let row: (i64,) = query_as( + "SELECT id FROM users + WHERE account_id = ? and user_id = ?", + ) + .bind(account_id) + .bind(user.user_id().as_str()) + .fetch_one(&mut *connection) + .await?; + + let user_row_id = row.0; + + for (key_id, key) in user.master_key() { + query( + "INSERT OR IGNORE INTO user_keys ( + user_id, key_type, key_id, key, usage + ) VALUES (?1, ?2, ?3, ?4, ?5) + ", + ) + .bind(user_row_id) + .bind("master_key") + .bind(key_id.as_str()) + .bind(key) + .bind(serde_json::to_string(user.master_key().usage())?) + .execute(&mut *connection) + .await?; + + let row: (i64,) = query_as( + "SELECT id FROM user_keys + WHERE user_id = ? and key_id = ? and key_type = ?", + ) + .bind(user_row_id) + .bind(key_id.as_str()) + .bind("master_key") + .fetch_one(&mut *connection) + .await?; + + let key_row_id = row.0; + + for (user_id, signature_map) in user.master_key().signatures() { + for (key_id, signature) in signature_map { + query( + "INSERT OR IGNORE INTO user_key_signatures ( + user_key, user_id, key_id, signature + ) VALUES (?1, ?2, ?3, ?4) + ", + ) + .bind(key_row_id) + .bind(user_id.as_str()) + .bind(key_id.as_str()) + .bind(signature) + .execute(&mut *connection) + .await?; + } + } + } + + Ok(()) + } } #[async_trait] @@ -899,11 +1109,15 @@ impl CryptoStore for SqliteStore { Ok(self.devices.user_devices(user_id)) } - async fn get_user_identity(&self, _user_id: &UserId) -> Result> { - Ok(None) + async fn get_user_identity(&self, user_id: &UserId) -> Result> { + self.load_user(user_id).await } - async fn save_user_identities(&self, _users: &[UserIdentities]) -> Result<()> { + async fn save_user_identities(&self, users: &[UserIdentities]) -> Result<()> { + for user in users { + self.save_user_helper(user).await?; + } + Ok(()) } } @@ -922,7 +1136,7 @@ impl std::fmt::Debug for SqliteStore { #[cfg(test)] mod test { use crate::{ - identities::device::test::get_device, + identities::{device::test::get_device, user::test::get_own_identity}, olm::{Account, GroupSessionKey, InboundGroupSession, Session}, }; use matrix_sdk_common::{ @@ -1311,4 +1525,41 @@ mod test { assert!(loaded_device.is_none()); } + + #[tokio::test] + async fn user_saving() { + let (_account, store, dir) = get_loaded_store().await; + let own_identity = get_own_identity(); + + store + .save_user_identities(&[own_identity.into()]) + .await + .expect("Can't save identity"); + + drop(store); + + // let store = SqliteStore::open(&alice_id(), &alice_device_id(), dir.path()) + // .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.devices().next().unwrap(), &device); + } } From d35cf56dc81ff652d89d7b16c93290ea6003dd6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 7 Sep 2020 16:59:30 +0200 Subject: [PATCH 06/24] crypto: Disable the real life key export test since it take a lot of time. --- matrix_sdk_crypto/src/key_export.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/matrix_sdk_crypto/src/key_export.rs b/matrix_sdk_crypto/src/key_export.rs index d4b4045a..753a1fcb 100644 --- a/matrix_sdk_crypto/src/key_export.rs +++ b/matrix_sdk_crypto/src/key_export.rs @@ -172,9 +172,9 @@ mod test { assert_eq!(data, decrypted); } - #[test] - fn test_real_decrypt() { - let export = export_wihtout_headers(); - decrypt(&export, PASSPHRASE).expect("Can't decrypt key export"); - } + // #[test] + // fn test_real_decrypt() { + // let export = export_wihtout_headers(); + // decrypt(&export, PASSPHRASE).expect("Can't decrypt key export"); + // } } From 9810a2f630910f8277c1a0e4acadd73ed916e967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 8 Sep 2020 14:30:23 +0200 Subject: [PATCH 07/24] crypto: Finish up the cross signing storing for the sqlite store. --- matrix_sdk_crypto/Cargo.toml | 2 +- matrix_sdk_crypto/src/identities/user.rs | 96 ++++++ matrix_sdk_crypto/src/store/sqlite.rs | 408 +++++++++++++++-------- 3 files changed, 373 insertions(+), 133 deletions(-) diff --git a/matrix_sdk_crypto/Cargo.toml b/matrix_sdk_crypto/Cargo.toml index 3ee88c84..1ed7498c 100644 --- a/matrix_sdk_crypto/Cargo.toml +++ b/matrix_sdk_crypto/Cargo.toml @@ -54,7 +54,7 @@ features = ["std", "std-future"] version = "0.3.5" optional = true default-features = false -features = ["runtime-tokio", "sqlite"] +features = ["runtime-tokio", "sqlite", "macros"] [dev-dependencies] tokio = { version = "0.2.22", features = ["rt-threaded", "macros"] } diff --git a/matrix_sdk_crypto/src/identities/user.rs b/matrix_sdk_crypto/src/identities/user.rs index 5893b7ad..1d585e0f 100644 --- a/matrix_sdk_crypto/src/identities/user.rs +++ b/matrix_sdk_crypto/src/identities/user.rs @@ -56,6 +56,54 @@ impl PartialEq for MasterPubkey { } } +impl PartialEq for SelfSigningPubkey { + fn eq(&self, other: &SelfSigningPubkey) -> bool { + self.0.user_id == other.0.user_id && self.0.keys == other.0.keys + } +} + +impl PartialEq for UserSigningPubkey { + fn eq(&self, other: &UserSigningPubkey) -> bool { + self.0.user_id == other.0.user_id && self.0.keys == other.0.keys + } +} + +impl From for MasterPubkey { + fn from(key: CrossSigningKey) -> Self { + Self(Arc::new(key)) + } +} + +impl From for SelfSigningPubkey { + fn from(key: CrossSigningKey) -> Self { + Self(Arc::new(key)) + } +} + +impl From for UserSigningPubkey { + fn from(key: CrossSigningKey) -> Self { + Self(Arc::new(key)) + } +} + +impl AsRef for MasterPubkey { + fn as_ref(&self) -> &CrossSigningKey { + &self.0 + } +} + +impl AsRef for SelfSigningPubkey { + fn as_ref(&self) -> &CrossSigningKey { + &self.0 + } +} + +impl AsRef for UserSigningPubkey { + fn as_ref(&self) -> &CrossSigningKey { + &self.0 + } +} + impl From<&CrossSigningKey> for MasterPubkey { fn from(key: &CrossSigningKey) -> Self { Self(Arc::new(key.clone())) @@ -304,6 +352,12 @@ impl From for UserIdentities { } } +impl From for UserIdentities { + fn from(identity: UserIdentity) -> Self { + UserIdentities::Other(identity) + } +} + impl UserIdentities { /// The unique user id of this identity. pub fn user_id(&self) -> &UserId { @@ -321,6 +375,23 @@ impl UserIdentities { } } + /// Get the self-signing key of the identity. + pub fn self_signing_key(&self) -> &SelfSigningPubkey { + match self { + UserIdentities::Own(i) => &i.self_signing_key, + UserIdentities::Other(i) => &i.self_signing_key, + } + } + + /// Get the user-signing key of the identity, this is only present for our + /// own user identity.. + pub fn user_signing_key(&self) -> Option<&UserSigningPubkey> { + match self { + UserIdentities::Own(i) => Some(&i.user_signing_key), + UserIdentities::Other(_) => None, + } + } + /// Destructure the enum into an `OwnUserIdentity` if it's of the correct /// type. pub fn own(&self) -> Option<&OwnUserIdentity> { @@ -383,6 +454,11 @@ impl UserIdentity { &self.master_key } + /// Get the public self-signing key of the identity. + pub fn self_signing_key(&self) -> &SelfSigningPubkey { + &self.self_signing_key + } + /// Update the identity with a new master key and self signing key. /// /// # Arguments @@ -483,6 +559,16 @@ impl OwnUserIdentity { &self.master_key } + /// Get the public self-signing key of the identity. + pub fn self_signing_key(&self) -> &SelfSigningPubkey { + &self.self_signing_key + } + + /// Get the public user-signing key of the identity. + pub fn user_signing_key(&self) -> &UserSigningPubkey { + &self.user_signing_key + } + /// Check if the given identity has been signed by this identity. /// /// # Arguments @@ -760,6 +846,16 @@ pub(crate) mod test { own_identity(&own_key_query()) } + pub(crate) fn get_other_identity() -> UserIdentity { + let user_id = user_id!("@example2:localhost"); + let response = other_key_query(); + + let master_key = response.master_keys.get(&user_id).unwrap(); + let self_signing = response.self_signing_keys.get(&user_id).unwrap(); + + UserIdentity::new(master_key.into(), self_signing.into()).unwrap() + } + #[test] fn own_identity_create() { let user_id = user_id!("@example:localhost"); diff --git a/matrix_sdk_crypto/src/store/sqlite.rs b/matrix_sdk_crypto/src/store/sqlite.rs index f4619a59..795e36cf 100644 --- a/matrix_sdk_crypto/src/store/sqlite.rs +++ b/matrix_sdk_crypto/src/store/sqlite.rs @@ -21,7 +21,7 @@ use std::{ }; use async_trait::async_trait; -use dashmap::{DashMap, DashSet}; +use dashmap::DashSet; use matrix_sdk_common::{ api::r0::keys::{CrossSigningKey, KeyUsage}, identifiers::{ @@ -39,7 +39,7 @@ use super::{ CryptoStore, CryptoStoreError, Result, }; use crate::{ - identities::{LocalTrust, ReadOnlyDevice, UserIdentities}, + identities::{LocalTrust, OwnUserIdentity, ReadOnlyDevice, UserIdentities, UserIdentity}, olm::{ Account, AccountPickle, IdentityKeys, InboundGroupSession, InboundGroupSessionPickle, PickledAccount, PickledInboundGroupSession, PickledSession, PicklingMode, Session, @@ -72,6 +72,24 @@ struct AccountInfo { identity_keys: Arc, } +#[derive(Debug, PartialEq, Copy, Clone, sqlx::Type)] +#[repr(i32)] +enum CrosssigningKeyType { + Master = 0, + SelfSigning = 1, + UserSigning = 2, +} + +impl Into for CrosssigningKeyType { + fn into(self) -> KeyUsage { + match self { + CrosssigningKeyType::Master => KeyUsage::Master, + CrosssigningKeyType::SelfSigning => KeyUsage::SelfSigning, + CrosssigningKeyType::UserSigning => KeyUsage::UserSigning, + } + } +} + static DATABASE_NAME: &str = "matrix-sdk-crypto.db"; impl SqliteStore { @@ -329,6 +347,23 @@ impl SqliteStore { ) .await?; + connection + .execute( + r#" + CREATE TABLE IF NOT EXISTS cross_signing_keys ( + "id" INTEGER NOT NULL PRIMARY KEY, + "key_type" INTEGER NOT NULL, + "usage" STRING NOT NULL, + "user_id" INTEGER NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE + UNIQUE(user_id, key_type) + ); + + CREATE INDEX IF NOT EXISTS "cross_signing_keys_users" ON "users" ("user_id"); + "#, + ) + .await?; + connection .execute( r#" @@ -336,14 +371,12 @@ impl SqliteStore { "id" INTEGER NOT NULL PRIMARY KEY, "key" TEXT NOT NULL, "key_id" TEXT NOT NULL, - "key_type" TEXT NOT NULL, - "usage" TEXT NOT NULL, - "user_id" INTEGER NOT NULL, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE - UNIQUE(user_id, key_id, key_type) + "cross_signing_key" INTEGER NOT NULL, + FOREIGN KEY ("cross_signing_key") REFERENCES "cross_signing_keys" ("id") ON DELETE CASCADE + UNIQUE(cross_signing_key, key_id) ); - CREATE INDEX IF NOT EXISTS "user_keys_user_id" ON "users" ("user_id"); + CREATE INDEX IF NOT EXISTS "cross_signing_keys_keys" ON "cross_signing_keys" ("cross_signing_key"); "#, ) .await?; @@ -356,13 +389,13 @@ impl SqliteStore { "user_id" TEXT NOT NULL, "key_id" INTEGER NOT NULL, "signature" TEXT NOT NULL, - "user_key" INTEGER NOT NULL, - FOREIGN KEY ("user_key") REFERENCES "user_keys" ("id") + "cross_signing_key" INTEGER NOT NULL, + FOREIGN KEY ("cross_signing_key") REFERENCES "cross_signing_keys" ("id") ON DELETE CASCADE - UNIQUE(user_id, key_id, user_key) + UNIQUE(user_id, key_id, cross_signing_key) ); - CREATE INDEX IF NOT EXISTS "user_keys_device_id" ON "device_keys" ("device_id"); + CREATE INDEX IF NOT EXISTS "cross_signing_keys_signatures" ON "cross_signing_keys" ("cross_signing_key"); "#, ) .await?; @@ -728,6 +761,69 @@ impl SqliteStore { } } + async fn load_cross_signing_key( + connection: &mut SqliteConnection, + user_id: &UserId, + user_row_id: i64, + key_type: CrosssigningKeyType, + ) -> Result { + let row: (i64, String) = + query_as("SELECT id, usage FROM cross_signing_keys WHERE user_id =? and key_type =?") + .bind(user_row_id) + .bind(key_type) + .fetch_one(&mut *connection) + .await?; + + let key_row_id = row.0; + let usage: Vec = serde_json::from_str(&row.1)?; + + let key_rows: Vec<(String, String)> = + query_as("SELECT key_id, key FROM user_keys WHERE cross_signing_key = ?") + .bind(key_row_id) + .fetch_all(&mut *connection) + .await?; + + let mut keys = BTreeMap::new(); + let mut signatures = BTreeMap::new(); + + for row in key_rows { + let key_id = row.0; + let key = row.1; + + keys.insert(key_id, key); + } + + let mut signature_rows: Vec<(String, String, String)> = query_as( + "SELECT user_id, key_id, signature FROM user_key_signatures WHERE cross_signing_key = ?", + ) + .bind(key_row_id) + .fetch_all(&mut *connection) + .await?; + + for row in signature_rows.drain(..) { + let user_id = if let Ok(u) = UserId::try_from(row.0) { + u + } else { + continue; + }; + + let key_id = row.1; + let signature = row.2; + + signatures + .entry(user_id) + .or_insert_with(BTreeMap::new) + .insert(key_id, signature); + } + + Ok(CrossSigningKey { + user_id: user_id.to_owned(), + usage, + keys, + signatures, + }) + } + async fn load_user(&self, user_id: &UserId) -> Result> { let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; let mut connection = self.connection.lock().await; @@ -745,65 +841,104 @@ impl SqliteStore { return Ok(None); }; - let key_rows: Vec<(i64, String, String, String)> = query_as( - "SELECT id, key_id, key, usage FROM user_keys WHERE user_id = ? and key_type = ?", + let master = SqliteStore::load_cross_signing_key( + &mut connection, + user_id, + user_row_id, + CrosssigningKeyType::Master, + ) + .await?; + let self_singing = SqliteStore::load_cross_signing_key( + &mut connection, + user_id, + user_row_id, + CrosssigningKeyType::SelfSigning, ) - .bind(user_row_id) - .bind("master_key") - .fetch_all(&mut *connection) .await?; - let mut keys = BTreeMap::new(); - let mut signatures = BTreeMap::new(); - let mut key_usage = HashSet::new(); - - for row in key_rows { - let key_row_id = row.0; - let key_id = row.1; - let key = row.2; - let usage: Vec = serde_json::from_str(&row.3)?; - - keys.insert(key_id, key); - key_usage.extend(usage); - - let mut signature_rows: Vec<(String, String, String)> = query_as( - "SELECT user_id, key_id, signature, FROM user_key_signatures WHERE user_key = ?", + if user_id == &*self.user_id { + let user_signing = SqliteStore::load_cross_signing_key( + &mut connection, + user_id, + user_row_id, + CrosssigningKeyType::UserSigning, ) - .bind(user_row_id) - .bind("master_key") - .fetch_all(&mut *connection) .await?; - for row in signature_rows.drain(..) { - let user_id = if let Ok(u) = UserId::try_from(row.0) { - u - } else { - continue; - }; + Ok(Some(UserIdentities::Own( + OwnUserIdentity::new(master.into(), self_singing.into(), user_signing.into()) + .unwrap(), + ))) + } else { + Ok(Some(UserIdentities::Other( + UserIdentity::new(master.into(), self_singing.into()).unwrap(), + ))) + } + } - let key_id = row.1; - let signature = row.2; + async fn save_cross_signing_key( + connection: &mut SqliteConnection, + user_row_id: i64, + key_type: CrosssigningKeyType, + cross_signing_key: impl AsRef, + ) -> Result<()> { + let cross_signing_key: &CrossSigningKey = cross_signing_key.as_ref(); - signatures - .entry(user_id) - .or_insert_with(BTreeMap::new) - .insert(key_id, signature); + query( + "REPLACE INTO cross_signing_keys ( + user_id, key_type, usage + ) VALUES (?1, ?2, ?3) + ", + ) + .bind(user_row_id) + .bind(key_type) + .bind(serde_json::to_string(&cross_signing_key.usage)?) + .execute(&mut *connection) + .await?; + + let row: (i64,) = query_as( + "SELECT id FROM cross_signing_keys + WHERE user_id = ? and key_type = ?", + ) + .bind(user_row_id) + .bind(key_type) + .fetch_one(&mut *connection) + .await?; + + let key_row_id = row.0; + + for (key_id, key) in &cross_signing_key.keys { + query( + "REPLACE INTO user_keys ( + cross_signing_key, key_id, key + ) VALUES (?1, ?2, ?3) + ", + ) + .bind(key_row_id) + .bind(key_id.as_str()) + .bind(key) + .execute(&mut *connection) + .await?; + } + + for (user_id, signature_map) in &cross_signing_key.signatures { + for (key_id, signature) in signature_map { + query( + "REPLACE INTO user_key_signatures ( + cross_signing_key, user_id, key_id, signature + ) VALUES (?1, ?2, ?3, ?4) + ", + ) + .bind(key_row_id) + .bind(user_id.as_str()) + .bind(key_id.as_str()) + .bind(signature) + .execute(&mut *connection) + .await?; } } - let usage: Vec = key_usage - .iter() - .filter_map(|u| serde_json::from_str(u).ok()) - .collect(); - - let key = CrossSigningKey { - user_id: user_id.to_owned(), - usage, - keys, - signatures, - }; - - Ok(None) + Ok(()) } async fn save_user_helper(&self, user: &UserIdentities) -> Result<()> { @@ -811,16 +946,11 @@ impl SqliteStore { let mut connection = self.connection.lock().await; - query( - "INSERT OR IGNORE INTO users ( - account_id, user_id - ) VALUES (?1, ?2) - ", - ) - .bind(account_id) - .bind(user.user_id().as_str()) - .execute(&mut *connection) - .await?; + query("REPLACE INTO users (account_id, user_id) VALUES (?1, ?2)") + .bind(account_id) + .bind(user.user_id().as_str()) + .execute(&mut *connection) + .await?; let row: (i64,) = query_as( "SELECT id FROM users @@ -833,49 +963,29 @@ impl SqliteStore { let user_row_id = row.0; - for (key_id, key) in user.master_key() { - query( - "INSERT OR IGNORE INTO user_keys ( - user_id, key_type, key_id, key, usage - ) VALUES (?1, ?2, ?3, ?4, ?5) - ", + SqliteStore::save_cross_signing_key( + &mut connection, + user_row_id, + CrosssigningKeyType::Master, + user.master_key(), + ) + .await?; + SqliteStore::save_cross_signing_key( + &mut connection, + user_row_id, + CrosssigningKeyType::SelfSigning, + user.self_signing_key(), + ) + .await?; + + if let Some(user_signing_key) = user.user_signing_key() { + SqliteStore::save_cross_signing_key( + &mut connection, + user_row_id, + CrosssigningKeyType::UserSigning, + user_signing_key, ) - .bind(user_row_id) - .bind("master_key") - .bind(key_id.as_str()) - .bind(key) - .bind(serde_json::to_string(user.master_key().usage())?) - .execute(&mut *connection) .await?; - - let row: (i64,) = query_as( - "SELECT id FROM user_keys - WHERE user_id = ? and key_id = ? and key_type = ?", - ) - .bind(user_row_id) - .bind(key_id.as_str()) - .bind("master_key") - .fetch_one(&mut *connection) - .await?; - - let key_row_id = row.0; - - for (user_id, signature_map) in user.master_key().signatures() { - for (key_id, signature) in signature_map { - query( - "INSERT OR IGNORE INTO user_key_signatures ( - user_key, user_id, key_id, signature - ) VALUES (?1, ?2, ?3, ?4) - ", - ) - .bind(key_row_id) - .bind(user_id.as_str()) - .bind(key_id.as_str()) - .bind(signature) - .execute(&mut *connection) - .await?; - } - } } Ok(()) @@ -1136,7 +1246,10 @@ impl std::fmt::Debug for SqliteStore { #[cfg(test)] mod test { use crate::{ - identities::{device::test::get_device, user::test::get_own_identity}, + identities::{ + device::test::get_device, + user::test::{get_other_identity, get_own_identity}, + }, olm::{Account, GroupSessionKey, InboundGroupSession, Session}, }; use matrix_sdk_common::{ @@ -1528,38 +1641,69 @@ mod test { #[tokio::test] async fn user_saving() { - let (_account, store, dir) = get_loaded_store().await; + let dir = tempdir().unwrap(); + let tmpdir_path = dir.path().to_str().unwrap(); + + let user_id = user_id!("@example:localhost"); + let device_id: &DeviceId = "WSKKLTJZCL".into(); + + let store = SqliteStore::open(&user_id, &device_id, tmpdir_path) + .await + .expect("Can't create store"); + + let account = Account::new(&user_id, &device_id); + + store + .save_account(account.clone()) + .await + .expect("Can't save account"); + let own_identity = get_own_identity(); store - .save_user_identities(&[own_identity.into()]) + .save_user_identities(&[own_identity.clone().into()]) .await .expect("Can't save identity"); drop(store); - // let store = SqliteStore::open(&alice_id(), &alice_device_id(), dir.path()) - // .await - // .expect("Can't create store"); + let store = SqliteStore::open(&user_id, &device_id, dir.path()) + .await + .expect("Can't create store"); - // store.load_account().await.unwrap(); + store.load_account().await.unwrap(); - // let loaded_device = store - // .get_device(device.user_id(), device.device_id()) - // .await - // .unwrap() - // .unwrap(); + let loaded_user = store + .get_user_identity(own_identity.user_id()) + .await + .unwrap() + .unwrap(); - // assert_eq!(device, loaded_device); + 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.into()); - // 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 other_identity = get_other_identity(); - // 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.devices().next().unwrap(), &device); + store + .save_user_identities(&[other_identity.clone().into()]) + .await + .unwrap(); + + let loaded_user = store + .load_user(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()); } } From 70ffc43ce056791c4e5649c302f766ad1cc9a3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 8 Sep 2020 16:07:37 +0200 Subject: [PATCH 08/24] crypto: Store the trust state of our own identities as well. --- matrix_sdk_crypto/src/store/sqlite.rs | 58 +++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/matrix_sdk_crypto/src/store/sqlite.rs b/matrix_sdk_crypto/src/store/sqlite.rs index 795e36cf..408183b4 100644 --- a/matrix_sdk_crypto/src/store/sqlite.rs +++ b/matrix_sdk_crypto/src/store/sqlite.rs @@ -347,6 +347,21 @@ impl SqliteStore { ) .await?; + connection + .execute( + r#" + CREATE TABLE IF NOT EXISTS users_trust_state ( + "id" INTEGER NOT NULL PRIMARY KEY, + "trusted" INTEGER NOT NULL, + "user_id" INTEGER NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") + ON DELETE CASCADE + UNIQUE(user_id) + ); + "#, + ) + .await?; + connection .execute( r#" @@ -865,13 +880,27 @@ impl SqliteStore { ) .await?; - Ok(Some(UserIdentities::Own( + let verified: Option<(bool,)> = + query_as("SELECT trusted FROM users_trust_state WHERE user_id = ?") + .bind(user_row_id) + .fetch_optional(&mut *connection) + .await?; + + let verified = verified.map_or(false, |r| r.0); + + let identity = OwnUserIdentity::new(master.into(), self_singing.into(), user_signing.into()) - .unwrap(), - ))) + .expect("Signature check failed on stored identity"); + + if verified { + identity.mark_as_verified(); + } + + Ok(Some(UserIdentities::Own(identity))) } else { Ok(Some(UserIdentities::Other( - UserIdentity::new(master.into(), self_singing.into()).unwrap(), + UserIdentity::new(master.into(), self_singing.into()) + .expect("Signature check failed on stored identity"), ))) } } @@ -978,14 +1007,20 @@ impl SqliteStore { ) .await?; - if let Some(user_signing_key) = user.user_signing_key() { + if let UserIdentities::Own(own_identity) = user { SqliteStore::save_cross_signing_key( &mut connection, user_row_id, CrosssigningKeyType::UserSigning, - user_signing_key, + own_identity.user_signing_key(), ) .await?; + + query("REPLACE INTO users_trust_state (user_id, trusted) VALUES (?1, ?2)") + .bind(user_row_id) + .bind(own_identity.is_verified()) + .execute(&mut *connection) + .await?; } Ok(()) @@ -1684,7 +1719,7 @@ mod test { loaded_user.self_signing_key(), own_identity.self_signing_key() ); - assert_eq!(loaded_user, own_identity.into()); + assert_eq!(loaded_user, own_identity.clone().into()); let other_identity = get_other_identity(); @@ -1705,5 +1740,14 @@ mod test { other_identity.self_signing_key() ); assert_eq!(loaded_user, other_identity.into()); + + own_identity.mark_as_verified(); + + store + .save_user_identities(&[own_identity.into()]) + .await + .unwrap(); + let loaded_user = store.load_user(&user_id).await.unwrap().unwrap(); + assert!(loaded_user.own().unwrap().is_verified()) } } From 14226c07780f87df42a9b0f5cf6ab816d08ba5af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 8 Sep 2020 16:17:17 +0200 Subject: [PATCH 09/24] crypto: Refactor some tests. --- matrix_sdk_crypto/src/identities/user.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/matrix_sdk_crypto/src/identities/user.rs b/matrix_sdk_crypto/src/identities/user.rs index 1d585e0f..f50e02cd 100644 --- a/matrix_sdk_crypto/src/identities/user.rs +++ b/matrix_sdk_crypto/src/identities/user.rs @@ -870,19 +870,13 @@ pub(crate) mod test { #[test] fn other_identity_create() { - let user_id = user_id!("@example2:localhost"); - let response = other_key_query(); - - let master_key = response.master_keys.get(&user_id).unwrap(); - let self_signing = response.self_signing_keys.get(&user_id).unwrap(); - - UserIdentity::new(master_key.into(), self_signing.into()).unwrap(); + get_other_identity(); } #[test] fn own_identity_check_signatures() { let response = own_key_query(); - let identity = own_identity(&response); + let identity = get_own_identity(); let (first, second) = device(&response); assert!(identity.is_device_signed(&first).is_err()); From fc605938010e19cf7565df30d716e22f14817c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 8 Sep 2020 17:34:34 +0200 Subject: [PATCH 10/24] crypto: Remove some unused into implementation. --- matrix_sdk_crypto/src/store/sqlite.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/matrix_sdk_crypto/src/store/sqlite.rs b/matrix_sdk_crypto/src/store/sqlite.rs index 408183b4..d40ca12b 100644 --- a/matrix_sdk_crypto/src/store/sqlite.rs +++ b/matrix_sdk_crypto/src/store/sqlite.rs @@ -80,16 +80,6 @@ enum CrosssigningKeyType { UserSigning = 2, } -impl Into for CrosssigningKeyType { - fn into(self) -> KeyUsage { - match self { - CrosssigningKeyType::Master => KeyUsage::Master, - CrosssigningKeyType::SelfSigning => KeyUsage::SelfSigning, - CrosssigningKeyType::UserSigning => KeyUsage::UserSigning, - } - } -} - static DATABASE_NAME: &str = "matrix-sdk-crypto.db"; impl SqliteStore { From acfd0cdb075c8904bc006738b3e14d4f93ea04ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Sep 2020 11:07:49 +0200 Subject: [PATCH 11/24] crypto: Split out the group session module into multiple files. --- .../inbound.rs} | 274 +----------------- .../src/olm/group_sessions/mod.rs | 84 ++++++ .../src/olm/group_sessions/outbound.rs | 274 ++++++++++++++++++ 3 files changed, 364 insertions(+), 268 deletions(-) rename matrix_sdk_crypto/src/olm/{group_sessions.rs => group_sessions/inbound.rs} (53%) create mode 100644 matrix_sdk_crypto/src/olm/group_sessions/mod.rs create mode 100644 matrix_sdk_crypto/src/olm/group_sessions/outbound.rs diff --git a/matrix_sdk_crypto/src/olm/group_sessions.rs b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs similarity index 53% rename from matrix_sdk_crypto/src/olm/group_sessions.rs rename to matrix_sdk_crypto/src/olm/group_sessions/inbound.rs index 729bf5c6..5bbd09fc 100644 --- a/matrix_sdk_crypto/src/olm/group_sessions.rs +++ b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs @@ -12,34 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - cmp::min, - convert::TryInto, - fmt, - sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, - Arc, - }, - time::Duration, -}; +use std::{convert::TryInto, fmt, sync::Arc, time::Duration}; use matrix_sdk_common::{ events::{ room::{encrypted::EncryptedEventContent, encryption::EncryptionEventContent}, - AnyMessageEventContent, AnySyncRoomEvent, EventContent, SyncMessageEvent, + AnySyncRoomEvent, SyncMessageEvent, }, - identifiers::{DeviceId, EventEncryptionAlgorithm, RoomId}, - instant::Instant, + identifiers::{EventEncryptionAlgorithm, RoomId}, locks::Mutex, Raw, }; use olm_rs::{ - errors::OlmGroupSessionError, inbound_group_session::OlmInboundGroupSession, - outbound_group_session::OlmOutboundGroupSession, PicklingMode, + errors::OlmGroupSessionError, inbound_group_session::OlmInboundGroupSession, PicklingMode, }; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use zeroize::Zeroize; +use serde_json::Value; pub use olm_rs::{ account::IdentityKeys, @@ -47,6 +35,7 @@ pub use olm_rs::{ utility::OlmUtility, }; +use super::GroupSessionKey; use crate::error::{EventError, MegolmResult}; const ROTATION_PERIOD: Duration = Duration::from_millis(604800000); @@ -92,12 +81,6 @@ impl From<&EncryptionEventContent> for EncryptionSettings { } } -/// The private session key of a group session. -/// Can be used to create a new inbound group session. -#[derive(Clone, Debug, Serialize, Zeroize)] -#[zeroize(drop)] -pub struct GroupSessionKey(pub String); - /// Inbound group session. /// /// Inbound group sessions are used to exchange room messages between a group of @@ -314,248 +297,3 @@ impl InboundGroupSessionPickle { &self.0 } } - -/// Outbound group session. -/// -/// Outbound group sessions are used to exchange room messages between a group -/// of participants. Outbound group sessions are used to encrypt the room -/// messages. -#[derive(Clone)] -pub struct OutboundGroupSession { - inner: Arc>, - device_id: Arc>, - account_identity_keys: Arc, - session_id: Arc, - room_id: Arc, - creation_time: Arc, - message_count: Arc, - shared: Arc, - settings: Arc, -} - -impl OutboundGroupSession { - /// Create a new outbound group session for the given room. - /// - /// Outbound group sessions are used to encrypt room messages. - /// - /// # Arguments - /// - /// * `device_id` - The id of the device that created this session. - /// - /// * `identity_keys` - The identity keys of the account that created this - /// session. - /// - /// * `room_id` - The id of the room that the session is used in. - /// - /// * `settings` - Settings determining the algorithm and rotation period of - /// the outbound group session. - pub fn new( - device_id: Arc>, - identity_keys: Arc, - room_id: &RoomId, - settings: EncryptionSettings, - ) -> Self { - let session = OlmOutboundGroupSession::new(); - let session_id = session.session_id(); - - OutboundGroupSession { - inner: Arc::new(Mutex::new(session)), - room_id: Arc::new(room_id.to_owned()), - device_id, - account_identity_keys: identity_keys, - session_id: Arc::new(session_id), - creation_time: Arc::new(Instant::now()), - message_count: Arc::new(AtomicU64::new(0)), - shared: Arc::new(AtomicBool::new(false)), - settings: Arc::new(settings), - } - } - - /// Encrypt the given plaintext using this session. - /// - /// Returns the encrypted ciphertext. - /// - /// # Arguments - /// - /// * `plaintext` - The plaintext that should be encrypted. - pub(crate) async fn encrypt_helper(&self, plaintext: String) -> String { - let session = self.inner.lock().await; - self.message_count.fetch_add(1, Ordering::SeqCst); - session.encrypt(plaintext) - } - - /// Encrypt a room message for the given room. - /// - /// Beware that a group session needs to be shared before this method can be - /// called using the `share_group_session()` method. - /// - /// Since group sessions can expire or become invalid if the room membership - /// changes client authors should check with the - /// `should_share_group_session()` method if a new group session needs to - /// be shared. - /// - /// # Arguments - /// - /// * `content` - The plaintext content of the message that should be - /// encrypted. - /// - /// # Panics - /// - /// Panics if the content can't be serialized. - pub async fn encrypt(&self, content: AnyMessageEventContent) -> EncryptedEventContent { - let json_content = json!({ - "content": content, - "room_id": &*self.room_id, - "type": content.event_type(), - }); - - let plaintext = cjson::to_string(&json_content).unwrap_or_else(|_| { - panic!(format!( - "Can't serialize {} to canonical JSON", - json_content - )) - }); - - let ciphertext = self.encrypt_helper(plaintext).await; - - EncryptedEventContent::MegolmV1AesSha2( - matrix_sdk_common::events::room::encrypted::MegolmV1AesSha2ContentInit { - ciphertext, - sender_key: self.account_identity_keys.curve25519().to_owned(), - session_id: self.session_id().to_owned(), - device_id: (&*self.device_id).to_owned(), - } - .into(), - ) - } - - /// Check if the session has expired and if it should be rotated. - /// - /// A session will expire after some time or if enough messages have been - /// encrypted using it. - pub fn expired(&self) -> bool { - let count = self.message_count.load(Ordering::SeqCst); - - count >= self.settings.rotation_period_msgs - || self.creation_time.elapsed() - // Since the encryption settings are provided by users and not - // checked someone could set a really low rotation perdiod so - // clamp it at a minute. - >= min(self.settings.rotation_period, Duration::from_secs(3600)) - } - - /// Mark the session as shared. - /// - /// Messages shouldn't be encrypted with the session before it has been - /// shared. - pub fn mark_as_shared(&self) { - self.shared.store(true, Ordering::Relaxed); - } - - /// Check if the session has been marked as shared. - pub fn shared(&self) -> bool { - self.shared.load(Ordering::Relaxed) - } - - /// Get the session key of this session. - /// - /// A session key can be used to to create an `InboundGroupSession`. - pub async fn session_key(&self) -> GroupSessionKey { - let session = self.inner.lock().await; - GroupSessionKey(session.session_key()) - } - - /// Returns the unique identifier for this session. - pub fn session_id(&self) -> &str { - &self.session_id - } - - /// Get the current message index for this session. - /// - /// Each message is sent with an increasing index. This returns the - /// message index that will be used for the next encrypted message. - pub async fn message_index(&self) -> u32 { - let session = self.inner.lock().await; - session.session_message_index() - } - - /// Get the outbound group session key as a json value that can be sent as a - /// m.room_key. - pub async fn as_json(&self) -> Value { - json!({ - "algorithm": EventEncryptionAlgorithm::MegolmV1AesSha2, - "room_id": &*self.room_id, - "session_id": &*self.session_id, - "session_key": self.session_key().await, - "chain_index": self.message_index().await, - }) - } -} - -#[cfg(not(tarpaulin_include))] -impl std::fmt::Debug for OutboundGroupSession { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("OutboundGroupSession") - .field("session_id", &self.session_id) - .field("room_id", &self.room_id) - .field("creation_time", &self.creation_time) - .field("message_count", &self.message_count) - .finish() - } -} - -#[cfg(test)] -mod test { - use std::{ - sync::Arc, - time::{Duration, Instant}, - }; - - use matrix_sdk_common::{ - events::{ - room::message::{MessageEventContent, TextMessageEventContent}, - AnyMessageEventContent, - }, - identifiers::{room_id, user_id}, - }; - - use super::EncryptionSettings; - use crate::Account; - - #[tokio::test] - #[cfg(not(target_os = "macos"))] - async fn expiration() { - let settings = EncryptionSettings { - rotation_period_msgs: 1, - ..Default::default() - }; - - let account = Account::new(&user_id!("@alice:example.org"), "DEVICEID".into()); - let (session, _) = account - .create_group_session_pair(&room_id!("!test_room:example.org"), settings) - .await - .unwrap(); - - assert!(!session.expired()); - let _ = session - .encrypt(AnyMessageEventContent::RoomMessage( - MessageEventContent::Text(TextMessageEventContent::plain("Test message")), - )) - .await; - assert!(session.expired()); - - let settings = EncryptionSettings { - rotation_period: Duration::from_millis(100), - ..Default::default() - }; - - let (mut session, _) = account - .create_group_session_pair(&room_id!("!test_room:example.org"), settings) - .await - .unwrap(); - - assert!(!session.expired()); - session.creation_time = Arc::new(Instant::now() - Duration::from_secs(60 * 60)); - assert!(session.expired()); - } -} diff --git a/matrix_sdk_crypto/src/olm/group_sessions/mod.rs b/matrix_sdk_crypto/src/olm/group_sessions/mod.rs new file mode 100644 index 00000000..fae5080c --- /dev/null +++ b/matrix_sdk_crypto/src/olm/group_sessions/mod.rs @@ -0,0 +1,84 @@ +// 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. + +use serde::{Deserialize, Serialize}; +use zeroize::Zeroize; + +mod inbound; +mod outbound; + +pub use inbound::{InboundGroupSession, InboundGroupSessionPickle, PickledInboundGroupSession}; +pub use outbound::{EncryptionSettings, OutboundGroupSession}; + +/// The private session key of a group session. +/// Can be used to create a new inbound group session. +#[derive(Clone, Debug, Serialize, Deserialize, Zeroize)] +#[zeroize(drop)] +pub struct GroupSessionKey(pub String); + +#[cfg(test)] +mod test { + use std::{ + sync::Arc, + time::{Duration, Instant}, + }; + + use matrix_sdk_common::{ + events::{ + room::message::{MessageEventContent, TextMessageEventContent}, + AnyMessageEventContent, + }, + identifiers::{room_id, user_id}, + }; + + use super::EncryptionSettings; + use crate::Account; + + #[tokio::test] + #[cfg(not(target_os = "macos"))] + async fn expiration() { + let settings = EncryptionSettings { + rotation_period_msgs: 1, + ..Default::default() + }; + + let account = Account::new(&user_id!("@alice:example.org"), "DEVICEID".into()); + let (session, _) = account + .create_group_session_pair(&room_id!("!test_room:example.org"), settings) + .await + .unwrap(); + + assert!(!session.expired()); + let _ = session + .encrypt(AnyMessageEventContent::RoomMessage( + MessageEventContent::Text(TextMessageEventContent::plain("Test message")), + )) + .await; + assert!(session.expired()); + + let settings = EncryptionSettings { + rotation_period: Duration::from_millis(100), + ..Default::default() + }; + + let (mut session, _) = account + .create_group_session_pair(&room_id!("!test_room:example.org"), settings) + .await + .unwrap(); + + assert!(!session.expired()); + session.creation_time = Arc::new(Instant::now() - Duration::from_secs(60 * 60)); + assert!(session.expired()); + } +} diff --git a/matrix_sdk_crypto/src/olm/group_sessions/outbound.rs b/matrix_sdk_crypto/src/olm/group_sessions/outbound.rs new file mode 100644 index 00000000..453be884 --- /dev/null +++ b/matrix_sdk_crypto/src/olm/group_sessions/outbound.rs @@ -0,0 +1,274 @@ +// 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. + +use std::{ + cmp::min, + fmt, + sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + Arc, + }, + time::Duration, +}; + +use matrix_sdk_common::{ + events::{ + room::{encrypted::EncryptedEventContent, encryption::EncryptionEventContent}, + AnyMessageEventContent, EventContent, + }, + identifiers::{DeviceId, EventEncryptionAlgorithm, RoomId}, + instant::Instant, + locks::Mutex, +}; +use olm_rs::outbound_group_session::OlmOutboundGroupSession; +use serde_json::{json, Value}; + +pub use olm_rs::{ + account::IdentityKeys, + session::{OlmMessage, PreKeyMessage}, + utility::OlmUtility, +}; + +use super::GroupSessionKey; + +const ROTATION_PERIOD: Duration = Duration::from_millis(604800000); +const ROTATION_MESSAGES: u64 = 100; + +/// Settings for an encrypted room. +/// +/// This determines the algorithm and rotation periods of a group session. +#[derive(Debug)] +pub struct EncryptionSettings { + /// The encryption algorithm that should be used in the room. + pub algorithm: EventEncryptionAlgorithm, + /// How long the session should be used before changing it. + pub rotation_period: Duration, + /// How many messages should be sent before changing the session. + pub rotation_period_msgs: u64, +} + +impl Default for EncryptionSettings { + fn default() -> Self { + Self { + algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, + rotation_period: ROTATION_PERIOD, + rotation_period_msgs: ROTATION_MESSAGES, + } + } +} + +impl From<&EncryptionEventContent> for EncryptionSettings { + fn from(content: &EncryptionEventContent) -> Self { + let rotation_period: Duration = content + .rotation_period_ms + .map_or(ROTATION_PERIOD, |r| Duration::from_millis(r.into())); + let rotation_period_msgs: u64 = content + .rotation_period_msgs + .map_or(ROTATION_MESSAGES, Into::into); + + Self { + algorithm: content.algorithm.clone(), + rotation_period, + rotation_period_msgs, + } + } +} +/// Outbound group session. +/// +/// Outbound group sessions are used to exchange room messages between a group +/// of participants. Outbound group sessions are used to encrypt the room +/// messages. +#[derive(Clone)] +pub struct OutboundGroupSession { + inner: Arc>, + device_id: Arc>, + account_identity_keys: Arc, + session_id: Arc, + room_id: Arc, + pub(crate) creation_time: Arc, + message_count: Arc, + shared: Arc, + settings: Arc, +} + +impl OutboundGroupSession { + /// Create a new outbound group session for the given room. + /// + /// Outbound group sessions are used to encrypt room messages. + /// + /// # Arguments + /// + /// * `device_id` - The id of the device that created this session. + /// + /// * `identity_keys` - The identity keys of the account that created this + /// session. + /// + /// * `room_id` - The id of the room that the session is used in. + /// + /// * `settings` - Settings determining the algorithm and rotation period of + /// the outbound group session. + pub fn new( + device_id: Arc>, + identity_keys: Arc, + room_id: &RoomId, + settings: EncryptionSettings, + ) -> Self { + let session = OlmOutboundGroupSession::new(); + let session_id = session.session_id(); + + OutboundGroupSession { + inner: Arc::new(Mutex::new(session)), + room_id: Arc::new(room_id.to_owned()), + device_id, + account_identity_keys: identity_keys, + session_id: Arc::new(session_id), + creation_time: Arc::new(Instant::now()), + message_count: Arc::new(AtomicU64::new(0)), + shared: Arc::new(AtomicBool::new(false)), + settings: Arc::new(settings), + } + } + + /// Encrypt the given plaintext using this session. + /// + /// Returns the encrypted ciphertext. + /// + /// # Arguments + /// + /// * `plaintext` - The plaintext that should be encrypted. + pub(crate) async fn encrypt_helper(&self, plaintext: String) -> String { + let session = self.inner.lock().await; + self.message_count.fetch_add(1, Ordering::SeqCst); + session.encrypt(plaintext) + } + + /// Encrypt a room message for the given room. + /// + /// Beware that a group session needs to be shared before this method can be + /// called using the `share_group_session()` method. + /// + /// Since group sessions can expire or become invalid if the room membership + /// changes client authors should check with the + /// `should_share_group_session()` method if a new group session needs to + /// be shared. + /// + /// # Arguments + /// + /// * `content` - The plaintext content of the message that should be + /// encrypted. + /// + /// # Panics + /// + /// Panics if the content can't be serialized. + pub async fn encrypt(&self, content: AnyMessageEventContent) -> EncryptedEventContent { + let json_content = json!({ + "content": content, + "room_id": &*self.room_id, + "type": content.event_type(), + }); + + let plaintext = cjson::to_string(&json_content).unwrap_or_else(|_| { + panic!(format!( + "Can't serialize {} to canonical JSON", + json_content + )) + }); + + let ciphertext = self.encrypt_helper(plaintext).await; + + EncryptedEventContent::MegolmV1AesSha2( + matrix_sdk_common::events::room::encrypted::MegolmV1AesSha2ContentInit { + ciphertext, + sender_key: self.account_identity_keys.curve25519().to_owned(), + session_id: self.session_id().to_owned(), + device_id: (&*self.device_id).to_owned(), + } + .into(), + ) + } + + /// Check if the session has expired and if it should be rotated. + /// + /// A session will expire after some time or if enough messages have been + /// encrypted using it. + pub fn expired(&self) -> bool { + let count = self.message_count.load(Ordering::SeqCst); + + count >= self.settings.rotation_period_msgs + || self.creation_time.elapsed() + // Since the encryption settings are provided by users and not + // checked someone could set a really low rotation perdiod so + // clamp it at a minute. + >= min(self.settings.rotation_period, Duration::from_secs(3600)) + } + + /// Mark the session as shared. + /// + /// Messages shouldn't be encrypted with the session before it has been + /// shared. + pub fn mark_as_shared(&self) { + self.shared.store(true, Ordering::Relaxed); + } + + /// Check if the session has been marked as shared. + pub fn shared(&self) -> bool { + self.shared.load(Ordering::Relaxed) + } + + /// Get the session key of this session. + /// + /// A session key can be used to to create an `InboundGroupSession`. + pub async fn session_key(&self) -> GroupSessionKey { + let session = self.inner.lock().await; + GroupSessionKey(session.session_key()) + } + + /// Returns the unique identifier for this session. + pub fn session_id(&self) -> &str { + &self.session_id + } + + /// Get the current message index for this session. + /// + /// Each message is sent with an increasing index. This returns the + /// message index that will be used for the next encrypted message. + pub async fn message_index(&self) -> u32 { + let session = self.inner.lock().await; + session.session_message_index() + } + + /// Get the outbound group session key as a json value that can be sent as a + /// m.room_key. + pub async fn as_json(&self) -> Value { + json!({ + "algorithm": EventEncryptionAlgorithm::MegolmV1AesSha2, + "room_id": &*self.room_id, + "session_id": &*self.session_id, + "session_key": self.session_key().await, + "chain_index": self.message_index().await, + }) + } +} + +#[cfg(not(tarpaulin_include))] +impl std::fmt::Debug for OutboundGroupSession { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutboundGroupSession") + .field("session_id", &self.session_id) + .field("room_id", &self.room_id) + .field("creation_time", &self.creation_time) + .field("message_count", &self.message_count) + .finish() + } +} From 98f69aed4188de568414deaf786b424e06675a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Sep 2020 11:51:00 +0200 Subject: [PATCH 12/24] crypto: Remove some duplicated types after the group session split. --- .../src/olm/group_sessions/inbound.rs | 52 ++----------------- 1 file changed, 3 insertions(+), 49 deletions(-) diff --git a/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs index 5bbd09fc..1942f18f 100644 --- a/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs +++ b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs @@ -12,14 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{convert::TryInto, fmt, sync::Arc, time::Duration}; +use std::{collections::BTreeMap, convert::TryInto, fmt, sync::Arc}; use matrix_sdk_common::{ - events::{ - room::{encrypted::EncryptedEventContent, encryption::EncryptionEventContent}, - AnySyncRoomEvent, SyncMessageEvent, - }, - identifiers::{EventEncryptionAlgorithm, RoomId}, + events::{room::encrypted::EncryptedEventContent, AnySyncRoomEvent, SyncMessageEvent}, + identifiers::{DeviceKeyAlgorithm, EventEncryptionAlgorithm, RoomId}, locks::Mutex, Raw, }; @@ -38,49 +35,6 @@ pub use olm_rs::{ use super::GroupSessionKey; use crate::error::{EventError, MegolmResult}; -const ROTATION_PERIOD: Duration = Duration::from_millis(604800000); -const ROTATION_MESSAGES: u64 = 100; - -/// Settings for an encrypted room. -/// -/// This determines the algorithm and rotation periods of a group session. -#[derive(Debug)] -pub struct EncryptionSettings { - /// The encryption algorithm that should be used in the room. - pub algorithm: EventEncryptionAlgorithm, - /// How long the session should be used before changing it. - pub rotation_period: Duration, - /// How many messages should be sent before changing the session. - pub rotation_period_msgs: u64, -} - -impl Default for EncryptionSettings { - fn default() -> Self { - Self { - algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, - rotation_period: ROTATION_PERIOD, - rotation_period_msgs: ROTATION_MESSAGES, - } - } -} - -impl From<&EncryptionEventContent> for EncryptionSettings { - fn from(content: &EncryptionEventContent) -> Self { - let rotation_period: Duration = content - .rotation_period_ms - .map_or(ROTATION_PERIOD, |r| Duration::from_millis(r.into())); - let rotation_period_msgs: u64 = content - .rotation_period_msgs - .map_or(ROTATION_MESSAGES, Into::into); - - Self { - algorithm: content.algorithm.clone(), - rotation_period, - rotation_period_msgs, - } - } -} - /// Inbound group session. /// /// Inbound group sessions are used to exchange room messages between a group of From aff1e1d0a88c648fb6d6d946a5bc4703cacabfbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Sep 2020 12:47:28 +0200 Subject: [PATCH 13/24] crypto: Add key export methods for inbound group sessions. --- .../src/olm/group_sessions/inbound.rs | 42 ++++++++- .../src/olm/group_sessions/mod.rs | 85 +++++++++++++++++++ matrix_sdk_crypto/src/olm/mod.rs | 3 +- 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs index 1942f18f..ce41675a 100644 --- a/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs +++ b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs @@ -12,7 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, convert::TryInto, fmt, sync::Arc}; +use std::{ + collections::BTreeMap, + convert::{TryFrom, TryInto}, + fmt, + sync::Arc, +}; use matrix_sdk_common::{ events::{room::encrypted::EncryptedEventContent, AnySyncRoomEvent, SyncMessageEvent}, @@ -32,7 +37,7 @@ pub use olm_rs::{ utility::OlmUtility, }; -use super::GroupSessionKey; +use super::{ExportedGroupSessionKey, ExportedRoomKey, GroupSessionKey}; use crate::error::{EventError, MegolmResult}; /// Inbound group session. @@ -103,6 +108,39 @@ impl InboundGroupSession { } } + /// Export this session. + pub async fn export(&self) -> ExportedRoomKey { + self.export_at_index(self.first_known_index().await) + .await + .expect("Can't export at the first known index") + } + + /// Export this session at the given message index. + pub async fn export_at_index(&self, message_index: u32) -> Option { + let session_key = + ExportedGroupSessionKey(self.inner.lock().await.export(message_index).ok()?); + + let mut sender_claimed_keys: BTreeMap = BTreeMap::new(); + + sender_claimed_keys.insert(DeviceKeyAlgorithm::Ed25519, (&*self.signing_key).to_owned()); + + Some(ExportedRoomKey { + algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, + room_id: (&*self.room_id).clone(), + sender_key: (&*self.sender_key).to_owned(), + session_id: self.session_id().to_owned(), + forwarding_curve25519_key_chain: self + .forwarding_chains + .lock() + .await + .as_ref() + .cloned() + .unwrap_or_default(), + sender_claimed_keys, + session_key, + }) + } + /// Restore a Session from a previously pickled string. /// /// Returns the restored group session or a `OlmGroupSessionError` if there diff --git a/matrix_sdk_crypto/src/olm/group_sessions/mod.rs b/matrix_sdk_crypto/src/olm/group_sessions/mod.rs index fae5080c..7f9ac026 100644 --- a/matrix_sdk_crypto/src/olm/group_sessions/mod.rs +++ b/matrix_sdk_crypto/src/olm/group_sessions/mod.rs @@ -12,7 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +use matrix_sdk_common::{ + events::forwarded_room_key::ForwardedRoomKeyEventContent, + identifiers::{DeviceKeyAlgorithm, EventEncryptionAlgorithm, RoomId}, +}; use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, convert::TryInto}; use zeroize::Zeroize; mod inbound; @@ -27,6 +32,86 @@ pub use outbound::{EncryptionSettings, OutboundGroupSession}; #[zeroize(drop)] pub struct GroupSessionKey(pub String); +/// The exported version of an private session key of a group session. +/// Can be used to create a new inbound group session. +#[derive(Clone, Debug, Serialize, Deserialize, Zeroize)] +#[zeroize(drop)] +pub struct ExportedGroupSessionKey(pub String); + +/// An exported version of a `InboundGroupSession` +/// +/// This can be used to share the `InboundGroupSession` in an exported file. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ExportedRoomKey { + /// The encryption algorithm that the session uses. + pub algorithm: EventEncryptionAlgorithm, + + /// The room where the session is used. + pub room_id: RoomId, + + /// The Curve25519 key of the device which initiated the session originally. + pub sender_key: String, + + /// The ID of the session that the key is for. + pub session_id: String, + + /// The key for the session. + pub session_key: ExportedGroupSessionKey, + + /// The Ed25519 key of the device which initiated the session originally. + pub sender_claimed_keys: BTreeMap, + + /// Chain of Curve25519 keys through which this session was forwarded, via + /// m.forwarded_room_key events. + pub forwarding_curve25519_key_chain: Vec, +} + +impl TryInto for ExportedRoomKey { + type Error = (); + + fn try_into(self) -> Result { + if self.sender_claimed_keys.len() != 1 { + Err(()) + } else { + let (algorithm, claimed_key) = self.sender_claimed_keys.iter().next().ok_or(())?; + + if algorithm != &DeviceKeyAlgorithm::Ed25519 { + return Err(()); + } + + Ok(ForwardedRoomKeyEventContent { + algorithm: self.algorithm, + room_id: self.room_id, + sender_key: self.sender_key, + session_id: self.session_id, + session_key: self.session_key.0.clone(), + sender_claimed_ed25519_key: claimed_key.to_owned(), + forwarding_curve25519_key_chain: self.forwarding_curve25519_key_chain, + }) + } + } +} + +impl From for ExportedRoomKey { + fn from(forwarded_key: ForwardedRoomKeyEventContent) -> Self { + let mut sender_claimed_keys: BTreeMap = BTreeMap::new(); + sender_claimed_keys.insert( + DeviceKeyAlgorithm::Ed25519, + forwarded_key.sender_claimed_ed25519_key, + ); + + Self { + algorithm: forwarded_key.algorithm, + room_id: forwarded_key.room_id, + session_id: forwarded_key.session_id, + forwarding_curve25519_key_chain: forwarded_key.forwarding_curve25519_key_chain, + sender_claimed_keys, + sender_key: forwarded_key.sender_key, + session_key: ExportedGroupSessionKey(forwarded_key.session_key), + } + } +} + #[cfg(test)] mod test { use std::{ diff --git a/matrix_sdk_crypto/src/olm/mod.rs b/matrix_sdk_crypto/src/olm/mod.rs index c69293c5..05e6b874 100644 --- a/matrix_sdk_crypto/src/olm/mod.rs +++ b/matrix_sdk_crypto/src/olm/mod.rs @@ -24,7 +24,8 @@ mod utility; pub use account::{Account, AccountPickle, IdentityKeys, PickledAccount}; pub use group_sessions::{ - EncryptionSettings, InboundGroupSession, InboundGroupSessionPickle, PickledInboundGroupSession, + EncryptionSettings, ExportedRoomKey, InboundGroupSession, InboundGroupSessionPickle, + PickledInboundGroupSession, }; pub(crate) use group_sessions::{GroupSessionKey, OutboundGroupSession}; pub use olm_rs::PicklingMode; From 3e9b0a8e7f2d5fa82a2d1e676d8465fe5bfbbf30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Sep 2020 15:03:19 +0200 Subject: [PATCH 14/24] crypto: Correctly store the ed25519 key map for inbound group sessions. --- .../src/olm/group_sessions/inbound.rs | 24 ++-- matrix_sdk_crypto/src/store/sqlite.rs | 130 +++++++++++++----- 2 files changed, 107 insertions(+), 47 deletions(-) diff --git a/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs index ce41675a..ad9edf77 100644 --- a/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs +++ b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs @@ -49,9 +49,10 @@ pub struct InboundGroupSession { inner: Arc>, session_id: Arc, pub(crate) sender_key: Arc, - pub(crate) signing_key: Arc, + pub(crate) signing_key: Arc>, pub(crate) room_id: Arc, forwarding_chains: Arc>>>, + imported: Arc, } impl InboundGroupSession { @@ -80,13 +81,17 @@ impl InboundGroupSession { let session = OlmInboundGroupSession::new(&session_key.0)?; let session_id = session.session_id(); + let mut keys: BTreeMap = BTreeMap::new(); + keys.insert(DeviceKeyAlgorithm::Ed25519, signing_key.to_owned()); + Ok(InboundGroupSession { inner: Arc::new(Mutex::new(session)), session_id: Arc::new(session_id), sender_key: Arc::new(sender_key.to_owned()), - signing_key: Arc::new(signing_key.to_owned()), + signing_key: Arc::new(keys), room_id: Arc::new(room_id.clone()), forwarding_chains: Arc::new(Mutex::new(None)), + imported: Arc::new(false), }) } @@ -102,9 +107,10 @@ impl InboundGroupSession { PickledInboundGroupSession { pickle: InboundGroupSessionPickle::from(pickle), sender_key: self.sender_key.to_string(), - signing_key: self.signing_key.to_string(), + signing_key: (&*self.signing_key).clone(), room_id: (&*self.room_id).clone(), forwarding_chains: self.forwarding_chains.lock().await.clone(), + imported: *self.imported, } } @@ -120,10 +126,6 @@ impl InboundGroupSession { let session_key = ExportedGroupSessionKey(self.inner.lock().await.export(message_index).ok()?); - let mut sender_claimed_keys: BTreeMap = BTreeMap::new(); - - sender_claimed_keys.insert(DeviceKeyAlgorithm::Ed25519, (&*self.signing_key).to_owned()); - Some(ExportedRoomKey { algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, room_id: (&*self.room_id).clone(), @@ -136,7 +138,7 @@ impl InboundGroupSession { .as_ref() .cloned() .unwrap_or_default(), - sender_claimed_keys, + sender_claimed_keys: (&*self.signing_key).clone(), session_key, }) } @@ -166,6 +168,7 @@ impl InboundGroupSession { signing_key: Arc::new(pickle.signing_key), room_id: Arc::new(pickle.room_id), forwarding_chains: Arc::new(Mutex::new(pickle.forwarding_chains)), + imported: Arc::new(pickle.imported), }) } @@ -265,12 +268,15 @@ pub struct PickledInboundGroupSession { /// The public curve25519 key of the account that sent us the session pub sender_key: String, /// The public ed25519 key of the account that sent us the session. - pub signing_key: String, + pub signing_key: BTreeMap, /// The id of the room that the session is used in. pub room_id: RoomId, /// The list of claimed ed25519 that forwarded us this key. Will be None if /// we dirrectly received this session. pub forwarding_chains: Option>, + /// Flag remembering if the session was dirrectly sent to us by the sender + /// or if it was imported. + pub imported: bool, } /// The typed representation of a base64 encoded string of the GroupSession pickle. diff --git a/matrix_sdk_crypto/src/store/sqlite.rs b/matrix_sdk_crypto/src/store/sqlite.rs index d40ca12b..219ffb06 100644 --- a/matrix_sdk_crypto/src/store/sqlite.rs +++ b/matrix_sdk_crypto/src/store/sqlite.rs @@ -231,14 +231,16 @@ impl SqliteStore { .execute( r#" CREATE TABLE IF NOT EXISTS inbound_group_sessions ( - "session_id" TEXT NOT NULL PRIMARY KEY, + "id" INTEGER NOT NULL PRIMARY KEY, + "session_id" TEXT NOT NULL, "account_id" INTEGER NOT NULL, "sender_key" TEXT NOT NULL, - "signing_key" TEXT NOT NULL, "room_id" TEXT NOT NULL, "pickle" BLOB NOT NULL, + "imported" INTEGER NOT NULL, FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE + UNIQUE(account_id,session_id,sender_key) ); CREATE INDEX IF NOT EXISTS "olm_groups_sessions_account_id" ON "inbound_group_sessions" ("account_id"); @@ -246,6 +248,24 @@ impl SqliteStore { ) .await?; + connection + .execute( + r#" + CREATE TABLE IF NOT EXISTS group_session_claimed_keys ( + "id" INTEGER NOT NULL PRIMARY KEY, + "session_id" INTEGER NOT NULL, + "algorithm" TEXT NOT NULL, + "key" TEXT NOT NULL, + FOREIGN KEY ("session_id") REFERENCES "inbound_group_sessions" ("id") + ON DELETE CASCADE + UNIQUE(session_id, algorithm) + ); + + CREATE INDEX IF NOT EXISTS "group_session_claimed_keys_session_id" ON "inbound_group_sessions" ("session_id"); + "#, + ) + .await?; + connection .execute( r#" @@ -475,45 +495,55 @@ impl SqliteStore { let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; let mut connection = self.connection.lock().await; - let mut rows: Vec<(String, String, String, String)> = query_as( - "SELECT pickle, sender_key, signing_key, room_id + let mut rows: Vec<(i64, String, String, String, bool)> = query_as( + "SELECT id, pickle, sender_key, room_id, imported FROM inbound_group_sessions WHERE account_id = ?", ) .bind(account_id) .fetch_all(&mut *connection) .await?; - let mut group_sessions = rows - .drain(..) - .map(|row| { - let pickle = row.0; - let sender_key = row.1; - let signing_key = row.2; - let room_id = row.3; + for row in rows.drain(..) { + let session_row_id = row.0; + let pickle = row.1; + let sender_key = row.2; + let room_id = row.3; + let imported = row.4; - let pickle = PickledInboundGroupSession { - pickle: InboundGroupSessionPickle::from(pickle), - sender_key, - signing_key, - room_id: RoomId::try_from(room_id)?, - // Fixme we need to store/restore these once we get support - // for key requesting/forwarding. - forwarding_chains: None, - }; + let key_rows: Vec<(String, String)> = query_as( + "SELECT algorithm, key FROM group_session_claimed_keys WHERE session_id = ?", + ) + .bind(session_row_id) + .fetch_all(&mut *connection) + .await?; - Ok(InboundGroupSession::from_pickle( + let claimed_keys: BTreeMap = key_rows + .into_iter() + .filter_map(|row| { + let algorithm = row.0.parse::().ok()?; + let key = row.1; + + Some((algorithm, key)) + }) + .collect(); + + let pickle = PickledInboundGroupSession { + pickle: InboundGroupSessionPickle::from(pickle), + sender_key, + signing_key: claimed_keys, + room_id: RoomId::try_from(room_id)?, + // Fixme we need to store/restore these once we get support + // for key requesting/forwarding. + forwarding_chains: None, + imported, + }; + + self.inbound_group_sessions + .add(InboundGroupSession::from_pickle( pickle, self.get_pickle_mode(), - )?) - }) - .collect::>>()?; - - group_sessions - .drain(..) - .map(|s| { - self.inbound_group_sessions.add(s); - }) - .for_each(drop); + )?); + } Ok(()) } @@ -1146,23 +1176,47 @@ impl CryptoStore for SqliteStore { // the key import feature. query( - "INSERT INTO inbound_group_sessions ( - session_id, account_id, sender_key, signing_key, - room_id, pickle + "REPLACE INTO inbound_group_sessions ( + session_id, account_id, sender_key, + room_id, pickle, imported ) VALUES (?1, ?2, ?3, ?4, ?5, ?6) - ON CONFLICT(session_id) DO UPDATE SET - pickle = excluded.pickle ", ) .bind(session_id) .bind(account_id) - .bind(pickle.sender_key) - .bind(pickle.signing_key) + .bind(&pickle.sender_key) .bind(pickle.room_id.as_str()) .bind(pickle.pickle.as_str()) + .bind(pickle.imported) .execute(&mut *connection) .await?; + let row: (i64,) = query_as( + "SELECT id FROM inbound_group_sessions + WHERE account_id = ? and session_id = ? and sender_key = ?", + ) + .bind(account_id) + .bind(session_id) + .bind(pickle.sender_key) + .fetch_one(&mut *connection) + .await?; + + let session_row_id = row.0; + + for (key_id, key) in pickle.signing_key { + query( + "INSERT OR IGNORE INTO group_session_claimed_keys ( + session_id, algorithm, key + ) VALUES (?1, ?2, ?3) + ", + ) + .bind(session_row_id) + .bind(serde_json::to_string(&key_id)?) + .bind(key) + .execute(&mut *connection) + .await?; + } + Ok(self.inbound_group_sessions.add(session)) } From e828828ace130f8e2f9b33705432a5808f8093b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Sep 2020 15:11:25 +0200 Subject: [PATCH 15/24] crypto: Document the exported key -> forwarded room key conversion methods. --- matrix_sdk_crypto/src/olm/group_sessions/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/matrix_sdk_crypto/src/olm/group_sessions/mod.rs b/matrix_sdk_crypto/src/olm/group_sessions/mod.rs index 7f9ac026..a8b2137e 100644 --- a/matrix_sdk_crypto/src/olm/group_sessions/mod.rs +++ b/matrix_sdk_crypto/src/olm/group_sessions/mod.rs @@ -69,6 +69,12 @@ pub struct ExportedRoomKey { impl TryInto for ExportedRoomKey { type Error = (); + /// Convert an exported room key into a content for a forwarded room key + /// event. + /// + /// This will fail if the exported room key has multiple sender claimed keys + /// or if the algorithm of the claimed sender key isn't + /// `DeviceKeyAlgorithm::Ed25519`. fn try_into(self) -> Result { if self.sender_claimed_keys.len() != 1 { Err(()) @@ -93,6 +99,7 @@ impl TryInto for ExportedRoomKey { } impl From for ExportedRoomKey { + /// Convert the content of a forwarded room key into a exported room key. fn from(forwarded_key: ForwardedRoomKeyEventContent) -> Self { let mut sender_claimed_keys: BTreeMap = BTreeMap::new(); sender_claimed_keys.insert( From 9617d9aac91cfc41792ee27802400bd6db580f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Sep 2020 16:10:16 +0200 Subject: [PATCH 16/24] crypto: Test the import/export of group sessions. --- .../src/olm/group_sessions/inbound.rs | 43 ++++++++++++++++++- matrix_sdk_crypto/src/olm/mod.rs | 21 ++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs index ad9edf77..c0a295f5 100644 --- a/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs +++ b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs @@ -95,6 +95,20 @@ impl InboundGroupSession { }) } + /// Create a InboundGroupSession from an exported version of the group + /// session. + /// + /// Most notably this can be called with an `ExportedRoomKey` from a + /// previous [`export()`] call. + /// + /// + /// [`export()`]: #method.export + pub fn from_export( + exported_session: impl Into, + ) -> Result { + Self::try_from(exported_session.into()) + } + /// Store the group session as a base64 encoded string. /// /// # Arguments @@ -114,7 +128,10 @@ impl InboundGroupSession { } } - /// Export this session. + /// Export this session at the first known message index. + /// + /// If only a limited part of this session should be exported use + /// [`export_at_index()`](#method.export_at_index). pub async fn export(&self) -> ExportedRoomKey { self.export_at_index(self.first_known_index().await) .await @@ -295,3 +312,27 @@ impl InboundGroupSessionPickle { &self.0 } } + +impl TryFrom for InboundGroupSession { + type Error = OlmGroupSessionError; + + fn try_from(key: ExportedRoomKey) -> Result { + let session = OlmInboundGroupSession::import(&key.session_key.0)?; + + let forwarding_chains = if key.forwarding_curve25519_key_chain.is_empty() { + None + } else { + Some(key.forwarding_curve25519_key_chain) + }; + + Ok(InboundGroupSession { + inner: Arc::new(Mutex::new(session)), + session_id: Arc::new(key.session_id), + sender_key: Arc::new(key.sender_key), + signing_key: Arc::new(key.sender_claimed_keys), + room_id: Arc::new(key.room_id), + forwarding_chains: Arc::new(Mutex::new(forwarding_chains)), + imported: Arc::new(true), + }) + } +} diff --git a/matrix_sdk_crypto/src/olm/mod.rs b/matrix_sdk_crypto/src/olm/mod.rs index 05e6b874..e88b4959 100644 --- a/matrix_sdk_crypto/src/olm/mod.rs +++ b/matrix_sdk_crypto/src/olm/mod.rs @@ -38,10 +38,11 @@ pub(crate) mod test { use crate::olm::{Account, InboundGroupSession, Session}; use matrix_sdk_common::{ api::r0::keys::SignedKey, + events::forwarded_room_key::ForwardedRoomKeyEventContent, identifiers::{room_id, user_id, DeviceId, UserId}, }; use olm_rs::session::OlmMessage; - use std::collections::BTreeMap; + use std::{collections::BTreeMap, convert::TryInto}; fn alice_id() -> UserId { user_id!("@alice:example.org") @@ -222,4 +223,22 @@ pub(crate) mod test { inbound.decrypt_helper(ciphertext).await.unwrap().0 ); } + + #[tokio::test] + async fn group_session_export() { + let alice = Account::new(&alice_id(), &alice_device_id()); + let room_id = room_id!("!test:localhost"); + + let (_, inbound) = alice + .create_group_session_pair(&room_id, Default::default()) + .await + .unwrap(); + + let export = inbound.export().await; + let export: ForwardedRoomKeyEventContent = export.try_into().unwrap(); + + let imported = InboundGroupSession::from_export(export).unwrap(); + + assert_eq!(inbound.session_id(), imported.session_id()); + } } From 127d4c225b7f0bc2e8ae789ccf5d850a7e177116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Sep 2020 16:34:18 +0200 Subject: [PATCH 17/24] crypto: Change the crypto store so we can save multiple group sessions at once. --- matrix_sdk_crypto/src/machine.rs | 4 +- matrix_sdk_crypto/src/store/memorystore.rs | 10 +- matrix_sdk_crypto/src/store/mod.rs | 9 +- matrix_sdk_crypto/src/store/sqlite.rs | 116 ++++++++++++--------- 4 files changed, 79 insertions(+), 60 deletions(-) diff --git a/matrix_sdk_crypto/src/machine.rs b/matrix_sdk_crypto/src/machine.rs index a0935c27..55ee480e 100644 --- a/matrix_sdk_crypto/src/machine.rs +++ b/matrix_sdk_crypto/src/machine.rs @@ -984,7 +984,7 @@ impl OlmMachine { &event.content.room_id, session_key, )?; - let _ = self.store.save_inbound_group_session(session).await?; + let _ = self.store.save_inbound_group_sessions(&[session]).await?; let event = Raw::from(AnyToDeviceEvent::RoomKey(event.clone())); Ok(Some(event)) @@ -1014,7 +1014,7 @@ impl OlmMachine { .await .map_err(|_| EventError::UnsupportedAlgorithm)?; - let _ = self.store.save_inbound_group_session(inbound).await?; + let _ = self.store.save_inbound_group_sessions(&[inbound]).await?; let _ = self .outbound_group_sessions diff --git a/matrix_sdk_crypto/src/store/memorystore.rs b/matrix_sdk_crypto/src/store/memorystore.rs index f7d3d753..ec4a5246 100644 --- a/matrix_sdk_crypto/src/store/memorystore.rs +++ b/matrix_sdk_crypto/src/store/memorystore.rs @@ -80,8 +80,12 @@ impl CryptoStore for MemoryStore { Ok(self.sessions.get(sender_key)) } - async fn save_inbound_group_session(&self, session: InboundGroupSession) -> Result { - Ok(self.inbound_group_sessions.add(session)) + async fn save_inbound_group_sessions(&self, sessions: &[InboundGroupSession]) -> Result<()> { + for session in sessions { + self.inbound_group_sessions.add(session.clone()); + } + + Ok(()) } async fn get_inbound_group_session( @@ -208,7 +212,7 @@ mod test { let store = MemoryStore::new(); let _ = store - .save_inbound_group_session(inbound.clone()) + .save_inbound_group_sessions(&[inbound.clone()]) .await .unwrap(); diff --git a/matrix_sdk_crypto/src/store/mod.rs b/matrix_sdk_crypto/src/store/mod.rs index a7cce545..2efd7d29 100644 --- a/matrix_sdk_crypto/src/store/mod.rs +++ b/matrix_sdk_crypto/src/store/mod.rs @@ -157,15 +157,12 @@ pub trait CryptoStore: Debug { /// * `sender_key` - The sender key that was used to establish the sessions. async fn get_sessions(&self, sender_key: &str) -> Result>>>>; - /// Save the given inbound group session in the store. - /// - /// If the session wasn't already in the store true is returned, false - /// otherwise. + /// Save the given inbound group sessions in the store. /// /// # Arguments /// - /// * `session` - The session that should be stored. - async fn save_inbound_group_session(&self, session: InboundGroupSession) -> Result; + /// * `sessions` - The sessions that should be stored. + async fn save_inbound_group_sessions(&self, session: &[InboundGroupSession]) -> Result<()>; /// Get the inbound group session from our store. /// diff --git a/matrix_sdk_crypto/src/store/sqlite.rs b/matrix_sdk_crypto/src/store/sqlite.rs index 219ffb06..00dfccd5 100644 --- a/matrix_sdk_crypto/src/store/sqlite.rs +++ b/matrix_sdk_crypto/src/store/sqlite.rs @@ -796,6 +796,64 @@ impl SqliteStore { } } + async fn save_inbound_group_session_helper( + &self, + account_id: i64, + connection: &mut SqliteConnection, + session: &InboundGroupSession, + ) -> Result<()> { + let pickle = session.pickle(self.get_pickle_mode()).await; + let session_id = session.session_id(); + + // FIXME we need to store/restore the forwarding chains. + // FIXME this should be converted so it accepts an array of sessions for + // the key import feature. + + query( + "REPLACE INTO inbound_group_sessions ( + session_id, account_id, sender_key, + room_id, pickle, imported + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ", + ) + .bind(session_id) + .bind(account_id) + .bind(&pickle.sender_key) + .bind(pickle.room_id.as_str()) + .bind(pickle.pickle.as_str()) + .bind(pickle.imported) + .execute(&mut *connection) + .await?; + + let row: (i64,) = query_as( + "SELECT id FROM inbound_group_sessions + WHERE account_id = ? and session_id = ? and sender_key = ?", + ) + .bind(account_id) + .bind(session_id) + .bind(pickle.sender_key) + .fetch_one(&mut *connection) + .await?; + + let session_row_id = row.0; + + for (key_id, key) in pickle.signing_key { + query( + "INSERT OR IGNORE INTO group_session_claimed_keys ( + session_id, algorithm, key + ) VALUES (?1, ?2, ?3) + ", + ) + .bind(session_row_id) + .bind(serde_json::to_string(&key_id)?) + .bind(key) + .execute(&mut *connection) + .await?; + } + + Ok(()) + } + async fn load_cross_signing_key( connection: &mut SqliteConnection, user_id: &UserId, @@ -1165,59 +1223,19 @@ impl CryptoStore for SqliteStore { Ok(self.get_sessions_for(sender_key).await?) } - async fn save_inbound_group_session(&self, session: InboundGroupSession) -> Result { + async fn save_inbound_group_sessions(&self, sessions: &[InboundGroupSession]) -> Result<()> { let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; - let pickle = session.pickle(self.get_pickle_mode()).await; let mut connection = self.connection.lock().await; - let session_id = session.session_id(); - // FIXME we need to store/restore the forwarding chains. - // FIXME this should be converted so it accepts an array of sessions for - // the key import feature. + // FIXME use a transaction here once sqlx gets better support for them. - query( - "REPLACE INTO inbound_group_sessions ( - session_id, account_id, sender_key, - room_id, pickle, imported - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6) - ", - ) - .bind(session_id) - .bind(account_id) - .bind(&pickle.sender_key) - .bind(pickle.room_id.as_str()) - .bind(pickle.pickle.as_str()) - .bind(pickle.imported) - .execute(&mut *connection) - .await?; - - let row: (i64,) = query_as( - "SELECT id FROM inbound_group_sessions - WHERE account_id = ? and session_id = ? and sender_key = ?", - ) - .bind(account_id) - .bind(session_id) - .bind(pickle.sender_key) - .fetch_one(&mut *connection) - .await?; - - let session_row_id = row.0; - - for (key_id, key) in pickle.signing_key { - query( - "INSERT OR IGNORE INTO group_session_claimed_keys ( - session_id, algorithm, key - ) VALUES (?1, ?2, ?3) - ", - ) - .bind(session_row_id) - .bind(serde_json::to_string(&key_id)?) - .bind(key) - .execute(&mut *connection) - .await?; + for session in sessions { + self.save_inbound_group_session_helper(account_id, &mut connection, session) + .await?; + self.inbound_group_sessions.add(session.clone()); } - Ok(self.inbound_group_sessions.add(session)) + Ok(()) } async fn get_inbound_group_session( @@ -1581,7 +1599,7 @@ mod test { .expect("Can't create session"); store - .save_inbound_group_session(session) + .save_inbound_group_sessions(&[session]) .await .expect("Can't save group session"); } @@ -1601,7 +1619,7 @@ mod test { .expect("Can't create session"); store - .save_inbound_group_session(session.clone()) + .save_inbound_group_sessions(&[session.clone()]) .await .expect("Can't save group session"); From 7bd0e4975ba863ed6218f35833a9c58b4ddd20ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Sep 2020 17:27:10 +0200 Subject: [PATCH 18/24] crypto: Store the forwarding chains for group sessions. --- matrix_sdk_crypto/src/store/sqlite.rs | 73 +++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/matrix_sdk_crypto/src/store/sqlite.rs b/matrix_sdk_crypto/src/store/sqlite.rs index 00dfccd5..e55bcb8b 100644 --- a/matrix_sdk_crypto/src/store/sqlite.rs +++ b/matrix_sdk_crypto/src/store/sqlite.rs @@ -261,7 +261,24 @@ impl SqliteStore { UNIQUE(session_id, algorithm) ); - CREATE INDEX IF NOT EXISTS "group_session_claimed_keys_session_id" ON "inbound_group_sessions" ("session_id"); + CREATE INDEX IF NOT EXISTS "group_session_claimed_keys_session_id" ON "group_session_claimed_keys" ("session_id"); + "#, + ) + .await?; + + connection + .execute( + r#" + CREATE TABLE IF NOT EXISTS group_session_chains ( + "id" INTEGER NOT NULL PRIMARY KEY, + "key" TEXT NOT NULL, + "session_id" INTEGER NOT NULL, + FOREIGN KEY ("session_id") REFERENCES "inbound_group_sessions" ("id") + ON DELETE CASCADE + UNIQUE(session_id, key) + ); + + CREATE INDEX IF NOT EXISTS "group_session_chains_session_id" ON "group_session_chains" ("session_id"); "#, ) .await?; @@ -527,14 +544,26 @@ impl SqliteStore { }) .collect(); + let mut chain_rows: Vec<(String,)> = + query_as("SELECT key, key FROM group_session_chains WHERE session_id = ?") + .bind(session_row_id) + .fetch_all(&mut *connection) + .await?; + + let chains: Vec = chain_rows.drain(..).map(|r| r.0).collect(); + + let chains = if chains.is_empty() { + None + } else { + Some(chains) + }; + let pickle = PickledInboundGroupSession { pickle: InboundGroupSessionPickle::from(pickle), sender_key, signing_key: claimed_keys, room_id: RoomId::try_from(room_id)?, - // Fixme we need to store/restore these once we get support - // for key requesting/forwarding. - forwarding_chains: None, + forwarding_chains: chains, imported, }; @@ -805,10 +834,6 @@ impl SqliteStore { let pickle = session.pickle(self.get_pickle_mode()).await; let session_id = session.session_id(); - // FIXME we need to store/restore the forwarding chains. - // FIXME this should be converted so it accepts an array of sessions for - // the key import feature. - query( "REPLACE INTO inbound_group_sessions ( session_id, account_id, sender_key, @@ -839,7 +864,7 @@ impl SqliteStore { for (key_id, key) in pickle.signing_key { query( - "INSERT OR IGNORE INTO group_session_claimed_keys ( + "REPLACE INTO group_session_claimed_keys ( session_id, algorithm, key ) VALUES (?1, ?2, ?3) ", @@ -851,6 +876,21 @@ impl SqliteStore { .await?; } + if let Some(chains) = pickle.forwarding_chains { + for key in chains { + query( + "REPLACE INTO group_session_chains ( + session_id, key + ) VALUES (?1, ?2) + ", + ) + .bind(session_row_id) + .bind(key) + .execute(&mut *connection) + .await?; + } + } + Ok(()) } @@ -1606,7 +1646,7 @@ mod test { #[tokio::test] async fn load_inbound_group_session() { - let (account, store, _dir) = get_loaded_store().await; + let (account, store, dir) = get_loaded_store().await; let identity_keys = account.identity_keys(); let outbound_session = OlmOutboundGroupSession::new(); @@ -1618,11 +1658,22 @@ mod test { ) .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(); + store .save_inbound_group_sessions(&[session.clone()]) .await .expect("Can't save group session"); + let store = SqliteStore::open(&alice_id(), &alice_device_id(), dir.path()) + .await + .expect("Can't create store"); + + store.load_account().await.unwrap(); store.load_inbound_group_sessions().await.unwrap(); let loaded_session = store @@ -1631,6 +1682,8 @@ mod test { .unwrap() .unwrap(); assert_eq!(session, loaded_session); + let export = loaded_session.export().await; + assert!(!export.forwarding_curve25519_key_chain.is_empty()) } #[tokio::test] From 464e181f6636beb186bd9c48e2ae13014a483e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 10 Sep 2020 14:59:20 +0200 Subject: [PATCH 19/24] crypto: Add a method to get all group sessions from the store. --- matrix_sdk_crypto/src/store/caches.rs | 13 +++++++++++++ matrix_sdk_crypto/src/store/memorystore.rs | 4 ++++ matrix_sdk_crypto/src/store/mod.rs | 3 +++ matrix_sdk_crypto/src/store/sqlite.rs | 4 ++++ 4 files changed, 24 insertions(+) diff --git a/matrix_sdk_crypto/src/store/caches.rs b/matrix_sdk_crypto/src/store/caches.rs index 91344257..cc56c100 100644 --- a/matrix_sdk_crypto/src/store/caches.rs +++ b/matrix_sdk_crypto/src/store/caches.rs @@ -106,6 +106,19 @@ impl GroupSessionStore { .is_none() } + /// Get all the group sessions the store knows about. + pub fn get_all(&self) -> Vec { + self.entries + .iter() + .flat_map(|d| { + d.value() + .values() + .flat_map(|t| t.values().cloned().collect::>()) + .collect::>() + }) + .collect() + } + /// Get a inbound group session from our store. /// /// # Arguments diff --git a/matrix_sdk_crypto/src/store/memorystore.rs b/matrix_sdk_crypto/src/store/memorystore.rs index ec4a5246..4de015bf 100644 --- a/matrix_sdk_crypto/src/store/memorystore.rs +++ b/matrix_sdk_crypto/src/store/memorystore.rs @@ -99,6 +99,10 @@ impl CryptoStore for MemoryStore { .get(room_id, sender_key, session_id)) } + async fn get_inbound_group_sessions(&self) -> Result> { + Ok(self.inbound_group_sessions.get_all()) + } + fn users_for_key_query(&self) -> HashSet { #[allow(clippy::map_clone)] self.users_for_key_query.iter().map(|u| u.clone()).collect() diff --git a/matrix_sdk_crypto/src/store/mod.rs b/matrix_sdk_crypto/src/store/mod.rs index 2efd7d29..88d2ec90 100644 --- a/matrix_sdk_crypto/src/store/mod.rs +++ b/matrix_sdk_crypto/src/store/mod.rs @@ -179,6 +179,9 @@ pub trait CryptoStore: Debug { session_id: &str, ) -> Result>; + /// Get all the inbound group sessions we have stored. + async fn get_inbound_group_sessions(&self) -> Result>; + /// Is the given user already tracked. fn is_user_tracked(&self, user_id: &UserId) -> bool; diff --git a/matrix_sdk_crypto/src/store/sqlite.rs b/matrix_sdk_crypto/src/store/sqlite.rs index e55bcb8b..91b2a339 100644 --- a/matrix_sdk_crypto/src/store/sqlite.rs +++ b/matrix_sdk_crypto/src/store/sqlite.rs @@ -1289,6 +1289,10 @@ impl CryptoStore for SqliteStore { .get(room_id, sender_key, session_id)) } + async fn get_inbound_group_sessions(&self) -> Result> { + Ok(self.inbound_group_sessions.get_all()) + } + fn is_user_tracked(&self, user_id: &UserId) -> bool { self.tracked_users.contains(user_id) } From 23e953d9cfa082eb83098d096bf4b278312ce9d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 10 Sep 2020 15:49:34 +0200 Subject: [PATCH 20/24] crypto: Hide some methods that shouldn't be public. --- matrix_sdk_crypto/src/olm/group_sessions/inbound.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs index c0a295f5..a35c6190 100644 --- a/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs +++ b/matrix_sdk_crypto/src/olm/group_sessions/inbound.rs @@ -72,7 +72,7 @@ impl InboundGroupSession { /// /// * `session_key` - The private session key that is used to decrypt /// messages. - pub fn new( + pub(crate) fn new( sender_key: &str, signing_key: &str, room_id: &RoomId, @@ -189,6 +189,11 @@ impl InboundGroupSession { }) } + /// The room where this session is used in. + pub fn room_id(&self) -> &RoomId { + &self.room_id + } + /// Returns the unique identifier for this session. pub fn session_id(&self) -> &str { &self.session_id @@ -207,7 +212,7 @@ impl InboundGroupSession { /// # Arguments /// /// * `message` - The message that should be decrypted. - pub async fn decrypt_helper( + pub(crate) async fn decrypt_helper( &self, message: String, ) -> Result<(String, u32), OlmGroupSessionError> { @@ -219,7 +224,7 @@ impl InboundGroupSession { /// # Arguments /// /// * `event` - The event that should be decrypted. - pub async fn decrypt( + pub(crate) async fn decrypt( &self, event: &SyncMessageEvent, ) -> MegolmResult<(Raw, u32)> { From 848156213b57effbafef201000ffdad109abfd0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 10 Sep 2020 15:51:39 +0200 Subject: [PATCH 21/24] crypto: Add a PartialEq derive for the exported key struct. --- matrix_sdk_crypto/src/olm/group_sessions/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix_sdk_crypto/src/olm/group_sessions/mod.rs b/matrix_sdk_crypto/src/olm/group_sessions/mod.rs index a8b2137e..b0e69599 100644 --- a/matrix_sdk_crypto/src/olm/group_sessions/mod.rs +++ b/matrix_sdk_crypto/src/olm/group_sessions/mod.rs @@ -34,14 +34,14 @@ pub struct GroupSessionKey(pub String); /// The exported version of an private session key of a group session. /// Can be used to create a new inbound group session. -#[derive(Clone, Debug, Serialize, Deserialize, Zeroize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Zeroize)] #[zeroize(drop)] pub struct ExportedGroupSessionKey(pub String); /// An exported version of a `InboundGroupSession` /// /// This can be used to share the `InboundGroupSession` in an exported file. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct ExportedRoomKey { /// The encryption algorithm that the session uses. pub algorithm: EventEncryptionAlgorithm, From e3f4c1849c805d28cf20d501239ae352056b77e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 10 Sep 2020 15:54:41 +0200 Subject: [PATCH 22/24] crypto: Finish up the key export feature. --- matrix_sdk_crypto/src/key_export.rs | 181 +++++++++++++++++++++++----- matrix_sdk_crypto/src/lib.rs | 1 + matrix_sdk_crypto/src/machine.rs | 104 +++++++++++++++- 3 files changed, 249 insertions(+), 37 deletions(-) diff --git a/matrix_sdk_crypto/src/key_export.rs b/matrix_sdk_crypto/src/key_export.rs index 753a1fcb..0a490bc1 100644 --- a/matrix_sdk_crypto/src/key_export.rs +++ b/matrix_sdk_crypto/src/key_export.rs @@ -26,24 +26,103 @@ use hmac::{Hmac, Mac, NewMac}; use pbkdf2::pbkdf2; use sha2::{Sha256, Sha512}; +use crate::olm::ExportedRoomKey; + const SALT_SIZE: usize = 16; const IV_SIZE: usize = 16; const MAC_SIZE: usize = 32; const KEY_SIZE: usize = 32; +const VERSION: u8 = 1; -pub fn decode(input: impl AsRef<[u8]>) -> Result, DecodeError> { +const HEADER: &'static str = "-----BEGIN MEGOLM SESSION DATA-----"; +const FOOTER: &'static str = "-----END MEGOLM SESSION DATA-----"; + +fn decode(input: impl AsRef<[u8]>) -> Result, DecodeError> { decode_config(input, STANDARD_NO_PAD) } -pub fn encode(input: impl AsRef<[u8]>) -> String { +fn encode(input: impl AsRef<[u8]>) -> String { encode_config(input, STANDARD_NO_PAD) } -pub fn encrypt(mut plaintext: &mut [u8], passphrase: &str, rounds: u32) -> String { +/// Try to decrypt a reader into a list of exported room keys. +/// +/// # Arguments +/// +/// * `passphrase` - The passphrase that was used to encrypt the exported keys. +/// +/// # Examples +/// ```no_run +/// # use std::io::Cursor; +/// # use matrix_sdk_crypto::{OlmMachine, decrypt_key_export}; +/// # use matrix_sdk_common::identifiers::user_id; +/// # use futures::executor::block_on; +/// # let alice = user_id!("@alice:example.org"); +/// # let machine = OlmMachine::new(&alice, "DEVICEID".into()); +/// # block_on(async { +/// # let export = Cursor::new("".to_owned()); +/// let exported_keys = decrypt_key_export(export, "1234").unwrap(); +/// machine.import_keys(exported_keys).await.unwrap(); +/// # }); +/// ``` +pub fn decrypt_key_export( + mut input: impl Read, + passphrase: &str, +) -> Result, DecodeError> { + let mut x: String = String::new(); + + input.read_to_string(&mut x).expect("Can't read string"); + + if !(x.trim_start().starts_with(HEADER) && x.trim_end().ends_with(FOOTER)) { + panic!("Invalid header/footer"); + } + + let payload: String = x + .lines() + .filter(|l| !(l.starts_with(HEADER) || l.starts_with(FOOTER))) + .collect(); + + Ok(serde_json::from_str(&decrypt_helper(&payload, passphrase)?).unwrap()) +} + +/// Encrypt the list of exported room keys using the given passphrase. +/// +/// # Arguments +/// +/// * `keys` - A list of sessions that should be encrypted. +/// +/// * `passphrase` - The passphrase that will be used to encrypt the exported +/// room keys. +/// +/// * `rounds` - The number of rounds that should be used for the key +/// derivation when the passphrase gets turned into an AES key. More rounds are +/// increasingly computationally intensive and as such help against bruteforce +/// attacks. Should be at least `10000`, while values in the `100000` ranges +/// should be preferred. +/// +/// # Examples +/// ```no_run +/// # use matrix_sdk_crypto::{OlmMachine, encrypt_key_export}; +/// # use matrix_sdk_common::identifiers::{user_id, room_id}; +/// # use futures::executor::block_on; +/// # let alice = user_id!("@alice:example.org"); +/// # let machine = OlmMachine::new(&alice, "DEVICEID".into()); +/// # block_on(async { +/// let room_id = room_id!("!test:localhost"); +/// let exported_keys = machine.export_keys(|s| s.room_id() == &room_id).await.unwrap(); +/// let encrypted_export = encrypt_key_export(&exported_keys, "1234", 1); +/// # }); +/// ``` +pub fn encrypt_key_export(keys: &Vec, passphrase: &str, rounds: u32) -> String { + let mut plaintext = serde_json::to_string(keys).unwrap().into_bytes(); + let ciphertext = encrypt_helper(&mut plaintext, passphrase, rounds); + [HEADER.to_owned(), ciphertext, FOOTER.to_owned()].join("\n") +} + +fn encrypt_helper(mut plaintext: &mut [u8], passphrase: &str, rounds: u32) -> String { let mut salt = [0u8; SALT_SIZE]; let mut iv = [0u8; IV_SIZE]; let mut derived_keys = [0u8; KEY_SIZE * 2]; - let version: u8 = 1; getrandom(&mut salt).expect("Can't generate randomness"); getrandom(&mut iv).expect("Can't generate randomness"); @@ -60,7 +139,7 @@ pub fn encrypt(mut plaintext: &mut [u8], passphrase: &str, rounds: u32) -> Strin let mut payload: Vec = vec![]; - payload.extend(&version.to_be_bytes()); + payload.extend(&VERSION.to_be_bytes()); payload.extend(&salt); payload.extend(&iv.to_be_bytes()); payload.extend(&rounds.to_be_bytes()); @@ -75,7 +154,7 @@ pub fn encrypt(mut plaintext: &mut [u8], passphrase: &str, rounds: u32) -> Strin encode(payload) } -pub fn decrypt(ciphertext: &str, passphrase: &str) -> Result { +fn decrypt_helper(ciphertext: &str, passphrase: &str) -> Result { let decoded = decode(ciphertext)?; let mut decoded = Cursor::new(decoded); @@ -99,7 +178,7 @@ pub fn decrypt(ciphertext: &str, passphrase: &str) -> Result Result OlmResult<()> { @@ -1529,6 +1529,102 @@ impl OlmMachine { device_owner_identity, }) } + + /// Import the given room keys into our store. + /// + /// # Arguments + /// + /// * `exported_keys` - A list of previously exported keys that should be + /// imported into our store. If we already have a better version of a key + /// the key will *not* be imported. + /// + /// Returns the number of sessions that were imported to the store. + /// + /// # Examples + /// ```no_run + /// # use std::io::Cursor; + /// # use matrix_sdk_crypto::{OlmMachine, decrypt_key_export}; + /// # use matrix_sdk_common::identifiers::user_id; + /// # use futures::executor::block_on; + /// # let alice = user_id!("@alice:example.org"); + /// # let machine = OlmMachine::new(&alice, "DEVICEID".into()); + /// # block_on(async { + /// # let export = Cursor::new("".to_owned()); + /// let exported_keys = decrypt_key_export(export, "1234").unwrap(); + /// machine.import_keys(exported_keys).await.unwrap(); + /// # }); + /// ``` + pub async fn import_keys(&self, mut exported_keys: Vec) -> StoreResult { + let mut sessions = Vec::new(); + + for key in exported_keys.drain(..) { + let session = InboundGroupSession::from_export(key)?; + + // Only import the session if we didn't have this session or if it's + // a better version of the same session, that is the first known + // index is lower. + if let Some(existing_session) = self + .store + .get_inbound_group_session( + &session.room_id, + &session.sender_key, + session.session_id(), + ) + .await? + { + if session.first_known_index().await < existing_session.first_known_index().await { + sessions.push(session) + } + } else { + sessions.push(session) + } + } + + let num_sessions = sessions.len(); + + self.store.save_inbound_group_sessions(&sessions).await?; + + Ok(num_sessions) + } + + /// Export the keys that match the given predicate. + /// + /// + /// # Examples + /// + /// ```no_run + /// # use matrix_sdk_crypto::{OlmMachine, encrypt_key_export}; + /// # use matrix_sdk_common::identifiers::{user_id, room_id}; + /// # use futures::executor::block_on; + /// # let alice = user_id!("@alice:example.org"); + /// # let machine = OlmMachine::new(&alice, "DEVICEID".into()); + /// # block_on(async { + /// let room_id = room_id!("!test:localhost"); + /// let exported_keys = machine.export_keys(|s| s.room_id() == &room_id).await.unwrap(); + /// let encrypted_export = encrypt_key_export(&exported_keys, "1234", 1); + /// # }); + /// ``` + pub async fn export_keys( + &self, + mut predicate: impl FnMut(&InboundGroupSession) -> bool, + ) -> StoreResult> { + let mut exported = Vec::new(); + + let mut sessions: Vec = self + .store + .get_inbound_group_sessions() + .await? + .drain(..) + .filter(|s| predicate(&s)) + .collect(); + + for session in sessions.drain(..) { + let export = session.export().await; + exported.push(export); + } + + Ok(exported) + } } #[cfg(test)] @@ -1623,7 +1719,7 @@ pub(crate) mod test { content.deserialize().unwrap() } - async fn get_prepared_machine() -> (OlmMachine, OneTimeKeys) { + pub(crate) async fn get_prepared_machine() -> (OlmMachine, OneTimeKeys) { let machine = OlmMachine::new(&user_id(), &alice_device_id()); machine.account.update_uploaded_key_count(0); let request = machine From 7790c3db8f3f1e8200d6e0e25c5ed1eb9d6f7dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 10 Sep 2020 16:07:28 +0200 Subject: [PATCH 23/24] crypto: Fix a bunch of clippy warnings. --- matrix_sdk_crypto/src/key_export.rs | 8 ++++---- matrix_sdk_crypto/src/machine.rs | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/matrix_sdk_crypto/src/key_export.rs b/matrix_sdk_crypto/src/key_export.rs index 0a490bc1..cc54a3cd 100644 --- a/matrix_sdk_crypto/src/key_export.rs +++ b/matrix_sdk_crypto/src/key_export.rs @@ -34,8 +34,8 @@ const MAC_SIZE: usize = 32; const KEY_SIZE: usize = 32; const VERSION: u8 = 1; -const HEADER: &'static str = "-----BEGIN MEGOLM SESSION DATA-----"; -const FOOTER: &'static str = "-----END MEGOLM SESSION DATA-----"; +const HEADER: &str = "-----BEGIN MEGOLM SESSION DATA-----"; +const FOOTER: &str = "-----END MEGOLM SESSION DATA-----"; fn decode(input: impl AsRef<[u8]>) -> Result, DecodeError> { decode_config(input, STANDARD_NO_PAD) @@ -113,7 +113,7 @@ pub fn decrypt_key_export( /// let encrypted_export = encrypt_key_export(&exported_keys, "1234", 1); /// # }); /// ``` -pub fn encrypt_key_export(keys: &Vec, passphrase: &str, rounds: u32) -> String { +pub fn encrypt_key_export(keys: &[ExportedRoomKey], passphrase: &str, rounds: u32) -> String { let mut plaintext = serde_json::to_string(keys).unwrap().into_bytes(); let ciphertext = encrypt_helper(&mut plaintext, passphrase, rounds); [HEADER.to_owned(), ciphertext, FOOTER.to_owned()].join("\n") @@ -246,7 +246,7 @@ mod test { let mut plaintext_bytes = plaintext.clone().into_bytes(); let ciphertext = encrypt_helper(&mut plaintext_bytes, "test", 1); - let decrypted = decrypt_helper(&mut ciphertext.clone(), "test").unwrap(); + let decrypted = decrypt_helper(&ciphertext, "test").unwrap(); prop_assert!(plaintext == decrypted); } diff --git a/matrix_sdk_crypto/src/machine.rs b/matrix_sdk_crypto/src/machine.rs index 8a3449b4..3fe74fd8 100644 --- a/matrix_sdk_crypto/src/machine.rs +++ b/matrix_sdk_crypto/src/machine.rs @@ -1572,7 +1572,10 @@ impl OlmMachine { ) .await? { - if session.first_known_index().await < existing_session.first_known_index().await { + let first_index = session.first_known_index().await; + let existing_index = existing_session.first_known_index().await; + + if first_index < existing_index { sessions.push(session) } } else { From 8af18a4df7ec18ad4325fe086fe949304eda8a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 10 Sep 2020 16:21:23 +0200 Subject: [PATCH 24/24] crypto: Test the EncryptionSettings conversion. --- .../src/olm/group_sessions/outbound.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/matrix_sdk_crypto/src/olm/group_sessions/outbound.rs b/matrix_sdk_crypto/src/olm/group_sessions/outbound.rs index 453be884..ac521272 100644 --- a/matrix_sdk_crypto/src/olm/group_sessions/outbound.rs +++ b/matrix_sdk_crypto/src/olm/group_sessions/outbound.rs @@ -272,3 +272,32 @@ impl std::fmt::Debug for OutboundGroupSession { .finish() } } + +#[cfg(test)] +mod test { + use std::time::Duration; + + use matrix_sdk_common::{ + events::room::encryption::EncryptionEventContent, identifiers::EventEncryptionAlgorithm, + js_int::uint, + }; + + use super::{EncryptionSettings, ROTATION_MESSAGES, ROTATION_PERIOD}; + + #[test] + fn encryption_settings_conversion() { + let mut content = EncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2); + let settings = EncryptionSettings::from(&content); + + assert_eq!(settings.rotation_period, ROTATION_PERIOD); + assert_eq!(settings.rotation_period_msgs, ROTATION_MESSAGES); + + content.rotation_period_ms = Some(uint!(3600)); + content.rotation_period_msgs = Some(uint!(500)); + + let settings = EncryptionSettings::from(&content); + + assert_eq!(settings.rotation_period, Duration::from_millis(3600)); + assert_eq!(settings.rotation_period_msgs, 500); + } +}