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