Skip to main content

bitwarden_vault/cipher/
bank_account.rs

1use bitwarden_api_api::models::CipherBankAccountModel;
2use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
3use bitwarden_crypto::{
4    CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext,
5    PrimitiveEncryptable,
6};
7use serde::{Deserialize, Serialize};
8#[cfg(feature = "wasm")]
9use tsify::Tsify;
10
11use super::cipher::CipherKind;
12use crate::{Cipher, VaultParseError, cipher::cipher::CopyableCipherFields};
13
14#[derive(Serialize, Deserialize, Debug, Clone)]
15#[serde(rename_all = "camelCase")]
16#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
17#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
18pub struct BankAccount {
19    pub bank_name: Option<EncString>,
20    pub name_on_account: Option<EncString>,
21    pub account_type: Option<EncString>,
22    pub account_number: Option<EncString>,
23    pub routing_number: Option<EncString>,
24    pub branch_number: Option<EncString>,
25    pub pin: Option<EncString>,
26    pub swift_code: Option<EncString>,
27    pub iban: Option<EncString>,
28    pub bank_contact_phone: Option<EncString>,
29}
30
31#[allow(missing_docs)]
32#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
33#[serde(rename_all = "camelCase", deny_unknown_fields)]
34#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
35#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
36pub struct BankAccountView {
37    pub bank_name: Option<String>,
38    pub name_on_account: Option<String>,
39    pub account_type: Option<String>,
40    pub account_number: Option<String>,
41    pub routing_number: Option<String>,
42    pub branch_number: Option<String>,
43    pub pin: Option<String>,
44    pub swift_code: Option<String>,
45    pub iban: Option<String>,
46    pub bank_contact_phone: Option<String>,
47}
48
49impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, BankAccount> for BankAccountView {
50    fn encrypt_composite(
51        &self,
52        ctx: &mut KeyStoreContext<KeySlotIds>,
53        key: SymmetricKeySlotId,
54    ) -> Result<BankAccount, CryptoError> {
55        Ok(BankAccount {
56            bank_name: self.bank_name.encrypt(ctx, key)?,
57            name_on_account: self.name_on_account.encrypt(ctx, key)?,
58            account_type: self.account_type.encrypt(ctx, key)?,
59            account_number: self.account_number.encrypt(ctx, key)?,
60            routing_number: self.routing_number.encrypt(ctx, key)?,
61            branch_number: self.branch_number.encrypt(ctx, key)?,
62            pin: self.pin.encrypt(ctx, key)?,
63            swift_code: self.swift_code.encrypt(ctx, key)?,
64            iban: self.iban.encrypt(ctx, key)?,
65            bank_contact_phone: self.bank_contact_phone.encrypt(ctx, key)?,
66        })
67    }
68}
69
70impl Decryptable<KeySlotIds, SymmetricKeySlotId, BankAccountView> for BankAccount {
71    fn decrypt(
72        &self,
73        ctx: &mut KeyStoreContext<KeySlotIds>,
74        key: SymmetricKeySlotId,
75    ) -> Result<BankAccountView, CryptoError> {
76        Ok(BankAccountView {
77            bank_name: self.bank_name.decrypt(ctx, key).ok().flatten(),
78            name_on_account: self.name_on_account.decrypt(ctx, key).ok().flatten(),
79            account_type: self.account_type.decrypt(ctx, key).ok().flatten(),
80            account_number: self.account_number.decrypt(ctx, key).ok().flatten(),
81            routing_number: self.routing_number.decrypt(ctx, key).ok().flatten(),
82            branch_number: self.branch_number.decrypt(ctx, key).ok().flatten(),
83            pin: self.pin.decrypt(ctx, key).ok().flatten(),
84            swift_code: self.swift_code.decrypt(ctx, key).ok().flatten(),
85            iban: self.iban.decrypt(ctx, key).ok().flatten(),
86            bank_contact_phone: self.bank_contact_phone.decrypt(ctx, key).ok().flatten(),
87        })
88    }
89}
90
91impl CipherKind for BankAccount {
92    fn decrypt_subtitle(
93        &self,
94        ctx: &mut KeyStoreContext<KeySlotIds>,
95        key: SymmetricKeySlotId,
96    ) -> Result<String, CryptoError> {
97        let bank_name = self
98            .bank_name
99            .as_ref()
100            .map(|b| b.decrypt(ctx, key))
101            .transpose()?;
102        Ok(bank_name.unwrap_or_default())
103    }
104
105    fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
106        [
107            self.name_on_account
108                .as_ref()
109                .map(|_| CopyableCipherFields::BankAccountNameOnAccount),
110            self.account_number
111                .as_ref()
112                .map(|_| CopyableCipherFields::BankAccountAccountNumber),
113            self.routing_number
114                .as_ref()
115                .map(|_| CopyableCipherFields::BankAccountRoutingNumber),
116            self.branch_number
117                .as_ref()
118                .map(|_| CopyableCipherFields::BankAccountBranchNumber),
119            self.pin
120                .as_ref()
121                .map(|_| CopyableCipherFields::BankAccountPin),
122            self.iban
123                .as_ref()
124                .map(|_| CopyableCipherFields::BankAccountIban),
125            self.swift_code
126                .as_ref()
127                .map(|_| CopyableCipherFields::BankAccountSwift),
128        ]
129        .into_iter()
130        .flatten()
131        .collect()
132    }
133}
134
135impl TryFrom<CipherBankAccountModel> for BankAccount {
136    type Error = VaultParseError;
137
138    fn try_from(bank_account: CipherBankAccountModel) -> Result<Self, Self::Error> {
139        Ok(Self {
140            bank_name: EncString::try_from_optional(bank_account.bank_name)?,
141            name_on_account: EncString::try_from_optional(bank_account.name_on_account)?,
142            account_type: EncString::try_from_optional(bank_account.account_type)?,
143            account_number: EncString::try_from_optional(bank_account.account_number)?,
144            routing_number: EncString::try_from_optional(bank_account.routing_number)?,
145            branch_number: EncString::try_from_optional(bank_account.branch_number)?,
146            pin: EncString::try_from_optional(bank_account.pin)?,
147            swift_code: EncString::try_from_optional(bank_account.swift_code)?,
148            iban: EncString::try_from_optional(bank_account.iban)?,
149            bank_contact_phone: EncString::try_from_optional(bank_account.bank_contact_phone)?,
150        })
151    }
152}
153
154impl From<BankAccount> for CipherBankAccountModel {
155    fn from(bank_account: BankAccount) -> Self {
156        Self {
157            bank_name: bank_account.bank_name.map(|n| n.to_string()),
158            name_on_account: bank_account.name_on_account.map(|n| n.to_string()),
159            account_type: bank_account.account_type.map(|n| n.to_string()),
160            account_number: bank_account.account_number.map(|n| n.to_string()),
161            routing_number: bank_account.routing_number.map(|n| n.to_string()),
162            branch_number: bank_account.branch_number.map(|n| n.to_string()),
163            pin: bank_account.pin.map(|n| n.to_string()),
164            swift_code: bank_account.swift_code.map(|n| n.to_string()),
165            iban: bank_account.iban.map(|n| n.to_string()),
166            bank_contact_phone: bank_account.bank_contact_phone.map(|n| n.to_string()),
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use bitwarden_core::key_management::create_test_crypto_with_user_key;
174    use bitwarden_crypto::SymmetricCryptoKey;
175
176    use super::*;
177    use crate::cipher::cipher::CopyableCipherFields;
178
179    const TEST_VECTOR_BANK_KEY: &str =
180        "87OtUxruEdWbBBltgpTWDvfEN2SkB47dlsolBL9VTzQPCb5Y2lssAf6IsX4sbLrCHnsefRg8Ytl/mO448St8vg==";
181    const TEST_VECTOR_BANK_JSON: &str = r#"{"bankName":"2.eqcWQjGiNAVqUAf5RJvRFQ==|YM9+btP60yWBbVssawdeAA==|BZLLWJoD8ADuc38jbxQssJhiUkQBe8ch62jpY/AUwDM=","nameOnAccount":"2.j7P0TX6M5kUcQQTqBG/20Q==|I2mGyc88/VhW0bBws4ZDJg==|g8COyKMEP7XKUQbFr3Rv77QrAfmpTss2U4LBxTs13AQ=","accountType":"2.lBE1RRgEkOvu0q5v1JceRg==|S30rS63nMW5RWS8ezjjKFg==|PA0mDwAq8R6xDkb/PEFCdDvzlsvBMWRDFmV6s6Vp5cc=","accountNumber":"2.Ug7T5mTCpdFFnRJ/lA3rxg==|P0fXXwA5TwoM7KNi9Pb0Tw==|EGrQpm9ijZLfS1DIhgeCObmi2hjSN7xRjmhv43BdFhY=","routingNumber":"2.PaY/GIqJBPCbW4qWCn2XdA==|CHca6ALtCXYSpaKgGWnTCQ==|Xzp3oG5VkPkAoZnPbO+x5GOlBdw8Lif14vdh4WImoIw=","branchNumber":"2.91kPZnZCtEh7BZ0Lt4x9mQ==|L7CrT5suvLlI33EHnaNblA==|eO4449EVHBgZ0Suf+TRQ0BqQQa0nNTYZZBwj2GBsP3o=","pin":"2.JVEfmyd2PCTGhxz55riZ8g==|LyhlwYJnI2crRG5XFU8AhA==|SG+QGCbbJ471eIgieZxhD8IxxGoKR029lrfJq0V8BI8=","swiftCode":"2.Bv7iudCC9csI6oRgN7Xf0w==|2kiKO0AfaeVfcnDaH17rGA==|XETLOedxHsIcmrfKN+z1hJOElILYEUisCPQtJziUDBs=","iban":"2.QV8nLQgAA9rrGyA49kzqCw==|LjZxJYUTzRogkMRb+vTns11yMuGXPvWVIvDN+Bqz074=|6AFItuC2kJolnXY6e02MtDd3Q6kD8+i/Pkrep8djqnw=","bankContactPhone":"2.bS2uYl/iIKXO1esIFEvzog==|mBLhybB2I6+5j1/tRB6lDA==|eBDl337l2AcDcL1HDFqJ+NwP+IEVNGhYqUsQsT4zilY="}"#;
182
183    fn test_bank_account_view() -> BankAccountView {
184        BankAccountView {
185            bank_name: Some("Test Bank".to_string()),
186            name_on_account: Some("John Doe".to_string()),
187            account_type: Some("Checking".to_string()),
188            account_number: Some("1234567890".to_string()),
189            routing_number: Some("021000021".to_string()),
190            branch_number: Some("001".to_string()),
191            pin: Some("1234".to_string()),
192            swift_code: Some("TESTUS33".to_string()),
193            iban: Some("US12345678901234567890".to_string()),
194            bank_contact_phone: Some("555-0123".to_string()),
195        }
196    }
197
198    #[test]
199    #[ignore]
200    fn generate_test_vector() {
201        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
202        let key_b64 = key.to_base64();
203        let key_store = create_test_crypto_with_user_key(key);
204        let key_slot = SymmetricKeySlotId::User;
205        let mut ctx = key_store.context();
206
207        let encrypted = test_bank_account_view()
208            .encrypt_composite(&mut ctx, key_slot)
209            .unwrap();
210        let json = serde_json::to_string(&encrypted).unwrap();
211
212        println!("const TEST_VECTOR_BANK_KEY: &str = \"{key_b64}\";");
213        println!("const TEST_VECTOR_BANK_JSON: &str = r#\"{json}\"#;");
214    }
215
216    #[test]
217    fn test_recorded_bank_account_test_vector() {
218        let key =
219            SymmetricCryptoKey::try_from(TEST_VECTOR_BANK_KEY.to_string()).expect("valid test key");
220        let key_store = create_test_crypto_with_user_key(key);
221        let key_slot = SymmetricKeySlotId::User;
222        let mut ctx = key_store.context();
223
224        let encrypted: BankAccount =
225            serde_json::from_str(TEST_VECTOR_BANK_JSON).expect("valid test vector JSON");
226        let decrypted: BankAccountView = encrypted
227            .decrypt(&mut ctx, key_slot)
228            .expect("BankAccount has changed in a backwards-incompatible way. Existing encrypted data must remain decryptable. If a new format is needed, create a new version instead of modifying the existing one.");
229
230        assert_eq!(decrypted, test_bank_account_view());
231    }
232
233    #[test]
234    fn test_subtitle_bank_account() {
235        let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
236        let key_store = create_test_crypto_with_user_key(key);
237        let key = SymmetricKeySlotId::User;
238        let mut ctx = key_store.context();
239
240        let original_subtitle = "My Bank".to_string();
241        let bank_name_encrypted = original_subtitle.to_owned().encrypt(&mut ctx, key).unwrap();
242
243        let bank_account = BankAccount {
244            bank_name: Some(bank_name_encrypted),
245            name_on_account: None,
246            account_type: None,
247            account_number: None,
248            routing_number: None,
249            branch_number: None,
250            pin: None,
251            swift_code: None,
252            iban: None,
253            bank_contact_phone: None,
254        };
255
256        assert_eq!(
257            bank_account.decrypt_subtitle(&mut ctx, key).unwrap(),
258            original_subtitle
259        );
260    }
261
262    #[test]
263    fn test_get_copyable_fields_bank_account() {
264        let enc_str: EncString = "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap();
265
266        let bank_account = BankAccount {
267            bank_name: Some(enc_str.clone()),
268            name_on_account: Some(enc_str.clone()),
269            account_type: Some(enc_str.clone()),
270            account_number: Some(enc_str.clone()),
271            routing_number: Some(enc_str.clone()),
272            branch_number: Some(enc_str.clone()),
273            pin: Some(enc_str.clone()),
274            swift_code: Some(enc_str.clone()),
275            iban: Some(enc_str.clone()),
276            bank_contact_phone: Some(enc_str),
277        };
278
279        let copyable_fields = bank_account.get_copyable_fields(None);
280        assert_eq!(
281            copyable_fields,
282            vec![
283                CopyableCipherFields::BankAccountNameOnAccount,
284                CopyableCipherFields::BankAccountAccountNumber,
285                CopyableCipherFields::BankAccountRoutingNumber,
286                CopyableCipherFields::BankAccountBranchNumber,
287                CopyableCipherFields::BankAccountPin,
288                CopyableCipherFields::BankAccountIban,
289                CopyableCipherFields::BankAccountSwift,
290            ]
291        );
292    }
293
294    #[test]
295    fn test_get_copyable_fields_bank_account_partial() {
296        let enc_str: EncString = "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap();
297
298        let bank_account = BankAccount {
299            bank_name: None,
300            name_on_account: None,
301            account_type: None,
302            account_number: Some(enc_str.clone()),
303            routing_number: None,
304            branch_number: None,
305            pin: Some(enc_str),
306            swift_code: None,
307            iban: None,
308            bank_contact_phone: None,
309        };
310
311        let copyable_fields = bank_account.get_copyable_fields(None);
312        assert_eq!(
313            copyable_fields,
314            vec![
315                CopyableCipherFields::BankAccountAccountNumber,
316                CopyableCipherFields::BankAccountPin,
317            ]
318        );
319    }
320}