From 19bd2104c7ecb941dd3ad1719307781e23fbf4ab Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 5 Sep 2025 11:38:05 +0200 Subject: [PATCH 1/3] Implement missing to view --- crates/bitwarden-exporters/src/lib.rs | 283 ++++++++++++++++++++++- crates/bitwarden-exporters/src/models.rs | 8 + 2 files changed, 281 insertions(+), 10 deletions(-) diff --git a/crates/bitwarden-exporters/src/lib.rs b/crates/bitwarden-exporters/src/lib.rs index bfb792c0d..0a6a9b5b3 100644 --- a/crates/bitwarden-exporters/src/lib.rs +++ b/crates/bitwarden-exporters/src/lib.rs @@ -99,7 +99,7 @@ pub struct ImportingCipher { impl From for CipherView { fn from(value: ImportingCipher) -> Self { - let login = match value.r#type { + let (cipher_type, login, identity, card, secure_note, ssh_key) = match value.r#type { CipherType::Login(login) => { let l: Vec = login .login_uris @@ -107,7 +107,7 @@ impl From for CipherView { .map(LoginUriView::from) .collect(); - Some(bitwarden_vault::LoginView { + let login_view = bitwarden_vault::LoginView { username: login.username, password: login.password, password_revision_date: None, @@ -115,9 +115,92 @@ impl From for CipherView { totp: login.totp, autofill_on_page_load: None, fido2_credentials: None, - }) + }; + ( + bitwarden_vault::CipherType::Login, + Some(login_view), + None, + None, + None, + None, + ) + } + CipherType::SecureNote(secure_note) => { + let secure_note_view = bitwarden_vault::SecureNoteView { + r#type: secure_note.r#type.into(), + }; + ( + bitwarden_vault::CipherType::SecureNote, + None, + None, + None, + Some(secure_note_view), + None, + ) + } + CipherType::Card(card) => { + let card_view = bitwarden_vault::CardView { + cardholder_name: card.cardholder_name, + brand: card.brand, + number: card.number, + exp_month: card.exp_month, + exp_year: card.exp_year, + code: card.code, + }; + ( + bitwarden_vault::CipherType::Card, + None, + None, + Some(card_view), + None, + None, + ) + } + CipherType::Identity(identity) => { + let identity_view = bitwarden_vault::IdentityView { + title: identity.title, + first_name: identity.first_name, + middle_name: identity.middle_name, + last_name: identity.last_name, + address1: identity.address1, + address2: identity.address2, + address3: identity.address3, + city: identity.city, + state: identity.state, + postal_code: identity.postal_code, + country: identity.country, + company: identity.company, + email: identity.email, + phone: identity.phone, + ssn: identity.ssn, + username: identity.username, + passport_number: identity.passport_number, + license_number: identity.license_number, + }; + ( + bitwarden_vault::CipherType::Identity, + None, + Some(identity_view), + None, + None, + None, + ) + } + CipherType::SshKey(ssh_key) => { + let ssh_key_view = bitwarden_vault::SshKeyView { + private_key: ssh_key.private_key, + public_key: ssh_key.public_key, + fingerprint: ssh_key.fingerprint, + }; + ( + bitwarden_vault::CipherType::SshKey, + None, + None, + None, + None, + Some(ssh_key_view), + ) } - _ => None, }; Self { @@ -127,13 +210,13 @@ impl From for CipherView { collection_ids: vec![], key: None, name: value.name, - notes: None, - r#type: bitwarden_vault::CipherType::Login, + notes: value.notes, + r#type: cipher_type, login, - identity: None, - card: None, - secure_note: None, - ssh_key: None, + identity, + card, + secure_note, + ssh_key, favorite: value.favorite, reprompt: CipherRepromptType::None, organization_use_totp: true, @@ -312,3 +395,183 @@ pub struct SshKey { /// SSH fingerprint using SHA256 in the format: `SHA256:BASE64_ENCODED_FINGERPRINT` pub fingerprint: String, } + +#[cfg(test)] +mod tests { + use bitwarden_vault::CipherType as VaultCipherType; + use chrono::{DateTime, Utc}; + + use super::*; + + #[test] + fn test_importing_cipher_to_cipher_view_login() { + let test_date: DateTime = "2024-01-30T17:55:36.150Z".parse().unwrap(); + let test_folder_id = uuid::Uuid::new_v4(); + + let importing_cipher = ImportingCipher { + folder_id: Some(test_folder_id), + name: "Test Login".to_string(), + notes: Some("Test notes".to_string()), + r#type: CipherType::Login(Box::new(Login { + username: Some("test@example.com".to_string()), + password: Some("password123".to_string()), + login_uris: vec![LoginUri { + uri: Some("https://example.com".to_string()), + r#match: Some(0), // Domain match + }], + totp: Some("otpauth://totp/test".to_string()), + fido2_credentials: None, + })), + favorite: true, + reprompt: 1, + fields: vec![], + revision_date: test_date, + creation_date: test_date, + deleted_date: None, + }; + + let cipher_view: CipherView = importing_cipher.into(); + + assert_eq!(cipher_view.id, None); + assert_eq!(cipher_view.organization_id, None); + assert_eq!( + cipher_view.folder_id.unwrap().to_string(), + test_folder_id.to_string() + ); + assert_eq!(cipher_view.name, "Test Login"); + assert_eq!(cipher_view.notes.unwrap(), "Test notes"); + assert_eq!(cipher_view.r#type, VaultCipherType::Login); + assert!(cipher_view.favorite); + assert_eq!(cipher_view.creation_date, test_date); + assert_eq!(cipher_view.revision_date, test_date); + + let login = cipher_view.login.expect("Login should be present"); + assert_eq!(login.username, Some("test@example.com".to_string())); + assert_eq!(login.password, Some("password123".to_string())); + assert_eq!(login.totp, Some("otpauth://totp/test".to_string())); + + let uris = login.uris.expect("URIs should be present"); + assert_eq!(uris.len(), 1); + assert_eq!(uris[0].uri, Some("https://example.com".to_string())); + assert_eq!(uris[0].r#match, Some(bitwarden_vault::UriMatchType::Domain)); + } + + #[test] + fn test_importing_cipher_to_cipher_view_secure_note() { + let test_date: DateTime = "2024-01-30T17:55:36.150Z".parse().unwrap(); + + let importing_cipher = ImportingCipher { + folder_id: None, + name: "My Note".to_string(), + notes: Some("This is a secure note".to_string()), + r#type: CipherType::SecureNote(Box::new(SecureNote { + r#type: SecureNoteType::Generic, + })), + favorite: false, + reprompt: 0, + fields: vec![], + revision_date: test_date, + creation_date: test_date, + deleted_date: None, + }; + + let cipher_view: CipherView = importing_cipher.into(); + + // Verify basic fields + assert_eq!(cipher_view.id, None); + assert_eq!(cipher_view.organization_id, None); + assert_eq!(cipher_view.folder_id, None); + assert_eq!(cipher_view.name, "My Note"); + assert_eq!(cipher_view.notes, Some("This is a secure note".to_string())); + assert_eq!(cipher_view.r#type, bitwarden_vault::CipherType::SecureNote); + assert!(!cipher_view.favorite); + assert_eq!(cipher_view.creation_date, test_date); + assert_eq!(cipher_view.revision_date, test_date); + + // For SecureNote type, secure_note should be populated and others should be None + assert!(cipher_view.login.is_none()); + assert!(cipher_view.identity.is_none()); + assert!(cipher_view.card.is_none()); + assert!(cipher_view.secure_note.is_some()); + assert!(cipher_view.ssh_key.is_none()); + + // Verify the secure note content + let secure_note = cipher_view.secure_note.unwrap(); + assert!(matches!( + secure_note.r#type, + bitwarden_vault::SecureNoteType::Generic + )); + } + + #[test] + fn test_importing_cipher_to_cipher_view_card() { + let test_date: DateTime = "2024-01-30T17:55:36.150Z".parse().unwrap(); + + let importing_cipher = ImportingCipher { + folder_id: None, + name: "My Credit Card".to_string(), + notes: Some("Credit card notes".to_string()), + r#type: CipherType::Card(Box::new(Card { + cardholder_name: Some("John Doe".to_string()), + brand: Some("Visa".to_string()), + number: Some("1234567812345678".to_string()), + exp_month: Some("12".to_string()), + exp_year: Some("2025".to_string()), + code: Some("123".to_string()), + })), + favorite: false, + reprompt: 0, + fields: vec![], + revision_date: test_date, + creation_date: test_date, + deleted_date: None, + }; + + let cipher_view: CipherView = importing_cipher.into(); + + assert_eq!(cipher_view.r#type, bitwarden_vault::CipherType::Card); + assert!(cipher_view.card.is_some()); + assert!(cipher_view.login.is_none()); + + let card = cipher_view.card.unwrap(); + assert_eq!(card.cardholder_name, Some("John Doe".to_string())); + assert_eq!(card.brand, Some("Visa".to_string())); + assert_eq!(card.number, Some("1234567812345678".to_string())); + } + + #[test] + fn test_importing_cipher_to_cipher_view_identity() { + let test_date: DateTime = "2024-01-30T17:55:36.150Z".parse().unwrap(); + + let importing_cipher = ImportingCipher { + folder_id: None, + name: "My Identity".to_string(), + notes: None, + r#type: CipherType::Identity(Box::new(Identity { + title: Some("Dr.".to_string()), + first_name: Some("Jane".to_string()), + last_name: Some("Smith".to_string()), + email: Some("jane@example.com".to_string()), + ..Default::default() + })), + favorite: false, + reprompt: 0, + fields: vec![], + revision_date: test_date, + creation_date: test_date, + deleted_date: None, + }; + + let cipher_view: CipherView = importing_cipher.into(); + + assert_eq!(cipher_view.r#type, bitwarden_vault::CipherType::Identity); + assert!(cipher_view.identity.is_some()); + assert!(cipher_view.login.is_none()); + + let identity = cipher_view.identity.unwrap(); + assert_eq!(identity.title, Some("Dr.".to_string())); + assert_eq!(identity.first_name, Some("Jane".to_string())); + assert_eq!(identity.last_name, Some("Smith".to_string())); + assert_eq!(identity.email, Some("jane@example.com".to_string())); + } +} diff --git a/crates/bitwarden-exporters/src/models.rs b/crates/bitwarden-exporters/src/models.rs index 3bc3a85c4..feaf78ede 100644 --- a/crates/bitwarden-exporters/src/models.rs +++ b/crates/bitwarden-exporters/src/models.rs @@ -196,6 +196,14 @@ impl From for crate::SecureNoteType { } } +impl From for SecureNoteType { + fn from(value: crate::SecureNoteType) -> Self { + match value { + crate::SecureNoteType::Generic => SecureNoteType::Generic, + } + } +} + #[cfg(test)] mod tests { use bitwarden_core::key_management::create_test_crypto_with_user_key; From 85ac0a760697eeef26498e684c551221697729d5 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 5 Sep 2025 11:48:49 +0200 Subject: [PATCH 2/3] Refactor --- crates/bitwarden-exporters/src/lib.rs | 217 ++++++++++++++------------ 1 file changed, 116 insertions(+), 101 deletions(-) diff --git a/crates/bitwarden-exporters/src/lib.rs b/crates/bitwarden-exporters/src/lib.rs index 0a6a9b5b3..0f5c741c6 100644 --- a/crates/bitwarden-exporters/src/lib.rs +++ b/crates/bitwarden-exporters/src/lib.rs @@ -97,110 +97,125 @@ pub struct ImportingCipher { pub deleted_date: Option>, } +impl From for bitwarden_vault::LoginView { + fn from(login: Login) -> Self { + let l: Vec = login + .login_uris + .into_iter() + .map(LoginUriView::from) + .collect(); + + bitwarden_vault::LoginView { + username: login.username, + password: login.password, + password_revision_date: None, + uris: if l.is_empty() { None } else { Some(l) }, + totp: login.totp, + autofill_on_page_load: None, + fido2_credentials: None, + } + } +} + +impl From for bitwarden_vault::SecureNoteView { + fn from(secure_note: SecureNote) -> Self { + bitwarden_vault::SecureNoteView { + r#type: secure_note.r#type.into(), + } + } +} + +impl From for bitwarden_vault::CardView { + fn from(card: Card) -> Self { + bitwarden_vault::CardView { + cardholder_name: card.cardholder_name, + brand: card.brand, + number: card.number, + exp_month: card.exp_month, + exp_year: card.exp_year, + code: card.code, + } + } +} + +impl From for bitwarden_vault::IdentityView { + fn from(identity: Identity) -> Self { + bitwarden_vault::IdentityView { + title: identity.title, + first_name: identity.first_name, + middle_name: identity.middle_name, + last_name: identity.last_name, + address1: identity.address1, + address2: identity.address2, + address3: identity.address3, + city: identity.city, + state: identity.state, + postal_code: identity.postal_code, + country: identity.country, + company: identity.company, + email: identity.email, + phone: identity.phone, + ssn: identity.ssn, + username: identity.username, + passport_number: identity.passport_number, + license_number: identity.license_number, + } + } +} + +impl From for bitwarden_vault::SshKeyView { + fn from(ssh_key: SshKey) -> Self { + bitwarden_vault::SshKeyView { + private_key: ssh_key.private_key, + public_key: ssh_key.public_key, + fingerprint: ssh_key.fingerprint, + } + } +} + impl From for CipherView { fn from(value: ImportingCipher) -> Self { let (cipher_type, login, identity, card, secure_note, ssh_key) = match value.r#type { - CipherType::Login(login) => { - let l: Vec = login - .login_uris - .into_iter() - .map(LoginUriView::from) - .collect(); - - let login_view = bitwarden_vault::LoginView { - username: login.username, - password: login.password, - password_revision_date: None, - uris: if l.is_empty() { None } else { Some(l) }, - totp: login.totp, - autofill_on_page_load: None, - fido2_credentials: None, - }; - ( - bitwarden_vault::CipherType::Login, - Some(login_view), - None, - None, - None, - None, - ) - } - CipherType::SecureNote(secure_note) => { - let secure_note_view = bitwarden_vault::SecureNoteView { - r#type: secure_note.r#type.into(), - }; - ( - bitwarden_vault::CipherType::SecureNote, - None, - None, - None, - Some(secure_note_view), - None, - ) - } - CipherType::Card(card) => { - let card_view = bitwarden_vault::CardView { - cardholder_name: card.cardholder_name, - brand: card.brand, - number: card.number, - exp_month: card.exp_month, - exp_year: card.exp_year, - code: card.code, - }; - ( - bitwarden_vault::CipherType::Card, - None, - None, - Some(card_view), - None, - None, - ) - } - CipherType::Identity(identity) => { - let identity_view = bitwarden_vault::IdentityView { - title: identity.title, - first_name: identity.first_name, - middle_name: identity.middle_name, - last_name: identity.last_name, - address1: identity.address1, - address2: identity.address2, - address3: identity.address3, - city: identity.city, - state: identity.state, - postal_code: identity.postal_code, - country: identity.country, - company: identity.company, - email: identity.email, - phone: identity.phone, - ssn: identity.ssn, - username: identity.username, - passport_number: identity.passport_number, - license_number: identity.license_number, - }; - ( - bitwarden_vault::CipherType::Identity, - None, - Some(identity_view), - None, - None, - None, - ) - } - CipherType::SshKey(ssh_key) => { - let ssh_key_view = bitwarden_vault::SshKeyView { - private_key: ssh_key.private_key, - public_key: ssh_key.public_key, - fingerprint: ssh_key.fingerprint, - }; - ( - bitwarden_vault::CipherType::SshKey, - None, - None, - None, - None, - Some(ssh_key_view), - ) - } + CipherType::Login(login) => ( + bitwarden_vault::CipherType::Login, + Some((*login).into()), + None, + None, + None, + None, + ), + CipherType::SecureNote(secure_note) => ( + bitwarden_vault::CipherType::SecureNote, + None, + None, + None, + Some((*secure_note).into()), + None, + ), + CipherType::Card(card) => ( + bitwarden_vault::CipherType::Card, + None, + None, + Some((*card).into()), + None, + None, + ), + CipherType::Identity(identity) => ( + bitwarden_vault::CipherType::Identity, + None, + Some((*identity).into()), + None, + None, + None, + ), + CipherType::SshKey(ssh_key) => ( + bitwarden_vault::CipherType::SshKey, + None, + None, + None, + None, + Some((*ssh_key).into()), + ), }; Self { From c5bc481940fc0c0bda3fb9bc27893653efa5a2e2 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 5 Sep 2025 11:50:21 +0200 Subject: [PATCH 3/3] Add a comment to fido2 credentials --- crates/bitwarden-exporters/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitwarden-exporters/src/lib.rs b/crates/bitwarden-exporters/src/lib.rs index 0f5c741c6..5894fd1e6 100644 --- a/crates/bitwarden-exporters/src/lib.rs +++ b/crates/bitwarden-exporters/src/lib.rs @@ -112,6 +112,7 @@ impl From for bitwarden_vault::LoginView { uris: if l.is_empty() { None } else { Some(l) }, totp: login.totp, autofill_on_page_load: None, + // Fido2Credentials are set by `encrypt_import`. fido2_credentials: None, } }