sos_migrate/import/csv/
mod.rs

1//! Conversion types for various CSV formats.
2
3pub mod bitwarden;
4pub mod chrome;
5pub mod dashlane;
6pub mod firefox;
7pub mod macos;
8pub mod one_password;
9
10use crate::Convert;
11use async_trait::async_trait;
12use sos_backend::AccessPoint;
13use sos_core::{crypto::AccessKey, UtcDateTime};
14use sos_search::SearchIndex;
15use sos_vault::{
16    secret::{
17        IdentityKind, Secret, SecretId, SecretMeta, SecretRow, UserData,
18    },
19    SecretAccess, Vault,
20};
21use std::collections::{HashMap, HashSet};
22use url::Url;
23use vcard4::Vcard;
24
25/// Default label for CSV records when a title is not available.
26pub const UNTITLED: &str = "Untitled";
27
28/// Generic CSV entry type.
29pub enum GenericCsvEntry {
30    /// Password Eentry.
31    Password(GenericPasswordRecord),
32    /// Note entry.
33    Note(GenericNoteRecord),
34    /// Identity entry.
35    Id(GenericIdRecord),
36    /// Payment entry.
37    Payment(GenericPaymentRecord),
38    /// Contact entry.
39    Contact(Box<GenericContactRecord>),
40}
41
42impl GenericCsvEntry {
43    /// Get the label for the record.
44    fn label(&self) -> &str {
45        match self {
46            Self::Password(record) => &record.label,
47            Self::Note(record) => &record.label,
48            Self::Id(record) => &record.label,
49            Self::Payment(record) => record.label(),
50            Self::Contact(record) => &record.label,
51        }
52    }
53
54    /// Get the tags for the record.
55    fn tags(&mut self) -> &mut Option<HashSet<String>> {
56        match self {
57            Self::Password(record) => &mut record.tags,
58            Self::Note(record) => &mut record.tags,
59            Self::Id(record) => &mut record.tags,
60            Self::Payment(record) => record.tags(),
61            Self::Contact(record) => &mut record.tags,
62        }
63    }
64
65    /// Get the note for the record.
66    fn note(&mut self) -> &mut Option<String> {
67        match self {
68            Self::Password(record) => &mut record.note,
69            Self::Note(record) => &mut record.note,
70            Self::Id(record) => &mut record.note,
71            Self::Payment(record) => record.note(),
72            Self::Contact(record) => &mut record.note,
73        }
74    }
75}
76
77impl From<GenericCsvEntry> for Secret {
78    fn from(value: GenericCsvEntry) -> Self {
79        match value {
80            GenericCsvEntry::Password(record) => Secret::Account {
81                account: record.username,
82                password: record.password.into(),
83                url: record.url,
84                user_data: if let Some(notes) = record.note {
85                    UserData::new_comment(notes)
86                } else {
87                    Default::default()
88                },
89            },
90            GenericCsvEntry::Note(record) => Secret::Note {
91                text: record.text.into(),
92                user_data: if let Some(notes) = record.note {
93                    UserData::new_comment(notes)
94                } else {
95                    Default::default()
96                },
97            },
98            GenericCsvEntry::Id(record) => Secret::Identity {
99                id_kind: record.id_kind,
100                number: record.number.into(),
101                issue_place: record.issue_place,
102                issue_date: record.issue_date,
103                expiry_date: record.expiration_date,
104                user_data: if let Some(notes) = record.note {
105                    UserData::new_comment(notes)
106                } else {
107                    Default::default()
108                },
109            },
110            GenericCsvEntry::Payment(record) => match record {
111                GenericPaymentRecord::Card {
112                    number,
113                    code,
114                    expiration,
115                    note,
116                    ..
117                } => {
118                    // TODO: handle country?
119                    Secret::Card {
120                        number: number.into(),
121                        cvv: code.into(),
122                        expiry: expiration,
123                        name: None,
124                        atm_pin: None,
125                        user_data: if let Some(notes) = note {
126                            UserData::new_comment(notes)
127                        } else {
128                            Default::default()
129                        },
130                    }
131                }
132                GenericPaymentRecord::BankAccount {
133                    account_number,
134                    routing_number,
135                    note,
136                    ..
137                } => {
138                    // TODO: handle country and account_holder
139                    Secret::Bank {
140                        number: account_number.into(),
141                        routing: routing_number.into(),
142                        bic: None,
143                        iban: None,
144                        swift: None,
145                        user_data: if let Some(notes) = note {
146                            UserData::new_comment(notes)
147                        } else {
148                            Default::default()
149                        },
150                    }
151                }
152            },
153            GenericCsvEntry::Contact(record) => Secret::Contact {
154                vcard: Box::new(record.vcard),
155                user_data: if let Some(notes) = record.note {
156                    UserData::new_comment(notes)
157                } else {
158                    Default::default()
159                },
160            },
161        }
162    }
163}
164
165/// Generic password record.
166pub struct GenericPasswordRecord {
167    /// The label of the entry.
168    pub label: String,
169    /// The URLs of the entry.
170    pub url: Vec<Url>,
171    /// The username for the entry.
172    pub username: String,
173    /// The password for the entry.
174    pub password: String,
175    /// OTP auth information for the entry.
176    pub otp_auth: Option<String>,
177    /// Collection of tags.
178    pub tags: Option<HashSet<String>>,
179    /// Optional note.
180    pub note: Option<String>,
181}
182
183/// Generic note record.
184pub struct GenericNoteRecord {
185    /// The label of the entry.
186    pub label: String,
187    /// The text for the note entry.
188    pub text: String,
189    /// Collection of tags.
190    pub tags: Option<HashSet<String>>,
191    /// Optional note.
192    pub note: Option<String>,
193}
194
195/// Generic contact record.
196pub struct GenericContactRecord {
197    /// The label of the entry.
198    pub label: String,
199    /// The vcard for the entry.
200    pub vcard: Vcard,
201    /// Collection of tags.
202    pub tags: Option<HashSet<String>>,
203    /// Optional note.
204    pub note: Option<String>,
205}
206
207/// Generic identification record.
208pub struct GenericIdRecord {
209    /// The label of the entry.
210    pub label: String,
211    /// The kind of identification.
212    pub id_kind: IdentityKind,
213    /// The number for the entry.
214    pub number: String,
215    /// The issue place for the entry.
216    pub issue_place: Option<String>,
217    /// The issue date for the entry.
218    pub issue_date: Option<UtcDateTime>,
219    /// The expiration date for the entry.
220    pub expiration_date: Option<UtcDateTime>,
221    /// Collection of tags.
222    pub tags: Option<HashSet<String>>,
223    /// Optional note.
224    pub note: Option<String>,
225}
226
227/// Generic payment record.
228pub enum GenericPaymentRecord {
229    /// Card payment information.
230    Card {
231        /// The label of the entry.
232        label: String,
233        /// The card number.
234        number: String,
235        /// The CVV code.
236        code: String,
237        /// An expiration date.
238        expiration: Option<UtcDateTime>,
239        /// The country for the entry.
240        country: String,
241        /// A note for the entry.
242        note: Option<String>,
243        /// Collection of tags.
244        tags: Option<HashSet<String>>,
245    },
246    /// Bank account payment information.
247    BankAccount {
248        /// The label of the entry.
249        label: String,
250        /// The account holder of the entry.
251        account_holder: String,
252        /// The account number of the entry.
253        account_number: String,
254        /// The routing number of the entry.
255        routing_number: String,
256        /// The country for the entry.
257        country: String,
258        /// A note for the entry.
259        note: Option<String>,
260        /// Collection of tags.
261        tags: Option<HashSet<String>>,
262    },
263}
264
265impl GenericPaymentRecord {
266    /// Get the label for the record.
267    fn label(&self) -> &str {
268        match self {
269            Self::Card { label, .. } => label,
270            Self::BankAccount { label, .. } => label,
271        }
272    }
273
274    /// Get the tags for the record.
275    fn tags(&mut self) -> &mut Option<HashSet<String>> {
276        match self {
277            Self::Card { tags, .. } => tags,
278            Self::BankAccount { tags, .. } => tags,
279        }
280    }
281
282    /// Get the note for the record.
283    fn note(&mut self) -> &mut Option<String> {
284        match self {
285            Self::Card { note, .. } => note,
286            Self::BankAccount { note, .. } => note,
287        }
288    }
289}
290
291/// Convert from generic password records.
292pub struct GenericCsvConvert;
293
294#[async_trait]
295impl Convert for GenericCsvConvert {
296    type Input = Vec<GenericCsvEntry>;
297
298    async fn convert(
299        &self,
300        source: Self::Input,
301        vault: Vault,
302        key: &AccessKey,
303    ) -> crate::Result<Vault> {
304        let mut index = SearchIndex::new();
305        let mut keeper = AccessPoint::from_vault(vault);
306        keeper.unlock(key).await?;
307
308        let mut duplicates: HashMap<String, usize> = HashMap::new();
309
310        for mut entry in source {
311            // Handle duplicate labels by incrementing a counter
312            let mut label = entry.label().to_owned();
313
314            let rename_label = {
315                if index
316                    .find_by_label(keeper.vault().id(), &label, None)
317                    .is_some()
318                {
319                    duplicates
320                        .entry(label.clone())
321                        .and_modify(|counter| *counter += 1)
322                        .or_insert(1);
323                    let counter = duplicates.get(&label).unwrap();
324                    Some(format!("{} {}", label, counter))
325                } else {
326                    None
327                }
328            };
329
330            if let Some(renamed) = rename_label {
331                label = renamed;
332            }
333
334            let tags = entry.tags().take();
335            let note = entry.note().take();
336            let mut secret: Secret = entry.into();
337            secret.user_data_mut().set_comment(note);
338            let mut meta = SecretMeta::new(label, secret.kind());
339            if let Some(tags) = tags {
340                meta.set_tags(tags);
341            }
342
343            let id = SecretId::new_v4();
344            let index_doc = index.prepare(keeper.id(), &id, &meta, &secret);
345            let secret_data = SecretRow::new(id, meta, secret);
346            keeper.create_secret(&secret_data).await?;
347            index.commit(index_doc);
348        }
349
350        keeper.lock();
351        Ok(keeper.into())
352    }
353}