diff --git a/migrations/20231022210311_optional_encryption.down.sql b/migrations/20231022210311_optional_encryption.down.sql new file mode 100644 index 0000000..a33b235 --- /dev/null +++ b/migrations/20231022210311_optional_encryption.down.sql @@ -0,0 +1 @@ +ALTER TABLE files DROP COLUMN encrypted; \ No newline at end of file diff --git a/migrations/20231022210311_optional_encryption.up.sql b/migrations/20231022210311_optional_encryption.up.sql new file mode 100644 index 0000000..f733739 --- /dev/null +++ b/migrations/20231022210311_optional_encryption.up.sql @@ -0,0 +1 @@ +ALTER TABLE files ADD COLUMN encrypted INTEGER NOT NULL DEFAULT 1; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 52123e2..a7dd78d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ use sqlx::{ }; use storage::Storage; -use crate::routes::{file::file_routes, user::user_routes, gen::gen_routes}; +use crate::routes::{file::file_routes, gen::gen_routes, user::user_routes}; struct ConfigCache { public_url: String, @@ -46,7 +46,11 @@ async fn main() -> Result<()> { // todo: support other databases (mysql, postgresql, etc) sqlx::migrate!().run(&pool).await?; - let data = Data::new(AppData { pool, config, storage }); + let data = Data::new(AppData { + pool, + config, + storage, + }); let bind = std::env::var("BIND").expect("BIND not set in environment"); println!("Lumen is running on {}", bind); diff --git a/src/models.rs b/src/models.rs index cedc5a9..ca7b170 100644 --- a/src/models.rs +++ b/src/models.rs @@ -67,6 +67,7 @@ pub struct File { pub r#type: String, pub hash: String, pub size: i64, + pub encrypted: bool, pub user_id: i64, pub created_at: NaiveDateTime, } diff --git a/src/routes/file.rs b/src/routes/file.rs index 649979c..bff0248 100644 --- a/src/routes/file.rs +++ b/src/routes/file.rs @@ -1,5 +1,7 @@ use actix_web::{ - get, post, + get, + http::header::HeaderValue, + post, web::{self, Bytes, Data}, HttpRequest, HttpResponse, Responder, }; @@ -47,6 +49,11 @@ async fn upload(bytes: Bytes, req: HttpRequest, data: Data) -> impl Res } let user = user.unwrap(); + let encrypted = match req.headers().get("x-encrypted") { + Some(header) => header == HeaderValue::from_static("true"), + None => true, + }; + let file_size = bytes.len() as i64; if user.used + file_size > user.quota { return HttpResponse::PayloadTooLarge().body("Quota exceeded"); @@ -70,27 +77,28 @@ async fn upload(bytes: Bytes, req: HttpRequest, data: Data) -> impl Res let file_extension = file_type.split("/").last().unwrap(); let file_hash = format!("{:x}", Sha3_512::digest(&bytes)); - let cipher = Cipher::default(); - let encrypted_bytes = cipher.encrypt(&bytes); - let encoded = cipher.to_base64(); - - data.storage - .save(String::from(&uuid), &encrypted_bytes) - .await - .unwrap(); - - sqlx::query( - "INSERT INTO files (uuid, name, type, hash, size, user_id) VALUES ($1, $2, $3, $4, $5, $6)", - ) - .bind(&uuid) - .bind(file_name) - .bind(file_type) - .bind(&file_hash) - .bind(file_size) - .bind(user.id) - .execute(&data.pool) - .await - .unwrap(); + let file_query = sqlx::query("INSERT INTO files (uuid, name, type, hash, size, user_id, encrypted) VALUES ($1, $2, $3, $4, $5, $6, $7)").bind(&uuid).bind(file_name).bind(file_type).bind(&file_hash).bind(file_size).bind(user.id); + let (key, nonce) = if encrypted { + let cipher = Cipher::default(); + let encrypted_bytes = cipher.encrypt(&bytes); + let encoded = cipher.to_base64(); + + data.storage + .save(String::from(&uuid), &encrypted_bytes) + .await + .unwrap(); + + file_query.bind(true).execute(&data.pool).await.unwrap(); + (encoded.0, encoded.1) + } else { + data.storage + .save(String::from(&uuid), &bytes) + .await + .unwrap(); + + file_query.bind(false).execute(&data.pool).await.unwrap(); + (String::new(), String::new()) + }; sqlx::query("UPDATE users SET used = used + $1 WHERE id = $2") .bind(file_size) @@ -102,8 +110,8 @@ async fn upload(bytes: Bytes, req: HttpRequest, data: Data) -> impl Res HttpResponse::Ok().json(UploadResponse { id: String::from(&uuid), ext: String::from(file_extension), - key: encoded.0, - nonce: encoded.1, + key, + nonce, }) } @@ -137,9 +145,17 @@ async fn download( let file = file.unwrap(); - let cipher = Cipher::from_base64(&info.key, &info.nonce); - let encrypted_bytes = data.storage.load(id).await.unwrap(); - let bytes = cipher.decrypt(&encrypted_bytes); + if file.encrypted && (info.key.is_empty() || info.nonce.is_empty()) { + return HttpResponse::BadRequest().body("Missing decryption key or nonce"); + } + + let bytes = if file.encrypted { + let cipher = Cipher::from_base64(&info.key, &info.nonce); + let encrypted_bytes = data.storage.load(id).await.unwrap(); + cipher.decrypt(&encrypted_bytes) + } else { + data.storage.load(id).await.unwrap() + }; HttpResponse::Ok() .append_header(("content-disposition", format!("filename=\"{}\"", file.name))) @@ -194,9 +210,17 @@ async fn delete( return HttpResponse::Unauthorized().body("Invalid API key"); } - let cipher = Cipher::from_base64(&info.key, &info.nonce); - let encrypted_bytes = data.storage.load(id).await.unwrap(); - let valid = cipher.verify(&encrypted_bytes); + if file.encrypted && (info.key.is_empty() || info.nonce.is_empty()) { + return HttpResponse::BadRequest().body("Missing decryption key or nonce"); + } + + let valid = if file.encrypted { + let cipher = Cipher::from_base64(&info.key, &info.nonce); + let encrypted_bytes = data.storage.load(id).await.unwrap(); + cipher.verify(&encrypted_bytes) + } else { + true + }; if !valid { return HttpResponse::Unauthorized().body("Invalid decryption key or nonce"); diff --git a/src/routes/mod.rs b/src/routes/mod.rs index ffdd784..caf731f 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,3 +1,3 @@ -pub mod user; pub mod file; -pub mod gen; \ No newline at end of file +pub mod gen; +pub mod user;