bitwarden_user_crypto_management/
change_kdf.rs1use 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 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 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, ¤t_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 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 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#[derive(Debug, Error)]
89#[bitwarden_error(flat)]
90pub enum ChangeKdfError {
91 #[error(transparent)]
93 MasterPassword(#[from] MasterPasswordError),
94 #[error("Master password unlock data is not available")]
96 MissingMasterPasswordUnlockData,
97 #[error(transparent)]
99 NotAuthenticated(#[from] NotAuthenticatedError),
100 #[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 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 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 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 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 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 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}