sos_migrate/import/csv/
bitwarden.rs1use async_trait::async_trait;
7use serde::Deserialize;
8use sos_core::crypto::AccessKey;
9use sos_vault::Vault;
10use sos_vfs as vfs;
11use std::path::{Path, PathBuf};
12use tokio::io::AsyncRead;
13use url::Url;
14
15use super::{
16 GenericCsvConvert, GenericCsvEntry, GenericNoteRecord,
17 GenericPasswordRecord, UNTITLED,
18};
19use crate::{import::read_csv_records, Convert, Result};
20
21const TYPE_LOGIN: &str = "login";
22const TYPE_NOTE: &str = "note";
23
24#[derive(Deserialize)]
26pub struct BitwardenPasswordRecord {
27 pub folder: String,
29 pub favorite: String,
31 #[serde(rename = "type")]
33 pub kind: String,
34 pub name: String,
36 pub notes: String,
38 pub fields: String,
40 pub reprompt: String,
42 pub login_uri: Option<Url>,
44 pub login_username: String,
46 pub login_password: String,
48 pub login_totp: String,
50}
51
52impl From<BitwardenPasswordRecord> for GenericPasswordRecord {
53 fn from(value: BitwardenPasswordRecord) -> Self {
54 let label = if value.name.is_empty() {
55 UNTITLED.to_owned()
56 } else {
57 value.name
58 };
59
60 let note = if !value.notes.is_empty() {
61 Some(value.notes)
62 } else {
63 None
64 };
65
66 let url = if let Some(uri) = value.login_uri {
67 vec![uri]
68 } else {
69 vec![]
70 };
71
72 Self {
73 label,
74 url,
75 username: value.login_username,
76 password: value.login_password,
77 otp_auth: None,
78 tags: None,
79 note,
80 }
81 }
82}
83
84impl From<BitwardenPasswordRecord> for GenericNoteRecord {
85 fn from(value: BitwardenPasswordRecord) -> Self {
86 let label = if value.name.is_empty() {
87 UNTITLED.to_owned()
88 } else {
89 value.name
90 };
91 Self {
92 label,
93 text: value.notes,
94 tags: None,
95 note: None,
96 }
97 }
98}
99
100impl From<BitwardenPasswordRecord> for GenericCsvEntry {
101 fn from(value: BitwardenPasswordRecord) -> Self {
102 if value.kind == TYPE_LOGIN {
103 Self::Password(value.into())
104 } else {
105 Self::Note(value.into())
106 }
107 }
108}
109
110pub async fn parse_reader<R: AsyncRead + Unpin + Send>(
112 reader: R,
113) -> Result<Vec<BitwardenPasswordRecord>> {
114 read_csv_records::<BitwardenPasswordRecord, _>(reader).await
115}
116
117pub async fn parse_path<P: AsRef<Path>>(
119 path: P,
120) -> Result<Vec<BitwardenPasswordRecord>> {
121 parse_reader(vfs::File::open(path).await?).await
122}
123
124pub struct BitwardenCsv;
126
127#[async_trait]
128impl Convert for BitwardenCsv {
129 type Input = PathBuf;
130
131 async fn convert(
132 &self,
133 source: Self::Input,
134 vault: Vault,
135 key: &AccessKey,
136 ) -> crate::Result<Vault> {
137 let records: Vec<GenericCsvEntry> = parse_path(source)
138 .await?
139 .into_iter()
140 .filter(|record| {
141 record.kind == TYPE_LOGIN || record.kind == TYPE_NOTE
142 })
143 .map(|r| r.into())
144 .collect();
145 GenericCsvConvert.convert(records, vault, key).await
146 }
147}