Skip to main content

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(&current), 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(&current, &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;