bitwarden_vault/cipher/
identity.rs

1use bitwarden_api_api::models::CipherIdentityModel;
2use bitwarden_core::key_management::{KeyIds, SymmetricKeyId};
3use bitwarden_crypto::{CryptoError, Decryptable, EncString, Encryptable, KeyStoreContext};
4use serde::{Deserialize, Serialize};
5#[cfg(feature = "wasm")]
6use tsify_next::Tsify;
7
8use super::cipher::CipherKind;
9use crate::{cipher::cipher::CopyableCipherFields, Cipher, VaultParseError};
10
11#[derive(Serialize, Deserialize, Debug, Clone)]
12#[serde(rename_all = "camelCase", deny_unknown_fields)]
13#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
14#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
15pub struct Identity {
16    pub title: Option<EncString>,
17    pub first_name: Option<EncString>,
18    pub middle_name: Option<EncString>,
19    pub last_name: Option<EncString>,
20    pub address1: Option<EncString>,
21    pub address2: Option<EncString>,
22    pub address3: Option<EncString>,
23    pub city: Option<EncString>,
24    pub state: Option<EncString>,
25    pub postal_code: Option<EncString>,
26    pub country: Option<EncString>,
27    pub company: Option<EncString>,
28    pub email: Option<EncString>,
29    pub phone: Option<EncString>,
30    pub ssn: Option<EncString>,
31    pub username: Option<EncString>,
32    pub passport_number: Option<EncString>,
33    pub license_number: Option<EncString>,
34}
35
36#[allow(missing_docs)]
37#[derive(Serialize, Deserialize, Debug, Clone)]
38#[serde(rename_all = "camelCase", deny_unknown_fields)]
39#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
40#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
41pub struct IdentityView {
42    pub title: Option<String>,
43    pub first_name: Option<String>,
44    pub middle_name: Option<String>,
45    pub last_name: Option<String>,
46    pub address1: Option<String>,
47    pub address2: Option<String>,
48    pub address3: Option<String>,
49    pub city: Option<String>,
50    pub state: Option<String>,
51    pub postal_code: Option<String>,
52    pub country: Option<String>,
53    pub company: Option<String>,
54    pub email: Option<String>,
55    pub phone: Option<String>,
56    pub ssn: Option<String>,
57    pub username: Option<String>,
58    pub passport_number: Option<String>,
59    pub license_number: Option<String>,
60}
61
62impl Encryptable<KeyIds, SymmetricKeyId, Identity> for IdentityView {
63    fn encrypt(
64        &self,
65        ctx: &mut KeyStoreContext<KeyIds>,
66        key: SymmetricKeyId,
67    ) -> Result<Identity, CryptoError> {
68        Ok(Identity {
69            title: self.title.encrypt(ctx, key)?,
70            first_name: self.first_name.encrypt(ctx, key)?,
71            middle_name: self.middle_name.encrypt(ctx, key)?,
72            last_name: self.last_name.encrypt(ctx, key)?,
73            address1: self.address1.encrypt(ctx, key)?,
74            address2: self.address2.encrypt(ctx, key)?,
75            address3: self.address3.encrypt(ctx, key)?,
76            city: self.city.encrypt(ctx, key)?,
77            state: self.state.encrypt(ctx, key)?,
78            postal_code: self.postal_code.encrypt(ctx, key)?,
79            country: self.country.encrypt(ctx, key)?,
80            company: self.company.encrypt(ctx, key)?,
81            email: self.email.encrypt(ctx, key)?,
82            phone: self.phone.encrypt(ctx, key)?,
83            ssn: self.ssn.encrypt(ctx, key)?,
84            username: self.username.encrypt(ctx, key)?,
85            passport_number: self.passport_number.encrypt(ctx, key)?,
86            license_number: self.license_number.encrypt(ctx, key)?,
87        })
88    }
89}
90
91impl Decryptable<KeyIds, SymmetricKeyId, IdentityView> for Identity {
92    fn decrypt(
93        &self,
94        ctx: &mut KeyStoreContext<KeyIds>,
95        key: SymmetricKeyId,
96    ) -> Result<IdentityView, CryptoError> {
97        Ok(IdentityView {
98            title: self.title.decrypt(ctx, key).ok().flatten(),
99            first_name: self.first_name.decrypt(ctx, key).ok().flatten(),
100            middle_name: self.middle_name.decrypt(ctx, key).ok().flatten(),
101            last_name: self.last_name.decrypt(ctx, key).ok().flatten(),
102            address1: self.address1.decrypt(ctx, key).ok().flatten(),
103            address2: self.address2.decrypt(ctx, key).ok().flatten(),
104            address3: self.address3.decrypt(ctx, key).ok().flatten(),
105            city: self.city.decrypt(ctx, key).ok().flatten(),
106            state: self.state.decrypt(ctx, key).ok().flatten(),
107            postal_code: self.postal_code.decrypt(ctx, key).ok().flatten(),
108            country: self.country.decrypt(ctx, key).ok().flatten(),
109            company: self.company.decrypt(ctx, key).ok().flatten(),
110            email: self.email.decrypt(ctx, key).ok().flatten(),
111            phone: self.phone.decrypt(ctx, key).ok().flatten(),
112            ssn: self.ssn.decrypt(ctx, key).ok().flatten(),
113            username: self.username.decrypt(ctx, key).ok().flatten(),
114            passport_number: self.passport_number.decrypt(ctx, key).ok().flatten(),
115            license_number: self.license_number.decrypt(ctx, key).ok().flatten(),
116        })
117    }
118}
119
120impl TryFrom<CipherIdentityModel> for Identity {
121    type Error = VaultParseError;
122
123    fn try_from(identity: CipherIdentityModel) -> Result<Self, Self::Error> {
124        Ok(Self {
125            title: EncString::try_from_optional(identity.title)?,
126            first_name: EncString::try_from_optional(identity.first_name)?,
127            middle_name: EncString::try_from_optional(identity.middle_name)?,
128            last_name: EncString::try_from_optional(identity.last_name)?,
129            address1: EncString::try_from_optional(identity.address1)?,
130            address2: EncString::try_from_optional(identity.address2)?,
131            address3: EncString::try_from_optional(identity.address3)?,
132            city: EncString::try_from_optional(identity.city)?,
133            state: EncString::try_from_optional(identity.state)?,
134            postal_code: EncString::try_from_optional(identity.postal_code)?,
135            country: EncString::try_from_optional(identity.country)?,
136            company: EncString::try_from_optional(identity.company)?,
137            email: EncString::try_from_optional(identity.email)?,
138            phone: EncString::try_from_optional(identity.phone)?,
139            ssn: EncString::try_from_optional(identity.ssn)?,
140            username: EncString::try_from_optional(identity.username)?,
141            passport_number: EncString::try_from_optional(identity.passport_number)?,
142            license_number: EncString::try_from_optional(identity.license_number)?,
143        })
144    }
145}
146
147impl CipherKind for Identity {
148    fn decrypt_subtitle(
149        &self,
150        ctx: &mut KeyStoreContext<KeyIds>,
151        key: SymmetricKeyId,
152    ) -> Result<String, CryptoError> {
153        let first_name = self
154            .first_name
155            .as_ref()
156            .map(|f| f.decrypt(ctx, key))
157            .transpose()?;
158        let last_name = self
159            .last_name
160            .as_ref()
161            .map(|l| l.decrypt(ctx, key))
162            .transpose()?;
163
164        Ok(build_subtitle_identity(first_name, last_name))
165    }
166
167    fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
168        [
169            self.username
170                .as_ref()
171                .map(|_| CopyableCipherFields::IdentityUsername),
172            self.email
173                .as_ref()
174                .map(|_| CopyableCipherFields::IdentityEmail),
175            self.phone
176                .as_ref()
177                .map(|_| CopyableCipherFields::IdentityPhone),
178            self.address1
179                .as_ref()
180                .or(self.address2.as_ref())
181                .or(self.address3.as_ref())
182                .or(self.city.as_ref())
183                .or(self.state.as_ref())
184                .or(self.postal_code.as_ref())
185                .map(|_| CopyableCipherFields::IdentityAddress),
186        ]
187        .into_iter()
188        .flatten()
189        .collect()
190    }
191}
192
193/// Builds the subtitle for a card cipher
194fn build_subtitle_identity(first_name: Option<String>, last_name: Option<String>) -> String {
195    let len = match (first_name.as_ref(), last_name.as_ref()) {
196        (Some(first_name), Some(last_name)) => first_name.len() + last_name.len() + 1,
197        (Some(first_name), None) => first_name.len(),
198        (None, Some(last_name)) => last_name.len(),
199        (None, None) => 0,
200    };
201
202    let mut subtitle = String::with_capacity(len);
203
204    if let Some(first_name) = &first_name {
205        subtitle.push_str(first_name);
206    }
207
208    if let Some(last_name) = &last_name {
209        if !subtitle.is_empty() {
210            subtitle.push(' ');
211        }
212        subtitle.push_str(last_name);
213    }
214
215    subtitle
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::cipher::cipher::CopyableCipherFields;
222
223    fn create_identity() -> Identity {
224        Identity {
225            title: None,
226            first_name: None,
227            middle_name: None,
228            last_name: None,
229            address1: None,
230            address2: None,
231            address3: None,
232            city: None,
233            state: None,
234            postal_code: None,
235            country: None,
236            company: None,
237            email: None,
238            phone: None,
239            ssn: None,
240            username: None,
241            passport_number: None,
242            license_number: None,
243        }
244    }
245
246    #[test]
247    fn test_build_subtitle_identity() {
248        let first_name = Some("John".to_owned());
249        let last_name = Some("Doe".to_owned());
250
251        let subtitle = build_subtitle_identity(first_name, last_name);
252        assert_eq!(subtitle, "John Doe");
253    }
254
255    #[test]
256    fn test_build_subtitle_identity_only_first() {
257        let first_name = Some("John".to_owned());
258        let last_name = None;
259
260        let subtitle = build_subtitle_identity(first_name, last_name);
261        assert_eq!(subtitle, "John");
262    }
263
264    #[test]
265    fn test_build_subtitle_identity_only_last() {
266        let first_name = None;
267        let last_name = Some("Doe".to_owned());
268
269        let subtitle = build_subtitle_identity(first_name, last_name);
270        assert_eq!(subtitle, "Doe");
271    }
272
273    #[test]
274    fn test_build_subtitle_identity_none() {
275        let first_name = None;
276        let last_name = None;
277
278        let subtitle = build_subtitle_identity(first_name, last_name);
279        assert_eq!(subtitle, "");
280    }
281
282    #[test]
283    fn test_get_copyable_fields_identity_empty() {
284        let identity = create_identity();
285
286        let copyable_fields = identity.get_copyable_fields(None);
287        assert_eq!(copyable_fields, vec![]);
288    }
289
290    #[test]
291    fn test_get_copyable_fields_identity_has_username() {
292        let mut identity = create_identity();
293        identity.username = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
294
295        let copyable_fields = identity.get_copyable_fields(None);
296        assert_eq!(
297            copyable_fields,
298            vec![CopyableCipherFields::IdentityUsername]
299        );
300    }
301
302    #[test]
303    fn test_get_copyable_fields_identity_has_email() {
304        let mut identity = create_identity();
305        identity.email = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
306
307        let copyable_fields = identity.get_copyable_fields(None);
308        assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityEmail]);
309    }
310
311    #[test]
312    fn test_get_copyable_fields_identity_has_phone() {
313        let mut identity = create_identity();
314        identity.phone = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
315
316        let copyable_fields = identity.get_copyable_fields(None);
317        assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityPhone]);
318    }
319
320    #[test]
321    fn test_get_copyable_fields_identity_has_address() {
322        let mut identity = create_identity();
323
324        identity.address1 = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
325
326        let mut copyable_fields = identity.get_copyable_fields(None);
327
328        assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
329
330        identity.state = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
331        identity.address1 = None;
332
333        copyable_fields = identity.get_copyable_fields(None);
334        assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
335    }
336}