bitwarden_user_crypto_management/
key_connector_migration.rs1use bitwarden_api_api::models::KeyConnectorEnrollmentRequestModel;
4use bitwarden_api_key_connector::models::user_key_request_model::UserKeyKeyRequestModel;
5use bitwarden_core::key_management::SymmetricKeySlotId;
6use bitwarden_crypto::{EncString, KeyConnectorKey};
7use bitwarden_encoding::B64;
8use bitwarden_error::bitwarden_error;
9use thiserror::Error;
10use tracing::{error, info};
11
12use crate::UserCryptoManagementClient;
13
14#[bitwarden_ffi::wasm_export]
15impl UserCryptoManagementClient {
16 pub async fn migrate_to_key_connector(
20 &self,
21 key_connector_url: String,
22 ) -> Result<(), MigrateToKeyConnectorError> {
23 let internal = &self.client.internal;
24 let api_configuration = internal.get_api_configurations();
25 let key_connector_api_client = internal.get_key_connector_client(key_connector_url);
26
27 internal_migrate_to_key_connector(
28 self,
29 &api_configuration.api_client,
30 &key_connector_api_client,
31 )
32 .await
33 }
34}
35
36async fn internal_migrate_to_key_connector(
37 user_crypto_management_client: &UserCryptoManagementClient,
38 api_client: &bitwarden_api_api::apis::ApiClient,
39 key_connector_api_client: &bitwarden_api_key_connector::apis::ApiClient,
40) -> Result<(), MigrateToKeyConnectorError> {
41 let key_connector_key = KeyConnectorKey::make();
53
54 let key_connector_key_wrapped_user_key = {
56 let key_store = user_crypto_management_client
57 .client
58 .internal
59 .get_key_store();
60 let ctx = key_store.context();
61 key_connector_key
62 .wrap_user_key(SymmetricKeySlotId::User, &ctx)
63 .map_err(|_| MigrateToKeyConnectorError::UserKeyNotAvailable)?
64 };
65
66 info!("Posting key connector key to key connector server");
68 post_key_connector_key_to_key_connector(key_connector_api_client, key_connector_key).await?;
69
70 info!("Posting wrapped user key for key connector migration");
72 enroll_user_into_key_connector(api_client, key_connector_key_wrapped_user_key).await?;
73
74 info!("Successfully migrated account to key connector unlock");
75 Ok(())
76}
77
78async fn enroll_user_into_key_connector(
79 api_client: &bitwarden_api_api::apis::ApiClient,
80 key_connector_key_wrapped_user_key: EncString,
81) -> Result<(), MigrateToKeyConnectorError> {
82 let request = KeyConnectorEnrollmentRequestModel {
83 key_connector_key_wrapped_user_key: Some(key_connector_key_wrapped_user_key.to_string()),
84 };
85
86 api_client
87 .accounts_key_management_api()
88 .post_enroll_to_key_connector(Some(request))
89 .await
90 .map_err(|e| {
91 error!("Failed to post key connector migration request: {e:?}");
92 MigrateToKeyConnectorError::Api
93 })
94}
95
96async fn post_key_connector_key_to_key_connector(
97 key_connector_api_client: &bitwarden_api_key_connector::apis::ApiClient,
98 key_connector_key: KeyConnectorKey,
99) -> Result<(), MigrateToKeyConnectorError> {
100 let encoded_key_connector_key: B64 = key_connector_key.into();
101 let request = UserKeyKeyRequestModel {
102 key: encoded_key_connector_key.to_string(),
103 };
104
105 let result = if key_connector_api_client
108 .user_keys_api()
109 .get_user_key()
110 .await
111 .is_ok()
112 {
113 info!("User's key connector key exists, updating");
114 key_connector_api_client
115 .user_keys_api()
116 .put_user_key(request)
117 .await
118 } else {
119 info!("User's key connector key does not exist, creating");
120 key_connector_api_client
121 .user_keys_api()
122 .post_user_key(request)
123 .await
124 };
125
126 result.map_err(|e| {
127 error!("Failed to post key connector key to key connector server: {e:?}");
128 MigrateToKeyConnectorError::KeyConnectorApi
129 })
130}
131
132#[derive(Debug, Error)]
133#[bitwarden_error(flat)]
134pub enum MigrateToKeyConnectorError {
135 #[error("Current user key is not available")]
136 UserKeyNotAvailable,
137 #[error("Cryptographic error during key connector migration")]
138 Crypto,
139 #[error("Bitwarden API call failed during key connector migration")]
140 Api,
141 #[error("Key Connector API call failed during key connector migration")]
142 KeyConnectorApi,
143}
144
145#[cfg(test)]
146mod tests {
147 use bitwarden_api_api::apis::ApiClient;
148 use bitwarden_core::Client;
149 use bitwarden_crypto::EncString;
150
151 use super::*;
152
153 fn unlocked_client() -> UserCryptoManagementClient {
154 let client = Client::new(None);
155 {
156 let key_store = client.internal.get_key_store();
157 let mut ctx = key_store.context_mut();
158 let local_user_key =
159 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
160 let _ = ctx.persist_symmetric_key(local_user_key, SymmetricKeySlotId::User);
161 }
162
163 UserCryptoManagementClient::new(client)
164 }
165
166 #[tokio::test]
167 async fn test_migrate_to_key_connector_success() {
168 let user_crypto_management_client = unlocked_client();
169
170 let api_client = ApiClient::new_mocked(|mock| {
171 mock.accounts_key_management_api
172 .expect_post_enroll_to_key_connector()
173 .once()
174 .returning(move |body| {
175 let body = body.expect("body should be Some");
176 let wrapped_key = body
177 .key_connector_key_wrapped_user_key
178 .expect("key_connector_key_wrapped_user_key should be Some");
179 wrapped_key
180 .parse::<EncString>()
181 .expect("key_connector_key_wrapped_user_key should be a valid EncString");
182 Ok(())
183 });
184 });
185
186 let key_connector_api_client =
187 bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
188 mock.user_keys_api
189 .expect_get_user_key()
190 .once()
191 .returning(move || {
192 Err(bitwarden_api_key_connector::apis::Error::ResponseError(
193 bitwarden_api_key_connector::apis::ResponseContent {
194 status: reqwest::StatusCode::NOT_FOUND,
195 content: "Not Found".to_string(),
196 },
197 ))
198 });
199 mock.user_keys_api
200 .expect_post_user_key()
201 .once()
202 .returning(move |_body| Ok(()));
203 });
204
205 let result = internal_migrate_to_key_connector(
206 &user_crypto_management_client,
207 &api_client,
208 &key_connector_api_client,
209 )
210 .await;
211
212 assert!(result.is_ok());
213
214 if let ApiClient::Mock(mut mock) = api_client {
215 mock.accounts_key_management_api.checkpoint();
216 }
217 if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
218 key_connector_api_client
219 {
220 mock.user_keys_api.checkpoint();
221 }
222 }
223
224 #[tokio::test]
225 async fn test_migrate_to_key_connector_key_connector_api_failure() {
226 let user_crypto_management_client = unlocked_client();
227
228 let api_client = ApiClient::new_mocked(|mock| {
229 mock.accounts_key_management_api
230 .expect_post_enroll_to_key_connector()
231 .never();
232 });
233
234 let key_connector_api_client =
235 bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
236 mock.user_keys_api
237 .expect_get_user_key()
238 .once()
239 .returning(move || {
240 Err(bitwarden_api_key_connector::apis::Error::ResponseError(
241 bitwarden_api_key_connector::apis::ResponseContent {
242 status: reqwest::StatusCode::NOT_FOUND,
243 content: "Not Found".to_string(),
244 },
245 ))
246 });
247 mock.user_keys_api
248 .expect_post_user_key()
249 .once()
250 .returning(move |_body| {
251 Err(bitwarden_api_key_connector::apis::Error::Serde(
252 serde_json::Error::io(std::io::Error::other("API error")),
253 ))
254 });
255 });
256
257 let result = internal_migrate_to_key_connector(
258 &user_crypto_management_client,
259 &api_client,
260 &key_connector_api_client,
261 )
262 .await;
263
264 assert!(matches!(
265 result,
266 Err(MigrateToKeyConnectorError::KeyConnectorApi)
267 ));
268
269 if let ApiClient::Mock(mut mock) = api_client {
270 mock.accounts_key_management_api.checkpoint();
271 }
272 if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
273 key_connector_api_client
274 {
275 mock.user_keys_api.checkpoint();
276 }
277 }
278
279 #[tokio::test]
280 async fn test_migrate_to_key_connector_api_failure() {
281 let user_crypto_management_client = unlocked_client();
282
283 let api_client = ApiClient::new_mocked(|mock| {
284 mock.accounts_key_management_api
285 .expect_post_enroll_to_key_connector()
286 .once()
287 .returning(move |body| {
288 let body = body.expect("body should be Some");
289 let wrapped_key = body
290 .key_connector_key_wrapped_user_key
291 .expect("key_connector_key_wrapped_user_key should be Some");
292 wrapped_key
293 .parse::<EncString>()
294 .expect("key_connector_key_wrapped_user_key should be a valid EncString");
295 Err(serde_json::Error::io(std::io::Error::other("API error")).into())
296 });
297 });
298
299 let key_connector_api_client =
300 bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
301 mock.user_keys_api
302 .expect_get_user_key()
303 .once()
304 .returning(move || {
305 Err(bitwarden_api_key_connector::apis::Error::ResponseError(
306 bitwarden_api_key_connector::apis::ResponseContent {
307 status: reqwest::StatusCode::NOT_FOUND,
308 content: "Not Found".to_string(),
309 },
310 ))
311 });
312 mock.user_keys_api
313 .expect_post_user_key()
314 .once()
315 .returning(move |_body| Ok(()));
316 });
317
318 let result = internal_migrate_to_key_connector(
319 &user_crypto_management_client,
320 &api_client,
321 &key_connector_api_client,
322 )
323 .await;
324
325 assert!(matches!(result, Err(MigrateToKeyConnectorError::Api)));
326
327 if let ApiClient::Mock(mut mock) = api_client {
328 mock.accounts_key_management_api.checkpoint();
329 }
330 if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
331 key_connector_api_client
332 {
333 mock.user_keys_api.checkpoint();
334 }
335 }
336
337 #[tokio::test]
338 async fn test_migrate_to_key_connector_user_key_not_available() {
339 let user_crypto_management_client = UserCryptoManagementClient::new(Client::new(None));
340
341 let api_client = ApiClient::new_mocked(|mock| {
342 mock.accounts_key_management_api
343 .expect_post_enroll_to_key_connector()
344 .never();
345 });
346
347 let key_connector_api_client =
348 bitwarden_api_key_connector::apis::ApiClient::new_mocked(|mock| {
349 mock.user_keys_api.expect_get_user_key().never();
350 mock.user_keys_api.expect_post_user_key().never();
351 mock.user_keys_api.expect_put_user_key().never();
352 });
353
354 let result = internal_migrate_to_key_connector(
355 &user_crypto_management_client,
356 &api_client,
357 &key_connector_api_client,
358 )
359 .await;
360
361 assert!(matches!(
362 result,
363 Err(MigrateToKeyConnectorError::UserKeyNotAvailable)
364 ));
365
366 if let ApiClient::Mock(mut mock) = api_client {
367 mock.accounts_key_management_api.checkpoint();
368 }
369 if let bitwarden_api_key_connector::apis::ApiClient::Mock(mut mock) =
370 key_connector_api_client
371 {
372 mock.user_keys_api.checkpoint();
373 }
374 }
375}