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