bitwarden_vault/cipher/blob/conversions/
mod.rs1use 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
23macro_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}