1use chrono::{DateTime, Utc};
2use credential_exchange_format::{
3 Account as CxfAccount, AddressCredential, ApiKeyCredential, BasicAuthCredential, Credential,
4 CreditCardCredential, CustomFieldsCredential, DriversLicenseCredential, EditableField,
5 EditableFieldString, EditableFieldValue, IdentityDocumentCredential, Item, NoteCredential,
6 PasskeyCredential, PassportCredential, PersonNameCredential, SshKeyCredential, TotpCredential,
7 WifiCredential,
8};
9
10use crate::{
11 cxf::{
12 api_key::api_key_to_fields,
13 card::to_card,
14 editable_field::create_field,
15 identity::{
16 address_to_identity, drivers_license_to_identity, identity_document_to_identity,
17 passport_to_identity, person_name_to_identity,
18 },
19 login::to_login,
20 note::extract_note_content,
21 ssh::to_ssh,
22 wifi::wifi_to_fields,
23 CxfError,
24 },
25 CipherType, Field, ImportingCipher, SecureNote, SecureNoteType,
26};
27
28pub(crate) fn parse_cxf(payload: String) -> Result<Vec<ImportingCipher>, CxfError> {
32 let account: CxfAccount = serde_json::from_str(&payload)?;
33
34 let items: Vec<ImportingCipher> = account.items.into_iter().flat_map(parse_item).collect();
35
36 Ok(items)
37}
38
39fn convert_date(ts: Option<u64>) -> DateTime<Utc> {
43 ts.and_then(|ts| DateTime::from_timestamp(ts as i64, 0))
44 .unwrap_or(Utc::now())
45}
46
47fn custom_fields_to_fields(custom_fields: &CustomFieldsCredential) -> Vec<Field> {
50 custom_fields
51 .fields
52 .iter()
53 .map(|field_value| match field_value {
54 EditableFieldValue::String(field) => create_field(field, None::<String>),
55 EditableFieldValue::ConcealedString(field) => create_field(field, None::<String>),
56 EditableFieldValue::Boolean(field) => create_field(field, None::<String>),
57 EditableFieldValue::Date(field) => create_field(field, None::<String>),
58 EditableFieldValue::YearMonth(field) => create_field(field, None::<String>),
59 EditableFieldValue::SubdivisionCode(field) => create_field(field, None::<String>),
60 EditableFieldValue::CountryCode(field) => create_field(field, None::<String>),
61 EditableFieldValue::WifiNetworkSecurityType(field) => {
62 create_field(field, None::<String>)
63 }
64 _ => create_field(
65 &EditableField {
66 id: None,
67 label: Some("Unknown Field".to_string()),
68 value: EditableFieldString("".to_string()),
69 extensions: None,
70 },
71 None::<String>,
72 ),
73 })
74 .collect()
75}
76
77pub(super) fn parse_item(value: Item) -> Vec<ImportingCipher> {
78 let grouped = group_credentials_by_type(value.credentials);
79
80 let creation_date = convert_date(value.creation_at);
81 let revision_date = convert_date(value.modified_at);
82
83 let mut output = vec![];
84
85 let scope = value.scope.as_ref();
86
87 let note_content = grouped.note.first().map(extract_note_content);
89
90 let mut add_item = |t: CipherType, fields: Vec<Field>| {
92 output.push(ImportingCipher {
93 folder_id: None, name: value.title.clone(),
95 notes: note_content.clone(),
96 r#type: t,
97 favorite: false,
98 reprompt: 0,
99 fields,
100 revision_date,
101 creation_date,
102 deleted_date: None,
103 })
104 };
105
106 if !grouped.basic_auth.is_empty() || !grouped.passkey.is_empty() || !grouped.totp.is_empty() {
108 let basic_auth = grouped.basic_auth.first();
109 let passkey = grouped.passkey.first();
110 let totp = grouped.totp.first();
111
112 let login = to_login(creation_date, basic_auth, passkey, totp, scope);
113 add_item(CipherType::Login(Box::new(login)), vec![]);
114 }
115
116 if let Some(credit_card) = grouped.credit_card.first() {
118 let (card, fields) = to_card(credit_card);
119
120 add_item(CipherType::Card(Box::new(card)), fields);
121 }
122
123 let secure_note_type = || {
125 CipherType::SecureNote(Box::new(SecureNote {
126 r#type: SecureNoteType::Generic,
127 }))
128 };
129
130 if let Some(api_key) = grouped.api_key.first() {
132 let fields = api_key_to_fields(api_key);
133 add_item(secure_note_type(), fields);
134 }
135
136 if let Some(wifi) = grouped.wifi.first() {
138 let fields = wifi_to_fields(wifi);
139 add_item(secure_note_type(), fields);
140 }
141
142 [
144 grouped
145 .address
146 .first()
147 .map(|a| address_to_identity(a.clone())),
148 grouped
149 .passport
150 .first()
151 .map(|p| passport_to_identity(p.clone())),
152 grouped
153 .person_name
154 .first()
155 .map(|p| person_name_to_identity(p.clone())),
156 grouped
157 .drivers_license
158 .first()
159 .map(|d| drivers_license_to_identity(d.clone())),
160 grouped
161 .identity_document
162 .first()
163 .map(|i| identity_document_to_identity(i.clone())),
164 ]
165 .into_iter()
166 .flatten()
167 .for_each(|(identity, custom_fields)| {
168 add_item(CipherType::Identity(Box::new(identity)), custom_fields);
169 });
170
171 if let Some(ssh) = grouped.ssh.first() {
173 match to_ssh(ssh) {
174 Ok((ssh_key, fields)) => add_item(CipherType::SshKey(Box::new(ssh_key)), fields),
175 Err(_) => {
176 }
178 }
179 }
180
181 if let Some(custom_fields) = grouped.custom_fields.first() {
183 let fields = custom_fields_to_fields(custom_fields);
184 add_item(secure_note_type(), fields);
185 }
186
187 if !grouped.note.is_empty() && output.is_empty() {
189 let standalone_note_content = grouped.note.first().map(extract_note_content);
190 output.push(ImportingCipher {
191 folder_id: None, name: value.title.clone(),
193 notes: standalone_note_content,
194 r#type: secure_note_type(),
195 favorite: false,
196 reprompt: 0,
197 fields: vec![],
198 revision_date,
199 creation_date,
200 deleted_date: None,
201 });
202 }
203
204 output
205}
206
207fn group_credentials_by_type(credentials: Vec<Credential>) -> GroupedCredentials {
214 fn filter_credentials<T>(
215 credentials: &[Credential],
216 f: impl Fn(&Credential) -> Option<&T>,
217 ) -> Vec<T>
218 where
219 T: Clone,
220 {
221 credentials.iter().filter_map(f).cloned().collect()
222 }
223
224 macro_rules! extract_credential {
225 ($field:ident, $variant:path, $type:ty) => {
226 filter_credentials(&credentials, |c| match c {
227 $variant(ref inner) => Some(inner.as_ref()),
228 _ => None,
229 })
230 };
231 }
232
233 GroupedCredentials {
234 api_key: extract_credential!(api_key, Credential::ApiKey, ApiKeyCredential),
235 basic_auth: extract_credential!(basic_auth, Credential::BasicAuth, BasicAuthCredential),
236 credit_card: extract_credential!(credit_card, Credential::CreditCard, CreditCardCredential),
237 custom_fields: extract_credential!(custom_fields, Credential::CustomFields, CustomFields),
238 passkey: extract_credential!(passkey, Credential::Passkey, PasskeyCredential),
239 ssh: extract_credential!(ssh, Credential::SshKey, SshKeyCredential),
240 totp: extract_credential!(totp, Credential::Totp, TotpCredential),
241 wifi: extract_credential!(wifi, Credential::Wifi, WifiCredential),
242 address: extract_credential!(address, Credential::Address, AddressCredential),
243 passport: extract_credential!(passport, Credential::Passport, PassportCredential),
244 person_name: extract_credential!(person_name, Credential::PersonName, PersonNameCredential),
245 drivers_license: extract_credential!(
246 drivers_license,
247 Credential::DriversLicense,
248 DriversLicenseCredential
249 ),
250 identity_document: extract_credential!(
251 identity_document,
252 Credential::IdentityDocument,
253 IdentityDocumentCredential
254 ),
255 note: extract_credential!(note, Credential::Note, NoteCredential),
256 }
257}
258
259struct GroupedCredentials {
260 address: Vec<AddressCredential>,
261 api_key: Vec<ApiKeyCredential>,
262 basic_auth: Vec<BasicAuthCredential>,
263 credit_card: Vec<CreditCardCredential>,
264 custom_fields: Vec<CustomFieldsCredential>,
265 drivers_license: Vec<DriversLicenseCredential>,
266 identity_document: Vec<IdentityDocumentCredential>,
267 note: Vec<NoteCredential>,
268 passkey: Vec<PasskeyCredential>,
269 passport: Vec<PassportCredential>,
270 person_name: Vec<PersonNameCredential>,
271 ssh: Vec<SshKeyCredential>,
272 totp: Vec<TotpCredential>,
273 wifi: Vec<WifiCredential>,
274}
275
276#[cfg(test)]
277mod tests {
278 use chrono::{Duration, Month};
279 use credential_exchange_format::{B64Url, CreditCardCredential, EditableFieldYearMonth};
280
281 use super::*;
282
283 #[test]
284 fn test_convert_date() {
285 let timestamp: u64 = 1706613834;
286 let datetime = convert_date(Some(timestamp));
287 assert_eq!(
288 datetime,
289 "2024-01-30T11:23:54Z".parse::<DateTime<Utc>>().unwrap()
290 );
291 }
292
293 #[test]
294 fn test_convert_date_none() {
295 let datetime = convert_date(None);
296 assert!(datetime > Utc::now() - Duration::seconds(1));
297 assert!(datetime <= Utc::now());
298 }
299
300 #[test]
301 fn test_parse_empty_item() {
302 let item = Item {
303 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
304 creation_at: Some(1706613834),
305 modified_at: Some(1706623773),
306 title: "Bitwarden".to_string(),
307 subtitle: None,
308 favorite: None,
309 credentials: vec![],
310 tags: None,
311 extensions: None,
312 scope: None,
313 };
314
315 let ciphers: Vec<ImportingCipher> = parse_item(item);
316 assert_eq!(ciphers.len(), 0);
317 }
318
319 #[test]
320 fn test_parse_passkey() {
321 let item = Item {
322 id: B64Url::try_from("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF")
323 .unwrap(),
324 creation_at: Some(1732181986),
325 modified_at: Some(1732182026),
326 title: "example.com".to_string(),
327 subtitle: None,
328 favorite: None,
329 credentials: vec![Credential::Passkey(Box::new(PasskeyCredential {
330 credential_id: B64Url::try_from("6NiHiekW4ZY8vYHa-ucbvA")
331 .unwrap(),
332 rp_id: "example.com".to_string(),
333 username: "pj-fry".to_string(),
334 user_display_name: "Philip J. Fry".to_string(),
335 user_handle: B64Url::try_from("YWxleCBtdWxsZXI").unwrap(),
336 key: B64Url::try_from("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl")
337 .unwrap(),
338 fido2_extensions: None,
339 }))],
340 tags: None,
341 extensions: None,
342 scope: None,
343 };
344
345 let ciphers: Vec<ImportingCipher> = parse_item(item);
346 assert_eq!(ciphers.len(), 1);
347 let cipher = ciphers.first().unwrap();
348
349 assert_eq!(cipher.folder_id, None);
350 assert_eq!(cipher.name, "example.com");
351
352 let login = match &cipher.r#type {
353 CipherType::Login(login) => login,
354 _ => panic!("Expected login"),
355 };
356
357 assert_eq!(login.username, Some("pj-fry".to_string()));
358 assert_eq!(login.password, None);
359 assert_eq!(login.login_uris.len(), 1);
360 assert_eq!(
361 login.login_uris[0].uri,
362 Some("https://example.com".to_string())
363 );
364 assert_eq!(login.totp, None);
365
366 let passkey = login.fido2_credentials.as_ref().unwrap().first().unwrap();
367 assert_eq!(passkey.credential_id, "b64.6NiHiekW4ZY8vYHa-ucbvA");
368 assert_eq!(passkey.key_type, "public-key");
369 assert_eq!(passkey.key_algorithm, "ECDSA");
370 assert_eq!(passkey.key_curve, "P-256");
371 assert_eq!(
372 passkey.key_value,
373 "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl"
374 );
375 assert_eq!(passkey.rp_id, "example.com");
376 assert_eq!(
377 passkey.user_handle.as_ref().map(|h| h.to_string()).unwrap(),
378 "YWxleCBtdWxsZXI"
379 );
380 assert_eq!(passkey.user_name, Some("pj-fry".to_string()));
381 assert_eq!(passkey.counter, 0);
382 assert_eq!(passkey.rp_name, Some("example.com".to_string()));
383 assert_eq!(passkey.user_display_name, Some("Philip J. Fry".to_string()));
384 assert_eq!(passkey.discoverable, "true");
385 assert_eq!(
386 passkey.creation_date,
387 "2024-11-21T09:39:46Z".parse::<DateTime<Utc>>().unwrap()
388 );
389 }
390
391 #[test]
392 fn test_passkey_with_basic_auth_and_scope() {
393 use credential_exchange_format::{BasicAuthCredential, CredentialScope};
394
395 let item = Item {
396 id: B64Url::try_from("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF")
397 .unwrap(),
398 creation_at: Some(1732181986),
399 modified_at: Some(1732182026),
400 title: "Combined Login".to_string(),
401 subtitle: None,
402 favorite: None,
403 credentials: vec![
404 Credential::BasicAuth(Box::new(BasicAuthCredential {
405 username: Some("basic_username".to_string().into()),
406 password: Some("basic_password".to_string().into()),
407 })),
408 Credential::Passkey(Box::new(PasskeyCredential {
409 credential_id: B64Url::try_from("6NiHiekW4ZY8vYHa-ucbvA")
410 .unwrap(),
411 rp_id: "passkey-domain.com".to_string(),
412 username: "passkey_username".to_string(),
413 user_display_name: "Passkey User".to_string(),
414 user_handle: B64Url::try_from("YWxleCBtdWxsZXI")
415 .unwrap(),
416 key: B64Url::try_from("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl")
417 .unwrap(),
418 fido2_extensions: None,
419 }))
420 ],
421 tags: None,
422 extensions: None,
423 scope: Some(CredentialScope {
424 urls: vec!["https://example.com".to_string()],
425 android_apps: vec![],
426 }),
427 };
428
429 let ciphers: Vec<ImportingCipher> = parse_item(item);
430 assert_eq!(ciphers.len(), 1);
431 let cipher = ciphers.first().unwrap();
432
433 let login = match &cipher.r#type {
434 CipherType::Login(login) => login,
435 _ => panic!("Expected login"),
436 };
437
438 assert_eq!(login.username, Some("basic_username".to_string()));
440 assert_eq!(login.password, Some("basic_password".to_string()));
441
442 assert_eq!(login.login_uris.len(), 1);
444 assert_eq!(
445 login.login_uris[0].uri,
446 Some("https://example.com".to_string())
447 );
448
449 assert!(login.fido2_credentials.is_some());
451 }
452
453 #[test]
454 fn test_passkey_with_empty_username() {
455 let item = Item {
456 id: B64Url::try_from("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF").unwrap(),
457 creation_at: Some(1732181986),
458 modified_at: Some(1732182026),
459 title: "Empty Username Passkey".to_string(),
460 subtitle: None,
461 favorite: None,
462 credentials: vec![Credential::Passkey(Box::new(PasskeyCredential {
463 credential_id: B64Url::try_from("6NiHiekW4ZY8vYHa-ucbvA")
464 .unwrap(),
465 rp_id: "example.com".to_string(),
466 username: "".to_string(), user_display_name: "User Display".to_string(),
468 user_handle: B64Url::try_from("YWxleCBtdWxsZXI")
469 .unwrap(),
470 key: B64Url::try_from("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl")
471 .unwrap(),
472 fido2_extensions: None,
473 }))],
474 tags: None,
475 extensions: None,
476 scope: None,
477 };
478
479 let ciphers: Vec<ImportingCipher> = parse_item(item);
480 assert_eq!(ciphers.len(), 1);
481 let cipher = ciphers.first().unwrap();
482
483 let login = match &cipher.r#type {
484 CipherType::Login(login) => login,
485 _ => panic!("Expected login"),
486 };
487
488 assert_eq!(login.username, None);
490 assert_eq!(login.password, None);
491
492 assert_eq!(login.login_uris.len(), 1);
494 assert_eq!(
495 login.login_uris[0].uri,
496 Some("https://example.com".to_string())
497 );
498 }
499
500 #[test]
501 fn test_credit_card() {
502 let item = Item {
503 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
504 creation_at: Some(1706613834),
505 modified_at: Some(1706623773),
506 title: "My MasterCard".to_string(),
507 subtitle: None,
508 favorite: None,
509 credentials: vec![Credential::CreditCard(Box::new(CreditCardCredential {
510 number: Some("1234 5678 9012 3456".to_string().into()),
511 full_name: Some("John Doe".to_string().into()),
512 card_type: Some("MasterCard".to_string().into()),
513 verification_number: Some("123".to_string().into()),
514 pin: None,
515 expiry_date: Some(
516 EditableFieldYearMonth {
517 year: 2026,
518 month: Month::January,
519 }
520 .into(),
521 ),
522 valid_from: None,
523 }))],
524 tags: None,
525 extensions: None,
526 scope: None,
527 };
528
529 let ciphers: Vec<ImportingCipher> = parse_item(item);
530 assert_eq!(ciphers.len(), 1);
531 let cipher = ciphers.first().unwrap();
532
533 assert_eq!(cipher.folder_id, None);
534 assert_eq!(cipher.name, "My MasterCard");
535
536 let card = match &cipher.r#type {
537 CipherType::Card(card) => card,
538 _ => panic!("Expected card"),
539 };
540
541 assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
542 assert_eq!(card.exp_month, Some("1".to_string()));
543 assert_eq!(card.exp_year, Some("2026".to_string()));
544 assert_eq!(card.code, Some("123".to_string()));
545 assert_eq!(card.brand, Some("Mastercard".to_string()));
546 assert_eq!(card.number, Some("1234 5678 9012 3456".to_string()));
547 }
548
549 #[test]
550 fn test_totp() {
551 use credential_exchange_format::{OTPHashAlgorithm, TotpCredential};
552
553 let item = Item {
554 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
555 creation_at: Some(1706613834),
556 modified_at: Some(1706623773),
557 title: "My TOTP".to_string(),
558 subtitle: None,
559 favorite: None,
560 credentials: vec![Credential::Totp(Box::new(TotpCredential {
561 secret: "Hello World!".as_bytes().to_vec().into(),
562 period: 30,
563 digits: 6,
564 username: Some("[email protected]".to_string()),
565 algorithm: OTPHashAlgorithm::Sha1,
566 issuer: Some("Example Service".to_string()),
567 }))],
568 tags: None,
569 extensions: None,
570 scope: None,
571 };
572
573 let ciphers: Vec<ImportingCipher> = parse_item(item);
574 assert_eq!(ciphers.len(), 1);
575 let cipher = ciphers.first().unwrap();
576
577 assert_eq!(cipher.folder_id, None);
578 assert_eq!(cipher.name, "My TOTP");
579 assert_eq!(cipher.notes, None);
580 assert!(!cipher.favorite);
581 assert_eq!(cipher.reprompt, 0);
582 assert_eq!(cipher.fields, vec![]);
583
584 let login = match &cipher.r#type {
585 CipherType::Login(login) => login,
586 _ => panic!("Expected login cipher for TOTP"),
587 };
588
589 assert!(login.totp.is_some());
591 let otpauth = login.totp.as_ref().unwrap();
592
593 assert!(
595 otpauth.starts_with("otpauth://totp/Example%20Service:test%40example%2Ecom?secret=")
596 );
597 assert!(otpauth.contains("&issuer=Example%20Service"));
598
599 assert!(!otpauth.contains("&period=30"));
601 assert!(!otpauth.contains("&digits=6"));
602 assert!(!otpauth.contains("&algorithm=SHA1"));
603
604 assert_eq!(login.username, None);
606 assert_eq!(login.password, None);
607 assert_eq!(login.login_uris, vec![]);
608 }
609
610 #[test]
611 fn test_totp_combined_with_basic_auth() {
612 use credential_exchange_format::{BasicAuthCredential, OTPHashAlgorithm, TotpCredential};
613
614 let item = Item {
615 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
616 creation_at: Some(1706613834),
617 modified_at: Some(1706623773),
618 title: "Login with TOTP".to_string(),
619 subtitle: None,
620 favorite: None,
621 credentials: vec![
622 Credential::BasicAuth(Box::new(BasicAuthCredential {
623 username: Some("myuser".to_string().into()),
624 password: Some("mypass".to_string().into()),
625 })),
626 Credential::Totp(Box::new(TotpCredential {
627 secret: "totpkey".as_bytes().to_vec().into(),
628 period: 30,
629 digits: 6,
630 username: Some("totpuser".to_string()),
631 algorithm: OTPHashAlgorithm::Sha1,
632 issuer: Some("Service".to_string()),
633 })),
634 ],
635 tags: None,
636 extensions: None,
637 scope: None,
638 };
639
640 let ciphers: Vec<ImportingCipher> = parse_item(item);
641 assert_eq!(ciphers.len(), 1);
642 let cipher = ciphers.first().unwrap();
643
644 let login = match &cipher.r#type {
645 CipherType::Login(login) => login,
646 _ => panic!("Expected login cipher"),
647 };
648
649 assert_eq!(login.username, Some("myuser".to_string()));
651 assert_eq!(login.password, Some("mypass".to_string()));
652 assert!(login.totp.is_some());
653
654 let otpauth = login.totp.as_ref().unwrap();
655 assert!(otpauth.starts_with("otpauth://totp/Service:totpuser?secret="));
656 assert!(otpauth.contains("&issuer=Service"));
657 }
658
659 #[test]
662 fn test_note_as_part_of_login() {
663 use credential_exchange_format::{BasicAuthCredential, Credential, Item, NoteCredential};
664
665 let item = Item {
666 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
667 creation_at: Some(1706613834),
668 modified_at: Some(1706623773),
669 title: "Login with Note".to_string(),
670 subtitle: None,
671 favorite: None,
672 credentials: vec![
673 Credential::BasicAuth(Box::new(BasicAuthCredential {
674 username: Some("testuser".to_string().into()),
675 password: Some("testpass".to_string().into()),
676 })),
677 Credential::Note(Box::new(NoteCredential {
678 content: "This note should be added to the login cipher."
679 .to_string()
680 .into(),
681 })),
682 ],
683 tags: None,
684 extensions: None,
685 scope: None,
686 };
687
688 let ciphers: Vec<ImportingCipher> = parse_item(item);
689 assert_eq!(ciphers.len(), 1); let cipher = ciphers.first().unwrap();
691
692 assert_eq!(cipher.name, "Login with Note");
693 assert_eq!(
694 cipher.notes,
695 Some("This note should be added to the login cipher.".to_string())
696 );
697
698 match &cipher.r#type {
699 CipherType::Login(_) => (), _ => panic!("Expected Login cipher with note content"),
701 };
702 }
703
704 #[test]
705 fn test_note_as_part_of_api_key() {
706 use credential_exchange_format::{ApiKeyCredential, Credential, Item, NoteCredential};
707
708 let item = Item {
709 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
710 creation_at: Some(1706613834),
711 modified_at: Some(1706623773),
712 title: "API Key with Note".to_string(),
713 subtitle: None,
714 favorite: None,
715 credentials: vec![
716 Credential::ApiKey(Box::new(ApiKeyCredential {
717 key: Some("api-key-12345".to_string().into()),
718 username: Some("api-user".to_string().into()),
719 key_type: Some("Bearer".to_string().into()),
720 url: None,
721 valid_from: None,
722 expiry_date: None,
723 })),
724 Credential::Note(Box::new(NoteCredential {
725 content: "This note should be added to the API key cipher."
726 .to_string()
727 .into(),
728 })),
729 ],
730 tags: None,
731 extensions: None,
732 scope: None,
733 };
734
735 let ciphers: Vec<ImportingCipher> = parse_item(item);
736 assert_eq!(ciphers.len(), 1); let cipher = ciphers.first().unwrap();
738
739 assert_eq!(cipher.name, "API Key with Note");
740 assert_eq!(
741 cipher.notes,
742 Some("This note should be added to the API key cipher.".to_string())
743 );
744
745 match &cipher.r#type {
746 CipherType::SecureNote(_) => (), _ => panic!("Expected SecureNote cipher with note content"),
748 };
749
750 assert!(!cipher.fields.is_empty());
752 }
753
754 #[test]
755 fn test_note_as_part_of_credit_card() {
756 use chrono::Month;
757 use credential_exchange_format::{Credential, CreditCardCredential, Item, NoteCredential};
758
759 let item = Item {
760 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
761 creation_at: Some(1706613834),
762 modified_at: Some(1706623773),
763 title: "Credit Card with Note".to_string(),
764 subtitle: None,
765 favorite: None,
766 credentials: vec![
767 Credential::CreditCard(Box::new(CreditCardCredential {
768 number: Some("1234 5678 9012 3456".to_string().into()),
769 full_name: Some("John Doe".to_string().into()),
770 card_type: Some("Visa".to_string().into()),
771 verification_number: Some("123".to_string().into()),
772 pin: None,
773 expiry_date: Some(
774 credential_exchange_format::EditableFieldYearMonth {
775 year: 2026,
776 month: Month::December,
777 }
778 .into(),
779 ),
780 valid_from: None,
781 })),
782 Credential::Note(Box::new(NoteCredential {
783 content: "This note should be added to the credit card cipher."
784 .to_string()
785 .into(),
786 })),
787 ],
788 tags: None,
789 extensions: None,
790 scope: None,
791 };
792
793 let ciphers: Vec<ImportingCipher> = parse_item(item);
794 assert_eq!(ciphers.len(), 1); let cipher = ciphers.first().unwrap();
796
797 assert_eq!(cipher.name, "Credit Card with Note");
798 assert_eq!(
799 cipher.notes,
800 Some("This note should be added to the credit card cipher.".to_string())
801 );
802
803 match &cipher.r#type {
804 CipherType::Card(_) => (), _ => panic!("Expected Card cipher with note content"),
806 };
807 }
808
809 #[test]
810 fn test_note_as_part_of_wifi() {
811 use credential_exchange_format::{
812 Credential, EditableFieldWifiNetworkSecurityType, Item, NoteCredential, WifiCredential,
813 };
814
815 let item = Item {
816 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
817 creation_at: Some(1706613834),
818 modified_at: Some(1706623773),
819 title: "WiFi with Note".to_string(),
820 subtitle: None,
821 favorite: None,
822 credentials: vec![
823 Credential::Wifi(Box::new(WifiCredential {
824 ssid: Some("MyNetwork".to_string().into()),
825 passphrase: Some("password123".to_string().into()),
826 network_security_type: Some(
827 EditableFieldWifiNetworkSecurityType::Wpa3Personal.into(),
828 ),
829 hidden: Some(false.into()),
830 })),
831 Credential::Note(Box::new(NoteCredential {
832 content: "This note should be added to the WiFi cipher."
833 .to_string()
834 .into(),
835 })),
836 ],
837 tags: None,
838 extensions: None,
839 scope: None,
840 };
841
842 let ciphers: Vec<ImportingCipher> = parse_item(item);
843 assert_eq!(ciphers.len(), 1); let cipher = ciphers.first().unwrap();
845
846 assert_eq!(cipher.name, "WiFi with Note");
847 assert_eq!(
848 cipher.notes,
849 Some("This note should be added to the WiFi cipher.".to_string())
850 );
851
852 match &cipher.r#type {
853 CipherType::SecureNote(_) => (), _ => panic!("Expected SecureNote cipher with note content"),
855 };
856
857 assert!(!cipher.fields.is_empty());
859 }
860
861 #[test]
862 fn test_note_as_part_of_identity() {
863 use credential_exchange_format::{AddressCredential, Credential, Item, NoteCredential};
864
865 let item = Item {
866 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
867 creation_at: Some(1706613834),
868 modified_at: Some(1706623773),
869 title: "Address with Note".to_string(),
870 subtitle: None,
871 favorite: None,
872 credentials: vec![
873 Credential::Address(Box::new(AddressCredential {
874 street_address: Some("123 Main St".to_string().into()),
875 city: Some("Springfield".to_string().into()),
876 territory: Some("CA".to_string().into()),
877 postal_code: Some("12345".to_string().into()),
878 country: Some("US".to_string().into()),
879 tel: Some("+1-555-123-4567".to_string().into()),
880 })),
881 Credential::Note(Box::new(NoteCredential {
882 content: "This note should be added to the address identity cipher."
883 .to_string()
884 .into(),
885 })),
886 ],
887 tags: None,
888 extensions: None,
889 scope: None,
890 };
891
892 let ciphers: Vec<ImportingCipher> = parse_item(item);
893 assert_eq!(ciphers.len(), 1); let cipher = ciphers.first().unwrap();
895
896 assert_eq!(cipher.name, "Address with Note");
897 assert_eq!(
898 cipher.notes,
899 Some("This note should be added to the address identity cipher.".to_string())
900 );
901
902 match &cipher.r#type {
903 CipherType::Identity(_) => (), _ => panic!("Expected Identity cipher"),
905 };
906 }
907}