1use bitwarden_auth::send_access::{
25 SendAccessCredentials, SendAccessTokenError, SendAccessTokenRequest, SendAccessTokenResponse,
26 SendEmailCredentials, SendEmailOtpCredentials, SendPasswordCredentials,
27 api::{
28 SendAccessTokenApiErrorResponse, SendAccessTokenInvalidGrantError,
29 SendAccessTokenInvalidRequestError,
30 },
31};
32use bitwarden_core::get_host_platform_info;
33use bitwarden_pm::PasswordManagerClient;
34use bitwarden_send::{SendAccessKey, SendAccessView, SendType};
35use color_eyre::eyre::{Context as _, Result, bail, eyre};
36use inquire::{Password, Text, validator::Validation};
37use url::Url;
38
39use super::send_access_token_cache;
40use crate::{
41 platform::{ConfigFile, read_config_json},
42 render::{CommandOutput, CommandResult},
43 tools::file_output::{default_send_file_name, reject_path_traversal, save_file},
44};
45
46const CLOUD_HOSTS: &[CloudRegion] = &[
53 CloudRegion {
54 hosts: &["send.bitwarden.com", "vault.bitwarden.com"],
55 api_url: "https://api.bitwarden.com",
56 identity_url: "https://identity.bitwarden.com",
57 },
58 CloudRegion {
59 hosts: &["send.bitwarden.eu", "vault.bitwarden.eu"],
60 api_url: "https://api.bitwarden.eu",
61 identity_url: "https://identity.bitwarden.eu",
62 },
63];
64
65struct CloudRegion {
66 hosts: &'static [&'static str],
67 api_url: &'static str,
68 identity_url: &'static str,
69}
70
71pub(crate) struct ReceiveInputs {
74 pub url: String,
76 pub password: Option<String>,
78 pub passwordenv: Option<String>,
80 pub passwordfile: Option<String>,
82 pub output_path: Option<String>,
88 pub full_object: bool,
90}
91
92pub(crate) async fn run_receive(inputs: ReceiveInputs) -> CommandResult {
96 let url = Url::parse(&inputs.url).wrap_err("Failed to parse the provided Send url")?;
97 let (send_id, key_b64) = parse_send_url(&url)?;
98
99 let access_key = SendAccessKey::from_url_b64(&key_b64)
103 .map_err(|_| eyre!("Failed to parse url, the url provided is not a valid Send url"))?;
104
105 let (api_url, identity_url, trusted) =
106 resolve_urls(&url, read_config_json().ok().flatten().as_ref());
107 let cache_host = format!("{api_url}|{identity_url}");
112 let client = PasswordManagerClient::new(Some(
113 get_host_platform_info().to_client_settings(api_url, identity_url),
114 ));
115
116 if trusted && let Some(token) = send_access_token_cache::get(&cache_host, &send_id) {
117 return match render_access(&client, &access_key, token, &inputs).await {
118 Ok(output) => Ok(output),
119 Err(err) => {
120 send_access_token_cache::evict(&cache_host, &send_id);
124 Err(err)
125 }
126 };
127 }
128
129 let token_response = attempt_access(&client, &send_id, &access_key, &inputs).await?;
130 if trusted {
131 send_access_token_cache::set(
132 &cache_host,
133 &send_id,
134 &token_response.token,
135 token_response.expires_at,
136 );
137 }
138 render_access(&client, &access_key, token_response.token, &inputs).await
139}
140
141fn parse_send_url(url: &Url) -> Result<(String, String)> {
148 let fragment = url.fragment().unwrap_or_default();
149 let mut trailing = fragment.rsplit('/');
150 let key = trailing.next().unwrap_or_default();
151 let id = trailing.next().unwrap_or_default();
152
153 if id.trim().is_empty() || key.trim().is_empty() {
154 return Err(eyre!(
155 "Failed to parse url, the url provided is not a valid Send url"
156 ));
157 }
158
159 Ok((id.to_string(), key.to_string()))
160}
161
162fn resolve_urls(url: &Url, config: Option<&ConfigFile>) -> (String, String, bool) {
187 if let Some(host) = url.host_str()
188 && let Some(region) = CLOUD_HOSTS
189 .iter()
190 .find(|region| region.hosts.contains(&host))
191 {
192 return (
193 region.api_url.to_string(),
194 region.identity_url.to_string(),
195 true,
196 );
197 }
198
199 let origin = url.origin().ascii_serialization();
200
201 if let Some(config) = config
202 && let Some(base) = [config.web_vault.as_deref(), config.server.as_deref()]
203 .into_iter()
204 .flatten()
205 .find(|configured| same_origin(configured, &origin))
206 {
207 let base = base.trim_end_matches('/');
208 let api = trimmed(config.api.as_deref()).unwrap_or_else(|| format!("{base}/api"));
209 let identity =
210 trimmed(config.identity.as_deref()).unwrap_or_else(|| format!("{base}/identity"));
211 return (api, identity, true);
212 }
213
214 (format!("{origin}/api"), format!("{origin}/identity"), false)
215}
216
217fn same_origin(configured: &str, origin: &str) -> bool {
221 match Url::parse(configured) {
222 Ok(parsed) => parsed.origin().ascii_serialization() == origin,
223 Err(_) => configured.trim_end_matches('/') == origin,
224 }
225}
226
227fn trimmed(value: Option<&str>) -> Option<String> {
228 value
229 .map(|v| v.trim_end_matches('/'))
230 .filter(|v| !v.is_empty())
231 .map(str::to_string)
232}
233
234async fn attempt_access(
245 client: &PasswordManagerClient,
246 send_id: &str,
247 access_key: &SendAccessKey,
248 inputs: &ReceiveInputs,
249) -> Result<SendAccessTokenResponse> {
250 match request_token(client, send_id, None).await {
251 Ok(token) => Ok(token),
252 Err(err) => match invalid_request_type(&err) {
253 Some(SendAccessTokenInvalidRequestError::PasswordHashB64Required) => {
254 access_with_password(client, send_id, access_key, inputs).await
255 }
256 Some(SendAccessTokenInvalidRequestError::EmailRequired) => {
257 access_with_email_otp(client, send_id).await
258 }
259 _ if invalid_grant_type(&err)
260 == Some(&SendAccessTokenInvalidGrantError::SendIdInvalid) =>
261 {
262 Err(eyre!("Not found."))
263 }
264 _ => Err(token_error(err)),
265 },
266 }
267}
268
269async fn access_with_password(
272 client: &PasswordManagerClient,
273 send_id: &str,
274 access_key: &SendAccessKey,
275 inputs: &ReceiveInputs,
276) -> Result<SendAccessTokenResponse> {
277 let password = resolve_password(inputs)?;
278 let credentials = SendAccessCredentials::Password(SendPasswordCredentials {
279 password_hash_b64: access_key.hash_password_b64(&password),
280 });
281
282 match request_token(client, send_id, Some(credentials)).await {
283 Ok(token) => Ok(token),
284 Err(err)
285 if invalid_grant_type(&err)
286 == Some(&SendAccessTokenInvalidGrantError::PasswordHashB64Invalid) =>
287 {
288 Err(eyre!("Invalid password"))
289 }
290 Err(err) => Err(token_error(err)),
291 }
292}
293
294async fn access_with_email_otp(
297 client: &PasswordManagerClient,
298 send_id: &str,
299) -> Result<SendAccessTokenResponse> {
300 if !can_interact() {
301 return Err(eyre!(
302 "Email verification required. Run in interactive mode."
303 ));
304 }
305
306 let email = prompt_email()?;
307 let credentials = SendAccessCredentials::Email(SendEmailCredentials {
308 email: email.clone(),
309 });
310
311 let err = match request_token(client, send_id, Some(credentials)).await {
312 Ok(_) => return Err(eyre!("Unexpected server response")),
315 Err(err) => err,
316 };
317
318 if invalid_request_type(&err) != Some(&SendAccessTokenInvalidRequestError::EmailAndOtpRequired)
319 {
320 return Err(token_error(err));
321 }
322
323 let otp = prompt_otp()?;
324 let credentials = SendAccessCredentials::EmailOtp(SendEmailOtpCredentials { email, otp });
325
326 match request_token(client, send_id, Some(credentials)).await {
327 Ok(token) => Ok(token),
328 Err(err)
330 if invalid_request_type(&err)
331 == Some(&SendAccessTokenInvalidRequestError::EmailAndOtpRequired) =>
332 {
333 Err(eyre!("Invalid email or verification code"))
334 }
335 Err(err) => Err(token_error(err)),
336 }
337}
338
339async fn request_token(
340 client: &PasswordManagerClient,
341 send_id: &str,
342 credentials: Option<SendAccessCredentials>,
343) -> Result<SendAccessTokenResponse, SendAccessTokenError> {
344 client
345 .auth()
346 .send_access()
347 .request_send_access_token(SendAccessTokenRequest {
348 send_id: send_id.to_string(),
349 send_access_credentials: credentials,
350 })
351 .await
352}
353
354fn invalid_request_type(err: &SendAccessTokenError) -> Option<&SendAccessTokenInvalidRequestError> {
360 match err {
361 SendAccessTokenError::Expected(SendAccessTokenApiErrorResponse::InvalidRequest {
362 send_access_error_type,
363 ..
364 }) => send_access_error_type.as_ref(),
365 _ => None,
366 }
367}
368
369fn invalid_grant_type(err: &SendAccessTokenError) -> Option<&SendAccessTokenInvalidGrantError> {
372 match err {
373 SendAccessTokenError::Expected(SendAccessTokenApiErrorResponse::InvalidGrant {
374 send_access_error_type,
375 ..
376 }) => send_access_error_type.as_ref(),
377 _ => None,
378 }
379}
380
381fn token_error(err: SendAccessTokenError) -> color_eyre::eyre::Error {
385 match err {
386 SendAccessTokenError::Unexpected(inner) => eyre!("Server error: {inner:?}"),
387 SendAccessTokenError::Expected(inner) => eyre!("Error: {inner:?}"),
388 }
389}
390
391#[allow(deprecated)]
395async fn render_access(
396 client: &PasswordManagerClient,
397 access_key: &SendAccessKey,
398 token: String,
399 inputs: &ReceiveInputs,
400) -> CommandResult {
401 let response = client.sends().access_send(token.clone()).await?;
402 let view = access_key.decrypt_response(response)?;
403
404 if inputs.full_object {
405 return Ok(CommandOutput::Object(Box::new(view)));
406 }
407
408 match view.type_ {
409 Some(SendType::Text) => Ok(CommandOutput::Plain(
413 view.text.and_then(|text| text.text).unwrap_or_default(),
414 )),
415 Some(SendType::File) => {
416 save_file_send(
417 client,
418 access_key,
419 &token,
420 view,
421 inputs.output_path.as_deref(),
422 )
423 .await
424 }
425 Some(SendType::Item) => {
426 bail!("Accessing item Sends is not supported by the CLI.");
427 }
428 None => Ok(CommandOutput::Object(Box::new(view))),
431 }
432}
433
434#[allow(deprecated)]
438async fn save_file_send(
439 client: &PasswordManagerClient,
440 access_key: &SendAccessKey,
441 token: &str,
442 view: SendAccessView,
443 output: Option<&str>,
444) -> CommandResult {
445 let file = view
446 .file
447 .ok_or_else(|| eyre!("The Send is a file Send but carries no file metadata."))?;
448 let file_id = file
449 .id
450 .ok_or_else(|| eyre!("The Send's file is missing an id; cannot download it."))?;
451
452 let download = client
453 .sends()
454 .get_file_download_data(token.to_string(), file_id)
455 .await?;
456 let download_url = download
457 .url
458 .ok_or_else(|| eyre!("The server did not return a download url for the Send's file."))?;
459
460 let encrypted = download_bytes(client, &download_url).await?;
461 let decrypted = access_key.decrypt_file_buffer(&encrypted)?;
462
463 let file_name = default_send_file_name(file.file_name.as_deref());
464 let path = save_file(output, &file_name, &decrypted)?;
465
466 Ok(CommandOutput::Plain(format!("Saved {}", path.display())))
467}
468
469async fn download_bytes(client: &PasswordManagerClient, url: &str) -> Result<Vec<u8>> {
476 let response = client
477 .0
478 .internal
479 .get_http_client()
480 .get(url)
481 .header("cache-control", "no-cache")
483 .send()
484 .await?;
485
486 if !response.status().is_success() {
487 return Err(eyre!(
488 "A {} error occurred while downloading the attachment.",
489 response.status().as_u16()
490 ));
491 }
492
493 Ok(response.bytes().await?.to_vec())
494}
495
496fn resolve_password(inputs: &ReceiveInputs) -> Result<String> {
501 let supplied = password_from_args(
502 inputs.password.as_deref(),
503 inputs.passwordfile.as_deref(),
504 inputs.passwordenv.as_deref(),
505 )?;
506
507 let password = match supplied {
508 Some(password) => password,
509 None if can_interact() => prompt_password()?,
510 None => return Err(eyre!("Password required")),
511 };
512
513 if password.is_empty() {
514 return Err(eyre!("Password required"));
515 }
516
517 Ok(password)
518}
519
520fn password_from_args(
527 password: Option<&str>,
528 passwordfile: Option<&str>,
529 passwordenv: Option<&str>,
530) -> Result<Option<String>> {
531 if let Some(password) = password.filter(|p| !p.is_empty()) {
532 return Ok(Some(password.to_string()));
533 }
534
535 if let Some(path) = passwordfile {
536 reject_path_traversal("--passwordfile", path)?;
537 let contents = std::fs::read_to_string(path)
538 .wrap_err_with(|| format!("Could not read --passwordfile {path}"))?;
539 return Ok(contents.lines().next().map(str::to_string));
543 }
544
545 if let Some(name) = passwordenv {
546 return match std::env::var(name) {
547 Ok(value) => Ok(Some(value)),
548 Err(_) => {
551 tracing::warn!("--passwordenv variable `{name}` is not set; ignoring it.");
552 Ok(None)
553 }
554 };
555 }
556
557 Ok(None)
558}
559
560fn can_interact() -> bool {
569 std::env::var("BW_NOINTERACTION").as_deref() != Ok("true")
570}
571
572fn prompt_password() -> Result<String> {
576 Ok(Password::new("Send password")
577 .without_confirmation()
578 .prompt()?)
579}
580
581fn prompt_email() -> Result<String> {
582 Ok(Text::new("Enter your email address:")
583 .with_validator(|input: &str| {
584 if input.contains('@') {
585 Ok(Validation::Valid)
586 } else {
587 Ok(Validation::Invalid(
588 "Please enter a valid email address".into(),
589 ))
590 }
591 })
592 .prompt()?)
593}
594
595fn prompt_otp() -> Result<String> {
596 Ok(Text::new("Enter the verification code sent to your email:").prompt()?)
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602
603 fn parse(url: &str) -> Url {
604 Url::parse(url).expect("test url parses")
605 }
606
607 #[test]
610 fn parses_the_web_vault_send_route() {
611 let (id, key) = parse_send_url(&parse(
612 "https://vault.bitwarden.com/#/send/access-id/Pgui0FK85cNhBGWHAlBHBw",
613 ))
614 .unwrap();
615 assert_eq!(id, "access-id");
616 assert_eq!(key, "Pgui0FK85cNhBGWHAlBHBw");
617 }
618
619 #[test]
620 fn parses_the_send_vanity_host_route() {
621 let (id, key) = parse_send_url(&parse(
623 "https://send.bitwarden.com/#access-id/Pgui0FK85cNhBGWHAlBHBw",
624 ))
625 .unwrap();
626 assert_eq!(id, "access-id");
627 assert_eq!(key, "Pgui0FK85cNhBGWHAlBHBw");
628 }
629
630 #[test]
631 fn parses_self_hosted_links_with_a_path_prefix() {
632 let (id, key) = parse_send_url(&parse(
633 "https://example.com/bitwarden/#/send/access-id/Pgui0FK85cNhBGWHAlBHBw",
634 ))
635 .unwrap();
636 assert_eq!(id, "access-id");
637 assert_eq!(key, "Pgui0FK85cNhBGWHAlBHBw");
638 }
639
640 #[test]
641 fn rejects_a_url_without_a_fragment() {
642 let err = parse_send_url(&parse("https://vault.bitwarden.com/")).unwrap_err();
643 assert!(
644 err.to_string().contains("not a valid Send url"),
645 "got: {err}"
646 );
647 }
648
649 #[test]
650 fn rejects_a_fragment_with_only_one_segment() {
651 let err = parse_send_url(&parse("https://send.bitwarden.com/#access-id")).unwrap_err();
652 assert!(
653 err.to_string().contains("not a valid Send url"),
654 "got: {err}"
655 );
656 }
657
658 #[test]
659 fn rejects_a_fragment_with_a_blank_segment() {
660 assert!(
662 parse_send_url(&parse("https://vault.bitwarden.com/#/send//key")).is_err(),
663 "blank id must be rejected"
664 );
665 assert!(
666 parse_send_url(&parse("https://vault.bitwarden.com/#/send/id/")).is_err(),
667 "blank key must be rejected"
668 );
669 }
670
671 #[test]
674 fn cloud_send_and_vault_hosts_map_to_their_region() {
675 for host in ["send.bitwarden.com", "vault.bitwarden.com"] {
676 let (api, identity, trusted) =
677 resolve_urls(&parse(&format!("https://{host}/#id/key")), None);
678 assert_eq!(api, "https://api.bitwarden.com");
679 assert_eq!(identity, "https://identity.bitwarden.com");
680 assert!(trusted, "a known cloud host must be trusted");
681 }
682 for host in ["send.bitwarden.eu", "vault.bitwarden.eu"] {
683 let (api, identity, trusted) =
684 resolve_urls(&parse(&format!("https://{host}/#id/key")), None);
685 assert_eq!(api, "https://api.bitwarden.eu");
686 assert_eq!(identity, "https://identity.bitwarden.eu");
687 assert!(trusted, "a known cloud host must be trusted");
688 }
689 }
690
691 #[test]
695 fn a_lookalike_host_is_never_treated_as_cloud() {
696 for host in [
697 "send.bitwarden.com.evil.tld",
698 "evil-send.bitwarden.com.attacker.example",
699 "notsend.bitwarden.com.evil.tld",
700 ] {
701 let (api, identity, trusted) =
702 resolve_urls(&parse(&format!("https://{host}/#id/key")), None);
703 assert_eq!(api, format!("https://{host}/api"));
704 assert_eq!(identity, format!("https://{host}/identity"));
705 assert!(!trusted, "a lookalike host must not be trusted");
706 }
707 }
708
709 #[test]
710 fn unknown_host_falls_back_to_the_single_domain_convention() {
711 let (api, identity, trusted) =
712 resolve_urls(&parse("https://vault.example.com/#/send/id/key"), None);
713 assert_eq!(api, "https://vault.example.com/api");
714 assert_eq!(identity, "https://vault.example.com/identity");
715 assert!(
716 !trusted,
717 "an unknown, unconfigured host must not be trusted"
718 );
719 }
720
721 #[test]
722 fn a_non_default_port_is_preserved_in_the_fallback() {
723 let (api, identity, trusted) =
724 resolve_urls(&parse("https://localhost:8080/#/send/id/key"), None);
725 assert_eq!(api, "https://localhost:8080/api");
726 assert_eq!(identity, "https://localhost:8080/identity");
727 assert!(!trusted);
728 }
729
730 #[test]
731 fn a_matching_configured_server_supplies_the_base() {
732 let config = ConfigFile {
733 server: Some("https://bw.example.com".to_string()),
734 ..Default::default()
735 };
736 let (api, identity, trusted) = resolve_urls(
737 &parse("https://bw.example.com/#/send/id/key"),
738 Some(&config),
739 );
740 assert_eq!(api, "https://bw.example.com/api");
741 assert_eq!(identity, "https://bw.example.com/identity");
742 assert!(trusted, "the locally configured deployment must be trusted");
743 }
744
745 #[test]
746 fn a_matching_configured_web_vault_supplies_the_base() {
747 let config = ConfigFile {
748 web_vault: Some("https://vault.example.com/".to_string()),
749 ..Default::default()
750 };
751 let (api, identity, trusted) = resolve_urls(
752 &parse("https://vault.example.com/#/send/id/key"),
753 Some(&config),
754 );
755 assert_eq!(api, "https://vault.example.com/api");
756 assert_eq!(identity, "https://vault.example.com/identity");
757 assert!(trusted);
758 }
759
760 #[test]
763 fn explicit_configured_api_and_identity_win_over_the_base() {
764 let config = ConfigFile {
765 web_vault: Some("https://vault.example.com".to_string()),
766 api: Some("https://api.example.com".to_string()),
767 identity: Some("https://identity.example.com/".to_string()),
768 ..Default::default()
769 };
770 let (api, identity, trusted) = resolve_urls(
771 &parse("https://vault.example.com/#/send/id/key"),
772 Some(&config),
773 );
774 assert_eq!(api, "https://api.example.com");
775 assert_eq!(identity, "https://identity.example.com");
776 assert!(trusted);
777 }
778
779 #[test]
782 fn a_config_for_a_different_origin_is_ignored() {
783 let config = ConfigFile {
784 server: Some("https://bw.example.com".to_string()),
785 api: Some("https://api.example.com".to_string()),
786 ..Default::default()
787 };
788 let (api, identity, trusted) =
789 resolve_urls(&parse("https://send.bitwarden.com/#id/key"), Some(&config));
790 assert_eq!(api, "https://api.bitwarden.com");
791 assert_eq!(identity, "https://identity.bitwarden.com");
792 assert!(trusted);
793
794 let (api, identity, trusted) = resolve_urls(
795 &parse("https://other.example.com/#/send/id/key"),
796 Some(&config),
797 );
798 assert_eq!(api, "https://other.example.com/api");
799 assert_eq!(identity, "https://other.example.com/identity");
800 assert!(
801 !trusted,
802 "an origin that matches neither the cloud table nor the configured deployment \
803 must not be trusted"
804 );
805 }
806
807 #[test]
810 fn password_flag_wins() {
811 let password = password_from_args(Some("hunter2"), None, None).unwrap();
812 assert_eq!(password.as_deref(), Some("hunter2"));
813 }
814
815 #[test]
816 fn empty_password_flag_falls_through() {
817 assert_eq!(password_from_args(Some(""), None, None).unwrap(), None);
819 }
820
821 #[test]
822 fn password_file_supplies_its_first_line_only() {
823 let path = std::env::temp_dir().join(format!(
824 "bw-receive-passwordfile-{}",
825 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
826 ));
827 std::fs::write(&path, "first-line\nsecond-line\n").unwrap();
828
829 let password = password_from_args(None, Some(&path.to_string_lossy()), None).unwrap();
830
831 assert_eq!(password.as_deref(), Some("first-line"));
832 std::fs::remove_file(&path).ok();
833 }
834
835 #[test]
836 fn password_file_strips_a_windows_line_terminator() {
837 let path = std::env::temp_dir().join(format!(
838 "bw-receive-passwordfile-crlf-{}",
839 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
840 ));
841 std::fs::write(&path, "first-line\r\nsecond\r\n").unwrap();
842
843 let password = password_from_args(None, Some(&path.to_string_lossy()), None).unwrap();
844
845 assert_eq!(password.as_deref(), Some("first-line"));
846 std::fs::remove_file(&path).ok();
847 }
848
849 #[test]
850 fn password_file_rejects_traversal() {
851 let err = password_from_args(None, Some("../secrets/pw.txt"), None).unwrap_err();
852 assert!(
853 err.to_string().contains("--passwordfile"),
854 "expected a --passwordfile traversal error, got: {err}"
855 );
856 }
857
858 #[test]
859 fn missing_password_file_is_an_error_not_a_fallthrough() {
860 assert!(password_from_args(None, Some("/nonexistent/bw-receive-pw.txt"), None).is_err());
863 }
864
865 #[test]
868 fn password_file_short_circuits_password_env() {
869 let path = std::env::temp_dir().join(format!(
870 "bw-receive-passwordfile-empty-{}",
871 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
872 ));
873 std::fs::write(&path, "").unwrap();
874
875 let password = password_from_args(
876 None,
877 Some(&path.to_string_lossy()),
878 Some("BW_RECEIVE_TEST_UNSET_VAR"),
882 )
883 .unwrap();
884
885 assert_eq!(password, None);
886 std::fs::remove_file(&path).ok();
887 }
888
889 #[test]
890 fn unset_password_env_yields_no_password() {
891 assert_eq!(
892 password_from_args(None, None, Some("BW_RECEIVE_TEST_DEFINITELY_UNSET")).unwrap(),
893 None
894 );
895 }
896
897 #[test]
898 fn no_inputs_yields_no_password() {
899 assert_eq!(password_from_args(None, None, None).unwrap(), None);
900 }
901}