Skip to content

Commit f1b8e22

Browse files
feat: client data encryption (#246)
Co-authored-by: Github Bot <[email protected]>
1 parent 930cc0d commit f1b8e22

File tree

7 files changed

+640
-200
lines changed

7 files changed

+640
-200
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
250822.0
1+
250824.0

crates/client/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ tokio = { workspace = true, features = ["sync"] }
3131
# O11y
3232
tracing = { workspace = true }
3333

34+
# Crypto
35+
ring = "0.17"
36+
3437
# Other
3538
libp2p-identity = { workspace = true }
3639

crates/client/src/encryption.rs

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
use {
2+
rand::{Rng, SeedableRng as _, rngs::StdRng},
3+
ring::{aead, hkdf},
4+
wcn_storage_api::{MapEntry, Record, RecordBorrowed, operation as op},
5+
};
6+
7+
const INFO_ENCRYPTION_KEY: &[u8] = b"encryption_key";
8+
const KEY_SALT: &[u8] = b"wcn_client";
9+
const KEY_LEN_ENCRYPTION: usize = 32;
10+
11+
#[derive(Debug, PartialEq, Eq, thiserror::Error, Clone)]
12+
pub enum Error {
13+
#[error("Invalid data")]
14+
Data,
15+
16+
#[error("Invalid secret")]
17+
Secret,
18+
19+
#[error("Failed to encrypt")]
20+
Encrypt,
21+
22+
#[error("Failed to decrypt")]
23+
Decrypt,
24+
}
25+
26+
#[derive(Clone)]
27+
pub struct Key {
28+
key: aead::LessSafeKey,
29+
}
30+
31+
impl Key {
32+
pub fn new(secret: &[u8]) -> Result<Self, Error> {
33+
if secret.is_empty() {
34+
return Err(Error::Secret);
35+
}
36+
37+
let prk = hkdf::Salt::new(hkdf::HKDF_SHA256, KEY_SALT).extract(secret);
38+
39+
let mut encryption_key = [0; KEY_LEN_ENCRYPTION];
40+
41+
// Both unwraps are safe as the length everywhere is guaranteed to be
42+
// compatible.
43+
prk.expand(&[INFO_ENCRYPTION_KEY], &aead::CHACHA20_POLY1305)
44+
.unwrap()
45+
.fill(&mut encryption_key)
46+
.unwrap();
47+
48+
let key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &encryption_key).unwrap();
49+
let key = aead::LessSafeKey::new(key);
50+
51+
Ok(Self { key })
52+
}
53+
54+
fn seal(&self, data: &[u8]) -> Result<Vec<u8>, Error> {
55+
if data.is_empty() {
56+
return Ok(Vec::new());
57+
}
58+
59+
// Generate a unique nonce.
60+
let nonce: [u8; aead::NONCE_LEN] = StdRng::from_rng(&mut rand::rng()).random();
61+
62+
// Compute the total data length for `nonce + serialized data + tag`.
63+
let mut out =
64+
Vec::with_capacity(aead::NONCE_LEN + data.len() + aead::CHACHA20_POLY1305.tag_len());
65+
66+
// Write nonce bytes.
67+
out.extend_from_slice(&nonce);
68+
69+
// Write data.
70+
out.extend_from_slice(data);
71+
72+
// Encrypt the data.
73+
let tag = self
74+
.key
75+
.seal_in_place_separate_tag(
76+
aead::Nonce::assume_unique_for_key(nonce),
77+
aead::Aad::empty(),
78+
&mut out[aead::NONCE_LEN..],
79+
)
80+
.map_err(|_| Error::Encrypt)?;
81+
82+
// Write tag.
83+
out.extend_from_slice(tag.as_ref());
84+
85+
Ok(out)
86+
}
87+
88+
fn open_in_place<'in_out>(&self, data: &'in_out mut [u8]) -> Result<&'in_out [u8], Error> {
89+
if data.is_empty() {
90+
Ok(data)
91+
} else if data.len() < aead::NONCE_LEN + aead::CHACHA20_POLY1305.tag_len() + 1 {
92+
Err(Error::Data)
93+
} else {
94+
// Safe unwrap as nonce length is guaranteed to be correct.
95+
let nonce = aead::Nonce::try_assume_unique_for_key(&data[..aead::NONCE_LEN]).unwrap();
96+
97+
Ok(self
98+
.key
99+
.open_in_place(nonce, aead::Aad::empty(), &mut data[aead::NONCE_LEN..])
100+
.map_err(|_| Error::Decrypt)?)
101+
}
102+
}
103+
}
104+
105+
pub(super) fn decrypt_output(output: &mut op::Output, key: &Key) -> Result<(), Error> {
106+
match output {
107+
op::Output::Record(Some(rec)) => {
108+
let decrypted = key.open_in_place(&mut rec.value)?.into();
109+
rec.value = decrypted;
110+
}
111+
112+
op::Output::MapPage(page) => {
113+
for entry in &mut page.entries {
114+
let decrypted = key.open_in_place(&mut entry.record.value)?.into();
115+
entry.record.value = decrypted;
116+
}
117+
}
118+
119+
_ => {}
120+
}
121+
122+
Ok(())
123+
}
124+
125+
pub(super) trait Encrypt {
126+
type Output;
127+
128+
fn encrypt(self, key: &Key) -> Result<Self::Output, Error>;
129+
}
130+
131+
impl Encrypt for RecordBorrowed<'_> {
132+
type Output = Record;
133+
134+
fn encrypt(self, key: &Key) -> Result<Self::Output, Error> {
135+
Ok(Record {
136+
value: key.seal(self.value)?,
137+
expiration: self.expiration,
138+
version: self.version,
139+
})
140+
}
141+
}
142+
143+
impl Encrypt for Record {
144+
type Output = Self;
145+
146+
fn encrypt(self, key: &Key) -> Result<Self::Output, Error> {
147+
Ok(Self {
148+
value: key.seal(&self.value)?,
149+
..self
150+
})
151+
}
152+
}
153+
154+
impl Encrypt for op::SetBorrowed<'_> {
155+
type Output = op::Set;
156+
157+
fn encrypt(self, key: &Key) -> Result<Self::Output, Error> {
158+
Ok(op::Set {
159+
namespace: self.namespace,
160+
key: self.key.to_owned(),
161+
record: self.record.encrypt(key)?,
162+
keyspace_version: self.keyspace_version,
163+
})
164+
}
165+
}
166+
167+
impl Encrypt for op::Set {
168+
type Output = Self;
169+
170+
fn encrypt(self, key: &Key) -> Result<Self::Output, Error> {
171+
Ok(Self {
172+
record: self.record.encrypt(key)?,
173+
..self
174+
})
175+
}
176+
}
177+
178+
impl Encrypt for op::HSetBorrowed<'_> {
179+
type Output = op::HSet;
180+
181+
fn encrypt(self, key: &Key) -> Result<Self::Output, Error> {
182+
Ok(op::HSet {
183+
namespace: self.namespace,
184+
key: self.key.to_owned(),
185+
entry: MapEntry {
186+
field: self.entry.field.to_owned(),
187+
record: self.entry.record.encrypt(key)?,
188+
},
189+
keyspace_version: self.keyspace_version,
190+
})
191+
}
192+
}
193+
194+
impl Encrypt for op::HSet {
195+
type Output = Self;
196+
197+
fn encrypt(self, key: &Key) -> Result<Self::Output, Error> {
198+
Ok(Self {
199+
entry: MapEntry {
200+
field: self.entry.field,
201+
record: self.entry.record.encrypt(key)?,
202+
},
203+
..self
204+
})
205+
}
206+
}
207+
208+
impl Encrypt for op::Operation<'_> {
209+
type Output = Self;
210+
211+
fn encrypt(self, key: &Key) -> Result<Self::Output, Error> {
212+
Ok(match self {
213+
op::Operation::Owned(op) => match op {
214+
op::Owned::Set(op) => op::Owned::Set(op.encrypt(key)?).into(),
215+
op::Owned::HSet(op) => op::Owned::HSet(op.encrypt(key)?).into(),
216+
_ => op.into(),
217+
},
218+
219+
op::Operation::Borrowed(op) => match op {
220+
op::Borrowed::Set(op) => op::Owned::Set(op.encrypt(key)?).into(),
221+
op::Borrowed::HSet(op) => op::Owned::HSet(op.encrypt(key)?).into(),
222+
_ => op.into(),
223+
},
224+
})
225+
}
226+
}
227+
228+
#[cfg(test)]
229+
mod tests {
230+
use super::*;
231+
232+
#[test]
233+
fn encryption() {
234+
let auth1 = Key::new(b"secret1").unwrap();
235+
let auth2 = Key::new(b"secret2").unwrap();
236+
237+
let data = vec![1u8, 2, 3, 4, 5];
238+
let encrypted = auth1.seal(&data).unwrap();
239+
240+
let mut encrypted1 = encrypted.clone();
241+
let decrypted = auth1.open_in_place(&mut encrypted1).unwrap();
242+
243+
assert_eq!(data, decrypted);
244+
245+
let mut encrypted2 = encrypted;
246+
let decrypted = auth2.open_in_place(&mut encrypted2);
247+
248+
assert!(decrypted.is_err());
249+
}
250+
}

0 commit comments

Comments
 (0)