Skip to main content

bitwarden_user_crypto_management/
change_kdf.rs

1//! Client operation for changing the account's KDF (key derivation function) settings.
2
3use bitwarden_api_api::models::ChangeKdfRequestModel;
4use bitwarden_core::{
5    ApiError, NotAuthenticatedError,
6    key_management::{
7        MasterPasswordAuthenticationData, MasterPasswordError, MasterPasswordUnlockData,
8        SymmetricKeySlotId,
9    },
10};
11use bitwarden_crypto::Kdf;
12use bitwarden_error::bitwarden_error;
13use thiserror::Error;
14use tracing::error;
15
16use crate::UserCryptoManagementClient;
17
18#[bitwarden_ffi::wasm_export]
19#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
20impl UserCryptoManagementClient {
21    /// Changes the account's KDF settings, and sets them on the server.
22    pub async fn change_kdf(&self, password: String, new_kdf: Kdf) -> Result<(), ChangeKdfError> {
23        let bridge = self.client.km_state_bridge();
24
25        let current_unlock_data = bridge
26            .get_masterpassword_unlock_data()
27            .await
28            .ok_or(ChangeKdfError::MissingMasterPasswordUnlockData)?;
29        let salt = current_unlock_data.salt;
30        let current_kdf = current_unlock_data.kdf;
31
32        // Re-derive the authentication and unlock data. The key store context must not be held
33        // across the await on the server request below, so it is scoped to this block.
34        let (old_authentication_data, authentication_data, unlock_data) = {
35            let ctx = self.client.internal.get_key_store().context();
36
37            let old_authentication_data =
38                MasterPasswordAuthenticationData::derive(&password, &current_kdf, &salt)?;
39            let authentication_data =
40                MasterPasswordAuthenticationData::derive(&password, &new_kdf, &salt)?;
41            let unlock_data = MasterPasswordUnlockData::derive(
42                &password,
43                &new_kdf,
44                &salt,
45                SymmetricKeySlotId::User,
46                &ctx,
47            )?;
48
49            (old_authentication_data, authentication_data, unlock_data)
50        };
51
52        // Build and post the change-KDF request to the server. The old authentication hash proves
53        // possession of the current password; the new authentication and unlock data replace it.
54        let request = ChangeKdfRequestModel {
55            master_password_hash: old_authentication_data
56                .master_password_authentication_hash
57                .to_string(),
58            authentication_data: Box::new((&authentication_data).into()),
59            unlock_data: Box::new((&unlock_data).into()),
60        };
61
62        self.client
63            .internal
64            .get_api_configurations()
65            .api_client
66            .accounts_api()
67            .post_kdf(Some(request))
68            .await
69            .map_err(|e| {
70                error!("Failed to post change-kdf request: {e:?}");
71                ApiError::from(e)
72            })?;
73
74        // Persist the new unlock data and KDF config to client-managed state via the state bridge,
75        // then update the KDF held in the internal client so in-memory state stays consistent.
76        bridge.set_masterpassword_unlock_data(&unlock_data).await;
77        bridge.set_kdf_config(&new_kdf).await;
78        self.client
79            .internal
80            .set_user_master_password_unlock(unlock_data)
81            .await?;
82
83        Ok(())
84    }
85}
86
87/// Errors that can occur while changing the account KDF settings.
88#[derive(Debug, Error)]
89#[bitwarden_error(flat)]
90pub enum ChangeKdfError {
91    /// Deriving the new authentication or unlock data failed.
92    #[error(transparent)]
93    MasterPassword(#[from] MasterPasswordError),
94    /// The current master-password unlock data is not available in the state bridge.
95    #[error("Master password unlock data is not available")]
96    MissingMasterPasswordUnlockData,
97    /// The client is not authenticated with a master password.
98    #[error(transparent)]
99    NotAuthenticated(#[from] NotAuthenticatedError),
100    /// The server rejected the change-KDF request.
101    #[error(transparent)]
102    Api(#[from] ApiError),
103}
104
105#[cfg(test)]
106mod tests {
107    use std::num::NonZeroU32;
108
109    use bitwarden_api_api::apis::ApiClient;
110    use bitwarden_core::{
111        Client,
112        client::test_accounts::test_bitwarden_com_account,
113        key_management::{
114            MasterPasswordUnlockData, state_bridge::test_support::InMemoryStateBridge,
115        },
116    };
117    use bitwarden_crypto::Kdf;
118
119    use super::*;
120    use crate::UserCryptoManagementClientExt;
121
122    const TEST_PASSWORD: &str = "asdfasdfasdf";
123    const TEST_EMAIL: &str = "[email protected]";
124    // A valid EncString; only the salt/kdf of the seeded unlock data are used by derivation.
125    const TEST_WRAPPED_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
126
127    fn new_kdf() -> Kdf {
128        Kdf::PBKDF2 {
129            iterations: NonZeroU32::new(700_000).unwrap(),
130        }
131    }
132
133    // Sets the master-password unlock data in the state bridge to a fixed value for testing.
134    async fn test_unlock_data(client: &Client) {
135        let current_kdf = client.internal.get_kdf().await.unwrap();
136        client
137            .km_state_bridge()
138            .set_masterpassword_unlock_data(&MasterPasswordUnlockData {
139                kdf: current_kdf,
140                master_key_wrapped_user_key: TEST_WRAPPED_USER_KEY.parse().unwrap(),
141                salt: TEST_EMAIL.to_string(),
142                contained_key_id: None,
143            })
144            .await;
145    }
146
147    #[tokio::test]
148    async fn test_change_kdf_success_posts_and_persists() {
149        let api_client = ApiClient::new_mocked(|mock| {
150            mock.accounts_api
151                .expect_post_kdf()
152                .once()
153                .returning(|body| {
154                    let body = body.expect("body should be Some");
155                    // The unlock/authentication data must carry the new KDF settings.
156                    assert_eq!(body.authentication_data.kdf.iterations, 700_000);
157                    assert_eq!(body.unlock_data.kdf.iterations, 700_000);
158                    assert!(!body.master_password_hash.is_empty());
159                    Ok(())
160                });
161        });
162
163        let client =
164            Client::init_test_account_with_api_client(test_bitwarden_com_account(), api_client)
165                .await;
166        client
167            .km_state_bridge()
168            .register_bridge(Box::new(InMemoryStateBridge::default()));
169        test_unlock_data(&client).await;
170
171        client
172            .user_crypto_management()
173            .change_kdf(TEST_PASSWORD.to_string(), new_kdf())
174            .await
175            .unwrap();
176
177        // The new KDF config and unlock data are persisted to state.
178        let bridge = client.km_state_bridge();
179        assert_eq!(bridge.get_kdf_config().await, Some(new_kdf()));
180        let unlock = bridge
181            .get_masterpassword_unlock_data()
182            .await
183            .expect("unlock data persisted");
184        assert_eq!(unlock.kdf, new_kdf());
185        // The internal client's KDF is updated as well.
186        assert_eq!(client.internal.get_kdf().await.unwrap(), new_kdf());
187    }
188
189    #[tokio::test]
190    async fn test_change_kdf_api_failure_does_not_persist() {
191        let api_client = ApiClient::new_mocked(|mock| {
192            mock.accounts_api
193                .expect_post_kdf()
194                .once()
195                .returning(|_body| Err(std::io::Error::other("Simulated error").into()));
196        });
197
198        let client =
199            Client::init_test_account_with_api_client(test_bitwarden_com_account(), api_client)
200                .await;
201        client
202            .km_state_bridge()
203            .register_bridge(Box::new(InMemoryStateBridge::default()));
204        test_unlock_data(&client).await;
205
206        let current_kdf = client.internal.get_kdf().await.unwrap();
207
208        let result = client
209            .user_crypto_management()
210            .change_kdf(TEST_PASSWORD.to_string(), new_kdf())
211            .await;
212
213        assert!(matches!(result, Err(ChangeKdfError::Api(_))));
214        // The KDF config is untouched when the server call fails.
215        assert_eq!(client.km_state_bridge().get_kdf_config().await, None);
216        assert_eq!(client.internal.get_kdf().await.unwrap(), current_kdf);
217    }
218
219    #[tokio::test]
220    async fn test_change_kdf_missing_unlock_data_errors() {
221        let api_client = ApiClient::new_mocked(|_mock| {});
222        let client =
223            Client::init_test_account_with_api_client(test_bitwarden_com_account(), api_client)
224                .await;
225        client
226            .km_state_bridge()
227            .register_bridge(Box::new(InMemoryStateBridge::default()));
228
229        let result = client
230            .user_crypto_management()
231            .change_kdf(TEST_PASSWORD.to_string(), new_kdf())
232            .await;
233
234        assert!(matches!(
235            result,
236            Err(ChangeKdfError::MissingMasterPasswordUnlockData)
237        ));
238    }
239}