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 Ok(build_subtitle_drivers_license(first_name, last_name))
112 }
113
114 fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
115 [
116 self.first_name
117 .as_ref()
118 .map(|_| CopyableCipherFields::DriversLicenseFirstName),
119 self.middle_name
120 .as_ref()
121 .map(|_| CopyableCipherFields::DriversLicenseMiddleName),
122 self.last_name
123 .as_ref()
124 .map(|_| CopyableCipherFields::DriversLicenseLastName),
125 self.license_number
126 .as_ref()
127 .map(|_| CopyableCipherFields::DriversLicenseLicenseNumber),
128 ]
129 .into_iter()
130 .flatten()
131 .collect()
132 }
133}
134
135pub(super) fn build_subtitle_drivers_license(
137 first_name: Option<String>,
138 last_name: Option<String>,
139) -> String {
140 [first_name, last_name]
141 .into_iter()
142 .flatten()
143 .filter(|s| !s.is_empty())
144 .collect::<Vec<_>>()
145 .join(" ")
146}
147
148impl TryFrom<CipherDriversLicenseModel> for DriversLicense {
149 type Error = VaultParseError;
150
151 fn try_from(dl: CipherDriversLicenseModel) -> Result<Self, Self::Error> {
152 Ok(Self {
153 first_name: EncString::try_from_optional(dl.first_name)?,
154 middle_name: EncString::try_from_optional(dl.middle_name)?,
155 last_name: EncString::try_from_optional(dl.last_name)?,
156 date_of_birth: EncString::try_from_optional(dl.date_of_birth)?,
157 license_number: EncString::try_from_optional(dl.license_number)?,
158 issuing_country: EncString::try_from_optional(dl.issuing_country)?,
159 issuing_state: EncString::try_from_optional(dl.issuing_state)?,
160 issue_date: EncString::try_from_optional(dl.issue_date)?,
161 expiration_date: EncString::try_from_optional(dl.expiration_date)?,
162 issuing_authority: EncString::try_from_optional(dl.issuing_authority)?,
163 license_class: EncString::try_from_optional(dl.license_class)?,
164 })
165 }
166}
167
168impl From<DriversLicense> for CipherDriversLicenseModel {
169 fn from(dl: DriversLicense) -> Self {
170 Self {
171 first_name: dl.first_name.map(|n| n.to_string()),
172 middle_name: dl.middle_name.map(|n| n.to_string()),
173 last_name: dl.last_name.map(|n| n.to_string()),
174 date_of_birth: dl.date_of_birth.map(|n| n.to_string()),
175 license_number: dl.license_number.map(|n| n.to_string()),
176 issuing_country: dl.issuing_country.map(|n| n.to_string()),
177 issuing_state: dl.issuing_state.map(|n| n.to_string()),
178 issue_date: dl.issue_date.map(|n| n.to_string()),
179 expiration_date: dl.expiration_date.map(|n| n.to_string()),
180 issuing_authority: dl.issuing_authority.map(|n| n.to_string()),
181 license_class: dl.license_class.map(|n| n.to_string()),
182 }
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use bitwarden_core::key_management::create_test_crypto_with_user_key;
189 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
190
191 use super::*;
192 use crate::cipher::cipher::CopyableCipherFields;
193
194 const TEST_VECTOR_DL_KEY: &str =
195 "taqu8EG0R01PCl/p0mM8q2Pz3OmCcw66AEoXF82dwhsIUgSR7Fw7yZNXkjtWNC3qxtjkKsFn8xMg1zwUQplD3Q==";
196 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="}"#;
197
198 fn test_drivers_license_view() -> DriversLicenseView {
199 DriversLicenseView {
200 first_name: Some("John".to_string()),
201 middle_name: Some("Michael".to_string()),
202 last_name: Some("Doe".to_string()),
203 date_of_birth: Some("1985-06-15".to_string()),
204 license_number: Some("DL-987654".to_string()),
205 issuing_country: Some("US".to_string()),
206 issuing_state: Some("NY".to_string()),
207 issue_date: Some("2020-01-01".to_string()),
208 expiration_date: Some("2028-01-01".to_string()),
209 issuing_authority: Some("NY DMV".to_string()),
210 license_class: Some("D".to_string()),
211 }
212 }
213
214 #[test]
215 #[ignore]
216 fn generate_test_vector() {
217 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
218 let key_b64 = key.to_base64();
219 let key_store = create_test_crypto_with_user_key(key);
220 let key_slot = SymmetricKeySlotId::User;
221 let mut ctx = key_store.context();
222
223 let encrypted = test_drivers_license_view()
224 .encrypt_composite(&mut ctx, key_slot)
225 .unwrap();
226 let json = serde_json::to_string(&encrypted).unwrap();
227
228 println!("const TEST_VECTOR_DL_KEY: &str = \"{key_b64}\";");
229 println!("const TEST_VECTOR_DL_JSON: &str = r#\"{json}\"#;");
230 }
231
232 #[test]
233 fn test_recorded_drivers_license_test_vector() {
234 let key =
235 SymmetricCryptoKey::try_from(TEST_VECTOR_DL_KEY.to_string()).expect("valid test key");
236 let key_store = create_test_crypto_with_user_key(key);
237 let key_slot = SymmetricKeySlotId::User;
238 let mut ctx = key_store.context();
239
240 let encrypted: DriversLicense =
241 serde_json::from_str(TEST_VECTOR_DL_JSON).expect("valid test vector JSON");
242 let decrypted: DriversLicenseView = encrypted
243 .decrypt(&mut ctx, key_slot)
244 .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.");
245
246 assert_eq!(decrypted, test_drivers_license_view());
247 }
248
249 #[test]
250 fn test_subtitle_drivers_license() {
251 let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
252 let key_store = create_test_crypto_with_user_key(key);
253 let key = SymmetricKeySlotId::User;
254 let mut ctx = key_store.context();
255
256 let first_name_encrypted = "John".to_owned().encrypt(&mut ctx, key).unwrap();
257 let last_name_encrypted = "Doe".to_owned().encrypt(&mut ctx, key).unwrap();
258
259 let dl = DriversLicense {
260 first_name: Some(first_name_encrypted),
261 middle_name: None,
262 last_name: Some(last_name_encrypted),
263 date_of_birth: None,
264 license_number: None,
265 issuing_country: None,
266 issuing_state: None,
267 issue_date: None,
268 expiration_date: None,
269 issuing_authority: None,
270 license_class: None,
271 };
272
273 assert_eq!(
274 dl.decrypt_subtitle(&mut ctx, key).unwrap(),
275 "John Doe".to_string()
276 );
277 }
278
279 #[test]
280 fn test_get_copyable_fields_drivers_license() {
281 let enc_str: EncString = "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap();
282
283 let dl = DriversLicense {
284 first_name: Some(enc_str.clone()),
285 middle_name: Some(enc_str.clone()),
286 last_name: Some(enc_str.clone()),
287 date_of_birth: Some(enc_str.clone()),
288 license_number: Some(enc_str.clone()),
289 issuing_country: Some(enc_str.clone()),
290 issuing_state: Some(enc_str.clone()),
291 issue_date: Some(enc_str.clone()),
292 expiration_date: Some(enc_str.clone()),
293 issuing_authority: Some(enc_str.clone()),
294 license_class: Some(enc_str),
295 };
296
297 let copyable_fields = dl.get_copyable_fields(None);
298 assert_eq!(
299 copyable_fields,
300 vec![
301 CopyableCipherFields::DriversLicenseFirstName,
302 CopyableCipherFields::DriversLicenseMiddleName,
303 CopyableCipherFields::DriversLicenseLastName,
304 CopyableCipherFields::DriversLicenseLicenseNumber,
305 ]
306 );
307 }
308}