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_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
196/// Builds the subtitle for a card cipher
197fn 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}