Skip to content

Commit 52c1885

Browse files
authored
Merge pull request #32 from G8XSU/obfuscate
2 parents e012e68 + e187833 commit 52c1885

File tree

5 files changed

+207
-11
lines changed

5 files changed

+207
-11
lines changed

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ build = "build.rs"
1313

1414
[features]
1515
default = ["lnurl-auth"]
16-
lnurl-auth = ["dep:bitcoin", "dep:url", "dep:base64", "dep:serde", "dep:serde_json", "reqwest/json"]
16+
lnurl-auth = ["dep:bitcoin", "dep:url", "dep:serde", "dep:serde_json", "reqwest/json"]
1717

1818
[dependencies]
1919
prost = "0.11.6"
@@ -23,14 +23,17 @@ rand = "0.8.5"
2323
async-trait = "0.1.77"
2424
bitcoin = { version = "0.32.2", default-features = false, features = ["std", "rand-std"], optional = true }
2525
url = { version = "2.5.0", default-features = false, optional = true }
26-
base64 = { version = "0.21.7", default-features = false, optional = true }
26+
base64 = { version = "0.21.7", default-features = false}
2727
serde = { version = "1.0.196", default-features = false, features = ["serde_derive"], optional = true }
2828
serde_json = { version = "1.0.113", default-features = false, optional = true }
2929

30+
bitcoin_hashes = "0.14.0"
31+
3032
[target.'cfg(genproto)'.build-dependencies]
3133
prost-build = { version = "0.11.3" }
3234
reqwest = { version = "0.11.13", default-features = false, features = ["rustls-tls", "blocking"] }
3335

3436
[dev-dependencies]
3537
mockito = "0.31.1"
38+
proptest = "1.1.0"
3639
tokio = { version = "1.22.0", features = ["macros"]}

src/crypto/chacha20poly1305.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,14 @@ mod real_chachapoly {
7676
self.finish_and_get_tag(out_tag);
7777
}
7878

