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
250pub(super) fn build_subtitle_identity(
252 first_name: Option<String>,
253 last_name: Option<String>,
254) -> String {
255 let len = match (first_name.as_ref(), last_name.as_ref()) {
256 (Some(first_name), Some(last_name)) => first_name.len() + last_name.len() + 1,
257 (Some(first_name), None) => first_name.len(),
258 (None, Some(last_name)) => last_name.len(),
259 (None, None) => 0,
260 };
261
262 let mut subtitle = String::with_capacity(len);
263
264 if let Some(first_name) = &first_name {
265 subtitle.push_str(first_name);
266 }
267
268 if let Some(last_name) = &last_name {
269 if !subtitle.is_empty() {
270 subtitle.push(' ');
271 }
272 subtitle.push_str(last_name);
273 }
274
275 subtitle
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::cipher::cipher::CopyableCipherFields;
282
283 fn create_identity() -> Identity {
284 Identity {
285 title: None,
286 first_name: None,
287 middle_name: None,
288 last_name: None,
289 address1: None,
290 address2: None,
291 address3: None,
292 city: None,
293 state: None,
294 postal_code: None,
295 country: None,
296 company: None,
297 email: None,
298 phone: None,
299 ssn: None,
300 username: None,
301 passport_number: None,
302 license_number: None,
303 }
304 }
305
306 #[test]
307 fn test_build_subtitle_identity() {
308 let first_name = Some("John".to_owned());
309 let last_name = Some("Doe".to_owned());
310
311 let subtitle = build_subtitle_identity(first_name, last_name);
312 assert_eq!(subtitle, "John Doe");
313 }
314
315 #[test]
316 fn test_build_subtitle_identity_only_first() {
317 let first_name = Some("John".to_owned());
318 let last_name = None;
319
320 let subtitle = build_subtitle_identity(first_name, last_name);
321 assert_eq!(subtitle, "John");
322 }
323
324 #[test]
325 fn test_build_subtitle_identity_only_last() {
326 let first_name = None;
327 let last_name = Some("Doe".to_owned());
328
329 let subtitle = build_subtitle_identity(first_name, last_name);
330 assert_eq!(subtitle, "Doe");
331 }
332
333 #[test]
334 fn test_build_subtitle_identity_none() {
335 let first_name = None;
336 let last_name = None;
337
338 let subtitle = build_subtitle_identity(first_name, last_name);
339 assert_eq!(subtitle, "");
340 }
341
342 #[test]
343 fn test_get_copyable_fields_identity_empty() {
344 let identity = create_identity();
345
346 let copyable_fields = identity.get_copyable_fields(None);
347 assert_eq!(copyable_fields, vec![]);
348 }
349
350 #[test]
351 fn test_get_copyable_fields_identity_has_username() {
352 let mut identity = create_identity();
353 identity.username = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
354
355 let copyable_fields = identity.get_copyable_fields(None);
356 assert_eq!(
357 copyable_fields,
358 vec![CopyableCipherFields::IdentityUsername]
359 );
360 }
361
362 #[test]
363 fn test_get_copyable_fields_identity_has_email() {
364 let mut identity = create_identity();
365 identity.email = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
366
367 let copyable_fields = identity.get_copyable_fields(None);
368 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityEmail]);
369 }
370
371 #[test]
372 fn test_get_copyable_fields_identity_has_phone() {
373 let mut identity = create_identity();
374 identity.phone = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
375
376 let copyable_fields = identity.get_copyable_fields(None);
377 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityPhone]);
378 }
379
380 #[test]
381 fn test_get_copyable_fields_identity_has_address() {
382 let mut identity = create_identity();
383
384 identity.address1 = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
385
386 let mut copyable_fields = identity.get_copyable_fields(None);
387
388 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
389
390 identity.state = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
391 identity.address1 = None;
392
393 copyable_fields = identity.get_copyable_fields(None);
394 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
395 }
396}