Skip to main content

bitwarden_user_crypto_management/key_rotation/
crypto.rs

1//! Functionality for re-encrypting account cryptographic state during user key rotation.
2use bitwarden_api_api::models::{
3    AccountKeysRequestModel, WrappedAccountCryptographicStateRequestModel,
4};
5use bitwarden_core::key_management::{
6    KeySlotIds, SymmetricKeySlotId, account_cryptographic_state::WrappedAccountCryptographicState,
7};
8use tracing::debug;
9
10/// Rotates an account cryptographic state and upgrades it to V2 if necessary.
11/// This function fails and logs an error via tracing if the passed keys are invalid, or if the
12/// account cryptographic state is malformed.
13pub(super) fn rotate_account_cryptographic_state_to_request_model(
14    wrapped_account_cryptographic_state: &WrappedAccountCryptographicState,
15    current_user_key_id: &SymmetricKeySlotId,
16    new_user_key_id: &SymmetricKeySlotId,
17    ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
18) -> Result<AccountKeysRequestModel, ()> {
19    let rotated_account_cryptographic_state = rotate_account_cryptographic_state(
20        wrapped_account_cryptographic_state,
21        current_user_key_id,
22        new_user_key_id,
23        ctx,
24    )?;
25
26    debug!("Converting rotated account cryptographic state to account keys request model",);
27
28    // Rotate the account keys for the user
29    let account_keys_model = rotated_account_cryptographic_state
30        .to_request_model(new_user_key_id, ctx)
31        .map_err(|_| ())?;
32    Ok(account_keys_model)
33}
34
35/// Converts a rotated account cryptographic state to a wrapped model for the request.
36/// This function fails and logs an error via tracing if the passed keys are invalid, or if the
37/// account cryptographic state is malformed.
38pub(super) fn account_cryptographic_state_to_wrapped_model(
39    wrapped_account_cryptographic_state: &WrappedAccountCryptographicState,
40    new_user_key_id: &SymmetricKeySlotId,
41    ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
42) -> Result<WrappedAccountCryptographicStateRequestModel, ()> {
43    debug!("Converting rotated account cryptographic state to wrapped request model",);
44
45    // Rotate the account keys for the user
46    let wrapped_request_model = wrapped_account_cryptographic_state
47        .to_wrapped_request_model(new_user_key_id, ctx)
48        .map_err(|_| ())?;
49    Ok(wrapped_request_model)
50}
51
52pub(super) fn rotate_account_cryptographic_state(
53    wrapped_account_cryptographic_state: &WrappedAccountCryptographicState,
54    current_user_key_id: &SymmetricKeySlotId,
55    new_user_key_id: &SymmetricKeySlotId,
56    ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
57) -> Result<WrappedAccountCryptographicState, ()> {
58    debug!(
59        ?current_user_key_id,
60        ?new_user_key_id,
61        "Rotating account cryptographic state",
62    );
63
64    // We can't really handle the error variants here. Either the provided account cryptographic
65    // state is broken, or a key is missing.
66    let rotated_account_cryptographic_state = WrappedAccountCryptographicState::rotate(
67        wrapped_account_cryptographic_state,
68        current_user_key_id,
69        new_user_key_id,
70        ctx,
71    )
72    .map_err(|_| ())?;
73
74    Ok(rotated_account_cryptographic_state)
75}
76
77#[cfg(test)]
78mod tests {
79    use bitwarden_core::key_management::account_cryptographic_state::WrappedAccountCryptographicState;
80    use bitwarden_crypto::{
81        CoseSerializable, KeyStore, PublicKey, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm,
82    };
83    use bitwarden_encoding::B64;
84
85    use super::*;
86
87    /// Creates a V1 wrapped state for testing. This mimics what make_v1 does in bitwarden-core,
88    /// but is accessible from this crate.
89    fn make_v1_wrapped_state(
90        ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
91    ) -> (
92        SymmetricKeySlotId,
93        PublicKey,
94        WrappedAccountCryptographicState,
95    ) {
96        let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
97        let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
98        let wrapped_private_key = ctx.wrap_private_key(user_key, private_key).unwrap();
99
100        (
101            user_key,
102            ctx.get_public_key(private_key).unwrap(),
103            WrappedAccountCryptographicState::V1 {
104                private_key: wrapped_private_key,
105            },
106        )
107    }
108
109    fn assert_wrapped_request_model_fields(
110        model: &WrappedAccountCryptographicStateRequestModel,
111        actual_public_key: &B64,
112    ) {
113        let model_public_key = model
114            .public_key_encryption_key_pair
115            .public_key
116            .as_ref()
117            .expect("public_key should be present");
118        assert_eq!(
119            actual_public_key.to_string(),
120            *model_public_key,
121            "Public key should be correctly included in the model"
122        );
123
124        // Assert signed_public_key is present
125        assert!(
126            model
127                .public_key_encryption_key_pair
128                .signed_public_key
129                .is_some(),
130            "signed_public_key should be present for V2 state"
131        );
132
133        // Note: The actual cryptographic correctness of these values (signatures, key material)
134        // is verified in the account_cryptographic_state tests. This test only asserts that
135        // the conversion to AccountKeysRequestModel is reasonable (i.e., expected fields are
136        // present).
137        assert!(
138            model.signature_key_pair.verifying_key.is_some(),
139            "verifying_key should be present"
140        );
141        assert!(
142            model.signature_key_pair.wrapped_signing_key.is_some(),
143            "wrapped_signing_key should be present"
144        );
145        assert!(
146            model.signature_key_pair.signature_algorithm.is_some(),
147            "signature_algorithm should be present"
148        );
149        assert!(
150            model.security_state.security_state.is_some(),
151            "security_state content should be present"
152        );
153    }
154
155    #[test]
156    fn test_rotate_v1_to_v2_returns_wrapped_request_model() {
157        // Create a key store and context
158        let store: KeyStore<KeySlotIds> = KeyStore::default();
159        let mut ctx = store.context_mut();
160
161        // Create a V1-style wrapped state
162        let (old_user_key_id, public_key, wrapped_state) = make_v1_wrapped_state(&mut ctx);
163
164        // Create a new user key for rotation
165        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
166
167        let rotated_state = rotate_account_cryptographic_state(
168            &wrapped_state,
169            &old_user_key_id,
170            &new_user_key_id,
171            &mut ctx,
172        )
173        .expect("rotate_account_cryptographic_state should succeed");
174        let model = account_cryptographic_state_to_wrapped_model(
175            &rotated_state,
176            &new_user_key_id,
177            &mut ctx,
178        )
179        .expect("rotate_account_cryptographic_state_to_wrapped_model should succeed");
180
181        let actual_public_key: B64 = public_key.to_der().unwrap().into();
182        assert_wrapped_request_model_fields(&model, &actual_public_key);
183    }
184
185    #[test]
186    fn test_rotate_v2_to_v2_returns_wrapped_request_model() {
187        // Create a key store and context
188        let store: KeyStore<KeySlotIds> = KeyStore::default();
189        let mut ctx = store.context_mut();
190
191        // Create a V2-style wrapped state
192        let (old_user_key_id, wrapped_state) =
193            WrappedAccountCryptographicState::make(&mut ctx).unwrap();
194
195        // Get the public key before rotation
196        let private_key_id = match &wrapped_state {
197            WrappedAccountCryptographicState::V2 { private_key, .. } => ctx
198                .unwrap_private_key(old_user_key_id, private_key)
199                .unwrap(),
200            _ => panic!("Expected V2 state"),
201        };
202        let signing_key_id = match &wrapped_state {
203            WrappedAccountCryptographicState::V2 { signing_key, .. } => ctx
204                .unwrap_signing_key(old_user_key_id, signing_key)
205                .unwrap(),
206            _ => panic!("Expected V2 state"),
207        };
208        let public_key = ctx.get_public_key(private_key_id).unwrap();
209        ctx.get_verifying_key(signing_key_id)
210            .expect("verifying key should be obtainable");
211
212        // Create a new user key for rotation
213        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
214
215        let rotated_state = rotate_account_cryptographic_state(
216            &wrapped_state,
217            &old_user_key_id,
218            &new_user_key_id,
219            &mut ctx,
220        )
221        .expect("rotate_account_cryptographic_state should succeed");
222        let model = account_cryptographic_state_to_wrapped_model(
223            &rotated_state,
224            &new_user_key_id,
225            &mut ctx,
226        )
227        .expect("account_cryptographic_state_to_wrapped_model should succeed");
228
229        let actual_public_key: B64 = public_key.to_der().unwrap().into();
230        assert_wrapped_request_model_fields(&model, &actual_public_key);
231    }
232
233    #[test]
234    fn test_rotate_v1_to_v2_returns_account_keys_model() {
235        // Create a key store and context
236        let store: KeyStore<KeySlotIds> = KeyStore::default();
237        let mut ctx = store.context_mut();
238
239        // Create a V1-style wrapped state
240        let (old_user_key_id, public_key, wrapped_state) = make_v1_wrapped_state(&mut ctx);
241
242        // Create a new user key for rotation
243        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
244
245        let model = rotate_account_cryptographic_state_to_request_model(
246            &wrapped_state,
247            &old_user_key_id,
248            &new_user_key_id,
249            &mut ctx,
250        )
251        .expect("rotate_account_cryptographic_state_to_request_model should succeed");
252
253        let actual_public_key: B64 = public_key.to_der().unwrap().into();
254        let public_key_encryption_key_pair = model
255            .public_key_encryption_key_pair
256            .as_ref()
257            .expect("public_key_encryption_key_pair should be present");
258        let model_public_key = public_key_encryption_key_pair
259            .public_key
260            .as_ref()
261            .expect("public_key should be present");
262        assert_eq!(
263            actual_public_key.to_string(),
264            *model_public_key,
265            "Public key should be correctly included in the model"
266        );
267
268        // Assert signed_public_key is present
269        assert!(
270            public_key_encryption_key_pair.signed_public_key.is_some(),
271            "signed_public_key should be present for V2 state"
272        );
273
274        // Note: The actual cryptographic correctness of these values (signatures, key material)
275        // is verified in the account_cryptographic_state tests. This test only asserts that
276        // the conversion to AccountKeysRequestModel is reasonable (i.e., expected fields are
277        // present).
278
279        // Assert signature_key_pair (verifying key) is present
280        let signature_key_pair = model
281            .signature_key_pair
282            .as_ref()
283            .expect("signature_key_pair should be present for V2 state");
284        assert!(
285            signature_key_pair.verifying_key.is_some(),
286            "verifying_key should be present"
287        );
288        assert!(
289            signature_key_pair.wrapped_signing_key.is_some(),
290            "wrapped_signing_key should be present"
291        );
292        assert!(
293            signature_key_pair.signature_algorithm.is_some(),
294            "signature_algorithm should be present"
295        );
296
297        // Assert security_state is present
298        let security_state = model
299            .security_state
300            .as_ref()
301            .expect("security_state should be present for V2 state");
302        assert!(
303            security_state.security_state.is_some(),
304            "security_state content should be present"
305        );
306    }
307
308    #[test]
309    fn test_rotate_v2_to_v2_returns_account_keys_model() {
310        // Create a key store and context
311        let store: KeyStore<KeySlotIds> = KeyStore::default();
312        let mut ctx = store.context_mut();
313
314        // Create a V2-style wrapped state
315        let (old_user_key_id, wrapped_state) =
316            WrappedAccountCryptographicState::make(&mut ctx).unwrap();
317
318        // Get the public key before rotation
319        let private_key_id = match &wrapped_state {
320            WrappedAccountCryptographicState::V2 { private_key, .. } => ctx
321                .unwrap_private_key(old_user_key_id, private_key)
322                .unwrap(),
323            _ => panic!("Expected V2 state"),
324        };
325        let signing_key_id = match &wrapped_state {
326            WrappedAccountCryptographicState::V2 { signing_key, .. } => ctx
327                .unwrap_signing_key(old_user_key_id, signing_key)
328                .unwrap(),
329            _ => panic!("Expected V2 state"),
330        };
331        let public_key = ctx.get_public_key(private_key_id).unwrap();
332        let verifying_key = ctx
333            .get_verifying_key(signing_key_id)
334            .expect("verifying key should be obtainable");
335
336        // Create a new user key for rotation
337        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
338
339        let model = rotate_account_cryptographic_state_to_request_model(
340            &wrapped_state,
341            &old_user_key_id,
342            &new_user_key_id,
343            &mut ctx,
344        )
345        .expect("rotate_account_cryptographic_state_to_request_model should succeed");
346
347        let actual_public_key: B64 = public_key.to_der().unwrap().into();
348        let public_key_encryption_key_pair = model
349            .public_key_encryption_key_pair
350            .as_ref()
351            .expect("public_key_encryption_key_pair should be present");
352        let model_public_key = public_key_encryption_key_pair
353            .public_key
354            .as_ref()
355            .expect("public_key should be present");
356        assert_eq!(
357            actual_public_key.to_string(),
358            *model_public_key,
359            "Public key should be correctly included in the model"
360        );
361
362        // Assert signed_public_key is present
363        assert!(
364            public_key_encryption_key_pair.signed_public_key.is_some(),
365            "signed_public_key should be present for V2 state"
366        );
367
368        // Note: The actual cryptographic correctness of these values (signatures, key material)
369        // is verified in the account_cryptographic_state tests. This test only asserts that
370        // the conversion to AccountKeysRequestModel is reasonable (i.e., expected fields are
371        // present).
372
373        // Assert signature_key_pair (verifying key) is present and equal to the old key
374        let signature_key_pair = model
375            .signature_key_pair
376            .as_ref()
377            .expect("signature_key_pair should be present for V2 state");
378        let actual_verifying_key: B64 = verifying_key.to_cose().into();
379        let model_verifying_key = signature_key_pair
380            .verifying_key
381            .as_ref()
382            .expect("verifying_key should be present");
383        assert_eq!(
384            actual_verifying_key.to_string(),
385            *model_verifying_key,
386            "Verifying key should be correctly included in the model"
387        );
388
389        assert!(
390            signature_key_pair.wrapped_signing_key.is_some(),
391            "wrapped_signing_key should be present"
392        );
393        assert!(
394            signature_key_pair.signature_algorithm.is_some(),
395            "signature_algorithm should be present"
396        );
397
398        // Assert security_state is present
399        let security_state = model
400            .security_state
401            .as_ref()
402            .expect("security_state should be present for V2 state");
403        assert!(
404            security_state.security_state.is_some(),
405            "security_state content should be present"
406        );
407    }
408}