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 decrypted_key: None,
292 };
293
294 let contents = b"This is a test file that we will encrypt. It's 100 bytes long, the encrypted version will be longer!";
295
296 let attachment_file = AttachmentFileView {
297 cipher: Cipher {
298 id: None,
299 organization_id: None,
300 folder_id: None,
301 collection_ids: Vec::new(),
302 key: Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap()),
303 name: "2.d24xECyEdMZ3MG9s6SrGNw==|XvJlTeu5KJ22M3jKosy6iw==|8xGiQty4X61cDMx6PVqkJfSQ0ZTdA/5L9TpG7QfovoM=".parse().unwrap(),
304 notes: None,
305 r#type: CipherType::Login,
306 login: None,
307 identity: None,
308 card: None,
309 secure_note: None,
310 ssh_key: None,
311 favorite: false,
312 reprompt: CipherRepromptType::None,
313 organization_use_totp: false,
314 edit: true,
315 permissions: None,
316 view_password: true,
317 local_data: None,
318 attachments: None,
319 fields: None,
320 password_history: None,
321 creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(),
322 deleted_date: None,
323 revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(),
324 archived_date: None,
325 },
326 attachment,
327 contents: contents.as_slice(),
328 };
329
330 let result = key_store.encrypt(attachment_file).unwrap();
331
332 assert_eq!(result.contents.len(), 161);
333 assert_eq!(result.attachment.size, Some("161".into()));
334 assert_eq!(result.attachment.size_name, Some("161 Bytes".into()));
335 }
336
337 #[test]
338 fn test_attachment_key() {
339 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
340 let key_store = create_test_crypto_with_user_key(user_key);
341
342 let attachment = AttachmentView {
343 id: None,
344 url: None,
345 size: Some("161".into()),
346 size_name: Some("161 Bytes".into()),
347 file_name: Some("Test.txt".into()),
348 key: Some("2.r288/AOSPiaLFkW07EBGBw==|SAmnnCbOLFjX5lnURvoualOetQwuyPc54PAmHDTRrhT0gwO9ailna9U09q9bmBfI5XrjNNEsuXssgzNygRkezoVQvZQggZddOwHB6KQW5EQ=|erIMUJp8j+aTcmhdE50zEX+ipv/eR1sZ7EwULJm/6DY=".parse().unwrap()),
349 decrypted_key: None,
350 };
351
352 let cipher = Cipher {
353 id: None,
354 organization_id: None,
355 folder_id: None,
356 collection_ids: Vec::new(),
357 key: Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap()),
358 name: "2.d24xECyEdMZ3MG9s6SrGNw==|XvJlTeu5KJ22M3jKosy6iw==|8xGiQty4X61cDMx6PVqkJfSQ0ZTdA/5L9TpG7QfovoM=".parse().unwrap(),
359 notes: None,
360 r#type: CipherType::Login,
361 login: None,
362 identity: None,
363 card: None,
364 secure_note: None,
365 ssh_key: None,
366 favorite: false,
367 reprompt: CipherRepromptType::None,
368 organization_use_totp: false,
369 edit: true,
370 permissions: None,
371 view_password: true,
372 local_data: None,
373 attachments: None,
374 fields: None,
375 password_history: None,
376 creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(),
377 deleted_date: None,
378 revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(),
379 archived_date: None,
380 };
381
382 let enc_file = B64::try_from("Ao00qr1xLsV+ZNQpYZ/UwEwOWo3hheKwCYcOGIbsorZ6JIG2vLWfWEXCVqP0hDuzRvmx8otApNZr8pJYLNwCe1aQ+ySHQYGkdubFjoMojulMbQ959Y4SJ6Its/EnVvpbDnxpXTDpbutDxyhxfq1P3lstL2G9rObJRrxiwdGlRGu1h94UA1fCCkIUQux5LcqUee6W4MyQmRnsUziH8gGzmtI=").unwrap();
383 let original = B64::try_from("rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap();
384
385 let dec = key_store
386 .decrypt(&AttachmentFile {
387 cipher,
388 attachment,
389 contents: EncString::from_buffer(enc_file.as_bytes()).unwrap(),
390 })
391 .unwrap();
392
393 assert_eq!(dec, original.as_bytes());
394 }
395
396 #[test]
397 fn test_attachment_without_key() {
398 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
399 let key_store = create_test_crypto_with_user_key(user_key);
400
401 let attachment = AttachmentView {
402 id: None,
403 url: None,
404 size: Some("161".into()),
405 size_name: Some("161 Bytes".into()),
406 file_name: Some("Test.txt".into()),
407 key: None,
408 decrypted_key: None,
409 };
410
411 let cipher = Cipher {
412 id: None,
413 organization_id: None,
414 folder_id: None,
415 collection_ids: Vec::new(),
416 key: None,
417 name: "2.d24xECyEdMZ3MG9s6SrGNw==|XvJlTeu5KJ22M3jKosy6iw==|8xGiQty4X61cDMx6PVqkJfSQ0ZTdA/5L9TpG7QfovoM=".parse().unwrap(),
418 notes: None,
419 r#type: CipherType::Login,
420 login: None,
421 identity: None,
422 card: None,
423 secure_note: None,
424 ssh_key: None,
425 favorite: false,
426 reprompt: CipherRepromptType::None,
427 organization_use_totp: false,
428 edit: true,
429 permissions: None,
430 view_password: true,
431 local_data: None,
432 attachments: None,
433 fields: None,
434 password_history: None,
435 creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(),
436 deleted_date: None,
437 revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(),
438 archived_date: None,
439 };
440
441 let enc_file = B64::try_from("AsQLXOBHrJ8porroTUlPxeJOm9XID7LL9D2+KwYATXEpR1EFjLBpcCvMmnqcnYLXIEefe9TCeY4Us50ux43kRSpvdB7YkjxDKV0O1/y6tB7qC4vvv9J9+O/uDEnMx/9yXuEhAW/LA/TsU/WAgxkOM0uTvm8JdD9LUR1z9Ql7zOWycMVzkvGsk2KBNcqAdrotS5FlDftZOXyU8pWecNeyA/w=").unwrap();
442 let original = B64::try_from("rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap();
443
444 let dec = key_store
445 .decrypt(&AttachmentFile {
446 cipher,
447 attachment,
448 contents: EncString::from_buffer(enc_file.as_bytes()).unwrap(),
449 })
450 .unwrap();
451
452 assert_eq!(dec, original.as_bytes());
453 }
454}