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;
28
29#[allow(missing_docs)]
30#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
31#[cfg_attr(
32 feature = "wasm",
33 derive(serde::Serialize, serde::Deserialize, tsify::Tsify),
34 tsify(into_wasm_abi, from_wasm_abi)
35)]
36pub enum ExportFormat {
37 Csv,
38 Json,
39 EncryptedJson { password: String },
40}
41
42#[allow(missing_docs)]
47pub struct Folder {
48 pub id: Uuid,
49 pub name: String,
50}
51
52#[allow(missing_docs)]
57#[derive(Clone)]
58pub struct Cipher {
59 pub id: Uuid,
60 pub folder_id: Option<Uuid>,
61
62 pub name: String,
63 pub notes: Option<String>,
64
65 pub r#type: CipherType,
66
67 pub favorite: bool,
68 pub reprompt: u8,
69
70 pub fields: Vec<Field>,
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)]
82#[derive(Clone)]
83#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
84pub struct ImportingCipher {
85 pub folder_id: Option<Uuid>,
86
87 pub name: String,
88 pub notes: Option<String>,
89
90 pub r#type: CipherType,
91
92 pub favorite: bool,
93 pub reprompt: u8,
94
95 pub fields: Vec<Field>,
96
97 pub revision_date: DateTime<Utc>,
98 pub creation_date: DateTime<Utc>,
99 pub deleted_date: Option<DateTime<Utc>>,
100}
101
102impl From<Login> for bitwarden_vault::LoginView {
103 fn from(login: Login) -> Self {
104 let l: Vec<LoginUriView> = login
105 .login_uris
106 .into_iter()
107 .map(LoginUriView::from)
108 .collect();
109
110 bitwarden_vault::LoginView {
111 username: login.username,
112 password: login.password,
113 password_revision_date: None,
114 uris: if l.is_empty() { None } else { Some(l) },
115 totp: login.totp,
116 autofill_on_page_load: None,
117 fido2_credentials: None,
119 }
120 }
121}
122
123impl From<SecureNote> for bitwarden_vault::SecureNoteView {
124 fn from(secure_note: SecureNote) -> Self {
125 bitwarden_vault::SecureNoteView {
126 r#type: secure_note.r#type.into(),
127 }
128 }
129}
130
131impl From<Card> for bitwarden_vault::CardView {
132 fn from(card: Card) -> Self {
133 bitwarden_vault::CardView {
134 cardholder_name: card.cardholder_name,
135 brand: card.brand,
136 number: card.number,
137 exp_month: card.exp_month,
138 exp_year: card.exp_year,
139 code: card.code,
140 }
141 }
142}
143
144impl From<Identity> for bitwarden_vault::IdentityView {
145 fn from(identity: Identity) -> Self {
146 bitwarden_vault::IdentityView {
147 title: identity.title,
148 first_name: identity.first_name,
149 middle_name: identity.middle_name,
150 last_name: identity.last_name,
151 address1: identity.address1,
152 address2: identity.address2,
153 address3: identity.address3,
154 city: identity.city,
155 state: identity.state,
156 postal_code: identity.postal_code,
157 country: identity.country,
158 company: identity.company,
159 email: identity.email,
160 phone: identity.phone,
161 ssn: identity.ssn,
162 username: identity.username,
163 passport_number: identity.passport_number,
164 license_number: identity.license_number,
165 }
166 }
167}
168
169impl From<SshKey> for bitwarden_vault::SshKeyView {
170 fn from(ssh_key: SshKey) -> Self {
171 bitwarden_vault::SshKeyView {
172 private_key: ssh_key.private_key,
173 public_key: ssh_key.public_key,
174 fingerprint: ssh_key.fingerprint,
175 }
176 }
177}
178
179impl From<ImportingCipher> for CipherView {
180 fn from(value: ImportingCipher) -> Self {
181 let (cipher_type, login, identity, card, secure_note, ssh_key) = match value.r#type {
182 CipherType::Login(login) => (
183 bitwarden_vault::CipherType::Login,
184 Some((*login).into()),
185 None,
186 None,
187 None,
188 None,
189 ),
190 CipherType::SecureNote(secure_note) => (
191 bitwarden_vault::CipherType::SecureNote,
192 None,
193 None,
194 None,
195 Some((*secure_note).into()),
196 None,
197 ),
198 CipherType::Card(card) => (
199 bitwarden_vault::CipherType::Card,
200 None,
201 None,
202 Some((*card).into()),
203 None,
204 None,
205 ),
206 CipherType::Identity(identity) => (
207 bitwarden_vault::CipherType::Identity,
208 None,
209 Some((*identity).into()),
210 None,
211 None,
212 None,
213 ),
214 CipherType::SshKey(ssh_key) => (
215 bitwarden_vault::CipherType::SshKey,
216 None,
217 None,
218 None,
219 None,
220 Some((*ssh_key).into()),
221 ),
222 };
223
224 Self {
225 id: None,
226 organization_id: None,
227 folder_id: value.folder_id.map(FolderId::new),
228 collection_ids: vec![],
229 key: None,
230 name: value.name,
231 notes: value.notes,
232 r#type: cipher_type,
233 login,
234 identity,
235 card,
236 secure_note,
237 ssh_key,
238 favorite: value.favorite,
239 reprompt: CipherRepromptType::None,
240 organization_use_totp: true,
241 edit: true,
242 permissions: None,
243 view_password: true,
244 local_data: None,
245 attachments: None,
246 attachment_decryption_failures: None,
247 fields: {
248 let fields: Vec<FieldView> = value.fields.into_iter().map(Into::into).collect();
249 if fields.is_empty() {
250 None
251 } else {
252 Some(fields)
253 }
254 },
255 password_history: None,
256 creation_date: value.creation_date,
257 deleted_date: None,
258 revision_date: value.revision_date,
259 archived_date: None,
260 }
261 }
262}
263
264impl From<LoginUri> for bitwarden_vault::LoginUriView {
265 fn from(value: LoginUri) -> Self {
266 Self {
267 uri: value.uri,
268 r#match: value.r#match.and_then(|m| match m {
269 0 => Some(UriMatchType::Domain),
270 1 => Some(UriMatchType::Host),
271 2 => Some(UriMatchType::StartsWith),
272 3 => Some(UriMatchType::Exact),
273 4 => Some(UriMatchType::RegularExpression),
274 5 => Some(UriMatchType::Never),
275 _ => None,
276 }),
277 uri_checksum: None,
278 }
279 }
280}
281
282#[allow(missing_docs)]
283#[derive(Clone, Debug, PartialEq, Eq)]
284pub struct Field {
285 pub name: Option<String>,
286 pub value: Option<String>,
287 pub r#type: u8,
288 pub linked_id: Option<u32>,
289}
290
291#[allow(missing_docs)]
292#[derive(Clone)]
293#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
294pub enum CipherType {
295 Login(Box<Login>),
296 SecureNote(Box<SecureNote>),
297 Card(Box<Card>),
298 Identity(Box<Identity>),
299 SshKey(Box<SshKey>),
300}
301
302impl fmt::Display for CipherType {
303 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
304 match self {
305 CipherType::Login(_) => write!(f, "login"),
306 CipherType::SecureNote(_) => write!(f, "note"),
307 CipherType::Card(_) => write!(f, "card"),
308 CipherType::Identity(_) => write!(f, "identity"),
309 CipherType::SshKey(_) => write!(f, "ssh_key"),
310 }
311 }
312}
313
314#[allow(missing_docs)]
315#[derive(Clone)]
316#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
317pub struct Login {
318 pub username: Option<String>,
319 pub password: Option<String>,
320 pub login_uris: Vec<LoginUri>,
321 pub totp: Option<String>,
322
323 pub fido2_credentials: Option<Vec<Fido2Credential>>,
324}
325
326#[allow(missing_docs)]
327#[derive(Clone, Debug, PartialEq, Eq)]
328pub struct LoginUri {
329 pub uri: Option<String>,
330 pub r#match: Option<u8>,
331}
332
333#[allow(missing_docs)]
334#[derive(Clone)]
335#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
336pub struct Fido2Credential {
337 pub credential_id: String,
338 pub key_type: String,
339 pub key_algorithm: String,
340 pub key_curve: String,
341 pub key_value: String,
342 pub rp_id: String,
343 pub user_handle: Option<String>,
344 pub user_name: Option<String>,
345 pub counter: u32,
346 pub rp_name: Option<String>,
347 pub user_display_name: Option<String>,
348 pub discoverable: String,
349 pub creation_date: DateTime<Utc>,
350}
351
352impl From<Fido2Credential> for Fido2CredentialFullView {
353 fn from(value: Fido2Credential) -> Self {
354 Fido2CredentialFullView {
355 credential_id: value.credential_id,
356 key_type: value.key_type,
357 key_algorithm: value.key_algorithm,
358 key_curve: value.key_curve,
359 key_value: value.key_value,
360 rp_id: value.rp_id,
361 user_handle: value.user_handle,
362 user_name: value.user_name,
363 counter: value.counter.to_string(),
364 rp_name: value.rp_name,
365 user_display_name: value.user_display_name,
366 discoverable: value.discoverable,
367 creation_date: value.creation_date,
368 }
369 }
370}
371
372#[allow(missing_docs)]
373#[derive(Clone)]
374#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
375pub struct Card {
376 pub cardholder_name: Option<String>,
377 pub exp_month: Option<String>,
378 pub exp_year: Option<String>,
379 pub code: Option<String>,
380 pub brand: Option<String>,
381 pub number: Option<String>,
382}
383
384#[allow(missing_docs)]
385#[derive(Clone)]
386#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
387pub struct SecureNote {
388 pub r#type: SecureNoteType,
389}
390
391#[allow(missing_docs)]
392#[derive(Clone)]
393#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
394pub enum SecureNoteType {
395 Generic = 0,
396}
397
398#[allow(missing_docs)]
399#[derive(Clone, Default, Debug, PartialEq, Eq)]
400pub struct Identity {
401 pub title: Option<String>,
402 pub first_name: Option<String>,
403 pub middle_name: Option<String>,
404 pub last_name: Option<String>,
405 pub address1: Option<String>,
406 pub address2: Option<String>,
407 pub address3: Option<String>,
408 pub city: Option<String>,
409 pub state: Option<String>,
410 pub postal_code: Option<String>,
411 pub country: Option<String>,
412 pub company: Option<String>,
413 pub email: Option<String>,
414 pub phone: Option<String>,
415 pub ssn: Option<String>,
416 pub username: Option<String>,
417 pub passport_number: Option<String>,
418 pub license_number: Option<String>,
419}
420
421#[allow(missing_docs)]
422#[derive(Clone)]
423#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
424pub struct SshKey {
425 pub private_key: String,
427 pub public_key: String,
429 pub fingerprint: String,
431}
432
433#[cfg(test)]
434mod tests {
435 use bitwarden_vault::{CipherType as VaultCipherType, FieldType};
436 use chrono::{DateTime, Utc};
437
438 use super::*;
439
440 #[test]
441 fn test_importing_cipher_to_cipher_view_login() {
442 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
443 let test_folder_id = uuid::Uuid::new_v4();
444
445 let importing_cipher = ImportingCipher {
446 folder_id: Some(test_folder_id),
447 name: "Test Login".to_string(),
448 notes: Some("Test notes".to_string()),
449 r#type: CipherType::Login(Box::new(Login {
450 username: Some("[email protected]".to_string()),
451 password: Some("password123".to_string()),
452 login_uris: vec![LoginUri {
453 uri: Some("https://example.com".to_string()),
454 r#match: Some(0), }],
456 totp: Some("otpauth://totp/test".to_string()),
457 fido2_credentials: None,
458 })),
459 favorite: true,
460 reprompt: 1,
461 fields: vec![Field {
462 name: Some("CustomField".to_string()),
463 value: Some("CustomValue".to_string()),
464 r#type: 0,
465 linked_id: None,
466 }],
467 revision_date: test_date,
468 creation_date: test_date,
469 deleted_date: None,
470 };
471
472 let cipher_view: CipherView = importing_cipher.into();
473
474 assert_eq!(cipher_view.id, None);
475 assert_eq!(cipher_view.organization_id, None);
476 assert_eq!(
477 cipher_view.folder_id.unwrap().to_string(),
478 test_folder_id.to_string()
479 );
480 assert_eq!(cipher_view.name, "Test Login");
481 assert_eq!(cipher_view.notes.unwrap(), "Test notes");
482 assert_eq!(cipher_view.r#type, VaultCipherType::Login);
483 assert!(cipher_view.favorite);
484 assert_eq!(cipher_view.creation_date, test_date);
485 assert_eq!(cipher_view.revision_date, test_date);
486
487 let fields = cipher_view.fields.unwrap();
488 assert_eq!(fields.len(), 1);
489
490 let field = fields.first().unwrap();
491 assert_eq!(field.name, Some("CustomField".to_string()));
492 assert_eq!(field.value, Some("CustomValue".to_string()));
493 assert_eq!(field.r#type, FieldType::Text);
494 assert_eq!(field.linked_id, None);
495
496 let login = cipher_view.login.expect("Login should be present");
497 assert_eq!(login.username, Some("[email protected]".to_string()));
498 assert_eq!(login.password, Some("password123".to_string()));
499 assert_eq!(login.totp, Some("otpauth://totp/test".to_string()));
500
501 let uris = login.uris.expect("URIs should be present");
502 assert_eq!(uris.len(), 1);
503 assert_eq!(uris[0].uri, Some("https://example.com".to_string()));
504 assert_eq!(uris[0].r#match, Some(bitwarden_vault::UriMatchType::Domain));
505 }
506
507 #[test]
508 fn test_importing_cipher_to_cipher_view_secure_note() {
509 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
510
511 let importing_cipher = ImportingCipher {
512 folder_id: None,
513 name: "My Note".to_string(),
514 notes: Some("This is a secure note".to_string()),
515 r#type: CipherType::SecureNote(Box::new(SecureNote {
516 r#type: SecureNoteType::Generic,
517 })),
518 favorite: false,
519 reprompt: 0,
520 fields: vec![],
521 revision_date: test_date,
522 creation_date: test_date,
523 deleted_date: None,
524 };
525
526 let cipher_view: CipherView = importing_cipher.into();
527
528 assert_eq!(cipher_view.id, None);
530 assert_eq!(cipher_view.organization_id, None);
531 assert_eq!(cipher_view.folder_id, None);
532 assert_eq!(cipher_view.name, "My Note");
533 assert_eq!(cipher_view.notes, Some("This is a secure note".to_string()));
534 assert_eq!(cipher_view.r#type, bitwarden_vault::CipherType::SecureNote);
535 assert!(!cipher_view.favorite);
536 assert_eq!(cipher_view.creation_date, test_date);
537 assert_eq!(cipher_view.revision_date, test_date);
538
539 assert!(cipher_view.login.is_none());
541 assert!(cipher_view.identity.is_none());
542 assert!(cipher_view.card.is_none());
543 assert!(cipher_view.secure_note.is_some());
544 assert!(cipher_view.ssh_key.is_none());
545
546 let secure_note = cipher_view.secure_note.unwrap();
548 assert!(matches!(
549 secure_note.r#type,
550 bitwarden_vault::SecureNoteType::Generic
551 ));
552 }
553
554 #[test]
555 fn test_importing_cipher_to_cipher_view_card() {
556 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
557
558 let importing_cipher = ImportingCipher {
559 folder_id: None,
560 name: "My Credit Card".to_string(),
561 notes: Some("Credit card notes".to_string()),
562 r#type: CipherType::Card(Box::new(Card {
563 cardholder_name: Some("John Doe".to_string()),
564 brand: Some("Visa".to_string()),
565 number: Some("1234567812345678".to_string()),
566 exp_month: Some("12".to_string()),
567 exp_year: Some("2025".to_string()),
568 code: Some("123".to_string()),
569 })),
570 favorite: false,
571 reprompt: 0,
572 fields: vec![],
573 revision_date: test_date,
574 creation_date: test_date,
575 deleted_date: None,
576 };
577
578 let cipher_view: CipherView = importing_cipher.into();
579
580 assert_eq!(cipher_view.r#type, bitwarden_vault::CipherType::Card);
581 assert!(cipher_view.card.is_some());
582 assert!(cipher_view.login.is_none());
583
584 let card = cipher_view.card.unwrap();
585 assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
586 assert_eq!(card.brand, Some("Visa".to_string()));
587 assert_eq!(card.number, Some("1234567812345678".to_string()));
588 }
589
590 #[test]
591 fn test_importing_cipher_to_cipher_view_identity() {
592 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
593
594 let importing_cipher = ImportingCipher {
595 folder_id: None,
596 name: "My Identity".to_string(),
597 notes: None,
598 r#type: CipherType::Identity(Box::new(Identity {
599 title: Some("Dr.".to_string()),
600 first_name: Some("Jane".to_string()),
601 last_name: Some("Smith".to_string()),
602 email: Some("[email protected]".to_string()),
603 ..Default::default()
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.r#type, bitwarden_vault::CipherType::Identity);
616 assert!(cipher_view.identity.is_some());
617 assert!(cipher_view.login.is_none());
618
619 let identity = cipher_view.identity.unwrap();
620 assert_eq!(identity.title, Some("Dr.".to_string()));
621 assert_eq!(identity.first_name, Some("Jane".to_string()));
622 assert_eq!(identity.last_name, Some("Smith".to_string()));
623 assert_eq!(identity.email, Some("[email protected]".to_string()));
624 }
625}