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