diff --git a/crates/matrix-store-key/Cargo.toml b/crates/matrix-store-key/Cargo.toml new file mode 100644 index 000000000..335a69feb --- /dev/null +++ b/crates/matrix-store-key/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "matrix-store-key" +version = "0.1.0" +edition = "2021" + +[dependencies] +blake3 = "1.3.1" +chacha20poly1305 = { version = "0.9.0", features = ["std"] } +displaydoc = "0.2" +hmac = "0.12.1" +pbkdf2 = "0.10.1" +rand = "0.8.4" +serde = { version = "1.0.126", features = ["derive"] } +serde_json = "1.0.64" +sha2 = "0.10.2" +thiserror = "1.0.26" +zeroize = { version = "1.3.0", features = ["zeroize_derive"] } + +[dev-dependencies] +anyhow = "1.0.56" diff --git a/crates/matrix-store-key/README.md b/crates/matrix-store-key/README.md new file mode 100644 index 000000000..962c774ab --- /dev/null +++ b/crates/matrix-store-key/README.md @@ -0,0 +1,72 @@ +A general purpose encryption scheme for key/value stores. + +# Usage + +```rust +use matrix_store_key::StoreKey; +use serde_json::{json, value::Value}; + +fn main() -> anyhow::Result<()> { + let store_key = StoreKey::new()?; + + // Export the store key and persist it in your key/value store + let export = store_key.export("secret-passphrase")?; + + let value = json!({ + "some": "data", + }); + + let encrypted = store_key.encrypt_value(&value)?; + let decrypted: Value = store_key.decrypt_value(&encrypted)?; + + assert_eq!(value, decrypted); + + let key = "bulbasaur"; + + // Hash the key so people don't know which pokemon we have collected. + let hashed_key = store_key.hash_key("list-of-pokemon", key.as_ref()); + let another_table = store_key.hash_key("my-starter", key.as_ref()); + let same_key = store_key.hash_key("my-starter", key.as_ref()); + + assert_ne!(key.as_ref(), hashed_key); + assert_ne!(hashed_key, another_table); + assert_eq!(another_table, same_key); + + Ok(()) +} +``` + +# ⚠️ Security Warning: Hazmat! + +This crate implements only the low-level block cipher function, and is intended +for use for implementing higher-level constructions *only*. It is NOT +intended for direct use in applications. + +USE AT YOUR OWN RISK! + +# Encryption scheme + +A `StoreKey` consists of two randomly generated 32 byte-sized slices. + +The first 32 bytes are used to encrypt values. XChaCha20Poly1305 with a random +nonce is used to encrypt each value. The nonce is saved with the ciphertext. + +The second 32 bytes are used as a seed to derive table specific keys that will +be used to hash data. The data will then be hashed using the blake3 keyed hash +using the table specific key. + +```text + ┌───────────────────────────────────────┐ + │ StoreKey │ + │ Encryption key | Hash key seed │ + │ [u8; 32] | [u8; 32] │ + └───────────────────────────────────────┘ +``` + +The `StoreKey` has some Matrix specific assumptions built in which ensure that +the limits of the cryptographic primitives are not exceeded. If this crate is +used for non-Matrix specific data users need to ensure: + +1. That individual values are chunked, otherwise decryption might be succeptible + to a DOS attack. +2. The `StoreKey` is periodically rotated/rekeyed. diff --git a/crates/matrix-store-key/src/lib.rs b/crates/matrix-store-key/src/lib.rs new file mode 100644 index 000000000..db6c1398a --- /dev/null +++ b/crates/matrix-store-key/src/lib.rs @@ -0,0 +1,520 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// Copyright 2021 Damir Jelić +// +// 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. + +#![doc = include_str!("../README.md")] +#![warn(missing_debug_implementations, missing_docs)] + +use blake3::{derive_key, Hash}; +use chacha20poly1305::{ + aead::{Aead, Error as EncryptionError, NewAead}, + Key as ChachaKey, XChaCha20Poly1305, XNonce, +}; +use displaydoc::Display; +use hmac::Hmac; +use pbkdf2::pbkdf2; +use rand::{thread_rng, Error as RandomError, Fill}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use zeroize::Zeroize; + +const VERSION: u8 = 1; +const KDF_SALT_SIZE: usize = 32; +const XNONCE_SIZE: usize = 24; +#[cfg(not(test))] +const KDF_ROUNDS: u32 = 200_000; +#[cfg(test)] +const KDF_ROUNDS: u32 = 1000; + +type MacKeySeed = [u8; 32]; + +/// Error type for the `StoreKey` operations. +#[derive(Debug, Display, thiserror::Error)] +pub enum Error { + /// Failed to serialize or deserialize a value {0} + Serialization(#[from] serde_json::Error), + /// Error encrypting or decrypting a value {0} + Encryption(#[from] EncryptionError), + /// Coulnd't generate enough randomness for a cryptographic operation: {0} + Random(#[from] RandomError), + /// Unsupported ciphertext version, expected {0}, got {1} + Version(u8, u8), + /// The ciphertext had an invalid length, expected {0}, got {1} + Length(usize, usize), +} + +/// An encryption key that can be used to encrypt data for key/value stores. +/// +/// # Examples +/// +/// ``` +/// # let example = || { +/// use matrix_store_key::StoreKey; +/// use serde_json::{json, value::Value}; +/// +/// let store_key = StoreKey::new()?; +/// +/// // Export the store key and persist it in your key/value store +/// let export = store_key.export("secret-passphrase")?; +/// +/// let value = json!({ +/// "some": "data", +/// }); +/// +/// let encrypted = store_key.encrypt_value(&value)?; +/// let decrypted: Value = store_key.decrypt_value(&encrypted)?; +/// +/// assert_eq!(value, decrypted); +/// # Result::<_, anyhow::Error>::Ok(()) }; +/// ``` +#[allow(missing_debug_implementations)] +pub struct StoreKey { + inner: Keys, +} + +impl StoreKey { + /// Generate a new random store key. + pub fn new() -> Result { + Ok(Self { inner: Keys::new()? }) + } + + /// Encrypt the store key using the given passphrase and export it. + /// + /// This method can be used to persist the `StoreKey` in the key/value store + /// in a safe manner. + /// + /// The `StoreKey` can later on be restored using [`StoreKey::import`]. + /// + /// # Arguments + /// + /// * `passphrase` - The passphrase that should be used to encrypt the + /// store key. + /// + /// # Examples + /// + /// ``` + /// # let example = || { + /// use matrix_store_key::StoreKey; + /// use serde_json::json; + /// + /// let store_key = StoreKey::new()?; + /// + /// // Export the store key and persist it in your key/value store + /// let export = store_key.export("secret-passphrase"); + /// + /// // Save the export in your key/value store. + /// # Result::<_, anyhow::Error>::Ok(()) }; + /// ``` + pub fn export(&self, passphrase: &str) -> Result, Error> { + let mut rng = thread_rng(); + + let mut salt = [0u8; KDF_SALT_SIZE]; + salt.try_fill(&mut rng)?; + + let key = StoreKey::expand_key(passphrase, &salt, KDF_ROUNDS); + let key = ChachaKey::from_slice(key.as_ref()); + let cipher = XChaCha20Poly1305::new(key); + + let nonce = Keys::get_nonce()?; + + let mut keys = [0u8; 64]; + + keys[0..32].copy_from_slice(self.inner.encryption_key.as_ref()); + keys[32..64].copy_from_slice(self.inner.mac_key_seed.as_ref()); + + let ciphertext = cipher.encrypt(XNonce::from_slice(&nonce), keys.as_ref())?; + + keys.zeroize(); + + let store_key = EncryptedStoreKey { + kdf_info: KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds: KDF_ROUNDS, kdf_salt: salt }, + ciphertext_info: CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext }, + }; + + Ok(serde_json::to_vec(&store_key).expect("Can't serialize the store key")) + } + + /// Restore a store key from an encrypted export. + /// + /// # Arguments + /// + /// * `passphrase` - The passphrase that was used to encrypt the store key. + /// + /// * `encrypted` - The exported and encrypted version of the store key. + /// + /// # Examples + /// + /// ``` + /// # let example = || { + /// use matrix_store_key::StoreKey; + /// use serde_json::json; + /// + /// let store_key = StoreKey::new()?; + /// + /// // Export the store key and persist it in your key/value store + /// let export = store_key.export("secret-passphrase")?; + /// + /// // This is now the same as `store_key`. + /// let imported = StoreKey::import("secret-passphrase", &export)?; + /// + /// // Save the export in your key/value store. + /// # Result::<_, anyhow::Error>::Ok(()) }; + /// ``` + pub fn import(passphrase: &str, encrypted: &[u8]) -> Result { + let encrypted: EncryptedStoreKey = serde_json::from_slice(encrypted)?; + + let key = match encrypted.kdf_info { + KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds, kdf_salt } => { + Self::expand_key(passphrase, &kdf_salt, rounds) + } + }; + + let key = ChachaKey::from_slice(key.as_ref()); + + let mut decrypted = match encrypted.ciphertext_info { + CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext } => { + let cipher = XChaCha20Poly1305::new(key); + let nonce = XNonce::from_slice(&nonce); + cipher.decrypt(nonce, ciphertext.as_ref())? + } + }; + + if decrypted.len() != 64 { + Err(Error::Length(64, decrypted.len())) + } else { + let mut encryption_key = Box::new([0u8; 32]); + let mut mac_key_seed = Box::new([0u8; 32]); + + encryption_key.copy_from_slice(&decrypted[0..32]); + mac_key_seed.copy_from_slice(&decrypted[32..64]); + + let keys = Keys { encryption_key, mac_key_seed }; + + decrypted.zeroize(); + + Ok(Self { inner: keys }) + } + } + + /// Hash a key before it is inserted into the key/value store. + /// + /// # Arguments + /// + /// * `table_name` - The name of the key/value table this key will be + /// inserted into. This can also contain additional unique data. It will be + /// used to derive a table specific cryptographic key which will be used + /// in a keyed hash function. This ensures data independence between the + /// different tables of the key/value store. + /// + /// * `key` - The key that should be hashed before it is inserted into the + /// key/value store. + /// + /// **Note**: a key that is hashed by this method can't be retrieved + /// anymore. + /// + /// # Examples + /// + /// ``` + /// # let example = || { + /// use matrix_store_key::StoreKey; + /// use serde_json::json; + /// + /// let store_key = StoreKey::new()?; + /// + /// let key = "bulbasaur"; + /// + /// // Hash the key so people don't know which pokemon we have collected. + /// let hashed_key = store_key.hash_key("list-of-pokemon", key.as_ref()); + /// + /// // It's now safe to insert the key into our key/value store. + /// # Result::<_, anyhow::Error>::Ok(()) }; + /// ``` + pub fn hash_key(&self, table_name: &str, key: &[u8]) -> [u8; 32] { + let mac_key = self.inner.get_mac_key_for_table(table_name); + + mac_key.mac(key).into() + } + + /// Encrypt a value before it is inserted into the key/value store. + /// + /// A value can be decrypted using the [`StoreKey::decrypt_value()`] method. + /// + /// # Arguments + /// + /// * `value` - A value that should be encrypted, any value that implements + /// `Serialize` can be given to this method. The value will be serialized as + /// json before it is encrypted. + /// + /// # Examples + /// + /// ``` + /// # let example = || { + /// use matrix_store_key::StoreKey; + /// use serde_json::{json, value::Value}; + /// + /// let store_key = StoreKey::new()?; + /// + /// let value = json!({ + /// "some": "data", + /// }); + /// + /// let encrypted = store_key.encrypt_value(&value)?; + /// let decrypted: Value = store_key.decrypt_value(&encrypted)?; + /// + /// assert_eq!(value, decrypted); + /// # Result::<_, anyhow::Error>::Ok(()) }; + /// ``` + pub fn encrypt_value(&self, value: &impl Serialize) -> Result, Error> { + let data = serde_json::to_vec(value)?; + + let nonce = Keys::get_nonce()?; + let cipher = XChaCha20Poly1305::new(self.inner.encryption_key()); + + let ciphertext = cipher.encrypt(XNonce::from_slice(&nonce), data.as_ref())?; + + Ok(serde_json::to_vec(&EncryptedValue { version: VERSION, ciphertext, nonce })?) + } + + /// Decrypt a value after it was fetchetd from the key/value store. + /// + /// A value can be encrypted using the [`StoreKey::encrypt_value()`] method. + /// + /// # Arguments + /// + /// * `value` - The ciphertext of a value that should be decrypted. + /// + /// The method will deserialize the decrypted value into the expected type. + /// + /// # Examples + /// + /// ``` + /// # let example = || { + /// use matrix_store_key::StoreKey; + /// use serde_json::{json, value::Value}; + /// + /// let store_key = StoreKey::new()?; + /// + /// let value = json!({ + /// "some": "data", + /// }); + /// + /// let encrypted = store_key.encrypt_value(&value)?; + /// let decrypted: Value = store_key.decrypt_value(&encrypted)?; + /// + /// assert_eq!(value, decrypted); + /// # Result::<_, anyhow::Error>::Ok(()) }; + pub fn decrypt_value Deserialize<'b>>(&self, value: &[u8]) -> Result { + let value: EncryptedValue = serde_json::from_slice(value)?; + + if value.version != VERSION { + return Err(Error::Version(VERSION, value.version)); + } + + let cipher = XChaCha20Poly1305::new(self.inner.encryption_key()); + let nonce = XNonce::from_slice(&value.nonce); + let plaintext = cipher.decrypt(nonce, value.ciphertext.as_ref())?; + + Ok(serde_json::from_slice(&plaintext)?) + } + + /// Expand the given passphrase into a KEY_SIZE long key. + fn expand_key(passphrase: &str, salt: &[u8], rounds: u32) -> Box<[u8; 32]> { + let mut key = Box::new([0u8; 32]); + pbkdf2::>(passphrase.as_bytes(), salt, rounds, &mut *key); + + key + } +} + +struct MacKey(Box<[u8; 32]>); + +impl MacKey { + fn mac(&self, input: &[u8]) -> Hash { + blake3::keyed_hash(&self.0, input) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +struct EncryptedValue { + version: u8, + ciphertext: Vec, + nonce: [u8; XNONCE_SIZE], +} + +#[derive(Zeroize)] +#[zeroize(drop)] +struct Keys { + encryption_key: Box<[u8; 32]>, + mac_key_seed: Box<[u8; 32]>, +} + +impl Keys { + fn new() -> Result { + let mut encryption_key = Box::new([0u8; 32]); + let mut mac_key_seed = Box::new([0u8; 32]); + + let mut rng = thread_rng(); + + encryption_key.try_fill(&mut rng)?; + mac_key_seed.try_fill(&mut rng)?; + + Ok(Self { encryption_key, mac_key_seed }) + } + + fn encryption_key(&self) -> &ChachaKey { + ChachaKey::from_slice(self.encryption_key.as_slice()) + } + + fn mac_key_seed(&self) -> &MacKeySeed { + &self.mac_key_seed + } + + fn get_mac_key_for_table(&self, table_name: &str) -> MacKey { + let mut key = MacKey(Box::new([0u8; 32])); + let mut output = derive_key(table_name, self.mac_key_seed()); + + key.0.copy_from_slice(&output); + + output.zeroize(); + + key + } + + fn get_nonce() -> Result<[u8; XNONCE_SIZE], RandomError> { + let mut nonce = [0u8; XNONCE_SIZE]; + let mut rng = thread_rng(); + + nonce.try_fill(&mut rng)?; + + Ok(nonce) + } +} + +/// Version specific info for the key derivation method that is used. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +enum KdfInfo { + /// The PBKDF2 to Chacha key derivation variant. + 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: [u8; KDF_SALT_SIZE], + }, +} + +/// Version specific info for encryption method that is used to encrypt our +/// store key. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +enum CipherTextInfo { + /// A store key encrypted using the ChaCha20Poly1305 AEAD. + ChaCha20Poly1305 { + /// The nonce that was used to encrypt the ciphertext. + nonce: [u8; XNONCE_SIZE], + /// 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)] +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, +} + +#[cfg(test)] +mod test { + use serde_json::{json, Value}; + + use super::{Error, StoreKey}; + + #[test] + fn generating() { + StoreKey::new().unwrap(); + } + + #[test] + fn exporting_store_key() -> Result<(), Error> { + let passphrase = "it's a secret to everybody"; + let store_key = StoreKey::new()?; + + let value = json!({ + "some": "data" + }); + + let encrypted_value = store_key.encrypt_value(&value)?; + + let encrypted = store_key.export(passphrase)?; + let decrypted = StoreKey::import(passphrase, &encrypted)?; + + assert_eq!(store_key.inner.encryption_key, decrypted.inner.encryption_key); + assert_eq!(store_key.inner.mac_key_seed, decrypted.inner.mac_key_seed); + + let decrypted_value: Value = decrypted.decrypt_value(&encrypted_value)?; + + assert_eq!(value, decrypted_value); + + Ok(()) + } + + #[test] + fn encrypting_values() -> Result<(), Error> { + let event = json!({ + "content": { + "body": "Bee Gees - Stayin' Alive", + "info": { + "duration": 2140786u32, + "mimetype": "audio/mpeg", + "size": 1563685u32 + }, + "msgtype": "m.audio", + "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd" + }, + }); + + let store_key = StoreKey::new()?; + + let encrypted = store_key.encrypt_value(&event)?; + let decrypted: Value = store_key.decrypt_value(&encrypted)?; + + assert_eq!(event, decrypted); + + Ok(()) + } + + #[test] + fn encrypting_keys() -> Result<(), Error> { + let store_key = StoreKey::new()?; + + let first = store_key.hash_key("some_table", b"It's dangerous to go alone"); + let second = store_key.hash_key("some_table", b"It's dangerous to go alone"); + let third = store_key.hash_key("another_table", b"It's dangerous to go alone"); + let fourth = store_key.hash_key("another_table", b"It's dangerous to go alone"); + let fifth = store_key.hash_key("another_table", b"It's not dangerous to go alone"); + + assert_eq!(first, second); + assert_ne!(first, third); + assert_eq!(third, fourth); + assert_ne!(fourth, fifth); + + Ok(()) + } +}