1use chrono::{DateTime, SecondsFormat, Utc};
2use serde::Serializer;
3use thiserror::Error;
4use uuid::Uuid;
5
6use crate::{
7 Card, Cipher, CipherType, Fido2Credential, Field, Folder, Identity, Login, LoginUri,
8 PasswordHistory, SecureNote, SshKey,
9};
10
11fn rfc3339_millis_serialize<S: Serializer>(date: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error> {
14 s.serialize_str(&date.to_rfc3339_opts(SecondsFormat::Millis, true))
15}
16
17fn rfc3339_millis_serialize_opt<S: Serializer>(
19 date: &Option<DateTime<Utc>>,
20 s: S,
21) -> Result<S::Ok, S::Error> {
22 match date {
23 Some(d) => s.serialize_str(&d.to_rfc3339_opts(SecondsFormat::Millis, true)),
24 None => s.serialize_none(),
25 }
26}
27
28#[derive(Error, Debug)]
29pub enum JsonError {
30 #[error("JSON error: {0}")]
31 Serde(#[from] serde_json::Error),
32}
33
34pub(crate) fn export_json(folders: Vec<Folder>, ciphers: Vec<Cipher>) -> Result<String, JsonError> {
35 let export = JsonExport {
36 encrypted: false,
37 folders: folders.into_iter().map(|f| f.into()).collect(),
38 items: ciphers
39 .into_iter()
40 .filter(|c| {
41 !matches!(
42 c.r#type,
43 CipherType::BankAccount | CipherType::Passport | CipherType::DriversLicense
44 )
45 })
46 .map(|c| c.into())
47 .collect(),
48 };
49
50 Ok(serde_json::to_string_pretty(&export)?)
51}
52
53#[derive(serde::Serialize)]
58#[serde(rename_all = "camelCase")]
59struct JsonExport {
60 encrypted: bool,
61 folders: Vec<JsonFolder>,
62 items: Vec<JsonCipher>,
63}
64
65#[derive(serde::Serialize)]
66#[serde(rename_all = "camelCase")]
67struct JsonFolder {
68 id: Uuid,
69 name: String,
70}
71
72impl From<Folder> for JsonFolder {
73 fn from(folder: Folder) -> Self {
74 JsonFolder {
75 id: folder.id,
76 name: folder.name,
77 }
78 }
79}
80
81#[derive(serde::Serialize)]
82#[serde(rename_all = "camelCase")]
83struct JsonCipher {
84 id: Uuid,
85 folder_id: Option<Uuid>,
86 organization_id: Option<Uuid>,
88 collection_ids: Option<Vec<Uuid>>,
89
90 name: String,
91 notes: Option<String>,
92
93 r#type: u8,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 login: Option<JsonLogin>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 identity: Option<JsonIdentity>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 card: Option<JsonCard>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 secure_note: Option<JsonSecureNote>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 ssh_key: Option<JsonSshKey>,
104
105 favorite: bool,
106 reprompt: u8,
107
108 #[serde(skip_serializing_if = "Vec::is_empty")]
109 fields: Vec<JsonField>,
110 password_history: Option<Vec<JsonPasswordHistory>>,
111
112 #[serde(serialize_with = "rfc3339_millis_serialize")]
113 revision_date: DateTime<Utc>,
114 #[serde(serialize_with = "rfc3339_millis_serialize")]
115 creation_date: DateTime<Utc>,
116 #[serde(serialize_with = "rfc3339_millis_serialize_opt")]
117 deleted_date: Option<DateTime<Utc>>,
118}
119
120#[derive(serde::Serialize)]
121#[serde(rename_all = "camelCase")]
122struct JsonLogin {
123 username: Option<String>,
124 password: Option<String>,
125 uris: Vec<JsonLoginUri>,
126 totp: Option<String>,
127 fido2_credentials: Vec<JsonFido2Credential>,
128}
129
130impl From<Login> for JsonLogin {
131 fn from(login: Login) -> Self {
132 JsonLogin {
133 username: login.username,
134 password: login.password,
135 uris: login.login_uris.into_iter().map(|u| u.into()).collect(),
136 totp: login.totp,
137 fido2_credentials: login
138 .fido2_credentials
139 .unwrap_or_default()
140 .into_iter()
141 .map(|c| c.into())
142 .collect(),
143 }
144 }
145}
146
147#[derive(serde::Serialize)]
148#[serde(rename_all = "camelCase")]
149struct JsonFido2Credential {
150 credential_id: String,
151 key_type: String,
152 key_algorithm: String,
153 key_curve: String,
154 key_value: String,
155 rp_id: String,
156 user_handle: Option<String>,
157 user_name: Option<String>,
158 counter: String,
160 rp_name: Option<String>,
161 user_display_name: Option<String>,
162 discoverable: String,
164 #[serde(serialize_with = "rfc3339_millis_serialize")]
165 creation_date: DateTime<Utc>,
166}
167
168impl From<Fido2Credential> for JsonFido2Credential {
169 fn from(credential: Fido2Credential) -> Self {
170 JsonFido2Credential {
171 credential_id: credential.credential_id,
172 key_type: credential.key_type,
173 key_algorithm: credential.key_algorithm,
174 key_curve: credential.key_curve,
175 key_value: credential.key_value,
176 rp_id: credential.rp_id,
177 user_handle: credential.user_handle,
178 user_name: credential.user_name,
179 counter: credential.counter.to_string(),
180 rp_name: credential.rp_name,
181 user_display_name: credential.user_display_name,
182 discoverable: credential.discoverable,
183 creation_date: credential.creation_date,
184 }
185 }
186}
187
188#[derive(serde::Serialize)]
189#[serde(rename_all = "camelCase")]
190struct JsonPasswordHistory {
191 password: String,
192 #[serde(serialize_with = "rfc3339_millis_serialize")]
193 last_used_date: DateTime<Utc>,
194}
195
196impl From<PasswordHistory> for JsonPasswordHistory {
197 fn from(history: PasswordHistory) -> Self {
198 JsonPasswordHistory {
199 password: history.password,
200 last_used_date: history.last_used_date,
201 }
202 }
203}
204
205#[derive(serde::Serialize)]
206#[serde(rename_all = "camelCase")]
207struct JsonLoginUri {
208 uri: Option<String>,
209 r#match: Option<u8>,
210}
211
212impl From<LoginUri> for JsonLoginUri {
213 fn from(login_uri: LoginUri) -> Self {
214 JsonLoginUri {
215 uri: login_uri.uri,
216 r#match: login_uri.r#match,
217 }
218 }
219}
220
221#[derive(serde::Serialize)]
222#[serde(rename_all = "camelCase")]
223struct JsonSecureNote {
224 r#type: u8,
225}
226
227impl From<SecureNote> for JsonSecureNote {
228 fn from(note: SecureNote) -> Self {
229 JsonSecureNote {
230 r#type: note.r#type as u8,
231 }
232 }
233}
234
235#[derive(serde::Serialize)]
236#[serde(rename_all = "camelCase")]
237struct JsonCard {
238 cardholder_name: Option<String>,
239 exp_month: Option<String>,
240 exp_year: Option<String>,
241 code: Option<String>,
242 brand: Option<String>,
243 number: Option<String>,
244}
245
246impl From<Card> for JsonCard {
247 fn from(card: Card) -> Self {
248 JsonCard {
249 cardholder_name: card.cardholder_name,
250 exp_month: card.exp_month,
251 exp_year: card.exp_year,
252 code: card.code,
253 brand: card.brand,
254 number: card.number,
255 }
256 }
257}
258
259#[derive(serde::Serialize)]
260#[serde(rename_all = "camelCase")]
261struct JsonIdentity {
262 title: Option<String>,
263 first_name: Option<String>,
264 middle_name: Option<String>,
265 last_name: Option<String>,
266 address1: Option<String>,
267 address2: Option<String>,
268 address3: Option<String>,
269 city: Option<String>,
270 state: Option<String>,
271 postal_code: Option<String>,
272 country: Option<String>,
273 company: Option<String>,
274 email: Option<String>,
275 phone: Option<String>,
276 ssn: Option<String>,
277 username: Option<String>,
278 passport_number: Option<String>,
279 license_number: Option<String>,
280}
281
282impl From<Identity> for JsonIdentity {
283 fn from(identity: Identity) -> Self {
284 JsonIdentity {
285 title: identity.title,
286 first_name: identity.first_name,
287 middle_name: identity.middle_name,
288 last_name: identity.last_name,
289 address1: identity.address1,
290 address2: identity.address2,
291 address3: identity.address3,
292 city: identity.city,
293 state: identity.state,
294 postal_code: identity.postal_code,
295 country: identity.country,
296 company: identity.company,
297 email: identity.email,
298 phone: identity.phone,
299 ssn: identity.ssn,
300 username: identity.username,
301 passport_number: identity.passport_number,
302 license_number: identity.license_number,
303 }
304 }
305}
306
307#[derive(serde::Serialize)]
308#[serde(rename_all = "camelCase")]
309struct JsonSshKey {
310 private_key: String,
311 public_key: String,
312 key_fingerprint: String,
313}
314
315impl From<SshKey> for JsonSshKey {
316 fn from(ssh_key: SshKey) -> Self {
317 JsonSshKey {
318 private_key: ssh_key.private_key,
319 public_key: ssh_key.public_key,
320 key_fingerprint: ssh_key.fingerprint,
321 }
322 }
323}
324
325#[derive(serde::Serialize)]
326#[serde(rename_all = "camelCase")]
327struct JsonField {
328 name: Option<String>,
329 value: Option<String>,
330 r#type: u8,
331 linked_id: Option<u32>,
332}
333
334impl From<Field> for JsonField {
335 fn from(field: Field) -> Self {
336 JsonField {
337 name: field.name,
338 value: field.value,
339 r#type: field.r#type,
340 linked_id: field.linked_id,
341 }
342 }
343}
344
345impl From<Cipher> for JsonCipher {
346 fn from(cipher: Cipher) -> Self {
347 let r#type = match cipher.r#type {
348 CipherType::Login(_) => 1,
349 CipherType::SecureNote(_) => 2,
350 CipherType::Card(_) => 3,
351 CipherType::Identity(_) => 4,
352 CipherType::SshKey(_) => 5,
353 CipherType::BankAccount | CipherType::Passport | CipherType::DriversLicense => {
356 unreachable!(
357 "This cipher type is not supported for export and should be filtered out"
358 )
359 }
360 };
361
362 let (login, secure_note, card, identity, ssh_key) = match cipher.r#type {
363 CipherType::Login(l) => (Some((*l).into()), None, None, None, None),
364 CipherType::SecureNote(s) => (None, Some((*s).into()), None, None, None),
365 CipherType::Card(c) => (None, None, Some((*c).into()), None, None),
366 CipherType::Identity(i) => (None, None, None, Some((*i).into()), None),
367 CipherType::SshKey(ssh) => (None, None, None, None, Some((*ssh).into())),
368 CipherType::BankAccount | CipherType::Passport | CipherType::DriversLicense => {
369 unreachable!(
370 "This cipher type is not supported for export and should be filtered out"
371 )
372 }
373 };
374
375 JsonCipher {
376 id: cipher.id,
377 folder_id: cipher.folder_id,
378 organization_id: None,
379 collection_ids: None,
380 name: cipher.name,
381 notes: cipher.notes,
382 r#type,
383 login,
384 identity,
385 card,
386 secure_note,
387 ssh_key,
388 favorite: cipher.favorite,
389 reprompt: cipher.reprompt,
390 fields: cipher.fields.into_iter().map(|f| f.into()).collect(),
391 password_history: cipher
392 .password_history
393 .map(|h| h.into_iter().map(|p| p.into()).collect()),
394 revision_date: cipher.revision_date,
395 creation_date: cipher.creation_date,
396 deleted_date: cipher.deleted_date,
397 }
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use std::{fs, io::Read, path::PathBuf};
404
405 use super::*;
406 use crate::{Cipher, Fido2Credential, Field, LoginUri, PasswordHistory, SecureNoteType};
407
408 #[test]
409 fn test_convert_login() {
410 let cipher = Cipher {
411 id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
412 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
413
414 name: "Bitwarden".to_string(),
415 notes: Some("My note".to_string()),
416
417 r#type: CipherType::Login(Box::new(Login {
418 username: Some("[email protected]".to_string()),
419 password: Some("asdfasdfasdf".to_string()),
420 login_uris: vec![LoginUri {
421 uri: Some("https://vault.bitwarden.com".to_string()),
422 r#match: None,
423 }],
424 totp: Some("ABC".to_string()),
425 fido2_credentials: None,
426 })),
427
428 favorite: true,
429 reprompt: 0,
430
431 fields: vec![
432 Field {
433 name: Some("Text".to_string()),
434 value: Some("A".to_string()),
435 r#type: 0,
436 linked_id: None,
437 },
438 Field {
439 name: Some("Hidden".to_string()),
440 value: Some("B".to_string()),
441 r#type: 1,
442 linked_id: None,
443 },
444 Field {
445 name: Some("Boolean (true)".to_string()),
446 value: Some("true".to_string()),
447 r#type: 2,
448 linked_id: None,
449 },
450 Field {
451 name: Some("Boolean (false)".to_string()),
452 value: Some("false".to_string()),
453 r#type: 2,
454 linked_id: None,
455 },
456 Field {
457 name: Some("Linked".to_string()),
458 value: None,
459 r#type: 3,
460 linked_id: Some(101),
461 },
462 ],
463
464 password_history: None,
465 revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
466 creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
467 deleted_date: None,
468 };
469
470 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
471
472 let expected = r#"{
473 "passwordHistory": null,
474 "revisionDate": "2024-01-30T14:09:33.753Z",
475 "creationDate": "2024-01-30T11:23:54.416Z",
476 "deletedDate": null,
477 "id": "25c8c414-b446-48e9-a1bd-b10700bbd740",
478 "organizationId": null,
479 "folderId": "942e2984-1b9a-453b-b039-b107012713b9",
480 "type": 1,
481 "reprompt": 0,
482 "name": "Bitwarden",
483 "notes": "My note",
484 "favorite": true,
485 "fields": [
486 {
487 "name": "Text",
488 "value": "A",
489 "type": 0,
490 "linkedId": null
491 },
492 {
493 "name": "Hidden",
494 "value": "B",
495 "type": 1,
496 "linkedId": null
497 },
498 {
499 "name": "Boolean (true)",
500 "value": "true",
501 "type": 2,
502 "linkedId": null
503 },
504 {
505 "name": "Boolean (false)",
506 "value": "false",
507 "type": 2,
508 "linkedId": null
509 },
510 {
511 "name": "Linked",
512 "value": null,
513 "type": 3,
514 "linkedId": 101
515 }
516 ],
517 "login": {
518 "fido2Credentials": [],
519 "uris": [
520 {
521 "match": null,
522 "uri": "https://vault.bitwarden.com"
523 }
524 ],
525 "username": "[email protected]",
526 "password": "asdfasdfasdf",
527 "totp": "ABC"
528 },
529 "collectionIds": null
530 }"#;
531
532 assert_eq!(
533 json.parse::<serde_json::Value>().unwrap(),
534 expected.parse::<serde_json::Value>().unwrap()
535 )
536 }
537
538 #[test]
539 fn test_convert_secure_note() {
540 let cipher = Cipher {
541 id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
542 folder_id: None,
543
544 name: "My secure note".to_string(),
545 notes: Some("Very secure!".to_string()),
546
547 r#type: CipherType::SecureNote(Box::new(SecureNote {
548 r#type: SecureNoteType::Generic,
549 })),
550
551 favorite: false,
552 reprompt: 0,
553
554 fields: vec![],
555
556 password_history: None,
557 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
558 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
559 deleted_date: None,
560 };
561
562 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
563
564 let expected = r#"{
565 "passwordHistory": null,
566 "revisionDate": "2024-01-30T11:25:25.466Z",
567 "creationDate": "2024-01-30T11:25:25.466Z",
568 "deletedDate": null,
569 "id": "23f0f877-42b1-4820-a850-b10700bc41eb",
570 "organizationId": null,
571 "folderId": null,
572 "type": 2,
573 "reprompt": 0,
574 "name": "My secure note",
575 "notes": "Very secure!",
576 "favorite": false,
577 "secureNote": {
578 "type": 0
579 },
580 "collectionIds": null
581 }"#;
582
583 assert_eq!(
584 json.parse::<serde_json::Value>().unwrap(),
585 expected.parse::<serde_json::Value>().unwrap()
586 )
587 }
588
589 #[test]
590 fn test_convert_card() {
591 let cipher = Cipher {
592 id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(),
593 folder_id: None,
594
595 name: "My card".to_string(),
596 notes: None,
597
598 r#type: CipherType::Card(Box::new(Card {
599 cardholder_name: Some("John Doe".to_string()),
600 exp_month: Some("1".to_string()),
601 exp_year: Some("2032".to_string()),
602 code: Some("123".to_string()),
603 brand: Some("Visa".to_string()),
604 number: Some("4111111111111111".to_string()),
605 })),
606
607 favorite: false,
608 reprompt: 0,
609
610 fields: vec![],
611
612 password_history: None,
613 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
614 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
615 deleted_date: None,
616 };
617
618 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
619
620 let expected = r#"{
621 "passwordHistory": null,
622 "revisionDate": "2024-01-30T17:55:36.150Z",
623 "creationDate": "2024-01-30T17:55:36.150Z",
624 "deletedDate": null,
625 "id": "3ed8de45-48ee-4e26-a2dc-b10701276c53",
626 "organizationId": null,
627 "folderId": null,
628 "type": 3,
629 "reprompt": 0,
630 "name": "My card",
631 "notes": null,
632 "favorite": false,
633 "card": {
634 "cardholderName": "John Doe",
635 "brand": "Visa",
636 "number": "4111111111111111",
637 "expMonth": "1",
638 "expYear": "2032",
639 "code": "123"
640 },
641 "collectionIds": null
642 }"#;
643
644 assert_eq!(
645 json.parse::<serde_json::Value>().unwrap(),
646 expected.parse::<serde_json::Value>().unwrap()
647 )
648 }
649
650 #[test]
651 fn test_convert_identity() {
652 let cipher = Cipher {
653 id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(),
654 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
655
656 name: "My identity".to_string(),
657 notes: None,
658
659 r#type: CipherType::Identity(Box::new(Identity {
660 title: Some("Mr".to_string()),
661 first_name: Some("John".to_string()),
662 middle_name: None,
663 last_name: Some("Doe".to_string()),
664 address1: None,
665 address2: None,
666 address3: None,
667 city: None,
668 state: None,
669 postal_code: None,
670 country: None,
671 company: Some("Bitwarden".to_string()),
672 email: None,
673 phone: None,
674 ssn: None,
675 username: Some("JDoe".to_string()),
676 passport_number: None,
677 license_number: None,
678 })),
679
680 favorite: false,
681 reprompt: 0,
682
683 fields: vec![],
684
685 password_history: None,
686 revision_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
687 creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
688 deleted_date: None,
689 };
690
691 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
692
693 let expected = r#"{
694 "passwordHistory": null,
695 "revisionDate": "2024-01-30T17:54:50.706Z",
696 "creationDate": "2024-01-30T17:54:50.706Z",
697 "deletedDate": null,
698 "id": "41cc3bc1-c3d9-4637-876c-b10701273712",
699 "organizationId": null,
700 "folderId": "942e2984-1b9a-453b-b039-b107012713b9",
701 "type": 4,
702 "reprompt": 0,
703 "name": "My identity",
704 "notes": null,
705 "favorite": false,
706 "identity": {
707 "title": "Mr",
708 "firstName": "John",
709 "middleName": null,
710 "lastName": "Doe",
711 "address1": null,
712 "address2": null,
713 "address3": null,
714 "city": null,
715 "state": null,
716 "postalCode": null,
717 "country": null,
718 "company": "Bitwarden",
719 "email": null,
720 "phone": null,
721 "ssn": null,
722 "username": "JDoe",
723 "passportNumber": null,
724 "licenseNumber": null
725 },
726 "collectionIds": null
727 }"#;
728
729 assert_eq!(
730 json.parse::<serde_json::Value>().unwrap(),
731 expected.parse::<serde_json::Value>().unwrap()
732 )
733 }
734
735 #[test]
736 fn test_convert_ssh_key() {
737 let cipher = Cipher {
738 id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
739 folder_id: None,
740
741 name: "My ssh key".to_string(),
742 notes: None,
743
744 r#type: CipherType::SshKey(Box::new(SshKey {
745 private_key: "private".to_string(),
746 public_key: "public".to_string(),
747 fingerprint: "fingerprint".to_string(),
748 })),
749
750 favorite: false,
751 reprompt: 0,
752
753 fields: vec![],
754
755 password_history: None,
756 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
757 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
758 deleted_date: None,
759 };
760
761 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
762
763 let expected = r#"{
764 "passwordHistory": null,
765 "revisionDate": "2024-01-30T11:25:25.466Z",
766 "creationDate": "2024-01-30T11:25:25.466Z",
767 "deletedDate": null,
768 "id": "23f0f877-42b1-4820-a850-b10700bc41eb",
769 "organizationId": null,
770 "folderId": null,
771 "type": 5,
772 "reprompt": 0,
773 "name": "My ssh key",
774 "notes": null,
775 "sshKey": {
776 "privateKey": "private",
777 "publicKey": "public",
778 "keyFingerprint": "fingerprint"
779 },
780 "favorite": false,
781 "collectionIds": null
782 }"#;
783
784 assert_eq!(
785 json.parse::<serde_json::Value>().unwrap(),
786 expected.parse::<serde_json::Value>().unwrap()
787 )
788 }
789
790 #[test]
791 pub fn test_export() {
792 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
793 d.push("resources");
794 d.push("json_export.json");
795
796 let mut file = fs::File::open(d).unwrap();
797
798 let mut expected = String::new();
799 file.read_to_string(&mut expected).unwrap();
800
801 let export = export_json(
802 vec![Folder {
803 id: "942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap(),
804 name: "Important".to_string(),
805 }],
806 vec![
807 Cipher {
808 id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
809 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
810
811 name: "Bitwarden".to_string(),
812 notes: Some("My note".to_string()),
813
814 r#type: CipherType::Login(Box::new(Login {
815 username: Some("[email protected]".to_string()),
816 password: Some("asdfasdfasdf".to_string()),
817 login_uris: vec![LoginUri {
818 uri: Some("https://vault.bitwarden.com".to_string()),
819 r#match: None,
820 }],
821 totp: Some("ABC".to_string()),
822 fido2_credentials: None,
823 })),
824
825 favorite: true,
826 reprompt: 0,
827
828 fields: vec![
829 Field {
830 name: Some("Text".to_string()),
831 value: Some("A".to_string()),
832 r#type: 0,
833 linked_id: None,
834 },
835 Field {
836 name: Some("Hidden".to_string()),
837 value: Some("B".to_string()),
838 r#type: 1,
839 linked_id: None,
840 },
841 Field {
842 name: Some("Boolean (true)".to_string()),
843 value: Some("true".to_string()),
844 r#type: 2,
845 linked_id: None,
846 },
847 Field {
848 name: Some("Boolean (false)".to_string()),
849 value: Some("false".to_string()),
850 r#type: 2,
851 linked_id: None,
852 },
853 Field {
854 name: Some("Linked".to_string()),
855 value: None,
856 r#type: 3,
857 linked_id: Some(101),
858 },
859 ],
860
861 password_history: None,
862 revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
863 creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
864 deleted_date: None,
865 },
866 Cipher {
867 id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
868 folder_id: None,
869
870 name: "My secure note".to_string(),
871 notes: Some("Very secure!".to_string()),
872
873 r#type: CipherType::SecureNote(Box::new(SecureNote {
874 r#type: SecureNoteType::Generic,
875 })),
876
877 favorite: false,
878 reprompt: 0,
879
880 fields: vec![],
881
882 password_history: None,
883 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
884 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
885 deleted_date: None,
886 },
887 Cipher {
888 id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(),
889 folder_id: None,
890
891 name: "My card".to_string(),
892 notes: None,
893
894 r#type: CipherType::Card(Box::new(Card {
895 cardholder_name: Some("John Doe".to_string()),
896 exp_month: Some("1".to_string()),
897 exp_year: Some("2032".to_string()),
898 code: Some("123".to_string()),
899 brand: Some("Visa".to_string()),
900 number: Some("4111111111111111".to_string()),
901 })),
902
903 favorite: false,
904 reprompt: 0,
905
906 fields: vec![],
907
908 password_history: None,
909 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
910 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
911 deleted_date: None,
912 },
913 Cipher {
914 id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(),
915 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
916
917 name: "My identity".to_string(),
918 notes: None,
919
920 r#type: CipherType::Identity(Box::new(Identity {
921 title: Some("Mr".to_string()),
922 first_name: Some("John".to_string()),
923 middle_name: None,
924 last_name: Some("Doe".to_string()),
925 address1: None,
926 address2: None,
927 address3: None,
928 city: None,
929 state: None,
930 postal_code: None,
931 country: None,
932 company: Some("Bitwarden".to_string()),
933 email: None,
934 phone: None,
935 ssn: None,
936 username: Some("JDoe".to_string()),
937 passport_number: None,
938 license_number: None,
939 })),
940
941 favorite: false,
942 reprompt: 0,
943
944 fields: vec![],
945
946 password_history: None,
947 revision_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
948 creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
949 deleted_date: None,
950 },
951 Cipher {
952 id: "646594a9-a9cb-4082-9d57-0024c3fbcaa9".parse().unwrap(),
953 folder_id: None,
954
955 name: "My ssh key".to_string(),
956 notes: None,
957
958 r#type: CipherType::SshKey(Box::new(SshKey {
959 private_key: "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiwAAAKAy48fwMuPH\n8AAAAAtzc2gtZWQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiw\nAAAEAYUCIdfLI14K3XIy9V0FDZLQoZ9gcjOnvFjb4uA335HmKc0TlyEy0IeHcFXQfX4Kk+\nURAHBHlwP5dv2LwxocaLAAAAHHF1ZXh0ZW5ATWFjQm9vay1Qcm8tMTYubG9jYWwB\n-----END OPENSSH PRIVATE KEY-----".to_string(),
960 public_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGKc0TlyEy0IeHcFXQfX4Kk+URAHBHlwP5dv2LwxocaL".to_string(),
961 fingerprint: "SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0".to_string(),
962 })),
963
964 favorite: false,
965 reprompt: 0,
966
967 fields: vec![],
968
969 password_history: None,
970 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
971 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
972 deleted_date: None,
973 }
974 ],
975 )
976 .unwrap();
977
978 assert_eq!(
979 export.parse::<serde_json::Value>().unwrap(),
980 expected.parse::<serde_json::Value>().unwrap()
981 )
982 }
983
984 #[test]
988 fn test_login_with_fido2_credentials() {
989 let cipher = Cipher {
990 id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
991 folder_id: None,
992 name: "Bitwarden".to_string(),
993 notes: None,
994 r#type: CipherType::Login(Box::new(Login {
995 username: None,
996 password: None,
997 login_uris: vec![],
998 totp: None,
999 fido2_credentials: Some(vec![Fido2Credential {
1000 credential_id: "e8d88789-e916-e196-3cbd-81dafae71bbc".to_string(),
1001 key_type: "public-key".to_string(),
1002 key_algorithm: "ECDSA".to_string(),
1003 key_curve: "P-256".to_string(),
1004 key_value: "AAECAwQFBg".to_string(),
1005 rp_id: "bitwarden.com".to_string(),
1006 user_handle: Some("AAECAwQFBg".to_string()),
1007 user_name: Some("[email protected]".to_string()),
1008 counter: 0,
1009 rp_name: Some("Bitwarden".to_string()),
1010 user_display_name: Some("User".to_string()),
1011 discoverable: "true".to_string(),
1012 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
1013 }]),
1014 })),
1015 favorite: false,
1016 reprompt: 0,
1017 fields: vec![],
1018 password_history: None,
1019 revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
1020 creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
1021 deleted_date: None,
1022 };
1023
1024 let json = serde_json::to_value(JsonCipher::from(cipher)).unwrap();
1025 let creds = &json["login"]["fido2Credentials"];
1026
1027 assert_eq!(creds.as_array().unwrap().len(), 1);
1028 assert_eq!(
1029 creds[0]["credentialId"],
1030 "e8d88789-e916-e196-3cbd-81dafae71bbc"
1031 );
1032 assert_eq!(creds[0]["keyType"], "public-key");
1033 assert_eq!(creds[0]["counter"], "0");
1035 assert_eq!(creds[0]["discoverable"], "true");
1036 assert_eq!(creds[0]["creationDate"], "2024-06-07T14:12:36.150Z");
1038 }
1039
1040 #[test]
1043 fn test_cipher_with_password_history() {
1044 let cipher = Cipher {
1045 id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
1046 folder_id: None,
1047 name: "Bitwarden".to_string(),
1048 notes: None,
1049 r#type: CipherType::Login(Box::new(Login {
1050 username: None,
1051 password: None,
1052 login_uris: vec![],
1053 totp: None,
1054 fido2_credentials: None,
1055 })),
1056 favorite: false,
1057 reprompt: 0,
1058 fields: vec![],
1059 password_history: Some(vec![PasswordHistory {
1060 password: "old-password".to_string(),
1061 last_used_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
1062 }]),
1063 revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
1064 creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
1065 deleted_date: None,
1066 };
1067
1068 let json = serde_json::to_value(JsonCipher::from(cipher)).unwrap();
1069 let history = &json["passwordHistory"];
1070
1071 assert_eq!(history.as_array().unwrap().len(), 1);
1072 assert_eq!(history[0]["password"], "old-password");
1073 assert_eq!(history[0]["lastUsedDate"], "2024-01-30T14:09:33.753Z");
1074 }
1075
1076 #[test]
1079 fn test_dates_use_millisecond_precision() {
1080 let cipher = Cipher {
1081 id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
1082 folder_id: None,
1083 name: "Bitwarden".to_string(),
1084 notes: None,
1085 r#type: CipherType::SecureNote(Box::new(SecureNote {
1086 r#type: SecureNoteType::Generic,
1087 })),
1088 favorite: false,
1089 reprompt: 0,
1090 fields: vec![],
1091 password_history: None,
1092 revision_date: "2024-01-30T14:09:33.753456Z".parse().unwrap(),
1094 creation_date: "2024-01-30T11:23:54.416789123Z".parse().unwrap(),
1095 deleted_date: Some("2024-02-01T00:00:00.000000Z".parse().unwrap()),
1096 };
1097
1098 let json = serde_json::to_value(JsonCipher::from(cipher)).unwrap();
1099
1100 assert_eq!(json["revisionDate"], "2024-01-30T14:09:33.753Z");
1101 assert_eq!(json["creationDate"], "2024-01-30T11:23:54.416Z");
1102 assert_eq!(json["deletedDate"], "2024-02-01T00:00:00.000Z");
1103 }
1104}