strong_box/
stem_strong_box.rs

1use std::{fmt::Debug, time::Duration};
2
3#[cfg(doc)]
4use super::StrongBox;
5use super::{Key, RotatingStrongBox, StaticStrongBox, kdf};
6
7/// A way to derive many [`StrongBox`]es from one set of keys.
8///
9/// Splitting different encryption usages to use different keys prevents accidental misuse,
10/// and reduces the chances of insecurity from overuse.  Rather than have to manage a whole
11/// bunch of keys, though, a [`StemStrongBox`] allows you to "derive" [`StrongBox`]es for
12/// different uses from a single "root" [`StemStrongBox`].
13///
14/// Let us say, for instance, that you have a typical web application.  You want to keep
15/// session data in cookies, but that needs to be encrypted to prevent disclosure and
16/// tampering.  You also have to encrypt the state data that you send to your OAuth providers,
17/// and you have a couple of database fields that are *super* sensitive, that you'd like to
18/// encrypt.
19///
20/// Traditionally, you'd need to have a set of keys for each of those uses -- which is an absolute pain.
21/// However, with the [`StemStrongBox`], you only need to manage one set of keys (the current
22/// encryption key, and any previous decryption keys that old data might still be encrypted
23/// under), and the other keys can all be *derived* from that one "root" set of keys.
24///
25/// We might have a "key hierarchy" that looks something like this:
26///
27/// ```text
28///                              +--------+
29///                              |  root  |
30///                              +--------+
31///                                /  |  \
32///                              /    |    \
33///                            /      |      \
34///                          /        |        \
35///                        /          |          \
36///                +=========+    +-------+    +=========+
37///                | cookies |    |  DB   |    |  OAuth  |
38///                +=========+    +-------+    +=========+
39///                                 /   \
40///                               /       \
41///                             /           \
42///                           /               \
43///                     +----------+      +-----------+
44///                     |  table1  |      |   table2  |
45///                     +----------+      +-----------+
46///                       /                 /       \
47///                     /                 /           \
48///                   /                 /               \
49///                 /                 /                   \
50///             +===========+    +===========+    +===========+
51///             | sensitive |    | sensitive |    | sensitive |
52///             |  column A |    |  column B |    |  column C |
53///             +===========+    +===========+    +===========+
54/// ```
55///
56/// In the above diagram, the boxes with `---` at top and bottom are [`StemStrongBox`]es, from
57/// which you can derive other StrongBoxes (any of [`StemStrongBox`], [`StaticStrongBox`], or [``RotatingStrongBox`]).
58/// The boxes with `===` at top and bottom are regular [`StrongBox`]es, and are the ones we use to
59/// do cryptography.
60///
61/// You deliberately cannot have a kind of [`StrongBox`] that can both perform encryption and key
62/// derivation, because it is a terrible idea, security wise, to use the same key for different
63/// purposes.  Through the power of Rust's type system, we can enforce that.
64///
65/// # Example
66///
67/// This is how you might setup the above "tree" of [`StrongBox`]es.
68///
69/// ```rust
70/// # use strong_box::{Error, StemStrongBox};
71/// # use std::time::Duration;
72/// # fn main() -> Result<(), Error> {
73/// # const WEEKLY: Duration = Duration::from_secs(7 * 24 * 3600);
74///
75/// // A couple of keys are always useful to have
76/// let old_key = strong_box::generate_key();
77/// let new_key = strong_box::generate_key();
78///
79/// // This is the basis of all our other boxes
80/// let root = StemStrongBox::new(new_key.clone(), [old_key, new_key]);
81///
82/// // This creates a RotatingStrongBox for secure cookie storage
83/// let cookies = root.derive_rotating("cookies", WEEKLY, 52);
84///
85/// // This is the OAuth provider state box
86/// let oauth = root.derive_static("OAuth");
87///
88/// // Then the great tree of DB column encryption boxes
89/// let db = root.derive_stem("DB");
90/// let table1 = db.derive_stem("table1");
91/// let table2 = db.derive_stem("table2");
92///
93/// let sensitive_column_a = table1.derive_static("sensitive column A");
94/// let sensitive_column_b = table2.derive_static("sensitive column B");
95/// let sensitive_column_c = table2.derive_static("sensitive column C");
96///
97/// // We can now call encrypt/decrypt on any of the boxes created by .derive or .derive_rotating, but
98/// // not any of the boxes created by derive_stem, as they are only for further derivation
99/// # Ok(())
100/// # }
101/// ```
102#[derive(Clone, Debug)]
103pub struct StemStrongBox {
104	encryption_key: Key,
105	decryption_keys: Vec<Key>,
106}
107
108impl StemStrongBox {
109	/// Create a new [`StemStrongBox`].
110	#[tracing::instrument(level = "debug", skip(enc_key, dec_keys))]
111	pub fn new(
112		enc_key: impl Into<Key> + Debug,
113		dec_keys: impl IntoIterator<Item = impl Into<Key>> + Debug,
114	) -> Self {
115		Self {
116			encryption_key: enc_key.into(),
117			decryption_keys: dec_keys.into_iter().map(|k| k.into()).collect(),
118		}
119	}
120
121	/// Derive a [`StaticStrongBox`] from the keys in this [`StemStrongBox`], for the specified purpose.
122	#[tracing::instrument(level = "debug")]
123	pub fn derive_static(&self, purpose: impl AsRef<[u8]> + Debug) -> StaticStrongBox {
124		let mut context: Vec<u8> = b"derive::".to_vec();
125		context.extend_from_slice(purpose.as_ref());
126		StaticStrongBox::new(
127			kdf::derive_key(&self.encryption_key, &context),
128			self.decryption_keys
129				.iter()
130				.map(|k| kdf::derive_key(k, &context)),
131		)
132	}
133
134	/// Derive a new [`StemStrongBox`] from the keys in this [`StemStrongBox`], for the specified
135	/// purpose.
136	#[tracing::instrument(level = "debug")]
137	pub fn derive_stem(&self, purpose: impl AsRef<[u8]> + Debug) -> StemStrongBox {
138		let mut context: Vec<u8> = b"derive_stem::".to_vec();
139		context.extend_from_slice(purpose.as_ref());
140		StemStrongBox::new(
141			kdf::derive_key(&self.encryption_key, &context),
142			self.decryption_keys
143				.iter()
144				.map(|k| kdf::derive_key(k, &context)),
145		)
146	}
147
148	/// Derive a new [`RotatingStrongBox`] from the keys in this [`StemStrongBox`], for the specified purpose.
149	///
150	/// For data that is only valid for a certain period of time, it can be convenient to
151	/// automatically "expire" old data by just forgetting the key that encrypted that data.  You
152	/// can also avoid the "many encryptions" vulnerability by periodically rotating the key that
153	/// is actually used for encryption.
154	///
155	/// This method creates a [`RotatingStrongBox`], a variant of the regular [`StrongBox`] which
156	/// changes the encryption key periodically, and can "look back" a certain number of rotations
157	/// to decrypt data that was encrypted with a key produced in a previous rotation period.  You
158	/// can always decrypt ciphertexts encrypted in the *current* time period, which is indicated
159	/// by a `backtrack` of `0`.
160	///
161	/// Bear in mind that rotation periods are non-overlapping.  With a `backtrack` of `0`,
162	/// a ciphertext created at the very end of a rotation period will only be decryptable for
163	/// as little as a nanosecond before the key is expired.  Thus the minimum *practical* value
164	/// for `backtrack` is probably `1` in almost all cases.
165	///
166	/// # Example
167	///
168	/// Let's say you're encrypting an authentication cookie, and because you're encrypting so many
169	/// cookies, you want to rotate the key every week.  However, you allow users to stay logged in
170	/// for up to a year, so cookies from up to 52 weeks ago need to still be readable by your
171	/// application.
172	///
173	/// In that case, you could create a [`RotatingStrongBox`] with a "weekly" period, and
174	/// use up to 52 previous keys to decrypt the data, like this:
175	///
176	/// ```rust
177	/// # use strong_box::{StemStrongBox, Key};
178	/// # use std::time::Duration;
179	///
180	/// // Seven days, each of 24 hours, each hour with 3,600 seconds
181	/// const WEEKLY: Duration = Duration::from_secs(7 * 24 * 3600);
182	///
183	/// let key = strong_box::generate_key();
184	///
185	/// let cookie_box = StemStrongBox::new(key, Vec::<Key>::new()).derive_rotating(b"cookies", WEEKLY, 52);
186	///
187	/// // You can now encrypt/decrypt to your heart's content with the cookie_box
188	/// ```
189	pub fn derive_rotating(
190		&self,
191		purpose: impl AsRef<[u8]> + Debug,
192		period: Duration,
193		backtrack: u16,
194	) -> RotatingStrongBox {
195		let mut context: Vec<u8> = b"derive_stem::".to_vec();
196		context.extend_from_slice(purpose.as_ref());
197		RotatingStrongBox::new(
198			kdf::derive_key(&self.encryption_key, &context),
199			self.decryption_keys
200				.iter()
201				.map(|k| kdf::derive_key(k, &context))
202				.collect(),
203			period,
204			backtrack,
205		)
206	}
207}