Everywhere: Initial commit
This patch adds an initial implementation of a tool to download and (if encrypted) decrypt matrix media based on the decrypted event content.
This commit is contained in:
commit
af2baa5089
7 changed files with 3850 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"homeserver"
|
||||
]
|
||||
}
|
3566
Cargo.lock
generated
Normal file
3566
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "matrix-media-event-decrypt"
|
||||
description = "Decrypt matrix media events"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["networkException <git@nwex.de>"]
|
||||
license = "BSD-2-Clause"
|
||||
repository = "https://git.nwex.de/networkException/matrix-media-event-decrypt"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
clap = { version = "4.5.16", features = ["derive", "env"] }
|
||||
env_logger = "0.11.5"
|
||||
log = "0.4.22"
|
||||
# NOTE: We need newer apis for resolving the homeserver's base url
|
||||
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", rev = "40d447dc69a29f0d805984157565e5c5eeca265e", default-features = false, features = ["native-tls", "e2e-encryption"] }
|
||||
rpassword = "7.3.1"
|
||||
rustyline-async = "0.4.3"
|
||||
serde = { version = "1.0.208", features = ["derive"] }
|
||||
serde_json = "1.0.125"
|
||||
tokio = { version = "1.39.3", features = ["full"] }
|
25
LICENSE
Normal file
25
LICENSE
Normal file
|
@ -0,0 +1,25 @@
|
|||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2024, networkException <git@nwex.de>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
8
shell.nix
Normal file
8
shell.nix
Normal file
|
@ -0,0 +1,8 @@
|
|||
{ pkgs ? import <nixpkgs> {} }: pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
# building
|
||||
cargo
|
||||
pkg-config
|
||||
openssl
|
||||
];
|
||||
}
|
224
src/main.rs
Normal file
224
src/main.rs
Normal file
|
@ -0,0 +1,224 @@
|
|||
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(())
|
||||
}
|
Loading…
Reference in a new issue