diff --git a/crates/bitwarden-exporters/src/cxp/error.rs b/crates/bitwarden-exporters/src/cxp/error.rs index dd76abf6..774a4ddb 100644 --- a/crates/bitwarden-exporters/src/cxp/error.rs +++ b/crates/bitwarden-exporters/src/cxp/error.rs @@ -1,7 +1,12 @@ +use std::borrow::Cow; + use thiserror::Error; #[derive(Error, Debug)] pub enum CxpError { #[error("JSON error: {0}")] Serde(#[from] serde_json::Error), + + #[error("Internal error: {0}")] + Internal(Cow<'static, str>), } diff --git a/crates/bitwarden-exporters/src/cxp/export.rs b/crates/bitwarden-exporters/src/cxp/export.rs index 90fa2489..073b53c5 100644 --- a/crates/bitwarden-exporters/src/cxp/export.rs +++ b/crates/bitwarden-exporters/src/cxp/export.rs @@ -16,6 +16,8 @@ use uuid::Uuid; use crate::{cxp::CxpError, Cipher, CipherType, Fido2Credential, Login}; /// Temporary struct to hold metadata related to current account +/// +/// Eventually the SDK itself should have this state and we get rid of this struct. #[derive(Debug)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct Account { @@ -24,8 +26,12 @@ pub struct Account { name: Option, } +/// Builds a Credential Exchange Format (CXF) payload pub(crate) fn build_cxf(account: Account, ciphers: Vec) -> Result { - let items: Vec = ciphers.into_iter().map(|cipher| cipher.into()).collect(); + let items: Vec = ciphers + .into_iter() + .flat_map(|cipher| cipher.try_into()) + .collect(); let account = CxpAccount { id: account.id.as_bytes().as_slice().into(), @@ -33,7 +39,7 @@ pub(crate) fn build_cxf(account: Account, ciphers: Vec) -> Result) -> Result for Item { - fn from(value: Cipher) -> Self { +impl TryFrom for Item { + type Error = CxpError; + + fn try_from(value: Cipher) -> Result { let mut credentials: Vec = value.r#type.clone().into(); if let Some(note) = value.notes { credentials.push(Credential::Note { content: note }); } - Self { + Ok(Self { id: value.id.as_bytes().as_slice().into(), creation_at: value.creation_date.timestamp() as u64, modified_at: value.revision_date.timestamp() as u64, - ty: value.r#type.into(), + ty: value.r#type.try_into()?, title: value.name, subtitle: None, favorite: Some(value.favorite), credentials, tags: None, extensions: None, - } + }) } } -impl From for ItemType { - // TODO: We should probably change this to try_from, so we can ignore types - fn from(value: CipherType) -> Self { +impl TryFrom for ItemType { + type Error = CxpError; + + fn try_from(value: CipherType) -> Result { match value { - CipherType::Login(_) => ItemType::Login, - CipherType::Card(_) => ItemType::Login, - CipherType::Identity(_) => ItemType::Identity, - CipherType::SecureNote(_) => ItemType::Document, - CipherType::SshKey(_) => todo!(), + CipherType::Login(_) => Ok(ItemType::Login), + CipherType::Card(_) => Ok(ItemType::Login), + CipherType::Identity(_) => Ok(ItemType::Identity), + CipherType::SecureNote(_) => Ok(ItemType::Document), + CipherType::SshKey(_) => { + // TODO(PM-15448): Add support for SSH Keys + Err(CxpError::Internal("Unsupported CipherType: SshKey".into())) + } } } } @@ -81,9 +93,13 @@ impl From for Vec { fn from(value: CipherType) -> Self { match value { CipherType::Login(login) => (*login).into(), + // TODO(PM-15450): Add support for credit cards. CipherType::Card(_) => vec![], + // TODO(PM-15451): Add support for identities. CipherType::Identity(_) => vec![], + // Secure Notes only contains a note field which is handled by `TryFrom for Item`. CipherType::SecureNote(_) => vec![], + // TODO(PM-15448): Add support for SSH Keys. CipherType::SshKey(_) => vec![], } } @@ -281,7 +297,7 @@ mod tests { deleted_date: None, }; - let item: Item = cipher.into(); + let item: Item = cipher.try_into().unwrap(); assert_eq!( item.creation_at, diff --git a/crates/bitwarden-exporters/src/cxp/import.rs b/crates/bitwarden-exporters/src/cxp/import.rs index 43c557b4..bdb7ca3f 100644 --- a/crates/bitwarden-exporters/src/cxp/import.rs +++ b/crates/bitwarden-exporters/src/cxp/import.rs @@ -119,7 +119,12 @@ mod tests { extensions: None, }; - let _ciphers: Vec = parse_item(item); + let ciphers: Vec = parse_item(item); + assert_eq!(ciphers.len(), 1); + let cipher = ciphers.first().unwrap(); + + assert_eq!(cipher.folder_id, None); + assert_eq!(cipher.name, "Bitwarden"); } #[test] @@ -162,26 +167,5 @@ mod tests { }; let _ciphers: Vec = parse_item(item); - - /* - { - "id": "Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF", - "title": "opotonniee.github.io", - "modifiedAt": 1732182026, - "type": "login", - "credentials": [ - { - "key": "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl", - "userName": "alex muller", - "userHandle": "YWxleCBtdWxsZXI", - "credentialID": "6NiHiekW4ZY8vYHa-ucbvA", - "userDisplayName": "alex muller", - "rpID": "opotonniee.github.io", - "type": "passkey" - } - ], - "creationAt": 1732181986 - }, - */ } }