Use encrypt image names in html

This commit is contained in:
Klemens Schölhorn 2023-03-13 21:33:29 +01:00
parent 81b7339634
commit 0480943093
4 changed files with 140 additions and 13 deletions

94
Cargo.lock generated
View File

@ -2,6 +2,16 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "alloc-no-stdlib" name = "alloc-no-stdlib"
version = "2.0.4" version = "2.0.4"
@ -342,6 +352,30 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.23" version = "0.4.23"
@ -355,6 +389,17 @@ dependencies = [
"winapi", "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]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.11.1" version = "0.11.1"
@ -415,6 +460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"rand_core",
"typenum", "typenum",
] ]
@ -673,6 +719,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "hmac" name = "hmac"
version = "0.11.0" version = "0.11.0"
@ -802,10 +857,11 @@ dependencies = [
"axum", "axum",
"axum-sessions", "axum-sessions",
"axum-template", "axum-template",
"chacha20poly1305",
"hex",
"image", "image",
"kamadak-exif", "kamadak-exif",
"minijinja", "minijinja",
"rand",
"serde", "serde",
"tokio", "tokio",
"tower-http", "tower-http",
@ -813,6 +869,15 @@ dependencies = [
"tracing-subscriber", "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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.5" version = "1.0.5"
@ -1094,6 +1159,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 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]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -1619,6 +1695,16 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 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]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"
@ -1821,3 +1907,9 @@ name = "windows_x86_64_msvc"
version = "0.42.1" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "zeroize"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"

View File

@ -8,10 +8,11 @@ anyhow = "1.0.69"
axum = { version = "0.6.7", features = ["form", "macros"] } axum = { version = "0.6.7", features = ["form", "macros"] }
axum-sessions = "0.4.1" axum-sessions = "0.4.1"
axum-template = { version = "0.14.0", features = ["minijinja"] } 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"] } image = { version = "0.24.5", default-features = false, features = ["jpeg", "webp-encoder"] }
kamadak-exif = "0.5.5" kamadak-exif = "0.5.5"
minijinja = "0.30.4" minijinja = "0.30.4"
rand = { version = "0.8.5", features = ["min_const_gen"] }
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] }
tokio = { version = "1.25.0", features = ["full"] } tokio = { version = "1.25.0", features = ["full"] }
tower-http = { version = "0.3.5", features = ["fs", "trace", "compression-br"] } tower-http = { version = "0.3.5", features = ["fs", "trace", "compression-br"] }

View File

