Skip to main content

bitwarden_vault/cipher/
identity.rs

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
250/// Builds the subtitle for a card cipher
251fn 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}