-
Notifications
You must be signed in to change notification settings - Fork 8
Implement KeyObfuscator for Deterministic Encryption of storage keys. #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| use std::io::{Error, ErrorKind}; | ||
G8XSU marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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); | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So just to recap (simplified) what happened here so far:
Is this approach cryptographically sound? Are there any risks associated with it?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, With using ChaCha20Poly1305 here, we need to take care of two things mainly:
For e.g. if we store unencrypted nonce N with cipher-text, and known plain text it might be possible to reverse it. Hence we store the encrypted nonce. Addressing Concern 2: For nonce encryption we use hash(ciphertext) as nonce which isn't stored alongside data in server hence it should be safe. |
||
| debug_assert_eq!(tag.len(), TAG_LENGTH); | ||
| ciphertext.extend_from_slice(&tag); | ||
G8XSU marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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<String, Error> { | ||
| 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); | ||
|
|
||
G8XSU marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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) | ||
G8XSU marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| fn hkdf(initial_key_material: &[u8], salt: &[u8]) -> [u8; 32] { | ||
| let mut engine = HmacEngine::<sha256::Hash>::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); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.