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