diff --git a/matrix_sdk_base/Cargo.toml b/matrix_sdk_base/Cargo.toml index c9642e32..20ab701d 100644 --- a/matrix_sdk_base/Cargo.toml +++ b/matrix_sdk_base/Cargo.toml @@ -27,7 +27,6 @@ docs = ["encryption", "sled_cryptostore", "messages"] dashmap= "4.0.1" serde = { version = "1.0.119", features = ["rc"] } serde_json = "1.0.61" -zeroize = "1.2.0" tracing = "0.1.22" matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" } @@ -36,6 +35,12 @@ matrix-sdk-crypto = { version = "0.2.0", path = "../matrix_sdk_crypto", optional # Misc dependencies thiserror = "1.0.23" sled = "0.34.6" +chacha20poly1305 = "0.7.1" +getrandom = "0.2.1" +zeroize = { version = "1.2.0", features = ["zeroize_derive"] } +pbkdf2 = { version = "0.6.0", default-features = false } +hmac = "0.10.1" +sha2 = "0.9.2" futures = "0.3.8" [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] diff --git a/matrix_sdk_base/src/store/sled_store/mod.rs b/matrix_sdk_base/src/store/sled_store/mod.rs index bdfd7b66..f7f2b517 100644 --- a/matrix_sdk_base/src/store/sled_store/mod.rs +++ b/matrix_sdk_base/src/store/sled_store/mod.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod store_key; + use std::{convert::TryFrom, path::Path, time::SystemTime}; use futures::stream::{self, Stream}; diff --git a/matrix_sdk_base/src/store/sled_store/store_key.rs b/matrix_sdk_base/src/store/sled_store/store_key.rs new file mode 100644 index 00000000..3a11841a --- /dev/null +++ b/matrix_sdk_base/src/store/sled_store/store_key.rs @@ -0,0 +1,278 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![allow(dead_code)] + +use std::convert::TryFrom; + +use chacha20poly1305::{ + aead::{Aead, Error as EncryptionError, NewAead}, + ChaCha20Poly1305, Key, Nonce, XChaCha20Poly1305, XNonce, +}; +use getrandom::getrandom; +use hmac::Hmac; +use pbkdf2::pbkdf2; +use sha2::Sha256; +use zeroize::{Zeroize, Zeroizing}; + +use serde::{Deserialize, Serialize}; + +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("Unknown ciphertext version")] + InvalidVersion, +} + +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 Default for StoreKey { + fn default() -> Self { + let mut key = vec![0u8; KEY_SIZE]; + getrandom(&mut key).expect("Can't generate new store key"); + + Self { inner: key } + } +} + +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() -> Self { + Default::default() + } + + 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) -> EncryptedStoreKey { + let mut salt = vec![0u8; KDF_SALT_SIZE]; + getrandom(&mut salt).expect("Can't generate new random store key"); + + 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]; + getrandom(&mut nonce).expect("Can't generate new random nonce for the store key"); + + let ciphertext = cipher + .encrypt(Nonce::from_slice(nonce.as_ref()), self.inner.as_slice()) + .expect("Can't encrypt store key"); + + EncryptedStoreKey { + kdf_info: KdfInfo::Pbkdf2ToChaCha20Poly1305 { + rounds: KDF_ROUNDS, + kdf_salt: salt, + }, + ciphertext_info: CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext }, + } + } + + fn get_nonce() -> Vec { + let mut nonce = vec![0u8; XNONCE_SIZE]; + getrandom(&mut nonce).expect("Can't generate nonce"); + 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::InvalidVersion); + } + + 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 super::StoreKey; + use serde_json::{json, Value}; + + #[test] + fn generating() { + StoreKey::new(); + } + + #[test] + fn encrypting() { + let passphrase = "it's a secret to everybody"; + let store_key = StoreKey::new(); + + let encrypted = store_key.export(passphrase); + 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(); + + let encrypted = store_key.encrypt(&event).unwrap(); + let decrypted: Value = store_key.decrypt(encrypted).unwrap(); + assert_eq!(event, decrypted); + } +}