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_core::{
3    UserId,
4    key_management::{
5        KeyIds, SymmetricKeyId, account_cryptographic_state::WrappedAccountCryptographicState,
6    },
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(
14    wrapped_account_cryptographic_state: &WrappedAccountCryptographicState,
15    current_user_key_id: &SymmetricKeyId,
16    new_user_key_id: &SymmetricKeyId,
17    user_id: UserId,
18    ctx: &mut bitwarden_crypto::KeyStoreContext<KeyIds>,
19) -> Result<bitwarden_api_api::models::AccountKeysRequestModel, ()> {
20    debug!(
21        %user_id, ?current_user_key_id, ?new_user_key_id,
22        "Rotating account cryptographic state",
23    );
24
25    // We can't really handle the error variants here. Either the provided account cryptographic
26    // state is broken, or a key is missing.
27    let rotated_account_cryptographic_state = WrappedAccountCryptographicState::rotate(
28        wrapped_account_cryptographic_state,
29        current_user_key_id,
30        new_user_key_id,
31        user_id,
32        ctx,
33    )
34    .map_err(|_| ())?;
35
36    debug!(
37        %user_id,
38        "Converting rotated account cryptographic state to request model",
39    );
40
41    // Rotate the account keys for the user
42    let account_keys_model = rotated_account_cryptographic_state
43        .to_request_model(new_user_key_id, ctx)
44        .map_err(|_| ())?;
45    Ok(account_keys_model)
46}
47
48#[cfg(test)]
49mod tests {
50    use bitwarden_core::key_management::account_cryptographic_state::WrappedAccountCryptographicState;
51    use bitwarden_crypto::{
52        CoseSerializable, KeyStore, PublicKey, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm,
53    };
54    use bitwarden_encoding::B64;
55
56    use super::*;
57
58    /// Creates a V1 wrapped state for testing. This mimics what make_v1 does in bitwarden-core,
59    /// but is accessible from this crate.
60    fn make_v1_wrapped_state(
61        ctx: &mut bitwarden_crypto::KeyStoreContext<KeyIds>,
62    ) -> (SymmetricKeyId, PublicKey, WrappedAccountCryptographicState) {
63        let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
64        let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
65        let wrapped_private_key = ctx.wrap_private_key(user_key, private_key).unwrap();
66
67        (
68            user_key,
69            ctx.get_public_key(private_key).unwrap(),
70            WrappedAccountCryptographicState::V1 {
71                private_key: wrapped_private_key,
72            },
73        )
74    }
75
76    #[test]
77    fn test_rotate_v1_to_v2_returns_account_keys_model() {
78        // Create a key store and context
79        let store: KeyStore<KeyIds> = KeyStore::default();
80        let mut ctx = store.context_mut();
81
82        // Create a V1-style wrapped state
83        let user_id = UserId::new_v4();
84        let (old_user_key_id, public_key, wrapped_state) = make_v1_wrapped_state(&mut ctx);
85
86        // Create a new user key for rotation
87        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
88
89        let model = rotate_account_cryptographic_state(
90            &wrapped_state,
91            &old_user_key_id,
92            &new_user_key_id,
93            user_id,
94            &mut ctx,
95        )
96        .expect("rotate_account_cryptographic_state should succeed");
97
98        let actual_public_key: B64 = public_key.to_der().unwrap().into();
99        let public_key_encryption_key_pair = model
100            .public_key_encryption_key_pair
101            .as_ref()
102            .expect("public_key_encryption_key_pair should be present");
103        let model_public_key = public_key_encryption_key_pair
104            .public_key
105            .as_ref()
106            .expect("public_key should be present");
107        assert_eq!(
108            actual_public_key.to_string(),
109            *model_public_key,
110            "Public key should be correctly included in the model"
111        );
112
113        // Assert signed_public_key is present
114        assert!(
115            public_key_encryption_key_pair.signed_public_key.is_some(),
116            "signed_public_key should be present for V2 state"
117        );
118
119        // Note: The actual cryptographic correctness of these values (signatures, key material)
120        // is verified in the account_cryptographic_state tests. This test only asserts that
121        // the conversion to AccountKeysRequestModel is reasonable (i.e., expected fields are
122        // present).
123
124        // Assert signature_key_pair (verifying key) is present
125        let signature_key_pair = model
126            .signature_key_pair
127            .as_ref()
128            .expect("signature_key_pair should be present for V2 state");
129        assert!(
130            signature_key_pair.verifying_key.is_some(),
131            "verifying_key should be present"
132        );
133        assert!(
134            signature_key_pair.wrapped_signing_key.is_some(),
135            "wrapped_signing_key should be present"
136        );
137        assert!(
138            signature_key_pair.signature_algorithm.is_some(),
139            "signature_algorithm should be present"
140        );
141
142        // Assert security_state is present
143        let security_state = model
144            .security_state
145            .as_ref()
146            .expect("security_state should be present for V2 state");
147        assert!(
148            security_state.security_state.is_some(),
149            "security_state content should be present"
150        );
151    }
152
153    #[test]
154    fn test_rotate_v2_to_v2_returns_account_keys_model() {
155        // Create a key store and context
156        let store: KeyStore<KeyIds> = KeyStore::default();
157        let mut ctx = store.context_mut();
158
159        // Create a V2-style wrapped state
160        let user_id = UserId::new_v4();
161        let (old_user_key_id, wrapped_state) =
162            WrappedAccountCryptographicState::make(&mut ctx, user_id).unwrap();
163
164        // Get the public key before rotation
165        let private_key_id = match &wrapped_state {
166            WrappedAccountCryptographicState::V2 { private_key, .. } => ctx
167                .unwrap_private_key(old_user_key_id, private_key)
168                .unwrap(),
169            _ => panic!("Expected V2 state"),
170        };
171        let signing_key_id = match &wrapped_state {
172            WrappedAccountCryptographicState::V2 { signing_key, .. } => ctx
173                .unwrap_signing_key(old_user_key_id, signing_key)
174                .unwrap(),
175            _ => panic!("Expected V2 state"),
176        };
177        let public_key = ctx.get_public_key(private_key_id).unwrap();
178        let verifying_key = ctx
179            .get_verifying_key(signing_key_id)
180            .expect("verifying key should be obtainable");
181
182        // Create a new user key for rotation
183        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
184
185        let model = rotate_account_cryptographic_state(
186            &wrapped_state,
187            &old_user_key_id,
188            &new_user_key_id,
189            user_id,
190            &mut ctx,
191        )
192        .expect("rotate_account_cryptographic_state should succeed");
193
194        let actual_public_key: B64 = public_key.to_der().unwrap().into();
195        let public_key_encryption_key_pair = model
196            .public_key_encryption_key_pair
197            .as_ref()
198            .expect("public_key_encryption_key_pair should be present");
199        let model_public_key = public_key_encryption_key_pair
200            .public_key
201            .as_ref()
202            .expect("public_key should be present");
203        assert_eq!(
204            actual_public_key.to_string(),
205            *model_public_key,
206            "Public key should be correctly included in the model"
207        );
208
209        // Assert signed_public_key is present
210        assert!(
211            public_key_encryption_key_pair.signed_public_key.is_some(),
212            "signed_public_key should be present for V2 state"
213        );
214
215        // Note: The actual cryptographic correctness of these values (signatures, key material)
216        // is verified in the account_cryptographic_state tests. This test only asserts that
217        // the conversion to AccountKeysRequestModel is reasonable (i.e., expected fields are
218        // present).
219
220        // Assert signature_key_pair (verifying key) is present and equal to the old key
221        let signature_key_pair = model
222            .signature_key_pair
223            .as_ref()
224            .expect("signature_key_pair should be present for V2 state");
225        let actual_verifying_key: B64 = verifying_key.to_cose().into();
226        let model_verifying_key = signature_key_pair
227            .verifying_key
228            .as_ref()
229            .expect("verifying_key should be present");
230        assert_eq!(
231            actual_verifying_key.to_string(),
232            *model_verifying_key,
233            "Verifying key should be correctly included in the model"
234        );
235
236        assert!(
237            signature_key_pair.wrapped_signing_key.is_some(),
238            "wrapped_signing_key should be present"
239        );
240        assert!(
241            signature_key_pair.signature_algorithm.is_some(),
242            "signature_algorithm should be present"
243        );
244
245        // Assert security_state is present
246        let security_state = model
247            .security_state
248            .as_ref()
249            .expect("security_state should be present for V2 state");
250        assert!(
251            security_state.security_state.is_some(),
252            "security_state content should be present"
253        );
254    }
255}