1#![allow(clippy::redundant_pub_crate)]
4
5use std::path::Path;
6use std::sync::Arc;
7
8use chacha20poly1305::aead::{Aead, KeyInit, Payload};
9use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
10use hkdf::Hkdf;
11use hmac::{Hmac, Mac};
12use sha2::Sha256;
13
14use crate::{ThingdError, ThingdResult};
15
16const MANIFEST: &[u8] = b"THINGD_ENCRYPTED_V1\n";
17const CHECK_PLAINTEXT: &[u8] = b"thingd encryption check v1";
18const FORMAT_VERSION: u8 = 1;
19
20pub trait KeyProvider: Send + Sync {
22 fn key(&self) -> ThingdResult<[u8; 32]>;
28}
29
30#[derive(Clone)]
32pub struct StaticKeyProvider {
33 key: [u8; 32],
34}
35
36impl StaticKeyProvider {
37 pub fn new(key: &[u8]) -> ThingdResult<Self> {
43 let key: [u8; 32] = key.try_into().map_err(|_| {
44 ThingdError::InvalidEncryptionKey("encryption key must be exactly 32 bytes".to_string())
45 })?;
46 Ok(Self { key })
47 }
48}
49
50impl KeyProvider for StaticKeyProvider {
51 fn key(&self) -> ThingdResult<[u8; 32]> {
52 Ok(self.key)
53 }
54}
55
56#[derive(Clone)]
58pub struct EncryptionConfig {
59 pub key_provider: Arc<dyn KeyProvider>,
61}
62
63impl EncryptionConfig {
64 pub fn from_key(key: &[u8]) -> ThingdResult<Self> {
70 Ok(Self {
71 key_provider: Arc::new(StaticKeyProvider::new(key)?),
72 })
73 }
74}
75
76#[derive(Clone)]
77#[allow(clippy::redundant_pub_crate)]
78pub(crate) struct StorageCrypto {
79 data_key: [u8; 32],
80 index_key: [u8; 32],
81}
82
83pub(crate) trait StorageCodec: Send + Sync {
85 fn encode_value(&self, domain: &str, value: &[u8]) -> ThingdResult<Vec<u8>>;
86 fn decode_value(&self, domain: &str, value: &[u8]) -> ThingdResult<Vec<u8>>;
87 fn encode_key(&self, domain: &str, key: &[u8]) -> Vec<u8>;
88 fn encode_scoped_key(&self, domain: &str, namespace: &[u8], suffix: &[u8]) -> Vec<u8>;
89 fn encode_scoped_prefix(&self, domain: &str, namespace: &[u8]) -> Vec<u8>;
90 fn encrypted(&self) -> bool;
91}
92
93pub(crate) struct RawStorageCodec;
94
95impl StorageCodec for RawStorageCodec {
96 fn encode_value(&self, _domain: &str, value: &[u8]) -> ThingdResult<Vec<u8>> {
97 Ok(value.to_vec())
98 }
99
100 fn decode_value(&self, _domain: &str, value: &[u8]) -> ThingdResult<Vec<u8>> {
101 Ok(value.to_vec())
102 }
103
104 fn encode_key(&self, _domain: &str, key: &[u8]) -> Vec<u8> {
105 key.to_vec()
106 }
107
108 fn encode_scoped_key(&self, _domain: &str, namespace: &[u8], suffix: &[u8]) -> Vec<u8> {
109 let mut key = Vec::with_capacity(namespace.len() + 1 + suffix.len());
110 key.extend_from_slice(namespace);
111 key.push(0);
112 key.extend_from_slice(suffix);
113 key
114 }
115
116 fn encode_scoped_prefix(&self, _domain: &str, namespace: &[u8]) -> Vec<u8> {
117 let mut prefix = namespace.to_vec();
118 prefix.push(0);
119 prefix
120 }
121
122 fn encrypted(&self) -> bool {
123 false
124 }
125}
126
127pub(crate) struct EncryptedStorageCodec {
128 crypto: StorageCrypto,
129}
130
131impl StorageCodec for EncryptedStorageCodec {
132 fn encode_value(&self, domain: &str, value: &[u8]) -> ThingdResult<Vec<u8>> {
133 self.crypto.encrypt(value, domain)
134 }
135
136 fn decode_value(&self, domain: &str, value: &[u8]) -> ThingdResult<Vec<u8>> {
137 self.crypto.decrypt(value, domain)
138 }
139
140 fn encode_key(&self, domain: &str, key: &[u8]) -> Vec<u8> {
141 self.crypto.hash_key(domain, key)
142 }
143
144 fn encode_scoped_key(&self, domain: &str, namespace: &[u8], suffix: &[u8]) -> Vec<u8> {
145 let mut key = self
146 .crypto
147 .hash_key(&format!("{domain}:namespace"), namespace);
148 key.extend_from_slice(&self.crypto.hash_key(&format!("{domain}:suffix"), suffix));
149 key
150 }
151
152 fn encode_scoped_prefix(&self, domain: &str, namespace: &[u8]) -> Vec<u8> {
153 self.crypto
154 .hash_key(&format!("{domain}:namespace"), namespace)
155 }
156
157 fn encrypted(&self) -> bool {
158 true
159 }
160}
161
162pub(crate) fn make_codec(crypto: Option<StorageCrypto>) -> Box<dyn StorageCodec> {
163 match crypto {
164 Some(crypto) => Box::new(EncryptedStorageCodec { crypto }),
165 None => Box::new(RawStorageCodec),
166 }
167}
168
169impl StorageCrypto {
170 pub(crate) fn open(
171 path: &Path,
172 config: Option<&EncryptionConfig>,
173 ) -> ThingdResult<Option<Self>> {
174 let marker = path.join(".thingd-encryption");
175 let exists = marker.exists();
176 if exists && config.is_none() {
177 return Err(ThingdError::EncryptionRequired(
178 "database requires an encryption key".to_string(),
179 ));
180 }
181
182 let Some(config) = config else {
183 return Ok(None);
184 };
185
186 let has_existing_data = path.is_dir()
187 && std::fs::read_dir(path)
188 .map_err(|e| ThingdError::Storage(e.to_string()))?
189 .next()
190 .transpose()
191 .map_err(|e| ThingdError::Storage(e.to_string()))?
192 .is_some();
193 if !exists && has_existing_data {
194 return Ok(None);
197 }
198
199 let key = config.key_provider.key()?;
200 let crypto = Self::from_key(key);
201
202 if exists {
203 let found = std::fs::read(&marker).map_err(|e| ThingdError::Storage(e.to_string()))?;
204 if found != MANIFEST {
205 return Err(ThingdError::UnsupportedEncryptionVersion(
206 "unknown encryption manifest".to_string(),
207 ));
208 }
209 let check_path = path.join(".thingd-encryption-check");
210 let check =
211 std::fs::read(&check_path).map_err(|e| ThingdError::Storage(e.to_string()))?;
212 let decrypted = crypto.decrypt(&check, "manifest")?;
213 if decrypted != CHECK_PLAINTEXT {
214 return Err(ThingdError::EncryptionAuthentication(
215 "encryption key authentication failed".to_string(),
216 ));
217 }
218 return Ok(Some(crypto));
219 }
220
221 std::fs::create_dir_all(path).map_err(|e| ThingdError::Storage(e.to_string()))?;
222 std::fs::write(&marker, MANIFEST).map_err(|e| ThingdError::Storage(e.to_string()))?;
223 std::fs::write(
224 path.join(".thingd-encryption-check"),
225 crypto.encrypt(CHECK_PLAINTEXT, "manifest")?,
226 )
227 .map_err(|e| ThingdError::Storage(e.to_string()))?;
228 Ok(Some(crypto))
229 }
230
231 fn from_key(key: [u8; 32]) -> Self {
232 let hk = Hkdf::<Sha256>::new(None, &key);
233 let mut data_key = [0; 32];
234 let mut index_key = [0; 32];
235 hk.expand(b"thingd/data/v1", &mut data_key)
236 .expect("fixed HKDF output");
237 hk.expand(b"thingd/index/v1", &mut index_key)
238 .expect("fixed HKDF output");
239 Self {
240 data_key,
241 index_key,
242 }
243 }
244
245 pub(crate) fn encrypt(&self, plaintext: &[u8], domain: &str) -> ThingdResult<Vec<u8>> {
246 let cipher = XChaCha20Poly1305::new(Key::from_slice(&self.data_key));
247 let mut nonce = [0_u8; 24];
248 getrandom::fill(&mut nonce).map_err(|e| ThingdError::Storage(e.to_string()))?;
249 let associated = format!("thingd:{FORMAT_VERSION}:{domain}");
250 let ciphertext = cipher
251 .encrypt(
252 XNonce::from_slice(&nonce),
253 Payload {
254 msg: plaintext,
255 aad: associated.as_bytes(),
256 },
257 )
258 .map_err(|_| ThingdError::EncryptionAuthentication("encryption failed".to_string()))?;
259 let mut output = Vec::with_capacity(1 + nonce.len() + ciphertext.len());
260 output.push(FORMAT_VERSION);
261 output.extend_from_slice(&nonce);
262 output.extend_from_slice(&ciphertext);
263 Ok(output)
264 }
265
266 pub(crate) fn decrypt(&self, ciphertext: &[u8], domain: &str) -> ThingdResult<Vec<u8>> {
267 if ciphertext.len() < 1 + 24 + 16 || ciphertext[0] != FORMAT_VERSION {
268 return Err(ThingdError::UnsupportedEncryptionVersion(
269 "invalid encrypted value envelope".to_string(),
270 ));
271 }
272 let cipher = XChaCha20Poly1305::new(Key::from_slice(&self.data_key));
273 let associated = format!("thingd:{FORMAT_VERSION}:{domain}");
274 cipher
275 .decrypt(
276 XNonce::from_slice(&ciphertext[1..25]),
277 Payload {
278 msg: &ciphertext[25..],
279 aad: associated.as_bytes(),
280 },
281 )
282 .map_err(|_| {
283 ThingdError::EncryptionAuthentication(
284 "encrypted value authentication failed".to_string(),
285 )
286 })
287 }
288
289 #[allow(dead_code)]
291 pub(crate) fn hash_key(&self, domain: &str, key: &[u8]) -> Vec<u8> {
292 let mut mac =
293 <Hmac<Sha256> as Mac>::new_from_slice(&self.index_key).expect("fixed HMAC key");
294 mac.update(domain.as_bytes());
295 mac.update(b"\0");
296 mac.update(key);
297 mac.finalize().into_bytes().to_vec()
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn round_trip_and_nonce_uniqueness() {
307 let crypto = StorageCrypto::from_key([7; 32]);
308 let first = crypto.encrypt(b"secret", "test").unwrap();
309 let second = crypto.encrypt(b"secret", "test").unwrap();
310 assert_ne!(first, second);
311 assert_eq!(crypto.decrypt(&first, "test").unwrap(), b"secret");
312 assert!(crypto.decrypt(&first, "other").is_err());
313 }
314
315 #[test]
316 fn tampering_and_wrong_key_fail_authentication() {
317 let crypto = StorageCrypto::from_key([1; 32]);
318 let mut value = crypto.encrypt(b"secret", "test").unwrap();
319 value[30] ^= 1;
320 assert!(matches!(
321 crypto.decrypt(&value, "test"),
322 Err(ThingdError::EncryptionAuthentication(_))
323 ));
324 let wrong = StorageCrypto::from_key([2; 32]);
325 let value = crypto.encrypt(b"secret", "test").unwrap();
326 assert!(matches!(
327 wrong.decrypt(&value, "test"),
328 Err(ThingdError::EncryptionAuthentication(_))
329 ));
330 }
331}