Skip to content

Commit 1475a7a

Browse files
committed
Implement KeyObfuscator for Deterministic Encryption of storage keys.
Add KeyObfuscator to provide a helper object for client-side key obfuscation. This implementation uses AES-256-SIV for deterministic encryption and nonce misuse-resistance, enhancing security and preventing common pitfalls (foot-guns) in client-side encryption.
1 parent 59769c9 commit 1475a7a

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ tokio = { version = "1", default-features = false, features = ["time"] }
1818
rand = "0.8.5"
1919
async-trait = "0.1.77"
2020

21+
base64 = { version = "0.21.0", default-features = false }
22+
aes-siv = "0.7.0"
23+
bitcoin_hashes = "0.14.0"
24+
2125
[target.'cfg(genproto)'.build-dependencies]
2226
prost-build = { version = "0.11.3" }
2327
reqwest = { version = "0.11.13", default-features = false, features = ["rustls-tls", "blocking"] }

src/util/key_obfuscator.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use std::io;
2+
use std::io::{Error, ErrorKind};
3+
4+
use aes_siv::aead::generic_array::GenericArray;
5+
use aes_siv::aead::{Aead, Payload};
6+
use aes_siv::{Aes256SivAead, KeyInit};
7+
use base64::prelude::BASE64_STANDARD_NO_PAD;
8+
use base64::Engine;
9+
use bitcoin_hashes::{sha512, Hash, HashEngine, Hmac, HmacEngine};
10+
11+
/// [`KeyObfuscator`] is a utility to obfuscate and deobfuscate storage
12+
/// keys to be used for VSS operations.
13+
///
14+
/// It provides client-side deterministic encryption of given keys using AES-256-SIV.
15+
pub struct KeyObfuscator {
16+
obfuscation_key: [u8; 64],
17+
hashing_key: [u8; 64],
18+
}
19+
20+
impl KeyObfuscator {
21+
/// Constructs a new instance.
22+
pub fn new(obfuscation_master_key: [u8; 64]) -> KeyObfuscator {
23+
let (obfuscation_key, hashing_key) = Self::derive_obfuscation_and_hashing_keys(&obfuscation_master_key);
24+
Self { obfuscation_key, hashing_key }
25+
}
26+
}
27+
28+
impl KeyObfuscator {
29+
/// Obfuscates the given key.
30+
pub fn obfuscate(&self, key: &str) -> io::Result<String> {
31+
let cipher = Aes256SivAead::new(GenericArray::from_slice(&self.obfuscation_key));
32+
let mut engine = HmacEngine::<sha512::Hash>::new(&self.hashing_key);
33+
engine.input(key.as_bytes());
34+
let hmac = Hmac::from_engine(engine).to_byte_array();
35+
36+
let nonce = &hmac[..16];
37+
let mut ciphertext = self.encrypt(key.as_bytes(), nonce, &cipher).map_err(|e| {
38+
let msg = format!("Failed to encrypt key: {}, Error: {}", key, e);
39+
Error::new(ErrorKind::InvalidData, msg)
40+
})?;
41+
42+
let wrapped_nonce = self.encrypt(nonce, &[0; 16], &cipher).map_err(|e| {
43+
let msg = format!("Failed to wrap nonce. key: {}, Error: {}", key, e);
44+
Error::new(ErrorKind::InvalidData, msg)
45+
})?;
46+
debug_assert_eq!(wrapped_nonce.len(), 32);
47+
ciphertext.extend_from_slice(&wrapped_nonce);
48+
Ok(BASE64_STANDARD_NO_PAD.encode(ciphertext))
49+
}
50+
51+
/// Deobfuscates the given key.
52+
pub fn deobfuscate(&self, key: &str) -> io::Result<String> {
53+
let cipher = Aes256SivAead::new(GenericArray::from_slice(&self.obfuscation_key));
54+
55+
let ciphertext_and_wrapped_nonce = BASE64_STANDARD_NO_PAD.decode(key).map_err(|e| {
56+
let msg = format!("Failed to decode base64 while deobfuscating key: {}, Error: {}", key, e);
57+
Error::new(ErrorKind::InvalidData, msg)
58+
})?;
59+
let (ciphertext, wrapped_nonce) =
60+
ciphertext_and_wrapped_nonce.split_at(ciphertext_and_wrapped_nonce.len() - 32);
61+
62+
let nonce = self.decrypt(wrapped_nonce, &[0; 16], &cipher).map_err(|e| {
63+
let msg = format!("Failed to unwrap nonce. key: {}, Error: {}", key, e);
64+
Error::new(ErrorKind::InvalidData, msg)
65+
})?;
66+
let plaintext = self.decrypt(ciphertext, nonce.as_slice(), &cipher).map_err(|e| {
67+
let msg = format!("Failed to decrypt key: {}, Error: {}", key, e);
68+
Error::new(ErrorKind::InvalidData, msg)
69+
})?;
70+
71+
let original_key = String::from_utf8(plaintext).map_err(|e| {
72+
let msg = format!("Input was not valid utf8 while deobfuscating key: {}, Error: {}", key, e);
73+
Error::new(ErrorKind::InvalidData, msg)
74+
})?;
75+
Ok(original_key)
76+
}
77+
78+
/// Encrypts the given plaintext using the provided cipher and nonce.
79+
fn encrypt(&self, plaintext_bytes: &[u8], nonce: &[u8], cipher: &Aes256SivAead) -> Result<Vec<u8>, aes_siv::Error> {
80+
let nonce = GenericArray::from_slice(nonce);
81+
let payload = Payload { msg: plaintext_bytes, aad: b"" };
82+
83+
let ciphertext = cipher.encrypt(nonce, payload)?;
84+
Ok(ciphertext)
85+
}
86+
87+
/// Decrypts the given ciphertext using the provided cipher and nonce.
88+
fn decrypt(&self, ciphertext: &[u8], nonce: &[u8], cipher: &Aes256SivAead) -> Result<Vec<u8>, aes_siv::Error> {
89+
let nonce = GenericArray::from_slice(nonce);
90+
let cipher_payload = Payload { msg: ciphertext, aad: b"" };
91+
92+
let plaintext = cipher.decrypt(nonce, cipher_payload)?;
93+
Ok(plaintext)
94+
}
95+
96+
/// Derives the obfuscation and hashing keys from the master key.
97+
fn derive_obfuscation_and_hashing_keys(obfuscation_master_key: &[u8; 64]) -> ([u8; 64], [u8; 64]) {
98+
let hkdf = |key: &[u8], salt: &[u8]| -> [u8; 64] {
99+
let mut engine = HmacEngine::<sha512::Hash>::new(key);
100+
engine.input(salt);
101+
Hmac::from_engine(engine).to_byte_array()
102+
};
103+
let prk = hkdf(obfuscation_master_key, "pseudo_random_key".as_bytes());
104+
let k1 = hkdf(&prk, "obfuscation_key".as_bytes());
105+
let k2 = hkdf(&prk, &[&k1[..], "hashing_key".as_bytes()].concat());
106+
107+
(k1, k2)
108+
}
109+
}
110+
111+
#[cfg(test)]
112+
mod tests {
113+
use crate::util::key_obfuscator::KeyObfuscator;
114+
use crate::util::storable_builder::EntropySource;
115+
116+
pub struct TestEntropyProvider;
117+
impl EntropySource for TestEntropyProvider {
118+
/// A terrible implementation which fills a buffer with bytes from a simple counter for testing
119+
/// purposes.
120+
fn fill_bytes(&self, buffer: &mut [u8]) {
121+
for (i, byte) in buffer.iter_mut().enumerate() {
122+
*byte = (i % 256) as u8;
123+
}
124+
}
125+
}
126+
127+
#[test]
128+
fn obfuscate_deobfuscate() {
129+
let test_entropy_provider = TestEntropyProvider;
130+
let mut obfuscation_master_key = [0u8; 64];
131+
test_entropy_provider.fill_bytes(&mut obfuscation_master_key);
132+
let key_obfuscator = KeyObfuscator::new(obfuscation_master_key);
133+
let expected_key = "a_semi_secret_key";
134+
let obfuscated_key = key_obfuscator.obfuscate(expected_key).unwrap();
135+
136+
let actual_key = key_obfuscator.deobfuscate(obfuscated_key.as_str()).unwrap();
137+
assert_eq!(actual_key, expected_key);
138+
assert_eq!(
139+
obfuscated_key,
140+
"VR5MoPsIAz2jgV3czpsp8T58EXBs8EmZdXWqXg8J9a+w5+rsMQcP7Ku15Q2pmKW+6U55Irm8gR+OBTYDrpaCOjc"
141+
);
142+
}
143+
}

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;

0 commit comments

Comments
 (0)