From 199b5330c8df3114243425890eab4ec858fbf8a2 Mon Sep 17 00:00:00 2001 From: G8XSU <3442979+G8XSU@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:53:54 -0700 Subject: [PATCH 1/2] Change decrypt_in_place to return a Result. --- src/crypto/chacha20poly1305.rs | 8 ++++++-- src/util/storable_builder.rs | 14 +++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/crypto/chacha20poly1305.rs b/src/crypto/chacha20poly1305.rs index 9efe49f..c8576fd 100644 --- a/src/crypto/chacha20poly1305.rs +++ b/src/crypto/chacha20poly1305.rs @@ -76,10 +76,14 @@ mod real_chachapoly { self.finish_and_get_tag(out_tag); } - pub fn decrypt_inplace(&mut self, input_output: &mut [u8], tag: &[u8]) -> bool { + pub fn decrypt_inplace(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> { assert!(self.finished == false); self.decrypt_in_place(input_output); - self.finish_and_check_tag(tag) + if self.finish_and_check_tag(tag) { + Ok(()) + } else { + Err(()) + } } // Encrypt `input_output` in-place. To finish and calculate the tag, use `finish_and_get_tag` diff --git a/src/util/storable_builder.rs b/src/util/storable_builder.rs index 02df8b0..a5857d6 100644 --- a/src/util/storable_builder.rs +++ b/src/util/storable_builder.rs @@ -67,13 +67,13 @@ impl StorableBuilder { let mut cipher = ChaCha20Poly1305::new(&self.data_encryption_key, &encryption_metadata.nonce, &[]); - if cipher.decrypt_inplace(&mut storable.data, encryption_metadata.tag.borrow()) { - let data_blob = PlaintextBlob::decode(&storable.data[..]) - .map_err(|e| Error::new(ErrorKind::InvalidData, e))?; - Ok((data_blob.value, data_blob.version)) - } else { - Err(Error::new(ErrorKind::InvalidData, "Invalid Tag")) - } + cipher + .decrypt_inplace(&mut storable.data, encryption_metadata.tag.borrow()) + .map_err(|_| Error::new(ErrorKind::InvalidData, "Invalid Tag"))?; + + let data_blob = PlaintextBlob::decode(&storable.data[..]) + .map_err(|e| Error::new(ErrorKind::InvalidData, e))?; + Ok((data_blob.value, data_blob.version)) } } From e18783344441f2b20899c517514fe46dae0581d7 Mon Sep 17 00:00:00 2001 From: G8XSU <3442979+G8XSU@users.noreply.github.com> Date: Wed, 7 Aug 2024 12:37:05 -0700 Subject: [PATCH 2/2] Implement KeyObfuscator for Deterministic Encryption of storage keys. Add KeyObfuscator to provide a helper object for client-side key obfuscation. This implementation uses ChaCha20-Poly1305 for deterministic encryption and tag authentication, enhancing security and preventing common pitfalls (foot-guns) in client-side encryption. --- Cargo.toml | 7 +- src/util/key_obfuscator.rs | 184 +++++++++++++++++++++++++++++++++++++ src/util/mod.rs | 5 + 3 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 src/util/key_obfuscator.rs diff --git a/Cargo.toml b/Cargo.toml index cffdc83..e840df3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ build = "build.rs" [features] default = ["lnurl-auth"] -lnurl-auth = ["dep:bitcoin", "dep:url", "dep:base64", "dep:serde", "dep:serde_json", "reqwest/json"] +lnurl-auth = ["dep:bitcoin", "dep:url", "dep:serde", "dep:serde_json", "reqwest/json"] [dependencies] prost = "0.11.6" @@ -23,14 +23,17 @@ rand = "0.8.5" async-trait = "0.1.77" bitcoin = { version = "0.32.2", default-features = false, features = ["std", "rand-std"], optional = true } url = { version = "2.5.0", default-features = false, optional = true } -base64 = { version = "0.21.7", default-features = false, optional = true } +base64 = { version = "0.21.7", default-features = false} serde = { version = "1.0.196", default-features = false, features = ["serde_derive"], optional = true } serde_json = { version = "1.0.113", default-features = false, optional = true } +bitcoin_hashes = "0.14.0" + [target.'cfg(genproto)'.build-dependencies] prost-build = { version = "0.11.3" } reqwest = { version = "0.11.13", default-features = false, features = ["rustls-tls", "blocking"] } [dev-dependencies] mockito = "0.31.1" +proptest = "1.1.0" tokio = { version = "1.22.0", features = ["macros"]} diff --git a/src/util/key_obfuscator.rs b/src/util/key_obfuscator.rs new file mode 100644 index 0000000..056e050 --- /dev/null +++ b/src/util/key_obfuscator.rs @@ -0,0 +1,184 @@ +use std::io::{Error, ErrorKind}; + +use base64::prelude::BASE64_STANDARD_NO_PAD; +use base64::Engine; +use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; + +use crate::crypto::chacha20poly1305::ChaCha20Poly1305; + +/// [`KeyObfuscator`] is a utility to obfuscate and deobfuscate storage +/// keys to be used for VSS operations. +/// +/// It provides client-side deterministic encryption of given keys using ChaCha20-Poly1305. +pub struct KeyObfuscator { + obfuscation_key: [u8; 32], + hashing_key: [u8; 32], +} + +impl KeyObfuscator { + /// Constructs a new instance. + pub fn new(obfuscation_master_key: [u8; 32]) -> KeyObfuscator { + let (obfuscation_key, hashing_key) = + Self::derive_obfuscation_and_hashing_keys(&obfuscation_master_key); + Self { obfuscation_key, hashing_key } + } +} + +const TAG_LENGTH: usize = 16; +const NONCE_LENGTH: usize = 12; + +impl KeyObfuscator { + /// Obfuscates the given key. + pub fn obfuscate(&self, key: &str) -> String { + let key_bytes = key.as_bytes(); + let mut ciphertext = + Vec::with_capacity(key_bytes.len() + TAG_LENGTH + NONCE_LENGTH + TAG_LENGTH); + ciphertext.extend_from_slice(&key_bytes); + + // Encrypt key in-place using a synthetic nonce. + let (mut nonce, tag) = self.encrypt(&mut ciphertext, key.as_bytes()); + + // Wrap the synthetic nonce to store along-side key. + let (_, nonce_tag) = self.encrypt(&mut nonce, &ciphertext); + + debug_assert_eq!(tag.len(), TAG_LENGTH); + ciphertext.extend_from_slice(&tag); + debug_assert_eq!(nonce.len(), NONCE_LENGTH); + ciphertext.extend_from_slice(&nonce); + debug_assert_eq!(nonce_tag.len(), TAG_LENGTH); + ciphertext.extend_from_slice(&nonce_tag); + BASE64_STANDARD_NO_PAD.encode(ciphertext) + } + + /// Deobfuscates the given obfuscated_key. + pub fn deobfuscate(&self, obfuscated_key: &str) -> Result { + let obfuscated_key_bytes = BASE64_STANDARD_NO_PAD.decode(obfuscated_key).map_err(|e| { + let msg = format!( + "Failed to decode base64 while deobfuscating key: {}, Error: {}", + obfuscated_key, e + ); + Error::new(ErrorKind::InvalidData, msg) + })?; + + if obfuscated_key_bytes.len() < TAG_LENGTH + NONCE_LENGTH + TAG_LENGTH { + let msg = format!( + "Failed to deobfuscate, obfuscated_key was of invalid length. \ + Obfuscated key should at least have {} bytes, found: {}. Key: {}.", + (TAG_LENGTH + NONCE_LENGTH + TAG_LENGTH), + obfuscated_key_bytes.len(), + obfuscated_key + ); + return Err(Error::new(ErrorKind::InvalidData, msg)); + } + + // Split obfuscated_key into ciphertext, tag(for ciphertext), wrapped_nonce, tag(for wrapped_nonce). + let (ciphertext, remaining) = obfuscated_key_bytes + .split_at(obfuscated_key_bytes.len() - TAG_LENGTH - NONCE_LENGTH - TAG_LENGTH); + let (tag, remaining) = remaining.split_at(TAG_LENGTH); + let (wrapped_nonce_bytes, wrapped_nonce_tag) = remaining.split_at(NONCE_LENGTH); + debug_assert_eq!(wrapped_nonce_tag.len(), TAG_LENGTH); + + // Unwrap wrapped_nonce to get nonce. + let mut wrapped_nonce = [0u8; NONCE_LENGTH]; + wrapped_nonce.clone_from_slice(&wrapped_nonce_bytes); + self.decrypt(&mut wrapped_nonce, ciphertext, wrapped_nonce_tag).map_err(|_| { + let msg = format!( + "Failed to decrypt wrapped nonce, for key: {}, Invalid Tag.", + obfuscated_key + ); + Error::new(ErrorKind::InvalidData, msg) + })?; + + // Decrypt ciphertext using nonce. + let mut cipher = ChaCha20Poly1305::new(&self.obfuscation_key, &wrapped_nonce, &[]); + let mut ciphertext = ciphertext.to_vec(); + cipher.decrypt_inplace(&mut ciphertext, tag).map_err(|_| { + let msg = format!("Failed to decrypt key: {}, Invalid Tag.", obfuscated_key); + Error::new(ErrorKind::InvalidData, msg) + })?; + + let original_key = String::from_utf8(ciphertext).map_err(|e| { + let msg = format!( + "Input was not valid utf8 while deobfuscating key: {}, Error: {}", + obfuscated_key, e + ); + Error::new(ErrorKind::InvalidData, msg) + })?; + Ok(original_key) + } + + /// Encrypts the given plaintext in-place using a HMAC generated nonce. + fn encrypt( + &self, mut plaintext: &mut [u8], initial_nonce_material: &[u8], + ) -> ([u8; 12], [u8; 16]) { + let nonce = self.generate_synthetic_nonce(initial_nonce_material); + let mut cipher = ChaCha20Poly1305::new(&self.obfuscation_key, &nonce, &[]); + let mut tag = [0u8; TAG_LENGTH]; + cipher.encrypt_inplace(&mut plaintext, &mut tag); + (nonce, tag) + } + + /// Decrypts the given ciphertext in-place using a HMAC generated nonce. + fn decrypt( + &self, mut ciphertext: &mut [u8], initial_nonce_material: &[u8], tag: &[u8], + ) -> Result<(), ()> { + let nonce = self.generate_synthetic_nonce(initial_nonce_material); + let mut cipher = ChaCha20Poly1305::new(&self.obfuscation_key, &nonce, &[]); + cipher.decrypt_inplace(&mut ciphertext, tag) + } + + /// Generate a HMAC based nonce using provided `initial_nonce_material`. + fn generate_synthetic_nonce(&self, initial_nonce_material: &[u8]) -> [u8; 12] { + let hmac = Self::hkdf(&self.hashing_key, initial_nonce_material); + let mut nonce = [0u8; NONCE_LENGTH]; + nonce[4..].copy_from_slice(&hmac[..8]); + nonce + } + + /// Derives the obfuscation and hashing keys from the master key. + fn derive_obfuscation_and_hashing_keys( + obfuscation_master_key: &[u8; 32], + ) -> ([u8; 32], [u8; 32]) { + let prk = Self::hkdf(obfuscation_master_key, "pseudo_random_key".as_bytes()); + let k1 = Self::hkdf(&prk, "obfuscation_key".as_bytes()); + let k2 = Self::hkdf(&prk, &[&k1[..], "hashing_key".as_bytes()].concat()); + (k1, k2) + } + fn hkdf(initial_key_material: &[u8], salt: &[u8]) -> [u8; 32] { + let mut engine = HmacEngine::::new(salt); + engine.input(initial_key_material); + Hmac::from_engine(engine).to_byte_array() + } +} + +#[cfg(test)] +mod tests { + use crate::util::key_obfuscator::KeyObfuscator; + + #[test] + fn obfuscate_deobfuscate_deterministic() { + let obfuscation_master_key = [42u8; 32]; + let key_obfuscator = KeyObfuscator::new(obfuscation_master_key); + let expected_key = "a_semi_secret_key"; + let obfuscated_key = key_obfuscator.obfuscate(expected_key); + + let actual_key = key_obfuscator.deobfuscate(obfuscated_key.as_str()).unwrap(); + assert_eq!(actual_key, expected_key); + assert_eq!( + obfuscated_key, + "cMoet5WTvl0nYds+VW7JPCtXUq24DtMG2dR9apAi/T5jy8eNIEyDrUAJBS4geeUuX+XGXPqlizIByOip2g" + ); + } + + use proptest::prelude::*; + + proptest! { + #[test] + fn obfuscate_deobfuscate_proptest(expected_key in "[a-zA-Z0-9_!@#,;:%\\s\\*\\$\\^&\\(\\)\\[\\]\\{\\}\\.]*", obfuscation_master_key in any::<[u8; 32]>()) { + let key_obfuscator = KeyObfuscator::new(obfuscation_master_key); + let obfuscated_key = key_obfuscator.obfuscate(&expected_key); + let actual_key = key_obfuscator.deobfuscate(obfuscated_key.as_str()).unwrap(); + assert_eq!(actual_key, expected_key); + } + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index c9b3b48..7f40f97 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -5,3 +5,8 @@ pub mod storable_builder; /// Contains retry utilities. pub mod retry; + +/// Contains [`KeyObfuscator`] utility. +/// +/// [`KeyObfuscator`]: key_obfuscator::KeyObfuscator +pub mod key_obfuscator;