sos_migrate/import/csv/
chrome.rs

1//! Parser for the Chrome passwords CSV export.
2
3use async_trait::async_trait;
4use serde::Deserialize;
5use sos_core::crypto::AccessKey;
6use sos_vault::Vault;
7use sos_vfs as vfs;
8use std::path::{Path, PathBuf};
9use tokio::io::AsyncRead;
10use url::Url;
11
12use super::{
13    GenericCsvConvert, GenericCsvEntry, GenericPasswordRecord, UNTITLED,
14};
15use crate::{import::read_csv_records, Convert, Result};
16
17/// Record for an entry in a Chrome passwords CSV export.
18#[derive(Deserialize)]
19pub struct ChromePasswordRecord {
20    /// The name of the entry.
21    pub name: String,
22    /// The URL of the entry.
23    pub url: Option<String>,
24    /// The username for the entry.
25    pub username: String,
26    /// The password for the entry.
27    pub password: String,
28    /// The note for the entry.
29    pub note: Option<String>,
30}
31
32impl From<ChromePasswordRecord> for GenericPasswordRecord {
33    fn from(value: ChromePasswordRecord) -> Self {
34        let label = if value.name.is_empty() {
35            UNTITLED.to_owned()
36        } else {
37            value.name
38        };
39
40        let url = if let Some(url) = value.url {
41            let mut websites = Vec::new();
42            for u in url.split(",") {
43                if let Ok(url) = u.trim().parse::<Url>() {
44                    websites.push(url);
45                }
46            }
47            websites
48        } else {
49            vec![]
50        };
51
52        Self {
53            label,
54            url,
55            username: value.username,
56            password: value.password,
57            otp_auth: None,
58            tags: None,
59            note: value.note,
60        }
61    }
62}
63
64impl From<ChromePasswordRecord> for GenericCsvEntry {
65    fn from(value: ChromePasswordRecord) -> Self {
66        Self::Password(value.into())
67    }
68}
69
70/// Parse records from a reader.
71pub async fn parse_reader<R: AsyncRead + Unpin + Send>(
72    reader: R,
73) -> Result<Vec<ChromePasswordRecord>> {
74    read_csv_records::<ChromePasswordRecord, _>(reader).await
75}
76
77/// Parse records from a path.
78pub async fn parse_path<P: AsRef<Path>>(
79    path: P,
80) -> Result<Vec<ChromePasswordRecord>> {
81    parse_reader(vfs::File::open(path).await?).await
82}
83
84/// Import a Chrome passwords CSV export into a vault.
85pub struct ChromePasswordCsv;
86
87#[async_trait]
88impl Convert for ChromePasswordCsv {
89    type Input = PathBuf;
90
91    async fn convert(
92        &self,
93        source: Self::Input,
94        vault: Vault,
95        key: &AccessKey,
96    ) -> crate::Result<Vault> {
97        let records: Vec<GenericCsvEntry> = parse_path(source)
98            .await?
99            .into_iter()
100            .map(|r| r.into())
101            .collect();
102        GenericCsvConvert.convert(records, vault, key).await
103    }
104}