1use bitwarden_api_api::models::CipherCardModel;
2use bitwarden_core::key_management::{KeyIds, SymmetricKeyId};
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 Card {
19 pub cardholder_name: Option<EncString>,
20 pub exp_month: Option<EncString>,
21 pub exp_year: Option<EncString>,
22 pub code: Option<EncString>,
23 pub brand: Option<EncString>,
24 pub number: Option<EncString>,
25}
26
27#[allow(missing_docs)]
28#[derive(Serialize, Deserialize, Debug, Clone)]
29#[serde(rename_all = "camelCase", deny_unknown_fields)]
30#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
31#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
32pub struct CardView {
33 pub cardholder_name: Option<String>,
34 pub exp_month: Option<String>,
35 pub exp_year: Option<String>,
36 pub code: Option<String>,
37 pub brand: Option<String>,
38 pub number: Option<String>,
39}
40
41#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
43#[serde(rename_all = "camelCase", deny_unknown_fields)]
44#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
45#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
46pub struct CardListView {
47 pub brand: Option<String>,
49}
50
51#[allow(missing_docs)]
52#[derive(Serialize, Deserialize)]
53pub enum CardBrand {
54 Visa,
55 Mastercard,
56 Amex,
57 Discover,
58 #[serde(rename = "Diners Club")]
59 DinersClub,
60 #[serde(rename = "JCB")]
61 Jcb,
62 Maestro,
63 UnionPay,
64 RuPay,
65 #[serde(untagged)]
66 Other,
67}
68
69impl CompositeEncryptable<KeyIds, SymmetricKeyId, Card> for CardView {
70 fn encrypt_composite(
71 &self,
72 ctx: &mut KeyStoreContext<KeyIds>,
73 key: SymmetricKeyId,
74 ) -> Result<Card, CryptoError> {
75 Ok(Card {
76 cardholder_name: self.cardholder_name.encrypt(ctx, key)?,
77 exp_month: self.exp_month.encrypt(ctx, key)?,
78 exp_year: self.exp_year.encrypt(ctx, key)?,
79 code: self.code.encrypt(ctx, key)?,
80 brand: self.brand.encrypt(ctx, key)?,
81 number: self.number.encrypt(ctx, key)?,
82 })
83 }
84}
85
86impl Decryptable<KeyIds, SymmetricKeyId, CardListView> for Card {
87 fn decrypt(
88 &self,
89 ctx: &mut KeyStoreContext<KeyIds>,
90 key: SymmetricKeyId,
91 ) -> Result<CardListView, CryptoError> {
92 Ok(CardListView {
93 brand: self.brand.decrypt(ctx, key).ok().flatten(),
94 })
95 }
96}
97
98impl Decryptable<KeyIds, SymmetricKeyId, CardView> for Card {
99 fn decrypt(
100 &self,
101 ctx: &mut KeyStoreContext<KeyIds>,
102 key: SymmetricKeyId,
103 ) -> Result<CardView, CryptoError> {
104 Ok(CardView {
105 cardholder_name: self.cardholder_name.decrypt(ctx, key).ok().flatten(),
106 exp_month: self.exp_month.decrypt(ctx, key).ok().flatten(),
107 exp_year: self.exp_year.decrypt(ctx, key).ok().flatten(),
108 code: self.code.decrypt(ctx, key).ok().flatten(),
109 brand: self.brand.decrypt(ctx, key).ok().flatten(),
110 number: self.number.decrypt(ctx, key).ok().flatten(),
111 })
112 }
113}
114
115impl Decryptable<KeyIds, SymmetricKeyId, CardListView> for StrictDecrypt<&Card> {
116 fn decrypt(
117 &self,
118 ctx: &mut KeyStoreContext<KeyIds>,
119 key: SymmetricKeyId,
120 ) -> Result<CardListView, CryptoError> {
121 Ok(CardListView {
122 brand: self.0.brand.decrypt(ctx, key)?,
123 })
124 }
125}
126
127impl Decryptable<KeyIds, SymmetricKeyId, CardView> for StrictDecrypt<&Card> {
128 fn decrypt(
129 &self,
130 ctx: &mut KeyStoreContext<KeyIds>,
131 key: SymmetricKeyId,
132 ) -> Result<CardView, CryptoError> {
133 Ok(CardView {
134 cardholder_name: self.0.cardholder_name.decrypt(ctx, key)?,
135 exp_month: self.0.exp_month.decrypt(ctx, key)?,
136 exp_year: self.0.exp_year.decrypt(ctx, key)?,
137 code: self.0.code.decrypt(ctx, key)?,
138 brand: self.0.brand.decrypt(ctx, key)?,
139 number: self.0.number.decrypt(ctx, key)?,
140 })
141 }
142}
143
144impl TryFrom<CipherCardModel> for Card {
145 type Error = VaultParseError;
146
147 fn try_from(card: CipherCardModel) -> Result<Self, Self::Error> {
148 Ok(Self {
149 cardholder_name: EncString::try_from_optional(card.cardholder_name)?,
150 exp_month: EncString::try_from_optional(card.exp_month)?,
151 exp_year: EncString::try_from_optional(card.exp_year)?,
152 code: EncString::try_from_optional(card.code)?,
153 brand: EncString::try_from_optional(card.brand)?,
154 number: EncString::try_from_optional(card.number)?,
155 })
156 }
157}
158
159impl From<Card> for bitwarden_api_api::models::CipherCardModel {
160 fn from(card: Card) -> Self {
161 Self {
162 cardholder_name: card.cardholder_name.map(|n| n.to_string()),
163 brand: card.brand.map(|b| b.to_string()),
164 number: card.number.map(|n| n.to_string()),
165 exp_month: card.exp_month.map(|m| m.to_string()),
166 exp_year: card.exp_year.map(|y| y.to_string()),
167 code: card.code.map(|c| c.to_string()),
168 }
169 }
170}
171
172impl CipherKind for Card {
173 fn decrypt_subtitle(
174 &self,
175 ctx: &mut KeyStoreContext<KeyIds>,
176 key: SymmetricKeyId,
177 ) -> Result<String, CryptoError> {
178 let brand = self
179 .brand
180 .as_ref()
181 .map(|b| b.decrypt(ctx, key))
182 .transpose()?;
183 let number = self
184 .number
185 .as_ref()
186 .map(|n| n.decrypt(ctx, key))
187 .transpose()?;
188
189 Ok(build_subtitle_card(brand, number))
190 }
191
192 fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
193 [
194 self.number
195 .as_ref()
196 .map(|_| CopyableCipherFields::CardNumber),
197 self.code
198 .as_ref()
199 .map(|_| CopyableCipherFields::CardSecurityCode),
200 ]
201 .into_iter()
202 .flatten()
203 .collect()
204 }
205}
206
207fn build_subtitle_card(brand: Option<String>, number: Option<String>) -> String {
209 let mut subtitle =
211 String::with_capacity(brand.as_ref().map(|b| b.len()).unwrap_or_default() + 8);
212
213 if let Some(brand) = brand {
214 subtitle.push_str(&brand);
215 }
216
217 if let Some(number) = number {
218 let number_chars: Vec<_> = number.chars().collect();
219 let number_len = number_chars.len();
220 if number_len > 4 {
221 if !subtitle.is_empty() {
222 subtitle.push_str(", ");
223 }
224
225 let digit_count = match number_chars[0..2] {
227 ['3', '4'] | ['3', '7'] => 5,
228 _ => 4,
229 };
230
231 subtitle.push('*');
232 subtitle.extend(number_chars.iter().skip(number_len - digit_count));
233 }
234 }
235
236 subtitle
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_build_subtitle_card_visa() {
245 let brand = Some("Visa".to_owned());
246 let number = Some("4111111111111111".to_owned());
247
248 let subtitle = build_subtitle_card(brand, number);
249 assert_eq!(subtitle, "Visa, *1111");
250 }
251
252 #[test]
253 fn test_build_subtitle_card_mastercard() {
254 let brand = Some("Mastercard".to_owned());
255 let number = Some("5555555555554444".to_owned());
256
257 let subtitle = build_subtitle_card(brand, number);
258 assert_eq!(subtitle, "Mastercard, *4444");
259 }
260
261 #[test]
262 fn test_build_subtitle_card_amex() {
263 let brand = Some("Amex".to_owned());
264 let number = Some("378282246310005".to_owned());
265
266 let subtitle = build_subtitle_card(brand, number);
267 assert_eq!(subtitle, "Amex, *10005");
268 }
269
270 #[test]
271 fn test_build_subtitle_card_underflow() {
272 let brand = Some("Mastercard".to_owned());
273 let number = Some("4".to_owned());
274
275 let subtitle = build_subtitle_card(brand, number);
276 assert_eq!(subtitle, "Mastercard");
277 }
278
279 #[test]
280 fn test_build_subtitle_card_only_brand() {
281 let brand = Some("Mastercard".to_owned());
282 let number = None;
283
284 let subtitle = build_subtitle_card(brand, number);
285 assert_eq!(subtitle, "Mastercard");
286 }
287
288 #[test]
289 fn test_build_subtitle_card_only_card() {
290 let brand = None;
291 let number = Some("5555555555554444".to_owned());
292
293 let subtitle = build_subtitle_card(brand, number);
294 assert_eq!(subtitle, "*4444");
295 }
296 #[test]
297 fn test_get_copyable_fields_code() {
298 let card = Card {
299 cardholder_name: None,
300 exp_month: None,
301 exp_year: None,
302 code: Some("2.6TpmzzaQHgYr+mXjdGLQlg==|vT8VhfvMlWSCN9hxGYftZ5rjKRsZ9ofjdlUCx5Gubnk=|uoD3/GEQBWKKx2O+/YhZUCzVkfhm8rFK3sUEVV84mv8=".parse().unwrap()),
303 brand: None,
304 number: None,
305 };
306
307 let copyable_fields = card.get_copyable_fields(None);
308
309 assert_eq!(
310 copyable_fields,
311 vec![CopyableCipherFields::CardSecurityCode]
312 );
313 }
314
315 #[test]
316 fn test_build_subtitle_card_unicode() {
317 let brand = Some("Visa".to_owned());
318 let number = Some("•••• 3278".to_owned());
319
320 let subtitle = build_subtitle_card(brand, number);
321 assert_eq!(subtitle, "Visa, *3278");
322 }
323
324 #[test]
325 fn test_get_copyable_fields_number() {
326 let card = Card {
327 cardholder_name: None,
328 exp_month: None,
329 exp_year: None,
330 code: None,
331 brand: None,
332 number: Some("2.6TpmzzaQHgYr+mXjdGLQlg==|vT8VhfvMlWSCN9hxGYftZ5rjKRsZ9ofjdlUCx5Gubnk=|uoD3/GEQBWKKx2O+/YhZUCzVkfhm8rFK3sUEVV84mv8=".parse().unwrap()),
333 };
334
335 let copyable_fields = card.get_copyable_fields(None);
336
337 assert_eq!(copyable_fields, vec![CopyableCipherFields::CardNumber]);
338 }
339}