1use bitwarden_vault::{Totp, TotpAlgorithm};
2use credential_exchange_format::{
3 Account as CxfAccount, Credential, Item, NoteCredential, OTPHashAlgorithm, TotpCredential,
4};
5use uuid::Uuid;
6#[cfg(feature = "wasm")]
7use {tsify_next::Tsify, wasm_bindgen::prelude::*};
8
9use crate::{cxf::CxfError, Cipher, CipherType, Login};
10
11#[derive(Debug)]
15#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
16#[cfg_attr(
17 feature = "wasm",
18 derive(serde::Serialize, serde::Deserialize, Tsify),
19 tsify(into_wasm_abi, from_wasm_abi)
20)]
21pub struct Account {
22 id: Uuid,
23 email: String,
24 name: Option<String>,
25}
26
27pub(crate) fn build_cxf(account: Account, ciphers: Vec<Cipher>) -> Result<String, CxfError> {
29 let items: Vec<Item> = ciphers
30 .into_iter()
31 .flat_map(|cipher| cipher.try_into())
32 .collect();
33
34 let account = CxfAccount {
35 id: account.id.as_bytes().as_slice().into(),
36 username: "".to_owned(),
37 email: account.email,
38 full_name: account.name,
39 collections: vec![], items,
41 extensions: None,
42 };
43
44 Ok(serde_json::to_string(&account)?)
45}
46
47impl TryFrom<Cipher> for Item {
48 type Error = CxfError;
49
50 fn try_from(value: Cipher) -> Result<Self, Self::Error> {
51 let mut credentials: Vec<Credential> = value.r#type.clone().into();
52
53 if let Some(note) = value.notes {
54 credentials.push(Credential::Note(Box::new(NoteCredential {
55 content: note.into(),
56 })));
57 }
58
59 Ok(Self {
60 id: value.id.as_bytes().as_slice().into(),
61 creation_at: Some(value.creation_date.timestamp() as u64),
62 modified_at: Some(value.revision_date.timestamp() as u64),
63 title: value.name,
64 subtitle: None,
65 favorite: Some(value.favorite),
66 credentials,
67 tags: None,
68 extensions: None,
69 scope: match value.r#type {
70 CipherType::Login(login) => Some((*login).into()),
71 _ => None,
72 },
73 })
74 }
75}
76
77impl From<CipherType> for Vec<Credential> {
78 fn from(value: CipherType) -> Self {
79 match value {
80 CipherType::Login(login) => (*login).into(),
81 CipherType::Card(card) => (*card).into(),
83 CipherType::Identity(_) => vec![],
85 CipherType::SecureNote(_) => vec![],
88 CipherType::SshKey(_) => vec![],
90 }
91 }
92}
93
94impl From<Login> for Vec<Credential> {
96 fn from(login: Login) -> Self {
97 let mut credentials = vec![];
98
99 if login.username.is_some() || login.password.is_some() || !login.login_uris.is_empty() {
100 credentials.push(Credential::BasicAuth(Box::new(login.clone().into())));
101 }
102
103 if let Some(totp) = login.totp.and_then(|t| t.parse::<Totp>().ok()) {
104 credentials.push(Credential::Totp(Box::new(convert_totp(totp))));
105 }
106
107 if let Some(fido2_credentials) = login.fido2_credentials {
108 credentials.extend(
109 fido2_credentials
110 .into_iter()
111 .filter_map(|fido2_credential| fido2_credential.try_into().ok())
112 .map(|c| Credential::Passkey(Box::new(c))),
113 );
114 }
115
116 credentials
117 }
118}
119
120fn convert_totp(totp: Totp) -> TotpCredential {
122 TotpCredential {
124 secret: totp.secret.into(),
125 period: totp.period as u8,
126 digits: totp.digits as u8,
127 username: totp.account,
128 algorithm: match totp.algorithm {
129 TotpAlgorithm::Sha1 => OTPHashAlgorithm::Sha1,
130 TotpAlgorithm::Sha256 => OTPHashAlgorithm::Sha256,
131 TotpAlgorithm::Sha512 => OTPHashAlgorithm::Sha512,
132 TotpAlgorithm::Steam => OTPHashAlgorithm::Unknown("steam".to_string()),
133 },
134 issuer: totp.issuer,
135 }
136}
137
138#[cfg(test)]
139mod tests {
140
141 use super::*;
142 use crate::{Fido2Credential, Field, LoginUri};
143
144 #[test]
145 fn test_convert_totp() {
146 let totp = Totp {
147 account: Some("[email protected]".to_string()),
148 algorithm: TotpAlgorithm::Sha1,
149 digits: 4,
150 issuer: Some("test-issuer".to_string()),
151 period: 60,
152 secret: "secret".as_bytes().to_vec(),
153 };
154
155 let credential = convert_totp(totp);
156 assert_eq!(String::from(credential.secret), "ONSWG4TFOQ");
157 assert_eq!(credential.period, 60);
158 assert_eq!(credential.digits, 4);
159 assert_eq!(credential.username.unwrap(), "[email protected]");
160 assert_eq!(credential.algorithm, OTPHashAlgorithm::Sha1);
161 assert_eq!(credential.issuer, Some("test-issuer".to_string()));
162 }
163
164 #[test]
165 fn test_login_to_item() {
166 let cipher = Cipher {
167 id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
168 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
169
170 name: "Bitwarden".to_string(),
171 notes: Some("My note".to_string()),
172
173 r#type: CipherType::Login(Box::new(Login {
174 username: Some("[email protected]".to_string()),
175 password: Some("asdfasdfasdf".to_string()),
176 login_uris: vec![LoginUri {
177 uri: Some("https://vault.bitwarden.com".to_string()),
178 r#match: None,
179 }],
180 totp: Some("JBSWY3DPEHPK3PXP".to_string()),
181 fido2_credentials: Some(vec![Fido2Credential {
182 credential_id: "e8d88789-e916-e196-3cbd-81dafae71bbc".to_string(),
183 key_type: "public-key".to_string(),
184 key_algorithm: "ECDSA".to_string(),
185 key_curve: "P-256".to_string(),
186 key_value: "AAECAwQFBg".to_string(),
187 rp_id: "123".to_string(),
188 user_handle: Some("AAECAwQFBg".to_string()),
189 user_name: None,
190 counter: 0,
191 rp_name: None,
192 user_display_name: None,
193 discoverable: "true".to_string(),
194 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
195 }]),
196 })),
197
198 favorite: true,
199 reprompt: 0,
200
201 fields: vec![
202 Field {
203 name: Some("Text".to_string()),
204 value: Some("A".to_string()),
205 r#type: 0,
206 linked_id: None,
207 },
208 Field {
209 name: Some("Hidden".to_string()),
210 value: Some("B".to_string()),
211 r#type: 1,
212 linked_id: None,
213 },
214 Field {
215 name: Some("Boolean (true)".to_string()),
216 value: Some("true".to_string()),
217 r#type: 2,
218 linked_id: None,
219 },
220 Field {
221 name: Some("Boolean (false)".to_string()),
222 value: Some("false".to_string()),
223 r#type: 2,
224 linked_id: None,
225 },
226 Field {
227 name: Some("Linked".to_string()),
228 value: None,
229 r#type: 3,
230 linked_id: Some(101),
231 },
232 ],
233
234 revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
235 creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
236 deleted_date: None,
237 };
238
239 let item: Item = cipher.try_into().unwrap();
240
241 assert_eq!(item.id.to_string(), "JcjEFLRGSOmhvbEHALvXQA");
242 assert_eq!(item.creation_at, Some(1706613834));
243 assert_eq!(item.modified_at, Some(1706623773));
244 assert_eq!(item.title, "Bitwarden");
245 assert_eq!(item.subtitle, None);
246 assert_eq!(item.tags, None);
247 assert_eq!(
248 item.scope.unwrap().urls,
249 vec!["https://vault.bitwarden.com".to_string()]
250 );
251 assert!(item.extensions.is_none());
252
253 assert_eq!(item.credentials.len(), 4);
254
255 let credential = &item.credentials[0];
256
257 match credential {
258 Credential::BasicAuth(basic_auth) => {
259 let username = basic_auth.username.as_ref().unwrap();
260 assert_eq!(username.value.0, "[email protected]");
261 assert!(username.label.is_none());
262
263 let password = basic_auth.password.as_ref().unwrap();
264 assert_eq!(password.value.0, "asdfasdfasdf");
265 assert!(password.label.is_none());
266 }
267 _ => panic!("Expected Credential::BasicAuth"),
268 }
269
270 let credential = &item.credentials[1];
271
272 match credential {
273 Credential::Totp(totp) => {
274 assert_eq!(String::from(totp.secret.clone()), "JBSWY3DPEHPK3PXP");
275 assert_eq!(totp.period, 30);
276 assert_eq!(totp.digits, 6);
277 assert_eq!(totp.username, None);
278 assert_eq!(totp.algorithm, OTPHashAlgorithm::Sha1);
279 assert!(totp.issuer.is_none());
280 }
281 _ => panic!("Expected Credential::Passkey"),
282 }
283
284 let credential = &item.credentials[2];
285
286 match credential {
287 Credential::Passkey(passkey) => {
288 assert_eq!(passkey.credential_id.to_string(), "6NiHiekW4ZY8vYHa-ucbvA");
289 assert_eq!(passkey.rp_id, "123");
290 assert_eq!(passkey.username, "");
291 assert_eq!(passkey.user_display_name, "");
292 assert_eq!(String::from(passkey.user_handle.clone()), "AAECAwQFBg");
293 assert_eq!(String::from(passkey.key.clone()), "AAECAwQFBg");
294 assert!(passkey.fido2_extensions.is_none());
295 }
296 _ => panic!("Expected Credential::Passkey"),
297 }
298
299 let credential = &item.credentials[3];
300
301 match credential {
302 Credential::Note(n) => {
303 assert_eq!(n.content.value.0, "My note");
304 }
305 _ => panic!("Expected Credential::Passkey"),
306 }
307 }
308}