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