bitwarden_vault/cipher/
identity.rs

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