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 })
145 .await;
146 }
147
148 #[tokio::test]
149 async fn test_change_kdf_success_posts_and_persists() {
150 let api_client = ApiClient::new_mocked(|mock| {
151 mock.accounts_api
152 .expect_post_kdf()
153 .once()
154 .returning(|body| {
155 let body = body.expect("body should be Some");
156 assert_eq!(body.authentication_data.kdf.iterations, 700_000);
158 assert_eq!(body.unlock_data.kdf.iterations, 700_000);
159 assert!(!body.master_password_hash.is_empty());
160 Ok(())
161 });
162 });
163
164 let client =
165 Client::init_test_account_with_api_client(test_bitwarden_com_account(), api_client)
166 .await;
167 client
168 .km_state_bridge()
169 .register_bridge(Box::new(InMemoryStateBridge::default()));
170 test_unlock_data(&client).await;
171
172 client
173 .user_crypto_management()
174 .change_kdf(TEST_PASSWORD.to_string(), new_kdf())
175 .await
176 .unwrap();
177
178 let bridge = client.km_state_bridge();
180 assert_eq!(bridge.get_kdf_config().await, Some(new_kdf()));
181 let unlock = bridge
182 .get_masterpassword_unlock_data()
183 .await
184 .expect("unlock data persisted");
185 assert_eq!(unlock.kdf, new_kdf());
186 assert_eq!(client.internal.get_kdf().await.unwrap(), new_kdf());
188 }
189
190 #[tokio::test]
191 async fn test_change_kdf_api_failure_does_not_persist() {
192 let api_client = ApiClient::new_mocked(|mock| {
193 mock.accounts_api
194 .expect_post_kdf()
195 .once()
196 .returning(|_body| Err(std::io::Error::other("Simulated error").into()));
197 });
198
199 let client =
200 Client::init_test_account_with_api_client(test_bitwarden_com_account(), api_client)
201 .await;
202 client
203 .km_state_bridge()
204 .register_bridge(Box::new(InMemoryStateBridge::default()));
205 test_unlock_data(&client).await;
206
207 let current_kdf = client.internal.get_kdf().await.unwrap();
208
209 let result = client
210 .user_crypto_management()
211 .change_kdf(TEST_PASSWORD.to_string(), new_kdf())
212 .await;
213
214 assert!(matches!(result, Err(ChangeKdfError::Api(_))));
215 assert_eq!(client.km_state_bridge().get_kdf_config().await, None);
217 assert_eq!(client.internal.get_kdf().await.unwrap(), current_kdf);
218 }
219
220 #[tokio::test]
221 async fn test_change_kdf_missing_unlock_data_errors() {
222 let api_client = ApiClient::new_mocked(|_mock| {});
223 let client =
224 Client::init_test_account_with_api_client(test_bitwarden_com_account(), api_client)
225 .await;
226 client
227 .km_state_bridge()
228 .register_bridge(Box::new(InMemoryStateBridge::default()));
229
230 let result = client
231 .user_crypto_management()
232 .change_kdf(TEST_PASSWORD.to_string(), new_kdf())
233 .await;
234
235 assert!(matches!(
236 result,
237 Err(ChangeKdfError::MissingMasterPasswordUnlockData)
238 ));
239 }
240}