1use bitwarden_api_api::models::CipherIdentityModel;
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;
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<KeyIds, SymmetricKeyId, Identity> for IdentityView {
66 fn encrypt_composite(
67 &self,
68 ctx: &mut KeyStoreContext<KeyIds>,
69 key: SymmetricKeyId,
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<KeyIds, SymmetricKeyId, IdentityView> for Identity {
95 fn decrypt(
96 &self,
97 ctx: &mut KeyStoreContext<KeyIds>,
98 key: SymmetricKeyId,
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 TryFrom<CipherIdentityModel> for Identity {
124 type Error = VaultParseError;
125
126 fn try_from(identity: CipherIdentityModel) -> Result<Self, Self::Error> {
127 Ok(Self {
128 title: EncString::try_from_optional(identity.title)?,
129 first_name: EncString::try_from_optional(identity.first_name)?,
130 middle_name: EncString::try_from_optional(identity.middle_name)?,
131 last_name: EncString::try_from_optional(identity.last_name)?,
132 address1: EncString::try_from_optional(identity.address1)?,
133 address2: EncString::try_from_optional(identity.address2)?,
134 address3: EncString::try_from_optional(identity.address3)?,
135 city: EncString::try_from_optional(identity.city)?,
136 state: EncString::try_from_optional(identity.state)?,
137 postal_code: EncString::try_from_optional(identity.postal_code)?,
138 country: EncString::try_from_optional(identity.country)?,
139 company: EncString::try_from_optional(identity.company)?,
140 email: EncString::try_from_optional(identity.email)?,
141 phone: EncString::try_from_optional(identity.phone)?,
142 ssn: EncString::try_from_optional(identity.ssn)?,
143 username: EncString::try_from_optional(identity.username)?,
144 passport_number: EncString::try_from_optional(identity.passport_number)?,
145 license_number: EncString::try_from_optional(identity.license_number)?,
146 })
147 }
148}
149
150impl From<Identity> for bitwarden_api_api::models::CipherIdentityModel {
151 fn from(identity: Identity) -> Self {
152 Self {
153 title: identity.title.map(|t| t.to_string()),
154 first_name: identity.first_name.map(|n| n.to_string()),
155 middle_name: identity.middle_name.map(|n| n.to_string()),
156 last_name: identity.last_name.map(|n| n.to_string()),
157 address1: identity.address1.map(|a| a.to_string()),
158 address2: identity.address2.map(|a| a.to_string()),
159 address3: identity.address3.map(|a| a.to_string()),
160 city: identity.city.map(|c| c.to_string()),
161 state: identity.state.map(|s| s.to_string()),
162 postal_code: identity.postal_code.map(|p| p.to_string()),
163 country: identity.country.map(|c| c.to_string()),
164 company: identity.company.map(|c| c.to_string()),
165 email: identity.email.map(|e| e.to_string()),
166 phone: identity.phone.map(|p| p.to_string()),
167 ssn: identity.ssn.map(|s| s.to_string()),
168 username: identity.username.map(|u| u.to_string()),
169 passport_number: identity.passport_number.map(|p| p.to_string()),
170 license_number: identity.license_number.map(|l| l.to_string()),
171 }
172 }
173}
174
175impl CipherKind for Identity {
176 fn decrypt_subtitle(
177 &self,
178 ctx: &mut KeyStoreContext<KeyIds>,
179 key: SymmetricKeyId,
180 ) -> Result<String, CryptoError> {
181 let first_name = self
182 .first_name
183 .as_ref()
184 .map(|f| f.decrypt(ctx, key))
185 .transpose()?;
186 let last_name = self
187 .last_name
188 .as_ref()
189 .map(|l| l.decrypt(ctx, key))
190 .transpose()?;
191
192 Ok(build_subtitle_identity(first_name, last_name))
193 }
194
195 fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
196 [
197 self.username
198 .as_ref()
199 .map(|_| CopyableCipherFields::IdentityUsername),
200 self.email
201 .as_ref()
202 .map(|_| CopyableCipherFields::IdentityEmail),
203 self.phone
204 .as_ref()
205 .map(|_| CopyableCipherFields::IdentityPhone),
206 self.address1
207 .as_ref()
208 .or(self.address2.as_ref())
209 .or(self.address3.as_ref())
210 .or(self.city.as_ref())
211 .or(self.state.as_ref())
212 .or(self.postal_code.as_ref())
213 .map(|_| CopyableCipherFields::IdentityAddress),
214 ]
215 .into_iter()
216 .flatten()
217 .collect()
218 }
219}
220
221fn build_subtitle_identity(first_name: Option<String>, last_name: Option<String>) -> String {
223 let len = match (first_name.as_ref(), last_name.as_ref()) {
224 (Some(first_name), Some(last_name)) => first_name.len() + last_name.len() + 1,
225 (Some(first_name), None) => first_name.len(),
226 (None, Some(last_name)) => last_name.len(),
227 (None, None) => 0,
228 };
229
230 let mut subtitle = String::with_capacity(len);
231
232 if let Some(first_name) = &first_name {
233 subtitle.push_str(first_name);
234 }
235
236 if let Some(last_name) = &last_name {
237 if !subtitle.is_empty() {
238 subtitle.push(' ');
239 }
240 subtitle.push_str(last_name);
241 }
242
243 subtitle
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use crate::cipher::cipher::CopyableCipherFields;
250
251 fn create_identity() -> Identity {
252 Identity {
253 title: None,
254 first_name: None,
255 middle_name: None,
256 last_name: None,
257 address1: None,
258 address2: None,
259 address3: None,
260 city: None,
261 state: None,
262 postal_code: None,
263 country: None,
264 company: None,
265 email: None,
266 phone: None,
267 ssn: None,
268 username: None,
269 passport_number: None,
270 license_number: None,
271 }
272 }
273
274 #[test]
275 fn test_build_subtitle_identity() {
276 let first_name = Some("John".to_owned());
277 let last_name = Some("Doe".to_owned());
278
279 let subtitle = build_subtitle_identity(first_name, last_name);
280 assert_eq!(subtitle, "John Doe");
281 }
282
283 #[test]
284 fn test_build_subtitle_identity_only_first() {
285 let first_name = Some("John".to_owned());
286 let last_name = None;
287
288 let subtitle = build_subtitle_identity(first_name, last_name);
289 assert_eq!(subtitle, "John");
290 }
291
292 #[test]
293 fn test_build_subtitle_identity_only_last() {
294 let first_name = None;
295 let last_name = Some("Doe".to_owned());
296
297 let subtitle = build_subtitle_identity(first_name, last_name);
298 assert_eq!(subtitle, "Doe");
299 }
300
301 #[test]
302 fn test_build_subtitle_identity_none() {
303 let first_name = None;
304 let last_name = None;
305
306 let subtitle = build_subtitle_identity(first_name, last_name);
307 assert_eq!(subtitle, "");
308 }
309
310 #[test]
311 fn test_get_copyable_fields_identity_empty() {
312 let identity = create_identity();
313
314 let copyable_fields = identity.get_copyable_fields(None);
315 assert_eq!(copyable_fields, vec![]);
316 }
317
318 #[test]
319 fn test_get_copyable_fields_identity_has_username() {
320 let mut identity = create_identity();
321 identity.username = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
322
323 let copyable_fields = identity.get_copyable_fields(None);
324 assert_eq!(
325 copyable_fields,
326 vec![CopyableCipherFields::IdentityUsername]
327 );
328 }
329
330 #[test]
331 fn test_get_copyable_fields_identity_has_email() {
332 let mut identity = create_identity();
333 identity.email = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
334
335 let copyable_fields = identity.get_copyable_fields(None);
336 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityEmail]);
337 }
338
339 #[test]
340 fn test_get_copyable_fields_identity_has_phone() {
341 let mut identity = create_identity();
342 identity.phone = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
343
344 let copyable_fields = identity.get_copyable_fields(None);
345 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityPhone]);
346 }
347
348 #[test]
349 fn test_get_copyable_fields_identity_has_address() {
350 let mut identity = create_identity();
351
352 identity.address1 = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
353
354 let mut copyable_fields = identity.get_copyable_fields(None);
355
356 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
357
358 identity.state = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
359 identity.address1 = None;
360
361 copyable_fields = identity.get_copyable_fields(None);
362 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
363 }
364}