diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index 47b58694d..f0138beb9 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -139,10 +139,15 @@ pub struct CipherListView { } impl KeyEncryptable for CipherView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { + fn encrypt_with_key(mut self, key: &SymmetricCryptoKey) -> Result { let ciphers_key = Cipher::get_cipher_key(key, &self.key)?; let key = ciphers_key.as_ref().unwrap_or(key); + // For compatibility reasons, we only create checksums for ciphers that have a key + if ciphers_key.is_some() { + self.generate_checksums(); + } + Ok(Cipher { id: self.id, organization_id: self.organization_id, @@ -177,7 +182,7 @@ impl KeyDecryptable for Cipher { let ciphers_key = Cipher::get_cipher_key(key, &self.key)?; let key = ciphers_key.as_ref().unwrap_or(key); - Ok(CipherView { + let mut cipher = CipherView { id: self.id, organization_id: self.organization_id, folder_id: self.folder_id, @@ -202,7 +207,14 @@ impl KeyDecryptable for Cipher { creation_date: self.creation_date, deleted_date: self.deleted_date, revision_date: self.revision_date, - }) + }; + + // For compatibility we only remove URLs with invalid checksums if the cipher has a key + if ciphers_key.is_some() { + cipher.remove_invalid_checksums(); + } + + Ok(cipher) } } @@ -299,6 +311,20 @@ impl CipherView { self.key = Some(new_key.to_vec().encrypt_with_key(key)?); Ok(()) } + + pub fn generate_checksums(&mut self) { + if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) { + for uri in uris { + uri.generate_checksum(); + } + } + } + + pub fn remove_invalid_checksums(&mut self) { + if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) { + uris.retain(|u| u.is_checksum_valid()); + } + } } impl KeyDecryptable for Cipher { diff --git a/crates/bitwarden/src/vault/cipher/login.rs b/crates/bitwarden/src/vault/cipher/login.rs index 5d00c41e1..26fd59001 100644 --- a/crates/bitwarden/src/vault/cipher/login.rs +++ b/crates/bitwarden/src/vault/cipher/login.rs @@ -1,3 +1,4 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel}; use bitwarden_crypto::{ CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, @@ -28,6 +29,7 @@ pub enum UriMatchType { pub struct LoginUri { pub uri: Option, pub r#match: Option, + pub uri_checksum: Option, } #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -36,6 +38,35 @@ pub struct LoginUri { pub struct LoginUriView { pub uri: Option, pub r#match: Option, + pub uri_checksum: Option, +} + +impl LoginUriView { + pub(crate) fn is_checksum_valid(&self) -> bool { + let Some(uri) = &self.uri else { + return false; + }; + let Some(cs) = &self.uri_checksum else { + return false; + }; + let Ok(cs) = STANDARD.decode(cs) else { + return false; + }; + + use sha2::Digest; + let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize(); + + uri_hash.as_slice() == cs + } + + pub(crate) fn generate_checksum(&mut self) { + if let Some(uri) = &self.uri { + use sha2::Digest; + let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize(); + let uri_hash = STANDARD.encode(uri_hash.as_slice()); + self.uri_checksum = Some(uri_hash); + } + } } #[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] @@ -93,6 +124,7 @@ impl KeyEncryptable for LoginUriView { Ok(LoginUri { uri: self.uri.encrypt_with_key(key)?, r#match: self.r#match, + uri_checksum: self.uri_checksum.encrypt_with_key(key)?, }) } } @@ -116,6 +148,7 @@ impl KeyDecryptable for LoginUri { Ok(LoginUriView { uri: self.uri.decrypt_with_key(key)?, r#match: self.r#match, + uri_checksum: self.uri_checksum.decrypt_with_key(key)?, }) } } @@ -166,6 +199,7 @@ impl TryFrom for LoginUri { Ok(Self { uri: EncString::try_from_optional(uri.uri)?, r#match: uri.r#match.map(|m| m.into()), + uri_checksum: EncString::try_from_optional(uri.uri_checksum)?, }) } } @@ -208,3 +242,52 @@ impl TryFrom for Fido2Cre }) } } + +#[cfg(test)] +mod tests { + #[test] + fn test_valid_checksum() { + let uri = super::LoginUriView { + uri: Some("https://example.com".to_string()), + r#match: Some(super::UriMatchType::Domain), + uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()), + }; + assert!(uri.is_checksum_valid()); + } + + #[test] + fn test_invalid_checksum() { + let uri = super::LoginUriView { + uri: Some("https://example.com".to_string()), + r#match: Some(super::UriMatchType::Domain), + uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()), + }; + assert!(!uri.is_checksum_valid()); + } + + #[test] + fn test_missing_checksum() { + let uri = super::LoginUriView { + uri: Some("https://example.com".to_string()), + r#match: Some(super::UriMatchType::Domain), + uri_checksum: None, + }; + assert!(!uri.is_checksum_valid()); + } + + #[test] + fn test_generate_checksum() { + let mut uri = super::LoginUriView { + uri: Some("https://test.com".to_string()), + r#match: Some(super::UriMatchType::Domain), + uri_checksum: None, + }; + + uri.generate_checksum(); + + assert_eq!( + uri.uri_checksum.unwrap().as_str(), + "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w=" + ); + } +}