This patch adds an initial implementation of a tool to download and (if encrypted) decrypt matrix media based on the decrypted event content.
224 lines
7.3 KiB
Rust
224 lines
7.3 KiB
Rust
use std::fs;
|
|
use std::process::exit;
|
|
use std::io::Read;
|
|
|
|
use anyhow::Result;
|
|
|
|
use clap::Parser;
|
|
use log::{info, debug, error};
|
|
use serde::Deserialize;
|
|
use rustyline_async::{Readline, ReadlineEvent};
|
|
|
|
use matrix_sdk::{matrix_auth::{MatrixSession, MatrixSessionTokens}, ruma::{api::client::{authenticated_media, media}, device_id, user_id}, Client, SessionMeta};
|
|
use matrix_sdk::ruma::events::room::{MediaSource, message::{MessageType, RoomMessageEventContent}};
|
|
|
|
// NOTE: This has been stolen from the matrix-rust-sdk to patch out checks for authenticated media
|
|
async fn get_media_content(
|
|
client: &Client,
|
|
source: &MediaSource,
|
|
use_auth: bool
|
|
) -> matrix_sdk::Result<Vec<u8>> {
|
|
let content: Vec<u8> = match &source {
|
|
MediaSource::Encrypted(file) => {
|
|
let content = if use_auth {
|
|
let request = authenticated_media::get_content::v1::Request::from_uri(&file.url)?;
|
|
client.send(request, None).await?.file
|
|
} else {
|
|
#[allow(deprecated)]
|
|
let request = media::get_content::v3::Request::from_url(&file.url)?;
|
|
client.send(request, None).await?.file
|
|
};
|
|
|
|
let content = {
|
|
debug!("Decrypting file");
|
|
|
|
let content_len = content.len();
|
|
let mut cursor = std::io::Cursor::new(content);
|
|
let mut reader = match matrix_sdk::crypto::AttachmentDecryptor::new(
|
|
&mut cursor,
|
|
file.as_ref().clone().into(),
|
|
) {
|
|
Ok(reader) => reader,
|
|
Err(error) => {
|
|
error!("Unable to decrypt: {}", error);
|
|
exit(1);
|
|
}
|
|
};
|
|
|
|
// Encrypted size should be the same as the decrypted size,
|
|
// rounded up to a cipher block.
|
|
let mut decrypted = Vec::with_capacity(content_len);
|
|
|
|
reader.read_to_end(&mut decrypted)?;
|
|
|
|
decrypted
|
|
};
|
|
|
|
content
|
|
}
|
|
MediaSource::Plain(uri) => {
|
|
debug!("File doesn't appear to be encrypted");
|
|
|
|
if use_auth {
|
|
let request = authenticated_media::get_content::v1::Request::from_uri(uri)?;
|
|
client.send(request, None).await?.file
|
|
} else {
|
|
#[allow(deprecated)]
|
|
let request = media::get_content::v3::Request::from_url(uri)?;
|
|
client.send(request, None).await?.file
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(content)
|
|
}
|
|
|
|
async fn process_media(source: MediaSource, body: Option<String>, server: Option<String>) -> Result<()> {
|
|
let uri = match source.clone() {
|
|
MediaSource::Plain(uri) => uri,
|
|
MediaSource::Encrypted(encrypted) => encrypted.url,
|
|
};
|
|
|
|
let server_name = uri.server_name()?;
|
|
let media_id = uri.media_id()?;
|
|
|
|
let client = match server {
|
|
Some(ref url) => Client::builder().server_name_or_homeserver_url(url),
|
|
None => Client::builder().server_name(server_name),
|
|
}.build().await?;
|
|
|
|
debug!("Downloading {} from {}", uri, client.homeserver());
|
|
|
|
let content = match get_media_content(&client, &source, false).await {
|
|
Ok(content) => content,
|
|
Err(error) => {
|
|
let error_is_needs_authentication = match &error {
|
|
matrix_sdk::Error::Http(http) => match http {
|
|
matrix_sdk::HttpError::IntoHttp(into_http) => {
|
|
match into_http {
|
|
matrix_sdk::ruma::api::error::IntoHttpError::NeedsAuthentication => true,
|
|
_ => false,
|
|
}
|
|
},
|
|
_ => false
|
|
},
|
|
_ => false,
|
|
};
|
|
|
|
if error_is_needs_authentication {
|
|
info!("The server did not allow downloading without authentication, please enter an access token:");
|
|
|
|
let token = rpassword::prompt_password("> ")?;
|
|
|
|
client.restore_session(MatrixSession {
|
|
meta: SessionMeta {
|
|
user_id: user_id!("@unused:example.com").to_owned(),
|
|
device_id: device_id!("UNUSED").to_owned(),
|
|
},
|
|
tokens: MatrixSessionTokens {
|
|
access_token: token,
|
|
refresh_token: None,
|
|
},
|
|
}).await?;
|
|
|
|
get_media_content(&client, &source, false).await
|
|
.map_err(|error| anyhow::Error::msg(error.to_string()))?
|
|
} else {
|
|
return Err(anyhow::Error::msg(error.to_string()));
|
|
}
|
|
}
|
|
};
|
|
|
|
let file_name = match body {
|
|
Some(body) => format!("{server_name}-{media_id}-{}", body),
|
|
None => format!("{server_name}-{media_id}")
|
|
};
|
|
|
|
info!("Saving to {file_name}");
|
|
|
|
fs::write(file_name, content)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct DecryptedEventSource {
|
|
content: RoomMessageEventContent,
|
|
}
|
|
|
|
#[derive(clap::Parser)]
|
|
#[command(author, version, about, long_about = None)]
|
|
struct Arguments {
|
|
/// Homeserver URL or Name
|
|
#[clap(long, env)]
|
|
server: Option<String>,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
env_logger::init_from_env(env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "matrix_media_event_decrypt=debug"));
|
|
|
|
let Arguments {
|
|
server
|
|
} = Arguments::parse();
|
|
|
|
info!("Enter decrypted event source, confirm with Ctrl+D");
|
|
|
|
let (mut readline, mut _stdout) = Readline::new("> ".to_owned()).unwrap();
|
|
let mut lines = Vec::new();
|
|
|
|
loop {
|
|
match readline.readline().await {
|
|
Ok(ReadlineEvent::Line(line)) => {
|
|
let line = line.trim();
|
|
if line.is_empty() {
|
|
break;
|
|
}
|
|
|
|
// readline.add_history_entry(line.to_owned());
|
|
lines.push(line.to_owned());
|
|
},
|
|
Ok(ReadlineEvent::Eof) => {
|
|
break
|
|
},
|
|
Ok(ReadlineEvent::Interrupted) => {
|
|
println!("^C");
|
|
exit(1);
|
|
},
|
|
Err(error) => {
|
|
error!("Unable to read in decrypted event source: {}", error.to_string());
|
|
exit(1);
|
|
},
|
|
}
|
|
}
|
|
|
|
drop(_stdout);
|
|
drop(readline);
|
|
|
|
let event = match serde_json::from_str::<DecryptedEventSource>(lines.join("\n").as_str()) {
|
|
Ok(event) => event,
|
|
Err(error) => {
|
|
error!("Unable to parse event: {}", error);
|
|
exit(1);
|
|
}
|
|
};
|
|
|
|
println!();
|
|
|
|
debug!("Parsed event with {} content", event.content.msgtype.msgtype());
|
|
|
|
if let Err(error) = match event.content.msgtype {
|
|
MessageType::Audio(content) => process_media(content.source, Some(content.body), server).await,
|
|
MessageType::File(content) => process_media(content.source, Some(content.body), server).await,
|
|
MessageType::Image(content) => process_media(content.source, Some(content.body), server).await,
|
|
MessageType::Video(content) => process_media(content.source, Some(content.body), server).await,
|
|
_ => {
|
|
error!("Event content is not known to contain media, exiting");
|
|
exit(1);
|
|
},
|
|
} {
|
|
error!("Unable to process media: {}", error);
|
|
}
|
|
|
|
Ok(())
|
|
}
|