Repository: bitwarden/sdk-internal
Pull request #169: Update credential exchange to latest version
Author: login: Hinton
Created: 2025-02-20T09:33:21Z | Merged: 2025-03-04T09:57:19Z

## 🎟️ Tracking

<!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. -->

## 📔 Objective

<!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. -->

Update our credential implementation to follow the latest version.

## ⏰ Reminders before review

- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Protected functional changes with optionality (feature flags)
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation
  team

## 🦮 Reviewer guidelines

<!-- Suggested interactions but feel free to use (or not) as you desire! -->

- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed
  issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes



Diff:
diff --git a/Cargo.lock b/Cargo.lock
index b08c00ab8c..da265e3656 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -490,6 +490,7 @@ dependencies = [
  "chrono",
  "credential-exchange-types",
  "csv",
+ "num-traits",
  "rand",
  "schemars",
  "serde",
@@ -1043,8 +1044,9 @@ dependencies = [
 [[package]]
 name = "credential-exchange-types"
 version = "0.1.0"
-source = "git+https://github.com/bitwarden/credential-exchange.git?rev=60bf99f097af72144b0eaa757ccb50fd46049f24#60bf99f097af72144b0eaa757ccb50fd46049f24"
+source = "git+https://github.com/bitwarden/credential-exchange.git?rev=03f29bc63cea5bc8c9c8bfe27f002b3e90f9e051#03f29bc63cea5bc8c9c8bfe27f002b3e90f9e051"
 dependencies = [
+ "chrono",
  "data-encoding",
  "jose-jwk",
  "serde",
@@ -2289,9 +2291,6 @@ checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7"
 dependencies = [
  "jose-b64",
  "jose-jwa",
- "p256",
- "p384",
- "rsa",
  "serde",
  "zeroize",
 ]
@@ -2601,16 +2600,6 @@ dependencies = [
  "sha2",
 ]
 
-[[package]]
-name = "p384"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
-dependencies = [
- "elliptic-curve",
- "primeorder",
-]
-
 [[package]]
 name = "parking_lot"
 version = "0.12.3"
diff --git a/crates/bitwarden-exporters/Cargo.toml b/crates/bitwarden-exporters/Cargo.toml
index 84241c3ad5..b11aec421b 100644
--- a/crates/bitwarden-exporters/Cargo.toml
+++ b/crates/bitwarden-exporters/Cargo.toml
@@ -24,8 +24,9 @@ bitwarden-crypto = { workspace = true }
 bitwarden-fido = { workspace = true }
 bitwarden-vault = { workspace = true }
 chrono = { workspace = true, features = ["std"] }
-credential-exchange-types = { git = "https://github.com/bitwarden/credential-exchange.git", rev = "60bf99f097af72144b0eaa757ccb50fd46049f24" }
+credential-exchange-types = { git = "https://github.com/bitwarden/credential-exchange.git", rev = "03f29bc63cea5bc8c9c8bfe27f002b3e90f9e051" }
 csv = "1.3.0"
+num-traits = ">=0.2, <0.3"
 schemars = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
diff --git a/crates/bitwarden-exporters/src/cxf/card.rs b/crates/bitwarden-exporters/src/cxf/card.rs
index 75c3e0f482..1fbb8050c3 100644
--- a/crates/bitwarden-exporters/src/cxf/card.rs
+++ b/crates/bitwarden-exporters/src/cxf/card.rs
@@ -3,21 +3,36 @@
 //! Handles conversion between internal [Card] and credential exchange [CreditCardCredential].
 
 use bitwarden_vault::CardBrand;
-use credential_exchange_types::format::{Credential, CreditCardCredential};
+use chrono::Month;
+use credential_exchange_types::format::{Credential, CreditCardCredential, EditableFieldYearMonth};
+use num_traits::FromPrimitive;
 
 use crate::Card;
 
 impl From<Card> for Vec<Credential> {
     fn from(value: Card) -> Self {
+        let expiry_date = match (value.exp_year, value.exp_month) {
+            (Some(year), Some(month)) => {
+                let year_parsed = year.parse().ok();
+                let numeric_month: Option<u32> = month.parse().ok();
+                let month_parsed = numeric_month.and_then(Month::from_u32);
+                match (year_parsed, month_parsed) {
+                    (Some(year), Some(month)) => {
+                        Some(EditableFieldYearMonth { year, month }.into())
+                    }
+                    _ => None,
+                }
+            }
+            _ => None,
+        };
+
         vec![Credential::CreditCard(Box::new(CreditCardCredential {
-            number: value.number.unwrap_or_default(),
-            full_name: value.cardholder_name.unwrap_or_default(),
-            card_type: value.brand,
-            verification_number: value.code,
-            expiry_date: match (value.exp_year, value.exp_month) {
-                (Some(year), Some(month)) => Some(format!("{}-{}", year, month)),
-                _ => None,
-            },
+            number: value.number.map(|v| v.into()),
+            full_name: value.cardholder_name.map(|v| v.into()),
+            card_type: value.brand.map(|v| v.into()),
+            verification_number: value.code.map(|v| v.into()),
+            pin: None,
+            expiry_date,
             valid_from: None,
         }))]
     }
@@ -25,23 +40,19 @@ impl From<Card> for Vec<Credential> {
 
 impl From<&CreditCardCredential> for Card {
     fn from(value: &CreditCardCredential) -> Self {
-        let (year, month) = value.expiry_date.as_ref().map_or((None, None), |date| {
-            let parts: Vec<&str> = date.split('-').collect();
-            let year = parts.first().map(|s| s.to_string());
-            let month = parts.get(1).map(|s| s.to_string());
-            (year, month)
-        });
-
         Card {
-            cardholder_name: Some(value.full_name.clone()),
-            exp_month: month,
-            exp_year: year,
-            code: value.verification_number.clone(),
+            cardholder_name: value.full_name.clone().map(|v| v.into()),
+            exp_month: value
+                .expiry_date
+                .as_ref()
+                .map(|v| v.value.month.number_from_month().to_string()),
+            exp_year: value.expiry_date.as_ref().map(|v| v.value.year.to_string()),
+            code: value.verification_number.clone().map(|v| v.into()),
             brand: value
                 .card_type
                 .as_ref()
-                .and_then(|brand| sanitize_brand(brand)),
-            number: Some(value.number.clone()),
+                .and_then(|brand| sanitize_brand(&brand.value.0)),
+            number: value.number.clone().map(|v| v.into()),
         }
     }
 }
@@ -72,6 +83,9 @@ fn sanitize_brand(value: &str) -> Option<String> {
 
 #[cfg(test)]
 mod tests {
+    use chrono::Month;
+    use credential_exchange_types::format::EditableFieldYearMonth;
+
     use super::*;
 
     #[test]
@@ -104,11 +118,26 @@ mod tests {
         assert_eq!(credentials.len(), 1);
 
         if let Credential::CreditCard(credit_card) = &credentials[0] {
-            assert_eq!(credit_card.full_name, "John Doe");
-            assert_eq!(credit_card.expiry_date, Some("2025-12".to_string()));
-            assert_eq!(credit_card.verification_number, Some("123".to_string()));
-            assert_eq!(credit_card.card_type, Some("Visa".to_string()));
-            assert_eq!(credit_card.number, "4111111111111111");
+            assert_eq!(credit_card.full_name.as_ref().unwrap().value.0, "John Doe");
+            assert_eq!(
+                credit_card.expiry_date.as_ref().unwrap().value,
+                EditableFieldYearMonth {
+                    year: 2025,
+                    month: Month::December
+                }
+            );
+            assert_eq!(
+                credit_card.verification_number.as_ref().unwrap().value.0,
+                "123".to_string()
+            );
+            assert_eq!(
+                credit_card.card_type.as_ref().unwrap().value.0,
+                "Visa".to_string()
+            );
+            assert_eq!(
+                credit_card.number.as_ref().unwrap().value.0,
+                "4111111111111111"
+            );
         } else {
             panic!("Expected CreditCardCredential");
         }
@@ -117,11 +146,18 @@ mod tests {
     #[test]
     fn test_credit_card_credential_to_card() {
         let credit_card = CreditCardCredential {
-            number: "4111111111111111".to_string(),
-            full_name: "John Doe".to_string(),
-            card_type: Some("Visa".to_string()),
-            verification_number: Some("123".to_string()),
-            expiry_date: Some("2025-12".to_string()),
+            number: Some("4111111111111111".to_string().into()),
+            full_name: Some("John Doe".to_string().into()),
+            card_type: Some("Visa".to_string().into()),
+            verification_number: Some("123".to_string().into()),
+            pin: None,
+            expiry_date: Some(
+                EditableFieldYearMonth {
+                    year: 2025,
+                    month: Month::December,
+                }
+                .into(),
+            ),
             valid_from: None,
         };
 
diff --git a/crates/bitwarden-exporters/src/cxf/export.rs b/crates/bitwarden-exporters/src/cxf/export.rs
index c4b530dadc..f9df45a4bc 100644
--- a/crates/bitwarden-exporters/src/cxf/export.rs
+++ b/crates/bitwarden-exporters/src/cxf/export.rs
@@ -1,7 +1,6 @@
 use bitwarden_vault::{Totp, TotpAlgorithm};
 use credential_exchange_types::format::{
-    Account as CxfAccount, Credential, Item, ItemType, NoteCredential, OTPHashAlgorithm,
-    TotpCredential,
+    Account as CxfAccount, Credential, Item, NoteCredential, OTPHashAlgorithm, TotpCredential,
 };
 use uuid::Uuid;
 
@@ -27,10 +26,9 @@ pub(crate) fn build_cxf(account: Account, ciphers: Vec<Cipher>) -> Result<String
 
     let account = CxfAccount {
         id: account.id.as_bytes().as_slice().into(),
-        user_name: "".to_owned(),
+        username: "".to_owned(),
         email: account.email,
         full_name: account.name,
-        icon: None,
         collections: vec![], // TODO: Add support for folders
         items,
         extensions: None,
@@ -46,41 +44,29 @@ impl TryFrom<Cipher> for Item {
         let mut credentials: Vec<Credential> = value.r#type.clone().into();
 
         if let Some(note) = value.notes {
-            credentials.push(Credential::Note(Box::new(NoteCredential { content: note })));
+            credentials.push(Credential::Note(Box::new(NoteCredential {
+                content: note.into(),
+            })));
         }
 
         Ok(Self {
             id: value.id.as_bytes().as_slice().into(),
             creation_at: Some(value.creation_date.timestamp() as u64),
             modified_at: Some(value.revision_date.timestamp() as u64),
-            ty: value.r#type.try_into()?,
             title: value.name,
             subtitle: None,
             favorite: Some(value.favorite),
             credentials,
             tags: None,
             extensions: None,
+            scope: match value.r#type {
+                CipherType::Login(login) => Some((*login).into()),
+                _ => None,
+            },
         })
     }
 }
 
-impl TryFrom<CipherType> for ItemType {
-    type Error = CxfError;
-
-    fn try_from(value: CipherType) -> Result<Self, Self::Error> {
-        match value {
-            CipherType::Login(_) => Ok(ItemType::Login),
-            CipherType::Card(_) => Ok(ItemType::Identity),
-            CipherType::Identity(_) => Ok(ItemType::Identity),
-            CipherType::SecureNote(_) => Ok(ItemType::Document),
-            CipherType::SshKey(_) => {
-                // TODO(PM-15448): Add support for SSH Keys
-                Err(CxfError::Internal("Unsupported CipherType: SshKey".into()))
-            }
-        }
-    }
-}
-
 impl From<CipherType> for Vec<Credential> {
     fn from(value: CipherType) -> Self {
         match value {
@@ -131,7 +117,7 @@ fn convert_totp(totp: Totp) -> TotpCredential {
         secret: totp.secret.into(),
         period: totp.period as u8,
         digits: totp.digits as u8,
-        username: totp.account.unwrap_or("".to_string()),
+        username: totp.account,
         algorithm: match totp.algorithm {
             TotpAlgorithm::Sha1 => OTPHashAlgorithm::Sha1,
             TotpAlgorithm::Sha256 => OTPHashAlgorithm::Sha256,
@@ -144,7 +130,6 @@ fn convert_totp(totp: Totp) -> TotpCredential {
 
 #[cfg(test)]
 mod tests {
-    use credential_exchange_types::format::FieldType;
 
     use super::*;
     use crate::{Fido2Credential, Field, LoginUri};
@@ -164,7 +149,7 @@ mod tests {
         assert_eq!(String::from(credential.secret), "ONSWG4TFOQ");
         assert_eq!(credential.period, 60);
         assert_eq!(credential.digits, 4);
-        assert_eq!(credential.username, "test-account@example.com");
+        assert_eq!(credential.username.unwrap(), "test-account@example.com");
         assert_eq!(credential.algorithm, OTPHashAlgorithm::Sha1);
         assert_eq!(credential.issuer, Some("test-issuer".to_string()));
     }
@@ -249,10 +234,13 @@ mod tests {
         assert_eq!(item.id.to_string(), "JcjEFLRGSOmhvbEHALvXQA");
         assert_eq!(item.creation_at, Some(1706613834));
         assert_eq!(item.modified_at, Some(1706623773));
-        assert_eq!(item.ty, ItemType::Login);
         assert_eq!(item.title, "Bitwarden");
         assert_eq!(item.subtitle, None);
         assert_eq!(item.tags, None);
+        assert_eq!(
+            item.scope.unwrap().urls,
+            vec!["https://vault.bitwarden.com".to_string()]
+        );
         assert!(item.extensions.is_none());
 
         assert_eq!(item.credentials.len(), 4);
@@ -262,19 +250,12 @@ mod tests {
         match credential {
             Credential::BasicAuth(basic_auth) => {
                 let username = basic_auth.username.as_ref().unwrap();
-                assert_eq!(username.field_type, FieldType::String);
-                assert_eq!(username.value, "test@bitwarden.com");
+                assert_eq!(username.value.0, "test@bitwarden.com");
                 assert!(username.label.is_none());
 
                 let password = basic_auth.password.as_ref().unwrap();
-                assert_eq!(password.field_type, FieldType::ConcealedString);
-                assert_eq!(password.value, "asdfasdfasdf");
+                assert_eq!(password.value.0, "asdfasdfasdf");
                 assert!(password.label.is_none());
-
-                assert_eq!(
-                    basic_auth.urls,
-                    vec!["https://vault.bitwarden.com".to_string()]
-                );
             }
             _ => panic!("Expected Credential::BasicAuth"),
         }
@@ -286,7 +267,7 @@ mod tests {
                 assert_eq!(String::from(totp.secret.clone()), "JBSWY3DPEHPK3PXP");
                 assert_eq!(totp.period, 30);
                 assert_eq!(totp.digits, 6);
-                assert_eq!(totp.username, "");
+                assert_eq!(totp.username, None);
                 assert_eq!(totp.algorithm, OTPHashAlgorithm::Sha1);
                 assert!(totp.issuer.is_none());
             }
@@ -299,7 +280,7 @@ mod tests {
             Credential::Passkey(passkey) => {
                 assert_eq!(passkey.credential_id.to_string(), "6NiHiekW4ZY8vYHa-ucbvA");
                 assert_eq!(passkey.rp_id, "123");
-                assert_eq!(passkey.user_name, "");
+                assert_eq!(passkey.username, "");
                 assert_eq!(passkey.user_display_name, "");
                 assert_eq!(String::from(passkey.user_handle.clone()), "AAECAwQFBg");
                 assert_eq!(String::from(passkey.key.clone()), "AAECAwQFBg");
@@ -312,7 +293,7 @@ mod tests {
 
         match credential {
             Credential::Note(n) => {
-                assert_eq!(n.content, "My note");
+                assert_eq!(n.content.value.0, "My note");
             }
             _ => panic!("Expected Credential::Passkey"),
         }
diff --git a/crates/bitwarden-exporters/src/cxf/import.rs b/crates/bitwarden-exporters/src/cxf/import.rs
index 3ffd6783e0..980ea94499 100644
--- a/crates/bitwarden-exporters/src/cxf/import.rs
+++ b/crates/bitwarden-exporters/src/cxf/import.rs
@@ -38,7 +38,7 @@ fn parse_item(value: Item) -> Vec<ImportingCipher> {
         let basic_auth = grouped.basic_auth.first();
         let passkey = grouped.passkey.first();
 
-        let login = to_login(creation_date, basic_auth, passkey);
+        let login = to_login(creation_date, basic_auth, passkey, value.scope);
 
         output.push(ImportingCipher {
             folder_id: None, // TODO: Handle folders
@@ -119,8 +119,8 @@ struct GroupedCredentials {
 #[cfg(test)]
 mod tests {
     use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
-    use chrono::Duration;
-    use credential_exchange_types::format::{CreditCardCredential, ItemType};
+    use chrono::{Duration, Month};
+    use credential_exchange_types::format::{CreditCardCredential, EditableFieldYearMonth};
 
     use super::*;
 
@@ -147,13 +147,13 @@ mod tests {
             id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
             creation_at: Some(1706613834),
             modified_at: Some(1706623773),
-            ty: ItemType::Login,
             title: "Bitwarden".to_string(),
             subtitle: None,
             favorite: None,
             credentials: vec![],
             tags: None,
             extensions: None,
+            scope: None,
         };
 
         let ciphers: Vec<ImportingCipher> = parse_item(item);
@@ -170,7 +170,6 @@ mod tests {
                 .into(),
             creation_at: Some(1732181986),
             modified_at: Some(1732182026),
-            ty: ItemType::Login,
             title: "opotonniee.github.io".to_string(),
             subtitle: None,
             favorite: None,
@@ -181,7 +180,7 @@ mod tests {
                     .as_slice()
                     .into(),
                 rp_id: "opotonniee.github.io".to_string(),
-                user_name: "alex muller".to_string(),
+                username: "alex muller".to_string(),
                 user_display_name: "alex muller".to_string(),
                 user_handle: URL_SAFE_NO_PAD
                     .decode("YWxleCBtdWxsZXI")
@@ -197,6 +196,7 @@ mod tests {
             }))],
             tags: None,
             extensions: None,
+            scope: None,
         };
 
         let ciphers: Vec<ImportingCipher> = parse_item(item);
@@ -247,20 +247,27 @@ mod tests {
             id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
             creation_at: Some(1706613834),
             modified_at: Some(1706623773),
-            ty: ItemType::Identity,
             title: "My MasterCard".to_string(),
             subtitle: None,
             favorite: None,
             credentials: vec![Credential::CreditCard(Box::new(CreditCardCredential {
-                number: "1234 5678 9012 3456".to_string(),
-                full_name: "John Doe".to_string(),
-                card_type: Some("MasterCard".to_string()),
-                verification_number: Some("123".to_string()),
-                expiry_date: Some("2026-01".to_string()),
+                number: Some("1234 5678 9012 3456".to_string().into()),
+                full_name: Some("John Doe".to_string().into()),
+                card_type: Some("MasterCard".to_string().into()),
+                verification_number: Some("123".to_string().into()),
+                pin: None,
+                expiry_date: Some(
+                    EditableFieldYearMonth {
+                        year: 2026,
+                        month: Month::January,
+                    }
+                    .into(),
+                ),
                 valid_from: None,
             }))],
             tags: None,
             extensions: None,
+            scope: None,
         };
 
         let ciphers: Vec<ImportingCipher> = parse_item(item);
@@ -276,7 +283,7 @@ mod tests {
         };
 
         assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
-        assert_eq!(card.exp_month, Some("01".to_string()));
+        assert_eq!(card.exp_month, Some("1".to_string()));
         assert_eq!(card.exp_year, Some("2026".to_string()));
         assert_eq!(card.code, Some("123".to_string()));
         assert_eq!(card.brand, Some("Mastercard".to_string()));
diff --git a/crates/bitwarden-exporters/src/cxf/login.rs b/crates/bitwarden-exporters/src/cxf/login.rs
index 8a6040b996..f4f6e44f79 100644
--- a/crates/bitwarden-exporters/src/cxf/login.rs
+++ b/crates/bitwarden-exporters/src/cxf/login.rs
@@ -5,33 +5,23 @@
 
 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
 use bitwarden_core::MissingFieldError;
-use bitwarden_crypto::generate_random_bytes;
 use bitwarden_fido::{string_to_guid_bytes, InvalidGuid};
 use chrono::{DateTime, Utc};
-use credential_exchange_types::{
-    format::{BasicAuthCredential, EditableField, FieldType, PasskeyCredential},
-    B64Url,
-};
+use credential_exchange_types::format::{BasicAuthCredential, CredentialScope, PasskeyCredential};
 use thiserror::Error;
 
 use crate::{Fido2Credential, Login, LoginUri};
 
-/// Generate a 32 byte random ID
-///
-/// TODO: This should be removed shortly.
-fn random_id() -> B64Url {
-    generate_random_bytes::<[u8; 32]>().as_slice().into()
-}
-
 pub(super) fn to_login(
     creation_date: DateTime<Utc>,
     basic_auth: Option<&BasicAuthCredential>,
     passkey: Option<&PasskeyCredential>,
+    scope: Option<CredentialScope>,
 ) -> Login {
     let login = Login {
-        username: basic_auth.and_then(|v| v.username.as_ref().map(|u| u.value.clone())),
-        password: basic_auth.and_then(|v| v.password.as_ref().map(|u| u.value.clone())),
-        login_uris: basic_auth
+        username: basic_auth.and_then(|v| v.username.clone().map(|v| v.into())),
+        password: basic_auth.and_then(|v| v.password.clone().map(|u| u.into())),
+        login_uris: scope
             .map(|v| {
                 v.urls
                     .iter()
@@ -52,7 +42,7 @@ pub(super) fn to_login(
                 key_value: URL_SAFE_NO_PAD.encode(&p.key),
                 rp_id: p.rp_id.clone(),
                 user_handle: Some(p.user_handle.to_string()),
-                user_name: Some(p.user_name.clone()),
+                user_name: Some(p.username.clone()),
                 counter: 0,
                 rp_name: Some(p.rp_id.clone()),
                 user_display_name: Some(p.user_display_name.clone()),
@@ -67,23 +57,17 @@ pub(super) fn to_login(
 impl From<Login> for BasicAuthCredential {
     fn from(login: Login) -> Self {
         BasicAuthCredential {
-            urls: login
-                .login_uris
-                .into_iter()
-                .flat_map(|uri| uri.uri)
-                .collect(),
-            username: login.username.map(|value| EditableField {
-                id: random_id(),
-                field_type: FieldType::String,
-                value,
-                label: None,
-            }),
-            password: login.password.map(|value| EditableField {
-                id: random_id(),
-                field_type: FieldType::ConcealedString,
-                value,
-                label: None,
-            }),
+            username: login.username.map(|v| v.into()),
+            password: login.password.map(|v| v.into()),
+        }
+    }
+}
+
+impl From<Login> for CredentialScope {
+    fn from(login: Login) -> Self {
+        CredentialScope {
+            urls: login.login_uris.into_iter().filter_map(|u| u.uri).collect(),
+            android_apps: vec![],
         }
     }
 }
@@ -113,7 +97,7 @@ impl TryFrom<Fido2Credential> for PasskeyCredential {
                 .map_err(PasskeyError::InvalidGuid)?
                 .into(),
             rp_id: value.rp_id,
-            user_name: value.user_name.unwrap_or_default(),
+            username: value.user_name.unwrap_or_default(),
             user_display_name: value.user_display_name.unwrap_or_default(),
             user_handle: value
                 .user_handle
@@ -148,19 +132,30 @@ mod tests {
         let basic_auth: BasicAuthCredential = login.into();
 
         let username = basic_auth.username.as_ref().unwrap();
-        assert_eq!(username.field_type, FieldType::String);
-        assert_eq!(username.value, "test@bitwarden.com");
+        assert_eq!(username.value.0, "test@bitwarden.com");
         assert!(username.label.is_none());
 
         let password = basic_auth.password.as_ref().unwrap();
-        assert_eq!(password.field_type, FieldType::ConcealedString);
-        assert_eq!(password.value, "asdfasdfasdf");
+        assert_eq!(password.value.0, "asdfasdfasdf");
         assert!(password.label.is_none());
+    }
+
+    #[test]
+    fn test_credential_scope() {
+        let login = Login {
+            username: None,
+            password: None,
+            login_uris: vec![LoginUri {
+                uri: Some("https://vault.bitwarden.com".to_string()),
+                r#match: None,
+            }],
+            totp: None,
+            fido2_credentials: None,
+        };
+
+        let scope: CredentialScope = login.into();
 
-        assert_eq!(
-            basic_auth.urls,
-            vec!["https://vault.bitwarden.com".to_string()]
-        );
+        assert_eq!(scope.urls, vec!["https://vault.bitwarden.com".to_string()]);
     }
 
     #[test]
@@ -185,7 +180,7 @@ mod tests {
 
         assert_eq!(passkey.credential_id.to_string(), "6NiHiekW4ZY8vYHa-ucbvA");
         assert_eq!(passkey.rp_id, "123");
-        assert_eq!(passkey.user_name, "");
+        assert_eq!(passkey.username, "");
         assert_eq!(passkey.user_display_name, "");
         assert_eq!(String::from(passkey.user_handle.clone()), "AAECAwQFBg");
         assert_eq!(String::from(passkey.key.clone()), "AAECAwQFBg");


Reviews:
user: login: dani-garcia
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-03T11:53:18Z
user: login: Hinton
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T16:59:48Z
user: login: dani-garcia
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T17:09:49Z
user: login: Hinton
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T17:23:45Z
user: login: dani-garcia
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-03T17:40:35Z

Review comments:
diff_hunk: @@ -46,41 +45,32 @@ impl TryFrom<Cipher> for Item {
         let mut credentials: Vec<Credential> = value.r#type.clone().into();
 
         if let Some(note) = value.notes {
-            credentials.push(Credential::Note(Box::new(NoteCredential { content: note })));
+            credentials.push(Credential::Note(Box::new(NoteCredential {
+                content: note.into(),
+            })));
         }
 
         Ok(Self {
             id: value.id.as_bytes().as_slice().into(),
             creation_at: Some(value.creation_date.timestamp() as u64),
             modified_at: Some(value.revision_date.timestamp() as u64),
-            ty: value.r#type.try_into()?,
             title: value.name,
             subtitle: None,
             favorite: Some(value.favorite),
             credentials,
             tags: None,
             extensions: None,
+            scope: match value.r#type {
+                CipherType::Login(login) => Some((*login).into()),
+                _ => Some(CredentialScope {
+                    urls: vec![],
path: crates/bitwarden-exporters/src/cxf/export.rs
user: login: dani-garcia
body: If this is a valid scope, we could consider having CredentialScope implement default in the `credential_exchange_types` crate
created_at: 2025-03-03T11:49:17Z
author_association: MEMBER
diff_hunk: @@ -67,23 +57,18 @@ pub(super) fn to_login(
 impl From<Login> for BasicAuthCredential {
     fn from(login: Login) -> Self {
         BasicAuthCredential {
-            urls: login
-                .login_uris
-                .into_iter()
-                .flat_map(|uri| uri.uri)
-                .collect(),
-            username: login.username.map(|value| EditableField {
-                id: random_id(),
-                field_type: FieldType::String,
-                value,
-                label: None,
-            }),
-            password: login.password.map(|value| EditableField {
-                id: random_id(),
-                field_type: FieldType::ConcealedString,
-                value,
-                label: None,
-            }),
+            urls: vec![],
path: crates/bitwarden-exporters/src/cxf/login.rs
user: login: dani-garcia
body: If I understand this correctly, the urls have been moved from the `BasicAuthCredential` to the `CredentialScope` right? If so, why is there a `urls` field in `BasicAuthCredential` still?
created_at: 2025-03-03T11:52:07Z
author_association: MEMBER
diff_hunk: @@ -46,41 +45,32 @@ impl TryFrom<Cipher> for Item {
         let mut credentials: Vec<Credential> = value.r#type.clone().into();
 
         if let Some(note) = value.notes {
-            credentials.push(Credential::Note(Box::new(NoteCredential { content: note })));
+            credentials.push(Credential::Note(Box::new(NoteCredential {
+                content: note.into(),
+            })));
         }
 
         Ok(Self {
             id: value.id.as_bytes().as_slice().into(),
             creation_at: Some(value.creation_date.timestamp() as u64),
             modified_at: Some(value.revision_date.timestamp() as u64),
-            ty: value.r#type.try_into()?,
             title: value.name,
             subtitle: None,
             favorite: Some(value.favorite),
             credentials,
             tags: None,
             extensions: None,
+            scope: match value.r#type {
+                CipherType::Login(login) => Some((*login).into()),
+                _ => Some(CredentialScope {
+                    urls: vec![],
path: crates/bitwarden-exporters/src/cxf/export.rs
user: login: Hinton
body: This was intended to be None but `CredentialScope` was required for a bit and I forgot to update it.
created_at: 2025-03-03T16:59:48Z
author_association: MEMBER
diff_hunk: @@ -147,13 +149,16 @@ mod tests {
             id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
             creation_at: Some(1706613834),
             modified_at: Some(1706623773),
-            ty: ItemType::Login,
             title: "Bitwarden".to_string(),
             subtitle: None,
             favorite: None,
             credentials: vec![],
             tags: None,
             extensions: None,
+            scope: Some(CredentialScope {
path: crates/bitwarden-exporters/src/cxf/import.rs
user: login: dani-garcia
body: Should this one also be None?

There's another two in this file, lines 204 and 278
created_at: 2025-03-03T17:09:49Z
author_association: MEMBER
diff_hunk: @@ -147,13 +149,16 @@ mod tests {
             id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
             creation_at: Some(1706613834),
             modified_at: Some(1706623773),
-            ty: ItemType::Login,
             title: "Bitwarden".to_string(),
             subtitle: None,
             favorite: None,
             credentials: vec![],
             tags: None,
             extensions: None,
+            scope: Some(CredentialScope {
path: crates/bitwarden-exporters/src/cxf/import.rs
user: login: Hinton
body: It should.
created_at: 2025-03-03T17:23:45Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-02-20T09:37:31Z
body: ![Logo](https://cdn.ast.checkmarx.net/integrations/logo/Checkmarx.png)
**Checkmarx One – Scan Summary & Details** – [136637db\-1c4d\-4159\-b83b\-46fd426aa094](https://ast.checkmarx.net/projects/e5b53f86-841a-43de-ae67-5c0d8a207f74/scans?id=136637db-1c4d-4159-b83b-46fd426aa094&branch=cxf-update)
### Great job, no security vulnerabilities found in this Pull Request

author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-02-20T09:43:12Z
body: ## [Codecov](https://app.codecov.io/gh/bitwarden/sdk-internal/pull/169?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) Report
Attention: Patch coverage is `96.80000%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 66.15%. Comparing base [(`a0b0d73`)](https://app.codecov.io/gh/bitwarden/sdk-internal/commit/a0b0d73555118165ad8b3ed34afda207f62222d7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) to head [(`a1a5a0c`)](https://app.codecov.io/gh/bitwarden/sdk-internal/commit/a1a5a0c213cf7f11a2af9289cbd133a3b413dd86?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/bitwarden/sdk-internal/pull/169?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) | Patch % | Lines |
|---|---|---|
| [crates/bitwarden-exporters/src/cxf/card.rs](https://app.codecov.io/gh/bitwarden/sdk-internal/pull/169?src=pr&el=tree&filepath=crates%2Fbitwarden-exporters%2Fsrc%2Fcxf%2Fcard.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden#diff-Y3JhdGVzL2JpdHdhcmRlbi1leHBvcnRlcnMvc3JjL2N4Zi9jYXJkLnJz) | 96.49% | [2 Missing :warning: ](https://app.codecov.io/gh/bitwarden/sdk-internal/pull/169?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) |
| [crates/bitwarden-exporters/src/cxf/export.rs](https://app.codecov.io/gh/bitwarden/sdk-internal/pull/169?src=pr&el=tree&filepath=crates%2Fbitwarden-exporters%2Fsrc%2Fcxf%2Fexport.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden#diff-Y3JhdGVzL2JpdHdhcmRlbi1leHBvcnRlcnMvc3JjL2N4Zi9leHBvcnQucnM=) | 88.88% | [2 Missing :warning: ](https://app.codecov.io/gh/bitwarden/sdk-internal/pull/169?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #169      +/-   ##
==========================================
+ Coverage   66.07%   66.15%   +0.07%     
==========================================
  Files         198      198              
  Lines       15402    15427      +25     
==========================================
+ Hits        10177    10205      +28     
+ Misses       5225     5222       -3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bitwarden/sdk-internal/pull/169?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).

author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-03T17:25:34Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=bitwarden_sdk-internal&pullRequest=169) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=bitwarden_sdk-internal&pullRequest=169&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=bitwarden_sdk-internal&pullRequest=169&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=bitwarden_sdk-internal&pullRequest=169&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=bitwarden_sdk-internal&pullRequest=169&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=bitwarden_sdk-internal&pullRequest=169&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=bitwarden_sdk-internal&pullRequest=169)


author_association: NONE