From a673f09b31978e7c80a5095ab9bafdb65a1bcb07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Fri, 15 Mar 2024 15:26:05 +0100 Subject: [PATCH] Add support for URI checksums --- .../src/mobile/vault/client_ciphers.rs | 12 ++- crates/bitwarden/src/vault/cipher/cipher.rs | 14 ++++ crates/bitwarden/src/vault/cipher/login.rs | 83 +++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden/src/mobile/vault/client_ciphers.rs b/crates/bitwarden/src/mobile/vault/client_ciphers.rs index c35cf3080..bf7d36f24 100644 --- a/crates/bitwarden/src/mobile/vault/client_ciphers.rs +++ b/crates/bitwarden/src/mobile/vault/client_ciphers.rs @@ -24,6 +24,11 @@ impl<'a> ClientCiphers<'a> { cipher_view.generate_cipher_key(key)?; } + // For compatibility reasons, we only create checksums for ciphers that have a key + if cipher_view.key.is_some() { + cipher_view.generate_checksums(); + } + let cipher = cipher_view.encrypt(enc, &None)?; Ok(cipher) @@ -32,7 +37,12 @@ impl<'a> ClientCiphers<'a> { pub async fn decrypt(&self, cipher: Cipher) -> Result { let enc = self.client.get_encryption_settings()?; - let cipher_view = cipher.decrypt(enc, &None)?; + let mut cipher_view: CipherView = cipher.decrypt(enc, &None)?; + + // For compatibility we only remove URLs with invalid checksums if the cipher has a key + if cipher_view.key.is_some() { + cipher_view.remove_invalid_checksums(); + } Ok(cipher_view) } diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index 47b58694d..040aa6cf3 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -299,6 +299,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=" + ); + } +}