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] 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