diff --git a/server/Cargo.toml b/server/Cargo.toml index b0e54c1..2957270 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -24,7 +24,7 @@ tower = "0.4.13" log = "0.4" env_logger = "0.10" sailfish = "0.8.3" -tower-http = { version="0.5", features=["fs"], default-features=false } +tower-http = { version="0.5", features=["fs", "cors"], default-features=false } prometheus-client = "0.22.0" chacha20 = "0.9" @@ -33,7 +33,7 @@ hex = "0.4" bytes = "1.5" pin-project-lite = "0.2" poly1305 = "0.8.0" -zeroize = "1.7.0" +zeroize = { version="1.7.0", features=["zeroize_derive"]} axum-extra = { version="0.9.2", features=["async-read-body"]} reqwest = { version="0.11", default_features=false, features=["rustls-tls", "json"] } diff --git a/server/src/aeadstream.rs b/server/src/aeadstream.rs new file mode 100644 index 0000000..3ff2535 --- /dev/null +++ b/server/src/aeadstream.rs @@ -0,0 +1,322 @@ +use std::{ + io::ErrorKind, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use chacha20::cipher::{generic_array::GenericArray, KeyInit, StreamCipher, StreamCipherSeek}; +use pin_project_lite::pin_project; +use poly1305::{universal_hash::UniversalHash, Poly1305, Tag}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use zeroize::Zeroize; + +pin_project! { + pub struct AeadStreamWriter + where + T: AsyncWrite, + C: StreamCipher, + C: StreamCipherSeek + { + #[pin] + inner: T, + buf: Vec, + to_mac: Vec, + cipher: C, + mac: Poly1305, + tag: Option, + encrypted: usize, + authenticated_data_len: usize, + } +} + +impl AeadStreamWriter +where + T: AsyncWrite, + C: StreamCipher + StreamCipherSeek, +{ + pub fn new(inner: T, mut cipher: C, authenticated_data: &[u8]) -> Self { + let mut mac_key = poly1305::Key::default(); + cipher.apply_keystream(&mut mac_key); + + let mut mac = Poly1305::new(GenericArray::from_slice(&mac_key)); + mac_key.zeroize(); + + mac.update_padded(authenticated_data); + + // Set ChaCha20 counter to 1 + cipher.seek(64); + Self { + inner, + buf: vec![], + to_mac: vec![], + authenticated_data_len: authenticated_data.len(), + encrypted: 0, + cipher, + mac, + tag: None, + } + } + pub fn tag(&self) -> Option<&Tag> { + self.tag.as_ref() + } + pub fn size(&self) -> usize { + self.encrypted + } +} + +impl AsyncWrite for AeadStreamWriter +where + T: AsyncWrite, + C: StreamCipher + StreamCipherSeek, +{ + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let me = self.project(); + + let mut buf = Vec::from(buf); + + let buf_len = buf.len(); + me.cipher.apply_keystream(&mut buf); + *me.encrypted += buf_len; + + me.to_mac.append(&mut buf.clone()); + let macable = me + .to_mac + .drain(..align(me.to_mac.len(), 16)) + .collect::>(); + me.mac.update_padded(&macable); + + if !me.buf.is_empty() { + me.buf.append(&mut buf); + std::mem::swap(me.buf, &mut buf); + } + + if let Poll::Ready(Ok(written)) = me.inner.poll_write(cx, &buf) { + buf.truncate(buf.len() - written); + } + std::mem::swap(me.buf, &mut buf); + + Poll::Ready(Ok(buf_len)) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let me = self.project(); + + if !me.buf.is_empty() { + let written = ready!(me.inner.poll_write(cx, me.buf))?; + me.buf.truncate(written); + Poll::Pending + } else { + me.inner.poll_flush(cx) + } + } + + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + let me = self.project(); + + if !me.buf.is_empty() { + let written = ready!(me.inner.poll_write(cx, me.buf))?; + me.buf.truncate(written); + Poll::Pending + } else if me.tag.is_none() { + me.mac.update_padded(me.to_mac); + me.to_mac.clear(); + + authenticate_lengths(me.mac, *me.authenticated_data_len, *me.encrypted); + + *me.tag = Some(me.mac.clone().finalize()); + me.inner.poll_shutdown(cx) + } else { + me.inner.poll_shutdown(cx) + } + } +} + +pin_project! { + pub struct AeadStreamReader + where + T: AsyncRead, + C: StreamCipher, + C: StreamCipherSeek, + { + #[pin] + inner: T, + buf: Vec, + cipher: C, + mac: AeadStreamReaderMac, + decrypted_data_len: usize, + authenticated_data_len: usize, + expected_data_len: Option, + tag: Tag, + } +} + +impl AeadStreamReader +where + T: AsyncRead, + C: StreamCipher + StreamCipherSeek, +{ + pub fn new( + inner: T, + mut cipher: C, + authenticated_data: &[u8], + tag: Tag, + expected_data_len: Option, + ) -> Self { + let mut mac_key = poly1305::Key::default(); + cipher.apply_keystream(&mut mac_key); + + let mut mac = Poly1305::new(GenericArray::from_slice(&mac_key)); + mac_key.zeroize(); + + mac.update_padded(authenticated_data); + + // Set ChaCha20 counter to 1 + cipher.seek(64); + Self { + inner, + buf: vec![], + cipher, + mac: AeadStreamReaderMac::Running(mac), + authenticated_data_len: authenticated_data.len(), + decrypted_data_len: 0, + expected_data_len, + tag, + } + } +} + +#[derive(Debug)] +enum AeadStreamReaderMac { + Running(M), + Finished, + Failed, +} + +impl AsyncRead for AeadStreamReader +where + T: AsyncRead, + C: StreamCipher + StreamCipherSeek, +{ + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let me = self.project(); + + let mut mac = AeadStreamReaderMac::Failed; + std::mem::swap(&mut mac, me.mac); + + match mac { + AeadStreamReaderMac::Failed => Poll::Ready(Err(std::io::Error::new( + ErrorKind::InvalidData, + "decryption failed", + ))), + AeadStreamReaderMac::Finished => Poll::Ready(Ok(())), + AeadStreamReaderMac::Running(mut mac) => { + let unchanged = { + let prev_buf_len = me.buf.len(); + me.buf.extend_from_slice(&[0_u8; 512]); + let mut read_buf = ReadBuf::new(me.buf); + read_buf.set_filled(prev_buf_len); + + if let Poll::Ready(res) = me.inner.poll_read(cx, &mut read_buf) { + res?; + let buf_len = read_buf.filled().len(); + drop(read_buf); + me.buf.truncate(buf_len); + prev_buf_len == buf_len + } else { + me.buf.truncate(prev_buf_len); + + *me.mac = AeadStreamReaderMac::Running(mac); + return Poll::Pending; + } + }; + + if unchanged && me.buf.len() <= 16 { + let decryptable = me.buf; + if !decryptable.is_empty() { + mac.update_padded(decryptable); + me.cipher.apply_keystream(decryptable); + + *me.decrypted_data_len += decryptable.len(); + + buf.put_slice(decryptable); + } + + authenticate_lengths( + &mut mac, + *me.authenticated_data_len, + *me.decrypted_data_len, + ); + + match mac.verify(me.tag) { + Ok(_) => { + *me.mac = AeadStreamReaderMac::Finished; + Poll::Ready(Ok(())) + } + Err(_) => { + *me.mac = AeadStreamReaderMac::Failed; + Poll::Ready(Err(std::io::Error::new( + ErrorKind::InvalidData, + "decryption failed", + ))) + } + } + } else { + let (decryptable, residual) = { + let decryptable_buffer_len = align(min(buf.remaining(), me.buf.len()), 16); + me.buf.split_at_mut(decryptable_buffer_len) + }; + if !decryptable.is_empty() { + mac.update_padded(decryptable); + + me.cipher.apply_keystream(decryptable); + + *me.decrypted_data_len += decryptable.len(); + + buf.put_slice(decryptable); + + { + let decryptable_len = decryptable.len(); + let residual_len = residual.len(); + me.buf.copy_within(decryptable_len.., 0); + me.buf.truncate(residual_len); + } + + *me.mac = AeadStreamReaderMac::Running(mac); + Poll::Ready(Ok(())) + } else { + *me.mac = AeadStreamReaderMac::Running(mac); + Poll::Pending + } + } + } + } + } +} + +fn min(a: usize, b: usize) -> usize { + match a < b { + true => a, + false => b, + } +} +fn align(val: usize, m: usize) -> usize { + val - (val % m) +} + +fn authenticate_lengths(mac: &mut Poly1305, associated_data_len: usize, buffer_len: usize) { + let mut block = GenericArray::default(); + block[..8].copy_from_slice(&associated_data_len.to_le_bytes()); + block[8..].copy_from_slice(&buffer_len.to_le_bytes()); + mac.update(&[block]); +} diff --git a/server/src/main.rs b/server/src/main.rs index 247771e..1fdaffe 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,13 +1,12 @@ #![deny(clippy::unwrap_used)] +use aeadstream::AeadStreamReader; use axum_extra::body::AsyncReadBody; use axum_oidc::{ error::{ExtractorError, MiddlewareError}, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer, }; -use duration_str::parse_std; use jwt::JwtApplication; -use pin_project_lite::pin_project; -use poly1305::{universal_hash::UniversalHash, Poly1305, Tag}; +use poly1305::Tag; use prometheus_client::{ encoding::EncodeLabelSet, metrics::{counter::Counter, family::Family, gauge::Gauge}, @@ -17,57 +16,49 @@ use sailfish::TemplateOnce; use std::{ borrow::Borrow, env, - io::{ErrorKind, SeekFrom}, - pin::Pin, + io::SeekFrom, str::FromStr, sync::Arc, - task::{ready, Context, Poll}, time::{Duration, SystemTime, UNIX_EPOCH}, }; -use tower_http::services::ServeDir; +use tower_http::{cors::CorsLayer, services::ServeDir}; use tower::ServiceBuilder; use tower_sessions::{cookie::SameSite, MemoryStore, SessionManagerLayer}; use axum::{ - async_trait, body::Body, error_handling::HandleErrorLayer, - extract::{FromRef, FromRequest, Multipart, Path, Query, State}, + extract::{FromRef, Path, Query, State}, http::{ header::{self, CONTENT_TYPE}, - HeaderMap, HeaderValue, Request, StatusCode, Uri, + HeaderMap, HeaderValue, Method, StatusCode, Uri, }, - response::{Html, IntoResponse, Redirect, Response}, + response::{Html, IntoResponse, Redirect}, routing::{delete, get}, Router, }; -use chacha20::{ - cipher::{generic_array::GenericArray, KeyInit, KeyIvInit, StreamCipher, StreamCipherSeek}, - ChaCha20, -}; +use chacha20::{cipher::KeyIvInit, ChaCha20}; use futures_util::StreamExt; use garbage_collector::GarbageCollector; -use log::{debug, info, warn}; +use log::{debug, warn}; use serde::Deserialize; use tokio::{ fs::{self, File}, - io::{ - AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter, - ReadBuf, - }, + io::{AsyncSeekExt, AsyncWriteExt, BufReader, BufWriter}, net::TcpListener, }; use util::{IdSalt, KeySalt}; -use zeroize::Zeroize; use crate::{ + aeadstream::AeadStreamWriter, error::Error, jwt::Claims, metadata::Metadata, util::{Id, Key, Nonce, Phrase}, }; +mod aeadstream; mod error; mod garbage_collector; mod jwt; @@ -163,6 +154,14 @@ async fn main() { .await .expect("Jwt Authentication Client"); + let cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST, Method::PUT]) + .allow_origin( + application_base + .parse::() + .expect("valid APPLICATION_BASE"), + ); + let data_path = env::var("DATA_PATH").expect("DATA_PATH env var"); let mut registry = Registry::default(); @@ -218,7 +217,8 @@ async fn main() { .nest_service("/static", ServeDir::new("static")) .with_state(state) .layer(oidc_auth_service) - .layer(session_service); + .layer(session_service) + .layer(cors); let listener = TcpListener::bind("[::]:8080") .await @@ -322,7 +322,7 @@ async fn upload_bin( Query(params): Query, State(app_state): State, headers: HeaderMap, - data: MultipartOrStream, + body: Body, ) -> HandlerResult { let phrase = Phrase::from_str(&phrase)?; let id = Id::from_phrase(&phrase, &app_state.id_salt); @@ -340,66 +340,35 @@ async fn upload_bin( let file = File::create(&path).await?; let writer = BufWriter::new(file); - let mut ttl = params + let ttl = params .ttl .map(|x| duration_str::parse(x).map_err(|_| Error::InvalidTtl)) .transpose()? .unwrap_or(Duration::from_secs(24 * 3600)); - let mut writer = EncWriter::new(writer, key, nonce, &[]); + let cipher = ChaCha20::new(key.borrow(), nonce.borrow()); + let mut writer = AeadStreamWriter::new(writer, cipher, &[]); - match data { - MultipartOrStream::Stream(stream) => { - let mut stream = stream.into_data_stream(); - while let Some(chunk) = stream.next().await { - writer.write_all(chunk.unwrap_or_default().as_ref()).await?; - } - writer.shutdown().await?; - metadata.content_type = match headers.get(CONTENT_TYPE) { - Some(content_type) => Some( - content_type - .to_owned() - .to_str() - .unwrap_or_default() - .to_string(), - ), - None => Some("application/octet-stream".to_string()), - }; - } - MultipartOrStream::Multipart(mut multipart) => { - let mut file_read = false; - while let Some(mut field) = multipart - .next_field() - .await - .map_err(|_| Error::InvalidMultipart)? - { - if field.name().unwrap_or_default() == "file" && !file_read { - while let Some(chunk) = field.chunk().await.unwrap_or_default() { - writer.write_all(chunk.as_ref()).await?; - } - metadata.content_type = Some( - field - .content_type() - .map(|x| x.to_string()) - .unwrap_or("application/octet-stream".to_string()), - ); - file_read = true; - } - if field.name().unwrap_or_default() == "ttl" { - if let Some(mp_ttl) = - field.text().await.ok().and_then(|x| parse_std(x).ok()) - { - ttl = mp_ttl; - } - } - } - } + let mut stream = body.into_data_stream(); + while let Some(chunk) = stream.next().await { + writer.write_all(chunk.unwrap_or_default().as_ref()).await?; } + writer.flush().await?; writer.shutdown().await?; - let tag = writer.tag().expect("valid tag"); + metadata.content_type = match headers.get(CONTENT_TYPE) { + Some(content_type) => Some( + content_type + .to_owned() + .to_str() + .unwrap_or_default() + .to_string(), + ), + None => Some("application/octet-stream".to_string()), + }; + if let Some(expires_at) = SystemTime::now() .duration_since(UNIX_EPOCH)? .checked_add(ttl) @@ -466,41 +435,27 @@ async fn get_item( &hex::decode(metadata.tag.as_deref().unwrap_or("")).unwrap_or_default(), ); - { - let mut cipher = ChaCha20::new(key.borrow(), nonce.borrow()); - let mut mac_key = poly1305::Key::default(); - cipher.apply_keystream(&mut mac_key); - - let mut mac = Poly1305::new(GenericArray::from_slice(&mac_key)); - mac_key.zeroize(); - - mac.update_padded(&[]); - - let mut buf = [0_u8; 512]; - let mut read_length = 0; - while let Ok(read) = reader.read(&mut buf).await { - if read == 0 { - break; - } - mac.update_padded(&buf[..read]); - read_length += read; - } - debug!("{}", read_length); - - authenticate_lengths(&mut mac, 0, read_length); - - mac.verify(&tag).map_err(|_| Error::BinVerificationFailed)?; - } - reader.seek(SeekFrom::Start(0)).await?; - let body = AsyncReadBody::new(DecReader::new(reader, key, nonce, &[], tag)); + let cipher = ChaCha20::new(key.borrow(), nonce.borrow()); + let body = AsyncReadBody::new(AeadStreamReader::new( + reader, + cipher, + &[], + tag, + metadata.size.map(|x| x as usize), + )); let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_LENGTH, - metadata.size.unwrap_or_default().into(), - ); + // If send, the client directly disconnects after the CONTENT_LENGTH bytes are received. + // Because of that the MAC validation is skipped and the user may receive invalid data. + // Not sending the CONTENT_LENGTH results in the Client asking the server again, triggering + // MAC validation. + // TODO: check encrypted to the size, triggering the MAC validation on the last byte + //headers.insert( + // header::CONTENT_LENGTH, + // metadata.size.unwrap_or_default().into(), + //); if let Ok(subject) = metadata.subject.parse() { headers.insert("CreatedBy", subject); } @@ -533,41 +488,6 @@ async fn metrics(State(app_state): State) -> HandlerResult FromRequest for MultipartOrStream { - type Rejection = Response; - - async fn from_request(req: Request, state: &S) -> Result { - let is_multipart = req - .headers() - .get(CONTENT_TYPE) - .and_then(|x| { - x.to_str() - .ok() - .map(|y| y.starts_with("multipart/form-data")) - }) - .unwrap_or_default(); - if is_multipart { - Ok(Self::Multipart( - Multipart::from_request(req, state) - .await - .map_err(|x| x.into_response())?, - )) - } else { - Ok(Self::Stream( - Body::from_request(req, state) - .await - .map_err(|x| x.into_response())?, - )) - } - } -} - #[derive(TemplateOnce)] #[template(path = "index.stpl")] pub struct IndexTemplate<'a> { @@ -577,274 +497,3 @@ pub struct IndexTemplate<'a> { #[derive(TemplateOnce)] #[template(path = "delete.stpl")] pub struct DeleteTemplate; - -pin_project! { - struct EncWriter { - #[pin] - inner: T, - buf: Vec, - to_mac: Vec, - cipher: ChaCha20, - mac: Poly1305, - tag: Option, - encrypted: usize, - authenticated_data_len: usize, - } -} - -impl EncWriter { - pub fn new(inner: T, key: Key, nonce: Nonce, authenticated_data: &[u8]) -> Self { - let mut cipher = ChaCha20::new(key.borrow(), nonce.borrow()); - let mut mac_key = poly1305::Key::default(); - cipher.apply_keystream(&mut mac_key); - - let mut mac = Poly1305::new(GenericArray::from_slice(&mac_key)); - mac_key.zeroize(); - - mac.update_padded(authenticated_data); - - // Set ChaCha20 counter to 1 - cipher.seek(64); - Self { - inner, - buf: vec![], - to_mac: vec![], - authenticated_data_len: authenticated_data.len(), - encrypted: 0, - cipher, - mac, - tag: None, - } - } - pub fn tag(&self) -> Option<&Tag> { - self.tag.as_ref() - } - pub fn size(&self) -> usize { - self.encrypted - } -} - -impl AsyncWrite for EncWriter { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - let me = self.project(); - - if me.buf.len() > 16 { - return Poll::Pending; - } - - let mut buf = Vec::from(buf); - - me.cipher.apply_keystream(&mut buf); - - me.to_mac.extend_from_slice(&buf); - let macable = me.to_mac.drain(..align(buf.len(), 16)).collect::>(); - debug!("645: {}", macable.len() % 16); - me.mac.update_padded(&macable); - - *me.encrypted += buf.len(); - let buf_len = buf.len(); - - me.buf.append(&mut buf); - - if let Poll::Ready(Ok(written)) = me.inner.poll_write(cx, me.buf) { - me.buf.drain(..written); - } - - debug!("after write: {}", buf_len); - - Poll::Ready(Ok(buf_len)) - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let me = self.project(); - - debug!("flush"); - - if !me.buf.is_empty() { - let written = ready!(me.inner.poll_write(cx, me.buf))?; - me.buf.drain(..written); - Poll::Pending - } else { - me.inner.poll_flush(cx) - } - } - - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - let me = self.project(); - - debug!("shutdown"); - - if !me.buf.is_empty() { - let written = ready!(me.inner.poll_write(cx, me.buf))?; - me.buf.drain(..written); - Poll::Pending - } else if me.tag.is_none() { - me.mac.update_padded(me.to_mac); - me.to_mac.clear(); - - authenticate_lengths(me.mac, *me.authenticated_data_len, *me.encrypted); - - *me.tag = Some(me.mac.clone().finalize()); - Poll::Pending - } else { - me.inner.poll_shutdown(cx) - } - } -} - -pin_project! { - struct DecReader { - #[pin] - inner: T, - buf: Vec, - cipher: ChaCha20, - mac: Poly1305, - decrypted: usize, - authenticated_data_len: usize, - tag: Tag, - state: DecReaderState, - } -} - -impl DecReader { - pub fn new(inner: T, key: Key, nonce: Nonce, authenticated_data: &[u8], tag: Tag) -> Self { - let mut cipher = ChaCha20::new(key.borrow(), nonce.borrow()); - let mut mac_key = poly1305::Key::default(); - cipher.apply_keystream(&mut mac_key); - - let mut mac = Poly1305::new(GenericArray::from_slice(&mac_key)); - mac_key.zeroize(); - - mac.update_padded(authenticated_data); - - // Set ChaCha20 counter to 1 - cipher.seek(64); - Self { - inner, - buf: vec![0_u8; 512], - cipher, - mac, - authenticated_data_len: authenticated_data.len(), - decrypted: 0, - state: DecReaderState::Running, - tag, - } - } -} - -#[derive(Debug)] -enum DecReaderState { - Running, - Finished, - Failed, -} - -impl AsyncRead for DecReader { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - let me = self.project(); - - //TODO move buf.filled to struct - - match me.state { - DecReaderState::Finished => Poll::Ready(Ok(())), - DecReaderState::Failed => Poll::Ready(Err(std::io::Error::new( - ErrorKind::InvalidData, - "decryption failed", - ))), - DecReaderState::Running => { - let changed = { - me.buf.extend_from_slice(&[0_u8; 512]); - let mut read_buf = ReadBuf::new(me.buf); - - let prev_buf_len = read_buf.filled().len(); - ready!(me.inner.poll_read(cx, &mut read_buf))?; - let buf_len = read_buf.filled().len(); - drop(read_buf); - me.buf.drain(buf_len..); - prev_buf_len == buf_len - }; - - if changed { - let decryptable = me.buf; - me.mac.update_padded(decryptable); - me.cipher.apply_keystream(decryptable); - - *me.decrypted += decryptable.len(); - - buf.put_slice(decryptable); - - authenticate_lengths(me.mac, *me.authenticated_data_len, *me.decrypted); - - match me.mac.clone().verify(me.tag) { - Ok(_) => { - *me.state = DecReaderState::Finished; - - warn!("finished"); - Poll::Ready(Ok(())) - } - Err(_) => { - *me.state = DecReaderState::Failed; - warn!("decryption failed"); - - Poll::Ready(Err(std::io::Error::new( - ErrorKind::InvalidData, - "decryption failed", - ))) - } - } - } else { - let (decryptable, residual) = { - let decryptable_buffer_len = align(min(buf.remaining(), me.buf.len()), 16); - me.buf.split_at_mut(decryptable_buffer_len) - }; - if !decryptable.is_empty() { - me.mac.update_padded(decryptable); - me.cipher.apply_keystream(decryptable); - - *me.decrypted += decryptable.len(); - - buf.put_slice(decryptable); - - { - let decryptable_len = decryptable.len(); - let residual_len = residual.len(); - me.buf.copy_within(decryptable_len.., 0); - me.buf.drain(residual_len..); - } - Poll::Ready(Ok(())) - } else { - Poll::Pending - } - } - } - } - } -} - -fn min(a: usize, b: usize) -> usize { - match a < b { - true => a, - false => b, - } -} -fn align(val: usize, m: usize) -> usize { - val - (val % m) -} - -fn authenticate_lengths(mac: &mut Poly1305, associated_data_len: usize, buffer_len: usize) { - let mut block = GenericArray::default(); - block[..8].copy_from_slice(&associated_data_len.to_le_bytes()); - block[8..].copy_from_slice(&buffer_len.to_le_bytes()); - mac.update(&[block]); -} diff --git a/server/src/metadata.rs b/server/src/metadata.rs index 228714a..693b39e 100644 --- a/server/src/metadata.rs +++ b/server/src/metadata.rs @@ -1,5 +1,6 @@ use std::{io::ErrorKind, time::Instant}; +use log::debug; use serde::{Deserialize, Serialize}; use tokio::{fs::File, io::AsyncWriteExt}; @@ -19,7 +20,10 @@ impl Metadata { pub async fn from_file(path: &str) -> Result { let metadata = match tokio::fs::read_to_string(path).await { Ok(x) => Ok(x), - Err(err) if err.kind() == ErrorKind::NotFound => Err(Error::BinNotFound), + Err(err) if err.kind() == ErrorKind::NotFound => { + debug!("{} {:?}", path, err); + Err(Error::BinNotFound) + } Err(x) => Err(x.into()), }?; Ok(toml::from_str::(&metadata)?) diff --git a/server/src/web_util.rs b/server/src/web_util.rs deleted file mode 100644 index fc9ba39..0000000 --- a/server/src/web_util.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::{ - borrow::Borrow, - pin::Pin, - task::{Context, Poll}, -}; - -use bytes::{Bytes, BytesMut}; -use chacha20::{ - cipher::{KeyIvInit, StreamCipher}, - ChaCha20, -}; -use futures_util::Stream; -use log::{debug, warn}; -use pin_project_lite::pin_project; -use sha3::{Digest, Sha3_256}; -use tokio::io::AsyncRead; -use tokio_util::io::poll_read_buf; - -use crate::{ - metadata::Metadata, - util::{Id, Key, Nonce}, -}; -pin_project! { - pub(crate) struct DecryptingStream { - #[pin] - reader: Option, - buf: BytesMut, - // chunk size - capacity: usize, - // chacha20 cipher - cipher: ChaCha20, - // hash to verify against - target_hash: String, - // id of the file for logging purposes - id: Id, - // total file size - size: u64, - // current position of the "reading head" - progress: u64 - } -} -impl DecryptingStream { - pub(crate) fn new(reader: R, id: Id, metadata: &Metadata, key: &Key, nonce: &Nonce) -> Self { - let cipher = ChaCha20::new(key.borrow(), nonce.borrow()); - Self { - reader: Some(reader), - buf: BytesMut::new(), - capacity: 1 << 22, // 4 MiB - cipher, - target_hash: metadata.tag.clone().unwrap_or_default(), - id, - size: metadata.size.unwrap_or_default(), - progress: 0, - } - } -} - -impl Stream for DecryptingStream { - type Item = std::io::Result; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let mut this = self.as_mut().project(); - - let reader = match this.reader.as_pin_mut() { - Some(r) => r, - None => return Poll::Ready(None), - }; - - if this.buf.capacity() == 0 { - this.buf.reserve(*this.capacity); - } - - match poll_read_buf(reader, cx, &mut this.buf) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => { - debug!("failed to send bin {}", this.id); - self.project().reader.set(None); - Poll::Ready(Some(Err(err))) - } - Poll::Ready(Ok(0)) => { - if self.progress_check() == DecryptingStreamProgress::Failed { - // The hash is invalid, the file has been tampered with. Close reader and stream causing the download to fail - self.project().reader.set(None); - return Poll::Ready(None); - }; - self.project().reader.set(None); - Poll::Ready(None) - } - Poll::Ready(Ok(n)) => { - let mut chunk = this.buf.split(); - // decrypt the chunk using chacha - this.cipher.apply_keystream(&mut chunk); - // update the sha3 hasher - // track progress - *this.progress += n as u64; - if self.progress_check() == DecryptingStreamProgress::Failed { - // The hash is invalid, the file has been tampered with. Close reader and stream causing the download to fail - warn!("bin {} is corrupted! transmission failed", self.id); - self.project().reader.set(None); - return Poll::Ready(None); - }; - Poll::Ready(Some(Ok(chunk.freeze()))) - } - } - } -} - -impl DecryptingStream { - /// checks if the hash is correct when the last byte has been read - fn progress_check(&self) -> DecryptingStreamProgress { - if self.progress >= self.size { - let hash = hex::encode(self.hasher.clone().finalize()); - if hash != self.target_hash { - DecryptingStreamProgress::Failed - } else { - DecryptingStreamProgress::Finished - } - } else { - DecryptingStreamProgress::Running - } - } -} - -#[derive(PartialEq, Eq)] -enum DecryptingStreamProgress { - Finished, - Failed, - Running, -} diff --git a/server/static/zettoit.css b/server/static/zettoit.css deleted file mode 100644 index 0aa6884..0000000 --- a/server/static/zettoit.css +++ /dev/null @@ -1,172 +0,0 @@ -/* - * GENERAL - */ -:root { - --s0: 1px; - --s1: 2px; - --s2: 4px; - --s3: 8px; - --s4: 16px; -} -html { - font-family: monospace; - font-size: 16px; - line-height: calc(1em + var(--spacing-2)); -} -body { - min-height: 100vh; - max-width: 100%; - background-color: black; - color: #eceff4; - margin: 0; - - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 1rem; -} - -main { - border: 1px solid white; - padding: var(--s3); - max-width: calc(100% - 2*var(--s3) - 2*var(--s0)); - display: flex; - flex-direction: column; - gap: var(--s4); - -} - -.watermark { - position: fixed; - right: 0.25rem; - bottom: 0.25rem; - filter: invert(); - height: 1em; - color: white; -} -.watermark img { - height: 100%; -} - -/* - * LAYOUT - */ -.container { - width: 100%; - margin-left: auto; - margin-right: auto; -} -@media (min-width: 576px) { - .container { - max-width: 540px; - } -} -@media (min-width: 768px) { - .container { - max-width: 720px; - } -} -@media (min-width: 992px) { - .container { - max-width: 960px; - } -} -@media (min-width: 1200px) { - .container { - max-width: 1140px; - } -} - -.centered { - text-align: center; -} - -/* - * TYPOGRAPHY - */ -h1,h2,h3,h4,h5,h6,p { - margin: 0; -} -h1 { - font-size: 1.6rem; -} -h2 { - font-size: 1.5rem; -} -h3 { - font-size: 1.4rem; -} -h4 { - font-size: 1.3rem; -} -h5 { - font-size: 1.2rem; -} -h6 { - font-size: 1.1rem; -} -small { - font-size: 0.9rem; -} - -/* - * COMPONENTS - */ -pre { - border-width: var(--s0) var(--s0) var(--s0) var(--s2); - border-color: #e5e9f0; - border-style: solid; - padding-left: var(--s1); - font-size: 0.8rem; - overflow-x: auto; - margin: 0px; -} - -article { - border: var(--s0) solid #e5e9f0; - padding: var(--s3); -} - -button { - background-color: white; - color: black; - border: 1px solid white; - font-weight: bold; - padding: 0.5em; - font-size: 1.1em; - cursor: pointer; - display: block; - width: 100%; -} -button:active, input[type="file"]::file-selector-button:active { - background-color: black; - color: white; -} - -/* - * Form - */ -form{ - display: flex; - flex-direction: column; - gap: var(--s3); - margin: 0; -} - -input[type="file"] { - display: block; - border: 1px solid white; - font-weight: bold; - width: calc(100% - 2px); - cursor: pointer; -} -input[type="file"]::file-selector-button { - background-color: white; - color: black; - font-weight: bold; - border: none; - padding: 0.5em; - cursor: pointer; -} - diff --git a/server/static/zettoit.svg b/server/static/zettoit.svg deleted file mode 100644 index f77f51e..0000000 --- a/server/static/zettoit.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - diff --git a/server/templates/background.stpl b/server/templates/background.stpl new file mode 100644 index 0000000..c47b86a --- /dev/null +++ b/server/templates/background.stpl @@ -0,0 +1,23 @@ + diff --git a/server/templates/delete.stpl b/server/templates/delete.stpl index 821b201..30cd744 100644 --- a/server/templates/delete.stpl +++ b/server/templates/delete.stpl @@ -1,12 +1,13 @@ - zettoit bin - - + pfzetto bin + + <% include!("./style.stpl"); %> +

Confirm Deletion

The bin will be deleted. All data will be permanently lost.

@@ -14,6 +15,7 @@
- zettoIT Logo + pfzetto Logo + <% include!("./background.stpl"); %> diff --git a/server/templates/index.stpl b/server/templates/index.stpl index 737f889..f53a9bb 100644 --- a/server/templates/index.stpl +++ b/server/templates/index.stpl @@ -1,48 +1,57 @@ - zettoit bin - - + pfzetto bin + + <% include!("./style.stpl"); %> -
-

zettoIT bin

+ +
+

pfzetto bin

An empty bin was created for you. The first HTTP POST or PUT request can upload data. All following requests can only read the uploaded data.

-

To change the default expiration date, you can use the `?ttl=` parameter.

-

After uploading data, you can access it by accessing <%= bin_url %> with an optional file extension that suits the data that you uploaded.

-

Upload a image

+

To change the default expiration date, you can use the ?ttl= parameter.

+

Set the Content-Type header of the file you upload to make viewing easier. +

After uploading data, you can access it by accessing <%= bin_url %> with an optional file extension that suits the data that you uploaded.

+ +

web upload

+ + + +

upload an image

 $ curl -H "Content-Type: image/png" -T my-image.png <%= bin_url %>`
             
-

Upload a big file

+

create and upload tar

-$ curl -X POST -H "Content-Type: application/gzip" -T my-file.tar.gz <%= bin_url %>
+$ tar -cz . | curl -T - -H "Content-Type: application/gzip" <%= bin_url %>
             
- -

Pipe into curl

-
-$ tar -cz my-files | curl -H "Content-Type: application/gzip" -T - <%= bin_url %>
-            
- -

Encryption

-
-$ tar -cz my-files | gpg -co tmp.tar.gz
-$ curl -H "Content-Type: application/octet-stream" -T tmp.tar.gz <%= bin_url %>
-$ curl <%= bin_url %> | gpg -d - | tar -xzf
-            
- -

Browser upload

-
- - -
- zettoIT Logo + pfzetto Logo + <% include!("./background.stpl"); %> + diff --git a/server/templates/style.stpl b/server/templates/style.stpl new file mode 100644 index 0000000..d3829c7 --- /dev/null +++ b/server/templates/style.stpl @@ -0,0 +1,53 @@ +