diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d34668b4c..33e6bb751 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,10 @@ concurrency: jobs: build_and_test: - name: cargo build + strategy: + matrix: + cargo_flags: ["--all-features", "--no-default-features"] + name: cargo ${{ matrix.cargo_flags }} runs-on: ubuntu-latest env: RUSTFLAGS: -D warnings @@ -36,13 +39,13 @@ jobs: uses: actions-rs/cargo@v1 with: command: build - args: --all-targets + args: --all-targets ${{ matrix.cargo_flags }} - name: Run tests uses: actions-rs/cargo@v1 with: command: test - args: --all-targets + args: --all-targets ${{ matrix.cargo_flags }} rustfmt: name: rustfmt diff --git a/Cargo.toml b/Cargo.toml index 0d1e39ae0..19c49fa58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["presage", "presage-cli", "presage-store-sled"] +members = ["presage", "presage-cli", "presage-store-sled", "presage-store-cipher"] resolver = "2" [patch.crates-io] diff --git a/presage-store-cipher/Cargo.toml b/presage-store-cipher/Cargo.toml new file mode 100644 index 000000000..1f0d75f63 --- /dev/null +++ b/presage-store-cipher/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "presage-store-cipher" +version = "0.1.0" +edition = "2021" + +[dependencies] +blake3 = "1.5.0" +chacha20poly1305 = { version = "0.10.1", features = ["std"] } +hmac = "0.12.1" +pbkdf2 = "0.12.2" +rand = "0.8" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" +thiserror = "1.0" +zeroize = { version = "1.6.0", features = ["derive"] } diff --git a/presage-store-cipher/src/lib.rs b/presage-store-cipher/src/lib.rs new file mode 100644 index 000000000..4d8bdfeef --- /dev/null +++ b/presage-store-cipher/src/lib.rs @@ -0,0 +1,303 @@ +// Based on `matrix-sdk-store-encryption` (License Apache-2.0) + +use blake3::{derive_key, Hash}; +use chacha20poly1305::aead::Aead; +use chacha20poly1305::{AeadCore, KeyInit, XChaCha20Poly1305, XNonce}; +use hmac::Hmac; +use pbkdf2::pbkdf2; +use rand::{thread_rng, RngCore}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; + +use zeroize::{Zeroize, Zeroizing}; + +const VERSION: u8 = 1; +const KDF_SALT_SIZE: usize = 32; +const XNONCE_SIZE: usize = 24; +const KDF_ROUNDS: u32 = 200_000; + +/// Hashes keys and encrypts/decrypts values +/// +/// Allows to encrypt/decrypt data in a key/value store. Can be exported as bytes encrypted by a +/// passphrase, and imported back from bytes. +#[derive(Zeroize)] +#[zeroize(drop)] +pub struct StoreCipher { + encryption_key: Box<[u8; 32]>, + mac_key_seed: Box<[u8; 32]>, +} + +impl StoreCipher { + pub fn new() -> Self { + let mut rng = thread_rng(); + let mut store_cipher = Self::zero(); + rng.fill_bytes(store_cipher.encryption_key.as_mut_slice()); + rng.fill_bytes(store_cipher.mac_key_seed.as_mut_slice()); + store_cipher + } + + pub fn export(&self, passphrase: &str) -> Result, StoreCipherError> { + self.export_inner(passphrase, KDF_ROUNDS) + } + + pub fn insecure_export_fast_for_testing( + &self, + passphrase: &str, + ) -> Result, StoreCipherError> { + self.export_inner(passphrase, 1000) + } + + pub(crate) fn export_inner( + &self, + passphrase: &str, + rounds: u32, + ) -> Result, StoreCipherError> { + let mut rng = thread_rng(); + let mut salt = [0u8; KDF_SALT_SIZE]; + rng.fill_bytes(&mut salt); + + let key = StoreCipher::expand_key(passphrase, &salt, rounds); + let key = chacha20poly1305::Key::from(key); + let cipher = XChaCha20Poly1305::new(&key); + + let nonce = XChaCha20Poly1305::generate_nonce(rng); + + let mut keys = Zeroizing::new([0u8; 64]); + keys[0..32].copy_from_slice(&*self.encryption_key); + keys[32..64].copy_from_slice(&*self.mac_key_seed); + + let ciphertext = cipher.encrypt(&nonce, keys.as_slice())?; + + let store_cipher = EncryptedStoreCipher { + kdf_info: KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds, salt }, + ciphertext_info: CipherTextInfo::ChaCha20Poly1305 { + nonce: nonce.as_slice().try_into().expect("invalid array len"), + ciphertext, + }, + }; + Ok(serde_json::to_vec(&store_cipher)?) + } + + pub fn import(passphrase: &str, encrypted: &[u8]) -> Result { + let encrypted: EncryptedStoreCipher = serde_json::from_slice(encrypted)?; + let key = match encrypted.kdf_info { + KdfInfo::Pbkdf2ToChaCha20Poly1305 { + rounds, + salt: kdf_salt, + } => Self::expand_key(passphrase, &kdf_salt, rounds), + }; + + let key = chacha20poly1305::Key::from(key); + + let decrypted = match encrypted.ciphertext_info { + CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext } => { + let cipher = XChaCha20Poly1305::new(&key); + let nonce = XNonce::from_slice(&nonce); + Zeroizing::new(cipher.decrypt(nonce, &*ciphertext)?) + } + }; + + if decrypted.len() != 64 { + return Err(StoreCipherError::Length(64, decrypted.len())); + } + + let mut store_cipher = Self::zero(); + store_cipher + .encryption_key + .copy_from_slice(&decrypted[0..32]); + store_cipher + .mac_key_seed + .copy_from_slice(&decrypted[32..64]); + Ok(store_cipher) + } + + fn expand_key(passphrase: &str, salt: &[u8], rounds: u32) -> [u8; 32] { + let mut key = [0u8; 32]; + pbkdf2::>(passphrase.as_bytes(), salt, rounds, &mut key) + .expect("invalid length"); + key + } + + pub fn encrypt_value(&self, value: &impl Serialize) -> Result, StoreCipherError> { + Ok(serde_json::to_vec(&self.encrypt_value_typed(value)?)?) + } + + fn encrypt_value_typed( + &self, + value: &impl Serialize, + ) -> Result { + let data = serde_json::to_vec(value)?; + self.encrypt_value_data(data) + } + + fn encrypt_value_data(&self, mut data: Vec) -> Result { + let nonce = XChaCha20Poly1305::generate_nonce(thread_rng()); + let cipher = XChaCha20Poly1305::new(self.encryption_key()); + + let ciphertext = cipher.encrypt(&nonce, &*data)?; + + data.zeroize(); + Ok(EncryptedValue { + version: VERSION, + ciphertext, + nonce: nonce.as_slice().try_into().expect("invalid array len"), + }) + } + + pub fn decrypt_value(&self, value: &[u8]) -> Result { + let value: EncryptedValue = serde_json::from_slice(value)?; + self.decrypt_value_typed(value) + } + + fn decrypt_value_typed( + &self, + value: EncryptedValue, + ) -> Result { + let mut plaintext = self.decrypt_value_data(value)?; + let ret = serde_json::from_slice(&plaintext); + plaintext.zeroize(); + Ok(ret?) + } + + fn decrypt_value_data(&self, value: EncryptedValue) -> Result, StoreCipherError> { + if value.version != VERSION { + return Err(StoreCipherError::Version(VERSION, value.version)); + } + + let cipher = XChaCha20Poly1305::new(self.encryption_key()); + let nonce = XNonce::from_slice(&value.nonce); + Ok(cipher.decrypt(nonce, &*value.ciphertext)?) + } + + pub fn hash_key(&self, table_name: &str, key: &[u8]) -> [u8; 32] { + let mac_key = self.get_mac_key_for_table(table_name); + mac_key.mac(key).into() + } + + fn get_mac_key_for_table(&self, table_name: &str) -> MacKey { + let mut key = MacKey(Box::new([0u8; 32])); + let output = Zeroizing::new(derive_key(table_name, &*self.mac_key_seed)); + key.0.copy_from_slice(&*output); + key + } + + fn encryption_key(&self) -> &chacha20poly1305::Key { + chacha20poly1305::Key::from_slice(&*self.encryption_key) + } + + fn zero() -> StoreCipher { + Self { + encryption_key: Box::new([0; 32]), + mac_key_seed: Box::new([0; 32]), + } + } +} + +#[derive(Zeroize)] +#[zeroize(drop)] +struct MacKey(Box<[u8; 32]>); + +impl MacKey { + fn mac(&self, input: &[u8]) -> Hash { + blake3::keyed_hash(&self.0, input) + } +} + +impl Default for StoreCipher { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +struct EncryptedValue { + version: u8, + ciphertext: Vec, + nonce: [u8; XNONCE_SIZE], +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +enum KdfInfo { + Pbkdf2ToChaCha20Poly1305 { + rounds: u32, + salt: [u8; KDF_SALT_SIZE], + }, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +enum CipherTextInfo { + ChaCha20Poly1305 { + nonce: [u8; XNONCE_SIZE], + ciphertext: Vec, + }, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +struct EncryptedStoreCipher { + pub kdf_info: KdfInfo, + pub ciphertext_info: CipherTextInfo, +} + +#[derive(Debug, thiserror::Error)] +pub enum StoreCipherError { + #[error(transparent)] + Serde(#[from] serde_json::Error), + #[error("unsupported data version, expected {0}, got {1}")] + Version(u8, u8), + #[error(transparent)] + Encryption(#[from] chacha20poly1305::aead::Error), + #[error("invalid ciphertext length, expected {0}, got {1}")] + Length(usize, usize), +} + +#[cfg(test)] +mod tests { + use serde_json::{json, Value}; + + use super::*; + + #[test] + fn test_export_import() -> Result<(), StoreCipherError> { + let passphrase = "The first rule of Fight Club is: you do not talk about Fight Club."; + let store_cipher = StoreCipher::new(); + + let value = json!({"name": "Tyler Durden"}); + let encrypted_value = store_cipher.encrypt_value(&value)?; + + let encrypted = store_cipher.insecure_export_fast_for_testing(passphrase)?; + let decrypted = StoreCipher::import(passphrase, &encrypted)?; + + assert_eq!(store_cipher.encryption_key, decrypted.encryption_key); + + let decrypted_value: Value = decrypted.decrypt_value(&encrypted_value)?; + assert_eq!(value, decrypted_value); + + Ok(()) + } + + #[test] + fn test_encrypt_decrypt() -> Result<(), StoreCipherError> { + let store_cipher = StoreCipher::new(); + + let value = json!({"name": "Tyler Durden"}); + let encrypted_value = store_cipher.encrypt_value(&value)?; + let decrypted_value: Value = store_cipher.decrypt_value(&encrypted_value)?; + assert_eq!(value, decrypted_value); + + Ok(()) + } + + #[test] + fn test_hash_key() { + let store_cipher = StoreCipher::new(); + let k1 = store_cipher.hash_key("movie", b"Fight Club"); + let k2 = store_cipher.hash_key("movie", b"Fight Club"); + assert_eq!(k1, k2); + let k3 = store_cipher.hash_key("movie", b"Fifth Element"); + assert_ne!(k1, k3); + let k4 = store_cipher.hash_key("film", b"Fight Club"); + assert_ne!(k1, k4); + assert_ne!(k3, k4); + } +} diff --git a/presage-store-sled/Cargo.toml b/presage-store-sled/Cargo.toml index 7bfbde1dc..c58470091 100644 --- a/presage-store-sled/Cargo.toml +++ b/presage-store-sled/Cargo.toml @@ -9,6 +9,7 @@ prost-build = "0.10" [dependencies] presage = { path = "../presage" } +presage-store-cipher = { path = "../presage-store-cipher", optional = true } async-trait = "0.1" base64 = "0.12" @@ -17,10 +18,10 @@ log = "0.4.8" sled = { version = "0.34" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -matrix-sdk-store-encryption = { version = "0.2.0", optional = true } thiserror = "1.0" prost = "0.10" sha2 = "0.10" +quickcheck_macros = "1.0.0" [dev-dependencies] anyhow = "1.0" @@ -32,4 +33,4 @@ tokio = { version = "1.0", default-features = false, features = ["time"] } [features] default = ["encryption"] -encryption = ["dep:matrix-sdk-store-encryption"] +encryption = ["dep:presage-store-cipher"] diff --git a/presage-store-sled/src/error.rs b/presage-store-sled/src/error.rs index 4f03a39cf..e342f02d1 100644 --- a/presage-store-sled/src/error.rs +++ b/presage-store-sled/src/error.rs @@ -8,8 +8,9 @@ pub enum SledStoreError { Db(#[from] sled::Error), #[error("data store error: {0}")] DbTransaction(#[from] sled::transaction::TransactionError), + #[cfg(feature = "encryption")] #[error("store cipher error: {0}")] - StoreCipher(#[from] matrix_sdk_store_encryption::Error), + StoreCipher(#[from] presage_store_cipher::StoreCipherError), #[error("JSON error: {0}")] Json(#[from] serde_json::Error), #[error("Prost error: {0}")] diff --git a/presage-store-sled/src/lib.rs b/presage-store-sled/src/lib.rs index 4f2b47f51..897297a39 100644 --- a/presage-store-sled/src/lib.rs +++ b/presage-store-sled/src/lib.rs @@ -7,8 +7,6 @@ use std::{ use async_trait::async_trait; use log::{debug, error, trace, warn}; -#[cfg(feature = "encryption")] -use matrix_sdk_store_encryption::StoreCipher; use presage::libsignal_service::{ self, groups_v2::Group, @@ -55,12 +53,14 @@ const SLED_KEY_NEXT_PQ_PRE_KEY_ID: &str = "next_pq_pre_key_id"; const SLED_KEY_PRE_KEYS_OFFSET_ID: &str = "pre_keys_offset_id"; const SLED_KEY_REGISTRATION: &str = "registration"; const SLED_KEY_SCHEMA_VERSION: &str = "schema_version"; +#[cfg(feature = "encryption")] const SLED_KEY_STORE_CIPHER: &str = "store_cipher"; #[derive(Clone)] pub struct SledStore { db: Arc>, - cipher: Option>, + #[cfg(feature = "encryption")] + cipher: Option>, } /// Sometimes Migrations can't proceed without having to drop existing @@ -109,6 +109,7 @@ impl SchemaVersion { } impl SledStore { + #[allow(unused_variables)] fn new( db_path: impl AsRef, passphrase: Option>, @@ -120,6 +121,11 @@ impl SledStore { .map(|p| Self::get_or_create_store_cipher(&database, p.as_ref())) .transpose()?; + #[cfg(not(feature = "encryption"))] + if passphrase.is_some() { + panic!("A passphrase was supplied but the encryption feature flag is not enabled") + } + Ok(SledStore { db: Arc::new(RwLock::new(database)), #[cfg(feature = "encryption")] @@ -145,18 +151,19 @@ impl SledStore { Self::new(db_path, passphrase) } + #[cfg(feature = "encryption")] fn get_or_create_store_cipher( database: &sled::Db, passphrase: &str, - ) -> Result { + ) -> Result { let cipher = if let Some(key) = database.get(SLED_KEY_STORE_CIPHER)? { - StoreCipher::import(passphrase, &key)? + presage_store_cipher::StoreCipher::import(passphrase, &key)? } else { - let cipher = StoreCipher::new()?; + let cipher = presage_store_cipher::StoreCipher::new(); #[cfg(not(test))] let export = cipher.export(passphrase); #[cfg(test)] - let export = cipher._insecure_export_fast_for_testing(passphrase); + let export = cipher.insecure_export_fast_for_testing(passphrase); database.insert(SLED_KEY_STORE_CIPHER, export?)?; cipher }; @@ -169,7 +176,9 @@ impl SledStore { let db = sled::Config::new().temporary(true).open()?; Ok(Self { db: Arc::new(RwLock::new(db)), - cipher: None, + #[cfg(feature = "encryption")] + // use store cipher with a random key + cipher: Some(Arc::new(presage_store_cipher::StoreCipher::new())), }) } @@ -188,6 +197,34 @@ impl SledStore { .unwrap_or_default() } + #[cfg(feature = "encryption")] + fn decrypt_value(&self, value: &[u8]) -> Result { + if let Some(cipher) = self.cipher.as_ref() { + Ok(cipher.decrypt_value(value)?) + } else { + Ok(serde_json::from_slice(value)?) + } + } + + #[cfg(not(feature = "encryption"))] + fn decrypt_value(&self, value: &[u8]) -> Result { + Ok(serde_json::from_slice(value)?) + } + + #[cfg(feature = "encryption")] + fn encrypt_value(&self, value: &impl Serialize) -> Result, SledStoreError> { + if let Some(cipher) = self.cipher.as_ref() { + Ok(cipher.encrypt_value(value)?) + } else { + Ok(serde_json::to_vec(value)?) + } + } + + #[cfg(not(feature = "encryption"))] + fn encrypt_value(&self, value: &impl Serialize) -> Result, SledStoreError> { + Ok(serde_json::to_vec(value)?) + } + pub fn get(&self, tree: &str, key: K) -> Result, SledStoreError> where K: AsRef<[u8]>, @@ -196,12 +233,7 @@ impl SledStore { self.read() .open_tree(tree)? .get(key)? - .map(|p| { - self.cipher.as_ref().map_or_else( - || serde_json::from_slice(&p).map_err(SledStoreError::from), - |c| c.decrypt_value(&p).map_err(SledStoreError::from), - ) - }) + .map(|p| self.decrypt_value(&p)) .transpose() .map_err(SledStoreError::from) } @@ -211,10 +243,7 @@ impl SledStore { K: AsRef<[u8]>, V: Serialize, { - let value = self.cipher.as_ref().map_or_else( - || serde_json::to_vec(&value).map_err(SledStoreError::from), - |c| c.encrypt_value(&value).map_err(SledStoreError::from), - )?; + let value = self.encrypt_value(&value)?; let db = self.write(); let replaced = db.open_tree(tree)?.insert(key, value)?; db.flush()?; @@ -234,7 +263,9 @@ impl SledStore { /// build a hashed messages thread key fn messages_thread_tree_name(&self, t: &Thread) -> String { let key = match t { - Thread::Contact(uuid) => format!("{SLED_TREE_THREADS_PREFIX}:contact:{uuid}"), + Thread::Contact(uuid) => { + format!("{SLED_TREE_THREADS_PREFIX}:contact:{uuid}") + } Thread::Group(group_id) => format!( "{SLED_TREE_THREADS_PREFIX}:group:{}", base64::encode(group_id) @@ -445,6 +476,7 @@ impl Store for SledStore { fn contacts(&self) -> Result { Ok(SledContactsIter { iter: self.read().open_tree(SLED_TREE_CONTACTS)?.iter(), + #[cfg(feature = "encryption")] cipher: self.cipher.clone(), }) } @@ -465,6 +497,7 @@ impl Store for SledStore { fn groups(&self) -> Result { Ok(SledGroupsIter { iter: self.read().open_tree(SLED_TREE_GROUPS)?.iter(), + #[cfg(feature = "encryption")] cipher: self.cipher.clone(), }) } @@ -574,10 +607,13 @@ impl Store for SledStore { (Bound::Unbounded, Bound::Included(end)) => tree_thread.range(..=end.to_be_bytes()), (Bound::Unbounded, Bound::Excluded(end)) => tree_thread.range(..end.to_be_bytes()), (Bound::Unbounded, Bound::Unbounded) => tree_thread.range::<[u8; 8], RangeFull>(..), - (Bound::Excluded(_), _) => unreachable!("range that excludes the initial value"), + (Bound::Excluded(_), _) => { + unreachable!("range that excludes the initial value") + } }; Ok(SledMessagesIter { + #[cfg(feature = "encryption")] cipher: self.cipher.clone(), iter, }) @@ -609,10 +645,27 @@ impl Store for SledStore { } pub struct SledContactsIter { - cipher: Option>, + #[cfg(feature = "encryption")] + cipher: Option>, iter: sled::Iter, } +impl SledContactsIter { + #[cfg(feature = "encryption")] + fn decrypt_value(&self, value: &[u8]) -> Result { + if let Some(cipher) = self.cipher.as_ref() { + Ok(cipher.decrypt_value(value)?) + } else { + Ok(serde_json::from_slice(value)?) + } + } + + #[cfg(not(feature = "encryption"))] + fn decrypt_value(&self, value: &[u8]) -> Result { + Ok(serde_json::from_slice(value)?) + } +} + impl Iterator for SledContactsIter { type Item = Result; @@ -620,31 +673,40 @@ impl Iterator for SledContactsIter { self.iter .next()? .map_err(SledStoreError::from) - .and_then(|(_key, value)| { - self.cipher.as_ref().map_or_else( - || serde_json::from_slice(&value).map_err(SledStoreError::from), - |c| c.decrypt_value(&value).map_err(SledStoreError::from), - ) - }) + .and_then(|(_key, value)| self.decrypt_value(&value)) .into() } } pub struct SledGroupsIter { - cipher: Option>, + #[cfg(feature = "encryption")] + cipher: Option>, iter: sled::Iter, } +impl SledGroupsIter { + #[cfg(feature = "encryption")] + fn decrypt_value(&self, value: &[u8]) -> Result { + if let Some(cipher) = self.cipher.as_ref() { + Ok(cipher.decrypt_value(value)?) + } else { + Ok(serde_json::from_slice(value)?) + } + } + + #[cfg(not(feature = "encryption"))] + fn decrypt_value(&self, value: &[u8]) -> Result { + Ok(serde_json::from_slice(value)?) + } +} + impl Iterator for SledGroupsIter { type Item = Result<(GroupMasterKeyBytes, Group), SledStoreError>; fn next(&mut self) -> Option { Some(self.iter.next()?.map_err(SledStoreError::from).and_then( |(group_master_key_bytes, value)| { - let group = self.cipher.as_ref().map_or_else( - || serde_json::from_slice(&value).map_err(SledStoreError::from), - |c| c.decrypt_value(&value).map_err(SledStoreError::from), - )?; + let group = self.decrypt_value(&value)?; Ok(( group_master_key_bytes .as_ref() @@ -981,22 +1043,34 @@ impl SenderKeyStore for SledStore { } pub struct SledMessagesIter { - cipher: Option>, + #[cfg(feature = "encryption")] + cipher: Option>, iter: sled::Iter, } +impl SledMessagesIter { + #[cfg(feature = "encryption")] + fn decrypt_value(&self, value: &[u8]) -> Result { + if let Some(cipher) = self.cipher.as_ref() { + Ok(cipher.decrypt_value(value)?) + } else { + Ok(serde_json::from_slice(value)?) + } + } + + #[cfg(not(feature = "encryption"))] + fn decrypt_value(&self, value: &[u8]) -> Result { + Ok(serde_json::from_slice(value)?) + } +} + impl SledMessagesIter { fn decode( &self, elem: Result<(IVec, IVec), sled::Error>, ) -> Option> { elem.map_err(SledStoreError::from) - .and_then(|(_, value)| { - self.cipher.as_ref().map_or_else( - || serde_json::from_slice(&value).map_err(SledStoreError::from), - |c| c.decrypt_value(&value).map_err(SledStoreError::from), - ) - }) + .and_then(|(_, value)| self.decrypt_value(&value).map_err(SledStoreError::from)) .and_then(|data: Vec| ContentProto::decode(&data[..]).map_err(SledStoreError::from)) .map_or_else(|e| Some(Err(e)), |p| Some(p.try_into())) }