1use bitwarden_api_api::models::CipherPassportModel;
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 Passport {
19 pub surname: Option<EncString>,
20 pub given_name: Option<EncString>,
21 pub date_of_birth: Option<EncString>,
22 pub sex: Option<EncString>,
23 pub birth_place: Option<EncString>,
24 pub nationality: Option<EncString>,
25 pub issuing_country: Option<EncString>,
26 pub passport_number: Option<EncString>,
27 pub passport_type: Option<EncString>,
28 pub national_identification_number: Option<EncString>,
29 pub issuing_authority: Option<EncString>,
30 pub issue_date: Option<EncString>,
31 pub expiration_date: Option<EncString>,
32}
33
34#[allow(missing_docs)]
35#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
36#[serde(rename_all = "camelCase", deny_unknown_fields)]
37#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
38#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
39pub struct PassportView {
40 pub surname: Option<String>,
41 pub given_name: Option<String>,
42 pub date_of_birth: Option<String>,
43 pub sex: Option<String>,
44 pub birth_place: Option<String>,
45 pub nationality: Option<String>,
46 pub issuing_country: Option<String>,
47 pub passport_number: Option<String>,
48 pub passport_type: Option<String>,
49 pub national_identification_number: Option<String>,
50 pub issuing_authority: Option<String>,
51 pub issue_date: Option<String>,
52 pub expiration_date: Option<String>,
53}
54
55impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Passport> for PassportView {
56 fn encrypt_composite(
57 &self,
58 ctx: &mut KeyStoreContext<KeySlotIds>,
59 key: SymmetricKeySlotId,
60 ) -> Result<Passport, CryptoError> {
61 Ok(Passport {
62 surname: self.surname.encrypt(ctx, key)?,
63 given_name: self.given_name.encrypt(ctx, key)?,
64 date_of_birth: self.date_of_birth.encrypt(ctx, key)?,
65 sex: self.sex.encrypt(ctx, key)?,
66 birth_place: self.birth_place.encrypt(ctx, key)?,
67 nationality: self.nationality.encrypt(ctx, key)?,
68 issuing_country: self.issuing_country.encrypt(ctx, key)?,
69 passport_number: self.passport_number.encrypt(ctx, key)?,
70 passport_type: self.passport_type.encrypt(ctx, key)?,
71 national_identification_number: self
72 .national_identification_number
73 .encrypt(ctx, key)?,
74 issuing_authority: self.issuing_authority.encrypt(ctx, key)?,
75 issue_date: self.issue_date.encrypt(ctx, key)?,
76 expiration_date: self.expiration_date.encrypt(ctx, key)?,
77 })
78 }
79}
80
81impl Decryptable<KeySlotIds, SymmetricKeySlotId, PassportView> for Passport {
82 fn decrypt(
83 &self,
84 ctx: &mut KeyStoreContext<KeySlotIds>,
85 key: SymmetricKeySlotId,
86 ) -> Result<PassportView, CryptoError> {
87 Ok(PassportView {
88 surname: self.surname.decrypt(ctx, key).ok().flatten(),
89 given_name: self.given_name.decrypt(ctx, key).ok().flatten(),
90 date_of_birth: self.date_of_birth.decrypt(ctx, key).ok().flatten(),
91 sex: self.sex.decrypt(ctx, key).ok().flatten(),
92 birth_place: self.birth_place.decrypt(ctx, key).ok().flatten(),
93 nationality: self.nationality.decrypt(ctx, key).ok().flatten(),
94 issuing_country: self.issuing_country.decrypt(ctx, key).ok().flatten(),
95 passport_number: self.passport_number.decrypt(ctx, key).ok().flatten(),
96 passport_type: self.passport_type.decrypt(ctx, key).ok().flatten(),
97 national_identification_number: self
98 .national_identification_number
99 .decrypt(ctx, key)
100 .ok()
101 .flatten(),
102 issuing_authority: self.issuing_authority.decrypt(ctx, key).ok().flatten(),
103 issue_date: self.issue_date.decrypt(ctx, key).ok().flatten(),
104 expiration_date: self.expiration_date.decrypt(ctx, key).ok().flatten(),
105 })
106 }
107}
108
109impl CipherKind for Passport {
110 fn decrypt_subtitle(
111 &self,
112 ctx: &mut KeyStoreContext<KeySlotIds>,
113 key: SymmetricKeySlotId,
114 ) -> Result<String, CryptoError> {
115 let given_name: Option<String> = self
116 .given_name
117 .as_ref()
118 .map(|g| g.decrypt(ctx, key))
119 .transpose()?;
120 let surname: Option<String> = self
121 .surname
122 .as_ref()
123 .map(|s| s.decrypt(ctx, key))
124 .transpose()?;
125 Ok(build_subtitle_passport(given_name, surname))
126 }
127
128 fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
129 [
130 self.given_name
131 .as_ref()
132 .map(|_| CopyableCipherFields::PassportGivenName),
133 self.surname
134 .as_ref()
135 .map(|_| CopyableCipherFields::PassportSurname),
136 self.passport_number
137 .as_ref()
138 .map(|_| CopyableCipherFields::PassportPassportNumber),
139 self.national_identification_number
140 .as_ref()
141 .map(|_| CopyableCipherFields::PassportNationalIdentificationNumber),
142 ]
143 .into_iter()
144 .flatten()
145 .collect()
146 }
147}
148
149pub(super) fn build_subtitle_passport(
151 given_name: Option<String>,
152 surname: Option<String>,
153) -> String {
154 [given_name, surname]
155 .into_iter()
156 .flatten()
157 .filter(|s| !s.is_empty())
158 .collect::<Vec<_>>()
159 .join(" ")
160}
161
162impl TryFrom<CipherPassportModel> for Passport {
163 type Error = VaultParseError;
164
165 fn try_from(passport: CipherPassportModel) -> Result<Self, Self::Error> {
166 Ok(Self {
167 surname: EncString::try_from_optional(passport.surname)?,
168 given_name: EncString::try_from_optional(passport.given_name)?,
169 date_of_birth: EncString::try_from_optional(passport.date_of_birth)?,
170 sex: EncString::try_from_optional(passport.sex)?,
171 birth_place: EncString::try_from_optional(passport.birth_place)?,
172 nationality: EncString::try_from_optional(passport.nationality)?,
173 issuing_country: EncString::try_from_optional(passport.issuing_country)?,
174 passport_number: EncString::try_from_optional(passport.passport_number)?,
175 passport_type: EncString::try_from_optional(passport.passport_type)?,
176 national_identification_number: EncString::try_from_optional(
177 passport.national_identification_number,
178 )?,
179 issuing_authority: EncString::try_from_optional(passport.issuing_authority)?,
180 issue_date: EncString::try_from_optional(passport.issue_date)?,
181 expiration_date: EncString::try_from_optional(passport.expiration_date)?,
182 })
183 }
184}
185
186impl From<Passport> for CipherPassportModel {
187 fn from(passport: Passport) -> Self {
188 Self {
189 surname: passport.surname.map(|n| n.to_string()),
190 given_name: passport.given_name.map(|n| n.to_string()),
191 date_of_birth: passport.date_of_birth.map(|n| n.to_string()),
192 sex: passport.sex.map(|n| n.to_string()),
193 birth_place: passport.birth_place.map(|n| n.to_string()),
194 nationality: passport.nationality.map(|n| n.to_string()),
195 issuing_country: passport.issuing_country.map(|n| n.to_string()),
196 passport_number: passport.passport_number.map(|n| n.to_string()),
197 passport_type: passport.passport_type.map(|n| n.to_string()),
198 national_identification_number: passport
199 .national_identification_number
200 .map(|n| n.to_string()),
201 issuing_authority: passport.issuing_authority.map(|n| n.to_string()),
202 issue_date: passport.issue_date.map(|n| n.to_string()),
203 expiration_date: passport.expiration_date.map(|n| n.to_string()),
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use bitwarden_core::key_management::create_test_crypto_with_user_key;
211 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
212
213 use super::*;
214 use crate::cipher::cipher::CopyableCipherFields;
215
216 const TEST_VECTOR_PASSPORT_KEY: &str =
217 "eG6fDUuqSQFpX6ACdw2u65PkLKDiOk9aQfUgUC/BTU3+UFpAPP/CtE7b+ZdeK3SC2z5+rWvhm537jV3qIbzIBA==";
218 const TEST_VECTOR_PASSPORT_JSON: &str = r#"{"surname":"2.eQaSEApRodSjkXYijwVKog==|2gQz6ddMxUt3p400Axj43w==|XR6WmolSiIMD/DhF0SBZr7Qjb56fgSlQWyH8fb8LiL4=","givenName":"2.hS8vFkgoKxe31l8tOJT6vA==|DuciBPcIw4TgX7kobPc6dQ==|oRbxAqSJx5T5KBcPLhHhhJnQKjE5FjORIe9K41rXJKg=","dateOfBirth":"2.EQAdb/+PO6qGNTqYaufPYw==|dbUIcfU2w+zrtbxKcx1j2w==|F7AVClD4oXjS+1bqZtF8ociq/Gk8gPCi7cvtvjRNhF4=","sex":"2.pz3uUyCEKc5+1njtMm3lwg==|vB2Nyw9Dp/xKoTt/qPe30Q==|GuVXp0o6dxpV3iEdi2ifR5dTTd8XsiGE7LYFrpKVIIs=","birthPlace":"2.lGurnrRwJlAjugpdo9CdxQ==|kHdvvlXOsdgUNAMfUvIMoQ==|7pX2hZqP9S8srwEEzlZ2ZZCC2LUUzTiaHT/WBrPzb4A=","nationality":"2.0+Mjb6gfkirfw/cmHNIT8g==|GFLjpeEbID6jBS3a7/vvEA==|k0v1ArEI+iApsSj9UtucdsWD4hoM6vUjlHvRCI7bIz8=","issuingCountry":"2.PQhrBI1z8AJSzU9C07RkFA==|5OP/JHxFhEF/P8sKaTmwuA==|07Bfry57nZRyejesniXXt4xhgr4CN2JOSLVUHoDRL8Q=","passportNumber":"2.W+F0jcn8wY3W5d91zPwMTQ==|ny+5nUgNuiZ3SJR3Xbvsgg==|haWOAdcDbhs95hOsFknn5ABFbYm9B0Z7DoLN1hLaYEk=","passportType":"2.HO4v3tQ0QjZfazo5NxleZA==|fra59Rz9NKyrhoasK0UrEg==|n30Gb2pqLWtM8dzkfhsels76k9+7oWWqicK4kA10Hlw=","nationalIdentificationNumber":"2.+8kGWruSfQMZdXeB3X2s8Q==|KJp8B2lLNrmBFsCjN0u5dw==|zgW3uKX75k8/oFiEJEoXbcqeqiNI2jZZEeR8VQjF4jE=","issuingAuthority":"2.vDHido+bznW03M/CM9uY8g==|ryQ4ohHijQeEI8Fh3Rv1tA1eeCUqCsMuIGdGawJzSmo=|XbH99VX1LqpgVwzom6aHTsEUgoe5NekwRi0Fg7fKD8o=","issueDate":"2.rvY/6OVEEGxhLVMgFhT4gQ==|9/g9m7753eM2B3m6raLghw==|IsVUOXJ5r/J/P5M2kq5MJ7viDVZbkMP2opggVCiImmY=","expirationDate":"2.yIMuc7J5yxerSuZaYY0sbQ==|VIGDsMYFoUmJaVZ/ZR+gtg==|rMWvQHK24zUqZQnJgTNL58W71/ZJA/Dx3ernx4JRVVQ="}"#;
219
220 fn test_passport_view() -> PassportView {
221 PassportView {
222 surname: Some("Doe".to_string()),
223 given_name: Some("Jane".to_string()),
224 date_of_birth: Some("1990-01-01".to_string()),
225 sex: Some("F".to_string()),
226 birth_place: Some("New York".to_string()),
227 nationality: Some("American".to_string()),
228 issuing_country: Some("US".to_string()),
229 passport_number: Some("P12345678".to_string()),
230 passport_type: Some("P".to_string()),
231 national_identification_number: Some("123-45-6789".to_string()),
232 issuing_authority: Some("US State Department".to_string()),
233 issue_date: Some("2020-01-01".to_string()),
234 expiration_date: Some("2030-01-01".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_passport_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_PASSPORT_KEY: &str = \"{key_b64}\";");
253 println!("const TEST_VECTOR_PASSPORT_JSON: &str = r#\"{json}\"#;");
254 }
255
256 #[test]
257 fn test_recorded_passport_test_vector() {
258 let key = SymmetricCryptoKey::try_from(TEST_VECTOR_PASSPORT_KEY.to_string())
259 .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: Passport =
265 serde_json::from_str(TEST_VECTOR_PASSPORT_JSON).expect("valid test vector JSON");
266 let decrypted: PassportView = encrypted
267 .decrypt(&mut ctx, key_slot)
268 .expect("Passport 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_passport_view());
271 }
272
273 #[test]
274 fn test_subtitle_passport() {
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 given_name_encrypted = "Jane".to_owned().encrypt(&mut ctx, key).unwrap();
281 let surname_encrypted = "Doe".to_owned().encrypt(&mut ctx, key).unwrap();
282
283 let passport = Passport {
284 surname: Some(surname_encrypted),
285 given_name: Some(given_name_encrypted),
286 date_of_birth: None,
287 sex: None,
288 birth_place: None,
289 nationality: None,
290 issuing_country: None,
291 passport_number: None,
292 passport_type: None,
293 national_identification_number: None,
294 issuing_authority: None,
295 issue_date: None,
296 expiration_date: None,
297 };
298
299 assert_eq!(
300 passport.decrypt_subtitle(&mut ctx, key).unwrap(),
301 "Jane Doe".to_string()
302 );
303 }
304
305 #[test]
306 fn test_get_copyable_fields_passport() {
307 let enc_str: EncString = "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap();
308
309 let passport = Passport {
310 surname: Some(enc_str.clone()),
311 given_name: Some(enc_str.clone()),
312 date_of_birth: Some(enc_str.clone()),
313 sex: Some(enc_str.clone()),
314 birth_place: Some(enc_str.clone()),
315 nationality: Some(enc_str.clone()),
316 issuing_country: Some(enc_str.clone()),
317 passport_number: Some(enc_str.clone()),
318 passport_type: Some(enc_str.clone()),
319 national_identification_number: Some(enc_str.clone()),
320 issuing_authority: Some(enc_str.clone()),
321 issue_date: Some(enc_str.clone()),
322 expiration_date: Some(enc_str),
323 };
324
325 let copyable_fields = passport.get_copyable_fields(None);
326 assert_eq!(
327 copyable_fields,
328 vec![
329 CopyableCipherFields::PassportGivenName,
330 CopyableCipherFields::PassportSurname,
331 CopyableCipherFields::PassportPassportNumber,
332 CopyableCipherFields::PassportNationalIdentificationNumber,
333 ]
334 );
335 }
336}