1#![doc = include_str!("../README.md")]
2
3use std::fmt;
4
5use bitwarden_vault::{
6 CipherRepromptType, CipherView, Fido2CredentialFullView, FieldView, FolderId, LoginUriView,
7 UriMatchType,
8};
9use chrono::{DateTime, Utc};
10use uuid::Uuid;
11
12#[cfg(feature = "uniffi")]
13uniffi::setup_scaffolding!();
14#[cfg(feature = "uniffi")]
15mod uniffi_support;
16
17mod csv;
18mod cxf;
19pub use cxf::Account;
20mod encrypted_json;
21mod exporter_client;
22mod json;
23mod models;
24pub use exporter_client::{ExporterClient, ExporterClientExt};
25mod error;
26mod export;
27pub use error::ExportError;
28pub use export::encrypt_import;
29
30#[allow(missing_docs)]
31#[derive(serde::Serialize, serde::Deserialize)]
32#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
33#[bitwarden_ffi::wasm_record]
34pub enum ExportFormat {
35 Csv,
36 Json,
37 EncryptedJson { password: String },
38}
39
40#[allow(missing_docs)]
45pub struct Folder {
46 pub id: Uuid,
47 pub name: String,
48}
49
50#[allow(missing_docs)]
55#[derive(Clone)]
56pub struct Cipher {
57 pub id: Uuid,
58 pub folder_id: Option<Uuid>,
59
60 pub name: String,
61 pub notes: Option<String>,
62
63 pub r#type: CipherType,
64
65 pub favorite: bool,
66 pub reprompt: u8,
67
68 pub fields: Vec<Field>,
69
70 pub password_history: Option<Vec<PasswordHistory>>,
71
72 pub revision_date: DateTime<Utc>,
73 pub creation_date: DateTime<Utc>,
74 pub deleted_date: Option<DateTime<Utc>>,
75}
76
77#[allow(missing_docs)]
79#[derive(Clone)]
80#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
81pub struct PasswordHistory {
82 pub password: String,
83 pub last_used_date: DateTime<Utc>,
84}
85
86#[allow(missing_docs)]
91#[derive(Clone)]
92#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
93pub struct ImportingCipher {
94 pub folder_id: Option<Uuid>,
95
96 pub name: String,
97 pub notes: Option<String>,
98
99 pub r#type: CipherType,
100
101 pub favorite: bool,
102 pub reprompt: u8,
103
104 pub fields: Vec<Field>,
105
106 pub revision_date: DateTime<Utc>,
107 pub creation_date: DateTime<Utc>,
108 pub deleted_date: Option<DateTime<Utc>>,
109}
110
111impl From<Login> for bitwarden_vault::LoginView {
112 fn from(login: Login) -> Self {
113 let l: Vec<LoginUriView> = login
114 .login_uris
115 .into_iter()
116 .map(LoginUriView::from)
117 .collect();
118
119 bitwarden_vault::LoginView {
120 username: login.username,
121 password: login.password,
122 password_revision_date: None,
123 uris: if l.is_empty() { None } else { Some(l) },
124 totp: login.totp,
125 autofill_on_page_load: None,
126 fido2_credentials: None,
128 }
129 }
130}
131
132impl From<SecureNote> for bitwarden_vault::SecureNoteView {
133 fn from(secure_note: SecureNote) -> Self {
134 bitwarden_vault::SecureNoteView {
135 r#type: secure_note.r#type.into(),
136 }
137 }
138}
139
140impl From<Card> for bitwarden_vault::CardView {
141 fn from(card: Card) -> Self {
142 bitwarden_vault::CardView {
143 cardholder_name: card.cardholder_name,
144 brand: card.brand,
145 number: card.number,
146 exp_month: card.exp_month,
147 exp_year: card.exp_year,
148 code: card.code,
149 }
150 }
151}
152
153impl From<Identity> for bitwarden_vault::IdentityView {
154 fn from(identity: Identity) -> Self {
155 bitwarden_vault::IdentityView {
156 title: identity.title,
157 first_name: identity.first_name,
158 middle_name: identity.middle_name,
159 last_name: identity.last_name,
160 address1: identity.address1,
161 address2: identity.address2,
162 address3: identity.address3,
163 city: identity.city,
164 state: identity.state,
165 postal_code: identity.postal_code,
166 country: identity.country,
167 company: identity.company,
168 email: identity.email,
169 phone: identity.phone,
170 ssn: identity.ssn,
171 username: identity.username,
172 passport_number: identity.passport_number,
173 license_number: identity.license_number,
174 }
175 }
176}
177
178impl From<SshKey> for bitwarden_vault::SshKeyView {
179 fn from(ssh_key: SshKey) -> Self {
180 bitwarden_vault::SshKeyView {
181 private_key: ssh_key.private_key,
182 public_key: ssh_key.public_key,
183 fingerprint: ssh_key.fingerprint,
184 }
185 }
186}
187
188impl From<ImportingCipher> for CipherView {
189 fn from(value: ImportingCipher) -> Self {
190 let (cipher_type, login, identity, card, secure_note, ssh_key) = match value.r#type {
191 CipherType::Login(login) => (
192 bitwarden_vault::CipherType::Login,
193 Some((*login).into()),
194 None,
195 None,
196 None,
197 None,
198 ),
199 CipherType::SecureNote(secure_note) => (
200 bitwarden_vault::CipherType::SecureNote,
201 None,
202 None,
203 None,
204 Some((*secure_note).into()),
205 None,
206 ),
207 CipherType::Card(card) => (
208 bitwarden_vault::CipherType::Card,
209 None,
210 None,
211 Some((*card).into()),
212 None,
213 None,
214 ),
215 CipherType::Identity(identity) => (
216 bitwarden_vault::CipherType::Identity,
217 None,
218 Some((*identity).into()),
219 None,
220 None,
221 None,
222 ),
223 CipherType::SshKey(ssh_key) => (
224 bitwarden_vault::CipherType::SshKey,
225 None,
226 None,
227 None,
228 None,
229 Some((*ssh_key).into()),
230 ),
231 CipherType::BankAccount => (
232 bitwarden_vault::CipherType::BankAccount,
233 None,
234 None,
235 None,
236 None,
237 None,
238 ),
239 CipherType::Passport => (
240 bitwarden_vault::CipherType::Passport,
241 None,
242 None,
243 None,
244 None,
245 None,
246 ),
247 CipherType::DriversLicense => (
248 bitwarden_vault::CipherType::DriversLicense,
249 None,
250 None,
251 None,
252 None,
253 None,
254 ),
255 };
256
257 Self {
258 partial: false,
259 id: None,
260 organization_id: None,
261 folder_id: value.folder_id.map(FolderId::new),
262 collection_ids: vec![],
263 key: None,
264 name: value.name,
265 notes: value.notes,
266 r#type: cipher_type,
267 login,
268 identity,
269 card,
270 secure_note,
271 ssh_key,
272 bank_account: None,
273 drivers_license: None,
274 passport: None,
275 favorite: value.favorite,
276 reprompt: CipherRepromptType::None,
277 organization_use_totp: true,
278 edit: true,
279 permissions: None,
280 view_password: true,
281 local_data: None,
282 attachments: None,
283 attachment_decryption_failures: None,
284 fields: {
285 let fields: Vec<FieldView> = value.fields.into_iter().map(Into::into).collect();
286 if fields.is_empty() {
287 None
288 } else {
289 Some(fields)
290 }
291 },
292 password_history: None,
293 creation_date: value.creation_date,
294 deleted_date: None,
295 revision_date: value.revision_date,
296 archived_date: None,
297 }
298 }
299}
300
301impl From<LoginUri> for bitwarden_vault::LoginUriView {
302 fn from(value: LoginUri) -> Self {
303 Self {
304 uri: value.uri,
305 r#match: value.r#match.and_then(|m| match m {
306 0 => Some(UriMatchType::Domain),
307 1 => Some(UriMatchType::Host),
308 2 => Some(UriMatchType::StartsWith),
309 3 => Some(UriMatchType::Exact),
310 4 => Some(UriMatchType::RegularExpression),
311 5 => Some(UriMatchType::Never),
312 _ => None,
313 }),
314 uri_checksum: None,
315 }
316 }
317}
318
319#[allow(missing_docs)]
320#[derive(Clone, Debug, PartialEq, Eq)]
321pub struct Field {
322 pub name: Option<String>,
323 pub value: Option<String>,
324 pub r#type: u8,
325 pub linked_id: Option<u32>,
326}
327
328#[allow(missing_docs)]
329#[derive(Clone)]
330#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
331pub enum CipherType {
332 Login(Box<Login>),
333 SecureNote(Box<SecureNote>),
334 Card(Box<Card>),
335 Identity(Box<Identity>),
336 SshKey(Box<SshKey>),
337 BankAccount,
338 Passport,
339 DriversLicense,
340}
341
342impl fmt::Display for CipherType {
343 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
344 match self {
345 CipherType::Login(_) => write!(f, "login"),
346 CipherType::SecureNote(_) => write!(f, "note"),
347 CipherType::Card(_) => write!(f, "card"),
348 CipherType::Identity(_) => write!(f, "identity"),
349 CipherType::SshKey(_) => write!(f, "ssh_key"),
350 CipherType::BankAccount => write!(f, "bank_account"),
351 CipherType::DriversLicense => write!(f, "drivers_license"),
352 CipherType::Passport => write!(f, "passport"),
353 }
354 }
355}
356
357#[allow(missing_docs)]
358#[derive(Clone)]
359#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
360pub struct Login {
361 pub username: Option<String>,
362 pub password: Option<String>,
363 pub login_uris: Vec<LoginUri>,
364 pub totp: Option<String>,
365
366 pub fido2_credentials: Option<Vec<Fido2Credential>>,
367}
368
369impl Login {
370 pub fn sanitize_uris(&mut self) {
376 for login_uri in &mut self.login_uris {
377 if let Some(uri) = &login_uri.uri {
378 login_uri.uri = Some(sanitize_uri(uri));
379 }
380 }
381 }
382}
383
384fn sanitize_uri(uri: &str) -> String {
392 if let Ok(parsed) = url::Url::parse(uri)
393 && parsed.has_host()
394 {
395 return uri.to_string();
396 }
397
398 let with_http = format!("http://{uri}");
399 if let Ok(parsed) = url::Url::parse(&with_http)
400 && let Some(host) = parsed.host()
401 && matches!(host, url::Host::Ipv4(_))
402 {
403 return with_http;
404 }
405
406 if !uri.starts_with("http://") || !uri.starts_with("https://") {
407 return format!("https://{uri}");
408 }
409
410 uri.to_string()
411}
412
413#[allow(missing_docs)]
414#[derive(Clone, Debug, PartialEq, Eq)]
415pub struct LoginUri {
416 pub uri: Option<String>,
417 pub r#match: Option<u8>,
418}
419
420#[allow(missing_docs)]
421#[derive(Clone)]
422#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
423pub struct Fido2Credential {
424 pub credential_id: String,
425 pub key_type: String,
426 pub key_algorithm: String,
427 pub key_curve: String,
428 pub key_value: String,
429 pub rp_id: String,
430 pub user_handle: Option<String>,
431 pub user_name: Option<String>,
432 pub counter: u32,
433 pub rp_name: Option<String>,
434 pub user_display_name: Option<String>,
435 pub discoverable: String,
436 pub creation_date: DateTime<Utc>,
437}
438
439impl From<Fido2Credential> for Fido2CredentialFullView {
440 fn from(value: Fido2Credential) -> Self {
441 Fido2CredentialFullView {
442 credential_id: value.credential_id,
443 key_type: value.key_type,
444 key_algorithm: value.key_algorithm,
445 key_curve: value.key_curve,
446 key_value: value.key_value,
447 rp_id: value.rp_id,
448 user_handle: value.user_handle,
449 user_name: value.user_name,
450 counter: value.counter.to_string(),
451 rp_name: value.rp_name,
452 user_display_name: value.user_display_name,
453 discoverable: value.discoverable,
454 creation_date: value.creation_date,
455 }
456 }
457}
458
459#[allow(missing_docs)]
460#[derive(Clone)]
461#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
462pub struct Card {
463 pub cardholder_name: Option<String>,
464 pub exp_month: Option<String>,
465 pub exp_year: Option<String>,
466 pub code: Option<String>,
467 pub brand: Option<String>,
468 pub number: Option<String>,
469}
470
471#[allow(missing_docs)]
472#[derive(Clone)]
473#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
474pub struct SecureNote {
475 pub r#type: SecureNoteType,
476}
477
478#[allow(missing_docs)]
479#[derive(Clone)]
480#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
481pub enum SecureNoteType {
482 Generic = 0,
483}
484
485#[allow(missing_docs)]
486#[derive(Clone, Default, Debug, PartialEq, Eq)]
487pub struct Identity {
488 pub title: Option<String>,
489 pub first_name: Option<String>,
490 pub middle_name: Option<String>,
491 pub last_name: Option<String>,
492 pub address1: Option<String>,
493 pub address2: Option<String>,
494 pub address3: Option<String>,
495 pub city: Option<String>,
496 pub state: Option<String>,
497 pub postal_code: Option<String>,
498 pub country: Option<String>,
499 pub company: Option<String>,
500 pub email: Option<String>,
501 pub phone: Option<String>,
502 pub ssn: Option<String>,
503 pub username: Option<String>,
504 pub passport_number: Option<String>,
505 pub license_number: Option<String>,
506}
507
508#[allow(missing_docs)]
509#[derive(Clone)]
510#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
511pub struct SshKey {
512 pub private_key: String,
514 pub public_key: String,
516 pub fingerprint: String,
518}
519
520#[cfg(test)]
521mod tests {
522 use bitwarden_vault::{CipherType as VaultCipherType, FieldType};
523 use chrono::{DateTime, Utc};
524
525 use super::*;
526
527 #[test]
528 fn test_importing_cipher_to_cipher_view_login() {
529 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
530 let test_folder_id = uuid::Uuid::new_v4();
531
532 let importing_cipher = ImportingCipher {
533 folder_id: Some(test_folder_id),
534 name: "Test Login".to_string(),
535 notes: Some("Test notes".to_string()),
536 r#type: CipherType::Login(Box::new(Login {
537 username: Some("[email protected]".to_string()),
538 password: Some("password123".to_string()),
539 login_uris: vec![LoginUri {
540 uri: Some("https://example.com".to_string()),
541 r#match: Some(0), }],
543 totp: Some("otpauth://totp/test".to_string()),
544 fido2_credentials: None,
545 })),
546 favorite: true,
547 reprompt: 1,
548 fields: vec![Field {
549 name: Some("CustomField".to_string()),
550 value: Some("CustomValue".to_string()),
551 r#type: 0,
552 linked_id: None,
553 }],
554 revision_date: test_date,
555 creation_date: test_date,
556 deleted_date: None,
557 };
558
559 let cipher_view: CipherView = importing_cipher.into();
560
561 assert_eq!(cipher_view.id, None);
562 assert_eq!(cipher_view.organization_id, None);
563 assert_eq!(
564 cipher_view.folder_id.unwrap().to_string(),
565 test_folder_id.to_string()
566 );
567 assert_eq!(cipher_view.name, "Test Login");
568 assert_eq!(cipher_view.notes.unwrap(), "Test notes");
569 assert_eq!(cipher_view.r#type, VaultCipherType::Login);
570 assert!(cipher_view.favorite);
571 assert_eq!(cipher_view.creation_date, test_date);
572 assert_eq!(cipher_view.revision_date, test_date);
573
574 let fields = cipher_view.fields.unwrap();
575 assert_eq!(fields.len(), 1);
576
577 let field = fields.first().unwrap();
578 assert_eq!(field.name, Some("CustomField".to_string()));
579 assert_eq!(field.value, Some("CustomValue".to_string()));
580 assert_eq!(field.r#type, FieldType::Text);
581 assert_eq!(field.linked_id, None);
582
583 let login = cipher_view.login.expect("Login should be present");
584 assert_eq!(login.username, Some("[email protected]".to_string()));
585 assert_eq!(login.password, Some("password123".to_string()));
586 assert_eq!(login.totp, Some("otpauth://totp/test".to_string()));
587
588 let uris = login.uris.expect("URIs should be present");
589 assert_eq!(uris.len(), 1);
590 assert_eq!(uris[0].uri, Some("https://example.com".to_string()));
591 assert_eq!(uris[0].r#match, Some(bitwarden_vault::UriMatchType::Domain));
592 }
593
594 #[test]
595 fn test_importing_cipher_to_cipher_view_secure_note() {
596 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
597
598 let importing_cipher = ImportingCipher {
599 folder_id: None,
600 name: "My Note".to_string(),
601 notes: Some("This is a secure note".to_string()),
602 r#type: CipherType::SecureNote(Box::new(SecureNote {
603 r#type: SecureNoteType::Generic,
604 })),
605 favorite: false,
606 reprompt: 0,
607 fields: vec![],
608 revision_date: test_date,
609 creation_date: test_date,
610 deleted_date: None,
611 };
612
613 let cipher_view: CipherView = importing_cipher.into();
614
615 assert_eq!(cipher_view.id, None);
617 assert_eq!(cipher_view.organization_id, None);
618 assert_eq!(cipher_view.folder_id, None);
619 assert_eq!(cipher_view.name, "My Note");
620 assert_eq!(cipher_view.notes, Some("This is a secure note".to_string()));
621 assert_eq!(cipher_view.r#type, bitwarden_vault::CipherType::SecureNote);
622 assert!(!cipher_view.favorite);
623 assert_eq!(cipher_view.creation_date, test_date);
624 assert_eq!(cipher_view.revision_date, test_date);
625
626 assert!(cipher_view.login.is_none());
628 assert!(cipher_view.identity.is_none());
629 assert!(cipher_view.card.is_none());
630 assert!(cipher_view.secure_note.is_some());
631 assert!(cipher_view.ssh_key.is_none());
632
633 let secure_note = cipher_view.secure_note.unwrap();
635 assert!(matches!(
636 secure_note.r#type,
637 bitwarden_vault::SecureNoteType::Generic
638 ));
639 }
640
641 #[test]
642 fn test_importing_cipher_to_cipher_view_card() {
643 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
644
645 let importing_cipher = ImportingCipher {
646 folder_id: None,
647 name: "My Credit Card".to_string(),
648 notes: Some("Credit card notes".to_string()),
649 r#type: CipherType::Card(Box::new(Card {
650 cardholder_name: Some("John Doe".to_string()),
651 brand: Some("Visa".to_string()),
652 number: Some("1234567812345678".to_string()),
653 exp_month: Some("12".to_string()),
654 exp_year: Some("2025".to_string()),
655 code: Some("123".to_string()),
656 })),
657 favorite: false,
658 reprompt: 0,
659 fields: vec![],
660 revision_date: test_date,
661 creation_date: test_date,
662 deleted_date: None,
663 };
664
665 let cipher_view: CipherView = importing_cipher.into();
666
667 assert_eq!(cipher_view.r#type, bitwarden_vault::CipherType::Card);
668 assert!(cipher_view.card.is_some());
669 assert!(cipher_view.login.is_none());
670
671 let card = cipher_view.card.unwrap();
672 assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
673 assert_eq!(card.brand, Some("Visa".to_string()));
674 assert_eq!(card.number, Some("1234567812345678".to_string()));
675 }
676
677 #[test]
678 fn test_importing_cipher_to_cipher_view_identity() {
679 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
680
681 let importing_cipher = ImportingCipher {
682 folder_id: None,
683 name: "My Identity".to_string(),
684 notes: None,
685 r#type: CipherType::Identity(Box::new(Identity {
686 title: Some("Dr.".to_string()),
687 first_name: Some("Jane".to_string()),
688 last_name: Some("Smith".to_string()),
689 email: Some("[email protected]".to_string()),
690 ..Default::default()
691 })),
692 favorite: false,
693 reprompt: 0,
694 fields: vec![],
695 revision_date: test_date,
696 creation_date: test_date,
697 deleted_date: None,
698 };
699
700 let cipher_view: CipherView = importing_cipher.into();
701
702 assert_eq!(cipher_view.r#type, bitwarden_vault::CipherType::Identity);
703 assert!(cipher_view.identity.is_some());
704 assert!(cipher_view.login.is_none());
705
706 let identity = cipher_view.identity.unwrap();
707 assert_eq!(identity.title, Some("Dr.".to_string()));
708 assert_eq!(identity.first_name, Some("Jane".to_string()));
709 assert_eq!(identity.last_name, Some("Smith".to_string()));
710 assert_eq!(identity.email, Some("[email protected]".to_string()));
711 }
712
713 #[test]
714 fn test_sanitize_uri_valid_url_unchanged() {
715 assert_eq!(
716 sanitize_uri("https://bitwarden.com"),
717 "https://bitwarden.com"
718 );
719 assert_eq!(
720 sanitize_uri("https://bitwarden.com/path?q=1"),
721 "https://bitwarden.com/path?q=1"
722 );
723 assert_eq!(
724 sanitize_uri("http://192.168.0.1:8080"),
725 "http://192.168.0.1:8080"
726 );
727 }
728
729 #[test]
730 fn test_sanitize_uri_ip_address_gets_http() {
731 assert_eq!(sanitize_uri("192.168.0.1:8080"), "http://192.168.0.1:8080");
732 assert_eq!(sanitize_uri("10.0.0.1"), "http://10.0.0.1");
733 assert_eq!(
734 sanitize_uri("192.168.0.1:8080/path"),
735 "http://192.168.0.1:8080/path"
736 );
737 }
738
739 #[test]
740 fn test_sanitize_uri_non_ip_gets_https() {
741 assert_eq!(sanitize_uri("bitwarden.com"), "https://bitwarden.com");
742 assert_eq!(
743 sanitize_uri("bitwarden.co.uk/login"),
744 "https://bitwarden.co.uk/login"
745 );
746 }
747
748 #[test]
749 fn test_sanitize_uri_broken_http_prefix_unchanged() {
750 assert_eq!(
752 sanitize_uri("ht tp://bitwarden.com"),
753 "https://ht tp://bitwarden.com"
754 );
755 }
756
757 #[test]
758 fn test_sanitize_uri_schemeless_host_port() {
759 assert_eq!(sanitize_uri("localhost:8080"), "https://localhost:8080");
760 }
761
762 #[test]
763 fn test_sanitize_uri_scheme_on_string() {
764 assert_eq!(sanitize_uri("foo:bar"), "https://foo:bar");
765 }
766
767 #[test]
768 fn test_sanitize_uri_with_real_schemes_no_change() {
769 assert_eq!(
770 sanitize_uri("ftp://files.example.com"),
771 "ftp://files.example.com"
772 );
773 assert_eq!(
774 sanitize_uri("androidapp://com.example"),
775 "androidapp://com.example"
776 );
777 }
778
779 #[test]
780 fn test_login_sanitize_uris() {
781 let mut login = Login {
782 username: None,
783 password: None,
784 login_uris: vec![
785 LoginUri {
786 uri: Some("https://bitwarden.com".to_string()),
787 r#match: None,
788 },
789 LoginUri {
790 uri: Some("192.168.0.1:8080".to_string()),
791 r#match: None,
792 },
793 LoginUri {
794 uri: Some("example.com".to_string()),
795 r#match: None,
796 },
797 LoginUri {
798 uri: None,
799 r#match: None,
800 },
801 ],
802 totp: None,
803 fido2_credentials: None,
804 };
805
806 login.sanitize_uris();
807
808 assert_eq!(
809 login.login_uris[0].uri,
810 Some("https://bitwarden.com".to_string())
811 );
812 assert_eq!(
813 login.login_uris[1].uri,
814 Some("http://192.168.0.1:8080".to_string())
815 );
816 assert_eq!(
817 login.login_uris[2].uri,
818 Some("https://example.com".to_string())
819 );
820 assert_eq!(login.login_uris[3].uri, None);
821 }
822}