use async_trait::async_trait;
use serde::Deserialize;
use sos_core::{crypto::AccessKey, UtcDateTime};
use sos_vault::{secret::IdentityKind, Vault};
use sos_vfs as vfs;
use std::{
collections::HashSet,
io::Cursor,
path::{Path, PathBuf},
};
use time::{Date, Month};
use url::Url;
use vcard4::{property::DeliveryAddress, Uri, VcardBuilder};
use async_zip::tokio::read::seek::ZipFileReader;
use tokio::io::{AsyncBufRead, AsyncSeek, BufReader};
use super::{
GenericContactRecord, GenericCsvConvert, GenericCsvEntry,
GenericIdRecord, GenericNoteRecord, GenericPasswordRecord,
GenericPaymentRecord, UNTITLED,
};
use crate::{import::read_csv_records, Convert, Result};
#[derive(Debug)]
pub enum DashlaneRecord {
Password(DashlanePasswordRecord),
Note(DashlaneNoteRecord),
Id(DashlaneIdRecord),
Payment(DashlanePaymentRecord),
Contact(DashlaneContactRecord),
}
impl From<DashlaneRecord> for GenericCsvEntry {
fn from(value: DashlaneRecord) -> Self {
match value {
DashlaneRecord::Password(record) => {
GenericCsvEntry::Password(record.into())
}
DashlaneRecord::Note(record) => {
GenericCsvEntry::Note(record.into())
}
DashlaneRecord::Id(record) => GenericCsvEntry::Id(record.into()),
DashlaneRecord::Payment(record) => {
GenericCsvEntry::Payment(record.into())
}
DashlaneRecord::Contact(record) => {
GenericCsvEntry::Contact(Box::new(record.into()))
}
}
}
}
#[derive(Debug, Deserialize)]
pub struct DashlaneNoteRecord {
pub title: String,
pub note: String,
}
impl From<DashlaneNoteRecord> for DashlaneRecord {
fn from(value: DashlaneNoteRecord) -> Self {
Self::Note(value)
}
}
impl From<DashlaneNoteRecord> for GenericNoteRecord {
fn from(value: DashlaneNoteRecord) -> Self {
let label = if value.title.is_empty() {
UNTITLED.to_owned()
} else {
value.title
};
Self {
label,
text: value.note,
tags: None,
note: None,
}
}
}
#[derive(Debug, Deserialize)]
pub struct DashlaneIdRecord {
#[serde(rename = "type")]
pub kind: String,
pub number: String,
pub name: String,
pub issue_date: String,
pub expiration_date: String,
pub place_of_issue: String,
pub state: String,
}
impl From<DashlaneIdRecord> for DashlaneRecord {
fn from(value: DashlaneIdRecord) -> Self {
Self::Id(value)
}
}
impl From<DashlaneIdRecord> for GenericIdRecord {
fn from(value: DashlaneIdRecord) -> Self {
let label = if value.name.is_empty() {
UNTITLED.to_owned()
} else {
value.name
};
let id_kind = match &value.kind[..] {
"card" => IdentityKind::IdCard,
"passport" => IdentityKind::Passport,
"license" => IdentityKind::DriverLicense,
"social_security" => IdentityKind::SocialSecurity,
"tax_number" => IdentityKind::TaxNumber,
_ => {
panic!("unsupported type of id {}", value.kind);
}
};
let issue_place =
if !value.state.is_empty() && !value.place_of_issue.is_empty() {
format!("{}, {}", value.state, value.place_of_issue)
} else {
value.place_of_issue
};
let issue_place = if !issue_place.is_empty() {
Some(issue_place)
} else {
None
};
let issue_date = if !value.issue_date.is_empty() {
match UtcDateTime::parse_simple_date(&value.issue_date) {
Ok(date) => Some(date),
Err(_) => None,
}
} else {
None
};
let expiration_date = if !value.expiration_date.is_empty() {
match UtcDateTime::parse_simple_date(&value.expiration_date) {
Ok(date) => Some(date),
Err(_) => None,
}
} else {
None
};
Self {
label,
id_kind,
number: value.number,
issue_place,
issue_date,
expiration_date,
tags: None,
note: None,
}
}
}
#[derive(Debug, Deserialize)]
pub struct DashlanePaymentRecord {
#[serde(rename = "type")]
pub kind: String,
pub account_name: String,
pub account_holder: String,
pub account_number: String,
pub routing_number: String,
pub cc_number: String,
pub code: String,
pub expiration_month: String,
pub expiration_year: String,
pub country: String,
pub note: String,
}
impl From<DashlanePaymentRecord> for DashlaneRecord {
fn from(value: DashlanePaymentRecord) -> Self {
Self::Payment(value)
}
}
impl From<DashlanePaymentRecord> for GenericPaymentRecord {
fn from(value: DashlanePaymentRecord) -> Self {
let label = if value.account_name.is_empty() {
UNTITLED.to_owned()
} else {
value.account_name
};
let expiration = if let (Ok(month), Ok(year)) = (
value.expiration_month.parse::<u8>(),
value.expiration_year.parse::<i32>(),
) {
if let Ok(month) = Month::try_from(month) {
UtcDateTime::from_calendar_date(year, month, 1).ok()
} else {
None
}
} else {
None
};
let note = if !value.note.is_empty() {
Some(value.note)
} else {
None
};
match &value.kind[..] {
"bank" => GenericPaymentRecord::BankAccount {
label,
account_holder: value.account_holder,
account_number: value.account_number,
routing_number: value.routing_number,
country: value.country,
note,
tags: None,
},
"payment_card" => GenericPaymentRecord::Card {
label,
number: value.cc_number,
code: value.code,
expiration,
country: value.country,
note,
tags: None,
},
_ => panic!("unexpected payment type {}", value.kind),
}
}
}
#[derive(Debug, Deserialize)]
pub struct DashlanePasswordRecord {
pub title: String,
pub url: Option<Url>,
pub username: String,
pub password: String,
pub note: String,
pub category: String,
#[serde(rename = "otpSecret")]
pub otp_secret: String,
}
impl From<DashlanePasswordRecord> for DashlaneRecord {
fn from(value: DashlanePasswordRecord) -> Self {
Self::Password(value)
}
}
impl From<DashlanePasswordRecord> for GenericPasswordRecord {
fn from(value: DashlanePasswordRecord) -> Self {
let label = if value.title.is_empty() {
UNTITLED.to_owned()
} else {
value.title
};
let tags = if !value.category.is_empty() {
let mut tags = HashSet::new();
tags.insert(value.category);
Some(tags)
} else {
None
};
let note = if !value.note.is_empty() {
Some(value.note)
} else {
None
};
let url = if let Some(url) = value.url {
vec![url]
} else {
vec![]
};
Self {
label,
url,
username: value.username,
password: value.password,
otp_auth: None,
tags,
note,
}
}
}
#[derive(Debug, Deserialize)]
pub struct DashlaneContactRecord {
pub item_name: String,
pub title: String,
pub first_name: String,
pub middle_name: String,
pub last_name: String,
pub address: String,
pub city: String,
pub state: String,
pub country: String,
pub zip: String,
pub address_recipient: String,
pub address_apartment: String,
pub address_floor: String,
pub address_building: String,
pub phone_number: String,
pub email: String,
pub url: String,
pub date_of_birth: String,
pub job_title: String,
}
impl From<DashlaneContactRecord> for DashlaneRecord {
fn from(value: DashlaneContactRecord) -> Self {
Self::Contact(value)
}
}
impl From<DashlaneContactRecord> for GenericContactRecord {
fn from(value: DashlaneContactRecord) -> Self {
let has_some_name_parts = !value.last_name.is_empty()
|| !value.first_name.is_empty()
|| !value.middle_name.is_empty()
|| !value.title.is_empty();
let name: [String; 5] = [
value.last_name.clone(),
value.first_name.clone(),
value.middle_name.clone(),
value.title.clone(),
String::new(),
];
let formatted_name = if has_some_name_parts {
let mut parts: Vec<String> = Vec::new();
if !value.title.is_empty() {
parts.push(value.title);
}
if !value.first_name.is_empty() {
parts.push(value.first_name);
}
if !value.middle_name.is_empty() {
parts.push(value.middle_name);
}
if !value.last_name.is_empty() {
parts.push(value.last_name);
}
parts.join(" ")
} else if !value.item_name.is_empty() {
value.item_name.clone()
} else {
UNTITLED.to_owned()
};
let label = if value.item_name.is_empty() {
formatted_name.clone()
} else if !value.item_name.is_empty() {
value.item_name
} else {
UNTITLED.to_owned()
};
let date_of_birth: Option<Date> = if !value.date_of_birth.is_empty() {
if let Ok(date_time) =
UtcDateTime::parse_simple_date(&value.date_of_birth)
{
Some(date_time.into_date())
} else {
None
}
} else {
None
};
let url: Option<Uri> = if !value.url.is_empty() {
value.url.parse().ok()
} else {
None
};
let extended_address = vec![
value.address_recipient,
value.address_apartment,
value.address_floor,
value.address_building,
];
let has_some_address_parts = !value.address.is_empty()
|| !value.city.is_empty()
|| !value.state.is_empty()
|| !value.zip.is_empty()
|| !value.country.is_empty();
let address = if has_some_address_parts {
Some(DeliveryAddress {
po_box: None,
extended_address: if !extended_address.is_empty() {
Some(extended_address.join(","))
} else {
None
},
street_address: if !value.address.is_empty() {
Some(value.address)
} else {
None
},
locality: if !value.city.is_empty() {
Some(value.city)
} else {
None
},
region: if !value.state.is_empty() {
Some(value.state)
} else {
None
},
country_name: if !value.country.is_empty() {
Some(value.country)
} else {
None
},
postal_code: if !value.zip.is_empty() {
Some(value.zip)
} else {
None
},
})
} else {
None
};
let mut builder = VcardBuilder::new(formatted_name);
if has_some_name_parts {
builder = builder.name(name);
}
if let Some(address) = address {
builder = builder.address(address);
}
if !value.phone_number.is_empty() {
builder = builder.telephone(value.phone_number);
}
if !value.email.is_empty() {
builder = builder.email(value.email);
}
if let Some(url) = url {
builder = builder.url(url);
}
if !value.job_title.is_empty() {
builder = builder.title(value.job_title);
}
if let Some(date) = date_of_birth {
builder = builder.birthday(date.into());
}
let vcard = builder.finish();
Self {
label,
vcard,
tags: None,
note: None,
}
}
}
pub async fn parse_path<P: AsRef<Path>>(
path: P,
) -> Result<Vec<DashlaneRecord>> {
parse(BufReader::new(vfs::File::open(path.as_ref()).await?)).await
}
async fn read_entry<R: AsyncBufRead + AsyncSeek + Unpin>(
zip: &mut ZipFileReader<R>,
index: usize,
) -> Result<Vec<u8>> {
let mut reader = zip.reader_with_entry(index).await?;
let mut buffer = Vec::new();
reader.read_to_end_checked(&mut buffer).await?;
Ok(buffer)
}
async fn parse<R: AsyncBufRead + AsyncSeek + Unpin>(
rdr: R,
) -> Result<Vec<DashlaneRecord>> {
let mut records = Vec::new();
let mut zip = ZipFileReader::with_tokio(rdr).await?;
for index in 0..zip.file().entries().len() {
let entry = zip.file().entries().get(index).unwrap();
let file_name = entry.filename();
let file_name = file_name.as_str()?;
match file_name {
"securenotes.csv" => {
let mut buffer = read_entry(&mut zip, index).await?;
let reader = Cursor::new(&mut buffer);
let mut items: Vec<DashlaneRecord> =
read_csv_records::<DashlaneNoteRecord, _>(reader)
.await?
.into_iter()
.map(|r| r.into())
.collect();
records.append(&mut items);
}
"credentials.csv" => {
let mut buffer = read_entry(&mut zip, index).await?;
let reader = Cursor::new(&mut buffer);
let mut items: Vec<DashlaneRecord> =
read_csv_records::<DashlanePasswordRecord, _>(reader)
.await?
.into_iter()
.map(|r| r.into())
.collect();
records.append(&mut items);
}
"ids.csv" => {
let mut buffer = read_entry(&mut zip, index).await?;
let reader = Cursor::new(&mut buffer);
let mut items: Vec<DashlaneRecord> =
read_csv_records::<DashlaneIdRecord, _>(reader)
.await?
.into_iter()
.map(|r| r.into())
.collect();
records.append(&mut items);
}
"payments.csv" => {
let mut buffer = read_entry(&mut zip, index).await?;
let reader = Cursor::new(&mut buffer);
let mut items: Vec<DashlaneRecord> =
read_csv_records::<DashlanePaymentRecord, _>(reader)
.await?
.into_iter()
.map(|r| r.into())
.collect();
records.append(&mut items);
}
"personalInfo.csv" => {
let mut buffer = read_entry(&mut zip, index).await?;
let reader = Cursor::new(&mut buffer);
let mut items: Vec<DashlaneRecord> =
read_csv_records::<DashlaneContactRecord, _>(reader)
.await?
.into_iter()
.map(|r| r.into())
.collect();
records.append(&mut items);
}
_ => {
eprintln!(
"unsupported dashlane file encountered {}",
file_name
);
}
}
}
Ok(records)
}
pub struct DashlaneCsvZip;
#[async_trait]
impl Convert for DashlaneCsvZip {
type Input = PathBuf;
async fn convert(
&self,
source: Self::Input,
vault: Vault,
key: &AccessKey,
) -> crate::Result<Vault> {
let records: Vec<GenericCsvEntry> = parse_path(source)
.await?
.into_iter()
.map(|r| r.into())
.collect();
GenericCsvConvert.convert(records, vault, key).await
}
}