Skip to main content

bitwarden_vault/cipher/
attachment.rs

1use bitwarden_api_api::models::CipherAttachmentModel;
2use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
3use bitwarden_crypto::{
4    CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
5    OctetStreamBytes, PrimitiveEncryptable, SymmetricCryptoKey,
6};
7use serde::{Deserialize, Serialize};
8#[cfg(feature = "wasm")]
9use tsify::Tsify;
10
11use super::Cipher;
12use crate::VaultParseError;
13
14/// Cryptographic material for a new attachment, shared by the upgrade and create paths.
15pub(crate) struct AttachmentMaterial {
16    /// Raw attachment key, used to encrypt the attachment contents.
17    pub(crate) key: SymmetricCryptoKey,
18    /// Attachment key wrapped with the cipher key, stored on the attachment record.
19    pub(crate) wrapped_key: EncString,
20    /// File name encrypted with the cipher key.
21    pub(crate) encrypted_file_name: EncString,
22}
23
24#[allow(missing_docs)]
25#[derive(Serialize, Deserialize, Debug, Clone)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
28#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
29pub struct Attachment {
30    pub id: Option<String>,
31    pub url: Option<String>,
32    pub size: Option<String>,
33    /// Readable size, ex: "4.2 KB" or "1.43 GB"
34    pub size_name: Option<String>,
35    pub file_name: Option<EncString>,
36    pub key: Option<EncString>,
37}
38
39impl From<Attachment> for CipherAttachmentModel {
40    fn from(attachment: Attachment) -> Self {
41        Self {
42            file_name: attachment.file_name.map(|f| f.to_string()),
43            key: attachment.key.map(|k| k.to_string()),
44        }
45    }
46}
47
48/// The encryption format an attachment uses, derived from whether it carries its own key.
49#[derive(Debug, PartialEq, Eq)]
50pub(crate) enum AttachmentEncryptionVersion {
51    /// Legacy v1: contents are encrypted directly with the cipher key; no per-attachment key.
52    LegacyNoKeyV1,
53    /// V2: a per-attachment key, wrapped by the cipher key, encrypts the contents.
54    AttachmentKeyV2,
55}
56
57impl Attachment {
58    /// Returns the [`AttachmentEncryptionVersion`] this attachment uses.
59    pub(crate) fn encryption_version(&self) -> AttachmentEncryptionVersion {
60        match self.key {
61            Some(_) => AttachmentEncryptionVersion::AttachmentKeyV2,
62            None => AttachmentEncryptionVersion::LegacyNoKeyV1,
63        }
64    }
65}
66
67#[allow(missing_docs)]
68#[derive(Serialize, Deserialize, Debug, Clone)]
69#[serde(rename_all = "camelCase", deny_unknown_fields)]
70#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
71#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
72pub struct AttachmentView {
73    pub id: Option<String>,
74    pub url: Option<String>,
75    pub size: Option<String>,
76    pub size_name: Option<String>,
77    pub file_name: Option<String>,
78    pub key: Option<EncString>,
79    /// The decrypted attachmentkey in base64 format.
80    ///
81    /// **TEMPORARY FIELD**: This field is a temporary workaround to provide
82    /// decrypted attachment keys to the TypeScript client during the migration
83    /// process. It will be removed once the encryption/decryption logic is
84    /// fully migrated to the SDK.
85    ///
86    /// **Ticket**: <https://bitwarden.atlassian.net/browse/PM-23005>
87    ///
88    /// Do not rely on this field for long-term use.
89    #[cfg(feature = "wasm")]
90    pub decrypted_key: Option<String>,
91}
92
93impl AttachmentView {
94    pub(crate) fn reencrypt_key(
95        &mut self,
96        ctx: &mut KeyStoreContext<KeySlotIds>,
97        old_key: SymmetricKeySlotId,
98        new_key: SymmetricKeySlotId,
99    ) -> Result<(), CryptoError> {
100        if let Some(attachment_key) = &mut self.key {
101            let tmp_attachment_key_id = ctx.unwrap_symmetric_key(old_key, attachment_key)?;
102            *attachment_key = ctx.wrap_symmetric_key(new_key, tmp_attachment_key_id)?;
103        }
104        Ok(())
105    }
106
107    pub(crate) fn reencrypt_keys(
108        attachment_views: &mut Vec<AttachmentView>,
109        ctx: &mut KeyStoreContext<KeySlotIds>,
110        old_key: SymmetricKeySlotId,
111        new_key: SymmetricKeySlotId,
112    ) -> Result<(), CryptoError> {
113        for attachment in attachment_views {
114            attachment.reencrypt_key(ctx, old_key, new_key)?;
115        }
116        Ok(())
117    }
118}
119
120#[allow(missing_docs)]
121#[derive(Serialize, Deserialize, Debug)]
122#[serde(rename_all = "camelCase", deny_unknown_fields)]
123#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
124pub struct AttachmentEncryptResult {
125    pub attachment: Attachment,
126    pub contents: Vec<u8>,
127}
128
129#[allow(missing_docs)]
130pub struct AttachmentFile {
131    pub cipher: Cipher,
132    pub attachment: AttachmentView,
133
134    /// There are three different ways attachments are encrypted.
135    /// 1. UserKey / OrgKey (Contents) - Legacy
136    /// 2. AttachmentKey(Contents) - Pre CipherKey
137    /// 3. CipherKey(AttachmentKey(Contents)) - Current
138    pub contents: EncString,
139}
140
141#[allow(missing_docs)]
142pub struct AttachmentFileView<'a> {
143    pub cipher: Cipher,
144    pub attachment: AttachmentView,
145    pub contents: &'a [u8],
146}
147
148impl IdentifyKey<SymmetricKeySlotId> for AttachmentFileView<'_> {
149    fn key_identifier(&self) -> SymmetricKeySlotId {
150        self.cipher.key_identifier()
151    }
152}
153impl IdentifyKey<SymmetricKeySlotId> for AttachmentFile {
154    fn key_identifier(&self) -> SymmetricKeySlotId {
155        self.cipher.key_identifier()
156    }
157}
158
159impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, AttachmentEncryptResult>
160    for AttachmentFileView<'_>
161{
162    fn encrypt_composite(
163        &self,
164        ctx: &mut KeyStoreContext<KeySlotIds>,
165        key: SymmetricKeySlotId,
166    ) -> Result<AttachmentEncryptResult, CryptoError> {
167        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.cipher.key)?;
168
169        let mut attachment = self.attachment.clone();
170
171        // Because this is a new attachment, we have to generate a key for it, encrypt the contents
172        // with it, and then encrypt the key with the cipher key
173        let attachment_key = ctx.generate_symmetric_key();
174        let encrypted_contents =
175            OctetStreamBytes::from(self.contents).encrypt(ctx, attachment_key)?;
176        attachment.key = Some(ctx.wrap_symmetric_key(ciphers_key, attachment_key)?);
177
178        let contents = encrypted_contents.to_buffer()?;
179
180        // Once we have the encrypted contents, we can set the size of the attachment
181        attachment.size = Some(contents.len().to_string());
182        attachment.size_name = Some(size_name(contents.len()));
183
184        Ok(AttachmentEncryptResult {
185            attachment: attachment.encrypt_composite(ctx, ciphers_key)?,
186            contents,
187        })
188    }
189}
190
191fn size_name(size: usize) -> String {
192    let units = ["Bytes", "KB", "MB", "GB", "TB"];
193    let size = size as f64;
194    let unit = (size.ln() / 1024_f64.ln()).floor() as usize;
195    let size = size / 1024_f64.powi(unit as i32);
196
197    let size_round = (size * 10.0_f64).round() as usize as f64 / 10.0_f64;
198    format!("{} {}", size_round, units[unit])
199}
200
201impl Decryptable<KeySlotIds, SymmetricKeySlotId, Vec<u8>> for AttachmentFile {
202    fn decrypt(
203        &self,
204        ctx: &mut KeyStoreContext<KeySlotIds>,
205        key: SymmetricKeySlotId,
206    ) -> Result<Vec<u8>, CryptoError> {
207        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.cipher.key).map_err(|e| {
208            tracing::warn!(
209                attachment_id = ?self.attachment.id,
210                cipher_id = ?self.cipher.id,
211                has_cipher_key = self.cipher.key.is_some(),
212                error = %e,
213                "Failed to decrypt cipher key for attachment"
214            );
215            e
216        })?;
217
218        // Version 2 or 3, `AttachmentKey` or `CipherKey(AttachmentKey)`
219        if let Some(attachment_key) = &self.attachment.key {
220            let content_key = ctx
221                .unwrap_symmetric_key(ciphers_key, attachment_key)
222                .map_err(|e| {
223                    tracing::warn!(
224                        attachment_id = ?self.attachment.id,
225                        cipher_id = ?self.cipher.id,
226                        error = %e,
227                        "Failed to unwrap attachment key (v2/v3)"
228                    );
229                    e
230                })?;
231            self.contents.decrypt(ctx, content_key).map_err(|e| {
232                tracing::warn!(
233                    attachment_id = ?self.attachment.id,
234                    cipher_id = ?self.cipher.id,
235                    error = %e,
236                    "Failed to decrypt attachment contents with attachment key (v2/v3)"
237                );
238                e
239            })
240        } else {
241            // Legacy attachment version 1, use user/org key
242            self.contents.decrypt(ctx, key).map_err(|e| {
243                tracing::warn!(
244                    attachment_id = ?self.attachment.id,
245                    cipher_id = ?self.cipher.id,
246                    error = %e,
247                    "Failed to decrypt attachment contents with user/org key (legacy v1)"
248                );
249                e
250            })
251        }
252    }
253}
254
255// ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::CompositeEncryptable`: `AttachmentView` retains
256// key-bound ciphertext (`key`, the attachment content key wrapped under the cipher key) and copies
257// it through unchanged (`key: self.key.clone()` below) instead of re-wrapping it under `key`. As a
258// result decrypt(K) -> encrypt(K1) -> decrypt(K1) does NOT round-trip.
259impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Attachment> for AttachmentView {
260    fn encrypt_composite(
261        &self,
262        ctx: &mut KeyStoreContext<KeySlotIds>,
263        key: SymmetricKeySlotId,
264    ) -> Result<Attachment, CryptoError> {
265        Ok(Attachment {
266            id: self.id.clone(),
267            url: self.url.clone(),
268            size: self.size.clone(),
269            size_name: self.size_name.clone(),
270            file_name: self.file_name.encrypt(ctx, key)?,
271            // ⚠️ pass-through of wrapped key-bound ciphertext — see the contract-violation note
272            // above.
273            key: self.key.clone(),
274        })
275    }
276}
277
278impl Decryptable<KeySlotIds, SymmetricKeySlotId, AttachmentView> for Attachment {
279    fn decrypt(
280        &self,
281        ctx: &mut KeyStoreContext<KeySlotIds>,
282        key: SymmetricKeySlotId,
283    ) -> Result<AttachmentView, CryptoError> {
284        // Decrypt the file name or return an error if decryption fails
285        let file_name = self.file_name.decrypt(ctx, key)?;
286
287        #[cfg(feature = "wasm")]
288        let decrypted_key = if let Some(attachment_key) = &self.key {
289            let content_key_id = ctx.unwrap_symmetric_key(key, attachment_key)?;
290
291            #[allow(deprecated)]
292            let actual_key = ctx.dangerous_get_symmetric_key(content_key_id)?;
293
294            Some(actual_key.to_base64())
295        } else {
296            None
297        };
298
299        Ok(AttachmentView {
300            id: self.id.clone(),
301            url: self.url.clone(),
302            size: self.size.clone(),
303            size_name: self.size_name.clone(),
304            file_name,
305            // ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::Decryptable`: the resulting
306            // `AttachmentView` is a decrypted DTO, yet `key` (the attachment content key wrapped
307            // under the cipher key) is copied through still encrypted (`self.key.clone()`) rather
308            // than decrypted. The wrapped key is therefore key-bound to the original cipher key,
309            // which is what makes the `CompositeEncryptable` pass-through above non-round-tripping.
310            key: self.key.clone(),
311            #[cfg(feature = "wasm")]
312            decrypted_key: decrypted_key.map(|k| k.to_string()),
313        })
314    }
315}
316
317/// Decrypts a list of attachments, separating successful decryptions from failures.
318///
319/// Returns a tuple of (successful_attachments, failed_attachments).
320pub(crate) fn decrypt_attachments_with_failures(
321    attachments: &[Attachment],
322    ctx: &mut KeyStoreContext<KeySlotIds>,
323    key: SymmetricKeySlotId,
324) -> (Vec<AttachmentView>, Vec<AttachmentView>) {
325    let mut successes = Vec::new();
326    let mut failures = Vec::new();
327
328    for attachment in attachments {
329        match attachment.decrypt(ctx, key) {
330            Ok(decrypted) => successes.push(decrypted),
331            Err(e) => {
332                tracing::warn!(attachment_id = ?attachment.id, error = %e, "Failed to decrypt attachment");
333                failures.push(AttachmentView {
334                    id: attachment.id.clone(),
335                    url: attachment.url.clone(),
336                    size: attachment.size.clone(),
337                    size_name: attachment.size_name.clone(),
338                    file_name: None,
339                    key: attachment.key.clone(),
340                    #[cfg(feature = "wasm")]
341                    decrypted_key: None,
342                });
343            }
344        }
345    }
346
347    (successes, failures)
348}
349
350impl TryFrom<bitwarden_api_api::models::AttachmentResponseModel> for Attachment {
351    type Error = VaultParseError;
352
353    fn try_from(
354        attachment: bitwarden_api_api::models::AttachmentResponseModel,
355    ) -> Result<Self, Self::Error> {
356        Ok(Self {
357            id: attachment.id,
358            url: attachment.url,
359            size: attachment.size,
360            size_name: attachment.size_name,
361            file_name: EncString::try_from_optional(attachment.file_name)?,
362            key: EncString::try_from_optional(attachment.key)?,
363        })
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use bitwarden_core::key_management::create_test_crypto_with_user_key;
370    use bitwarden_crypto::{EncString, SymmetricCryptoKey};
371    use bitwarden_encoding::B64;
372
373    use crate::{
374        AttachmentFile, AttachmentFileView, AttachmentView, Cipher,
375        cipher::cipher::{CipherRepromptType, CipherType},
376    };
377
378    #[test]
379    fn test_size_name_conversions() {
380        assert_eq!(super::size_name(0), "0 Bytes");
381        assert_eq!(super::size_name(19), "19 Bytes");
382        assert_eq!(super::size_name(1024), "1 KB");
383        assert_eq!(super::size_name(1570), "1.5 KB");
384        assert_eq!(super::size_name(1024 * 1024), "1 MB");
385        assert_eq!(super::size_name(1024 * 18999), "18.6 MB");
386        assert_eq!(super::size_name(1024 * 1024 * 1024), "1 GB");
387        assert_eq!(super::size_name(1024 * 1024 * 1024 * 1024), "1 TB");
388    }
389
390    #[test]
391    fn test_encrypt_attachment() {
392        let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
393        let key_store = create_test_crypto_with_user_key(user_key);
394
395        let attachment = AttachmentView {
396            id: None,
397            url: None,
398            size: Some("100".into()),
399            size_name: Some("100 Bytes".into()),
400            file_name: Some("Test.txt".into()),
401            key: None,
402            #[cfg(feature = "wasm")]
403            decrypted_key: None,
404        };
405
406        let contents = b"This is a test file that we will encrypt. It's 100 bytes long, the encrypted version will be longer!";
407
408        let attachment_file = AttachmentFileView {
409            cipher: Cipher {
410                id: None,
411                organization_id: None,
412                folder_id: None,
413                collection_ids: Vec::new(),
414                key: Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap()),
415                name: Some("2.d24xECyEdMZ3MG9s6SrGNw==|XvJlTeu5KJ22M3jKosy6iw==|8xGiQty4X61cDMx6PVqkJfSQ0ZTdA/5L9TpG7QfovoM=".parse().unwrap()),
416                notes: None,
417                r#type: CipherType::Login,
418                login: None,
419                identity: None,
420                card: None,
421                secure_note: None,
422                ssh_key: None,
423                bank_account: None,
424                drivers_license: None,
425                passport: None,
426                favorite: false,
427                reprompt: CipherRepromptType::None,
428                organization_use_totp: false,
429                edit: true,
430                permissions: None,
431                view_password: true,
432                local_data: None,
433                attachments: None,
434                fields: None,
435                password_history: None,
436                creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(),
437                deleted_date: None,
438                revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(),
439                archived_date: None,
440                data: None,
441            },
442            attachment,
443            contents: contents.as_slice(),
444        };
445
446        let result = key_store.encrypt(attachment_file).unwrap();
447
448        assert_eq!(result.contents.len(), 161);
449        assert_eq!(result.attachment.size, Some("161".into()));
450        assert_eq!(result.attachment.size_name, Some("161 Bytes".into()));
451    }
452
453    #[test]
454    fn test_attachment_key() {
455        let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
456        let key_store = create_test_crypto_with_user_key(user_key);
457
458        let attachment = AttachmentView {
459            id: None,
460            url: None,
461            size: Some("161".into()),
462            size_name: Some("161 Bytes".into()),
463            file_name: Some("Test.txt".into()),
464            key: Some("2.r288/AOSPiaLFkW07EBGBw==|SAmnnCbOLFjX5lnURvoualOetQwuyPc54PAmHDTRrhT0gwO9ailna9U09q9bmBfI5XrjNNEsuXssgzNygRkezoVQvZQggZddOwHB6KQW5EQ=|erIMUJp8j+aTcmhdE50zEX+ipv/eR1sZ7EwULJm/6DY=".parse().unwrap()),
465            #[cfg(feature = "wasm")]
466            decrypted_key: None,
467        };
468
469        let cipher  = Cipher {
470            id: None,
471            organization_id: None,
472            folder_id: None,
473            collection_ids: Vec::new(),
474            key: Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap()),
475            name: Some("2.d24xECyEdMZ3MG9s6SrGNw==|XvJlTeu5KJ22M3jKosy6iw==|8xGiQty4X61cDMx6PVqkJfSQ0ZTdA/5L9TpG7QfovoM=".parse().unwrap()),
476            notes: None,
477            r#type: CipherType::Login,
478            login: None,
479            identity: None,
480            card: None,
481            secure_note: None,
482            ssh_key: None,
483            bank_account: None,
484            drivers_license: None,
485            passport: None,
486            favorite: false,
487            reprompt: CipherRepromptType::None,
488            organization_use_totp: false,
489            edit: true,
490            permissions: None,
491            view_password: true,
492            local_data: None,
493            attachments: None,
494            fields: None,
495            password_history: None,
496            creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(),
497            deleted_date: None,
498            revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(),
499            archived_date: None,
500            data: None,
501        };
502
503        let enc_file = B64::try_from("Ao00qr1xLsV+ZNQpYZ/UwEwOWo3hheKwCYcOGIbsorZ6JIG2vLWfWEXCVqP0hDuzRvmx8otApNZr8pJYLNwCe1aQ+ySHQYGkdubFjoMojulMbQ959Y4SJ6Its/EnVvpbDnxpXTDpbutDxyhxfq1P3lstL2G9rObJRrxiwdGlRGu1h94UA1fCCkIUQux5LcqUee6W4MyQmRnsUziH8gGzmtI=").unwrap();
504        let original = B64::try_from("rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap();
505
506        let dec = key_store
507            .decrypt(&AttachmentFile {
508                cipher,
509                attachment,
510                contents: EncString::from_buffer(enc_file.as_bytes()).unwrap(),
511            })
512            .unwrap();
513
514        assert_eq!(dec, original.as_bytes());
515    }
516
517    #[test]
518    fn test_attachment_without_key() {
519        let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
520        let key_store = create_test_crypto_with_user_key(user_key);
521
522        let attachment = AttachmentView {
523            id: None,
524            url: None,
525            size: Some("161".into()),
526            size_name: Some("161 Bytes".into()),
527            file_name: Some("Test.txt".into()),
528            key: None,
529            #[cfg(feature = "wasm")]
530            decrypted_key: None,
531        };
532
533        let cipher  = Cipher {
534            id: None,
535            organization_id: None,
536            folder_id: None,
537            collection_ids: Vec::new(),
538            key: None,
539            name: Some("2.d24xECyEdMZ3MG9s6SrGNw==|XvJlTeu5KJ22M3jKosy6iw==|8xGiQty4X61cDMx6PVqkJfSQ0ZTdA/5L9TpG7QfovoM=".parse().unwrap()),
540            notes: None,
541            r#type: CipherType::Login,
542            login: None,
543            identity: None,
544            card: None,
545            secure_note: None,
546            ssh_key: None,
547            bank_account: None,
548            drivers_license: None,
549            passport: None,
550            favorite: false,
551            reprompt: CipherRepromptType::None,
552            organization_use_totp: false,
553            edit: true,
554            permissions: None,
555            view_password: true,
556            local_data: None,
557            attachments: None,
558            fields: None,
559            password_history: None,
560            creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(),
561            deleted_date: None,
562            revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(),
563            archived_date: None,
564            data: None,
565        };
566
567        let enc_file = B64::try_from("AsQLXOBHrJ8porroTUlPxeJOm9XID7LL9D2+KwYATXEpR1EFjLBpcCvMmnqcnYLXIEefe9TCeY4Us50ux43kRSpvdB7YkjxDKV0O1/y6tB7qC4vvv9J9+O/uDEnMx/9yXuEhAW/LA/TsU/WAgxkOM0uTvm8JdD9LUR1z9Ql7zOWycMVzkvGsk2KBNcqAdrotS5FlDftZOXyU8pWecNeyA/w=").unwrap();
568        let original = B64::try_from("rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap();
569
570        let dec = key_store
571            .decrypt(&AttachmentFile {
572                cipher,
573                attachment,
574                contents: EncString::from_buffer(enc_file.as_bytes()).unwrap(),
575            })
576            .unwrap();
577
578        assert_eq!(dec, original.as_bytes());
579    }
580}