diff --git a/crates/bitwarden-exporters/src/lib.rs b/crates/bitwarden-exporters/src/lib.rs index 9cf2ccf40..89e4094ab 100644 --- a/crates/bitwarden-exporters/src/lib.rs +++ b/crates/bitwarden-exporters/src/lib.rs @@ -98,27 +98,126 @@ 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, + // Fido2Credentials are set by `encrypt_import`. + 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 login = match value.r#type { - CipherType::Login(login) => { - let l: Vec = login - .login_uris - .into_iter() - .map(LoginUriView::from) - .collect(); - - Some(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, - }) - } - _ => None, + let (cipher_type, login, identity, card, secure_note, ssh_key) = match value.r#type { + 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 { @@ -128,13 +227,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, @@ -320,3 +419,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;