pub mod bitwarden;
pub mod chrome;
pub mod dashlane;
pub mod firefox;
pub mod macos;
pub mod one_password;
use crate::Convert;
use async_trait::async_trait;
use sos_backend::AccessPoint;
use sos_core::{crypto::AccessKey, UtcDateTime};
use sos_search::SearchIndex;
use sos_vault::{
secret::{
IdentityKind, Secret, SecretId, SecretMeta, SecretRow, UserData,
},
SecretAccess, Vault,
};
use std::collections::{HashMap, HashSet};
use url::Url;
use vcard4::Vcard;
pub const UNTITLED: &str = "Untitled";
pub enum GenericCsvEntry {
Password(GenericPasswordRecord),
Note(GenericNoteRecord),
Id(GenericIdRecord),
Payment(GenericPaymentRecord),
Contact(Box<GenericContactRecord>),
}
impl GenericCsvEntry {
fn label(&self) -> &str {
match self {
Self::Password(record) => &record.label,
Self::Note(record) => &record.label,
Self::Id(record) => &record.label,
Self::Payment(record) => record.label(),
Self::Contact(record) => &record.label,
}
}
fn tags(&mut self) -> &mut Option<HashSet<String>> {
match self {
Self::Password(record) => &mut record.tags,
Self::Note(record) => &mut record.tags,
Self::Id(record) => &mut record.tags,
Self::Payment(record) => record.tags(),
Self::Contact(record) => &mut record.tags,
}
}
fn note(&mut self) -> &mut Option<String> {
match self {
Self::Password(record) => &mut record.note,
Self::Note(record) => &mut record.note,
Self::Id(record) => &mut record.note,
Self::Payment(record) => record.note(),
Self::Contact(record) => &mut record.note,
}
}
}
impl From<GenericCsvEntry> for Secret {
fn from(value: GenericCsvEntry) -> Self {
match value {
GenericCsvEntry::Password(record) => Secret::Account {
account: record.username,
password: record.password.into(),
url: record.url,
user_data: if let Some(notes) = record.note {
UserData::new_comment(notes)
} else {
Default::default()
},
},
GenericCsvEntry::Note(record) => Secret::Note {
text: record.text.into(),
user_data: if let Some(notes) = record.note {
UserData::new_comment(notes)
} else {
Default::default()
},
},
GenericCsvEntry::Id(record) => Secret::Identity {
id_kind: record.id_kind,
number: record.number.into(),
issue_place: record.issue_place,
issue_date: record.issue_date,
expiry_date: record.expiration_date,
user_data: if let Some(notes) = record.note {
UserData::new_comment(notes)
} else {
Default::default()
},
},
GenericCsvEntry::Payment(record) => match record {
GenericPaymentRecord::Card {
number,
code,
expiration,
note,
..
} => {
Secret::Card {
number: number.into(),
cvv: code.into(),
expiry: expiration,
name: None,
atm_pin: None,
user_data: if let Some(notes) = note {
UserData::new_comment(notes)
} else {
Default::default()
},
}
}
GenericPaymentRecord::BankAccount {
account_number,
routing_number,
note,
..
} => {
Secret::Bank {
number: account_number.into(),
routing: routing_number.into(),
bic: None,
iban: None,
swift: None,
user_data: if let Some(notes) = note {
UserData::new_comment(notes)
} else {
Default::default()
},
}
}
},
GenericCsvEntry::Contact(record) => Secret::Contact {
vcard: Box::new(record.vcard),
user_data: if let Some(notes) = record.note {
UserData::new_comment(notes)
} else {
Default::default()
},
},
}
}
}
pub struct GenericPasswordRecord {
pub label: String,
pub url: Vec<Url>,
pub username: String,
pub password: String,
pub otp_auth: Option<String>,
pub tags: Option<HashSet<String>>,
pub note: Option<String>,
}
pub struct GenericNoteRecord {
pub label: String,
pub text: String,
pub tags: Option<HashSet<String>>,
pub note: Option<String>,
}
pub struct GenericContactRecord {
pub label: String,
pub vcard: Vcard,
pub tags: Option<HashSet<String>>,
pub note: Option<String>,
}
pub struct GenericIdRecord {
pub label: String,
pub id_kind: IdentityKind,
pub number: String,
pub issue_place: Option<String>,
pub issue_date: Option<UtcDateTime>,
pub expiration_date: Option<UtcDateTime>,
pub tags: Option<HashSet<String>>,
pub note: Option<String>,
}
pub enum GenericPaymentRecord {
Card {
label: String,
number: String,
code: String,
expiration: Option<UtcDateTime>,
country: String,
note: Option<String>,
tags: Option<HashSet<String>>,
},
BankAccount {
label: String,
account_holder: String,
account_number: String,
routing_number: String,
country: String,
note: Option<String>,
tags: Option<HashSet<String>>,
},
}
impl GenericPaymentRecord {
fn label(&self) -> &str {
match self {
Self::Card { label, .. } => label,
Self::BankAccount { label, .. } => label,
}
}
fn tags(&mut self) -> &mut Option<HashSet<String>> {
match self {
Self::Card { tags, .. } => tags,
Self::BankAccount { tags, .. } => tags,
}
}
fn note(&mut self) -> &mut Option<String> {
match self {
Self::Card { note, .. } => note,
Self::BankAccount { note, .. } => note,
}
}
}
pub struct GenericCsvConvert;
#[async_trait]
impl Convert for GenericCsvConvert {
type Input = Vec<GenericCsvEntry>;
async fn convert(
&self,
source: Self::Input,
vault: Vault,
key: &AccessKey,
) -> crate::Result<Vault> {
let mut index = SearchIndex::new();
let mut keeper = AccessPoint::from_vault(vault);
keeper.unlock(key).await?;
let mut duplicates: HashMap<String, usize> = HashMap::new();
for mut entry in source {
let mut label = entry.label().to_owned();
let rename_label = {
if index
.find_by_label(keeper.vault().id(), &label, None)
.is_some()
{
duplicates
.entry(label.clone())
.and_modify(|counter| *counter += 1)
.or_insert(1);
let counter = duplicates.get(&label).unwrap();
Some(format!("{} {}", label, counter))
} else {
None
}
};
if let Some(renamed) = rename_label {
label = renamed;
}
let tags = entry.tags().take();
let note = entry.note().take();
let mut secret: Secret = entry.into();
secret.user_data_mut().set_comment(note);
let mut meta = SecretMeta::new(label, secret.kind());
if let Some(tags) = tags {
meta.set_tags(tags);
}
let id = SecretId::new_v4();
let index_doc = index.prepare(keeper.id(), &id, &meta, &secret);
let secret_data = SecretRow::new(id, meta, secret);
keeper.create_secret(&secret_data).await?;
index.commit(index_doc);
}
keeper.lock();
Ok(keeper.into())
}
}