Skip to main content

bitwarden_vault/cipher/
passport.rs

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
149/// Builds the subtitle for a passport cipher
150pub(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}