1use bitwarden_api_api::models::CipherIdentityModel;
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 Identity {
16 pub title: Option<EncString>,
17 pub first_name: Option<EncString>,
18 pub middle_name: Option<EncString>,
19 pub last_name: Option<EncString>,
20 pub address1: Option<EncString>,
21 pub address2: Option<EncString>,
22 pub address3: Option<EncString>,
23 pub city: Option<EncString>,
24 pub state: Option<EncString>,
25 pub postal_code: Option<EncString>,
26 pub country: Option<EncString>,
27 pub company: Option<EncString>,
28 pub email: Option<EncString>,
29 pub phone: Option<EncString>,
30 pub ssn: Option<EncString>,
31 pub username: Option<EncString>,
32 pub passport_number: Option<EncString>,
33 pub license_number: Option<EncString>,
34}
35
36#[allow(missing_docs)]
37#[derive(Serialize, Deserialize, Debug, Clone)]
38#[serde(rename_all = "camelCase", deny_unknown_fields)]
39#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
40#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
41pub struct IdentityView {
42 pub title: Option<String>,
43 pub first_name: Option<String>,
44 pub middle_name: Option<String>,
45 pub last_name: Option<String>,
46 pub address1: Option<String>,
47 pub address2: Option<String>,
48 pub address3: Option<String>,
49 pub city: Option<String>,
50 pub state: Option<String>,
51 pub postal_code: Option<String>,
52 pub country: Option<String>,
53 pub company: Option<String>,
54 pub email: Option<String>,
55 pub phone: Option<String>,
56 pub ssn: Option<String>,
57 pub username: Option<String>,
58 pub passport_number: Option<String>,
59 pub license_number: Option<String>,
60}
61
62impl Encryptable<KeyIds, SymmetricKeyId, Identity> for IdentityView {
63 fn encrypt(
64 &self,
65 ctx: &mut KeyStoreContext<KeyIds>,
66 key: SymmetricKeyId,
67 ) -> Result<Identity, CryptoError> {
68 Ok(Identity {
69 title: self.title.encrypt(ctx, key)?,
70 first_name: self.first_name.encrypt(ctx, key)?,
71 middle_name: self.middle_name.encrypt(ctx, key)?,
72 last_name: self.last_name.encrypt(ctx, key)?,
73 address1: self.address1.encrypt(ctx, key)?,
74 address2: self.address2.encrypt(ctx, key)?,
75 address3: self.address3.encrypt(ctx, key)?,
76 city: self.city.encrypt(ctx, key)?,
77 state: self.state.encrypt(ctx, key)?,
78 postal_code: self.postal_code.encrypt(ctx, key)?,
79 country: self.country.encrypt(ctx, key)?,
80 company: self.company.encrypt(ctx, key)?,
81 email: self.email.encrypt(ctx, key)?,
82 phone: self.phone.encrypt(ctx, key)?,
83 ssn: self.ssn.encrypt(ctx, key)?,
84 username: self.username.encrypt(ctx, key)?,
85 passport_number: self.passport_number.encrypt(ctx, key)?,
86 license_number: self.license_number.encrypt(ctx, key)?,
87 })
88 }
89}
90
91impl Decryptable<KeyIds, SymmetricKeyId, IdentityView> for Identity {
92 fn decrypt(
93 &self,
94 ctx: &mut KeyStoreContext<KeyIds>,
95 key: SymmetricKeyId,
96 ) -> Result<IdentityView, CryptoError> {
97 Ok(IdentityView {
98 title: self.title.decrypt(ctx, key).ok().flatten(),
99 first_name: self.first_name.decrypt(ctx, key).ok().flatten(),
100 middle_name: self.middle_name.decrypt(ctx, key).ok().flatten(),
101 last_name: self.last_name.decrypt(ctx, key).ok().flatten(),
102 address1: self.address1.decrypt(ctx, key).ok().flatten(),
103 address2: self.address2.decrypt(ctx, key).ok().flatten(),
104 address3: self.address3.decrypt(ctx, key).ok().flatten(),
105 city: self.city.decrypt(ctx, key).ok().flatten(),
106 state: self.state.decrypt(ctx, key).ok().flatten(),
107 postal_code: self.postal_code.decrypt(ctx, key).ok().flatten(),
108 country: self.country.decrypt(ctx, key).ok().flatten(),
109 company: self.company.decrypt(ctx, key).ok().flatten(),
110 email: self.email.decrypt(ctx, key).ok().flatten(),
111 phone: self.phone.decrypt(ctx, key).ok().flatten(),
112 ssn: self.ssn.decrypt(ctx, key).ok().flatten(),
113 username: self.username.decrypt(ctx, key).ok().flatten(),
114 passport_number: self.passport_number.decrypt(ctx, key).ok().flatten(),
115 license_number: self.license_number.decrypt(ctx, key).ok().flatten(),
116 })
117 }
118}
119
120impl TryFrom<CipherIdentityModel> for Identity {
121 type Error = VaultParseError;
122
123 fn try_from(identity: CipherIdentityModel) -> Result<Self, Self::Error> {
124 Ok(Self {
125 title: EncString::try_from_optional(identity.title)?,
126 first_name: EncString::try_from_optional(identity.first_name)?,
127 middle_name: EncString::try_from_optional(identity.middle_name)?,
128 last_name: EncString::try_from_optional(identity.last_name)?,
129 address1: EncString::try_from_optional(identity.address1)?,
130 address2: EncString::try_from_optional(identity.address2)?,
131 address3: EncString::try_from_optional(identity.address3)?,
132 city: EncString::try_from_optional(identity.city)?,
133 state: EncString::try_from_optional(identity.state)?,
134 postal_code: EncString::try_from_optional(identity.postal_code)?,
135 country: EncString::try_from_optional(identity.country)?,
136 company: EncString::try_from_optional(identity.company)?,
137 email: EncString::try_from_optional(identity.email)?,
138 phone: EncString::try_from_optional(identity.phone)?,
139 ssn: EncString::try_from_optional(identity.ssn)?,
140 username: EncString::try_from_optional(identity.username)?,
141 passport_number: EncString::try_from_optional(identity.passport_number)?,
142 license_number: EncString::try_from_optional(identity.license_number)?,
143 })
144 }
145}
146
147impl CipherKind for Identity {
148 fn decrypt_subtitle(
149 &self,
150 ctx: &mut KeyStoreContext<KeyIds>,
151 key: SymmetricKeyId,
152 ) -> Result<String, CryptoError> {
153 let first_name = self
154 .first_name
155 .as_ref()
156 .map(|f| f.decrypt(ctx, key))
157 .transpose()?;
158 let last_name = self
159 .last_name
160 .as_ref()
161 .map(|l| l.decrypt(ctx, key))
162 .transpose()?;
163
164 Ok(build_subtitle_identity(first_name, last_name))
165 }
166
167 fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
168 [
169 self.username
170 .as_ref()
171 .map(|_| CopyableCipherFields::IdentityUsername),
172 self.email
173 .as_ref()
174 .map(|_| CopyableCipherFields::IdentityEmail),
175 self.phone
176 .as_ref()
177 .map(|_| CopyableCipherFields::IdentityPhone),
178 self.address1
179 .as_ref()
180 .or(self.address2.as_ref())
181 .or(self.address3.as_ref())
182 .or(self.city.as_ref())
183 .or(self.state.as_ref())
184 .or(self.postal_code.as_ref())
185 .map(|_| CopyableCipherFields::IdentityAddress),
186 ]
187 .into_iter()
188 .flatten()
189 .collect()
190 }
191}
192
193fn build_subtitle_identity(first_name: Option<String>, last_name: Option<String>) -> String {
195 let len = match (first_name.as_ref(), last_name.as_ref()) {
196 (Some(first_name), Some(last_name)) => first_name.len() + last_name.len() + 1,
197 (Some(first_name), None) => first_name.len(),
198 (None, Some(last_name)) => last_name.len(),
199 (None, None) => 0,
200 };
201
202 let mut subtitle = String::with_capacity(len);
203
204 if let Some(first_name) = &first_name {
205 subtitle.push_str(first_name);
206 }
207
208 if let Some(last_name) = &last_name {
209 if !subtitle.is_empty() {
210 subtitle.push(' ');
211 }
212 subtitle.push_str(last_name);
213 }
214
215 subtitle
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::cipher::cipher::CopyableCipherFields;
222
223 fn create_identity() -> Identity {
224 Identity {
225 title: None,
226 first_name: None,
227 middle_name: None,
228 last_name: None,
229 address1: None,
230 address2: None,
231 address3: None,
232 city: None,
233 state: None,
234 postal_code: None,
235 country: None,
236 company: None,
237 email: None,
238 phone: None,
239 ssn: None,
240 username: None,
241 passport_number: None,
242 license_number: None,
243 }
244 }
245
246 #[test]
247 fn test_build_subtitle_identity() {
248 let first_name = Some("John".to_owned());
249 let last_name = Some("Doe".to_owned());
250
251 let subtitle = build_subtitle_identity(first_name, last_name);
252 assert_eq!(subtitle, "John Doe");
253 }
254
255 #[test]
256 fn test_build_subtitle_identity_only_first() {
257 let first_name = Some("John".to_owned());
258 let last_name = None;
259
260 let subtitle = build_subtitle_identity(first_name, last_name);
261 assert_eq!(subtitle, "John");
262 }
263
264 #[test]
265 fn test_build_subtitle_identity_only_last() {
266 let first_name = None;
267 let last_name = Some("Doe".to_owned());
268
269 let subtitle = build_subtitle_identity(first_name, last_name);
270 assert_eq!(subtitle, "Doe");
271 }
272
273 #[test]
274 fn test_build_subtitle_identity_none() {
275 let first_name = None;
276 let last_name = None;
277
278 let subtitle = build_subtitle_identity(first_name, last_name);
279 assert_eq!(subtitle, "");
280 }
281
282 #[test]
283 fn test_get_copyable_fields_identity_empty() {
284 let identity = create_identity();
285
286 let copyable_fields = identity.get_copyable_fields(None);
287 assert_eq!(copyable_fields, vec![]);
288 }
289
290 #[test]
291 fn test_get_copyable_fields_identity_has_username() {
292 let mut identity = create_identity();
293 identity.username = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
294
295 let copyable_fields = identity.get_copyable_fields(None);
296 assert_eq!(
297 copyable_fields,
298 vec![CopyableCipherFields::IdentityUsername]
299 );
300 }
301
302 #[test]
303 fn test_get_copyable_fields_identity_has_email() {
304 let mut identity = create_identity();
305 identity.email = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
306
307 let copyable_fields = identity.get_copyable_fields(None);
308 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityEmail]);
309 }
310
311 #[test]
312 fn test_get_copyable_fields_identity_has_phone() {
313 let mut identity = create_identity();
314 identity.phone = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
315
316 let copyable_fields = identity.get_copyable_fields(None);
317 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityPhone]);
318 }
319
320 #[test]
321 fn test_get_copyable_fields_identity_has_address() {
322 let mut identity = create_identity();
323
324 identity.address1 = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
325
326 let mut copyable_fields = identity.get_copyable_fields(None);
327
328 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
329
330 identity.state = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
331 identity.address1 = None;
332
333 copyable_fields = identity.get_copyable_fields(None);
334 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
335 }
336}