Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion crates/bitwarden-vault/src/cipher/bank_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
use tsify::Tsify;

use super::cipher::CipherKind;
use super::cipher::{CipherKind, StrictDecrypt};
use crate::{Cipher, VaultParseError, cipher::cipher::CopyableCipherFields};

#[derive(Serialize, Deserialize, Debug, Clone)]
Expand Down Expand Up @@ -46,6 +46,18 @@ pub struct BankAccountView {
pub bank_contact_phone: Option<String>,
}

/// Minimal BankAccountView only including the needed details for list views
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct BankAccountListView {
/// The account number of the bank account.
pub account_number: Option<String>,
/// The type of the bank account, e.g. Checking, Savings, etc.
pub account_type: Option<String>,
}

impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, BankAccount> for BankAccountView {
fn encrypt_composite(
&self,
Expand Down Expand Up @@ -88,6 +100,34 @@ impl Decryptable<KeySlotIds, SymmetricKeySlotId, BankAccountView> for BankAccoun
}
}

impl Decryptable<KeySlotIds, SymmetricKeySlotId, BankAccountListView> for BankAccount {
fn decrypt(
&self,
ctx: &mut KeyStoreContext<KeySlotIds>,
key: SymmetricKeySlotId,
) -> Result<BankAccountListView, CryptoError> {
Ok(BankAccountListView {
account_number: self.account_number.decrypt(ctx, key).ok().flatten(),
account_type: self.account_type.decrypt(ctx, key).ok().flatten(),
})
}
}

impl Decryptable<KeySlotIds, SymmetricKeySlotId, BankAccountListView>
for StrictDecrypt<&BankAccount>
{
fn decrypt(
&self,
ctx: &mut KeyStoreContext<KeySlotIds>,
key: SymmetricKeySlotId,
) -> Result<BankAccountListView, CryptoError> {
Ok(BankAccountListView {
account_number: self.0.account_number.decrypt(ctx, key)?,
account_type: self.0.account_type.decrypt(ctx, key)?,
})
}
}

