Skip to main content

matrix_sdk_contentscanner/
lib.rs

1// Copyright 2026 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#[cfg(feature = "e2e-encryption")]
16use std::io::Read;
17use std::{
18    fmt::{Debug, Formatter},
19    sync::Arc,
20};
21
22use api::{
23    download::{
24        encrypted::DownloadAndScanEncryptedMediaRequest, unencrypted::DownloadAndScanMediaRequest,
25    },
26    public_server_key::PublicServerKeyRequest,
27};
28use matrix_sdk::{
29    BoxFuture, Client, Error, IdParseError,
30    encryption::vodozemac::pk_encryption::Message,
31    locks::Mutex,
32    media::{MediaFetcher, MediaRequestParameters},
33    ruma::events::room::MediaSource,
34};
35#[cfg(feature = "e2e-encryption")]
36use matrix_sdk_crypto::AttachmentDecryptor;
37use matrix_sdk_crypto::olm::Curve25519PublicKey;
38use ruma::{
39    events::room::EncryptedFile,
40    serde::{Base64, base64::Standard},
41};
42use serde::{Deserialize, Serialize};
43use tracing::trace;
44
45#[cfg(feature = "uniffi")]
46uniffi::setup_scaffolding!();
47
48pub use crate::api::scan::MediaScanResponse;
49use crate::api::{
50    DownloadAndScanMediaResponse,
51    scan::{encrypted::EncryptedMediaScanRequest, unencrypted::MediaScanRequest},
52};
53
54mod api;
55
56/// A helper component to download and scan media from a content scanner server.
57#[derive(Debug)]
58pub struct ContentScanner {
59    scanner_url: String,
60    public_server_key: Arc<Mutex<Option<String>>>,
61}
62
63impl ContentScanner {
64    /// Instantiate a new [`ContentScanner`] using the `scanner_url`.
65    pub fn new(scanner_url: impl Into<String>) -> Self {
66        Self { scanner_url: scanner_url.into(), public_server_key: Arc::new(Mutex::new(None)) }
67    }
68
69    pub(crate) async fn fetch_public_server_key(&self, client: &Client) -> Result<String, Error> {
70        let response = client.send(PublicServerKeyRequest::new(self.scanner_url.clone())).await?;
71        Ok(response.public_key)
72    }
73
74    async fn get_or_fetch_public_server_key(&self, client: &Client) -> Option<Curve25519PublicKey> {
75        let public_server_key =
76            if let Some(public_server_key) = (*self.public_server_key.lock()).clone() {
77                trace!("Using cached public server key");
78                Some(public_server_key)
79            } else {
80                trace!("Using cached public server key");
81                let ret = self.fetch_public_server_key(client).await.ok();
82
83                if let Some(public_server_key) = &ret {
84                    trace!("Saved new public server key");
85                    let mut guard = self.public_server_key.lock();
86                    let _ = guard.insert(public_server_key.clone());
87                }
88
89                ret
90            };
91
92        public_server_key.and_then(|key| Curve25519PublicKey::from_base64(&key).ok())
93    }
94
95    pub(crate) async fn get_media(
96        &self,
97        client: &Client,
98        media_source: &MediaSource,
99    ) -> Result<DownloadAndScanMediaResponse, Error> {
100        match &media_source {
101            MediaSource::Encrypted(encrypted) => {
102                // Get the public server key if we don't have it yet.
103                let public_server_key = self.get_or_fetch_public_server_key(client).await;
104
105                Ok(client
106                    .send(DownloadAndScanEncryptedMediaRequest::new(
107                        self.scanner_url.clone(),
108                        public_server_key,
109                        *encrypted.clone(),
110                    ))
111                    .await?)
112            }
113            MediaSource::Plain(mxc) => {
114                let (server_name, media_id) =
115                    mxc.parts().map_err(|e| Error::Identifier(IdParseError::InvalidMxcUri(e)))?;
116                Ok(client
117                    .send(DownloadAndScanMediaRequest::new(
118                        &self.scanner_url,
119                        server_name.as_str(),
120                        media_id,
121                    ))
122                    .await?)
123            }
124        }
125    }
126
127    /// Scan a media source, returning a [`MediaScanResponse`] with the scan
128    /// result, or an error if something failed when trying to scan the media.
129    pub async fn scan(
130        &self,
131        client: &Client,
132        media_source: &MediaSource,
133    ) -> Result<MediaScanResponse, Error> {
134        match &media_source {
135            MediaSource::Encrypted(encrypted) => {
136                // Get the public server key if we don't have it yet.
137                let public_server_key = self.get_or_fetch_public_server_key(client).await;
138
139                Ok(client
140                    .send(EncryptedMediaScanRequest::new(
141                        self.scanner_url.clone(),
142                        public_server_key,
143                        *encrypted.clone(),
144                    ))
145                    .await?)
146            }
147            MediaSource::Plain(mxc) => {
148                let (server_name, media_id) =
149                    mxc.parts().map_err(|e| Error::Identifier(IdParseError::InvalidMxcUri(e)))?;
150                Ok(client
151                    .send(MediaScanRequest::new(
152                        self.scanner_url.clone(),
153                        server_name.to_string(),
154                        media_id.to_owned(),
155                    ))
156                    .await?)
157            }
158        }
159    }
160}
161
162#[derive(Debug, Clone, Serialize)]
163struct EncryptedBody {
164    ciphertext: String,
165    mac: String,
166    ephemeral: String,
167}
168
169impl From<Message> for EncryptedBody {
170    fn from(value: Message) -> Self {
171        Self {
172            ciphertext: Base64::<Standard>::new(value.ciphertext).to_string(),
173            mac: Base64::<Standard>::new(value.mac).to_string(),
174            ephemeral: value.ephemeral_key.to_base64(),
175        }
176    }
177}
178
179#[derive(Debug, Clone, Serialize)]
180pub(crate) struct EncryptedFileRequest {
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub file: Option<EncryptedFile>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub encrypted_body: Option<EncryptedBody>,
185}
186
187impl EncryptedFileRequest {
188    pub(crate) fn from_file_info(file_info: EncryptedFile) -> Self {
189        Self { file: Some(file_info), encrypted_body: None }
190    }
191
192    pub(crate) fn from_encrypted_body(encrypted_body: EncryptedBody) -> Self {
193        Self { file: None, encrypted_body: Some(encrypted_body) }
194    }
195}
196
197/// A media fetcher that uses the content scanner to download and scan media.
198pub struct ContentScannerMediaFetcher {
199    pub content_scanner: Arc<ContentScanner>,
200}
201
202impl ContentScannerMediaFetcher {
203    /// Instantiate a new [`MediaFetcher`] using the provided `scanner_url`.
204    pub fn new(scanner_url: impl Into<String>) -> Self {
205        Self { content_scanner: Arc::new(ContentScanner::new(scanner_url.into())) }
206    }
207
208    pub fn with_content_scanner(content_scanner: Arc<ContentScanner>) -> Self {
209        Self { content_scanner }
210    }
211}
212
213impl MediaFetcher for ContentScannerMediaFetcher {
214    fn fetch_media_content<'a>(
215        &'a self,
216        client: &'a Client,
217        request: &'a MediaRequestParameters,
218    ) -> BoxFuture<'a, matrix_sdk::Result<Vec<u8>, Error>> {
219        Box::pin(async move {
220            let content = self.content_scanner.get_media(client, &request.source).await?.content;
221            #[cfg(feature = "e2e-encryption")]
222            let content = {
223                match &request.source {
224                    MediaSource::Encrypted(file) => {
225                        let content_len = content.len();
226                        let mut cursor = std::io::Cursor::new(content);
227                        let mut reader =
228                            AttachmentDecryptor::new(&mut cursor, file.as_ref().clone().into())?;
229
230                        // Encrypted size should be the same as the decrypted size,
231                        // rounded up to a cipher block.
232                        let mut decrypted = Vec::with_capacity(content_len);
233
234                        reader.read_to_end(&mut decrypted)?;
235
236                        decrypted
237                    }
238                    MediaSource::Plain(_) => content,
239                }
240            };
241            Ok(content)
242        })
243    }
244}
245
246impl Debug for ContentScannerMediaFetcher {
247    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
248        f.write_str("ContentScannerMediaFetcher")
249    }
250}
251
252/// A content scanner error.
253#[derive(Debug, Deserialize)]
254pub struct ContentScannerError {
255    pub info: String,
256    pub reason: ErrorReason,
257}
258
259/// The reason for the content scanner error.
260#[allow(non_camel_case_types)]
261#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
262#[derive(Clone, Debug, Deserialize)]
263pub enum ErrorReason {
264    /// The JSON file is malformed.
265    MCS_MALFORMED_JSON,
266    /// The media could not be decrypted.
267    MCS_MEDIA_FAILED_TO_DECRYPT,
268    /// No access token was provided.
269    M_MISSING_TOKEN,
270    /// The access token provided is invalid.
271    M_UNKNOWN_TOKEN,
272    /// The media was not found.
273    M_NOT_FOUND,
274    /// The media has some potentially dangerous content.
275    MCS_MEDIA_NOT_CLEAN,
276    /// The media has been blocked by the server because of its mime type.
277    MCS_MIME_TYPE_FORBIDDEN,
278    /// The used public key is wrong.
279    MCS_BAD_DECRYPTION,
280    /// An unknown error occurred.
281    M_UNKNOWN,
282    /// The server failed to request media from the media repo.
283    MCS_MEDIA_REQUEST_FAILED,
284}
285
286#[cfg(test)]
287mod tests {
288    use std::ops::Not;
289    #[cfg(feature = "e2e-encryption")]
290    use std::sync::Arc;
291
292    use assert_matches2::assert_matches;
293    #[cfg(feature = "e2e-encryption")]
294    use matrix_sdk::media::{MediaFormat, MediaRequestParameters};
295    use matrix_sdk::{HttpError, RumaApiError, test_utils::mocks::MatrixMockServer};
296    use matrix_sdk_test::async_test;
297    use ruma::{
298        api::{
299            MatrixVersion,
300            error::{ErrorBody, FromHttpResponseError},
301        },
302        events::room::{
303            EncryptedFile, EncryptedFileHash, EncryptedFileHashes, EncryptedFileInfo, MediaSource,
304            V2EncryptedFileInfo,
305        },
306        exports::{http::StatusCode, serde_json::json},
307        owned_mxc_uri,
308        serde::Base64,
309    };
310    use serde::Deserialize;
311    use wiremock::{
312        Mock, MockServer, ResponseTemplate,
313        matchers::{header_exists, method, path, path_regex},
314    };
315
316    #[cfg(feature = "e2e-encryption")]
317    use crate::ContentScannerMediaFetcher;
318    use crate::{ContentScanner, ContentScannerError, ErrorReason};
319
320    #[async_test]
321    async fn test_fetch_public_key() {
322        let server = MatrixMockServer::new().await;
323        let client =
324            server.client_builder().server_versions(vec![MatrixVersion::V1_11]).build().await;
325
326        let content_scanner_server = MockServer::start().await;
327        Mock::given(method("GET"))
328            .and(path("/_matrix/media_proxy/unstable/public_key"))
329            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
330                "public_key": "1234567890"
331            })))
332            .mount(&content_scanner_server)
333            .await;
334
335        let content_scanner = ContentScanner::new(content_scanner_server.uri());
336        content_scanner.fetch_public_server_key(&client).await.expect("Load public key");
337    }
338
339    #[async_test]
340    async fn test_get_media() {
341        let server = MatrixMockServer::new().await;
342        let client =
343            server.client_builder().server_versions(vec![MatrixVersion::V1_11]).build().await;
344
345        let content_scanner_server = MockServer::start().await;
346        Mock::given(method("GET"))
347            .and(path_regex(r"/_matrix/media_proxy/unstable/download/.+/.+"))
348            .and(header_exists("Authorization"))
349            .respond_with(
350                ResponseTemplate::new(200).set_body_bytes(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]),
351            )
352            .mount(&content_scanner_server)
353            .await;
354
355        let content_scanner = ContentScanner::new(content_scanner_server.uri());
356        let media_source =
357            MediaSource::Plain(owned_mxc_uri!("mxc://matrix.org/RhfpOXOzAwzkuqcmbgMwQUrJ"));
358        content_scanner.get_media(&client, &media_source).await.expect("Get media");
359    }
360
361    #[async_test]
362    async fn test_get_media_unsupported() {
363        let server = MatrixMockServer::new().await;
364        let client =
365            server.client_builder().server_versions(vec![MatrixVersion::V1_11]).build().await;
366
367        let content_scanner_server = MockServer::start().await;
368        Mock::given(method("GET"))
369            .and(path_regex(r"/_matrix/media_proxy/unstable/download/.+/.+"))
370            .and(header_exists("Authorization"))
371            .respond_with(ResponseTemplate::new(403).set_body_json(json!({
372                "reason": "MCS_MIME_TYPE_FORBIDDEN",
373                "info": "File type: application/octet-stream not allowed",
374            })))
375            .mount(&content_scanner_server)
376            .await;
377
378        let content_scanner = ContentScanner::new(content_scanner_server.uri());
379        let media_source =
380            MediaSource::Plain(owned_mxc_uri!("mxc://matrix.org/ckTaStcNnFXLzKApkBmgRDoC"));
381        let err =
382            content_scanner.get_media(&client, &media_source).await.expect_err("Get media error");
383        let client_error = err.as_client_api_error().expect("Get client error");
384        assert_eq!(client_error.status_code, StatusCode::FORBIDDEN);
385        assert_eq!(
386            client_error.to_string(),
387            "[403] {\"info\":\"File type: application/octet-stream not allowed\",\"reason\":\"MCS_MIME_TYPE_FORBIDDEN\"}"
388        );
389    }
390
391    #[async_test]
392    async fn test_get_encrypted_media() {
393        let server = MatrixMockServer::new().await;
394        let client =
395            server.client_builder().server_versions(vec![MatrixVersion::V1_11]).build().await;
396
397        let content_scanner_server = MockServer::start().await;
398        Mock::given(method("POST"))
399            .and(path("/_matrix/media_proxy/unstable/download_encrypted"))
400            .and(header_exists("Authorization"))
401            .respond_with(
402                ResponseTemplate::new(200).set_body_bytes(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]),
403            )
404            .mount(&content_scanner_server)
405            .await;
406
407        let content_scanner = ContentScanner::new(content_scanner_server.uri());
408        let file_info = EncryptedFileInfo::V2(V2EncryptedFileInfo::new(
409            Base64::parse("9lpOscZyMOZRCF3v867nPPo3WPNMZt9JXMsuYiWRszc".as_bytes()).expect("k"),
410            Base64::parse("czvdfKSjfLEAAAAAAAAAAA".as_bytes()).expect("iv"),
411        ));
412        let mut hashes = EncryptedFileHashes::new();
413        hashes.insert(EncryptedFileHash::Sha256(
414            Base64::parse("SBbJ3hINT2LgwXK8ev82enjnhubUy5UuKGDF3SezAhs".as_bytes()).expect("hash"),
415        ));
416        let media_source = MediaSource::Encrypted(Box::new(EncryptedFile::new(
417            owned_mxc_uri!(
418                "mxc://element.io/b50f38aa8ae820c75992370e4e944a045481e3932057062074730676224"
419            ),
420            file_info,
421            hashes,
422        )));
423        content_scanner.get_media(&client, &media_source).await.expect("Get media");
424    }
425
426    #[async_test]
427    async fn test_get_encrypted_media_unsupported() {
428        let server = MatrixMockServer::new().await;
429        let client =
430            server.client_builder().server_versions(vec![MatrixVersion::V1_11]).build().await;
431
432        let content_scanner_server = MockServer::start().await;
433
434        Mock::given(method("POST"))
435            .and(path("/_matrix/media_proxy/unstable/download_encrypted"))
436            .and(header_exists("Authorization"))
437            .respond_with(ResponseTemplate::new(403).set_body_json(json!({
438                "reason": "MCS_MIME_TYPE_FORBIDDEN",
439                "info": "File type: application/octet-stream not allowed",
440            })))
441            .mount(&content_scanner_server)
442            .await;
443
444        let content_scanner = ContentScanner::new(content_scanner_server.uri());
445        let file_info = EncryptedFileInfo::V2(V2EncryptedFileInfo::new(
446            Base64::parse("tdHdCI5mc-g29IYfhYx2wkA5o-bILP9-nXY6Np1uSnM".as_bytes()).expect("k"),
447            Base64::parse("IBFdH65KqhoAAAAAAAAAAA".as_bytes()).expect("iv"),
448        ));
449        let mut hashes = EncryptedFileHashes::new();
450        hashes.insert(EncryptedFileHash::Sha256(
451            Base64::parse("HSkkamvMSvF3Q30HInorh0ccPrxjgu+wp1vyUOmov/8".as_bytes()).expect("hash"),
452        ));
453        let media_source = MediaSource::Encrypted(Box::new(EncryptedFile::new(
454            owned_mxc_uri!("mxc://matrix.org/WlfuejQQdpvWiWVpAGwfIKJL"),
455            file_info,
456            hashes,
457        )));
458        let err = content_scanner
459            .get_media(&client, &media_source)
460            .await
461            .expect_err("Invalid type error");
462        let client_error = err.as_client_api_error().expect("Invalid error");
463        assert_eq!(client_error.status_code, StatusCode::FORBIDDEN);
464        assert_eq!(
465            client_error.to_string(),
466            "[403] {\"info\":\"File type: application/octet-stream not allowed\",\"reason\":\"MCS_MIME_TYPE_FORBIDDEN\"}"
467        );
468    }
469
470    #[async_test]
471    async fn test_scan_media() {
472        let server = MatrixMockServer::new().await;
473        let client =
474            server.client_builder().server_versions(vec![MatrixVersion::V1_11]).build().await;
475
476        let content_scanner_server = MockServer::start().await;
477        Mock::given(method("GET"))
478            .and(path_regex(r"/_matrix/media_proxy/unstable/scan/.+/.+"))
479            .and(header_exists("Authorization"))
480            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
481                "clean": true,
482                "info": "All clear!"
483            })))
484            .mount(&content_scanner_server)
485            .await;
486
487        let content_scanner = ContentScanner::new(content_scanner_server.uri());
488        let media_source =
489            MediaSource::Plain(owned_mxc_uri!("mxc://matrix.org/RhfpOXOzAwzkuqcmbgMwQUrJ"));
490        let response = content_scanner.scan(&client, &media_source).await.expect("Get media");
491        assert!(response.clean);
492    }
493
494    #[async_test]
495    async fn test_scan_encrypted_media() {
496        let server = MatrixMockServer::new().await;
497        let client =
498            server.client_builder().server_versions(vec![MatrixVersion::V1_11]).build().await;
499
500        let content_scanner_server = MockServer::start().await;
501        Mock::given(method("POST"))
502            .and(path("/_matrix/media_proxy/unstable/scan_encrypted"))
503            .and(header_exists("Authorization"))
504            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
505                "clean": true,
506                "info": "All clear!"
507            })))
508            .mount(&content_scanner_server)
509            .await;
510
511        let content_scanner = ContentScanner::new(content_scanner_server.uri());
512        let file_info = EncryptedFileInfo::V2(V2EncryptedFileInfo::new(
513            Base64::parse("9lpOscZyMOZRCF3v867nPPo3WPNMZt9JXMsuYiWRszc".as_bytes()).expect("k"),
514            Base64::parse("czvdfKSjfLEAAAAAAAAAAA".as_bytes()).expect("iv"),
515        ));
516        let mut hashes = EncryptedFileHashes::new();
517        hashes.insert(EncryptedFileHash::Sha256(
518            Base64::parse("SBbJ3hINT2LgwXK8ev82enjnhubUy5UuKGDF3SezAhs".as_bytes()).expect("hash"),
519        ));
520        let media_source = MediaSource::Encrypted(Box::new(EncryptedFile::new(
521            owned_mxc_uri!(
522                "mxc://element.io/b50f38aa8ae820c75992370e4e944a045481e3932057062074730676224"
523            ),
524            file_info,
525            hashes,
526        )));
527        let response = content_scanner.scan(&client, &media_source).await.expect("Get media");
528        assert!(response.clean);
529    }
530
531    #[async_test]
532    async fn test_scan_media_unsupported() {
533        let server = MatrixMockServer::new().await;
534        let client =
535            server.client_builder().server_versions(vec![MatrixVersion::V1_11]).build().await;
536
537        let content_scanner_server = MockServer::start().await;
538        Mock::given(method("GET"))
539            .and(path_regex(r"/_matrix/media_proxy/unstable/scan/.+/.+"))
540            .and(header_exists("Authorization"))
541            .respond_with(
542                // This always returns a 200 status code for scan results, even for failures
543                ResponseTemplate::new(200).set_body_json(json!({
544                    "clean": false,
545                    "info": "***VIRUS DETECTED***"
546                })),
547            )
548            .mount(&content_scanner_server)
549            .await;
550
551        let content_scanner = ContentScanner::new(content_scanner_server.uri());
552        let media_source =
553            MediaSource::Plain(owned_mxc_uri!("mxc://matrix.org/RhfpOXOzAwzkuqcmbgMwQUrJ"));
554        let response = content_scanner.scan(&client, &media_source).await.expect("Get media");
555        assert!(response.clean.not());
556    }
557
558    #[test]
559    fn test_error_mapping() {
560        let error = HttpError::Api(Box::new(FromHttpResponseError::Server(
561            RumaApiError::MatrixError(ruma::api::error::Error::new(
562                StatusCode::FORBIDDEN,
563                ErrorBody::Json(json!({
564                    "info": "***VIRUS DETECTED***",
565                    "reason": "MCS_MEDIA_NOT_CLEAN"
566                })),
567            )),
568        )));
569        let api_error = error.as_client_api_error().expect("error as api error");
570        assert_eq!(
571            api_error.to_string(),
572            "[403] {\"info\":\"***VIRUS DETECTED***\",\"reason\":\"MCS_MEDIA_NOT_CLEAN\"}"
573        );
574        assert_matches!(&api_error.body, ErrorBody::Json(json_body));
575        let content_scanner_error =
576            ContentScannerError::deserialize(json_body).expect("deserialize");
577        assert_eq!(content_scanner_error.info, "***VIRUS DETECTED***");
578        assert_matches!(content_scanner_error.reason, ErrorReason::MCS_MEDIA_NOT_CLEAN);
579    }
580
581    #[cfg(feature = "e2e-encryption")]
582    #[async_test]
583    async fn test_content_scanner_media_fetcher_decrypts_media() {
584        let server = MatrixMockServer::new().await;
585
586        let media_fetcher = Arc::new(ContentScannerMediaFetcher::new(server.uri()));
587
588        let client = server
589            .client_builder()
590            .on_builder(|builder| builder.media_fetcher(media_fetcher.clone()))
591            .server_versions(vec![MatrixVersion::V1_11])
592            .build()
593            .await;
594
595        // The original (unencrypted) media content
596        let original = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
597        // The encrypted media content to return
598        let encrypted: Vec<u8> = vec![0xEA, 0x10, 0x2D, 0x01, 0x53, 0xB7, 0x87, 0xF0, 0x75, 0xED];
599
600        // Set up the mock server as the content scanner one, with the expected endpoint
601        Mock::given(method("POST"))
602            .and(path("/_matrix/media_proxy/unstable/download_encrypted"))
603            .respond_with(ResponseTemplate::new(200).set_body_bytes(encrypted.clone()))
604            .expect(1)
605            .mount(server.server())
606            .await;
607
608        let mut hashes = EncryptedFileHashes::new();
609        // The SHA256 hash of the unencrypted bytes
610        hashes.insert(EncryptedFileHash::Sha256(
611            Base64::parse("HT/BV9JX7tQgtvLt9NQs914ytE8kp4V6cBGN6SAU/do".as_bytes())
612                .expect("Hash deserialization"),
613        ));
614
615        let request = MediaRequestParameters {
616            source: MediaSource::Encrypted(Box::new(EncryptedFile::new(
617                owned_mxc_uri!("mxc://example.com/1234"),
618                EncryptedFileInfo::V2(V2EncryptedFileInfo::new(
619                    Base64::parse("QRsQvqumCgRLrZEMTZIc-DU-08Lak2c1dBQC6u5x7rE".as_bytes())
620                        .expect("k"),
621                    Base64::parse("uMwbioRbD6EAAAAAAAAAAA".as_bytes()).expect("iv"),
622                )),
623                hashes,
624            ))),
625            format: MediaFormat::File,
626        };
627
628        // If there was a decryption error, we'd be able to check the decryption took
629        // place
630        let result = client
631            .media()
632            .get_media_content(&request, false)
633            .await
634            .expect("Get media content from mock server");
635
636        // Check the original media content matches the decrypted downloaded one
637        assert_eq!(original, result);
638    }
639}