ocre/encryption.rs
1//! Attribute encryption for model columns: values are encrypted in Rust
2//! before they reach D1 and decrypted when rows are read, like Rails'
3//! `encrypts`.
4//!
5//! Keys are derived from [`SECRET_KEY_BASE`](crate::SECRET_KEY_BASE) with
6//! HKDF-SHA256 (separate from the session cookie key), and the cipher is
7//! AES-256-GCM, so a changed ciphertext fails to decrypt instead of giving a
8//! wrong value. A value is stored as text: `v1:` then the nonce and the
9//! ciphertext in URL-safe base64 (about 4/3 of the value plus 42
10//! characters).
11//!
12//! - [`Encrypted`]: a random nonce per write. The same value never gives the
13//! same text twice, so the column cannot be searched.
14//! - [`Deterministic`]: the nonce comes from the value (HMAC-SHA256), so
15//! equal values give equal texts: `WHERE email = ?1` and unique indexes
16//! work, at the price of revealing which rows share a value. Normalize
17//! before encrypting (e.g. lowercase an email) for case-insensitive lookups.
18//!
19//! Both are field types for a model's row struct: they deserialize by
20//! decrypting, bind as parameters by encrypting, and serialize to JSON as
21//! the plain value (see the Models guide, "Encrypted columns").
22//!
23//! # Keys and rotation
24//!
25//! [`Ctx`](crate::Ctx) installs the keys the first time a Worker instance
26//! handles a request: one HKDF derivation, then pure Rust (AES-GCM of a short
27//! value costs microseconds of CPU). Values encrypted with a secret listed in
28//! [`SECRET_KEY_BASE_PREVIOUS`](crate::SECRET_KEY_BASE_PREVIOUS) still
29//! decrypt; writes use the current secret. Deterministic lookups must try
30//! every key during a rotation ([`Encryptor::deterministic_candidates`] with
31//! `Query::is_in`), until a job has rewritten the rows with the current key.
32//! Losing `SECRET_KEY_BASE` loses the data: back it up.
33
34use std::{cell::RefCell, fmt};
35
36use aes_gcm::{
37 Aes256Gcm, KeyInit, Nonce,
38 aead::{Aead, Payload},
39};
40use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
41use hmac::{Hmac, Mac};
42use serde::{Deserialize, Deserializer, Serialize, Serializer};
43use sha2::Sha256;
44
45use crate::{Error, IntoParam, Param, Result, session};
46
47/// Prefix of every encrypted value: the format version.
48const PREFIX: &str = "v1:";
49const NONCE_BYTES: usize = 12;
50
51/// One secret's keys: the cipher and the HMAC key of deterministic nonces.
52#[derive(Clone)]
53struct Key {
54 cipher: Aes256Gcm,
55 nonce_key: [u8; 32],
56}
57
58impl Key {
59 fn derive(secret: &str) -> Self {
60 let hkdf = hkdf::Hkdf::<Sha256>::new(None, secret.as_bytes());
61 let mut cipher_key = [0u8; 32];
62 let mut nonce_key = [0u8; 32];
63 hkdf.expand(b"ocre attribute encryption: AES-256-GCM", &mut cipher_key).expect("32 bytes is a valid length");
64 hkdf.expand(b"ocre attribute encryption: deterministic nonce", &mut nonce_key)
65 .expect("32 bytes is a valid length");
66 Self { cipher: Aes256Gcm::new(&cipher_key.into()), nonce_key }
67 }
68
69 fn seal(&self, nonce: [u8; NONCE_BYTES], plaintext: &str) -> String {
70 let payload = Payload { msg: plaintext.as_bytes(), aad: PREFIX.as_bytes() };
71 let ciphertext = self.cipher.encrypt(Nonce::from_slice(&nonce), payload).expect("AES-GCM encrypts any length");
72 let mut bytes = nonce.to_vec();
73 bytes.extend(ciphertext);
74 format!("{PREFIX}{}", URL_SAFE_NO_PAD.encode(bytes))
75 }
76
77 fn deterministic_nonce(&self, plaintext: &str) -> [u8; NONCE_BYTES] {
78 let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(&self.nonce_key).expect("HMAC takes any key length");
79 mac.update(plaintext.as_bytes());
80 let digest = mac.finalize().into_bytes();
81 digest[..NONCE_BYTES].try_into().expect("SHA-256 is longer than a nonce")
82 }
83
84 fn open(&self, nonce: &[u8], ciphertext: &[u8]) -> Option<String> {
85 let payload = Payload { msg: ciphertext, aad: PREFIX.as_bytes() };
86 let plaintext = self.cipher.decrypt(Nonce::from_slice(nonce), payload).ok()?;
87 String::from_utf8(plaintext).ok()
88 }
89}
90
91/// Encrypts and decrypts column values with keys derived from `SECRET_KEY_BASE`.
92///
93/// Models use it through [`Encrypted`] and [`Deterministic`]; use it
94/// directly for values outside a model (a job argument, an API token to call
95/// another service).
96///
97/// # Examples
98///
99/// ```
100/// use ocre::encryption::Encryptor;
101///
102/// let secret = "a".repeat(64);
103/// let encryptor = Encryptor::new(&secret, &[]).unwrap();
104/// let stored = encryptor.encrypt("123-45-6789");
105/// assert!(stored.starts_with("v1:"));
106/// assert_ne!(stored, encryptor.encrypt("123-45-6789")); // a random nonce each time
107/// assert_eq!(encryptor.decrypt(&stored).unwrap(), "123-45-6789");
108/// // Deterministic: the same text for the same value, so it can be looked up.
109/// assert_eq!(encryptor.encrypt_deterministic("ada@example.com"), encryptor.encrypt_deterministic("ada@example.com"));
110/// ```
111#[derive(Clone)]
112pub struct Encryptor {
113 current: Key,
114 previous: Vec<Key>,
115}
116
117impl fmt::Debug for Encryptor {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 f.debug_struct("Encryptor").field("previous_keys", &self.previous.len()).finish_non_exhaustive()
120 }
121}
122
123impl Encryptor {
124 /// Derives the keys from the current secret and the previous ones (newest first).
125 ///
126 /// Each secret must be 64 characters or more, like `SECRET_KEY_BASE`.
127 ///
128 /// # Errors
129 ///
130 /// [`Error::Internal`] naming the fix when a secret is too short.
131 ///
132 /// # Examples
133 ///
134 /// ```
135 /// use ocre::encryption::Encryptor;
136 ///
137 /// let (old, new) = ("o".repeat(64), "n".repeat(64));
138 /// let stored = Encryptor::new(&old, &[]).unwrap().encrypt("secret");
139 /// let rotated = Encryptor::new(&new, &[old.as_str()]).unwrap();
140 /// assert_eq!(rotated.decrypt(&stored).unwrap(), "secret");
141 /// assert!(Encryptor::new("short", &[]).is_err());
142 /// ```
143 pub fn new(secret: &str, previous: &[&str]) -> Result<Self> {
144 let current = session::checked_secret(Some(secret.to_owned())).map_err(Error::internal)?;
145 let previous = previous
146 .iter()
147 .map(|secret| session::checked_secret(Some((*secret).to_owned())).map(|secret| Key::derive(&secret)))
148 .collect::<std::result::Result<Vec<_>, _>>()
149 .map_err(Error::internal)?;
150 Ok(Self { current: Key::derive(¤t), previous })
151 }
152
153 /// Encrypts `plaintext` with a random nonce: `v1:` plus URL-safe base64.
154 ///
155 /// # Examples
156 ///
157 /// ```
158 /// let encryptor = ocre::encryption::Encryptor::new(&"k".repeat(64), &[]).unwrap();
159 /// assert_eq!(encryptor.decrypt(&encryptor.encrypt("")).unwrap(), "");
160 /// ```
161 pub fn encrypt(&self, plaintext: &str) -> String {
162 self.current.seal(crate::token::random_bytes(), plaintext)
163 }
164
165 /// Encrypts `plaintext` so that equal values give equal texts (see [`Deterministic`]).
166 ///
167 /// # Examples
168 ///
169 /// ```
170 /// let encryptor = ocre::encryption::Encryptor::new(&"k".repeat(64), &[]).unwrap();
171 /// let a = encryptor.encrypt_deterministic("ada@example.com");
172 /// assert_ne!(a, encryptor.encrypt_deterministic("bob@example.com"));
173 /// assert_eq!(encryptor.decrypt(&a).unwrap(), "ada@example.com");
174 /// ```
175 pub fn encrypt_deterministic(&self, plaintext: &str) -> String {
176 self.current.seal(self.current.deterministic_nonce(plaintext), plaintext)
177 }
178
179 /// The deterministic texts of `plaintext` under every key, current first:
180 /// look rows up with `Query::is_in` during a key rotation.
181 ///
182 /// # Examples
183 ///
184 /// ```
185 /// use ocre::encryption::Encryptor;
186 ///
187 /// let (old, new) = ("o".repeat(64), "n".repeat(64));
188 /// let stored = Encryptor::new(&old, &[]).unwrap().encrypt_deterministic("ada@example.com");
189 /// let rotated = Encryptor::new(&new, &[old.as_str()]).unwrap();
190 /// let candidates = rotated.deterministic_candidates("ada@example.com");
191 /// assert_eq!(candidates.len(), 2);
192 /// assert!(candidates.contains(&stored));
193 /// ```
194 pub fn deterministic_candidates(&self, plaintext: &str) -> Vec<String> {
195 std::iter::once(&self.current)
196 .chain(&self.previous)
197 .map(|key| key.seal(key.deterministic_nonce(plaintext), plaintext))
198 .collect()
199 }
200
201 /// Decrypts a value from [`encrypt`](Self::encrypt) or
202 /// [`encrypt_deterministic`](Self::encrypt_deterministic), with the
203 /// current key or a previous one.
204 ///
205 /// # Errors
206 ///
207 /// [`Error::Internal`] when `ciphertext` is not an encrypted value, was
208 /// changed, or was encrypted with a key that is neither current nor
209 /// previous. The message never contains the value.
210 ///
211 /// # Examples
212 ///
213 /// ```
214 /// let encryptor = ocre::encryption::Encryptor::new(&"k".repeat(64), &[]).unwrap();
215 /// assert!(encryptor.decrypt("plain text").is_err());
216 /// ```
217 pub fn decrypt(&self, ciphertext: &str) -> Result<String> {
218 let failed = |why: &str| Error::internal(format!("cannot decrypt an encrypted column: {why}"));
219 let encoded = ciphertext.strip_prefix(PREFIX).ok_or_else(|| failed("the value is not encrypted"))?;
220 let bytes = URL_SAFE_NO_PAD.decode(encoded).map_err(|_| failed("the value is not valid base64"))?;
221 if bytes.len() < NONCE_BYTES {
222 return Err(failed("the value is too short"));
223 }
224 let (nonce, sealed) = bytes.split_at(NONCE_BYTES);
225 std::iter::once(&self.current).chain(&self.previous).find_map(|key| key.open(nonce, sealed)).ok_or_else(|| {
226 failed("wrong key or changed value (SECRET_KEY_BASE changed without SECRET_KEY_BASE_PREVIOUS?)")
227 })
228 }
229
230 /// Like [`decrypt`](Self::decrypt), but a value without the `v1:` prefix
231 /// is returned as is: read a column while a data migration encrypts its
232 /// existing rows (Rails' `support_unencrypted_data`).
233 ///
234 /// # Errors
235 ///
236 /// [`Error::Internal`] when an encrypted value does not decrypt.
237 ///
238 /// # Examples
239 ///
240 /// ```
241 /// let encryptor = ocre::encryption::Encryptor::new(&"k".repeat(64), &[]).unwrap();
242 /// assert_eq!(encryptor.decrypt_or_plaintext("not yet encrypted").unwrap(), "not yet encrypted");
243 /// ```
244 pub fn decrypt_or_plaintext(&self, text: &str) -> Result<String> {
245 if is_encrypted(text) { self.decrypt(text) } else { Ok(text.to_owned()) }
246 }
247}
248
249/// Whether `text` looks like an encrypted value (starts with `v1:`).
250///
251/// # Examples
252///
253/// ```
254/// assert!(!ocre::encryption::is_encrypted("hello"));
255/// ```
256pub fn is_encrypted(text: &str) -> bool {
257 text.starts_with(PREFIX)
258}
259
260thread_local! {
261 /// The Worker instance's keys. WebAssembly Workers have one thread;
262 /// native tests get one per test thread.
263 static INSTALLED: RefCell<Option<Encryptor>> = const { RefCell::new(None) };
264}
265
266/// Makes `encryptor` the one [`Encrypted`] and [`Deterministic`] use.
267///
268/// [`Ctx`](crate::Ctx) calls it with the keys of `SECRET_KEY_BASE`; call it
269/// yourself in tests of code that reads encrypted columns.
270///
271/// # Examples
272///
273/// ```
274/// use ocre::encryption::{self, Encrypted, Encryptor};
275///
276/// encryption::install(Encryptor::new(&"t".repeat(64), &[]).unwrap());
277/// let stored = encryption::installed().unwrap().encrypt("42");
278/// let value: Encrypted = serde_json::from_value(serde_json::json!(stored)).unwrap();
279/// assert_eq!(value.as_str(), "42");
280/// ```
281pub fn install(encryptor: Encryptor) {
282 INSTALLED.with(|installed| *installed.borrow_mut() = Some(encryptor));
283}
284
285/// The installed encryptor.
286///
287/// # Errors
288///
289/// [`Error::Internal`] when none is installed: the Worker has no valid
290/// `SECRET_KEY_BASE` (the message names the fix), or native code did not
291/// call [`install`].
292///
293/// # Examples
294///
295/// ```
296/// use ocre::encryption::{self, Encryptor};
297///
298/// encryption::install(Encryptor::new(&"t".repeat(64), &[]).unwrap());
299/// assert!(encryption::installed().is_ok());
300/// ```
301pub fn installed() -> Result<Encryptor> {
302 INSTALLED.with(|installed| installed.borrow().clone()).ok_or_else(|| {
303 Error::internal(
304 "no encryption key: SECRET_KEY_BASE is missing or shorter than 64 characters. \
305 Fix: `ocre secret` and put it in .dev.vars (local) or let `ocre deploy` create it (production)",
306 )
307 })
308}
309
310/// Installs the keys from the Worker's secrets unless already done.
311pub(crate) fn ensure_installed(secret: &dyn Fn(&str) -> Option<String>) {
312 if INSTALLED.with(|installed| installed.borrow().is_some()) {
313 return;
314 }
315 let Ok(current) = session::checked_secret(secret(session::SECRET_KEY_BASE)) else { return };
316 let Ok(previous) = session::previous_secrets(secret(session::SECRET_KEY_BASE_PREVIOUS)) else { return };
317 let previous: Vec<&str> = previous.iter().map(String::as_str).collect();
318 if let Ok(encryptor) = Encryptor::new(¤t, &previous) {
319 install(encryptor);
320 }
321}
322
323fn decrypt_for_serde<'de, D: Deserializer<'de>>(deserializer: D) -> std::result::Result<String, D::Error> {
324 let text = String::deserialize(deserializer)?;
325 let encryptor = installed().map_err(serde::de::Error::custom)?;
326 encryptor.decrypt(&text).map_err(serde::de::Error::custom)
327}
328
329/// A column encrypted with a random nonce: reads decrypt, writes encrypt.
330///
331/// Use it as the type of an encrypted column in the row struct: reading a
332/// row decrypts the stored text with the [`installed`] keys (a failure is a
333/// deserialization error, so the query fails with a 500). Input structs
334/// (`New`/`Changes`, forms, JSON bodies) keep a plain `String` and the
335/// model binds `Encrypted::from(value)`, which stores a new ciphertext.
336/// Serializing (JSON responses, templates) writes the plain value; `Debug`
337/// hides it.
338///
339/// # Examples
340///
341/// ```
342/// use ocre::{IntoParam, encryption::{self, Encrypted, Encryptor}};
343///
344/// encryption::install(Encryptor::new(&"t".repeat(64), &[]).unwrap());
345/// let ssn = Encrypted::from("123-45-6789");
346/// assert_eq!(serde_json::to_string(&ssn).unwrap(), r#""123-45-6789""#);
347/// assert_ne!(ssn.clone().into_param(), "123-45-6789".into_param());
348/// assert_eq!(format!("{ssn:?}"), "Encrypted(..)");
349/// ```
350#[derive(Clone, PartialEq, Eq)]
351pub struct Encrypted(pub String);
352
353/// A column encrypted deterministically: equal values give equal stored
354/// texts, so `query().eq("email", Deterministic::from(email))` finds the row
355/// and a `UNIQUE` index works.
356///
357/// Same reading and serializing rules as [`Encrypted`]. Equal values being
358/// visible as equal texts is the trade-off: use it only for columns you
359/// must look up.
360///
361/// # Examples
362///
363/// ```
364/// use ocre::{IntoParam, encryption::{self, Deterministic, Encryptor}};
365///
366/// encryption::install(Encryptor::new(&"t".repeat(64), &[]).unwrap());
367/// let a = Deterministic::from("ada@example.com").into_param();
368/// assert_eq!(a, Deterministic::from("ada@example.com").into_param());
369/// ```
370#[derive(Clone, PartialEq, Eq)]
371pub struct Deterministic(pub String);
372
373macro_rules! encrypted_type {
374 ($name:ident, $encrypt:ident) => {
375 impl $name {
376 /// The plain value.
377 ///
378 /// # Examples
379 ///
380 /// ```
381 #[doc = concat!("assert_eq!(ocre::encryption::", stringify!($name), "::from(\"x\").as_str(), \"x\");")]
382 /// ```
383 pub fn as_str(&self) -> &str {
384 &self.0
385 }
386 }
387
388 impl From<&str> for $name {
389 fn from(value: &str) -> Self {
390 Self(value.to_owned())
391 }
392 }
393
394 impl From<String> for $name {
395 fn from(value: String) -> Self {
396 Self(value)
397 }
398 }
399
400 impl fmt::Debug for $name {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 f.write_str(concat!(stringify!($name), "(..)"))
403 }
404 }
405
406 impl fmt::Display for $name {
407 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408 f.write_str(&self.0)
409 }
410 }
411
412 impl Serialize for $name {
413 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
414 serializer.serialize_str(&self.0)
415 }
416 }
417
418 impl<'de> Deserialize<'de> for $name {
419 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
420 decrypt_for_serde(deserializer).map(Self)
421 }
422 }
423
424 /// Binds the encrypted text. Without installed keys (no valid
425 /// `SECRET_KEY_BASE`) it binds `NULL`, which a `NOT NULL` column
426 /// refuses; the error is logged.
427 impl IntoParam for $name {
428 fn into_param(self) -> Param {
429 match installed() {
430 Ok(encryptor) => encryptor.$encrypt(&self.0).into_param(),
431 Err(err) => {
432 crate::error::log_internal(&err.to_string());
433 None::<String>.into_param()
434 }
435 }
436 }
437 }
438 };
439}
440
441encrypted_type!(Encrypted, encrypt);
442encrypted_type!(Deterministic, encrypt_deterministic);
443
444#[cfg(test)]
445#[path = "../tests/encryption.rs"]
446mod tests;