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