impl CipherKind for BankAccount {
fn decrypt_subtitle(
&self,
Expand Down Expand Up @@ -230,6 +270,52 @@ mod tests {
assert_eq!(decrypted, test_bank_account_view());
}

#[test]
fn test_bank_account_list_view() {
let key =
SymmetricCryptoKey::try_from(TEST_VECTOR_BANK_KEY.to_string()).expect("valid test key");
let key_store = create_test_crypto_with_user_key(key);
let key_slot = SymmetricKeySlotId::User;
let mut ctx = key_store.context();

let encrypted: BankAccount =
serde_json::from_str(TEST_VECTOR_BANK_JSON).expect("valid test vector JSON");
let list_view: BankAccountListView = encrypted
.decrypt(&mut ctx, key_slot)
.expect("BankAccount should decrypt to a list view");

assert_eq!(
list_view,
BankAccountListView {
account_number: Some("1234567890".to_string()),
account_type: Some("Checking".to_string()),
}
);
}

#[test]
fn test_bank_account_list_view_strict() {
let key =
SymmetricCryptoKey::try_from(TEST_VECTOR_BANK_KEY.to_string()).expect("valid test key");
let key_store = create_test_crypto_with_user_key(key);
let key_slot = SymmetricKeySlotId::User;
let mut ctx = key_store.context();

let encrypted: BankAccount =
serde_json::from_str(TEST_VECTOR_BANK_JSON).expect("valid test vector JSON");
let list_view: BankAccountListView = StrictDecrypt(&encrypted)
.decrypt(&mut ctx, key_slot)
.expect("BankAccount should strictly decrypt to a list view");

assert_eq!(
list_view,
BankAccountListView {
account_number: Some("1234567890".to_string()),
account_type: Some("Checking".to_string()),
}
);
}

#[test]
fn test_subtitle_bank_account() {
let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
Expand Down
49 changes: 43 additions & 6 deletions crates/bitwarden-vault/src/cipher/cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use wasm_bindgen::prelude::wasm_bindgen;

use super::{
attachment, bank_account,
bank_account::BankAccountListView,
blob::{decrypt_blob_cipher, encrypt_blob_cipher, try_parse_blob},
card,
card::CardListView,
Expand Down Expand Up @@ -484,7 +485,7 @@ pub enum CipherListViewType {
Card(CardListView),
Identity,
SshKey,
BankAccount,
BankAccount(BankAccountListView),
Passport,
DriversLicense,
}
Expand Down Expand Up @@ -1119,7 +1120,16 @@ impl CipherView {
}
CipherType::Identity => CipherListViewType::Identity,
CipherType::SshKey => CipherListViewType::SshKey,
CipherType::BankAccount => CipherListViewType::BankAccount,
CipherType::BankAccount => {
let bank_account = self
.bank_account
.as_ref()
.ok_or(CryptoError::MissingField("bank_account"))?;
CipherListViewType::BankAccount(BankAccountListView {
account_number: bank_account.account_number.clone(),
account_type: bank_account.account_type.clone(),
})
}
CipherType::DriversLicense => CipherListViewType::DriversLicense,
CipherType::Passport => CipherListViewType::Passport,
};
Expand Down Expand Up @@ -1208,13 +1218,20 @@ impl CipherView {
drivers_license::build_subtitle_drivers_license(
d.first_name.clone(),
d.last_name.clone(),
d.issuing_state.clone(),
)
})
.unwrap_or_default(),
CipherType::Passport => self
.passport
.as_ref()
.map(|p| passport::build_subtitle_passport(p.given_name.clone(), p.surname.clone()))
.map(|p| {
passport::build_subtitle_passport(
p.given_name.clone(),
p.surname.clone(),
p.issuing_country.clone(),
)
})
.unwrap_or_default(),
}
}
Expand Down Expand Up @@ -1414,7 +1431,13 @@ pub(crate) fn lenient_decrypt_cipher_list_view(
}
CipherType::Identity => CipherListViewType::Identity,
CipherType::SshKey => CipherListViewType::SshKey,
CipherType::BankAccount => CipherListViewType::BankAccount,
CipherType::BankAccount => {
let bank_account = cipher
.bank_account
.as_ref()
.ok_or(CryptoError::MissingField("bank_account"))?;
CipherListViewType::BankAccount(bank_account.decrypt(ctx, ciphers_key)?)
}
CipherType::Passport => CipherListViewType::Passport,
CipherType::DriversLicense => CipherListViewType::DriversLicense,
},
Expand Down Expand Up @@ -1686,7 +1709,15 @@ fn strict_decrypt_cipher_list_view(
}
CipherType::Identity => CipherListViewType::Identity,
CipherType::SshKey => CipherListViewType::SshKey,
CipherType::BankAccount => CipherListViewType::BankAccount,
CipherType::BankAccount => {
let bank_account = cipher
.bank_account
.as_ref()
.ok_or(CryptoError::MissingField("bank_account"))?;
CipherListViewType::BankAccount(
StrictDecrypt(bank_account).decrypt(ctx, ciphers_key)?,
)
}
CipherType::Passport => CipherListViewType::Passport,
CipherType::DriversLicense => CipherListViewType::DriversLicense,
},
Expand Down Expand Up @@ -3844,7 +3875,13 @@ mod tests {
let list_view = decrypt_blob_list_view(&key_store, view);
assert_eq!(list_view.name, "My Bank Account");
assert_eq!(list_view.subtitle, "Some Bank");
assert!(matches!(list_view.r#type, CipherListViewType::BankAccount));
assert_eq!(
list_view.r#type,
CipherListViewType::BankAccount(BankAccountListView {
account_number: Some("123456".to_string()),
account_type: None,
})
);
assert_eq!(
list_view.copyable_fields,
vec![
Expand Down
121 changes: 114 additions & 7 deletions crates/bitwarden-vault/src/cipher/drivers_license.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,16 @@ impl CipherKind for DriversLicense {
.as_ref()
.map(|l| l.decrypt(ctx, key))
.transpose()?;
Ok(build_subtitle_drivers_license(first_name, last_name))
let issuing_state: Option<String> = self
.issuing_state
.as_ref()
.map(|l| l.decrypt(ctx, key))
.transpose()?;
Ok(build_subtitle_drivers_license(
first_name,
last_name,
issuing_state,
))
}

fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
Expand Down Expand Up @@ -136,13 +145,28 @@ impl CipherKind for DriversLicense {
pub(super) fn build_subtitle_drivers_license(
first_name: Option<String>,
last_name: Option<String>,
issuing_state: Option<String>,
) -> String {
[first_name, last_name]
.into_iter()
.flatten()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
let mut subtitle = String::new();

if let Some(first_name) = first_name {
subtitle.push_str(&first_name);
}
if let Some(last_name) = last_name {
if !subtitle.is_empty() && !last_name.is_empty() {
subtitle.push(' ');
}
subtitle.push_str(&last_name);
}

if let Some(issuing_state) = issuing_state {
if !subtitle.is_empty() && !issuing_state.is_empty() {
subtitle.push_str(", ");
}
subtitle.push_str(&issuing_state);
}
Comment thread
jengstrom-bw marked this conversation as resolved.

subtitle
}

impl TryFrom<CipherDriversLicenseModel> for DriversLicense {
Expand Down Expand Up @@ -276,6 +300,89 @@ mod tests {
);
}

#[test]
fn test_subtitle_drivers_license_with_issuing_state() {
let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
let key_store = create_test_crypto_with_user_key(key);
let key = SymmetricKeySlotId::User;
let mut ctx = key_store.context();

let first_name_encrypted = "John".to_owned().encrypt(&mut ctx, key).unwrap();
let last_name_encrypted = "Doe".to_owned().encrypt(&mut ctx, key).unwrap();
let issuing_state_encrypted = "NY".to_owned().encrypt(&mut ctx, key).unwrap();

let dl = DriversLicense {
first_name: Some(first_name_encrypted),
middle_name: None,
last_name: Some(last_name_encrypted),
date_of_birth: None,
license_number: None,
issuing_country: None,
issuing_state: Some(issuing_state_encrypted),
issue_date: None,
expiration_date: None,
issuing_authority: None,
license_class: None,
};

assert_eq!(
dl.decrypt_subtitle(&mut ctx, key).unwrap(),
"John Doe, NY".to_string()
);
}

#[test]
fn test_build_subtitle_drivers_license() {
// All fields present
assert_eq!(
build_subtitle_drivers_license(
Some("John".to_string()),
Some("Doe".to_string()),
Some("NY".to_string()),
),
"John Doe, NY"
);

// Names only, no issuing state
assert_eq!(
build_subtitle_drivers_license(Some("John".to_string()), Some("Doe".to_string()), None),
"John Doe"
);

// Issuing state only
assert_eq!(
build_subtitle_drivers_license(None, None, Some("NY".to_string())),
"NY"
);

// Last name and issuing state, no first name
assert_eq!(
build_subtitle_drivers_license(None, Some("Doe".to_string()), Some("NY".to_string())),
"Doe, NY"
);

// Empty strings are treated as absent for separators
assert_eq!(
build_subtitle_drivers_license(
Some("".to_string()),
Some("".to_string()),
Some("NY".to_string()),
),
"NY"
);
assert_eq!(
build_subtitle_drivers_license(
Some("John".to_string()),
Some("".to_string()),
Some("NY".to_string()),
),
"John, NY"
);

// Nothing present
assert_eq!(build_subtitle_drivers_license(None, None, None), "");
}

#[test]
fn test_get_copyable_fields_drivers_license() {
let enc_str: EncString = "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap();
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-vault/src/cipher/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub use attachment_client::{
CipherRenewFileUploadUrlError, CipherUpgradeAttachmentError, CreateAttachmentRequest,
CreatedAttachment, DecryptFileError, DeleteAttachmentAdminError, EncryptFileError,
};
pub use bank_account::BankAccountView;
pub use bank_account::{BankAccountListView, BankAccountView};
pub use blob::{BlobEncryptionError, SealedCipherBlobError};
pub use card::{CardBrand, CardListView, CardView};
pub use cipher::{
Expand Down
Loading
Loading