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#[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 pub account_number: Option<String>,
57 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}