Skip to content

Commit e187a72

Browse files
authored
Update credential exchange to latest version (#169)
Update our credential implementation to follow the latest version.
1 parent a0b0d73 commit e187a72

File tree

6 files changed

+151
-142
lines changed

6 files changed

+151
-142
lines changed

Cargo.lock

Lines changed: 3 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-exporters/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ bitwarden-crypto = { workspace = true }
2424
bitwarden-fido = { workspace = true }
2525
bitwarden-vault = { workspace = true }
2626
chrono = { workspace = true, features = ["std"] }
27-
credential-exchange-types = { git = "https://github.com/bitwarden/credential-exchange.git", rev = "60bf99f097af72144b0eaa757ccb50fd46049f24" }
27+
credential-exchange-types = { git = "https://github.com/bitwarden/credential-exchange.git", rev = "03f29bc63cea5bc8c9c8bfe27f002b3e90f9e051" }
2828
csv = "1.3.0"
29+
num-traits = ">=0.2, <0.3"
2930
schemars = { workspace = true }
3031
serde = { workspace = true }
3132
serde_json = { workspace = true }

crates/bitwarden-exporters/src/cxf/card.rs

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,56 @@
33
//! Handles conversion between internal [Card] and credential exchange [CreditCardCredential].
44
55
use bitwarden_vault::CardBrand;
6-
use credential_exchange_types::format::{Credential, CreditCardCredential};
6+
use chrono::Month;
7+
use credential_exchange_types::format::{Credential, CreditCardCredential, EditableFieldYearMonth};
8+
use num_traits::FromPrimitive;
79

810
use crate::Card;
911

1012
impl From<Card> for Vec<Credential> {
1113
fn from(value: Card) -> Self {
14+
let expiry_date = match (value.exp_year, value.exp_month) {
15+
(Some(year), Some(month)) => {
16+
let year_parsed = year.parse().ok();
17+
let numeric_month: Option<u32> = month.parse().ok();
18+
let month_parsed = numeric_month.and_then(Month::from_u32);
19+
match (year_parsed, month_parsed) {
20+
(Some(year), Some(month)) => {
21+
Some(EditableFieldYearMonth { year, month }.into())
22+
}
23+
_ => None,
24+
}
25+
}
26+
_ => None,
27+
};
28+
1229
vec![Credential::CreditCard(Box::new(CreditCardCredential {
13-
number: value.number.unwrap_or_default(),
14-
full_name: value.cardholder_name.unwrap_or_default(),
15-
card_type: value.brand,
16-
verification_number: value.code,
17-
expiry_date: match (value.exp_year, value.exp_month) {
18-
(Some(year), Some(month)) => Some(format!("{}-{}", year, month)),
19-
_ => None,
20-
},
30+
number: value.number.map(|v| v.into()),
31+
full_name: value.cardholder_name.map(|v| v.into()),
32+
card_type: value.brand.map(|v| v.into()),
33+
verification_number: value.code.map(|v| v.into()),
34+
pin: None,
35+
expiry_date,
2136
valid_from: None,
2237
}))]
2338
}
2439
}
2540

2641
impl From<&CreditCardCredential> for Card {
2742
fn from(value: &CreditCardCredential) -> Self {
28-
let (year, month) = value.expiry_date.as_ref().map_or((None, None), |date| {
29-
let parts: Vec<&str> = date.split('-').collect();
30-
let year = parts.first().map(|s| s.to_string());
31-
let month = parts.get(1).map(|s| s.to_string());
32-
(year, month)
33-
});
34-
3543
Card {
36-
cardholder_name: Some(value.full_name.clone()),
37-
exp_month: month,
38-
exp_year: year,
39-
code: value.verification_number.clone(),
44+
cardholder_name: value.full_name.clone().map(|v| v.into()),
45+
exp_month: value
46+
.expiry_date
47+
.as_ref()
48+
.map(|v| v.value.month.number_from_month().to_string()),
49+
exp_year: value.expiry_date.as_ref().map(|v| v.value.year.to_string()),
50+
code: value.verification_number.clone().map(|v| v.into()),
4051
brand: value
4152
.card_type
4253
.as_ref()
43-
.and_then(|brand| sanitize_brand(brand)),
44-
number: Some(value.number.clone()),
54+
.and_then(|brand| sanitize_brand(&brand.value.0)),
55+
number: value.number.clone().map(|v| v.into()),
4556
}
4657
}
4758
}
@@ -72,6 +83,9 @@ fn sanitize_brand(value: &str) -> Option<String> {
7283

7384
#[cfg(test)]
7485
mod tests {
86+
use chrono::Month;
87+
use credential_exchange_types::format::EditableFieldYearMonth;
88+
7589
use super::*;
7690

7791
#[test]
@@ -104,11 +118,26 @@ mod tests {
104118
assert_eq!(credentials.len(), 1);
105119

106120
if let Credential::CreditCard(credit_card) = &credentials[0] {
107-
assert_eq!(credit_card.full_name, "John Doe");
108-
assert_eq!(credit_card.expiry_date, Some("2025-12".to_string()));
109-
assert_eq!(credit_card.verification_number, Some("123".to_string()));
110-
assert_eq!(credit_card.card_type, Some("Visa".to_string()));
111-
assert_eq!(credit_card.number, "4111111111111111");
121+
assert_eq!(credit_card.full_name.as_ref().unwrap().value.0, "John Doe");
122+
assert_eq!(
123+
credit_card.expiry_date.as_ref().unwrap().value,
124+
EditableFieldYearMonth {
125+
year: 2025,
126+
month: Month::December
127+
}
128+
);
129+
assert_eq!(
130+
credit_card.verification_number.as_ref().unwrap().value.0,
131+
"123".to_string()
132+
);
133+
assert_eq!(
134+
credit_card.card_type.as_ref().unwrap().value.0,
135+
"Visa".to_string()
136+
);
137+
assert_eq!(
138+
credit_card.number.as_ref().unwrap().value.0,
139+
"4111111111111111"
140+
);
112141
} else {
113142
panic!("Expected CreditCardCredential");
114143
}
@@ -117,11 +146,18 @@ mod tests {
117146
#[test]
118147
fn test_credit_card_credential_to_card() {
119148
let credit_card = CreditCardCredential {
120-
number: "4111111111111111".to_string(),
121-
full_name: "John Doe".to_string(),
122-
card_type: Some("Visa".to_string()),
123-
verification_number: Some("123".to_string()),
124-
expiry_date: Some("2025-12".to_string()),
149+
number: Some("4111111111111111".to_string().into()),
150+
full_name: Some("John Doe".to_string().into()),
151+
card_type: Some("Visa".to_string().into()),
152+
verification_number: Some("123".to_string().into()),
153+
pin: None,
154+
expiry_date: Some(
155+
EditableFieldYearMonth {
156+
year: 2025,
157+
month: Month::December,
158+
}
159+
.into(),
160+
),
125161
valid_from: None,
126162
};
127163

crates/bitwarden-exporters/src/cxf/export.rs

Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use bitwarden_vault::{Totp, TotpAlgorithm};
22
use credential_exchange_types::format::{
3-
Account as CxfAccount, Credential, Item, ItemType, NoteCredential, OTPHashAlgorithm,
4-
TotpCredential,
3+
Account as CxfAccount, Credential, Item, NoteCredential, OTPHashAlgorithm, TotpCredential,
54
};
65
use uuid::Uuid;
76

@@ -27,10 +26,9 @@ pub(crate) fn build_cxf(account: Account, ciphers: Vec<Cipher>) -> Result<String
2726

2827
let account = CxfAccount {
2928
id: account.id.as_bytes().as_slice().into(),
30-
user_name: "".to_owned(),
29+
username: "".to_owned(),
3130
email: account.email,
3231
full_name: account.name,
33-
icon: None,
3432
collections: vec![], // TODO: Add support for folders
3533
items,
3634
extensions: None,
@@ -46,41 +44,29 @@ impl TryFrom<Cipher> for Item {
4644
let mut credentials: Vec<Credential> = value.r#type.clone().into();
4745

4846
if let Some(note) = value.notes {
49-
credentials.push(Credential::Note(Box::new(NoteCredential { content: note })));
47+
credentials.push(Credential::Note(Box::new(NoteCredential {
48+
content: note.into(),
49+
})));
5050
}
5151

5252
Ok(Self {
5353
id: value.id.as_bytes().as_slice().into(),
5454
creation_at: Some(value.creation_date.timestamp() as u64),
5555
modified_at: Some(value.revision_date.timestamp() as u64),
56-
ty: value.r#type.try_into()?,
5756
title: value.name,
5857
subtitle: None,
5958
favorite: Some(value.favorite),
6059
credentials,
6160
tags: None,
6261
extensions: None,
62+
scope: match value.r#type {
63+
CipherType::Login(login) => Some((*login).into()),
64+
_ => None,
65+
},
6366
})
6467
}
6568
}
6669

67-
impl TryFrom<CipherType> for ItemType {
68-
type Error = CxfError;
69-
70-
fn try_from(value: CipherType) -> Result<Self, Self::Error> {
71-
match value {
72-
CipherType::Login(_) => Ok(ItemType::Login),
73-
CipherType::Card(_) => Ok(ItemType::Identity),
74-
CipherType::Identity(_) => Ok(ItemType::Identity),
75-
CipherType::SecureNote(_) => Ok(ItemType::Document),
76-
CipherType::SshKey(_) => {
77-
// TODO(PM-15448): Add support for SSH Keys
78-
Err(CxfError::Internal("Unsupported CipherType: SshKey".into()))
79-
}
80-
}
81-
}
82-
}
83-
8470
impl From<CipherType> for Vec<Credential> {
8571
fn from(value: CipherType) -> Self {
8672
match value {
@@ -131,7 +117,7 @@ fn convert_totp(totp: Totp) -> TotpCredential {
131117
secret: totp.secret.into(),
132118
period: totp.period as u8,
133119
digits: totp.digits as u8,
134-
username: totp.account.unwrap_or("".to_string()),
120+
username: totp.account,
135121
algorithm: match totp.algorithm {
136122
TotpAlgorithm::Sha1 => OTPHashAlgorithm::Sha1,
137123
TotpAlgorithm::Sha256 => OTPHashAlgorithm::Sha256,
@@ -144,7 +130,6 @@ fn convert_totp(totp: Totp) -> TotpCredential {
144130

145131
#[cfg(test)]
146132
mod tests {
147-
use credential_exchange_types::format::FieldType;
148133

149134
use super::*;
150135
use crate::{Fido2Credential, Field, LoginUri};
@@ -164,7 +149,7 @@ mod tests {
164149
assert_eq!(String::from(credential.secret), "ONSWG4TFOQ");
165150
assert_eq!(credential.period, 60);
166151
assert_eq!(credential.digits, 4);
167-
assert_eq!(credential.username, "[email protected]");
152+
assert_eq!(credential.username.unwrap(), "[email protected]");
168153
assert_eq!(credential.algorithm, OTPHashAlgorithm::Sha1);
169154
assert_eq!(credential.issuer, Some("test-issuer".to_string()));
170155
}
@@ -249,10 +234,13 @@ mod tests {
249234
assert_eq!(item.id.to_string(), "JcjEFLRGSOmhvbEHALvXQA");
250235
assert_eq!(item.creation_at, Some(1706613834));
251236
assert_eq!(item.modified_at, Some(1706623773));
252-
assert_eq!(item.ty, ItemType::Login);
253237
assert_eq!(item.title, "Bitwarden");
254238
assert_eq!(item.subtitle, None);
255239
assert_eq!(item.tags, None);
240+
assert_eq!(
241+
item.scope.unwrap().urls,
242+
vec!["https://vault.bitwarden.com".to_string()]
243+
);
256244
assert!(item.extensions.is_none());
257245

258246
assert_eq!(item.credentials.len(), 4);
@@ -262,19 +250,12 @@ mod tests {
262250
match credential {
263251
Credential::BasicAuth(basic_auth) => {
264252
let username = basic_auth.username.as_ref().unwrap();
265-
assert_eq!(username.field_type, FieldType::String);
266-
assert_eq!(username.value, "[email protected]");
253+
assert_eq!(username.value.0, "[email protected]");
267254
assert!(username.label.is_none());
268255

269256
let password = basic_auth.password.as_ref().unwrap();
270-
assert_eq!(password.field_type, FieldType::ConcealedString);
271-
assert_eq!(password.value, "asdfasdfasdf");
257+
assert_eq!(password.value.0, "asdfasdfasdf");
272258
assert!(password.label.is_none());
273-
274-
assert_eq!(
275-
basic_auth.urls,
276-
vec!["https://vault.bitwarden.com".to_string()]
277-
);
278259
}
279260
_ => panic!("Expected Credential::BasicAuth"),
280261
}
@@ -286,7 +267,7 @@ mod tests {
286267
assert_eq!(String::from(totp.secret.clone()), "JBSWY3DPEHPK3PXP");
287268
assert_eq!(totp.period, 30);
288269
assert_eq!(totp.digits, 6);
289-
assert_eq!(totp.username, "");
270+
assert_eq!(totp.username, None);
290271
assert_eq!(totp.algorithm, OTPHashAlgorithm::Sha1);
291272
assert!(totp.issuer.is_none());
292273
}
@@ -299,7 +280,7 @@ mod tests {
299280
Credential::Passkey(passkey) => {
300281
assert_eq!(passkey.credential_id.to_string(), "6NiHiekW4ZY8vYHa-ucbvA");
301282
assert_eq!(passkey.rp_id, "123");
302-
assert_eq!(passkey.user_name, "");
283+
assert_eq!(passkey.username, "");
303284
assert_eq!(passkey.user_display_name, "");
304285
assert_eq!(String::from(passkey.user_handle.clone()), "AAECAwQFBg");
305286
assert_eq!(String::from(passkey.key.clone()), "AAECAwQFBg");
@@ -312,7 +293,7 @@ mod tests {
312293

313294
match credential {
314295
Credential::Note(n) => {
315-
assert_eq!(n.content, "My note");
296+
assert_eq!(n.content.value.0, "My note");
316297
}
317298
_ => panic!("Expected Credential::Passkey"),
318299
}

0 commit comments

Comments
 (0)