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