Use encrypt image names in html

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

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 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<minijinja::Environment<'static>>;
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<u8>,
#[serde(skip_serializing)]
name: OsString,
}
#[derive(Debug, Serialize)]
@ -94,6 +102,7 @@ pub struct IndexTempalte {
async fn index(
engine: TemplateEngine,
State(image_dir): State<ImageDir>,
State(secret_key): State<SecretKey>,
session: ReadableSession,
) -> Result<impl IntoResponse, AppError> {
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::<Result<Vec<_>>>()?;
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<String>,
extract::Path(encrypted_filename_hex): extract::Path<String>,
State(image_dir): State<ImageDir>,
State(secret_key): State<SecretKey>,
session: ReadableSession,
) -> Result<impl IntoResponse, AppError> {
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<ImageInfo> {
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()
})
}