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_next::Tsify;
10
11use super::cipher::CipherKind;
12use crate::{cipher::cipher::CopyableCipherFields, Cipher, VaultParseError};
13
14#[derive(Serialize, Deserialize, Debug, Clone)]
15#[serde(rename_all = "camelCase", deny_unknown_fields)]
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 CipherKind for Identity {
151 fn decrypt_subtitle(
152 &self,
153 ctx: &mut KeyStoreContext<KeyIds>,
154 key: SymmetricKeyId,
155 ) -> Result<String, CryptoError> {
156 let first_name = self
157 .first_name
158 .as_ref()
159 .map(|f| f.decrypt(ctx, key))
160 .transpose()?;
161 let last_name = self
162 .last_name
163 .as_ref()
164 .map(|l| l.decrypt(ctx, key))
165 .transpose()?;
166
167 Ok(build_subtitle_identity(first_name, last_name))
168 }
169
170 fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
171 [
172 self.username
173 .as_ref()
174 .map(|_| CopyableCipherFields::IdentityUsername),
175 self.email
176 .as_ref()
177 .map(|_| CopyableCipherFields::IdentityEmail),
178 self.phone
179 .as_ref()
180 .map(|_| CopyableCipherFields::IdentityPhone),
181 self.address1
182 .as_ref()
183 .or(self.address2.as_ref())
184 .or(self.address3.as_ref())
185 .or(self.city.as_ref())
186 .or(self.state.as_ref())
187 .or(self.postal_code.as_ref())
188 .map(|_| CopyableCipherFields::IdentityAddress),
189 ]
190 .into_iter()
191 .flatten()
192 .collect()
193 }
194}
195
196fn build_subtitle_identity(first_name: Option<String>, last_name: Option<String>) -> String {
198 let len = match (first_name.as_ref(), last_name.as_ref()) {
199 (Some(first_name), Some(last_name)) => first_name.len() + last_name.len() + 1,
200 (Some(first_name), None) => first_name.len(),
201 (None, Some(last_name)) => last_name.len(),
202 (None, None) => 0,
203 };
204
205 let mut subtitle = String::with_capacity(len);
206
207 if let Some(first_name) = &first_name {
208 subtitle.push_str(first_name);
209 }
210
211 if let Some(last_name) = &last_name {
212 if !subtitle.is_empty() {
213 subtitle.push(' ');
214 }
215 subtitle.push_str(last_name);
216 }
217
218 subtitle
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::cipher::cipher::CopyableCipherFields;
225
226 fn create_identity() -> Identity {
227 Identity {
228 title: None,
229 first_name: None,
230 middle_name: None,
231 last_name: None,
232 address1: None,
233 address2: None,
234 address3: None,
235 city: None,
236 state: None,
237 postal_code: None,
238 country: None,
239 company: None,
240 email: None,
241 phone: None,
242 ssn: None,
243 username: None,
244 passport_number: None,
245 license_number: None,
246 }
247 }
248
249 #[test]
250 fn test_build_subtitle_identity() {
251 let first_name = Some("John".to_owned());
252 let last_name = Some("Doe".to_owned());
253
254 let subtitle = build_subtitle_identity(first_name, last_name);
255 assert_eq!(subtitle, "John Doe");
256 }
257
258 #[test]
259 fn test_build_subtitle_identity_only_first() {
260 let first_name = Some("John".to_owned());
261 let last_name = None;
262
263 let subtitle = build_subtitle_identity(first_name, last_name);
264 assert_eq!(subtitle, "John");
265 }
266
267 #[test]
268 fn test_build_subtitle_identity_only_last() {
269 let first_name = None;
270 let last_name = Some("Doe".to_owned());
271
272 let subtitle = build_subtitle_identity(first_name, last_name);
273 assert_eq!(subtitle, "Doe");
274 }
275
276 #[test]
277 fn test_build_subtitle_identity_none() {
278 let first_name = None;
279 let last_name = None;
280
281 let subtitle = build_subtitle_identity(first_name, last_name);
282 assert_eq!(subtitle, "");
283 }
284
285 #[test]
286 fn test_get_copyable_fields_identity_empty() {
287 let identity = create_identity();
288
289 let copyable_fields = identity.get_copyable_fields(None);
290 assert_eq!(copyable_fields, vec![]);
291 }
292
293 #[test]
294 fn test_get_copyable_fields_identity_has_username() {
295 let mut identity = create_identity();
296 identity.username = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
297
298 let copyable_fields = identity.get_copyable_fields(None);
299 assert_eq!(
300 copyable_fields,
301 vec![CopyableCipherFields::IdentityUsername]
302 );
303 }
304
305 #[test]
306 fn test_get_copyable_fields_identity_has_email() {
307 let mut identity = create_identity();
308 identity.email = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
309
310 let copyable_fields = identity.get_copyable_fields(None);
311 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityEmail]);
312 }
313
314 #[test]
315 fn test_get_copyable_fields_identity_has_phone() {
316 let mut identity = create_identity();
317 identity.phone = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
318
319 let copyable_fields = identity.get_copyable_fields(None);
320 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityPhone]);
321 }
322
323 #[test]
324 fn test_get_copyable_fields_identity_has_address() {
325 let mut identity = create_identity();
326
327 identity.address1 = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
328
329 let mut copyable_fields = identity.get_copyable_fields(None);
330
331 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
332
333 identity.state = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
334 identity.address1 = None;
335
336 copyable_fields = identity.get_copyable_fields(None);
337 assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
338 }
339}