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#[cfg(feature = "wasm")]
16use wasm_bindgen::prelude::*;
17
18use crate::UserCryptoManagementClient;
19
20#[cfg_attr(feature = "wasm", wasm_bindgen)]
21#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
22impl UserCryptoManagementClient {
23 pub async fn change_kdf(&self, password: String, new_kdf: Kdf) -> Result<(), ChangeKdfError> {
25 let bridge = self.client.km_state_bridge();
26
27 let current_unlock_data = bridge
28 .get_masterpassword_unlock_data()
29 .await
30 .ok_or(ChangeKdfError::MissingMasterPasswordUnlockData)?;
31 let salt = current_unlock_data.salt;
32 let current_kdf = current_unlock_data.kdf;
33
34 let (old_authentication_data, authentication_data, unlock_data) = {
37 let ctx = self.client.internal.get_key_store().context();
38
39 let old_authentication_data =
40 MasterPasswordAuthenticationData::derive(&password, ¤t_kdf, &salt)?;
41 let authentication_data =
42 MasterPasswordAuthenticationData::derive(&password, &new_kdf, &salt)?;
43 let unlock_data = MasterPasswordUnlockData::derive(
44 &password,
45 &new_kdf,
46 &salt,
47 SymmetricKeySlotId::User,
48 &ctx,
49 )?;
50
51 (old_authentication_data, authentication_data, unlock_data)
52 };
53
54 let request = ChangeKdfRequestModel {
57 master_password_hash: old_authentication_data
58 .master_password_authentication_hash
59 .to_string(),
60 authentication_data: Box::new((&authentication_data).into()),
61 unlock_data: Box::new((&unlock_data).into()),
62 };
63
64 self.client
65 .internal
66 .get_api_configurations()
67 .api_client
68 .accounts_api()
69 .post_kdf(Some(request))
70 .await
71 .map_err(|e| {
72 error!("Failed to post change-kdf request: {e:?}");
73 ApiError::from(e)
74 })?;
75
76 bridge.set_masterpassword_unlock_data(&unlock_data).await;
79 bridge.set_kdf_config(&new_kdf).await;
80 self.client
81 .internal
82 .set_user_master_password_unlock(unlock_data)
83 .await?;
84
85 Ok(())
86 }
87}
88
89#[derive(Debug, Error)]
91#[bitwarden_error(flat)]
92pub enum ChangeKdfError {
93 #[error(transparent)]
95 MasterPassword(#[from] MasterPasswordError),
96 #[error("Master password unlock data is not available")]
98 MissingMasterPasswordUnlockData,
99 #[error(transparent)]
101 NotAuthenticated(#[from] NotAuthenticatedError),
102 #[error(transparent)]
104 Api(#[from] ApiError),
105}
106
107#[cfg(test)]
108mod tests {
109 use std::num::NonZeroU32;
110
111 use bitwarden_api_api::apis::ApiClient;
112 use bitwarden_core::{
113 Client,
114 client::test_accounts::test_bitwarden_com_account,
115 key_management::{
116 MasterPasswordUnlockData, state_bridge::test_support::InMemoryStateBridge,
117 },
118 };
119 use bitwarden_crypto::Kdf;
120
121 use super::*;
122 use crate::UserCryptoManagementClientExt;
123
124 const TEST_PASSWORD: &str = "asdfasdfasdf";
125 const TEST_EMAIL: &str = "[email protected]";
126 const TEST_WRAPPED_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
128
129 fn new_kdf() -> Kdf {
130 Kdf::PBKDF2 {
131 iterations: NonZeroU32::new(700_000).unwrap(),
132 }
133 }
134
135 async fn test_unlock_data(client: &Client) {
137 let current_kdf = client.internal.get_kdf().await.unwrap();
138 client
139 .km_state_bridge()
140 .set_masterpassword_unlock_data(&MasterPasswordUnlockData {
141 kdf: current_kdf,
142 master_key_wrapped_user_key: TEST_WRAPPED_USER_KEY.parse().unwrap(),
143 salt: TEST_EMAIL.to_string(),
144 contained_key_id: None,
145 })
146 .await;
147 }
148
149 #[tokio::test]
150 async fn test_change_kdf_success_posts_and_persists() {
151 let api_client = ApiClient::new_mocked(|mock| {
152 mock.accounts_api
153 .expect_post_kdf()
154 .once()
155 .returning(|body| {
156 let body = body.expect("body should be Some");
157 assert_eq!(body.authentication_data.kdf.iterations, 700_000);
159 assert_eq!(body.unlock_data.kdf.iterations, 700_000);
160 assert!(!body.master_password_hash.is_empty());
161 Ok(())
162 });
163 });
164
165 let client =
166 Client::init_test_account_with_api_client(test_bitwarden_com_account(), api_client)
167 .await;
168 client
169 .km_state_bridge()
170 .register_bridge(Box::new(InMemoryStateBridge::default()));
171 test_unlock_data(&client).await;
172
173 client
174 .user_crypto_management()
175 .change_kdf(TEST_PASSWORD.to_string(), new_kdf())
176 .await
177 .unwrap();
178
179 let bridge = client.km_state_bridge();
181 assert_eq!(bridge.get_kdf_config().await, Some(new_kdf()));
182 let unlock = bridge
183 .get_masterpassword_unlock_data()
184 .await
185 .expect("unlock data persisted");
186 assert_eq!(unlock.kdf, new_kdf());
187 assert_eq!(client.internal.get_kdf().await.unwrap(), new_kdf());
189 }
190
191 #[tokio::test]
192 async fn test_change_kdf_api_failure_does_not_persist() {
193 let api_client = ApiClient::new_mocked(|mock| {
194 mock.accounts_api
195 .expect_post_kdf()
196 .once()
197 .returning(|_body| Err(std::io::Error::other("Simulated error").into()));
198 });
199
200 let client =
201 Client::init_test_account_with_api_client(test_bitwarden_com_account(), api_client)
202 .await;
203 client
204 .km_state_bridge()
205 .register_bridge(Box::new(InMemoryStateBridge::default()));
206 test_unlock_data(&client).await;
207
208 let current_kdf = client.internal.get_kdf().await.unwrap();
209
210 let result = client
211 .user_crypto_management()
212 .change_kdf(TEST_PASSWORD.to_string(), new_kdf())
213 .await;
214
215 assert!(matches!(result, Err(ChangeKdfError::Api(_))));
216 assert_eq!(client.km_state_bridge().get_kdf_config().await, None);
218 assert_eq!(client.internal.get_kdf().await.unwrap(), current_kdf);
219 }
220
221 #[tokio::test]
222 async fn test_change_kdf_missing_unlock_data_errors() {
223 let api_client = ApiClient::new_mocked(|_mock| {});
224 let client =
225 Client::init_test_account_with_api_client(test_bitwarden_com_account(), api_client)
226 .await;
227 client
228 .km_state_bridge()
229 .register_bridge(Box::new(InMemoryStateBridge::default()));
230
231 let result = client
232 .user_crypto_management()
233 .change_kdf(TEST_PASSWORD.to_string(), new_kdf())
234 .await;
235
236 assert!(matches!(
237 result,
238 Err(ChangeKdfError::MissingMasterPasswordUnlockData)
239 ));
240 }
241}