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