1use bitwarden_api_api::models::CipherAttachmentModel;
2use bitwarden_core::key_management::{KeyIds, SymmetricKeyId};
3use bitwarden_crypto::{
4 CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
5 OctetStreamBytes, PrimitiveEncryptable,
6};
7use serde::{Deserialize, Serialize};
8#[cfg(feature = "wasm")]
9use tsify::Tsify;
10
11use super::Cipher;
12use crate::VaultParseError;
13
14#[allow(missing_docs)]
15#[derive(Serialize, Deserialize, Debug, Clone)]
16#[serde(rename_all = "camelCase", deny_unknown_fields)]
17#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
18#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
19pub struct Attachment {
20 pub id: Option<String>,
21 pub url: Option<String>,
22 pub size: Option<String>,
23 pub size_name: Option<String>,
25 pub file_name: Option<EncString>,
26 pub key: Option<EncString>,
27}
28
29impl From<Attachment> for CipherAttachmentModel {
30 fn from(attachment: Attachment) -> Self {
31 Self {
32 file_name: attachment.file_name.map(|f| f.to_string()),
33 key: attachment.key.map(|k| k.to_string()),
34 }
35 }
36}
37
38#[allow(missing_docs)]
39#[derive(Serialize, Deserialize, Debug, Clone)]
40#[serde(rename_all = "camelCase", deny_unknown_fields)]
41#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
42#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
43pub struct AttachmentView {
44 pub id: Option<String>,
45 pub url: Option<String>,
46 pub size: Option<String>,
47 pub size_name: Option<String>,
48 pub file_name: Option<String>,
49 pub key: Option<EncString>,
50 #[cfg(feature = "wasm")]
61 pub decrypted_key: Option<String>,
62}
63
64impl AttachmentView {
65 pub(crate) fn reencrypt_key(
66 &mut self,
67 ctx: &mut KeyStoreContext<KeyIds>,
68 old_key: SymmetricKeyId,
69 new_key: SymmetricKeyId,
70 ) -> Result<(), CryptoError> {
71 if let Some(attachment_key) = &mut self.key {
72 let tmp_attachment_key_id = ctx.unwrap_symmetric_key(old_key, attachment_key)?;
73 *attachment_key = ctx.wrap_symmetric_key(new_key, tmp_attachment_key_id)?;
74 }
75 Ok(())
76 }
77
78 pub(crate) fn reencrypt_keys(
79 attachment_views: &mut Vec<AttachmentView>,
80 ctx: &mut KeyStoreContext<KeyIds>,
81 old_key: SymmetricKeyId,
82 new_key: SymmetricKeyId,
83 ) -> Result<(), CryptoError> {
84 for attachment in attachment_views {
85 attachment.reencrypt_key(ctx, old_key, new_key)?;
86 }
87 Ok(())
88 }
89}
90
91#[allow(missing_docs)]
92#[derive(Serialize, Deserialize, Debug)]
93#[serde(rename_all = "camelCase", deny_unknown_fields)]
94#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
95pub struct AttachmentEncryptResult {
96 pub attachment: Attachment,
97 pub contents: Vec<u8>,
98}
99
100#[allow(missing_docs)]
101pub struct AttachmentFile {
102 pub cipher: Cipher,
103 pub attachment: AttachmentView,
104
105 pub contents: EncString,
110}
111
112#[allow(missing_docs)]
113pub struct AttachmentFileView<'a> {
114 pub cipher: Cipher,
115 pub attachment: AttachmentView,
116 pub contents: &'a [u8],
117}
118
119impl IdentifyKey<SymmetricKeyId> for AttachmentFileView<'_> {
120 fn key_identifier(&self) -> SymmetricKeyId {
121 self.cipher.key_identifier()
122 }
123}
124impl IdentifyKey<SymmetricKeyId> for AttachmentFile {
125 fn key_identifier(&self) -> SymmetricKeyId {
126 self.cipher.key_identifier()
127 }
128}
129
130impl CompositeEncryptable<KeyIds, SymmetricKeyId, AttachmentEncryptResult>
131 for AttachmentFileView<'_>
132{
133 fn encrypt_composite(
134 &self,
135 ctx: &mut KeyStoreContext<KeyIds>,
136 key: SymmetricKeyId,
137 ) -> Result<AttachmentEncryptResult, CryptoError> {
138 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.cipher.key)?;
139
140 let mut attachment = self.attachment.clone();
141
142 let attachment_key = ctx.generate_symmetric_key();
145 let encrypted_contents =
146 OctetStreamBytes::from(self.contents).encrypt(ctx, attachment_key)?;
147 attachment.key = Some(ctx.wrap_symmetric_key(ciphers_key, attachment_key)?);
148
149 let contents = encrypted_contents.to_buffer()?;
150
151 attachment.size = Some(contents.len().to_string());
153 attachment.size_name = Some(size_name(contents.len()));
154
155 Ok(AttachmentEncryptResult {
156 attachment: attachment.encrypt_composite(ctx, ciphers_key)?,
157 contents,
158 })
159 }
160}
161
162fn size_name(size: usize) -> String {
163 let units = ["Bytes", "KB", "MB", "GB", "TB"];
164 let size = size as f64;
165 let unit = (size.ln() / 1024_f64.ln()).floor() as usize;
166 let size = size / 1024_f64.powi(unit as i32);
167
168 let size_round = (size * 10.0_f64).round() as usize as f64 / 10.0_f64;
169 format!("{} {}", size_round, units[unit])
170}
171
172impl Decryptable<KeyIds, SymmetricKeyId, Vec<u8>> for AttachmentFile {
173 fn decrypt(
174 &self,
175 ctx: &mut KeyStoreContext<KeyIds>,
176 key: SymmetricKeyId,
177 ) -> Result<Vec<u8>, CryptoError> {
178 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.cipher.key)?;
179
180 if let Some(attachment_key) = &self.attachment.key {
182 let content_key = ctx.unwrap_symmetric_key(ciphers_key, attachment_key)?;
183 self.contents.decrypt(ctx, content_key)
184 } else {
185 self.contents.decrypt(ctx, key)
187 }
188 }
189}
190
191impl CompositeEncryptable<KeyIds, SymmetricKeyId, Attachment> for AttachmentView {
192 fn encrypt_composite(
193 &self,
194 ctx: &mut KeyStoreContext<KeyIds>,
195 key: SymmetricKeyId,
196 ) -> Result<Attachment, CryptoError> {
197 Ok(Attachment {
198 id: self.id.clone(),
199 url: self.url.clone(),
200 size: self.size.clone(),
201 size_name: self.size_name.clone(),
202 file_name: self.file_name.encrypt(ctx, key)?,
203 key: self.key.clone(),
204 })
205 }
206}
207
208impl Decryptable<KeyIds, SymmetricKeyId, AttachmentView> for Attachment {
209 fn decrypt(
210 &self,
211 ctx: &mut KeyStoreContext<KeyIds>,
212 key: SymmetricKeyId,
213 ) -> Result<AttachmentView, CryptoError> {
214 #[cfg(feature = "wasm")]
215 let decrypted_key = if let Some(attachment_key) = &self.key {
216 let content_key_id = ctx.unwrap_symmetric_key(key, attachment_key)?;
217
218 #[allow(deprecated)]
219 let actual_key = ctx.dangerous_get_symmetric_key(content_key_id)?;
220
221 Some(actual_key.to_base64())
222 } else {
223 None
224 };
225
226 Ok(AttachmentView {
227 id: self.id.clone(),
228 url: self.url.clone(),
229 size: self.size.clone(),
230 size_name: self.size_name.clone(),
231 file_name: self.file_name.decrypt(ctx, key)?,
232 key: self.key.clone(),
233 #[cfg(feature = "wasm")]
234 decrypted_key: decrypted_key.map(|k| k.to_string()),
235 })
236 }
237}
238
239impl TryFrom<bitwarden_api_api::models::AttachmentResponseModel> for Attachment {
240 type Error = VaultParseError;
241
242 fn try_from(
243 attachment: bitwarden_api_api::models::AttachmentResponseModel,
244 ) -> Result<Self, Self::Error> {
245 Ok(Self {
246 id: attachment.id,
247 url: attachment.url,
248 size: attachment.size,
249 size_name: attachment.size_name,
250 file_name: EncString::try_from_optional(attachment.file_name)?,
251 key: EncString::try_from_optional(attachment.key)?,
252 })
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use bitwarden_core::key_management::create_test_crypto_with_user_key;
259 use bitwarden_crypto::{EncString, SymmetricCryptoKey};
260 use bitwarden_encoding::B64;
261
262 use crate::{
263 AttachmentFile, AttachmentFileView, AttachmentView, Cipher,
264 cipher::cipher::{CipherRepromptType, CipherType},
265 };
266
267 #[test]
268 fn test_size_name_conversions() {
269 assert_eq!(super::size_name(0), "0 Bytes");
270 assert_eq!(super::size_name(19), "19 Bytes");
271 assert_eq!(super::size_name(1024), "1 KB");
272 assert_eq!(super::size_name(1570), "1.5 KB");
273 assert_eq!(super::size_name(1024 * 1024), "1 MB");
274 assert_eq!(super::size_name(1024 * 18999), "18.6 MB");
275 assert_eq!(super::size_name(1024 * 1024 * 1024), "1 GB");
276 assert_eq!(super::size_name(1024 * 1024 * 1024 * 1024), "1 TB");
277 }
278
279 #[test]
280 fn test_encrypt_attachment() {
281 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
282 let key_store = create_test_crypto_with_user_key(user_key);
283
284 let attachment = AttachmentView {
285 id: None,
286 url: None,
287 size: Some("100".into()),
288 size_name: Some("100 Bytes".into()),
289 file_name: Some("Test.txt".into()),
290 key: None,
291 #[cfg(feature = "wasm")]
292 decrypted_key: None,
293 };
294
295 let contents = b"This is a test file that we will encrypt. It's 100 bytes long, the encrypted version will be longer!";
296
297 let attachment_file = AttachmentFileView {
298 cipher: Cipher {
299 id: None,
300 organization_id: None,
301 folder_id: None,
302 collection_ids: Vec::new(),
303 key: Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap()),
304 name: "2.d24xECyEdMZ3MG9s6SrGNw==|XvJlTeu5KJ22M3jKosy6iw==|8xGiQty4X61cDMx6PVqkJfSQ0ZTdA/5L9TpG7QfovoM=".parse().unwrap(),
305 notes: None,
306 r#type: CipherType::Login,
307 login: None,
308 identity: None,
309 card: None,
310 secure_note: None,
311 ssh_key: None,
312 favorite: false,
313 reprompt: CipherRepromptType::None,
314 organization_use_totp: false,
315 edit: true,
316 permissions: None,
317 view_password: true,
318 local_data: None,
319 attachments: None,
320 fields: None,
321 password_history: None,
322 creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(),
323 deleted_date: None,
324 revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(),
325 archived_date: None,
326 data: None,
327 },
328 attachment,
329 contents: contents.as_slice(),
330 };
331
332 let result = key_store.encrypt(attachment_file).unwrap();
333
334 assert_eq!(result.contents.len(), 161);
335 assert_eq!(result.attachment.size, Some("161".into()));
336 assert_eq!(result.attachment.size_name, Some("161 Bytes".into()));
337 }
338
339 #[test]
340 fn test_attachment_key() {
341 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
342 let key_store = create_test_crypto_with_user_key(user_key);
343
344 let attachment = AttachmentView {
345 id: None,
346 url: None,
347 size: Some("161".into()),
348 size_name: Some("161 Bytes".into()),
349 file_name: Some("Test.txt".into()),
350 key: Some("2.r288/AOSPiaLFkW07EBGBw==|SAmnnCbOLFjX5lnURvoualOetQwuyPc54PAmHDTRrhT0gwO9ailna9U09q9bmBfI5XrjNNEsuXssgzNygRkezoVQvZQggZddOwHB6KQW5EQ=|erIMUJp8j+aTcmhdE50zEX+ipv/eR1sZ7EwULJm/6DY=".parse().unwrap()),
351 #[cfg(feature = "wasm")]
352 decrypted_key: None,
353 };
354
355 let cipher = Cipher {
356 id: None,
357 organization_id: None,
358 folder_id: None,
359 collection_ids: Vec::new(),
360 key: Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap()),
361 name: "2.d24xECyEdMZ3MG9s6SrGNw==|XvJlTeu5KJ22M3jKosy6iw==|8xGiQty4X61cDMx6PVqkJfSQ0ZTdA/5L9TpG7QfovoM=".parse().unwrap(),
362 notes: None,
363 r#type: CipherType::Login,
364 login: None,
365 identity: None,
366 card: None,
367 secure_note: None,
368 ssh_key: None,
369 favorite: false,
370 reprompt: CipherRepromptType::None,
371 organization_use_totp: false,
372 edit: true,
373 permissions: None,
374 view_password: true,
375 local_data: None,
376 attachments: None,
377 fields: None,
378 password_history: None,
379 creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(),
380 deleted_date: None,
381 revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(),
382 archived_date: None,
383 data: None,
384 };
385
386 let enc_file = B64::try_from("Ao00qr1xLsV+ZNQpYZ/UwEwOWo3hheKwCYcOGIbsorZ6JIG2vLWfWEXCVqP0hDuzRvmx8otApNZr8pJYLNwCe1aQ+ySHQYGkdubFjoMojulMbQ959Y4SJ6Its/EnVvpbDnxpXTDpbutDxyhxfq1P3lstL2G9rObJRrxiwdGlRGu1h94UA1fCCkIUQux5LcqUee6W4MyQmRnsUziH8gGzmtI=").unwrap();
387 let original = B64::try_from("rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap();
388
389 let dec = key_store
390 .decrypt(&AttachmentFile {
391 cipher,
392 attachment,
393 contents: EncString::from_buffer(enc_file.as_bytes()).unwrap(),
394 })
395 .unwrap();
396
397 assert_eq!(dec, original.as_bytes());
398 }
399
400 #[test]
401 fn test_attachment_without_key() {
402 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
403 let key_store = create_test_crypto_with_user_key(user_key);
404
405 let attachment = AttachmentView {
406 id: None,
407 url: None,
408 size: Some("161".into()),
409 size_name: Some("161 Bytes".into()),
410 file_name: Some("Test.txt".into()),
411 key: None,
412 #[cfg(feature = "wasm")]
413 decrypted_key: None,
414 };
415
416 let cipher = Cipher {
417 id: None,
418 organization_id: None,
419 folder_id: None,
420 collection_ids: Vec::new(),
421 key: None,
422 name: "2.d24xECyEdMZ3MG9s6SrGNw==|XvJlTeu5KJ22M3jKosy6iw==|8xGiQty4X61cDMx6PVqkJfSQ0ZTdA/5L9TpG7QfovoM=".parse().unwrap(),
423 notes: None,
424 r#type: CipherType::Login,
425 login: None,
426 identity: None,
427 card: None,
428 secure_note: None,
429 ssh_key: None,
430 favorite: false,
431 reprompt: CipherRepromptType::None,
432 organization_use_totp: false,
433 edit: true,
434 permissions: None,
435 view_password: true,
436 local_data: None,
437 attachments: None,
438 fields: None,
439 password_history: None,
440 creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(),
441 deleted_date: None,
442 revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(),
443 archived_date: None,
444 data: None,
445 };
446
447 let enc_file = B64::try_from("AsQLXOBHrJ8porroTUlPxeJOm9XID7LL9D2+KwYATXEpR1EFjLBpcCvMmnqcnYLXIEefe9TCeY4Us50ux43kRSpvdB7YkjxDKV0O1/y6tB7qC4vvv9J9+O/uDEnMx/9yXuEhAW/LA/TsU/WAgxkOM0uTvm8JdD9LUR1z9Ql7zOWycMVzkvGsk2KBNcqAdrotS5FlDftZOXyU8pWecNeyA/w=").unwrap();
448 let original = B64::try_from("rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap();
449
450 let dec = key_store
451 .decrypt(&AttachmentFile {
452 cipher,
453 attachment,
454 contents: EncString::from_buffer(enc_file.as_bytes()).unwrap(),
455 })
456 .unwrap();
457
458 assert_eq!(dec, original.as_bytes());
459 }
460}