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