use std::io::{BufRead, Read, Seek, Write}; use aes_gcm::{ aead::{AeadCore, AeadMutInPlace, OsRng}, Aes256Gcm, Key, KeyInit, Nonce, }; use image::{DynamicImage, ImageBuffer}; use revolt_config::{config, report_internal_error, FilesS3}; use revolt_result::{create_error, Result}; use aws_sdk_s3::{ config::{Credentials, Region}, Client, Config, }; use base64::prelude::*; use tempfile::NamedTempFile; use tiny_skia::Pixmap; /// Size of the authentication tag in the buffer pub const AUTHENTICATION_TAG_SIZE_BYTES: usize = 16; /// Create an S3 client pub fn create_client(s3_config: FilesS3) -> Client { let provider_name = "my-creds"; let creds = Credentials::new( s3_config.access_key_id, s3_config.secret_access_key, None, None, provider_name, ); let config = Config::builder() .region(Region::new(s3_config.region)) .endpoint_url(s3_config.endpoint) .force_path_style(s3_config.path_style_buckets) .credentials_provider(creds) .build(); Client::from_conf(config) } /// Create an AES-256-GCM cipher pub fn create_cipher(key: &str) -> Aes256Gcm { let key = &BASE64_STANDARD.decode(key).expect("valid base64 string")[..]; let key: &Key = key.into(); Aes256Gcm::new(key) } /// Fetch a file from S3 (and decrypt it) pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result> { let config = config().await; let client = create_client(config.files.s3); // Send a request for the file let mut obj = report_internal_error!(client.get_object().bucket(bucket_id).key(path).send().await)?; // Read the file from remote let mut buf = vec![]; while let Some(bytes) = obj.body.next().await { let data = report_internal_error!(bytes)?; report_internal_error!(buf.write_all(&data))?; // is there a more efficient way to do this? // we just want the Vec } // File is not encrypted if nonce.is_empty() { return Ok(buf); } // Recover nonce as bytes let nonce = &BASE64_STANDARD.decode(nonce).unwrap()[..]; let nonce: &Nonce = nonce.into(); // Decrypt the file create_cipher(&config.files.encryption_key) .decrypt_in_place(nonce, b"", &mut buf) .map_err(|_| create_error!(InternalError))?; // Remove the authentication tag bytes that were added during encryption buf.truncate(buf.len() - AUTHENTICATION_TAG_SIZE_BYTES); Ok(buf) } /// Encrypt and upload a file to S3 (returning its nonce/IV) pub async fn upload_to_s3(bucket_id: &str, path: &str, buf: &[u8]) -> Result { let config = config().await; let client = create_client(config.files.s3); // Generate a nonce let nonce = Aes256Gcm::generate_nonce(&mut OsRng); // Extend the buffer for in-place encryption let mut buf = [buf, &[0; AUTHENTICATION_TAG_SIZE_BYTES]].concat(); // Encrypt the file in place create_cipher(&config.files.encryption_key) .encrypt_in_place(&nonce, b"", &mut buf) .map_err(|_| create_error!(InternalError))?; // Upload the file to remote report_internal_error!( client .put_object() .bucket(bucket_id) .key(path) .body(buf.into()) .send() .await )?; Ok(BASE64_STANDARD.encode(nonce)) } /// Delete a file from S3 by path pub async fn delete_from_s3(bucket_id: &str, path: &str) -> Result<()> { let config = config().await; let client = create_client(config.files.s3); report_internal_error!( client .delete_object() .bucket(bucket_id) .key(path) .send() .await )?; Ok(()) } /// Determine size of image at temp file pub fn image_size(f: &NamedTempFile) -> Option<(usize, usize)> { if let Ok(size) = imagesize::size(f.path()) .inspect_err(|err| tracing::error!("Failed to generate image size! {err:?}")) { Some((size.width, size.height)) } else { None } } /// Determine size of image with buffer pub fn image_size_vec(v: &[u8], mime: &str) -> Option<(usize, usize)> { match mime { "image/svg+xml" => { let tree = report_internal_error!(usvg::Tree::from_data(v, &Default::default())).ok()?; let size = tree.size(); Some((size.width() as usize, size.height() as usize)) } _ => { if let Ok(size) = imagesize::blob_size(v) .inspect_err(|err| tracing::error!("Failed to generate image size! {err:?}")) { Some((size.width, size.height)) } else { None } } } } /// Determine size of video at temp file pub fn video_size(f: &NamedTempFile) -> Option<(i64, i64)> { if let Ok(data) = ffprobe::ffprobe(f.path()) .inspect_err(|err| tracing::error!("Failed to ffprobe file! {err:?}")) { // Use first valid stream for stream in data.streams { if let (Some(w), Some(h)) = (stream.width, stream.height) { return Some((w, h)); } } None } else { None } } /// Decode image from reader pub fn decode_image(reader: &mut R, mime: &str) -> Result { match mime { // Read image using jxl-oxide crate "image/jxl" => { let jxl_image = report_internal_error!(jxl_oxide::JxlImage::builder().read(reader))?; if let Ok(frame) = jxl_image.render_frame(0) { match frame.color_channels().len() { 3 => Ok(DynamicImage::ImageRgb8( DynamicImage::ImageRgb32F( ImageBuffer::from_vec( jxl_image.width(), jxl_image.height(), frame.image().buf().to_vec(), ) .ok_or_else(|| create_error!(ImageProcessingFailed))?, ) .to_rgb8(), )), 4 => Ok(DynamicImage::ImageRgba8( DynamicImage::ImageRgba32F( ImageBuffer::from_vec( jxl_image.width(), jxl_image.height(), frame.image().buf().to_vec(), ) .ok_or_else(|| create_error!(ImageProcessingFailed))?, ) .to_rgba8(), )), _ => Err(create_error!(ImageProcessingFailed)), } } else { Err(create_error!(ImageProcessingFailed)) } } // Read image using resvg "image/svg+xml" => { // usvg doesn't support Read trait so copy to buffer let mut buf = Vec::new(); report_internal_error!(reader.read_to_end(&mut buf))?; let tree = report_internal_error!(usvg::Tree::from_data(&buf, &Default::default()))?; let size = tree.size(); let mut pixmap = Pixmap::new(size.width() as u32, size.height() as u32) .ok_or_else(|| create_error!(ImageProcessingFailed))?; let mut pixmap_mut = pixmap.as_mut(); resvg::render(&tree, Default::default(), &mut pixmap_mut); Ok(DynamicImage::ImageRgba8( ImageBuffer::from_vec( size.width() as u32, size.height() as u32, pixmap.data().to_vec(), ) .ok_or_else(|| create_error!(ImageProcessingFailed))?, )) } // Check if we can read using image-rs crate _ => report_internal_error!(report_internal_error!( image::ImageReader::new(reader).with_guessed_format() )? .decode()), } } /// Check whether given reader has a valid image pub fn is_valid_image(reader: &mut R, mime: &str) -> bool { match mime { // Check if we can read using jxl-oxide crate "image/jxl" => jxl_oxide::JxlImage::builder() .read(reader) .inspect_err(|err| tracing::error!("Failed to read JXL! {err:?}")) .is_ok(), // Check if we can read using image-rs crate _ => !matches!( image::ImageReader::new(reader) .with_guessed_format() .inspect_err(|err| tracing::error!("Failed to read image! {err:?}")) .map(|f| f.decode()), Err(_) | Ok(Err(_)) ), } } /// Create thumbnail from given image pub async fn create_thumbnail(image: DynamicImage, tag: &str) -> Vec { // Load configuration let config = config().await; let [w, h] = config.files.preview.get(tag).unwrap(); // Create thumbnail //.resize(width as u32, height as u32, image::imageops::FilterType::Gaussian) // resize is about 2.5x slower, // thumbnail doesn't have terrible quality // so we use thumbnail let image = image.thumbnail(image.width().min(*w as u32), image.height().min(*h as u32)); // Encode it into WEBP let encoder = webp::Encoder::from_image(&image).expect("Could not create encoder."); if config.files.webp_quality != 100.0 { encoder.encode(config.files.webp_quality).to_vec() } else { encoder.encode_lossless().to_vec() } }