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}