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.account_number
108                .as_ref()
109                .map(|_| CopyableCipherFields::BankAccountAccountNumber),
110            self.routing_number
111                .as_ref()
112                .map(|_| CopyableCipherFields::BankAccountRoutingNumber),
113            self.pin
114                .as_ref()
115                .map(|_| CopyableCipherFields::BankAccountPin),
116            self.iban
117                .as_ref()
118                .map(|_| CopyableCipherFields::BankAccountIban),
119        ]
120        .into_iter()
121        .flatten()
122        .collect()
123    }
124}
125
126impl TryFrom<CipherBankAccountModel> for BankAccount {
127    type Error = VaultParseError;
128
129    fn try_from(bank_account: CipherBankAccountModel) -> Result<Self, Self::Error> {
130        Ok(Self {
131            bank_name: EncString::try_from_optional(bank_account.bank_name)?,
132            name_on_account: EncString::try_from_optional(bank_account.name_on_account)?,
133            account_type: EncString::try_from_optional(bank_account.account_type)?,
134            account_number: EncString::try_from_optional(bank_account.account_number)?,
135            routing_number: EncString::try_from_optional(bank_account.routing_number)?,
136            branch_number: EncString::try_from_optional(bank_account.branch_number)?,
137            pin: EncString::try_from_optional(bank_account.pin)?,
138            swift_code: EncString::try_from_optional(bank_account.swift_code)?,
139            iban: EncString::try_from_optional(bank_account.iban)?,
140            bank_contact_phone: EncString::try_from_optional(bank_account.bank_contact_phone)?,
141        })
142    }
143}
144
145impl From<BankAccount> for CipherBankAccountModel {
146    fn from(bank_account: BankAccount) -> Self {
147        Self {
148            bank_name: bank_account.bank_name.map(|n| n.to_string()),
149            name_on_account: bank_account.name_on_account.map(|n| n.to_string()),
150            account_type: bank_account.account_type.map(|n| n.to_string()),
151            account_number: bank_account.account_number.map(|n| n.to_string()),
152            routing_number: bank_account.routing_number.map(|n| n.to_string()),
153            branch_number: bank_account.branch_number.map(|n| n.to_string()),
154            pin: bank_account.pin.map(|n| n.to_string()),
155            swift_code: bank_account.swift_code.map(|n| n.to_string()),
156            iban: bank_account.iban.map(|n| n.to_string()),
157            bank_contact_phone: bank_account.bank_contact_phone.map(|n| n.to_string()),
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use bitwarden_core::key_management::create_test_crypto_with_user_key;
165    use bitwarden_crypto::SymmetricCryptoKey;
166
167    use super::*;
168    use crate::cipher::cipher::CopyableCipherFields;
169
170    const TEST_VECTOR_BANK_KEY: &str =
171        "87OtUxruEdWbBBltgpTWDvfEN2SkB47dlsolBL9VTzQPCb5Y2lssAf6IsX4sbLrCHnsefRg8Ytl/mO448St8vg==";
172    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="}"#;
173
174    fn test_bank_account_view() -> BankAccountView {
175        BankAccountView {
176            bank_name: Some("Test Bank".to_string()),
177            name_on_account: Some("John Doe".to_string()),
178            account_type: Some("Checking".to_string()),
179            account_number: Some("1234567890".to_string()),
180            routing_number: Some("021000021".to_string()),
181            branch_number: Some("001".to_string()),
182            pin: Some("1234".to_string()),
183            swift_code: Some("TESTUS33".to_string()),
184            iban: Some("US12345678901234567890".to_string()),
185            bank_contact_phone: Some("555-0123".to_string()),
186        }
187    }
188
189    #[test]
190    #[ignore]
191    fn generate_test_vector() {
192        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
193        let key_b64 = key.to_base64();
194        let key_store = create_test_crypto_with_user_key(key);
195        let key_slot = SymmetricKeySlotId::User;
196        let mut ctx = key_store.context();
197
198        let encrypted = test_bank_account_view()
199            .encrypt_composite(&mut ctx, key_slot)
200            .unwrap();
201        let json = serde_json::to_string(&encrypted).unwrap();
202
203        println!("const TEST_VECTOR_BANK_KEY: &str = \"{key_b64}\";");
204        println!("const TEST_VECTOR_BANK_JSON: &str = r#\"{json}\"#;");
205    }
206
207    #[test]
208    fn test_recorded_bank_account_test_vector() {
209        let key =
210            SymmetricCryptoKey::try_from(TEST_VECTOR_BANK_KEY.to_string()).expect("valid test key");
211        let key_store = create_test_crypto_with_user_key(key);
212        let key_slot = SymmetricKeySlotId::User;
213        let mut ctx = key_store.context();
214
215        let encrypted: BankAccount =
216            serde_json::from_str(TEST_VECTOR_BANK_JSON).expect("valid test vector JSON");
217        let decrypted: BankAccountView = encrypted
218            .decrypt(&mut ctx, key_slot)
219            .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.");
220
221        assert_eq!(decrypted, test_bank_account_view());
222    }
223
224    #[test]
225    fn test_subtitle_bank_account() {
226        let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
227        let key_store = create_test_crypto_with_user_key(key);
228        let key = SymmetricKeySlotId::User;
229        let mut ctx = key_store.context();
230
231        let original_subtitle = "My Bank".to_string();
232        let bank_name_encrypted = original_subtitle.to_owned().encrypt(&mut ctx, key).unwrap();
233
234        let bank_account = BankAccount {
235            bank_name: Some(bank_name_encrypted),
236            name_on_account: None,
237            account_type: None,
238            account_number: None,
239            routing_number: None,
240            branch_number: None,
241            pin: None,
242            swift_code: None,
243            iban: None,
244            bank_contact_phone: None,
245        };
246
247        assert_eq!(
248            bank_account.decrypt_subtitle(&mut ctx, key).unwrap(),
249            original_subtitle
250        );
251    }
252
253    #[test]
254    fn test_get_copyable_fields_bank_account() {
255        let enc_str: EncString = "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap();
256
257        let bank_account = BankAccount {
258            bank_name: Some(enc_str.clone()),
259            name_on_account: Some(enc_str.clone()),
260            account_type: Some(enc_str.clone()),
261            account_number: Some(enc_str.clone()),
262            routing_number: Some(enc_str.clone()),
263            branch_number: Some(enc_str.clone()),
264            pin: Some(enc_str.clone()),
265            swift_code: Some(enc_str.clone()),
266            iban: Some(enc_str.clone()),
267            bank_contact_phone: Some(enc_str),
268        };
269
270        let copyable_fields = bank_account.get_copyable_fields(None);
271        assert_eq!(
272            copyable_fields,
273            vec![
274                CopyableCipherFields::BankAccountAccountNumber,
275                CopyableCipherFields::BankAccountRoutingNumber,
276                CopyableCipherFields::BankAccountPin,
277                CopyableCipherFields::BankAccountIban,
278            ]
279        );
280    }
281
282    #[test]
283    fn test_get_copyable_fields_bank_account_partial() {
284        let enc_str: EncString = "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap();
285
286        let bank_account = BankAccount {
287            bank_name: None,
288            name_on_account: None,
289            account_type: None,
290            account_number: Some(enc_str.clone()),
291            routing_number: None,
292            branch_number: None,
293            pin: Some(enc_str),
294            swift_code: None,
295            iban: None,
296            bank_contact_phone: None,
297        };
298
299        let copyable_fields = bank_account.get_copyable_fields(None);
300        assert_eq!(
301            copyable_fields,
302            vec![
303                CopyableCipherFields::BankAccountAccountNumber,
304                CopyableCipherFields::BankAccountPin,
305            ]
306        );
307    }
308}