Skip to main content

rainier_crypt/
lib.rs

1//! # rainier-crypt
2//!
3//! Reversible encryption and message signing — Laravel's `Crypt`, plus the
4//! signing half that Laravel spreads across signed URLs and cookie
5//! integrity.
6//!
7//! ```
8//! use rainier_crypt::{Encryption, Key, KeyRing};
9//!
10//! # fn main() -> rainier_support::Result<()> {
11//! let crypt = Encryption::from_keys(KeyRing::new(Key::generate()));
12//!
13//! let sealed = crypt.encrypt("a card number")?;
14//! assert_eq!(crypt.decrypt(&sealed)?, "a card number");
15//!
16//! // Signed, not sealed: the value stays readable, but not writable.
17//! let signed = crypt.sign("unsubscribe-42")?;
18//! assert!(signed.starts_with("unsubscribe-42."));
19//! assert_eq!(crypt.verify(&signed)?, "unsubscribe-42");
20//! # Ok(()) }
21//! ```
22//!
23//! ## Encrypt or sign?
24//!
25//! | | Hidden | Tamper-evident | Use for |
26//! |---|---|---|---|
27//! | [`encrypt`](Encryption::encrypt) | yes | yes | anything the client must not read |
28//! | [`sign`](Encryption::sign) | no | yes | anything the client may read but not change |
29//!
30//! Reach for signing when the value is not a secret. An unsubscribe link, a
31//! reset token, a "remember this choice" cookie — encrypting those makes them
32//! opaque in your own logs for no gain.
33//!
34//! ## This is not password hashing
35//!
36//! Passwords are hashed, never encrypted, and that lives in `rainier-auth`'s
37//! `Hasher`. If you can get the value back out, it is the wrong tool for a
38//! password — see the [hashing docs](https://github.com/safewords/rainier-framework/blob/main/docs/hashing.md).
39//!
40//! ## Rotation is designed in
41//!
42//! Every payload records which key produced it, and a [`KeyRing`] holds one
43//! current key plus any number of retired ones. Rotating is a deploy:
44//!
45//! ```
46//! # use rainier_crypt::{Encryption, Key, KeyRing};
47//! # fn main() -> rainier_support::Result<()> {
48//! # let retired = Key::generate();
49//! # let sealed = Encryption::from_keys(KeyRing::new(retired.clone())).encrypt("old")?;
50//! let crypt = Encryption::from_keys(
51//!     KeyRing::new(Key::generate()).with_previous(retired),
52//! );
53//!
54//! // Written with the old key, still readable; anything new uses the new one.
55//! assert_eq!(crypt.decrypt(&sealed)?, "old");
56//! # Ok(()) }
57//! ```
58//!
59//! Retrofitting that is painful, which is why it is here from the first
60//! release rather than deferred until a key is known to have leaked.
61
62#![deny(missing_docs)]
63#![deny(rustdoc::broken_intra_doc_links)]
64
65pub mod asymmetric;
66pub mod cipher;
67pub mod encrypter;
68#[cfg(feature = "jwt")]
69pub mod jwt;
70pub mod key;
71pub mod laravel;
72pub mod signer;
73pub mod url;
74
75pub use asymmetric::{
76    BoxKeyPair, BoxPublicKey, Ed25519Signer, SealedBox, SigningKeyPair, VerifyingPublicKey,
77};
78pub use cipher::Cipher;
79pub use encrypter::{AeadEncrypter, Encrypter, EncrypterExt};
80#[cfg(feature = "jwt")]
81pub use jwt::{Jwt, JwtAlgorithm, JwtKey, JwtKeyRing};
82pub use key::{Key, KeyRing, KEY_LEN};
83pub use laravel::{CryptScheme, LaravelEncrypter};
84pub use signer::{HmacSigner, Signer, SignerExt};
85pub use url::UrlSigner;
86
87use std::sync::Arc;
88
89use rainier_support::Result;
90use serde::de::DeserializeOwned;
91use serde::Serialize;
92
93/// The application's encrypter and signer, as one container-storable value.
94///
95/// A newtype over the two ports rather than binding them separately, so
96/// swapping either does not change a call site — the same reason
97/// `rainier-framework`'s `Views` wraps a view engine.
98#[derive(Clone)]
99pub struct Encryption {
100    encrypter: Arc<dyn Encrypter>,
101    signer: Arc<dyn Signer>,
102    /// The ring both were built from, when it is known.
103    ///
104    /// `None` for [`new`](Self::new), which takes two already-built halves and
105    /// cannot see inside them. Everything that needs the ring itself — signing
106    /// URLs, for one — has to cope with not getting it.
107    keys: Option<KeyRing>,
108}
109
110impl Encryption {
111    /// Wrap an encrypter and a signer.
112    pub fn new(encrypter: Arc<dyn Encrypter>, signer: Arc<dyn Signer>) -> Self {
113        Self { encrypter, signer, keys: None }
114    }
115
116    /// The default pairing — XChaCha20-Poly1305 and HMAC-SHA256 over one ring.
117    pub fn from_keys(keys: KeyRing) -> Self {
118        Self::from_keys_with(keys, Cipher::default())
119    }
120
121    /// The same, with an explicit [`Cipher`].
122    ///
123    /// Reading is unaffected: a payload names its own algorithm, so switching
124    /// this does not strand anything already written.
125    pub fn from_keys_with(keys: KeyRing, cipher: Cipher) -> Self {
126        Self {
127            encrypter: Arc::new(AeadEncrypter::new(keys.clone()).with_cipher(cipher)),
128            signer: Arc::new(HmacSigner::new(keys.clone())),
129            keys: Some(keys),
130        }
131    }
132
133    /// The key ring, when this was built from one.
134    ///
135    /// `None` when the encrypter and signer were supplied separately — see
136    /// [`new`](Self::new). A caller that needs a ring of its own should ask
137    /// for one rather than assume.
138    pub fn keys(&self) -> Option<&KeyRing> {
139        self.keys.as_ref()
140    }
141
142    /// The encrypter.
143    pub fn encrypter(&self) -> &Arc<dyn Encrypter> {
144        &self.encrypter
145    }
146
147    /// The signer.
148    pub fn signer(&self) -> &Arc<dyn Signer> {
149        &self.signer
150    }
151
152    /// Encrypt a string.
153    pub fn encrypt(&self, plain: &str) -> Result<String> {
154        self.encrypter.encrypt(plain)
155    }
156
157    /// Decrypt a string.
158    pub fn decrypt(&self, payload: &str) -> Result<String> {
159        self.encrypter.decrypt(payload)
160    }
161
162    /// Encrypt raw bytes.
163    pub fn encrypt_bytes(&self, plain: &[u8]) -> Result<String> {
164        self.encrypter.encrypt_bytes(plain)
165    }
166
167    /// Decrypt to raw bytes.
168    pub fn decrypt_bytes(&self, payload: &str) -> Result<Vec<u8>> {
169        self.encrypter.decrypt_bytes(payload)
170    }
171
172    /// Encrypt any serialisable value as JSON.
173    pub fn encrypt_json<T: Serialize>(&self, value: &T) -> Result<String> {
174        self.encrypter.encrypt_json(value)
175    }
176
177    /// Decrypt JSON back into a value.
178    pub fn decrypt_json<T: DeserializeOwned>(&self, payload: &str) -> Result<T> {
179        self.encrypter.decrypt_json(payload)
180    }
181
182    /// Sign a value, leaving it readable.
183    pub fn sign(&self, value: &str) -> Result<String> {
184        self.signer.sign(value)
185    }
186
187    /// Recover a signed value, or fail if it was altered.
188    pub fn verify(&self, signed: &str) -> Result<String> {
189        self.signer.verify(signed)
190    }
191
192    /// Whether a signed value is intact.
193    pub fn is_valid(&self, signed: &str) -> bool {
194        self.signer.is_valid(signed)
195    }
196}
197
198impl std::fmt::Debug for Encryption {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        f.write_str("Encryption(..)")
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn the_facade_value_does_both_halves() {
210        let crypt = Encryption::from_keys(KeyRing::new(Key::generate()));
211
212        assert_eq!(crypt.decrypt(&crypt.encrypt("sealed").unwrap()).unwrap(), "sealed");
213        assert_eq!(crypt.verify(&crypt.sign("signed").unwrap()).unwrap(), "signed");
214    }
215
216    #[test]
217    fn json_round_trips_through_the_facade_value() {
218        let crypt = Encryption::from_keys(KeyRing::new(Key::generate()));
219        let payload = crypt.encrypt_json(&vec![1u8, 2, 3]).unwrap();
220
221        assert_eq!(crypt.decrypt_json::<Vec<u8>>(&payload).unwrap(), vec![1, 2, 3]);
222    }
223
224    #[test]
225    fn debug_does_not_disclose_anything() {
226        let crypt = Encryption::from_keys(KeyRing::new(Key::from_bytes([3u8; KEY_LEN])));
227        assert_eq!(format!("{crypt:?}"), "Encryption(..)");
228    }
229}