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}