sos_migrate/import/csv/
one_password.rs

1//! Parser for the 1Password passwords CSV export.
2
3use async_trait::async_trait;
4use serde::{
5    de::{self, Deserializer, Unexpected, Visitor},
6    Deserialize,
7};
8use sos_core::crypto::AccessKey;
9use sos_vault::Vault;
10use sos_vfs as vfs;
11use std::{
12    collections::HashSet,
13    fmt,
14    path::{Path, PathBuf},
15};
16use tokio::io::AsyncRead;
17use url::Url;
18
19use super::{
20    GenericCsvConvert, GenericCsvEntry, GenericPasswordRecord, UNTITLED,
21};
22use crate::{import::read_csv_records, Convert, Result};
23
24/// Record for an entry in a MacOS passwords CSV export.
25#[derive(Deserialize)]
26pub struct OnePasswordRecord {
27    /// The title of the entry.
28    #[serde(rename = "Title")]
29    pub title: String,
30    /// The URL of the entry.
31    #[serde(rename = "Url")]
32    pub url: Option<Url>,
33    /// The username for the entry.
34    #[serde(rename = "Username")]
35    pub username: String,
36    /// The password for the entry.
37    #[serde(rename = "Password")]
38    pub password: String,
39    /// OTP auth information for the entry.
40    #[serde(rename = "OTPAuth")]
41    pub otp_auth: Option<String>,
42    /// Flag if the entry is a favorite.
43    #[serde(rename = "Favorite", deserialize_with = "deserialize_bool")]
44    pub favorite: bool,
45    /// Flag if the entry is archived.
46    #[serde(rename = "Archived", deserialize_with = "deserialize_bool")]
47    pub archived: bool,
48    /// Collection of tags, delimited by a semi-colon.
49    #[serde(rename = "Tags")]
50    pub tags: String,
51    /// Notes for the entry.
52    #[serde(rename = "Notes")]
53    pub notes: String,
54}
55
56impl From<OnePasswordRecord> for GenericPasswordRecord {
57    fn from(value: OnePasswordRecord) -> Self {
58        let tags: Option<HashSet<String>> = if !value.tags.is_empty() {
59            Some(value.tags.split(';').map(|s| s.trim().to_owned()).collect())
60        } else {
61            None
62        };
63
64        let label = if value.title.is_empty() {
65            UNTITLED.to_owned()
66        } else {
67            value.title
68        };
69
70        let note = if !value.notes.is_empty() {
71            Some(value.notes)
72        } else {
73            None
74        };
75
76        let url = if let Some(url) = value.url {
77            vec![url]
78        } else {
79            vec![]
80        };
81
82        Self {
83            label,
84            url,
85            username: value.username,
86            password: value.password,
87            otp_auth: value.otp_auth,
88            tags,
89            note,
90        }
91    }
92}
93
94impl From<OnePasswordRecord> for GenericCsvEntry {
95    fn from(value: OnePasswordRecord) -> Self {
96        Self::Password(value.into())
97    }
98}
99
100/// Parse records from a reader.
101pub async fn parse_reader<R: AsyncRead + Unpin + Send>(
102    reader: R,
103) -> Result<Vec<OnePasswordRecord>> {
104    read_csv_records::<OnePasswordRecord, _>(reader).await
105}
106
107/// Parse records from a path.
108pub async fn parse_path<P: AsRef<Path>>(
109    path: P,
110) -> Result<Vec<OnePasswordRecord>> {
111    parse_reader(vfs::File::open(path).await?).await
112}
113
114/// Import a MacOS passwords CSV export into a vault.
115pub struct OnePasswordCsv;
116
117#[async_trait]
118impl Convert for OnePasswordCsv {
119    type Input = PathBuf;
120
121    async fn convert(
122        &self,
123        source: Self::Input,
124        vault: Vault,
125        key: &AccessKey,
126    ) -> crate::Result<Vault> {
127        let records: Vec<GenericCsvEntry> = parse_path(source)
128            .await?
129            .into_iter()
130            .map(|r| r.into())
131            .collect();
132        GenericCsvConvert.convert(records, vault, key).await
133    }
134}
135
136struct BoolString;
137
138impl<'de> Visitor<'de> for BoolString {
139    type Value = String;
140
141    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
142        write!(formatter, "a string representing a boolean flag")
143    }
144
145    fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
146    where
147        E: de::Error,
148    {
149        let b = s.to_lowercase();
150        if b == "true" || b == "false" {
151            Ok(s.to_owned())
152        } else {
153            Err(de::Error::invalid_value(Unexpected::Str(s), &self))
154        }
155    }
156}
157
158fn deserialize_bool<'de, D>(
159    deserializer: D,
160) -> std::result::Result<bool, D::Error>
161where
162    D: Deserializer<'de>,
163{
164    let value = deserializer.deserialize_str(BoolString)?;
165    Ok(value.to_lowercase() == "true")
166}