79-
pub fn decrypt_inplace(&mut self, input_output: &mut [u8], tag: &[u8]) -> bool {
79+
pub fn decrypt_inplace(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
8080
assert!(self.finished == false);
8181
self.decrypt_in_place(input_output);
82-
self.finish_and_check_tag(tag)
82+
if self.finish_and_check_tag(tag) {
83+
Ok(())
84+
} else {
85+
Err(())
86+
}
8387
}
8488

8589
// Encrypt `input_output` in-place. To finish and calculate the tag, use `finish_and_get_tag`

src/util/key_obfuscator.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use std::io::{Error, ErrorKind};
2+
3+
use base64::prelude::BASE64_STANDARD_NO_PAD;
4+
use base64::Engine;
5+
use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
6+
7+
use crate::crypto::chacha20poly1305::ChaCha20Poly1305;
8+
9+
/// [`KeyObfuscator`] is a utility to obfuscate and deobfuscate storage
10+
/// keys to be used for VSS operations.
11+
///
12+
/// It provides client-side deterministic encryption of given keys using ChaCha20-Poly1305.
13+
pub struct KeyObfuscator {
14+
obfuscation_key: [u8; 32],
15+
hashing_key: [u8; 32],
16+
}
17+
18+
impl KeyObfuscator {
19+
/// Constructs a new instance.
20+
pub fn new(obfuscation_master_key: [u8; 32]) -> KeyObfuscator {
21+
let (obfuscation_key, hashing_key) =
22+
Self::derive_obfuscation_and_hashing_keys(&obfuscation_master_key);
23+
Self { obfuscation_key, hashing_key }
24+
}
25+
}
26+
27+
const TAG_LENGTH: usize = 16;
28+
const NONCE_LENGTH: usize = 12;
29+
30+
impl KeyObfuscator {
31+
/// Obfuscates the given key.
32+
pub fn obfuscate(&self, key: &str) -> String {
33+
let key_bytes = key.as_bytes();
34+
let mut ciphertext =
35+
Vec::with_capacity(key_bytes.len() + TAG_LENGTH + NONCE_LENGTH + TAG_LENGTH);
36+
ciphertext.extend_from_slice(&key_bytes);
37+
38+
// Encrypt key in-place using a synthetic nonce.
39+
let (mut nonce, tag) = self.encrypt(&mut ciphertext, key.as_bytes());
40+
41+
// Wrap the synthetic nonce to store along-side key.
42+
let (_, nonce_tag) = self.encrypt(&mut nonce, &ciphertext);
43+
44+
debug_assert_eq!(tag.len(), TAG_LENGTH);
45+
ciphertext.extend_from_slice(&tag);
46+
debug_assert_eq!(nonce.len(), NONCE_LENGTH);
47+
ciphertext.extend_from_slice(&nonce);
48+
debug_assert_eq!(nonce_tag.len(), TAG_LENGTH);
49+
ciphertext.extend_from_slice(&nonce_tag);
50+
BASE64_STANDARD_NO_PAD.encode(ciphertext)
51+
}
52+
53+
/// Deobfuscates the given obfuscated_key.
54+
pub fn deobfuscate(&self, obfuscated_key: &str) -> Result<String, Error> {
55+
let obfuscated_key_bytes = BASE64_STANDARD_NO_PAD.decode(obfuscated_key).map_err(|e| {
56+
let msg = format!(
57+
"Failed to decode base64 while deobfuscating key: {}, Error: {}",
58+
obfuscated_key, e
59+
);
60+
Error::new(ErrorKind::InvalidData, msg)
61+
})?;
62+
63+
if obfuscated_key_bytes.len() < TAG_LENGTH + NONCE_LENGTH + TAG_LENGTH {
64+
let msg = format!(
65+
"Failed to deobfuscate, obfuscated_key was of invalid length. \
66+
Obfuscated key should at least have {} bytes, found: {}. Key: {}.",
67+
(TAG_LENGTH + NONCE_LENGTH + TAG_LENGTH),
68+
obfuscated_key_bytes.len(),
69+
obfuscated_key
70+
);
71+
return Err(Error::new(ErrorKind::InvalidData, msg));
72+
}
73+
74+
// Split obfuscated_key into ciphertext, tag(for ciphertext), wrapped_nonce, tag(for wrapped_nonce).
75+
let (ciphertext, remaining) = obfuscated_key_bytes
76+
.split_at(obfuscated_key_bytes.len() - TAG_LENGTH - NONCE_LENGTH - TAG_LENGTH);
77+
let (tag, remaining) = remaining.split_at(TAG_LENGTH);
78+
let (wrapped_nonce_bytes, wrapped_nonce_tag) = remaining.split_at(NONCE_LENGTH);
79+
debug_assert_eq!(wrapped_nonce_tag.len(), TAG_LENGTH);
80+
81+
// Unwrap wrapped_nonce to get nonce.
82+
let mut wrapped_nonce = [0u8; NONCE_LENGTH];
83+
wrapped_nonce.clone_from_slice(&wrapped_nonce_bytes);
84+
self.decrypt(&mut wrapped_nonce, ciphertext, wrapped_nonce_tag).map_err(|_| {
85+
let msg = format!(
86+
"Failed to decrypt wrapped nonce, for key: {}, Invalid Tag.",
87+
obfuscated_key
88+
);
89+
Error::new(ErrorKind::InvalidData, msg)
90+
})?;
91+
92+
// Decrypt ciphertext using nonce.
93+
let mut cipher = ChaCha20Poly1305::new(&self.obfuscation_key, &wrapped_nonce, &[]);
94+
let mut ciphertext = ciphertext.to_vec();
95+
cipher.decrypt_inplace(&mut ciphertext, tag).map_err(|_| {
96+
let msg = format!("Failed to decrypt key: {}, Invalid Tag.", obfuscated_key);
97+
Error::new(ErrorKind::InvalidData, msg)
98+
})?;
99+
100+
let original_key = String::from_utf8(ciphertext).map_err(|e| {
101+
let msg = format!(
102+
"Input was not valid utf8 while deobfuscating key: {}, Error: {}",
103+
obfuscated_key, e
104+
);
105+
Error::new(ErrorKind::InvalidData, msg)
106+
})?;
107+
Ok(original_key)
108+
}
109+
110+
/// Encrypts the given plaintext in-place using a HMAC generated nonce.
111+
fn encrypt(
112+
&self, mut plaintext: &mut [u8], initial_nonce_material: &[u8],
113+
) -> ([u8; 12], [u8; 16]) {
114+
let nonce = self.generate_synthetic_nonce(initial_nonce_material);
115+
let mut cipher = ChaCha20Poly1305::new(&self.obfuscation_key, &nonce, &[]);
116+
let mut tag = [0u8; TAG_LENGTH];
117+
cipher.encrypt_inplace(&mut plaintext, &mut tag);
118+
(nonce, tag)
119+
}
120+
121+
/// Decrypts the given ciphertext in-place using a HMAC generated nonce.
122+
fn decrypt(
123+
&self, mut ciphertext: &mut [u8], initial_nonce_material: &[u8], tag: &[u8],
124+
) -> Result<(), ()> {
125+
let nonce = self.generate_synthetic_nonce(initial_nonce_material);
126+
let mut cipher = ChaCha20Poly1305::new(&self.obfuscation_key, &nonce, &[]);
127+
cipher.decrypt_inplace(&mut ciphertext, tag)
128+
}
129+
130+
/// Generate a HMAC based nonce using provided `initial_nonce_material`.
131+
fn generate_synthetic_nonce(&self, initial_nonce_material: &[u8]) -> [u8; 12] {
132+
let hmac = Self::hkdf(&self.hashing_key, initial_nonce_material);
133+
let mut nonce = [0u8; NONCE_LENGTH];
134+
nonce[4..].copy_from_slice(&hmac[..8]);
135+
nonce
136+
}
137+
138+
/// Derives the obfuscation and hashing keys from the master key.
139+
fn derive_obfuscation_and_hashing_keys(
140+
obfuscation_master_key: &[u8; 32],
141+
) -> ([u8; 32], [u8; 32]) {
142+
let prk = Self::hkdf(obfuscation_master_key, "pseudo_random_key".as_bytes());
143+
let k1 = Self::hkdf(&prk, "obfuscation_key".as_bytes());
144+
let k2 = Self::hkdf(&prk, &[&k1[..], "hashing_key".as_bytes()].concat());
145+
(k1, k2)
146+
}
147+
fn hkdf(initial_key_material: &[u8], salt: &[u8]) -> [u8; 32] {
148+
let mut engine = HmacEngine::<sha256::Hash>::new(salt);
149+
engine.input(initial_key_material);
150+
Hmac::from_engine(engine).to_byte_array()
151+
}
152+
}
153+
154+
#[cfg(test)]
155+
mod tests {
156+
use crate::util::key_obfuscator::KeyObfuscator;
157+
158+
#[test]
159+
fn obfuscate_deobfuscate_deterministic() {
160+
let obfuscation_master_key = [42u8; 32];
161+
let key_obfuscator = KeyObfuscator::new(obfuscation_master_key);
162+
let expected_key = "a_semi_secret_key";
163+
let obfuscated_key = key_obfuscator.obfuscate(expected_key);
164+
165+
let actual_key = key_obfuscator.deobfuscate(obfuscated_key.as_str()).unwrap();
166+
assert_eq!(actual_key, expected_key);
167+
assert_eq!(
168+
obfuscated_key,
169+
"cMoet5WTvl0nYds+VW7JPCtXUq24DtMG2dR9apAi/T5jy8eNIEyDrUAJBS4geeUuX+XGXPqlizIByOip2g"
170+
);
171+
}
172+
173+
use proptest::prelude::*;
174+
175+
proptest! {
176+
#[test]
177+
fn obfuscate_deobfuscate_proptest(expected_key in "[a-zA-Z0-9_!@#,;:%\\s\\*\\$\\^&\\(\\)\\[\\]\\{\\}\\.]*", obfuscation_master_key in any::<[u8; 32]>()) {
178+
let key_obfuscator = KeyObfuscator::new(obfuscation_master_key);
179+
let obfuscated_key = key_obfuscator.obfuscate(&expected_key);
180+
let actual_key = key_obfuscator.deobfuscate(obfuscated_key.as_str()).unwrap();
181+
assert_eq!(actual_key, expected_key);
182+
}
183+
}
184+
}

src/util/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ pub mod storable_builder;
55

66
/// Contains retry utilities.
77
pub mod retry;
8+
9+
/// Contains [`KeyObfuscator`] utility.
10+
///
11+
/// [`KeyObfuscator`]: key_obfuscator::KeyObfuscator
12+
pub mod key_obfuscator;

src/util/storable_builder.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ impl<T: EntropySource> StorableBuilder<T> {
6767
let mut cipher =
6868
ChaCha20Poly1305::new(&self.data_encryption_key, &encryption_metadata.nonce, &[]);
6969

70-
if cipher.decrypt_inplace(&mut storable.data, encryption_metadata.tag.borrow()) {
71-
let data_blob = PlaintextBlob::decode(&storable.data[..])
72-
.map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
73-
Ok((data_blob.value, data_blob.version))
74-
} else {
75-
Err(Error::new(ErrorKind::InvalidData, "Invalid Tag"))
76-
}
70+
cipher
71+
.decrypt_inplace(&mut storable.data, encryption_metadata.tag.borrow())
72+
.map_err(|_| Error::new(ErrorKind::InvalidData, "Invalid Tag"))?;
73+
74+
let data_blob = PlaintextBlob::decode(&storable.data[..])
75+
.map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
76+
Ok((data_blob.value, data_blob.version))
7777
}
7878
}
7979

0 commit comments

Comments
 (0)