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