1use rand::distr::SampleString as _;
2use sha2::Digest as _;
3use tokio::io::AsyncReadExt as _;
4
5use super::sso::{
6 classify_login_error, find_free_port, start_sso_callback_server,
7};
8use super::types::{KdfType, TwoFactorProviderType};
9use super::wire::{
10 CipherCard, CipherField, CipherIdentity, CipherLogin, CipherLoginUri,
11 CipherSecureNote, CiphersPostReq, CiphersPutReq, CiphersPutReqHistory,
12 ConnectErrorRes, ConnectRefreshTokenReq, ConnectRefreshTokenRes,
13 ConnectTokenAuth, ConnectTokenAuthCode, ConnectTokenClientCredentials,
14 ConnectTokenPassword, ConnectTokenReq, ConnectTokenRes, FoldersPostReq,
15 FoldersRes, FoldersResData, PreloginReq, PreloginRes, SendEmailLoginReq,
16 SyncRes,
17};
18use crate::json::{
19 DeserializeJsonWithPath as _, DeserializeJsonWithPathAsync as _,
20};
21use crate::prelude::*;
22
23const BITWARDEN_CLIENT: &str = "cli";
26
27const DEVICE_TYPE: u8 = 8;
29
30#[derive(Debug)]
31pub struct Client {
32 base_url: String,
33 identity_url: String,
34 ui_url: String,
35 client_cert_path: Option<std::path::PathBuf>,
36}
37
38impl Client {
39 pub fn new(
40 base_url: &str,
41 identity_url: &str,
42 ui_url: &str,
43 client_cert_path: Option<&std::path::Path>,
44 ) -> Self {
45 Self {
46 base_url: base_url.to_string(),
47 identity_url: identity_url.to_string(),
48 ui_url: ui_url.to_string(),
49 client_cert_path: client_cert_path
50 .map(std::path::Path::to_path_buf),
51 }
52 }
53
54 async fn reqwest_client(&self) -> Result<reqwest::Client> {
55 let mut default_headers = reqwest::header::HeaderMap::new();
56 default_headers.insert(
57 "Bitwarden-Client-Name",
58 reqwest::header::HeaderValue::from_static(BITWARDEN_CLIENT),
59 );
60 default_headers.insert(
61 "Bitwarden-Client-Version",
62 reqwest::header::HeaderValue::from_static(env!(
63 "CARGO_PKG_VERSION"
64 )),
65 );
66 default_headers.append(
67 "Device-Type",
68 reqwest::header::HeaderValue::from_str(&DEVICE_TYPE.to_string())
71 .unwrap(),
72 );
73 let user_agent = format!(
74 "{}/{}",
75 env!("CARGO_PKG_NAME"),
76 env!("CARGO_PKG_VERSION")
77 );
78 if let Some(client_cert_path) = self.client_cert_path.as_ref() {
79 let mut buf = Vec::new();
80 let mut f = tokio::fs::File::open(client_cert_path)
81 .await
82 .map_err(|e| Error::LoadClientCert {
83 source: e,
84 file: client_cert_path.clone(),
85 })?;
86 f.read_to_end(&mut buf).await.map_err(|e| {
87 Error::LoadClientCert {
88 source: e,
89 file: client_cert_path.clone(),
90 }
91 })?;
92 let pem = reqwest::Identity::from_pem(&buf)
93 .map_err(|e| Error::CreateReqwestClient { source: e })?;
94 Ok(reqwest::Client::builder()
95 .user_agent(user_agent)
96 .identity(pem)
97 .default_headers(default_headers)
98 .build()
99 .map_err(|e| Error::CreateReqwestClient { source: e })?)
100 } else {
101 Ok(reqwest::Client::builder()
102 .user_agent(user_agent)
103 .default_headers(default_headers)
104 .build()
105 .map_err(|e| Error::CreateReqwestClient { source: e })?)
106 }
107 }
108
109 pub async fn prelogin(
110 &self,
111 email: &str,
112 ) -> Result<(KdfType, u32, Option<u32>, Option<u32>)> {
113 let prelogin = PreloginReq {
114 email: email.to_string(),
115 };
116 let client = self.reqwest_client().await?;
117 let res = client
118 .post(self.identity_url("/accounts/prelogin"))
119 .json(&prelogin)
120 .send()
121 .await
122 .map_err(|source| Error::Reqwest { source })?;
123 let prelogin_res: PreloginRes = res.json_with_path().await?;
124 Ok((
125 prelogin_res.kdf,
126 prelogin_res.kdf_iterations,
127 prelogin_res.kdf_memory,
128 prelogin_res.kdf_parallelism,
129 ))
130 }
131
132 pub async fn register(
133 &self,
134 email: &str,
135 device_id: &str,
136 apikey: &crate::locked::ApiKey,
137 ) -> Result<()> {
138 let connect_req = ConnectTokenReq {
139 auth: ConnectTokenAuth::ClientCredentials(
140 ConnectTokenClientCredentials {
141 username: email.to_string(),
142 client_secret: String::from_utf8(
143 apikey.client_secret().to_vec(),
144 )
145 .unwrap(),
146 },
147 ),
148 grant_type: "client_credentials".to_string(),
149 scope: "api".to_string(),
150 client_id: String::from_utf8(apikey.client_id().to_vec())
152 .unwrap(),
153 device_type: u32::from(DEVICE_TYPE),
154 device_identifier: device_id.to_string(),
155 device_name: "bwx".to_string(),
156 device_push_token: String::new(),
157 two_factor_token: None,
158 two_factor_provider: None,
159 };
160 let client = self.reqwest_client().await?;
161 let res = client
162 .post(self.identity_url("/connect/token"))
163 .form(&connect_req)
164 .send()
165 .await
166 .map_err(|source| Error::Reqwest { source })?;
167 if res.status() == reqwest::StatusCode::OK {
168 Ok(())
169 } else {
170 let code = res.status().as_u16();
171 match res.text().await {
172 Ok(body) => match body.clone().json_with_path() {
173 Ok(json) => Err(classify_login_error(&json, code)),
174 Err(e) => {
175 log::warn!("{e}: {body}");
176 Err(Error::RequestFailed { status: code })
177 }
178 },
179 Err(e) => {
180 log::warn!("failed to read response body: {e}");
181 Err(Error::RequestFailed { status: code })
182 }
183 }
184 }
185 }
186
187 pub async fn login(
188 &self,
189 email: &str,
190 sso_id: Option<&str>,
191 device_id: &str,
192 password_hash: &crate::locked::PasswordHash,
193 two_factor_token: Option<&str>,
194 two_factor_provider: Option<TwoFactorProviderType>,
195 ) -> Result<(String, String, String)> {
196 let connect_req = match sso_id {
197 Some(sso_id) => {
198 let (sso_code, sso_code_verifier, callback_url) =
199 self.obtain_sso_code(sso_id).await?;
200
201 ConnectTokenReq {
202 auth: ConnectTokenAuth::AuthCode(ConnectTokenAuthCode {
203 code: sso_code,
204 code_verifier: sso_code_verifier,
205 redirect_uri: callback_url,
206 }),
207 grant_type: "authorization_code".to_string(),
208 scope: "api offline_access".to_string(),
209 client_id: "cli".to_string(),
210 device_type: u32::from(DEVICE_TYPE),
211 device_identifier: device_id.to_string(),
212 device_name: "bwx".to_string(),
213 device_push_token: String::new(),
214 two_factor_token: two_factor_token
215 .map(std::string::ToString::to_string),
216 two_factor_provider: two_factor_provider
217 .map(|ty| ty as u32),
218 }
219 }
220 None => ConnectTokenReq {
221 auth: ConnectTokenAuth::Password(ConnectTokenPassword {
222 username: email.to_string(),
223 password: crate::base64::encode(password_hash.hash()),
224 }),
225
226 grant_type: "password".to_string(),
227 scope: "api offline_access".to_string(),
228 client_id: "cli".to_string(),
229 device_type: 8,
230 device_identifier: device_id.to_string(),
231 device_name: "bwx".to_string(),
232 device_push_token: String::new(),
233 two_factor_token: two_factor_token
234 .map(std::string::ToString::to_string),
235 two_factor_provider: two_factor_provider.map(|ty| ty as u32),
236 },
237 };
238
239 let client = self.reqwest_client().await?;
240 let res = client
241 .post(self.identity_url("/connect/token"))
242 .form(&connect_req)
243 .header(
244 "auth-email",
245 crate::base64::encode_url_safe_no_pad(email),
246 )
247 .send()
248 .await
249 .map_err(|source| Error::Reqwest { source })?;
250
251 if res.status() == reqwest::StatusCode::OK {
252 let connect_res: ConnectTokenRes = res.json_with_path().await?;
253 Ok((
254 connect_res.access_token,
255 connect_res.refresh_token,
256 connect_res.key,
257 ))
258 } else {
259 let code = res.status().as_u16();
260 match res.text().await {
261 Ok(body) => match body.clone().json_with_path() {
262 Ok(json) => {
263 let json: ConnectErrorRes = json;
264 Err(classify_login_error(&json, code))
265 }
266 Err(e) => {
267 log::warn!("{e}: {body}");
268 Err(Error::RequestFailed { status: code })
269 }
270 },
271 Err(e) => {
272 log::warn!("failed to read response body: {e}");
273 Err(Error::RequestFailed { status: code })
274 }
275 }
276 }
277 }
278
279 pub async fn send_email_login(
280 &self,
281 email: &str,
282 device_id: &str,
283 sso_email_2fa_session_token: &str,
284 ) -> Result<()> {
285 let send_email_login_req = SendEmailLoginReq {
286 email: email.to_string(),
287 device_identifier: device_id.to_string(),
288 sso_email_2fa_session_token: sso_email_2fa_session_token
289 .to_string(),
290 };
291
292 let client = self.reqwest_client().await?;
293 let res = client
294 .post(self.api_url("/two-factor/send-email-login"))
295 .json(&send_email_login_req)
296 .header(
297 "auth-email",
298 crate::base64::encode_url_safe_no_pad(email),
299 )
300 .send()
301 .await
302 .map_err(|source| Error::Reqwest { source })?;
303
304 if res.status() == reqwest::StatusCode::OK {
305 Ok(())
306 } else {
307 let code = res.status().as_u16();
308 log::warn!("{code}: {:?}", res.text().await);
309 Err(Error::RequestFailed { status: code })
310 }
311 }
312
313 async fn obtain_sso_code(
314 &self,
315 sso_id: &str,
316 ) -> Result<(String, String, String)> {
317 let state =
318 rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 64);
319 let sso_code_verifier =
320 rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 64);
321
322 let mut hasher = sha2::Sha256::new();
323 hasher.update(sso_code_verifier.clone());
324 let code_challenge =
325 crate::base64::encode_url_safe_no_pad(hasher.finalize());
326
327 let port = find_free_port(8065, 8070).await?;
328
329 let listener = tokio::net::TcpListener::bind(("127.0.0.1", port))
330 .await
331 .map_err(|e| Error::CreateSSOCallbackServer { err: e })?;
332
333 let callback_server =
334 start_sso_callback_server(listener, state.as_str());
335
336 let callback_url =
337 "http://localhost:".to_string() + port.to_string().as_str();
338
339 let url = self.ui_url.clone()
340 + "/#/sso?clientId="
341 + "cli"
342 + "&redirectUri="
343 + urlencoding::encode(callback_url.as_str())
344 .into_owned()
345 .as_str()
346 + "&state="
347 + state.as_str()
348 + "&codeChallenge="
349 + code_challenge.as_str()
350 + "&identifier="
351 + sso_id;
352
353 #[cfg(feature = "sso-browser")]
354 open::that(&url)
355 .map_err(|e| Error::FailedToOpenWebBrowser { err: e })?;
356 #[cfg(not(feature = "sso-browser"))]
357 eprintln!("Open this URL to continue: {url}");
358 let sso_code = callback_server.await?;
363
364 Ok((sso_code, sso_code_verifier, callback_url))
365 }
366
367 pub async fn sync(
368 &self,
369 access_token: &str,
370 ) -> Result<(
371 String,
372 String,
373 std::collections::HashMap<String, String>,
374 Vec<crate::db::Entry>,
375 )> {
376 let client = self.reqwest_client().await?;
377 let res = client
378 .get(self.api_url("/sync"))
379 .header("Authorization", format!("Bearer {access_token}"))
380 .header("Bitwarden-Client-Version", "2024.12.0")
382 .send()
383 .await
384 .map_err(|source| Error::Reqwest { source })?;
385 match res.status() {
386 reqwest::StatusCode::OK => {
387 let sync_res: SyncRes = res.json_with_path().await?;
388 let folders = sync_res.folders.clone();
389 let ciphers = sync_res
390 .ciphers
391 .iter()
392 .filter_map(|cipher| cipher.to_entry(&folders))
393 .collect();
394 let org_keys = sync_res
395 .profile
396 .organizations
397 .iter()
398 .map(|org| (org.id.clone(), org.key.clone()))
399 .collect();
400 Ok((
401 sync_res.profile.key,
402 sync_res.profile.private_key,
403 org_keys,
404 ciphers,
405 ))
406 }
407 reqwest::StatusCode::UNAUTHORIZED => {
408 Err(Error::RequestUnauthorized)
409 }
410 _ => Err(Error::RequestFailed {
411 status: res.status().as_u16(),
412 }),
413 }
414 }
415
416 pub fn add(
417 &self,
418 access_token: &str,
419 name: &str,
420 data: &crate::db::EntryData,
421 notes: Option<&str>,
422 folder_id: Option<&str>,
423 ) -> Result<()> {
424 let mut req = CiphersPostReq {
425 ty: 1,
426 folder_id: folder_id.map(std::string::ToString::to_string),
427 name: name.to_string(),
428 notes: notes.map(std::string::ToString::to_string),
429 login: None,
430 card: None,
431 identity: None,
432 secure_note: None,
433 };
434 match data {
435 crate::db::EntryData::Login {
436 username,
437 password,
438 totp,
439 uris,
440 } => {
441 let uris = if uris.is_empty() {
442 None
443 } else {
444 Some(
445 uris.iter()
446 .map(|s| CipherLoginUri {
447 uri: Some(s.uri.clone()),
448 match_type: s.match_type,
449 })
450 .collect(),
451 )
452 };
453 req.login = Some(CipherLogin {
454 username: username.clone(),
455 password: password.clone(),
456 totp: totp.clone(),
457 uris,
458 });
459 }
460 crate::db::EntryData::Card {
461 cardholder_name,
462 number,
463 brand,
464 exp_month,
465 exp_year,
466 code,
467 } => {
468 req.card = Some(CipherCard {
469 cardholder_name: cardholder_name.clone(),
470 number: number.clone(),
471 brand: brand.clone(),
472 exp_month: exp_month.clone(),
473 exp_year: exp_year.clone(),
474 code: code.clone(),
475 });
476 }
477 crate::db::EntryData::Identity {
478 title,
479 first_name,
480 middle_name,
481 last_name,
482 address1,
483 address2,
484 address3,
485 city,
486 state,
487 postal_code,
488 country,
489 phone,
490 email,
491 ssn,
492 license_number,
493 passport_number,
494 username,
495 } => {
496 req.identity = Some(CipherIdentity {
497 title: title.clone(),
498 first_name: first_name.clone(),
499 middle_name: middle_name.clone(),
500 last_name: last_name.clone(),
501 address1: address1.clone(),
502 address2: address2.clone(),
503 address3: address3.clone(),
504 city: city.clone(),
505 state: state.clone(),
506 postal_code: postal_code.clone(),
507 country: country.clone(),
508 phone: phone.clone(),
509 email: email.clone(),
510 ssn: ssn.clone(),
511 license_number: license_number.clone(),
512 passport_number: passport_number.clone(),
513 username: username.clone(),
514 });
515 }
516 crate::db::EntryData::SecureNote => {
517 req.secure_note = Some(CipherSecureNote {});
518 }
519 crate::db::EntryData::SshKey { .. } => unreachable!(),
520 }
521 let client = reqwest::blocking::Client::new();
522 let res = client
523 .post(self.api_url("/ciphers"))
524 .header("Authorization", format!("Bearer {access_token}"))
525 .json(&req)
526 .send()
527 .map_err(|source| Error::Reqwest { source })?;
528 match res.status() {
529 reqwest::StatusCode::OK => Ok(()),
530 reqwest::StatusCode::UNAUTHORIZED => {
531 Err(Error::RequestUnauthorized)
532 }
533 _ => Err(Error::RequestFailed {
534 status: res.status().as_u16(),
535 }),
536 }
537 }
538
539 pub fn edit(
540 &self,
541 access_token: &str,
542 id: &str,
543 org_id: Option<&str>,
544 name: &str,
545 data: &crate::db::EntryData,
546 fields: &[crate::db::Field],
547 notes: Option<&str>,
548 folder_uuid: Option<&str>,
549 history: &[crate::db::HistoryEntry],
550 ) -> Result<()> {
551 let mut req = CiphersPutReq {
552 ty: match data {
553 crate::db::EntryData::Login { .. } => 1,
554 crate::db::EntryData::SecureNote => 2,
555 crate::db::EntryData::Card { .. } => 3,
556 crate::db::EntryData::Identity { .. } => 4,
557 crate::db::EntryData::SshKey { .. } => unreachable!(),
558 },
559 folder_id: folder_uuid.map(std::string::ToString::to_string),
560 organization_id: org_id.map(std::string::ToString::to_string),
561 name: name.to_string(),
562 notes: notes.map(std::string::ToString::to_string),
563 login: None,
564 card: None,
565 identity: None,
566 secure_note: None,
567 fields: fields
568 .iter()
569 .map(|field| CipherField {
570 ty: field.ty,
571 name: field.name.clone(),
572 value: field.value.clone(),
573 linked_id: field.linked_id,
574 })
575 .collect(),
576 password_history: history
577 .iter()
578 .map(|entry| CiphersPutReqHistory {
579 last_used_date: entry.last_used_date.clone(),
580 password: entry.password.clone(),
581 })
582 .collect(),
583 };
584 match data {
585 crate::db::EntryData::Login {
586 username,
587 password,
588 totp,
589 uris,
590 } => {
591 let uris = if uris.is_empty() {
592 None
593 } else {
594 Some(
595 uris.iter()
596 .map(|s| CipherLoginUri {
597 uri: Some(s.uri.clone()),
598 match_type: s.match_type,
599 })
600 .collect(),
601 )
602 };
603 req.login = Some(CipherLogin {
604 username: username.clone(),
605 password: password.clone(),
606 totp: totp.clone(),
607 uris,
608 });
609 }
610 crate::db::EntryData::Card {
611 cardholder_name,
612 number,
613 brand,
614 exp_month,
615 exp_year,
616 code,
617 } => {
618 req.card = Some(CipherCard {
619 cardholder_name: cardholder_name.clone(),
620 number: number.clone(),
621 brand: brand.clone(),
622 exp_month: exp_month.clone(),
623 exp_year: exp_year.clone(),
624 code: code.clone(),
625 });
626 }
627 crate::db::EntryData::Identity {
628 title,
629 first_name,
630 middle_name,
631 last_name,
632 address1,
633 address2,
634 address3,
635 city,
636 state,
637 postal_code,
638 country,
639 phone,
640 email,
641 ssn,
642 license_number,
643 passport_number,
644 username,
645 } => {
646 req.identity = Some(CipherIdentity {
647 title: title.clone(),
648 first_name: first_name.clone(),
649 middle_name: middle_name.clone(),
650 last_name: last_name.clone(),
651 address1: address1.clone(),
652 address2: address2.clone(),
653 address3: address3.clone(),
654 city: city.clone(),
655 state: state.clone(),
656 postal_code: postal_code.clone(),
657 country: country.clone(),
658 phone: phone.clone(),
659 email: email.clone(),
660 ssn: ssn.clone(),
661 license_number: license_number.clone(),
662 passport_number: passport_number.clone(),
663 username: username.clone(),
664 });
665 }
666 crate::db::EntryData::SecureNote => {
667 req.secure_note = Some(CipherSecureNote {});
668 }
669 crate::db::EntryData::SshKey { .. } => unreachable!(),
670 }
671 let client = reqwest::blocking::Client::new();
672 let res = client
673 .put(self.api_url(&format!("/ciphers/{id}")))
674 .header("Authorization", format!("Bearer {access_token}"))
675 .json(&req)
676 .send()
677 .map_err(|source| Error::Reqwest { source })?;
678 match res.status() {
679 reqwest::StatusCode::OK => Ok(()),
680 reqwest::StatusCode::UNAUTHORIZED => {
681 Err(Error::RequestUnauthorized)
682 }
683 _ => Err(Error::RequestFailed {
684 status: res.status().as_u16(),
685 }),
686 }
687 }
688
689 pub fn remove(&self, access_token: &str, id: &str) -> Result<()> {
690 let client = reqwest::blocking::Client::new();
691 let res = client
692 .delete(self.api_url(&format!("/ciphers/{id}")))
693 .header("Authorization", format!("Bearer {access_token}"))
694 .send()
695 .map_err(|source| Error::Reqwest { source })?;
696 match res.status() {
697 reqwest::StatusCode::OK => Ok(()),
698 reqwest::StatusCode::UNAUTHORIZED => {
699 Err(Error::RequestUnauthorized)
700 }
701 _ => Err(Error::RequestFailed {
702 status: res.status().as_u16(),
703 }),
704 }
705 }
706
707 pub fn folders(
708 &self,
709 access_token: &str,
710 ) -> Result<Vec<(String, String)>> {
711 let client = reqwest::blocking::Client::new();
712 let res = client
713 .get(self.api_url("/folders"))
714 .header("Authorization", format!("Bearer {access_token}"))
715 .send()
716 .map_err(|source| Error::Reqwest { source })?;
717 match res.status() {
718 reqwest::StatusCode::OK => {
719 let folders_res: FoldersRes = res.json_with_path()?;
720 Ok(folders_res
721 .data
722 .iter()
723 .map(|folder| (folder.id.clone(), folder.name.clone()))
724 .collect())
725 }
726 reqwest::StatusCode::UNAUTHORIZED => {
727 Err(Error::RequestUnauthorized)
728 }
729 _ => Err(Error::RequestFailed {
730 status: res.status().as_u16(),
731 }),
732 }
733 }
734
735 pub fn create_folder(
736 &self,
737 access_token: &str,
738 name: &str,
739 ) -> Result<String> {
740 let req = FoldersPostReq {
741 name: name.to_string(),
742 };
743 let client = reqwest::blocking::Client::new();
744 let res = client
745 .post(self.api_url("/folders"))
746 .header("Authorization", format!("Bearer {access_token}"))
747 .json(&req)
748 .send()
749 .map_err(|source| Error::Reqwest { source })?;
750 match res.status() {
751 reqwest::StatusCode::OK => {
752 let folders_res: FoldersResData = res.json_with_path()?;
753 Ok(folders_res.id)
754 }
755 reqwest::StatusCode::UNAUTHORIZED => {
756 Err(Error::RequestUnauthorized)
757 }
758 _ => Err(Error::RequestFailed {
759 status: res.status().as_u16(),
760 }),
761 }
762 }
763
764 pub fn exchange_refresh_token(
765 &self,
766 refresh_token: &str,
767 ) -> Result<String> {
768 let connect_req = ConnectRefreshTokenReq {
769 grant_type: "refresh_token".to_string(),
770 client_id: "cli".to_string(),
771 refresh_token: refresh_token.to_string(),
772 };
773 let client = reqwest::blocking::Client::new();
774 let res = client
775 .post(self.identity_url("/connect/token"))
776 .form(&connect_req)
777 .send()
778 .map_err(|source| Error::Reqwest { source })?;
779 let connect_res: ConnectRefreshTokenRes = res.json_with_path()?;
780 Ok(connect_res.access_token)
781 }
782
783 pub async fn exchange_refresh_token_async(
784 &self,
785 refresh_token: &str,
786 ) -> Result<String> {
787 let connect_req = ConnectRefreshTokenReq {
788 grant_type: "refresh_token".to_string(),
789 client_id: "cli".to_string(),
790 refresh_token: refresh_token.to_string(),
791 };
792 let client = self.reqwest_client().await?;
793 let res = client
794 .post(self.identity_url("/connect/token"))
795 .form(&connect_req)
796 .send()
797 .await
798 .map_err(|source| Error::Reqwest { source })?;
799 let connect_res: ConnectRefreshTokenRes =
800 res.json_with_path().await?;
801 Ok(connect_res.access_token)
802 }
803
804 fn api_url(&self, path: &str) -> String {
805 format!("{}{}", self.base_url, path)
806 }
807
808 fn identity_url(&self, path: &str) -> String {
809 format!("{}{}", self.identity_url, path)
810 }
811}