// 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::convert::TryFrom; use chacha20poly1305::{ aead::{Aead, Error as EncryptionError, NewAead}, ChaCha20Poly1305, Key, Nonce, XChaCha20Poly1305, XNonce, }; use hmac::Hmac; use pbkdf2::pbkdf2; use rand::{thread_rng, Error as RngError, Fill}; use serde::{Deserialize, Serialize}; use sha2::Sha256; use zeroize::{Zeroize, Zeroizing}; use crate::StoreError; const VERSION: u8 = 1; const KEY_SIZE: usize = 32; const NONCE_SIZE: usize = 12; const XNONCE_SIZE: usize = 24; const KDF_SALT_SIZE: usize = 32; #[cfg(not(test))] const KDF_ROUNDS: u32 = 200_000; #[cfg(test)] const KDF_ROUNDS: u32 = 1000; #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] Serialization(#[from] serde_json::Error), #[error("Error encrypting or decrypting an event {0}")] Encryption(String), #[error("Error generating enough random data for a cryptographic operation")] Random(#[from] RngError), } #[allow(clippy::from_over_into)] impl Into for Error { fn into(self) -> StoreError { match self { Error::Serialization(e) => StoreError::Json(e), Error::Encryption(e) => StoreError::Encryption(e), Error::Random(_) => StoreError::Encryption(self.to_string()), } } } impl From for Error { fn from(e: EncryptionError) -> Self { Error::Encryption(e.to_string()) } } #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct EncryptedEvent { version: u8, ciphertext: Vec, nonce: Vec, } /// Version specific info for the key derivation method that is used. #[derive(Debug, Serialize, Deserialize, PartialEq)] pub enum KdfInfo { Pbkdf2ToChaCha20Poly1305 { /// The number of PBKDF rounds that were used when deriving the store /// key. rounds: u32, /// The salt that was used when the passphrase was expanded into a store /// key. kdf_salt: Vec, }, } /// Version specific info for encryption method that is used to encrypt our /// store key. #[derive(Debug, Serialize, Deserialize, PartialEq)] pub enum CipherTextInfo { ChaCha20Poly1305 { /// The nonce that was used to encrypt the ciphertext. nonce: Vec, /// The encrypted store key. ciphertext: Vec, }, } /// An encrypted version of our store key, this can be safely stored in a /// database. #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct EncryptedStoreKey { /// Info about the key derivation method that was used to expand the /// passphrase into an encryption key. pub kdf_info: KdfInfo, /// The ciphertext with it's accompanying additional data that is needed to /// decrypt the store key. pub ciphertext_info: CipherTextInfo, } /// A store key that can be used to encrypt entries in the store. #[derive(Debug, Zeroize, PartialEq)] pub struct StoreKey { inner: Vec, } impl TryFrom> for StoreKey { type Error = (); fn try_from(value: Vec) -> Result { if value.len() != KEY_SIZE { Err(()) } else { Ok(Self { inner: value }) } } } impl StoreKey { /// Generate a new random store key. pub fn new() -> Result { let mut key = vec![0u8; KEY_SIZE]; let mut rng = thread_rng(); key.try_fill(&mut rng)?; Ok(Self { inner: key }) } /// Expand the given passphrase into a KEY_SIZE long key. fn expand_key(passphrase: &str, salt: &[u8], rounds: u32) -> Zeroizing> { let mut key = Zeroizing::from(vec![0u8; KEY_SIZE]); pbkdf2::>(passphrase.as_bytes(), &salt, rounds, &mut *key); key } /// Get the store key. fn key(&self) -> &Key { Key::from_slice(&self.inner) } /// Encrypt and export our store key using the given passphrase. /// /// # Arguments /// /// * `passphrase` - The passphrase that should be used to encrypt the /// store key. pub fn export(&self, passphrase: &str) -> Result { let mut rng = thread_rng(); let mut salt = vec![0u8; KDF_SALT_SIZE]; salt.try_fill(&mut rng)?; let key = StoreKey::expand_key(passphrase, &salt, KDF_ROUNDS); let key = Key::from_slice(key.as_ref()); let cipher = ChaCha20Poly1305::new(&key); let mut nonce = vec![0u8; NONCE_SIZE]; nonce.try_fill(&mut rng)?; let ciphertext = cipher.encrypt(Nonce::from_slice(nonce.as_ref()), self.inner.as_slice())?; Ok(EncryptedStoreKey { kdf_info: KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds: KDF_ROUNDS, kdf_salt: salt }, ciphertext_info: CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext }, }) } fn get_nonce() -> Result, RngError> { let mut nonce = vec![0u8; XNONCE_SIZE]; let mut rng = thread_rng(); nonce.try_fill(&mut rng)?; Ok(nonce) } pub fn encrypt(&self, event: &impl Serialize) -> Result { let event = serde_json::to_vec(event)?; let nonce = StoreKey::get_nonce()?; let cipher = XChaCha20Poly1305::new(self.key()); let xnonce = XNonce::from_slice(&nonce); let ciphertext = cipher.encrypt(xnonce, event.as_ref())?; Ok(EncryptedEvent { version: VERSION, ciphertext, nonce }) } pub fn decrypt Deserialize<'b>>(&self, event: EncryptedEvent) -> Result { if event.version != VERSION { return Err(Error::Encryption( "Error decrypting: Unknown ciphertext version".to_string(), )); } let cipher = XChaCha20Poly1305::new(self.key()); let nonce = XNonce::from_slice(&event.nonce); let plaintext = cipher.decrypt(nonce, event.ciphertext.as_ref())?; Ok(serde_json::from_slice(&plaintext)?) } /// Restore a store key from an encrypted export. /// /// # Arguments /// /// * `passphrase` - The passphrase that should be used to encrypt the /// store key. /// /// * `encrypted` - The exported and encrypted version of the store key. pub fn import(passphrase: &str, encrypted: EncryptedStoreKey) -> Result { let key = match encrypted.kdf_info { KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds, kdf_salt } => { Self::expand_key(passphrase, &kdf_salt, rounds) } }; let key = Key::from_slice(key.as_ref()); let decrypted = match encrypted.ciphertext_info { CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext } => { let cipher = ChaCha20Poly1305::new(&key); let nonce = Nonce::from_slice(&nonce); cipher.decrypt(nonce, ciphertext.as_ref())? } }; Ok(Self { inner: decrypted }) } } #[cfg(test)] mod test { use serde_json::{json, Value}; use super::StoreKey; #[test] fn generating() { StoreKey::new().unwrap(); } #[test] fn encrypting() { let passphrase = "it's a secret to everybody"; let store_key = StoreKey::new().unwrap(); let encrypted = store_key.export(passphrase).unwrap(); let decrypted = StoreKey::import(passphrase, encrypted).unwrap(); assert_eq!(store_key, decrypted); } #[test] fn encrypting_events() { let event = json!({ "content": { "body": "Bee Gees - Stayin' Alive", "info": { "duration": 2140786, "mimetype": "audio/mpeg", "size": 1563685 }, "msgtype": "m.audio", "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd" }, }); let store_key = StoreKey::new().unwrap(); let encrypted = store_key.encrypt(&event).unwrap(); let decrypted: Value = store_key.decrypt(encrypted).unwrap(); assert_eq!(event, decrypted); } }