// 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 aes::{ cipher::{generic_array::GenericArray, FromBlockCipher, NewBlockCipher, StreamCipher}, Aes256, Aes256Ctr, }; use byteorder::{BigEndian, ReadBytesExt}; use getrandom::getrandom; use hmac::{Hmac, Mac, NewMac}; use pbkdf2::pbkdf2; use serde_json::Error as SerdeError; use sha2::{Sha256, Sha512}; use thiserror::Error; use crate::{ olm::ExportedRoomKey, utilities::{decode, encode, DecodeError}, }; const SALT_SIZE: usize = 16; const IV_SIZE: usize = 16; const MAC_SIZE: usize = 32; const KEY_SIZE: usize = 32; const VERSION: u8 = 1; const HEADER: &str = "-----BEGIN MEGOLM SESSION DATA-----"; const FOOTER: &str = "-----END MEGOLM SESSION DATA-----"; /// Error representing a failure during key export or import. #[derive(Error, Debug)] pub enum KeyExportError { /// The key export doesn't contain valid headers. #[error("Invalid or missing key export headers.")] InvalidHeaders, /// The key export has been encrypted with an unsupported version. #[error("The key export has been encrypted with an unsupported version.")] UnsupportedVersion, /// The MAC of the encrypted payload is invalid. #[error("The MAC of the encrypted payload is invalid.")] InvalidMac, /// The decrypted key export isn't valid UTF-8. #[error(transparent)] InvalidUtf8(#[from] std::string::FromUtf8Error), /// The decrypted key export doesn't contain valid JSON. #[error(transparent)] Json(#[from] SerdeError), /// The key export string isn't valid base64. #[error(transparent)] Decode(#[from] DecodeError), /// The key export doesn't all the required fields. #[error(transparent)] Io(#[from] std::io::Error), } /// 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 ruma::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, KeyExportError> { let mut x: String = String::new(); input.read_to_string(&mut x)?; if !(x.trim_start().starts_with(HEADER) && x.trim_end().ends_with(FOOTER)) { return Err(KeyExportError::InvalidHeaders); } 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)?)?) } /// 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 brute-force /// attacks. Should be at least `10000`, while values in the `100000` ranges /// should be preferred. /// /// # Panics /// /// This method will panic if it can't get enough randomness from the OS to /// encrypt the exported keys securely. /// /// # Examples /// ```no_run /// # use matrix_sdk_crypto::{OlmMachine, encrypt_key_export}; /// # use ruma::{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: &[ExportedRoomKey], passphrase: &str, rounds: u32, ) -> Result { let mut plaintext = serde_json::to_string(keys)?.into_bytes(); let ciphertext = encrypt_helper(&mut plaintext, passphrase, rounds); Ok([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]; 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 key = GenericArray::from_slice(key); let iv = iv.to_be_bytes(); let iv = GenericArray::from_slice(&iv); let aes = Aes256::new(key); let mut aes = Aes256Ctr::from_block_cipher(aes, iv); aes.apply_keystream(&mut plaintext); let mut payload: Vec = vec![]; payload.extend(&VERSION.to_be_bytes()); payload.extend(&salt); payload.extend(&*iv); payload.extend(&rounds.to_be_bytes()); payload.extend_from_slice(plaintext); let mut hmac = Hmac::::new_from_slice(hmac_key).expect("Can't create HMAC object"); hmac.update(&payload); let mac = hmac.finalize(); payload.extend(mac.into_bytes()); encode(payload) } fn decrypt_helper(ciphertext: &str, passphrase: &str) -> 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()?; decoded.read_exact(&mut salt)?; decoded.read_exact(&mut iv)?; let rounds = decoded.read_u32::()?; let ciphertext_start = decoded.position() as usize; decoded.seek(SeekFrom::End(-32))?; let ciphertext_end = decoded.position() as usize; decoded.read_exact(&mut mac)?; let mut decoded = decoded.into_inner(); if version != VERSION { return Err(KeyExportError::UnsupportedVersion); } pbkdf2::>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys); let (key, hmac_key) = derived_keys.split_at(KEY_SIZE); let mut hmac = Hmac::::new_from_slice(hmac_key).expect("Can't create an HMAC object"); hmac.update(&decoded[0..ciphertext_end]); hmac.verify(&mac).map_err(|_| KeyExportError::InvalidMac)?; let key = GenericArray::from_slice(key); let iv = GenericArray::from_slice(&iv); let mut ciphertext = &mut decoded[ciphertext_start..ciphertext_end]; let aes = Aes256::new(key); let mut aes = Aes256Ctr::from_block_cipher(aes, iv); aes.apply_keystream(&mut ciphertext); Ok(String::from_utf8(ciphertext.to_owned())?) } #[cfg(test)] mod test { use std::io::Cursor; use indoc::indoc; use matrix_sdk_test::async_test; use proptest::prelude::*; use ruma::room_id; use super::{decode, decrypt_helper, decrypt_key_export, encrypt_helper, encrypt_key_export}; use crate::machine::test::get_prepared_machine; const PASSPHRASE: &str = "1234"; const TEST_EXPORT: &str = indoc! {" -----BEGIN MEGOLM SESSION DATA----- Af7mGhlzQ+eGvHu93u0YXd3D/+vYMs3E7gQqOhuCtkvGAAAAASH7pEdWvFyAP1JUisAcpEo Xke2Q7Kr9hVl/SCc6jXBNeJCZcrUbUV4D/tRQIl3E9L4fOk928YI1J+3z96qiH0uE7hpsCI CkHKwjPU+0XTzFdIk1X8H7sZ+MD/2Sg/q3y8rtUjz7uEj4GUTnb+9SCOTVmJsRfqgUpM1CU bDLytHf1JkohY4tWEgpsCc67xdzgodjr12qYrfg/zNm3LGpxlrffJknw4rk5QFTj4kMbqbD ZZgDTni+HxRTDGge2J620lMOiznvXX+H09Rwruqx5aJvvaaKd86jWRpiO2oSFqHn4u5ONl9 41uzm62Sj0eIm6ZbA9NQs87jQw4LxsejhZVL+NdjIg80zVSBTWhTdo0DTnbFSNP4ReOiz0U XosOF8A5T8Vdx2nvA0GXltfcHKVKQYh/LJAkNQ7P9UYL4ae/5TtQZkhB1KxCLTRWqADCl53 uBMGpG53EMgY6G6K2DEIOkcv7sdXQF5WpemiSWZqJRWj+cjfs9BpCTbkp/rszWFl2TniWpR RqIbT2jORlN4rTvdtF0F4z1pqP4qWyR3sLNTkXm9CFRzWADNG0RDZKxbCoo6RPvtaCTfaHo SwfvzBS6CjfAG+FOugpV48o7+XetaUUPZ6/tZSPhCdeV8eP9q5r0QwWeXFogzoNzWt4HYx9 MdXxzD+f0mtg5gzehrrEEARwI2bCvPpHxlt/Na9oW/GBpkjwR1LSKgg4CtpRyWngPjdEKpZ GYW19pdjg0qdXNk/eqZsQTsNWVo6A -----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()); } proptest! { #[test] fn proptest_encrypt_cycle(plaintext in prop::string::string_regex(".*").unwrap()) { let mut plaintext_bytes = plaintext.clone().into_bytes(); let ciphertext = encrypt_helper(&mut plaintext_bytes, "test", 1); let decrypted = decrypt_helper(&ciphertext, "test").unwrap(); prop_assert!(plaintext == decrypted); } } #[test] fn test_encrypt_decrypt() { let data = "It's a secret to everybody"; let mut bytes = data.to_owned().into_bytes(); let encrypted = encrypt_helper(&mut bytes, PASSPHRASE, 10); let decrypted = decrypt_helper(&encrypted, PASSPHRASE).unwrap(); assert_eq!(data, decrypted); } #[async_test] async fn test_session_encrypt() { let (machine, _) = get_prepared_machine().await; let room_id = room_id!("!test:localhost"); machine.create_outbound_group_session_with_defaults(&room_id).await.unwrap(); let export = machine.export_keys(|s| s.room_id() == &room_id).await.unwrap(); assert!(!export.is_empty()); let encrypted = encrypt_key_export(&export, "1234", 1).unwrap(); let decrypted = decrypt_key_export(Cursor::new(encrypted), "1234").unwrap(); assert_eq!(export, decrypted); assert_eq!(machine.import_keys(decrypted, |_, _| {}).await.unwrap(), (0, 1)); } #[test] fn test_real_decrypt() { let reader = Cursor::new(TEST_EXPORT); let imported = decrypt_key_export(reader, PASSPHRASE).expect("Can't decrypt key export"); assert!(!imported.is_empty()) } }