Skip to main content

bitwarden_vault/cipher/blob/conversions/
mod.rs

1use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
2use bitwarden_crypto::{CompositeEncryptable, CryptoError, Decryptable, KeyStoreContext};
3
4use super::v1::*;
5use crate::{
6    CipherView, PasswordHistoryView,
7    cipher::{
8        bank_account::BankAccountView,
9        card::CardView,
10        cipher::CipherType,
11        field::FieldView,
12        identity::IdentityView,
13        login::{Fido2CredentialFullView, LoginUriView, LoginView},
14        secure_note::SecureNoteView,
15        ssh_key::SshKeyView,
16    },
17};
18
19fn none_if_empty<T>(v: Vec<T>) -> Option<Vec<T>> {
20    if v.is_empty() { None } else { Some(v) }
21}
22
23/// Generates bidirectional `From` impls between two types that share
24/// identical field names. Every field is `.clone()`d.
25macro_rules! impl_bidirectional_from {
26    ($type_a:ty, $type_b:ty, [$($field:ident),+ $(,)?]) => {
27        impl From<&$type_a> for $type_b {
28            fn from(src: &$type_a) -> Self {
29                Self { $($field: src.$field.clone()),+ }
30            }
31        }
32        impl From<&$type_b> for $type_a {
33            fn from(src: &$type_b) -> Self {
34                Self { $($field: src.$field.clone()),+ }
35            }
36        }
37    };
38}
39
40impl_bidirectional_from!(FieldView, FieldDataV1, [name, value, r#type, linked_id,]);
41
42impl_bidirectional_from!(
43    PasswordHistoryView,
44    PasswordHistoryDataV1,
45    [password, last_used_date,]
46);
47
48mod bank_account;
49mod card;
50mod identity;
51mod login;
52mod secure_note;
53mod ssh_key;
54
55impl CipherBlobV1 {
56    #[allow(dead_code)]
57    pub(crate) fn from_cipher_view(
58        view: &CipherView,
59        ctx: &mut KeyStoreContext<KeySlotIds>,
60        key: SymmetricKeySlotId,
61    ) -> Result<Self, CryptoError> {
62        let type_data = match view.r#type {
63            CipherType::Login => {
64                let login = view
65                    .login
66                    .as_ref()
67                    .ok_or(CryptoError::MissingField("login"))?;
68
69                let fido2_credentials: Vec<Fido2CredentialDataV1> = login
70                    .fido2_credentials
71                    .as_ref()
72                    .map(|creds| -> Result<Vec<_>, CryptoError> {
73                        let full_views: Vec<Fido2CredentialFullView> = creds.decrypt(ctx, key)?;
74                        Ok(full_views.iter().map(Fido2CredentialDataV1::from).collect())
75                    })
76                    .transpose()?
77                    .unwrap_or_default();
78
79                CipherTypeDataV1::Login(LoginDataV1 {
80                    username: login.username.clone(),
81                    password: login.password.clone(),
82                    password_revision_date: login.password_revision_date,
83                    uris: login
84                        .uris
85                        .as_ref()
86                        .map(|u| u.iter().map(LoginUriDataV1::from).collect())
87                        .unwrap_or_default(),
88                    totp: login.totp.clone(),
89                    autofill_on_page_load: login.autofill_on_page_load,
90                    fido2_credentials,
91                })
92            }
93            CipherType::Card => {
94                let card = view
95                    .card
96                    .as_ref()
97                    .ok_or(CryptoError::MissingField("card"))?;
98                CipherTypeDataV1::Card(CardDataV1::from(card))
99            }
100            CipherType::Identity => {
101                let identity = view
102                    .identity
103                    .as_ref()
104                    .ok_or(CryptoError::MissingField("identity"))?;
105                CipherTypeDataV1::Identity(IdentityDataV1::from(identity))
106            }
107            CipherType::SecureNote => {
108                let secure_note = view
109                    .secure_note
110                    .as_ref()
111                    .ok_or(CryptoError::MissingField("secure_note"))?;
112                CipherTypeDataV1::SecureNote(SecureNoteDataV1::from(secure_note))
113            }
114            CipherType::SshKey => {
115                let ssh_key = view
116                    .ssh_key
117                    .as_ref()
118                    .ok_or(CryptoError::MissingField("ssh_key"))?;
119                CipherTypeDataV1::SshKey(SshKeyDataV1::from(ssh_key))
120            }
121            CipherType::BankAccount => {
122                let bank_account = view
123                    .bank_account
124                    .as_ref()
125                    .ok_or(CryptoError::MissingField("bank_account"))?;
126                CipherTypeDataV1::BankAccount(BankAccountDataV1::from(bank_account))
127            }
128        };
129
130        Ok(Self {
131            name: view.name.clone(),
132            notes: view.notes.clone(),
133            type_data,
134            fields: view
135                .fields
136                .as_ref()
137                .map(|f| f.iter().map(FieldDataV1::from).collect())
138                .unwrap_or_default(),
139            password_history: view
140                .password_history
141                .as_ref()
142                .map(|h| h.iter().map(PasswordHistoryDataV1::from).collect())
143                .unwrap_or_default(),
144        })
145    }
146
147    #[allow(dead_code)]
148    pub(crate) fn apply_to_cipher_view(
149        &self,
150        view: &mut CipherView,
151        ctx: &mut KeyStoreContext<KeySlotIds>,
152        key: SymmetricKeySlotId,
153    ) -> Result<(), CryptoError> {
154        view.name = self.name.clone();
155        view.notes = self.notes.clone();
156        view.fields = none_if_empty(self.fields.iter().map(FieldView::from).collect());
157        view.password_history = none_if_empty(
158            self.password_history
159                .iter()
160                .map(PasswordHistoryView::from)
161                .collect(),
162        );
163
164        view.login = None;
165        view.card = None;
166        view.identity = None;
167        view.secure_note = None;
168        view.ssh_key = None;
169        view.bank_account = None;
170
171        match &self.type_data {
172            CipherTypeDataV1::Login(login_data) => {
173                let fido2_credentials = if login_data.fido2_credentials.is_empty() {
174                    None
175                } else {
176                    let full_views: Vec<Fido2CredentialFullView> = login_data
177                        .fido2_credentials
178                        .iter()
179                        .map(Fido2CredentialFullView::from)
180                        .collect();
181                    Some(full_views.encrypt_composite(ctx, key)?)
182                };
183
184                view.r#type = CipherType::Login;
185                view.login = Some(LoginView {
186                    username: login_data.username.clone(),
187                    password: login_data.password.clone(),
188                    password_revision_date: login_data.password_revision_date,
189                    uris: none_if_empty(login_data.uris.iter().map(LoginUriView::from).collect()),
190                    totp: login_data.totp.clone(),
191                    autofill_on_page_load: login_data.autofill_on_page_load,
192                    fido2_credentials,
193                });
194            }
195            CipherTypeDataV1::Card(card_data) => {
196                view.r#type = CipherType::Card;
197                view.card = Some(CardView::from(card_data));
198            }
199            CipherTypeDataV1::Identity(identity_data) => {
200                view.r#type = CipherType::Identity;
201                view.identity = Some(IdentityView::from(identity_data));
202            }
203            CipherTypeDataV1::SecureNote(secure_note_data) => {
204                view.r#type = CipherType::SecureNote;
205                view.secure_note = Some(SecureNoteView::from(secure_note_data));
206            }
207            CipherTypeDataV1::SshKey(ssh_key_data) => {
208                view.r#type = CipherType::SshKey;
209                view.ssh_key = Some(SshKeyView::from(ssh_key_data));
210            }
211            CipherTypeDataV1::BankAccount(bank_account_data) => {
212                view.r#type = CipherType::BankAccount;
213                view.bank_account = Some(BankAccountView::from(bank_account_data));
214            }
215        }
216
217        Ok(())
218    }
219}
220
221#[cfg(test)]
222mod test_support {
223    use bitwarden_core::key_management::{
224        KeySlotIds, SymmetricKeySlotId, create_test_crypto_with_user_key,
225    };
226    use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
227    use chrono::{TimeZone, Utc};
228
229    use crate::{
230        CipherView,
231        cipher::cipher::{CipherRepromptType, CipherType},
232    };
233
234    pub(crate) fn create_test_key_store() -> (KeyStore<KeySlotIds>, SymmetricKeySlotId) {
235        let key = SymmetricCryptoKey::try_from(
236            "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string(),
237        )
238        .unwrap();
239        let key_store = create_test_crypto_with_user_key(key);
240        (key_store, SymmetricKeySlotId::User)
241    }
242
243    pub(crate) fn create_shell_cipher_view(cipher_type: CipherType) -> CipherView {
244        CipherView {
245            id: None,
246            organization_id: None,
247            folder_id: None,
248            collection_ids: vec![],
249            key: None,
250            name: String::new(),
251            notes: None,
252            r#type: cipher_type,
253            login: None,
254            identity: None,
255            card: None,
256            secure_note: None,
257            ssh_key: None,
258            bank_account: None,
259            favorite: false,
260            reprompt: CipherRepromptType::None,
261            organization_use_totp: false,
262            edit: true,
263            permissions: None,
264            view_password: true,
265            local_data: None,
266            attachments: None,
267            attachment_decryption_failures: None,
268            fields: None,
269            password_history: None,
270            creation_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
271            deleted_date: None,
272            revision_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
273            archived_date: None,
274        }
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::{CipherBlobV1, CipherTypeDataV1, test_support::*};
281    use crate::cipher::{
282        cipher::CipherType,
283        login::LoginView,
284        secure_note::{SecureNoteType, SecureNoteView},
285    };
286
287    #[test]
288    fn test_option_vec_normalization_none_to_empty_to_none() {
289        let (key_store, key_id) = create_test_key_store();
290        let mut ctx = key_store.context_mut();
291
292        let original = crate::CipherView {
293            name: "Minimal Note".to_string(),
294            notes: None,
295            r#type: CipherType::SecureNote,
296            secure_note: Some(SecureNoteView {
297                r#type: SecureNoteType::Generic,
298            }),
299            fields: None,
300            password_history: None,
301            ..create_shell_cipher_view(CipherType::SecureNote)
302        };
303
304        let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap();
305
306        assert!(blob.fields.is_empty());
307        assert!(blob.password_history.is_empty());
308
309        let mut restored = create_shell_cipher_view(CipherType::SecureNote);
310        blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id)
311            .unwrap();
312        assert!(restored.fields.is_none());
313        assert!(restored.password_history.is_none());
314    }
315
316    #[test]
317    fn test_login_none_uris_and_fido2_normalization() {
318        let (key_store, key_id) = create_test_key_store();
319        let mut ctx = key_store.context_mut();
320
321        let original = crate::CipherView {
322            name: "Simple Login".to_string(),
323            notes: None,
324            r#type: CipherType::Login,
325            login: Some(LoginView {
326                username: Some("user".to_string()),
327                password: None,
328                password_revision_date: None,
329                uris: None,
330                totp: None,
331                autofill_on_page_load: None,
332                fido2_credentials: None,
333            }),
334            ..create_shell_cipher_view(CipherType::Login)
335        };
336
337        let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap();
338
339        if let CipherTypeDataV1::Login(ref login_data) = blob.type_data {
340            assert!(login_data.uris.is_empty());
341            assert!(login_data.fido2_credentials.is_empty());
342        } else {
343            panic!("Expected Login type data");
344        }
345
346        let mut restored = create_shell_cipher_view(CipherType::Login);
347        blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id)
348            .unwrap();
349
350        let login = restored.login.unwrap();
351        assert!(login.uris.is_none());
352        assert!(login.fido2_credentials.is_none());
353    }
354}