Skip to main content

renox_core/db/
encrypted.rs

1//! `Encrypted<T>`: a model field stored encrypted with `APP_KEY`.
2
3use std::cell::RefCell;
4use std::fmt;
5use std::ops::{Deref, DerefMut};
6use std::sync::Arc;
7
8use cookie::Key;
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11
12use super::{DbValue, ToDbValue};
13
14/// A field stored encrypted (AES-256-GCM under `APP_KEY`), read and written
15/// as a plain `T` in Rust: a national id number, a bank account, a
16/// third-party API secret. Laravel's `encrypted` cast.
17///
18/// ```
19/// # use renox::prelude::*;
20/// use renox::db::Encrypted;
21///
22/// #[derive(Model, serde::Serialize, Default)]
23/// #[model(table = "suppliers")]
24/// struct Supplier {
25///     id: i64,
26///     name: String,
27///     bank_account: Encrypted<String>, // a TEXT column holding the sealed value
28///     api_key: Option<Encrypted<String>>,
29/// }
30///
31/// # async fn demo(db: Db) -> Result {
32/// let supplier = Supplier::create(&db, Supplier {
33///     name: "Corner Coffee".into(),
34///     bank_account: Encrypted::new("BCA 123-456-789".into()),
35///     ..Default::default()
36/// }).await?;
37/// assert_eq!(*supplier.bank_account, "BCA 123-456-789"); // `Deref` to the value
38/// # Ok(()) }
39/// ```
40///
41/// The value is sealed with the key of the [`Db`](super::Db) that writes it
42/// and opened with the key of the one that reads it (the app's `APP_KEY`,
43/// set at boot), so it works the same in handlers, jobs, commands, seeders
44/// and tests. Each write uses a fresh nonce, so the column can't be searched
45/// or indexed: look rows up by another column. Changing `APP_KEY` makes the
46/// values unreadable. `T` is stored as JSON, so any serde type works.
47///
48/// `Debug` prints `Encrypted(..)`, never the value; `Serialize` writes the
49/// plain value (for your JSON and templates, where you decide to show it).
50#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
51pub struct Encrypted<T>(T);
52
53impl<T> Encrypted<T> {
54    /// Wraps a plain value; it is encrypted when the model is saved.
55    pub fn new(value: T) -> Self {
56        Self(value)
57    }
58
59    /// The plain value.
60    pub fn into_inner(self) -> T {
61        self.0
62    }
63}
64
65impl<T> From<T> for Encrypted<T> {
66    fn from(value: T) -> Self {
67        Self(value)
68    }
69}
70
71impl<T> Deref for Encrypted<T> {
72    type Target = T;
73
74    fn deref(&self) -> &T {
75        &self.0
76    }
77}
78
79impl<T> DerefMut for Encrypted<T> {
80    fn deref_mut(&mut self) -> &mut T {
81        &mut self.0
82    }
83}
84
85impl<T> fmt::Debug for Encrypted<T> {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.write_str("Encrypted(..)")
88    }
89}
90
91impl<T: Serialize> Serialize for Encrypted<T> {
92    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
93        self.0.serialize(serializer)
94    }
95}
96
97impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Encrypted<T> {
98    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
99        T::deserialize(deserializer).map(Self)
100    }
101}
102
103impl<T: Serialize> ToDbValue for Encrypted<T> {
104    /// The plain value as JSON; the statement seals it with its database's
105    /// key when it runs.
106    fn to_db_value(&self) -> DbValue {
107        DbValue::Encrypted(Unsealed(serde_json::to_string(&self.0).unwrap_or_default()))
108    }
109}
110
111/// The plain value of an [`Encrypted`] field on its way to the database,
112/// in [`DbValue::Encrypted`]. Its `Debug` doesn't show it.
113#[derive(Clone, PartialEq)]
114pub struct Unsealed(pub(crate) String);
115
116impl fmt::Debug for Unsealed {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.write_str("..")
119    }
120}
121
122/// Associated data for sealed columns, so a sealed column can't pass as an
123/// encrypted cookie or a `state.encrypt` value, or the other way round.
124const COLUMN: &str = "renox.column";
125
126/// Seals a column value (for statements, just before they run).
127pub(crate) fn seal(key: &Key, plain: &str) -> String {
128    let mut jar = cookie::CookieJar::new();
129    jar.private_mut(key)
130        .add(cookie::Cookie::new(COLUMN, plain.to_owned()));
131    jar.get(COLUMN)
132        .map(|sealed| sealed.value().to_owned())
133        .unwrap_or_default()
134}
135
136fn open(key: &Key, sealed: &str) -> Option<String> {
137    cookie::CookieJar::new()
138        .private(key)
139        .decrypt(cookie::Cookie::new(COLUMN, sealed.to_owned()))
140        .map(|plain| plain.value().to_owned())
141}
142
143thread_local! {
144    /// The key of the row being read, while `Row::try_get` decodes a column.
145    static READING: RefCell<Option<Arc<Key>>> = const { RefCell::new(None) };
146}
147
148/// Runs `decode` with `key` as the one `Encrypted` columns are opened with.
149pub(crate) fn reading<R>(key: Option<&Arc<Key>>, decode: impl FnOnce() -> R) -> R {
150    let previous = READING.with(|current| current.replace(key.cloned()));
151    let result = decode();
152    READING.with(|current| *current.borrow_mut() = previous);
153    result
154}
155
156fn decode_sealed<T: DeserializeOwned>(
157    sealed: &str,
158) -> Result<Encrypted<T>, sqlx::error::BoxDynError> {
159    let key = READING
160        .with(|current| current.borrow().clone())
161        .ok_or("an Encrypted column was read without a key: read it through the app's Db")?;
162    let plain = open(&key, sealed).ok_or(
163        "an Encrypted column can't be decrypted with this APP_KEY (changed, or another key)",
164    )?;
165    Ok(Encrypted(serde_json::from_str(&plain)?))
166}
167
168macro_rules! encrypted_column {
169    ($db:ty) => {
170        impl<T> sqlx::Type<$db> for Encrypted<T> {
171            fn type_info() -> <$db as sqlx::Database>::TypeInfo {
172                <String as sqlx::Type<$db>>::type_info()
173            }
174
175            fn compatible(ty: &<$db as sqlx::Database>::TypeInfo) -> bool {
176                <String as sqlx::Type<$db>>::compatible(ty)
177            }
178        }
179
180        impl<'r, T: DeserializeOwned> sqlx::Decode<'r, $db> for Encrypted<T> {
181            fn decode(
182                value: <$db as sqlx::Database>::ValueRef<'r>,
183            ) -> Result<Self, sqlx::error::BoxDynError> {
184                let sealed = <String as sqlx::Decode<$db>>::decode(value)?;
185                decode_sealed(&sealed)
186            }
187        }
188    };
189}
190
191encrypted_column!(sqlx::sqlite::Sqlite);
192#[cfg(feature = "postgres")]
193encrypted_column!(sqlx::postgres::Postgres);
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn sealed_values_open_with_their_key_only() {
201        let key = Key::generate();
202        let sealed = seal(&key, "\"BCA 123\"");
203        assert!(!sealed.contains("BCA"));
204        assert_ne!(sealed, seal(&key, "\"BCA 123\""), "a fresh nonce each time");
205        let read = reading(Some(&Arc::new(key)), || decode_sealed::<String>(&sealed));
206        assert_eq!(read.unwrap().into_inner(), "BCA 123");
207        let other = reading(Some(&Arc::new(Key::generate())), || {
208            decode_sealed::<String>(&sealed)
209        });
210        assert!(other.is_err());
211        assert!(reading(None, || decode_sealed::<String>(&sealed)).is_err());
212        assert_eq!(format!("{:?}", Encrypted::new("secret")), "Encrypted(..)");
213    }
214}