@ -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 anyhow::{Context, Result, anyhow};
use axum::{Router, routing::{get, post}, response::{IntoResponse, Redirect}, http::{StatusCode, header}, extract::{self, State}, Form, handler::Handler}; 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_sessions::{async_session::CookieStore, SessionLayer, extractors::{ReadableSession, WritableSession}};
use axum_template::{RenderHtml, engine::Engine}; use axum_template::{RenderHtml, engine::Engine};
use chacha20poly1305::{XChaCha20Poly1305, KeyInit, AeadCore, aead::{OsRng, rand_core::RngCore, Aead}, XNonce};
use error::AppError; use error::AppError;
use exif::{Tag, Value, Field, In, Exif}; use exif::{Tag, Value, Field, In, Exif};
use image::{imageops::FilterType, DynamicImage}; use image::{imageops::FilterType, DynamicImage};
use rand::Rng;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use tokio::task; use tokio::task;
use tower_http::{trace::{self, TraceLayer}, compression::CompressionLayer}; use tower_http::{trace::{self, TraceLayer}, compression::CompressionLayer};
@ -25,17 +25,22 @@ mod error;
type TemplateEngine = Engine<minijinja::Environment<'static>>; type TemplateEngine = Engine<minijinja::Environment<'static>>;
type ImageDir = PathBuf; type ImageDir = PathBuf;
type SecretKey = [u8; 64];
#[derive(Clone, extract::FromRef)] #[derive(Clone, extract::FromRef)]
struct ApplicationState { struct ApplicationState {
engine: TemplateEngine, engine: TemplateEngine,
image_dir: ImageDir, image_dir: ImageDir,
secret_key: SecretKey,
} }
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let image_path = args_os().nth(1).expect("Usage: image-gallery IMAGE_DIRECTORY"); 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 default_tracing = "image_gallery=debug,tower_http=info".into();
let tracing_filter = EnvFilter::try_from_default_env().unwrap_or(default_tracing); let tracing_filter = EnvFilter::try_from_default_env().unwrap_or(default_tracing);
let tracing_formatter = tracing_subscriber::fmt::layer() 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("index", include_str!("../templates/index.html")).unwrap();
jinja.add_template("login", include_str!("../templates/login.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_key)
let session_layer = SessionLayer::new(CookieStore::new(), &secret)
.with_cookie_name("session") .with_cookie_name("session")
.with_session_ttl(None); .with_session_ttl(None);
let app = Router::new() let app = Router::new()
.route("/", get(index.layer(CompressionLayer::new()))) .route("/", get(index.layer(CompressionLayer::new())))
.route("/authenticate", post(authenticate)) .route("/authenticate", post(authenticate))
.route("/image/:image", get(converted_image)) .route("/image/:encrypted_filename_hex", get(converted_image))
.layer(TraceLayer::new_for_http() .layer(TraceLayer::new_for_http()
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)) .on_response(trace::DefaultOnResponse::new().level(Level::INFO))
@ -68,6 +72,7 @@ async fn main() {
.with_state(ApplicationState { .with_state(ApplicationState {
engine: Engine::from(jinja), engine: Engine::from(jinja),
image_dir: PathBuf::from(image_path), image_dir: PathBuf::from(image_path),
secret_key,
}); });
let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
@ -78,11 +83,14 @@ async fn main() {
.unwrap(); .unwrap();
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, Default)]
pub struct ImageInfo { pub struct ImageInfo {
width: u32, width: u32,
height: u32, height: u32,
name: String, #[serde(with = "hex")]
encrypted_name: Vec<u8>,
#[serde(skip_serializing)]
name: OsString,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -94,6 +102,7 @@ pub struct IndexTempalte {
async fn index( async fn index(
engine: TemplateEngine, engine: TemplateEngine,
State(image_dir): State<ImageDir>, State(image_dir): State<ImageDir>,
State(secret_key): State<SecretKey>,
session: ReadableSession, session: ReadableSession,
) -> Result<impl IntoResponse, AppError> { ) -> Result<impl IntoResponse, AppError> {
let logged_in = session.get::<()>("logged_in").is_some(); let logged_in = session.get::<()>("logged_in").is_some();
@ -101,6 +110,19 @@ async fn index(
if logged_in { if logged_in {
let images = read_images(&image_dir)?; 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::<Result<Vec<_>>>()?;
let images = images.into_iter().rev().collect(); let images = images.into_iter().rev().collect();
Ok(RenderHtml("index", engine, IndexTempalte { Ok(RenderHtml("index", engine, IndexTempalte {
@ -131,15 +153,26 @@ async fn authenticate(
} }
async fn converted_image( async fn converted_image(
extract::Path(image): extract::Path<String>, extract::Path(encrypted_filename_hex): extract::Path<String>,
State(image_dir): State<ImageDir>, State(image_dir): State<ImageDir>,
State(secret_key): State<SecretKey>,
session: ReadableSession, session: ReadableSession,
) -> Result<impl IntoResponse, AppError> { ) -> Result<impl IntoResponse, AppError> {
session.get::<()>("logged_in") session.get::<()>("logged_in")
.ok_or(anyhow!("Trying to load image while not logged in!")) .ok_or(anyhow!("Trying to load image while not logged in!"))
.context(StatusCode::FORBIDDEN)?; .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() image_path.exists()
.then_some(()) .then_some(())
@ -279,7 +312,8 @@ fn read_image_info(path: &Path) -> Result<ImageInfo> {
Ok(ImageInfo { Ok(ImageInfo {
width, width,
height, 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()
}) })
} }

View File

@ -45,7 +45,7 @@
<main> <main>
{% for image in images %} {% for image in images %}
<div style="aspect-ratio: {{image.width}} / {{image.height}}; flex-grow: {{10*image.width/image.height}};"> <div style="aspect-ratio: {{image.width}} / {{image.height}}; flex-grow: {{10*image.width/image.height}};">
<img loading="lazy" src="/image/{{image.name}}" alt="YOU SHALL NOT SAVE THIS IMAGE!" /> <img loading="lazy" src="/image/{{image.encrypted_name}}" alt="YOU SHALL NOT SAVE THIS IMAGE!" />
</div> </div>
{% endfor %} {% endfor %}
</main> </main>