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