1use async_trait::async_trait;
4use serde::Deserialize;
5use sos_core::{crypto::AccessKey, UtcDateTime};
6use sos_vault::{secret::IdentityKind, Vault};
7use sos_vfs as vfs;
8use std::{
9 collections::HashSet,
10 io::Cursor,
11 path::{Path, PathBuf},
12};
13use time::{Date, Month};
14use url::Url;
15use vcard4::{property::DeliveryAddress, Uri, VcardBuilder};
16
17use async_zip::tokio::read::seek::ZipFileReader;
18use tokio::io::{AsyncBufRead, AsyncSeek, BufReader};
19
20use super::{
21 GenericContactRecord, GenericCsvConvert, GenericCsvEntry,
22 GenericIdRecord, GenericNoteRecord, GenericPasswordRecord,
23 GenericPaymentRecord, UNTITLED,
24};
25use crate::{import::read_csv_records, Convert, Result};
26
27#[derive(Debug)]
29pub enum DashlaneRecord {
30 Password(DashlanePasswordRecord),
32 Note(DashlaneNoteRecord),
34 Id(DashlaneIdRecord),
36 Payment(DashlanePaymentRecord),
38 Contact(DashlaneContactRecord),
40}
41
42impl From<DashlaneRecord> for GenericCsvEntry {
43 fn from(value: DashlaneRecord) -> Self {
44 match value {
45 DashlaneRecord::Password(record) => {
46 GenericCsvEntry::Password(record.into())
47 }
48 DashlaneRecord::Note(record) => {
49 GenericCsvEntry::Note(record.into())
50 }
51 DashlaneRecord::Id(record) => GenericCsvEntry::Id(record.into()),
52 DashlaneRecord::Payment(record) => {
53 GenericCsvEntry::Payment(record.into())
54 }
55 DashlaneRecord::Contact(record) => {
56 GenericCsvEntry::Contact(Box::new(record.into()))
57 }
58 }
59 }
60}
61
62#[derive(Debug, Deserialize)]
64pub struct DashlaneNoteRecord {
65 pub title: String,
67 pub note: String,
69}
70
71impl From<DashlaneNoteRecord> for DashlaneRecord {
72 fn from(value: DashlaneNoteRecord) -> Self {
73 Self::Note(value)
74 }
75}
76
77impl From<DashlaneNoteRecord> for GenericNoteRecord {
78 fn from(value: DashlaneNoteRecord) -> Self {
79 let label = if value.title.is_empty() {
80 UNTITLED.to_owned()
81 } else {
82 value.title
83 };
84 Self {
85 label,
86 text: value.note,
87 tags: None,
88 note: None,
89 }
90 }
91}
92
93#[derive(Debug, Deserialize)]
95pub struct DashlaneIdRecord {
96 #[serde(rename = "type")]
98 pub kind: String,
99 pub number: String,
101 pub name: String,
103 pub issue_date: String,
105 pub expiration_date: String,
107 pub place_of_issue: String,
109 pub state: String,
111}
112
113impl From<DashlaneIdRecord> for DashlaneRecord {
114 fn from(value: DashlaneIdRecord) -> Self {
115 Self::Id(value)
116 }
117}
118
119impl From<DashlaneIdRecord> for GenericIdRecord {
120 fn from(value: DashlaneIdRecord) -> Self {
121 let label = if value.name.is_empty() {
122 UNTITLED.to_owned()
123 } else {
124 value.name
125 };
126
127 let id_kind = match &value.kind[..] {
128 "card" => IdentityKind::IdCard,
129 "passport" => IdentityKind::Passport,
130 "license" => IdentityKind::DriverLicense,
131 "social_security" => IdentityKind::SocialSecurity,
132 "tax_number" => IdentityKind::TaxNumber,
133 _ => {
134 panic!("unsupported type of id {}", value.kind);
135 }
136 };
137
138 let issue_place =
139 if !value.state.is_empty() && !value.place_of_issue.is_empty() {
140 format!("{}, {}", value.state, value.place_of_issue)
141 } else {
142 value.place_of_issue
143 };
144
145 let issue_place = if !issue_place.is_empty() {
146 Some(issue_place)
147 } else {
148 None
149 };
150
151 let issue_date = if !value.issue_date.is_empty() {
152 match UtcDateTime::parse_simple_date(&value.issue_date) {
153 Ok(date) => Some(date),
154 Err(_) => None,
155 }
156 } else {
157 None
158 };
159
160 let expiration_date = if !value.expiration_date.is_empty() {
161 match UtcDateTime::parse_simple_date(&value.expiration_date) {
162 Ok(date) => Some(date),
163 Err(_) => None,
164 }
165 } else {
166 None
167 };
168
169 Self {
170 label,
171 id_kind,
172 number: value.number,
173 issue_place,
174 issue_date,
175 expiration_date,
176 tags: None,
177 note: None,
178 }
179 }
180}
181
182#[derive(Debug, Deserialize)]
184pub struct DashlanePaymentRecord {
185 #[serde(rename = "type")]
187 pub kind: String,
188 pub account_name: String,
190 pub account_holder: String,
192 pub account_number: String,
194 pub routing_number: String,
196 pub cc_number: String,
198 pub code: String,
200 pub expiration_month: String,
202 pub expiration_year: String,
204 pub country: String,
206 pub note: String,
208}
209
210impl From<DashlanePaymentRecord> for DashlaneRecord {
211 fn from(value: DashlanePaymentRecord) -> Self {
212 Self::Payment(value)
213 }
214}
215
216impl From<DashlanePaymentRecord> for GenericPaymentRecord {
217 fn from(value: DashlanePaymentRecord) -> Self {
218 let label = if value.account_name.is_empty() {
219 UNTITLED.to_owned()
220 } else {
221 value.account_name
222 };
223
224 let expiration = if let (Ok(month), Ok(year)) = (
225 value.expiration_month.parse::<u8>(),
226 value.expiration_year.parse::<i32>(),
227 ) {
228 if let Ok(month) = Month::try_from(month) {
229 UtcDateTime::from_calendar_date(year, month, 1).ok()
230 } else {
231 None
232 }
233 } else {
234 None
235 };
236
237 let note = if !value.note.is_empty() {
238 Some(value.note)
239 } else {
240 None
241 };
242
243 match &value.kind[..] {
244 "bank" => GenericPaymentRecord::BankAccount {
245 label,
246 account_holder: value.account_holder,
247 account_number: value.account_number,
248 routing_number: value.routing_number,
249 country: value.country,
250 note,
251 tags: None,
252 },
253 "payment_card" => GenericPaymentRecord::Card {
254 label,
255 number: value.cc_number,
256 code: value.code,
257 expiration,
258 country: value.country,
259 note,
260 tags: None,
261 },
262 _ => panic!("unexpected payment type {}", value.kind),
263 }
264 }
265}
266
267#[derive(Debug, Deserialize)]
269pub struct DashlanePasswordRecord {
270 pub title: String,
272 pub url: Option<Url>,
274 pub username: String,
276 pub password: String,
278 pub note: String,
280 pub category: String,
282 #[serde(rename = "otpSecret")]
284 pub otp_secret: String,
285}
286
287impl From<DashlanePasswordRecord> for DashlaneRecord {
288 fn from(value: DashlanePasswordRecord) -> Self {
289 Self::Password(value)
290 }
291}
292
293impl From<DashlanePasswordRecord> for GenericPasswordRecord {
294 fn from(value: DashlanePasswordRecord) -> Self {
295 let label = if value.title.is_empty() {
296 UNTITLED.to_owned()
297 } else {
298 value.title
299 };
300
301 let tags = if !value.category.is_empty() {
302 let mut tags = HashSet::new();
303 tags.insert(value.category);
304 Some(tags)
305 } else {
306 None
307 };
308
309 let note = if !value.note.is_empty() {
310 Some(value.note)
311 } else {
312 None
313 };
314
315 let url = if let Some(url) = value.url {
316 vec![url]
317 } else {
318 vec![]
319 };
320
321 Self {
322 label,
323 url,
324 username: value.username,
325 password: value.password,
326 otp_auth: None,
327 tags,
328 note,
329 }
330 }
331}
332
333#[derive(Debug, Deserialize)]
343pub struct DashlaneContactRecord {
344 pub item_name: String,
346 pub title: String,
348 pub first_name: String,
350 pub middle_name: String,
352 pub last_name: String,
354
355 pub address: String,
357 pub city: String,
359 pub state: String,
361 pub country: String,
363 pub zip: String,
365
366 pub address_recipient: String,
368 pub address_apartment: String,
370 pub address_floor: String,
372 pub address_building: String,
374
375 pub phone_number: String,
377 pub email: String,
379 pub url: String,
381
382 pub date_of_birth: String,
384
385 pub job_title: String,
387}
388
389impl From<DashlaneContactRecord> for DashlaneRecord {
390 fn from(value: DashlaneContactRecord) -> Self {
391 Self::Contact(value)
392 }
393}
394
395impl From<DashlaneContactRecord> for GenericContactRecord {
396 fn from(value: DashlaneContactRecord) -> Self {
397 let has_some_name_parts = !value.last_name.is_empty()
398 || !value.first_name.is_empty()
399 || !value.middle_name.is_empty()
400 || !value.title.is_empty();
401
402 let name: [String; 5] = [
403 value.last_name.clone(),
404 value.first_name.clone(),
405 value.middle_name.clone(),
406 value.title.clone(),
407 String::new(),
408 ];
409
410 let formatted_name = if has_some_name_parts {
411 let mut parts: Vec<String> = Vec::new();
412 if !value.title.is_empty() {
413 parts.push(value.title);
414 }
415 if !value.first_name.is_empty() {
416 parts.push(value.first_name);
417 }
418 if !value.middle_name.is_empty() {
419 parts.push(value.middle_name);
420 }
421 if !value.last_name.is_empty() {
422 parts.push(value.last_name);
423 }
424 parts.join(" ")
425 } else if !value.item_name.is_empty() {
426 value.item_name.clone()
427 } else {
428 UNTITLED.to_owned()
429 };
430
431 let label = if value.item_name.is_empty() {
432 formatted_name.clone()
433 } else if !value.item_name.is_empty() {
434 value.item_name
435 } else {
436 UNTITLED.to_owned()
437 };
438
439 let date_of_birth: Option<Date> = if !value.date_of_birth.is_empty() {
440 if let Ok(date_time) =
441 UtcDateTime::parse_simple_date(&value.date_of_birth)
442 {
443 Some(date_time.into_date())
444 } else {
445 None
446 }
447 } else {
448 None
449 };
450
451 let url: Option<Uri> = if !value.url.is_empty() {
452 value.url.parse().ok()
453 } else {
454 None
455 };
456
457 let extended_address = vec![
458 value.address_recipient,
459 value.address_apartment,
460 value.address_floor,
461 value.address_building,
462 ];
463
464 let has_some_address_parts = !value.address.is_empty()
465 || !value.city.is_empty()
466 || !value.state.is_empty()
467 || !value.zip.is_empty()
468 || !value.country.is_empty();
469
470 let address = if has_some_address_parts {
471 Some(DeliveryAddress {
472 po_box: None,
473 extended_address: if !extended_address.is_empty() {
474 Some(extended_address.join(","))
475 } else {
476 None
477 },
478 street_address: if !value.address.is_empty() {
479 Some(value.address)
480 } else {
481 None
482 },
483 locality: if !value.city.is_empty() {
484 Some(value.city)
485 } else {
486 None
487 },
488 region: if !value.state.is_empty() {
489 Some(value.state)
490 } else {
491 None
492 },
493 country_name: if !value.country.is_empty() {
494 Some(value.country)
495 } else {
496 None
497 },
498 postal_code: if !value.zip.is_empty() {
499 Some(value.zip)
500 } else {
501 None
502 },
503 })
504 } else {
505 None
506 };
507
508 let mut builder = VcardBuilder::new(formatted_name);
509 if has_some_name_parts {
510 builder = builder.name(name);
511 }
512 if let Some(address) = address {
513 builder = builder.address(address);
514 }
515 if !value.phone_number.is_empty() {
516 builder = builder.telephone(value.phone_number);
517 }
518 if !value.email.is_empty() {
519 builder = builder.email(value.email);
520 }
521 if let Some(url) = url {
522 builder = builder.url(url);
523 }
524 if !value.job_title.is_empty() {
525 builder = builder.title(value.job_title);
526 }
527 if let Some(date) = date_of_birth {
528 builder = builder.birthday(date.into());
529 }
530 let vcard = builder.finish();
531 Self {
532 label,
533 vcard,
534 tags: None,
535 note: None,
536 }
537 }
538}
539
540pub async fn parse_path<P: AsRef<Path>>(
542 path: P,
543) -> Result<Vec<DashlaneRecord>> {
544 parse(BufReader::new(vfs::File::open(path.as_ref()).await?)).await
545}
546
547async fn read_entry<R: AsyncBufRead + AsyncSeek + Unpin>(
548 zip: &mut ZipFileReader<R>,
549 index: usize,
550) -> Result<Vec<u8>> {
551 let mut reader = zip.reader_with_entry(index).await?;
552 let mut buffer = Vec::new();
553 reader.read_to_end_checked(&mut buffer).await?;
554 Ok(buffer)
555}
556
557async fn parse<R: AsyncBufRead + AsyncSeek + Unpin>(
558 rdr: R,
559) -> Result<Vec<DashlaneRecord>> {
560 let mut records = Vec::new();
561 let mut zip = ZipFileReader::with_tokio(rdr).await?;
562
563 for index in 0..zip.file().entries().len() {
564 let entry = zip.file().entries().get(index).unwrap();
565 let file_name = entry.filename();
566 let file_name = file_name.as_str()?;
567
568 match file_name {
569 "securenotes.csv" => {
570 let mut buffer = read_entry(&mut zip, index).await?;
571 let reader = Cursor::new(&mut buffer);
572 let mut items: Vec<DashlaneRecord> =
573 read_csv_records::<DashlaneNoteRecord, _>(reader)
574 .await?
575 .into_iter()
576 .map(|r| r.into())
577 .collect();
578 records.append(&mut items);
579 }
580 "credentials.csv" => {
581 let mut buffer = read_entry(&mut zip, index).await?;
582 let reader = Cursor::new(&mut buffer);
583 let mut items: Vec<DashlaneRecord> =
584 read_csv_records::<DashlanePasswordRecord, _>(reader)
585 .await?
586 .into_iter()
587 .map(|r| r.into())
588 .collect();
589 records.append(&mut items);
590 }
591 "ids.csv" => {
592 let mut buffer = read_entry(&mut zip, index).await?;
593 let reader = Cursor::new(&mut buffer);
594 let mut items: Vec<DashlaneRecord> =
595 read_csv_records::<DashlaneIdRecord, _>(reader)
596 .await?
597 .into_iter()
598 .map(|r| r.into())
599 .collect();
600 records.append(&mut items);
601 }
602 "payments.csv" => {
603 let mut buffer = read_entry(&mut zip, index).await?;
604 let reader = Cursor::new(&mut buffer);
605 let mut items: Vec<DashlaneRecord> =
606 read_csv_records::<DashlanePaymentRecord, _>(reader)
607 .await?
608 .into_iter()
609 .map(|r| r.into())
610 .collect();
611 records.append(&mut items);
612 }
613 "personalInfo.csv" => {
614 let mut buffer = read_entry(&mut zip, index).await?;
615 let reader = Cursor::new(&mut buffer);
616 let mut items: Vec<DashlaneRecord> =
617 read_csv_records::<DashlaneContactRecord, _>(reader)
618 .await?
619 .into_iter()
620 .map(|r| r.into())
621 .collect();
622 records.append(&mut items);
623 }
624 _ => {
625 eprintln!(
626 "unsupported dashlane file encountered {}",
627 file_name
628 );
629 }
630 }
631 }
632 Ok(records)
633}
634
635pub struct DashlaneCsvZip;
637
638#[async_trait]
639impl Convert for DashlaneCsvZip {
640 type Input = PathBuf;
641
642 async fn convert(
643 &self,
644 source: Self::Input,
645 vault: Vault,
646 key: &AccessKey,
647 ) -> crate::Result<Vault> {
648 let records: Vec<GenericCsvEntry> = parse_path(source)
649 .await?
650 .into_iter()
651 .map(|r| r.into())
652 .collect();
653 GenericCsvConvert.convert(records, vault, key).await
654 }
655}