Skip to content

Commit

Permalink
Merge pull request #32 from G8XSU/obfuscate
Browse files Browse the repository at this point in the history
  • Loading branch information
G8XSU authored Aug 16, 2024
2 parents e012e68 + e187833 commit 52c1885
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 11 deletions.
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"]}
8 changes: 6 additions & 2 deletions src/crypto/chacha20poly1305.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
184 changes: 184 additions & 0 deletions src/util/key_obfuscator.rs
Original file line number Diff line number Diff line change
@@ -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<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);

// 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::<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);
}
}
}
5 changes: 5 additions & 0 deletions src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
14 changes: 7 additions & 7 deletions src/util/storable_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ impl<T: EntropySource> StorableBuilder<T> {
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))
}
}

Expand Down

0 comments on commit 52c1885

Please sign in to comment.