1use chrono::{DateTime, Utc};
2use thiserror::Error;
3use uuid::Uuid;
4
5use crate::{
6 Card, Cipher, CipherType, Field, Folder, Identity, Login, LoginUri, SecureNote, SshKey,
7};
8
9#[derive(Error, Debug)]
10pub enum JsonError {
11 #[error("JSON error: {0}")]
12 Serde(#[from] serde_json::Error),
13}
14
15pub(crate) fn export_json(folders: Vec<Folder>, ciphers: Vec<Cipher>) -> Result<String, JsonError> {
16 let export = JsonExport {
17 encrypted: false,
18 folders: folders.into_iter().map(|f| f.into()).collect(),
19 items: ciphers.into_iter().map(|c| c.into()).collect(),
20 };
21
22 Ok(serde_json::to_string_pretty(&export)?)
23}
24
25#[derive(serde::Serialize)]
30#[serde(rename_all = "camelCase")]
31struct JsonExport {
32 encrypted: bool,
33 folders: Vec<JsonFolder>,
34 items: Vec<JsonCipher>,
35}
36
37#[derive(serde::Serialize)]
38#[serde(rename_all = "camelCase")]
39struct JsonFolder {
40 id: Uuid,
41 name: String,
42}
43
44impl From<Folder> for JsonFolder {
45 fn from(folder: Folder) -> Self {
46 JsonFolder {
47 id: folder.id,
48 name: folder.name,
49 }
50 }
51}
52
53#[derive(serde::Serialize)]
54#[serde(rename_all = "camelCase")]
55struct JsonCipher {
56 id: Uuid,
57 folder_id: Option<Uuid>,
58 organization_id: Option<Uuid>,
60 collection_ids: Option<Vec<Uuid>>,
61
62 name: String,
63 notes: Option<String>,
64
65 r#type: u8,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 login: Option<JsonLogin>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 identity: Option<JsonIdentity>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 card: Option<JsonCard>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 secure_note: Option<JsonSecureNote>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 ssh_key: Option<JsonSshKey>,
76
77 favorite: bool,
78 reprompt: u8,
79
80 #[serde(skip_serializing_if = "Vec::is_empty")]
81 fields: Vec<JsonField>,
82 password_history: Option<Vec<String>>,
83
84 revision_date: DateTime<Utc>,
85 creation_date: DateTime<Utc>,
86 deleted_date: Option<DateTime<Utc>>,
87}
88
89#[derive(serde::Serialize)]
90#[serde(rename_all = "camelCase")]
91struct JsonLogin {
92 username: Option<String>,
93 password: Option<String>,
94 uris: Vec<JsonLoginUri>,
95 totp: Option<String>,
96 fido2_credentials: Vec<String>,
97}
98
99impl From<Login> for JsonLogin {
100 fn from(login: Login) -> Self {
101 JsonLogin {
102 username: login.username,
103 password: login.password,
104 uris: login.login_uris.into_iter().map(|u| u.into()).collect(),
105 totp: login.totp,
106 fido2_credentials: vec![],
107 }
108 }
109}
110
111#[derive(serde::Serialize)]
112#[serde(rename_all = "camelCase")]
113struct JsonLoginUri {
114 uri: Option<String>,
115 r#match: Option<u8>,
116}
117
118impl From<LoginUri> for JsonLoginUri {
119 fn from(login_uri: LoginUri) -> Self {
120 JsonLoginUri {
121 uri: login_uri.uri,
122 r#match: login_uri.r#match,
123 }
124 }
125}
126
127#[derive(serde::Serialize)]
128#[serde(rename_all = "camelCase")]
129struct JsonSecureNote {
130 r#type: u8,
131}
132
133impl From<SecureNote> for JsonSecureNote {
134 fn from(note: SecureNote) -> Self {
135 JsonSecureNote {
136 r#type: note.r#type as u8,
137 }
138 }
139}
140
141#[derive(serde::Serialize)]
142#[serde(rename_all = "camelCase")]
143struct JsonCard {
144 cardholder_name: Option<String>,
145 exp_month: Option<String>,
146 exp_year: Option<String>,
147 code: Option<String>,
148 brand: Option<String>,
149 number: Option<String>,
150}
151
152impl From<Card> for JsonCard {
153 fn from(card: Card) -> Self {
154 JsonCard {
155 cardholder_name: card.cardholder_name,
156 exp_month: card.exp_month,
157 exp_year: card.exp_year,
158 code: card.code,
159 brand: card.brand,
160 number: card.number,
161 }
162 }
163}
164
165#[derive(serde::Serialize)]
166#[serde(rename_all = "camelCase")]
167struct JsonIdentity {
168 title: Option<String>,
169 first_name: Option<String>,
170 middle_name: Option<String>,
171 last_name: Option<String>,
172 address1: Option<String>,
173 address2: Option<String>,
174 address3: Option<String>,
175 city: Option<String>,
176 state: Option<String>,
177 postal_code: Option<String>,
178 country: Option<String>,
179 company: Option<String>,
180 email: Option<String>,
181 phone: Option<String>,
182 ssn: Option<String>,
183 username: Option<String>,
184 passport_number: Option<String>,
185 license_number: Option<String>,
186}
187
188impl From<Identity> for JsonIdentity {
189 fn from(identity: Identity) -> Self {
190 JsonIdentity {
191 title: identity.title,
192 first_name: identity.first_name,
193 middle_name: identity.middle_name,
194 last_name: identity.last_name,
195 address1: identity.address1,
196 address2: identity.address2,
197 address3: identity.address3,
198 city: identity.city,
199 state: identity.state,
200 postal_code: identity.postal_code,
201 country: identity.country,
202 company: identity.company,
203 email: identity.email,
204 phone: identity.phone,
205 ssn: identity.ssn,
206 username: identity.username,
207 passport_number: identity.passport_number,
208 license_number: identity.license_number,
209 }
210 }
211}
212
213#[derive(serde::Serialize)]
214#[serde(rename_all = "camelCase")]
215struct JsonSshKey {
216 private_key: String,
217 public_key: String,
218 key_fingerprint: String,
219}
220
221impl From<SshKey> for JsonSshKey {
222 fn from(ssh_key: SshKey) -> Self {
223 JsonSshKey {
224 private_key: ssh_key.private_key,
225 public_key: ssh_key.public_key,
226 key_fingerprint: ssh_key.fingerprint,
227 }
228 }
229}
230
231#[derive(serde::Serialize)]
232#[serde(rename_all = "camelCase")]
233struct JsonField {
234 name: Option<String>,
235 value: Option<String>,
236 r#type: u8,
237 linked_id: Option<u32>,
238}
239
240impl From<Field> for JsonField {
241 fn from(field: Field) -> Self {
242 JsonField {
243 name: field.name,
244 value: field.value,
245 r#type: field.r#type,
246 linked_id: field.linked_id,
247 }
248 }
249}
250
251impl From<Cipher> for JsonCipher {
252 fn from(cipher: Cipher) -> Self {
253 let r#type = match cipher.r#type {
254 CipherType::Login(_) => 1,
255 CipherType::SecureNote(_) => 2,
256 CipherType::Card(_) => 3,
257 CipherType::Identity(_) => 4,
258 CipherType::SshKey(_) => 5,
259 };
260
261 let (login, secure_note, card, identity, ssh_key) = match cipher.r#type {
262 CipherType::Login(l) => (Some((*l).into()), None, None, None, None),
263 CipherType::SecureNote(s) => (None, Some((*s).into()), None, None, None),
264 CipherType::Card(c) => (None, None, Some((*c).into()), None, None),
265 CipherType::Identity(i) => (None, None, None, Some((*i).into()), None),
266 CipherType::SshKey(ssh) => (None, None, None, None, Some((*ssh).into())),
267 };
268
269 JsonCipher {
270 id: cipher.id,
271 folder_id: cipher.folder_id,
272 organization_id: None,
273 collection_ids: None,
274 name: cipher.name,
275 notes: cipher.notes,
276 r#type,
277 login,
278 identity,
279 card,
280 secure_note,
281 ssh_key,
282 favorite: cipher.favorite,
283 reprompt: cipher.reprompt,
284 fields: cipher.fields.into_iter().map(|f| f.into()).collect(),
285 password_history: None,
286 revision_date: cipher.revision_date,
287 creation_date: cipher.creation_date,
288 deleted_date: cipher.deleted_date,
289 }
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use std::{fs, io::Read, path::PathBuf};
296
297 use super::*;
298 use crate::{Cipher, Field, LoginUri, SecureNoteType};
299
300 #[test]
301 fn test_convert_login() {
302 let cipher = Cipher {
303 id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
304 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
305
306 name: "Bitwarden".to_string(),
307 notes: Some("My note".to_string()),
308
309 r#type: CipherType::Login(Box::new(Login {
310 username: Some("[email protected]".to_string()),
311 password: Some("asdfasdfasdf".to_string()),
312 login_uris: vec![LoginUri {
313 uri: Some("https://vault.bitwarden.com".to_string()),
314 r#match: None,
315 }],
316 totp: Some("ABC".to_string()),
317 fido2_credentials: None,
318 })),
319
320 favorite: true,
321 reprompt: 0,
322
323 fields: vec![
324 Field {
325 name: Some("Text".to_string()),
326 value: Some("A".to_string()),
327 r#type: 0,
328 linked_id: None,
329 },
330 Field {
331 name: Some("Hidden".to_string()),
332 value: Some("B".to_string()),
333 r#type: 1,
334 linked_id: None,
335 },
336 Field {
337 name: Some("Boolean (true)".to_string()),
338 value: Some("true".to_string()),
339 r#type: 2,
340 linked_id: None,
341 },
342 Field {
343 name: Some("Boolean (false)".to_string()),
344 value: Some("false".to_string()),
345 r#type: 2,
346 linked_id: None,
347 },
348 Field {
349 name: Some("Linked".to_string()),
350 value: None,
351 r#type: 3,
352 linked_id: Some(101),
353 },
354 ],
355
356 revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
357 creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
358 deleted_date: None,
359 };
360
361 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
362
363 let expected = r#"{
364 "passwordHistory": null,
365 "revisionDate": "2024-01-30T14:09:33.753Z",
366 "creationDate": "2024-01-30T11:23:54.416Z",
367 "deletedDate": null,
368 "id": "25c8c414-b446-48e9-a1bd-b10700bbd740",
369 "organizationId": null,
370 "folderId": "942e2984-1b9a-453b-b039-b107012713b9",
371 "type": 1,
372 "reprompt": 0,
373 "name": "Bitwarden",
374 "notes": "My note",
375 "favorite": true,
376 "fields": [
377 {
378 "name": "Text",
379 "value": "A",
380 "type": 0,
381 "linkedId": null
382 },
383 {
384 "name": "Hidden",
385 "value": "B",
386 "type": 1,
387 "linkedId": null
388 },
389 {
390 "name": "Boolean (true)",
391 "value": "true",
392 "type": 2,
393 "linkedId": null
394 },
395 {
396 "name": "Boolean (false)",
397 "value": "false",
398 "type": 2,
399 "linkedId": null
400 },
401 {
402 "name": "Linked",
403 "value": null,
404 "type": 3,
405 "linkedId": 101
406 }
407 ],
408 "login": {
409 "fido2Credentials": [],
410 "uris": [
411 {
412 "match": null,
413 "uri": "https://vault.bitwarden.com"
414 }
415 ],
416 "username": "[email protected]",
417 "password": "asdfasdfasdf",
418 "totp": "ABC"
419 },
420 "collectionIds": null
421 }"#;
422
423 assert_eq!(
424 json.parse::<serde_json::Value>().unwrap(),
425 expected.parse::<serde_json::Value>().unwrap()
426 )
427 }
428
429 #[test]
430 fn test_convert_secure_note() {
431 let cipher = Cipher {
432 id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
433 folder_id: None,
434
435 name: "My secure note".to_string(),
436 notes: Some("Very secure!".to_string()),
437
438 r#type: CipherType::SecureNote(Box::new(SecureNote {
439 r#type: SecureNoteType::Generic,
440 })),
441
442 favorite: false,
443 reprompt: 0,
444
445 fields: vec![],
446
447 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
448 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
449 deleted_date: None,
450 };
451
452 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
453
454 let expected = r#"{
455 "passwordHistory": null,
456 "revisionDate": "2024-01-30T11:25:25.466Z",
457 "creationDate": "2024-01-30T11:25:25.466Z",
458 "deletedDate": null,
459 "id": "23f0f877-42b1-4820-a850-b10700bc41eb",
460 "organizationId": null,
461 "folderId": null,
462 "type": 2,
463 "reprompt": 0,
464 "name": "My secure note",
465 "notes": "Very secure!",
466 "favorite": false,
467 "secureNote": {
468 "type": 0
469 },
470 "collectionIds": null
471 }"#;
472
473 assert_eq!(
474 json.parse::<serde_json::Value>().unwrap(),
475 expected.parse::<serde_json::Value>().unwrap()
476 )
477 }
478
479 #[test]
480 fn test_convert_card() {
481 let cipher = Cipher {
482 id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(),
483 folder_id: None,
484
485 name: "My card".to_string(),
486 notes: None,
487
488 r#type: CipherType::Card(Box::new(Card {
489 cardholder_name: Some("John Doe".to_string()),
490 exp_month: Some("1".to_string()),
491 exp_year: Some("2032".to_string()),
492 code: Some("123".to_string()),
493 brand: Some("Visa".to_string()),
494 number: Some("4111111111111111".to_string()),
495 })),
496
497 favorite: false,
498 reprompt: 0,
499
500 fields: vec![],
501
502 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
503 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
504 deleted_date: None,
505 };
506
507 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
508
509 let expected = r#"{
510 "passwordHistory": null,
511 "revisionDate": "2024-01-30T17:55:36.150Z",
512 "creationDate": "2024-01-30T17:55:36.150Z",
513 "deletedDate": null,
514 "id": "3ed8de45-48ee-4e26-a2dc-b10701276c53",
515 "organizationId": null,
516 "folderId": null,
517 "type": 3,
518 "reprompt": 0,
519 "name": "My card",
520 "notes": null,
521 "favorite": false,
522 "card": {
523 "cardholderName": "John Doe",
524 "brand": "Visa",
525 "number": "4111111111111111",
526 "expMonth": "1",
527 "expYear": "2032",
528 "code": "123"
529 },
530 "collectionIds": null
531 }"#;
532
533 assert_eq!(
534 json.parse::<serde_json::Value>().unwrap(),
535 expected.parse::<serde_json::Value>().unwrap()
536 )
537 }
538
539 #[test]
540 fn test_convert_identity() {
541 let cipher = Cipher {
542 id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(),
543 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
544
545 name: "My identity".to_string(),
546 notes: None,
547
548 r#type: CipherType::Identity(Box::new(Identity {
549 title: Some("Mr".to_string()),
550 first_name: Some("John".to_string()),
551 middle_name: None,
552 last_name: Some("Doe".to_string()),
553 address1: None,
554 address2: None,
555 address3: None,
556 city: None,
557 state: None,
558 postal_code: None,
559 country: None,
560 company: Some("Bitwarden".to_string()),
561 email: None,
562 phone: None,
563 ssn: None,
564 username: Some("JDoe".to_string()),
565 passport_number: None,
566 license_number: None,
567 })),
568
569 favorite: false,
570 reprompt: 0,
571
572 fields: vec![],
573
574 revision_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
575 creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
576 deleted_date: None,
577 };
578
579 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
580
581 let expected = r#"{
582 "passwordHistory": null,
583 "revisionDate": "2024-01-30T17:54:50.706Z",
584 "creationDate": "2024-01-30T17:54:50.706Z",
585 "deletedDate": null,
586 "id": "41cc3bc1-c3d9-4637-876c-b10701273712",
587 "organizationId": null,
588 "folderId": "942e2984-1b9a-453b-b039-b107012713b9",
589 "type": 4,
590 "reprompt": 0,
591 "name": "My identity",
592 "notes": null,
593 "favorite": false,
594 "identity": {
595 "title": "Mr",
596 "firstName": "John",
597 "middleName": null,
598 "lastName": "Doe",
599 "address1": null,
600 "address2": null,
601 "address3": null,
602 "city": null,
603 "state": null,
604 "postalCode": null,
605 "country": null,
606 "company": "Bitwarden",
607 "email": null,
608 "phone": null,
609 "ssn": null,
610 "username": "JDoe",
611 "passportNumber": null,
612 "licenseNumber": null
613 },
614 "collectionIds": null
615 }"#;
616
617 assert_eq!(
618 json.parse::<serde_json::Value>().unwrap(),
619 expected.parse::<serde_json::Value>().unwrap()
620 )
621 }
622
623 #[test]
624 fn test_convert_ssh_key() {
625 let cipher = Cipher {
626 id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
627 folder_id: None,
628
629 name: "My ssh key".to_string(),
630 notes: None,
631
632 r#type: CipherType::SshKey(Box::new(SshKey {
633 private_key: "private".to_string(),
634 public_key: "public".to_string(),
635 fingerprint: "fingerprint".to_string(),
636 })),
637
638 favorite: false,
639 reprompt: 0,
640
641 fields: vec![],
642
643 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
644 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
645 deleted_date: None,
646 };
647
648 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
649
650 let expected = r#"{
651 "passwordHistory": null,
652 "revisionDate": "2024-01-30T11:25:25.466Z",
653 "creationDate": "2024-01-30T11:25:25.466Z",
654 "deletedDate": null,
655 "id": "23f0f877-42b1-4820-a850-b10700bc41eb",
656 "organizationId": null,
657 "folderId": null,
658 "type": 5,
659 "reprompt": 0,
660 "name": "My ssh key",
661 "notes": null,
662 "sshKey": {
663 "privateKey": "private",
664 "publicKey": "public",
665 "keyFingerprint": "fingerprint"
666 },
667 "favorite": false,
668 "collectionIds": null
669 }"#;
670
671 assert_eq!(
672 json.parse::<serde_json::Value>().unwrap(),
673 expected.parse::<serde_json::Value>().unwrap()
674 )
675 }
676
677 #[test]
678 pub fn test_export() {
679 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
680 d.push("resources");
681 d.push("json_export.json");
682
683 let mut file = fs::File::open(d).unwrap();
684
685 let mut expected = String::new();
686 file.read_to_string(&mut expected).unwrap();
687
688 let export = export_json(
689 vec![Folder {
690 id: "942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap(),
691 name: "Important".to_string(),
692 }],
693 vec![
694 Cipher {
695 id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
696 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
697
698 name: "Bitwarden".to_string(),
699 notes: Some("My note".to_string()),
700
701 r#type: CipherType::Login(Box::new(Login {
702 username: Some("[email protected]".to_string()),
703 password: Some("asdfasdfasdf".to_string()),
704 login_uris: vec![LoginUri {
705 uri: Some("https://vault.bitwarden.com".to_string()),
706 r#match: None,
707 }],
708 totp: Some("ABC".to_string()),
709 fido2_credentials: None,
710 })),
711
712 favorite: true,
713 reprompt: 0,
714
715 fields: vec![
716 Field {
717 name: Some("Text".to_string()),
718 value: Some("A".to_string()),
719 r#type: 0,
720 linked_id: None,
721 },
722 Field {
723 name: Some("Hidden".to_string()),
724 value: Some("B".to_string()),
725 r#type: 1,
726 linked_id: None,
727 },
728 Field {
729 name: Some("Boolean (true)".to_string()),
730 value: Some("true".to_string()),
731 r#type: 2,
732 linked_id: None,
733 },
734 Field {
735 name: Some("Boolean (false)".to_string()),
736 value: Some("false".to_string()),
737 r#type: 2,
738 linked_id: None,
739 },
740 Field {
741 name: Some("Linked".to_string()),
742 value: None,
743 r#type: 3,
744 linked_id: Some(101),
745 },
746 ],
747
748 revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
749 creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
750 deleted_date: None,
751 },
752 Cipher {
753 id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
754 folder_id: None,
755
756 name: "My secure note".to_string(),
757 notes: Some("Very secure!".to_string()),
758
759 r#type: CipherType::SecureNote(Box::new(SecureNote {
760 r#type: SecureNoteType::Generic,
761 })),
762
763 favorite: false,
764 reprompt: 0,
765
766 fields: vec![],
767
768 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
769 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
770 deleted_date: None,
771 },
772 Cipher {
773 id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(),
774 folder_id: None,
775
776 name: "My card".to_string(),
777 notes: None,
778
779 r#type: CipherType::Card(Box::new(Card {
780 cardholder_name: Some("John Doe".to_string()),
781 exp_month: Some("1".to_string()),
782 exp_year: Some("2032".to_string()),
783 code: Some("123".to_string()),
784 brand: Some("Visa".to_string()),
785 number: Some("4111111111111111".to_string()),
786 })),
787
788 favorite: false,
789 reprompt: 0,
790
791 fields: vec![],
792
793 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
794 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
795 deleted_date: None,
796 },
797 Cipher {
798 id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(),
799 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
800
801 name: "My identity".to_string(),
802 notes: None,
803
804 r#type: CipherType::Identity(Box::new(Identity {
805 title: Some("Mr".to_string()),
806 first_name: Some("John".to_string()),
807 middle_name: None,
808 last_name: Some("Doe".to_string()),
809 address1: None,
810 address2: None,
811 address3: None,
812 city: None,
813 state: None,
814 postal_code: None,
815 country: None,
816 company: Some("Bitwarden".to_string()),
817 email: None,
818 phone: None,
819 ssn: None,
820 username: Some("JDoe".to_string()),
821 passport_number: None,
822 license_number: None,
823 })),
824
825 favorite: false,
826 reprompt: 0,
827
828 fields: vec![],
829
830 revision_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
831 creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
832 deleted_date: None,
833 },
834 Cipher {
835 id: "646594a9-a9cb-4082-9d57-0024c3fbcaa9".parse().unwrap(),
836 folder_id: None,
837
838 name: "My ssh key".to_string(),
839 notes: None,
840
841 r#type: CipherType::SshKey(Box::new(SshKey {
842 private_key: "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiwAAAKAy48fwMuPH\n8AAAAAtzc2gtZWQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiw\nAAAEAYUCIdfLI14K3XIy9V0FDZLQoZ9gcjOnvFjb4uA335HmKc0TlyEy0IeHcFXQfX4Kk+\nURAHBHlwP5dv2LwxocaLAAAAHHF1ZXh0ZW5ATWFjQm9vay1Qcm8tMTYubG9jYWwB\n-----END OPENSSH PRIVATE KEY-----".to_string(),
843 public_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGKc0TlyEy0IeHcFXQfX4Kk+URAHBHlwP5dv2LwxocaL".to_string(),
844 fingerprint: "SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0".to_string(),
845 })),
846
847 favorite: false,
848 reprompt: 0,
849
850 fields: vec![],
851
852 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
853 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
854 deleted_date: None,
855 }
856 ],
857 )
858 .unwrap();
859
860 assert_eq!(
861 export.parse::<serde_json::Value>().unwrap(),
862 expected.parse::<serde_json::Value>().unwrap()
863 )
864 }
865}