Skip to main content

bitwarden_vault/cipher/
drivers_license.rs

1use bitwarden_api_api::models::CipherDriversLicenseModel;
2use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
3use bitwarden_crypto::{
4    CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext,
5    PrimitiveEncryptable,
6};
7use chrono::NaiveDate;
8use serde::{Deserialize, Serialize};
9#[cfg(feature = "wasm")]
10use tsify::Tsify;
11
12use super::cipher::CipherKind;
13use crate::{Cipher, VaultParseError, cipher::cipher::CopyableCipherFields};
14
15#[derive(Serialize, Deserialize, Debug, Clone)]
16#[serde(rename_all = "camelCase")]
17#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
18#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
19pub struct DriversLicense {
20    pub first_name: Option<EncString>,
21    pub middle_name: Option<EncString>,
22    pub last_name: Option<EncString>,
23    pub date_of_birth: Option<EncString>,
24    pub license_number: Option<EncString>,
25    pub issuing_country: Option<EncString>,
26    pub issuing_state: Option<EncString>,
27    pub issue_date: Option<EncString>,
28    pub expiration_date: Option<EncString>,
29    pub issuing_authority: Option<EncString>,
30    pub license_class: Option<EncString>,
31}
32
33#[allow(missing_docs)]
34#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
35#[serde(rename_all = "camelCase", deny_unknown_fields)]
36#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
37#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
38pub struct DriversLicenseView {
39    pub first_name: Option<String>,
40    pub middle_name: Option<String>,
41    pub last_name: Option<String>,
42    pub date_of_birth: Option<NaiveDate>,
43    pub license_number: Option<String>,
44    pub issuing_country: Option<String>,
45    pub issuing_state: Option<String>,
46    pub issue_date: Option<NaiveDate>,
47    pub expiration_date: Option<NaiveDate>,
48    pub issuing_authority: Option<String>,
49    pub license_class: Option<String>,
50}
51
52impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, DriversLicense> for DriversLicenseView {
53    fn encrypt_composite(
54        &self,
55        ctx: &mut KeyStoreContext<KeySlotIds>,
56        key: SymmetricKeySlotId,
57    ) -> Result<DriversLicense, CryptoError> {
58        Ok(DriversLicense {
59            first_name: self.first_name.encrypt(ctx, key)?,
60            middle_name: self.middle_name.encrypt(ctx, key)?,
61            last_name: self.last_name.encrypt(ctx, key)?,
62            date_of_birth: self
63                .date_of_birth
64                .map(|d| d.to_string())
65                .encrypt(ctx, key)?,
66            license_number: self.license_number.encrypt(ctx, key)?,
67            issuing_country: self.issuing_country.encrypt(ctx, key)?,
68            issuing_state: self.issuing_state.encrypt(ctx, key)?,
69            issue_date: self.issue_date.map(|d| d.to_string()).encrypt(ctx, key)?,
70            expiration_date: self
71                .expiration_date
72                .map(|d| d.to_string())
73                .encrypt(ctx, key)?,
74            issuing_authority: self.issuing_authority.encrypt(ctx, key)?,
75            license_class: self.license_class.encrypt(ctx, key)?,
76        })
77    }
78}
79
80impl Decryptable<KeySlotIds, SymmetricKeySlotId, DriversLicenseView> for DriversLicense {
81    fn decrypt(
82        &self,
83        ctx: &mut KeyStoreContext<KeySlotIds>,
84        key: SymmetricKeySlotId,
85    ) -> Result<DriversLicenseView, CryptoError> {
86        Ok(DriversLicenseView {
87            first_name: self.first_name.decrypt(ctx, key).ok().flatten(),
88            middle_name: self.middle_name.decrypt(ctx, key).ok().flatten(),
89            last_name: self.last_name.decrypt(ctx, key).ok().flatten(),
90            date_of_birth: self
91                .date_of_birth
92                .decrypt(ctx, key)
93                .ok()
94                .flatten()
95                .and_then(|s: String| s.parse().ok()),
96            license_number: self.license_number.decrypt(ctx, key).ok().flatten(),
97            issuing_country: self.issuing_country.decrypt(ctx, key).ok().flatten(),
98            issuing_state: self.issuing_state.decrypt(ctx, key).ok().flatten(),
99            issue_date: self
100                .issue_date
101                .decrypt(ctx, key)
102                .ok()
103                .flatten()
104                .and_then(|s: String| s.parse().ok()),
105            expiration_date: self
106                .expiration_date
107                .decrypt(ctx, key)
108                .ok()
109                .flatten()
110                .and_then(|s: String| s.parse().ok()),
111            issuing_authority: self.issuing_authority.decrypt(ctx, key).ok().flatten(),
112            license_class: self.license_class.decrypt(ctx, key).ok().flatten(),
113        })
114    }
115}
116
117impl CipherKind for DriversLicense {
118    fn decrypt_subtitle(
119        &self,
120        ctx: &mut KeyStoreContext<KeySlotIds>,
121        key: SymmetricKeySlotId,
122    ) -> Result<String, CryptoError> {
123        let first_name: Option<String> = self
124            .first_name
125            .as_ref()
126            .map(|f| f.decrypt(ctx, key))
127            .transpose()?;
128        let last_name: Option<String> = self
129            .last_name
130            .as_ref()
131            .map(|l| l.decrypt(ctx, key))
132            .transpose()?;
133        let issuing_state: Option<String> = self
134            .issuing_state
135            .as_ref()
136            .map(|l| l.decrypt(ctx, key))
137            .transpose()?;
138        Ok(build_subtitle_drivers_license(
139            first_name,
140            last_name,
141            issuing_state,
142        ))
143    }
144
145    fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
146        [
147            self.first_name
148                .as_ref()
149                .map(|_| CopyableCipherFields::DriversLicenseFirstName),
150            self.middle_name
151                .as_ref()
152                .map(|_| CopyableCipherFields::DriversLicenseMiddleName),
153            self.last_name
154                .as_ref()
155                .map(|_| CopyableCipherFields::DriversLicenseLastName),
156            self.license_number
157                .as_ref()
158                .map(|_| CopyableCipherFields::DriversLicenseLicenseNumber),
159        ]
160        .into_iter()
161        .flatten()
162        .collect()
163    }
164}
165
166/// Builds the subtitle for a driver's license cipher
167pub(super) fn build_subtitle_drivers_license(
168    first_name: Option<String>,
169    last_name: Option<String>,
170    issuing_state: Option<String>,
171) -> String {
172    let mut subtitle = String::new();
173
174    if let Some(first_name) = first_name {
175        subtitle.push_str(&first_name);
176    }
177    if let Some(last_name) = last_name {
178        if !subtitle.is_empty() && !last_name.is_empty() {
179            subtitle.push(' ');
180        }
181        subtitle.push_str(&last_name);
182    }
183
184    if let Some(issuing_state) = issuing_state {
185        if !subtitle.is_empty() && !issuing_state.is_empty() {
186            subtitle.push_str(", ");
187        }
188        subtitle.push_str(&issuing_state);
189    }
190
191    subtitle
192}
193
194impl TryFrom<CipherDriversLicenseModel> for DriversLicense {
195    type Error = VaultParseError;
196
197    fn try_from(dl: CipherDriversLicenseModel) -> Result<Self, Self::Error> {
198        Ok(Self {
199            first_name: EncString::try_from_optional(dl.first_name)?,
200            middle_name: EncString::try_from_optional(dl.middle_name)?,
201            last_name: EncString::try_from_optional(dl.last_name)?,
202            date_of_birth: EncString::try_from_optional(dl.date_of_birth)?,
203            license_number: EncString::try_from_optional(dl.license_number)?,
204            issuing_country: EncString::try_from_optional(dl.issuing_country)?,
205            issuing_state: EncString::try_from_optional(dl.issuing_state)?,
206            issue_date: EncString::try_from_optional(dl.issue_date)?,
207            expiration_date: EncString::try_from_optional(dl.expiration_date)?,
208            issuing_authority: EncString::try_from_optional(dl.issuing_authority)?,
209            license_class: EncString::try_from_optional(dl.license_class)?,
210        })
211    }
212}
213
214impl From<DriversLicense> for CipherDriversLicenseModel {
215    fn from(dl: DriversLicense) -> Self {
216        Self {
217            first_name: dl.first_name.map(|n| n.to_string()),
218            middle_name: dl.middle_name.map(|n| n.to_string()),
219            last_name: dl.last_name.map(|n| n.to_string()),
220            date_of_birth: dl.date_of_birth.map(|n| n.to_string()),
221            license_number: dl.license_number.map(|n| n.to_string()),
222            issuing_country: dl.issuing_country.map(|n| n.to_string()),
223            issuing_state: dl.issuing_state.map(|n| n.to_string()),
224            issue_date: dl.issue_date.map(|n| n.to_string()),
225            expiration_date: dl.expiration_date.map(|n| n.to_string()),
226            issuing_authority: dl.issuing_authority.map(|n| n.to_string()),
227            license_class: dl.license_class.map(|n| n.to_string()),
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use bitwarden_core::key_management::create_test_crypto_with_user_key;
235    use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
236
237    use super::*;
238    use crate::cipher::cipher::CopyableCipherFields;
239
240    const TEST_VECTOR_DL_KEY: &str =
241        "taqu8EG0R01PCl/p0mM8q2Pz3OmCcw66AEoXF82dwhsIUgSR7Fw7yZNXkjtWNC3qxtjkKsFn8xMg1zwUQplD3Q==";
242    const TEST_VECTOR_DL_JSON: &str = r#"{"firstName":"2.knZfEnxppJSnCv2K1JLJZQ==|WdifZ8QIUkFuSeVk8WBlSQ==|OX4LNsv+l0Z2EhqNWMgemTZgMwLs5o8T6Osra9nzmU4=","middleName":"2.QrbWBvz1v1139ab0PXCE0g==|qjpNmAzfm5thbkfsb+inmA==|FVmBwCVB+VCKPGKSTLqBCpQWfYeomO/9K4M80i4Hz74=","lastName":"2.kLvD+H8AvuZ26sZSVXCwJw==|hOpCZQ1pSmRxU+10Mb6itg==|890LMSpvyTPumcaBZV2Q/sa0aU0xWSxHGn6Oz/aUvcY=","dateOfBirth":"2.tz5PMtlTQlGiyhrmtFpkfQ==|q7aKh0RO/3UpuzxkWJj/lw==|W+dL85zWGf6hmZby1rkekwFSiAe3Nlf8JcQ/r8aRvC8=","licenseNumber":"2.tqNWH0mhqCqVkGuylbGJPQ==|d60Z0GfOZrQdnDDRSQSYig==|bVQ9kEO13+pGFr5CnA2AcsXHlKdntsB7dWXxu9dPViQ=","issuingCountry":"2.4c/os2TnGc4lV48zTXtcrA==|48WLHeewxx53cR0oAJrT9Q==|DbdhHEl+ZjFJsAwCJqdx6smENOJ6aa6prOSSzrIaxsw=","issuingState":"2.sXVmW8/M1Dt9of6UR8bOFQ==|Se5KFBLQ0EiUywa3Hll6eg==|pIfsxpZrXh1Z3+VG2HX2sXpQfJ1GrlFq8DyunOr/vk0=","issueDate":"2.NiEamcsCLptp7ZGR5yv+Kw==|iS0jlJFbscygj+8q/E3FWA==|M0Iq6DqgDTI3l/OArBeqtdR4dHXLi87QexEK1H7XwsE=","expirationDate":"2.oEePzQ/7a8bC8y93Wf1cog==|QtbxhibvRGdBctqETfYqgQ==|zQflEdAhXKxZelF7qLbAdJNqhZXG0v331XwdGEzr10Q=","issuingAuthority":"2.prE7jFCIfr0+DU0XnOSXWw==|fyISTE3sQFp1GnmVpaTRGg==|a+i1vTOoPtj0bkvFjRUXdXxkVq2RtOkv6zuMxS+BOQc=","licenseClass":"2.Yk070ToPNCnbxxQ2CPe20w==|4eb1WaAOXenbQcotMhgaCw==|yGkq0dg6b65Nf6WxbOPV/r7MRDKFplcWLQ7sZNmOlCY="}"#;
243
244    fn test_drivers_license_view() -> DriversLicenseView {
245        DriversLicenseView {
246            first_name: Some("John".to_string()),
247            middle_name: Some("Michael".to_string()),
248            last_name: Some("Doe".to_string()),
249            date_of_birth: NaiveDate::from_ymd_opt(1985, 6, 15),
250            license_number: Some("DL-987654".to_string()),
251            issuing_country: Some("US".to_string()),
252            issuing_state: Some("NY".to_string()),
253            issue_date: NaiveDate::from_ymd_opt(2020, 1, 1),
254            expiration_date: NaiveDate::from_ymd_opt(2028, 1, 1),
255            issuing_authority: Some("NY DMV".to_string()),
256            license_class: Some("D".to_string()),
257        }
258    }
259
260    #[test]
261    #[ignore]
262    fn generate_test_vector() {
263        let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
264        let key_b64 = key.to_base64();
265        let key_store = create_test_crypto_with_user_key(key);
266        let key_slot = SymmetricKeySlotId::User;
267        let mut ctx = key_store.context();
268
269        let encrypted = test_drivers_license_view()
270            .encrypt_composite(&mut ctx, key_slot)
271            .unwrap();
272        let json = serde_json::to_string(&encrypted).unwrap();
273
274        println!("const TEST_VECTOR_DL_KEY: &str = \"{key_b64}\";");
275        println!("const TEST_VECTOR_DL_JSON: &str = r#\"{json}\"#;");
276    }
277
278    #[test]
279    fn test_recorded_drivers_license_test_vector() {
280        let key =
281            SymmetricCryptoKey::try_from(TEST_VECTOR_DL_KEY.to_string()).expect("valid test key");
282        let key_store = create_test_crypto_with_user_key(key);
283        let key_slot = SymmetricKeySlotId::User;
284        let mut ctx = key_store.context();
285
286        let encrypted: DriversLicense =
287            serde_json::from_str(TEST_VECTOR_DL_JSON).expect("valid test vector JSON");
288        let decrypted: DriversLicenseView = encrypted
289            .decrypt(&mut ctx, key_slot)
290            .expect("DriversLicense has changed in a backwards-incompatible way. Existing encrypted data must remain decryptable. If a new format is needed, create a new version instead of modifying the existing one.");
291
292        assert_eq!(decrypted, test_drivers_license_view());
293    }
294
295    #[test]
296    fn test_subtitle_drivers_license() {
297        let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
298        let key_store = create_test_crypto_with_user_key(key);
299        let key = SymmetricKeySlotId::User;
300        let mut ctx = key_store.context();
301
302        let first_name_encrypted = "John".to_owned().encrypt(&mut ctx, key).unwrap();
303        let last_name_encrypted = "Doe".to_owned().encrypt(&mut ctx, key).unwrap();
304
305        let dl = DriversLicense {
306            first_name: Some(first_name_encrypted),
307            middle_name: None,
308            last_name: Some(last_name_encrypted),
309            date_of_birth: None,
310            license_number: None,
311            issuing_country: None,
312            issuing_state: None,
313            issue_date: None,
314            expiration_date: None,
315            issuing_authority: None,
316            license_class: None,
317        };
318
319        assert_eq!(
320            dl.decrypt_subtitle(&mut ctx, key).unwrap(),
321            "John Doe".to_string()
322        );
323    }
324
325    #[test]
326    fn test_subtitle_drivers_license_with_issuing_state() {
327        let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
328        let key_store = create_test_crypto_with_user_key(key);
329        let key = SymmetricKeySlotId::User;
330        let mut ctx = key_store.context();
331
332        let first_name_encrypted = "John".to_owned().encrypt(&mut ctx, key).unwrap();
333        let last_name_encrypted = "Doe".to_owned().encrypt(&mut ctx, key).unwrap();
334        let issuing_state_encrypted = "NY".to_owned().encrypt(&mut ctx, key).unwrap();
335
336        let dl = DriversLicense {
337            first_name: Some(first_name_encrypted),
338            middle_name: None,
339            last_name: Some(last_name_encrypted),
340            date_of_birth: None,
341            license_number: None,
342            issuing_country: None,
343            issuing_state: Some(issuing_state_encrypted),
344            issue_date: None,
345            expiration_date: None,
346            issuing_authority: None,
347            license_class: None,
348        };
349
350        assert_eq!(
351            dl.decrypt_subtitle(&mut ctx, key).unwrap(),
352            "John Doe, NY".to_string()
353        );
354    }
355
356    #[test]
357    fn test_build_subtitle_drivers_license() {
358        // All fields present
359        assert_eq!(
360            build_subtitle_drivers_license(
361                Some("John".to_string()),
362                Some("Doe".to_string()),
363                Some("NY".to_string()),
364            ),
365            "John Doe, NY"
366        );
367
368        // Names only, no issuing state
369        assert_eq!(
370            build_subtitle_drivers_license(Some("John".to_string()), Some("Doe".to_string()), None),
371            "John Doe"
372        );
373
374        // Issuing state only
375        assert_eq!(
376            build_subtitle_drivers_license(None, None, Some("NY".to_string())),
377            "NY"
378        );
379
380        // Last name and issuing state, no first name
381        assert_eq!(
382            build_subtitle_drivers_license(None, Some("Doe".to_string()), Some("NY".to_string())),
383            "Doe, NY"
384        );
385
386        // Empty strings are treated as absent for separators
387        assert_eq!(
388            build_subtitle_drivers_license(
389                Some("".to_string()),
390                Some("".to_string()),
391                Some("NY".to_string()),
392            ),
393            "NY"
394        );
395        assert_eq!(
396            build_subtitle_drivers_license(
397                Some("John".to_string()),
398                Some("".to_string()),
399                Some("NY".to_string()),
400            ),
401            "John, NY"
402        );
403
404        // Nothing present
405        assert_eq!(build_subtitle_drivers_license(None, None, None), "");
406    }
407
408    #[test]
409    fn test_get_copyable_fields_drivers_license() {
410        let enc_str: EncString = "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap();
411
412        let dl = DriversLicense {
413            first_name: Some(enc_str.clone()),
414            middle_name: Some(enc_str.clone()),
415            last_name: Some(enc_str.clone()),
416            date_of_birth: Some(enc_str.clone()),
417            license_number: Some(enc_str.clone()),
418            issuing_country: Some(enc_str.clone()),
419            issuing_state: Some(enc_str.clone()),
420            issue_date: Some(enc_str.clone()),
421            expiration_date: Some(enc_str.clone()),
422            issuing_authority: Some(enc_str.clone()),
423            license_class: Some(enc_str),
424        };
425
426        let copyable_fields = dl.get_copyable_fields(None);
427        assert_eq!(
428            copyable_fields,
429            vec![
430                CopyableCipherFields::DriversLicenseFirstName,
431                CopyableCipherFields::DriversLicenseMiddleName,
432                CopyableCipherFields::DriversLicenseLastName,
433                CopyableCipherFields::DriversLicenseLicenseNumber,
434            ]
435        );
436    }
437}