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, StrictDecrypt};
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
49/// Minimal BankAccountView only including the needed details for list views
50#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
51#[serde(rename_all = "camelCase", deny_unknown_fields)]
52#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
53#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
54pub struct BankAccountListView {
55    /// The account number of the bank account.
56    pub account_number: Option<String>,
57    /// The type of the bank account, e.g. Checking, Savings, etc.
58    pub account_type: Option<String>,
59}
60
61impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, BankAccount> for BankAccountView {
62    fn encrypt_composite(
63        &self,
64        ctx: &mut KeyStoreContext<KeySlotIds>,
65        key: SymmetricKeySlotId,
66    ) -> Result<BankAccount, CryptoError> {
67        Ok(BankAccount {
68            bank_name: self.bank_name.encrypt(ctx, key)?,
69            name_on_account: self.name_on_account.encrypt(ctx, key)?,
70            account_type: self.account_type.encrypt(ctx, key)?,
71            account_number: self.account_number.encrypt(ctx, key)?,
72            routing_number: self.routing_number.encrypt(ctx, key)?,
73            branch_number: self.branch_number.encrypt(ctx, key)?,
74            pin: self.pin.encrypt(ctx, key)?,
75            swift_code: self.swift_code.encrypt(ctx, key)?,
76            iban: self.iban.encrypt(ctx, key)?,
77            bank_contact_phone: self.bank_contact_phone.encrypt(ctx, key)?,
78        })
79    }
80}
81
82impl Decryptable<KeySlotIds, SymmetricKeySlotId, BankAccountView> for BankAccount {
83    fn decrypt(
84        &self,
85        ctx: &mut KeyStoreContext<KeySlotIds>,
86        key: SymmetricKeySlotId,
87    ) -> Result<BankAccountView, CryptoError> {
88        Ok(BankAccountView {
89            bank_name: self.bank_name.decrypt(ctx, key).ok().flatten(),
90            name_on_account: self.name_on_account.decrypt(ctx, key).ok().flatten(),
91            account_type: self.account_type.decrypt(ctx, key).ok().flatten(),
92            account_number: self.account_number.decrypt(ctx, key).ok().flatten(),
93            routing_number: self.routing_number.decrypt(ctx, key).ok().flatten(),
94            branch_number: self.branch_number.decrypt(ctx, key).ok().flatten(),
95            pin: self.pin.decrypt(ctx, key).ok().flatten(),
96            swift_code: self.swift_code.decrypt(ctx, key).ok().flatten(),
97            iban: self.iban.decrypt(ctx, key).ok().flatten(),
98            bank_contact_phone: self.bank_contact_phone.decrypt(ctx, key).ok().flatten(),
99        })
100    }
101}
102
103impl Decryptable<KeySlotIds, SymmetricKeySlotId, BankAccountListView> for BankAccount {
104    fn decrypt(
105        &self,
106        ctx: &mut KeyStoreContext<KeySlotIds>,
107        key: SymmetricKeySlotId,
108    ) -> Result<BankAccountListView, CryptoError> {
109        Ok(BankAccountListView {
110            account_number: self.account_number.decrypt(ctx, key).ok().flatten(),
111            account_type: self.account_type.decrypt(ctx, key).ok().flatten(),
112        })
113    }
114}
115
116impl Decryptable<KeySlotIds, SymmetricKeySlotId, BankAccountListView>
117    for StrictDecrypt<&BankAccount>
118{
119    fn decrypt(
120        &self,
121        ctx: &mut KeyStoreContext<KeySlotIds>,
122        key: SymmetricKeySlotId,
123    ) -> Result<BankAccountListView, CryptoError> {
124        Ok(BankAccountListView {
125            account_number: self.0.account_number.decrypt(ctx, key)?,
126            account_type: self.0.account_type.decrypt(ctx, key)?,
127        })
128    }
129}
130
131impl CipherKind for BankAccount {
132    fn decrypt_subtitle(
133        &self,
134        ctx: &mut KeyStoreContext<KeySlotIds>,
135        key: SymmetricKeySlotId,
136    ) -> Result<String, CryptoError> {
137        let bank_name = self
138            .bank_name
139            .as_ref()
140            .map(|b| b.decrypt(ctx, key))
141            .transpose()?;
142        Ok(bank_name.unwrap_or_default())
143    }
144
145    fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
146        [
147            self.name_on_account
148                .as_ref()
149                .map(|_| CopyableCipherFields::BankAccountNameOnAccount),
150            self.account_number
151                .as_ref()
152                .map(|_| CopyableCipherFields::BankAccountAccountNumber),
153            self.routing_number
154                .as_ref()
155                .map(|_| CopyableCipherFields::BankAccountRoutingNumber),
156            self.branch_number
157                .as_ref()
158                .map(|_| CopyableCipherFields::BankAccountBranchNumber),
159            self.pin
160                .as_ref()
161                .map(|_| CopyableCipherFields::BankAccountPin),
162            self.iban
163                .as_ref()
164                .map(|_| CopyableCipherFields::BankAccountIban),
165            self.swift_code
166                .as_ref()
167                .map(|_| CopyableCipherFields::BankAccountSwift),
168        ]
169        .into_iter()
170        .flatten()
171        .collect()
172    }
173}
174
175impl TryFrom<CipherBankAccountModel> for BankAccount {
176    type Error = VaultParseError;
177
178    fn try_from(bank_account: CipherBankAccountModel) -> Result<Self, Self::Error> {
179        Ok(Self {
180            bank_name: EncString::try_from_optional(bank_account.bank_name)?,
181            name_on_account: EncString::try_from_optional(bank_account.name_on_account)?,
182            account_type: EncString::try_from_optional(bank_account.account_type)?,
183            account_number: EncString::try_from_optional(bank_account.account_number)?,
184            routing_number: EncString::try_from_optional(bank_account.routing_number)?,
185            branch_number: EncString::try_from_optional(bank_account.branch_number)?,
186            pin: EncString::try_from_optional(bank_account.pin)?,
187            swift_code: EncString::try_from_optional(bank_account.swift_code)?,
188            iban: EncString::try_from_optional(bank_account.iban)?,
189            bank_contact_phone: EncString::try_from_optional(bank_account.bank_contact_phone)?,
190        })
191    }
192}
193
194impl From<BankAccount> for CipherBankAccountModel {
195    fn from(bank_account: BankAccount) -> Self {
196        Self {
197            bank_name: bank_account.bank_name.map(|n| n.to_string()),
198            name_on_account: bank_account.name_on_account.map(|n| n.to_string()),
199            account_type: bank_account.account_type.map(|n| n.to_string()),
200            account_number: bank_account.account_number.map(|n| n.to_string()),
201            routing_number: bank_account.routing_number.map(|n| n.to_string()),
202            branch_number: bank_account.branch_number.map(|n| n.to_string()),
203            pin: bank_account.pin.map(|n| n.to_string()),
204            swift_code: bank_account.swift_code.map(|n| n.to_string()),
205            iban: bank_account.iban.map(|n| n.to_string()),
206            bank_contact_phone: bank_account.bank_contact_phone.map(|n| n.to_string()),
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use bitwarden_core::key_management::create_test_crypto_with_user_key;
214    use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
215
216    use super::*;
217    use crate::cipher::cipher::CopyableCipherFields;
218
219    const TEST_VECTOR_BANK_KEY: &str =
220        "87OtUxruEdWbBBltgpTWDvfEN2SkB47dlsolBL9VTzQPCb5Y2lssAf6IsX4sbLrCHnsefRg8Ytl/mO448St8vg==";
221    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="}"#;
222
223    fn test_bank_account_view() -> BankAccountView {
224        BankAccountView {
225            bank_name: Some("Test Bank".to_string()),
226            name_on_account: Some("John Doe".to_string()),
227            account_type: Some("Checking".to_string()),
228            account_number: Some("1234567890".to_string()),
229            routing_number: Some("021000021".to_string()),
230            branch_number: Some("001".to_string()),
231            pin: Some("1234".to_string()),
232            swift_code: Some("TESTUS33".to_string()),
233            iban: Some("US12345678901234567890".to_string()),
234            bank_contact_phone: Some("555-0123".to_string()),
235        }
236    }
237
238    #[test]
239    #[ignore]
240    fn generate_test_vector() {
241        let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
242        let key_b64 = key.to_base64();
243        let key_store = create_test_crypto_with_user_key(key);
244        let key_slot = SymmetricKeySlotId::User;
245        let mut ctx = key_store.context();
246
247        let encrypted = test_bank_account_view()
248            .encrypt_composite(&mut ctx, key_slot)
249            .unwrap();
250        let json = serde_json::to_string(&encrypted).unwrap();
251
252        println!("const TEST_VECTOR_BANK_KEY: &str = \"{key_b64}\";");
253        println!("const TEST_VECTOR_BANK_JSON: &str = r#\"{json}\"#;");
254    }
255
256    #[test]
257    fn test_recorded_bank_account_test_vector() {
258        let key =
259            SymmetricCryptoKey::try_from(TEST_VECTOR_BANK_KEY.to_string()).expect("valid test key");
260        let key_store = create_test_crypto_with_user_key(key);
261        let key_slot = SymmetricKeySlotId::User;
262        let mut ctx = key_store.context();
263
264        let encrypted: BankAccount =
265            serde_json::from_str(TEST_VECTOR_BANK_JSON).expect("valid test vector JSON");
266        let decrypted: BankAccountView = encrypted
267            .decrypt(&mut ctx, key_slot)
268            .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.");
269
270        assert_eq!(decrypted, test_bank_account_view());
271    }
272
273    #[test]
274    fn test_bank_account_list_view() {
275        let key =
276            SymmetricCryptoKey::try_from(TEST_VECTOR_BANK_KEY.to_string()).expect("valid test key");
277        let key_store = create_test_crypto_with_user_key(key);
278        let key_slot = SymmetricKeySlotId::User;
279        let mut ctx = key_store.context();
280
281        let encrypted: BankAccount =
282            serde_json::from_str(TEST_VECTOR_BANK_JSON).expect("valid test vector JSON");
283        let list_view: BankAccountListView = encrypted
284            .decrypt(&mut ctx, key_slot)
285            .expect("BankAccount should decrypt to a list view");
286
287        assert_eq!(
288            list_view,
289            BankAccountListView {
290                account_number: Some("1234567890".to_string()),
291                account_type: Some("Checking".to_string()),
292            }
293        );
294    }
295
296    #[test]
297    fn test_bank_account_list_view_strict() {
298        let key =
299            SymmetricCryptoKey::try_from(TEST_VECTOR_BANK_KEY.to_string()).expect("valid test key");
300        let key_store = create_test_crypto_with_user_key(key);
301        let key_slot = SymmetricKeySlotId::User;
302        let mut ctx = key_store.context();
303
304        let encrypted: BankAccount =
305            serde_json::from_str(TEST_VECTOR_BANK_JSON).expect("valid test vector JSON");
306        let list_view: BankAccountListView = StrictDecrypt(&encrypted)
307            .decrypt(&mut ctx, key_slot)
308            .expect("BankAccount should strictly decrypt to a list view");
309
310        assert_eq!(
311            list_view,
312            BankAccountListView {
313                account_number: Some("1234567890".to_string()),
314                account_type: Some("Checking".to_string()),
315            }
316        );
317    }
318
319    #[test]
320    fn test_subtitle_bank_account() {
321        let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
322        let key_store = create_test_crypto_with_user_key(key);
323        let key = SymmetricKeySlotId::User;
324        let mut ctx = key_store.context();
325
326        let original_subtitle = "My Bank".to_string();
327        let bank_name_encrypted = original_subtitle.to_owned().encrypt(&mut ctx, key).unwrap();
328
329        let bank_account = BankAccount {
330            bank_name: Some(bank_name_encrypted),
331            name_on_account: None,
332            account_type: None,
333            account_number: None,
334            routing_number: None,
335            branch_number: None,
336            pin: None,
337            swift_code: None,
338            iban: None,
339            bank_contact_phone: None,
340        };
341
342        assert_eq!(
343            bank_account.decrypt_subtitle(&mut ctx, key).unwrap(),
344            original_subtitle
345        );
346    }
347
348    #[test]
349    fn test_get_copyable_fields_bank_account() {
350        let enc_str: EncString = "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap();
351
352        let bank_account = BankAccount {
353            bank_name: Some(enc_str.clone()),
354            name_on_account: Some(enc_str.clone()),
355            account_type: Some(enc_str.clone()),
356            account_number: Some(enc_str.clone()),
357            routing_number: Some(enc_str.clone()),
358            branch_number: Some(enc_str.clone()),
359            pin: Some(enc_str.clone()),
360            swift_code: Some(enc_str.clone()),
361            iban: Some(enc_str.clone()),
362            bank_contact_phone: Some(enc_str),
363        };
364
365        let copyable_fields = bank_account.get_copyable_fields(None);
366        assert_eq!(
367            copyable_fields,
368            vec![
369                CopyableCipherFields::BankAccountNameOnAccount,
370                CopyableCipherFields::BankAccountAccountNumber,
371                CopyableCipherFields::BankAccountRoutingNumber,
372                CopyableCipherFields::BankAccountBranchNumber,
373                CopyableCipherFields::BankAccountPin,
374                CopyableCipherFields::BankAccountIban,
375                CopyableCipherFields::BankAccountSwift,
376            ]
377        );
378    }
379
380    #[test]
381    fn test_get_copyable_fields_bank_account_partial() {
382        let enc_str: EncString = "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap();
383
384        let bank_account = BankAccount {
385            bank_name: None,
386            name_on_account: None,
387            account_type: None,
388            account_number: Some(enc_str.clone()),
389            routing_number: None,
390            branch_number: None,
391            pin: Some(enc_str),
392            swift_code: None,
393            iban: None,
394            bank_contact_phone: None,
395        };
396
397        let copyable_fields = bank_account.get_copyable_fields(None);
398        assert_eq!(
399            copyable_fields,
400            vec![
401                CopyableCipherFields::BankAccountAccountNumber,
402                CopyableCipherFields::BankAccountPin,
403            ]
404        );
405    }
406}