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 fields: {
247 let fields: Vec<FieldView> = value.fields.into_iter().map(Into::into).collect();
248 if fields.is_empty() {
249 None
250 } else {
251 Some(fields)
252 }
253 },
254 password_history: None,
255 creation_date: value.creation_date,
256 deleted_date: None,
257 revision_date: value.revision_date,
258 archived_date: None,
259 }
260 }
261}
262
263impl From<LoginUri> for bitwarden_vault::LoginUriView {
264 fn from(value: LoginUri) -> Self {
265 Self {
266 uri: value.uri,
267 r#match: value.r#match.and_then(|m| match m {
268 0 => Some(UriMatchType::Domain),
269 1 => Some(UriMatchType::Host),
270 2 => Some(UriMatchType::StartsWith),
271 3 => Some(UriMatchType::Exact),
272 4 => Some(UriMatchType::RegularExpression),
273 5 => Some(UriMatchType::Never),
274 _ => None,
275 }),
276 uri_checksum: None,
277 }
278 }
279}
280
281#[allow(missing_docs)]
282#[derive(Clone, Debug, PartialEq, Eq)]
283pub struct Field {
284 pub name: Option<String>,
285 pub value: Option<String>,
286 pub r#type: u8,
287 pub linked_id: Option<u32>,
288}
289
290#[allow(missing_docs)]
291#[derive(Clone)]
292#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
293pub enum CipherType {
294 Login(Box<Login>),
295 SecureNote(Box<SecureNote>),
296 Card(Box<Card>),
297 Identity(Box<Identity>),
298 SshKey(Box<SshKey>),
299}
300
301impl fmt::Display for CipherType {
302 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
303 match self {
304 CipherType::Login(_) => write!(f, "login"),
305 CipherType::SecureNote(_) => write!(f, "note"),
306 CipherType::Card(_) => write!(f, "card"),
307 CipherType::Identity(_) => write!(f, "identity"),
308 CipherType::SshKey(_) => write!(f, "ssh_key"),
309 }
310 }
311}
312
313#[allow(missing_docs)]
314#[derive(Clone)]
315#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
316pub struct Login {
317 pub username: Option<String>,
318 pub password: Option<String>,
319 pub login_uris: Vec<LoginUri>,
320 pub totp: Option<String>,
321
322 pub fido2_credentials: Option<Vec<Fido2Credential>>,
323}
324
325#[allow(missing_docs)]
326#[derive(Clone, Debug, PartialEq, Eq)]
327pub struct LoginUri {
328 pub uri: Option<String>,
329 pub r#match: Option<u8>,
330}
331
332#[allow(missing_docs)]
333#[derive(Clone)]
334#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
335pub struct Fido2Credential {
336 pub credential_id: String,
337 pub key_type: String,
338 pub key_algorithm: String,
339 pub key_curve: String,
340 pub key_value: String,
341 pub rp_id: String,
342 pub user_handle: Option<String>,
343 pub user_name: Option<String>,
344 pub counter: u32,
345 pub rp_name: Option<String>,
346 pub user_display_name: Option<String>,
347 pub discoverable: String,
348 pub creation_date: DateTime<Utc>,
349}
350
351impl From<Fido2Credential> for Fido2CredentialFullView {
352 fn from(value: Fido2Credential) -> Self {
353 Fido2CredentialFullView {
354 credential_id: value.credential_id,
355 key_type: value.key_type,
356 key_algorithm: value.key_algorithm,
357 key_curve: value.key_curve,
358 key_value: value.key_value,
359 rp_id: value.rp_id,
360 user_handle: value.user_handle,
361 user_name: value.user_name,
362 counter: value.counter.to_string(),
363 rp_name: value.rp_name,
364 user_display_name: value.user_display_name,
365 discoverable: value.discoverable,
366 creation_date: value.creation_date,
367 }
368 }
369}
370
371#[allow(missing_docs)]
372#[derive(Clone)]
373#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
374pub struct Card {
375 pub cardholder_name: Option<String>,
376 pub exp_month: Option<String>,
377 pub exp_year: Option<String>,
378 pub code: Option<String>,
379 pub brand: Option<String>,
380 pub number: Option<String>,
381}
382
383#[allow(missing_docs)]
384#[derive(Clone)]
385#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
386pub struct SecureNote {
387 pub r#type: SecureNoteType,
388}
389
390#[allow(missing_docs)]
391#[derive(Clone)]
392#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
393pub enum SecureNoteType {
394 Generic = 0,
395}
396
397#[allow(missing_docs)]
398#[derive(Clone, Default, Debug, PartialEq, Eq)]
399pub struct Identity {
400 pub title: Option<String>,
401 pub first_name: Option<String>,
402 pub middle_name: Option<String>,
403 pub last_name: Option<String>,
404 pub address1: Option<String>,
405 pub address2: Option<String>,
406 pub address3: Option<String>,
407 pub city: Option<String>,
408 pub state: Option<String>,
409 pub postal_code: Option<String>,
410 pub country: Option<String>,
411 pub company: Option<String>,
412 pub email: Option<String>,
413 pub phone: Option<String>,
414 pub ssn: Option<String>,
415 pub username: Option<String>,
416 pub passport_number: Option<String>,
417 pub license_number: Option<String>,
418}
419
420#[allow(missing_docs)]
421#[derive(Clone)]
422#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
423pub struct SshKey {
424 pub private_key: String,
426 pub public_key: String,
428 pub fingerprint: String,
430}
431
432#[cfg(test)]
433mod tests {
434 use bitwarden_vault::{CipherType as VaultCipherType, FieldType};
435 use chrono::{DateTime, Utc};
436
437 use super::*;
438
439 #[test]
440 fn test_importing_cipher_to_cipher_view_login() {
441 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
442 let test_folder_id = uuid::Uuid::new_v4();
443
444 let importing_cipher = ImportingCipher {
445 folder_id: Some(test_folder_id),
446 name: "Test Login".to_string(),
447 notes: Some("Test notes".to_string()),
448 r#type: CipherType::Login(Box::new(Login {
449 username: Some("[email protected]".to_string()),
450 password: Some("password123".to_string()),
451 login_uris: vec![LoginUri {
452 uri: Some("https://example.com".to_string()),
453 r#match: Some(0), }],
455 totp: Some("otpauth://totp/test".to_string()),
456 fido2_credentials: None,
457 })),
458 favorite: true,
459 reprompt: 1,
460 fields: vec![Field {
461 name: Some("CustomField".to_string()),
462 value: Some("CustomValue".to_string()),
463 r#type: 0,
464 linked_id: None,
465 }],
466 revision_date: test_date,
467 creation_date: test_date,
468 deleted_date: None,
469 };
470
471 let cipher_view: CipherView = importing_cipher.into();
472
473 assert_eq!(cipher_view.id, None);
474 assert_eq!(cipher_view.organization_id, None);
475 assert_eq!(
476 cipher_view.folder_id.unwrap().to_string(),
477 test_folder_id.to_string()
478 );
479 assert_eq!(cipher_view.name, "Test Login");
480 assert_eq!(cipher_view.notes.unwrap(), "Test notes");
481 assert_eq!(cipher_view.r#type, VaultCipherType::Login);
482 assert!(cipher_view.favorite);
483 assert_eq!(cipher_view.creation_date, test_date);
484 assert_eq!(cipher_view.revision_date, test_date);
485
486 let fields = cipher_view.fields.unwrap();
487 assert_eq!(fields.len(), 1);
488
489 let field = fields.first().unwrap();
490 assert_eq!(field.name, Some("CustomField".to_string()));
491 assert_eq!(field.value, Some("CustomValue".to_string()));
492 assert_eq!(field.r#type, FieldType::Text);
493 assert_eq!(field.linked_id, None);
494
495 let login = cipher_view.login.expect("Login should be present");
496 assert_eq!(login.username, Some("[email protected]".to_string()));
497 assert_eq!(login.password, Some("password123".to_string()));
498 assert_eq!(login.totp, Some("otpauth://totp/test".to_string()));
499
500 let uris = login.uris.expect("URIs should be present");
501 assert_eq!(uris.len(), 1);
502 assert_eq!(uris[0].uri, Some("https://example.com".to_string()));
503 assert_eq!(uris[0].r#match, Some(bitwarden_vault::UriMatchType::Domain));
504 }
505
506 #[test]
507 fn test_importing_cipher_to_cipher_view_secure_note() {
508 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
509
510 let importing_cipher = ImportingCipher {
511 folder_id: None,
512 name: "My Note".to_string(),
513 notes: Some("This is a secure note".to_string()),
514 r#type: CipherType::SecureNote(Box::new(SecureNote {
515 r#type: SecureNoteType::Generic,
516 })),
517 favorite: false,
518 reprompt: 0,
519 fields: vec![],
520 revision_date: test_date,
521 creation_date: test_date,
522 deleted_date: None,
523 };
524
525 let cipher_view: CipherView = importing_cipher.into();
526
527 assert_eq!(cipher_view.id, None);
529 assert_eq!(cipher_view.organization_id, None);
530 assert_eq!(cipher_view.folder_id, None);
531 assert_eq!(cipher_view.name, "My Note");
532 assert_eq!(cipher_view.notes, Some("This is a secure note".to_string()));
533 assert_eq!(cipher_view.r#type, bitwarden_vault::CipherType::SecureNote);
534 assert!(!cipher_view.favorite);
535 assert_eq!(cipher_view.creation_date, test_date);
536 assert_eq!(cipher_view.revision_date, test_date);
537
538 assert!(cipher_view.login.is_none());
540 assert!(cipher_view.identity.is_none());
541 assert!(cipher_view.card.is_none());
542 assert!(cipher_view.secure_note.is_some());
543 assert!(cipher_view.ssh_key.is_none());
544
545 let secure_note = cipher_view.secure_note.unwrap();
547 assert!(matches!(
548 secure_note.r#type,
549 bitwarden_vault::SecureNoteType::Generic
550 ));
551 }
552
553 #[test]
554 fn test_importing_cipher_to_cipher_view_card() {
555 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
556
557 let importing_cipher = ImportingCipher {
558 folder_id: None,
559 name: "My Credit Card".to_string(),
560 notes: Some("Credit card notes".to_string()),
561 r#type: CipherType::Card(Box::new(Card {
562 cardholder_name: Some("John Doe".to_string()),
563 brand: Some("Visa".to_string()),
564 number: Some("1234567812345678".to_string()),
565 exp_month: Some("12".to_string()),
566 exp_year: Some("2025".to_string()),
567 code: Some("123".to_string()),
568 })),
569 favorite: false,
570 reprompt: 0,
571 fields: vec![],
572 revision_date: test_date,
573 creation_date: test_date,
574 deleted_date: None,
575 };
576
577 let cipher_view: CipherView = importing_cipher.into();
578
579 assert_eq!(cipher_view.r#type, bitwarden_vault::CipherType::Card);
580 assert!(cipher_view.card.is_some());
581 assert!(cipher_view.login.is_none());
582
583 let card = cipher_view.card.unwrap();
584 assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
585 assert_eq!(card.brand, Some("Visa".to_string()));
586 assert_eq!(card.number, Some("1234567812345678".to_string()));
587 }
588
589 #[test]
590 fn test_importing_cipher_to_cipher_view_identity() {
591 let test_date: DateTime<Utc> = "2024-01-30T17:55:36.150Z".parse().unwrap();
592
593 let importing_cipher = ImportingCipher {
594 folder_id: None,
595 name: "My Identity".to_string(),
596 notes: None,
597 r#type: CipherType::Identity(Box::new(Identity {
598 title: Some("Dr.".to_string()),
599 first_name: Some("Jane".to_string()),
600 last_name: Some("Smith".to_string()),
601 email: Some("[email protected]".to_string()),
602 ..Default::default()
603 })),
604 favorite: false,
605 reprompt: 0,
606 fields: vec![],
607 revision_date: test_date,
608 creation_date: test_date,
609 deleted_date: None,
610 };
611
612 let cipher_view: CipherView = importing_cipher.into();
613
614 assert_eq!(cipher_view.r#type, bitwarden_vault::CipherType::Identity);
615 assert!(cipher_view.identity.is_some());
616 assert!(cipher_view.login.is_none());
617
618 let identity = cipher_view.identity.unwrap();
619 assert_eq!(identity.title, Some("Dr.".to_string()));
620 assert_eq!(identity.first_name, Some("Jane".to_string()));
621 assert_eq!(identity.last_name, Some("Smith".to_string()));
622 assert_eq!(identity.email, Some("[email protected]".to_string()));
623 }
624}