From 0480943093f4b1c075fb831553c7c5aa195867a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemens=20Sch=C3=B6lhorn?= Date: Mon, 13 Mar 2023 21:33:29 +0100 Subject: [PATCH] Use encrypt image names in html --- Cargo.lock | 94 +++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- src/main.rs | 54 ++++++++++++++++++++----- templates/index.html | 2 +- 4 files changed, 140 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8932e8d..90d2dfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -342,6 +352,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fc89c7c5b9e7a02dfe45cd2367bae382f9ed31c61ca8debe5f827c420a2f08" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.23" @@ -355,6 +389,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -415,6 +460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -673,6 +719,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + [[package]] name = "hmac" version = "0.11.0" @@ -802,10 +857,11 @@ dependencies = [ "axum", "axum-sessions", "axum-template", + "chacha20poly1305", + "hex", "image", "kamadak-exif", "minijinja", - "rand", "serde", "tokio", "tower-http", @@ -813,6 +869,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "itoa" version = "1.0.5" @@ -1094,6 +1159,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1619,6 +1695,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "universal-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "valuable" version = "0.1.0" @@ -1821,3 +1907,9 @@ name = "windows_x86_64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/Cargo.toml b/Cargo.toml index 7602c10..4d56477 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,11 @@ anyhow = "1.0.69" axum = { version = "0.6.7", features = ["form", "macros"] } axum-sessions = "0.4.1" axum-template = { version = "0.14.0", features = ["minijinja"] } +chacha20poly1305 = { version = "0.10.1", features = ["std"] } +hex = { version = "0.4.3", features = ["serde"] } image = { version = "0.24.5", default-features = false, features = ["jpeg", "webp-encoder"] } kamadak-exif = "0.5.5" minijinja = "0.30.4" -rand = { version = "0.8.5", features = ["min_const_gen"] } serde = { version = "1.0.152", features = ["derive"] } tokio = { version = "1.25.0", features = ["full"] } tower-http = { version = "0.3.5", features = ["fs", "trace", "compression-br"] } diff --git a/src/main.rs b/src/main.rs index 9f1ae22..9424422 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ -use std::{net::SocketAddr, path::{Path, PathBuf}, ffi::OsStr, fs::File, io::{BufReader, Seek, Cursor, BufRead}, time::SystemTime, env::args_os}; +use std::{net::SocketAddr, path::{Path, PathBuf}, ffi::{OsStr, OsString}, fs::File, io::{BufReader, Seek, Cursor, BufRead}, time::SystemTime, env::args_os, os::unix::prelude::OsStrExt}; use anyhow::{Context, Result, anyhow}; use axum::{Router, routing::{get, post}, response::{IntoResponse, Redirect}, http::{StatusCode, header}, extract::{self, State}, Form, handler::Handler}; use axum_sessions::{async_session::CookieStore, SessionLayer, extractors::{ReadableSession, WritableSession}}; use axum_template::{RenderHtml, engine::Engine}; +use chacha20poly1305::{XChaCha20Poly1305, KeyInit, AeadCore, aead::{OsRng, rand_core::RngCore, Aead}, XNonce}; use error::AppError; use exif::{Tag, Value, Field, In, Exif}; use image::{imageops::FilterType, DynamicImage}; -use rand::Rng; use serde::{Serialize, Deserialize}; use tokio::task; use tower_http::{trace::{self, TraceLayer}, compression::CompressionLayer}; @@ -25,17 +25,22 @@ mod error; type TemplateEngine = Engine>; type ImageDir = PathBuf; +type SecretKey = [u8; 64]; #[derive(Clone, extract::FromRef)] struct ApplicationState { engine: TemplateEngine, image_dir: ImageDir, + secret_key: SecretKey, } #[tokio::main] async fn main() { let image_path = args_os().nth(1).expect("Usage: image-gallery IMAGE_DIRECTORY"); + let mut secret_key: SecretKey = [0; 64]; + OsRng.fill_bytes(&mut secret_key); + let default_tracing = "image_gallery=debug,tower_http=info".into(); let tracing_filter = EnvFilter::try_from_default_env().unwrap_or(default_tracing); let tracing_formatter = tracing_subscriber::fmt::layer() @@ -51,15 +56,14 @@ async fn main() { jinja.add_template("index", include_str!("../templates/index.html")).unwrap(); jinja.add_template("login", include_str!("../templates/login.html")).unwrap(); - let secret = rand::thread_rng().gen::<[u8; 128]>(); - let session_layer = SessionLayer::new(CookieStore::new(), &secret) + let session_layer = SessionLayer::new(CookieStore::new(), &secret_key) .with_cookie_name("session") .with_session_ttl(None); let app = Router::new() .route("/", get(index.layer(CompressionLayer::new()))) .route("/authenticate", post(authenticate)) - .route("/image/:image", get(converted_image)) + .route("/image/:encrypted_filename_hex", get(converted_image)) .layer(TraceLayer::new_for_http() .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) .on_response(trace::DefaultOnResponse::new().level(Level::INFO)) @@ -68,6 +72,7 @@ async fn main() { .with_state(ApplicationState { engine: Engine::from(jinja), image_dir: PathBuf::from(image_path), + secret_key, }); let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); @@ -78,11 +83,14 @@ async fn main() { .unwrap(); } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Default)] pub struct ImageInfo { width: u32, height: u32, - name: String, + #[serde(with = "hex")] + encrypted_name: Vec, + #[serde(skip_serializing)] + name: OsString, } #[derive(Debug, Serialize)] @@ -94,6 +102,7 @@ pub struct IndexTempalte { async fn index( engine: TemplateEngine, State(image_dir): State, + State(secret_key): State, session: ReadableSession, ) -> Result { let logged_in = session.get::<()>("logged_in").is_some(); @@ -101,6 +110,19 @@ async fn index( if logged_in { let images = read_images(&image_dir)?; + // Encrypt image names + let chacha_key = chacha20poly1305::Key::from_slice(&secret_key[0..32]); + let chacha = XChaCha20Poly1305::new(chacha_key); + let images = images.into_iter().map(|mut image_info| { + let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); + let mut ciphertext = chacha.encrypt(&nonce, image_info.name.as_bytes())?; + + image_info.encrypted_name = nonce.as_slice().to_vec(); + image_info.encrypted_name.append(&mut ciphertext); + + Ok(image_info) + }).collect::>>()?; + let images = images.into_iter().rev().collect(); Ok(RenderHtml("index", engine, IndexTempalte { @@ -131,15 +153,26 @@ async fn authenticate( } async fn converted_image( - extract::Path(image): extract::Path, + extract::Path(encrypted_filename_hex): extract::Path, State(image_dir): State, + State(secret_key): State, session: ReadableSession, ) -> Result { session.get::<()>("logged_in") .ok_or(anyhow!("Trying to load image while not logged in!")) .context(StatusCode::FORBIDDEN)?; - let image_path = image_dir.join(image); + // Decrypt image name + let image_name = { + let encrypted_filename = hex::decode(&encrypted_filename_hex)?; + let nonce = XNonce::from_slice(&encrypted_filename[0..24]); + let ciphertext = &encrypted_filename[24..]; + let chacha_key = chacha20poly1305::Key::from_slice(&secret_key[0..32]); + let chacha = XChaCha20Poly1305::new(chacha_key); + chacha.decrypt(nonce, ciphertext) + }.with_context(|| format!("Could not decrypt filename {}", encrypted_filename_hex))?; + + let image_path = image_dir.join(OsStr::from_bytes(&image_name)); image_path.exists() .then_some(()) @@ -279,7 +312,8 @@ fn read_image_info(path: &Path) -> Result { Ok(ImageInfo { width, height, - name: path.file_name().expect("invalid file path").to_string_lossy().to_string(), + name: path.file_name().expect("invalid file path").to_owned(), + ..Default::default() }) } diff --git a/templates/index.html b/templates/index.html index 6b9faaa..6415ca4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -45,7 +45,7 @@
{% for image in images %}
- YOU SHALL NOT SAVE THIS IMAGE! + YOU SHALL NOT SAVE THIS IMAGE!
{% endfor %}