aead umbau

This commit is contained in:
Paul Zinselmeyer 2024-01-25 22:36:52 +01:00
parent 5fb711bd85
commit 793463b826
Signed by: pfzetto
GPG key ID: B471A1AF06C895FD
11 changed files with 507 additions and 806 deletions

View file

@ -24,7 +24,7 @@ tower = "0.4.13"
log = "0.4" log = "0.4"
env_logger = "0.10" env_logger = "0.10"
sailfish = "0.8.3" 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" prometheus-client = "0.22.0"
chacha20 = "0.9" chacha20 = "0.9"
@ -33,7 +33,7 @@ hex = "0.4"
bytes = "1.5" bytes = "1.5"
pin-project-lite = "0.2" pin-project-lite = "0.2"
poly1305 = "0.8.0" 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"]} axum-extra = { version="0.9.2", features=["async-read-body"]}
reqwest = { version="0.11", default_features=false, features=["rustls-tls", "json"] } reqwest = { version="0.11", default_features=false, features=["rustls-tls", "json"] }

322
server/src/aeadstream.rs Normal file
View file

@ -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<T, C>
where
T: AsyncWrite,
C: StreamCipher,
C: StreamCipherSeek
{
#[pin]
inner: T,
buf: Vec<u8>,
to_mac: Vec<u8>,
cipher: C,
mac: Poly1305,
tag: Option<Tag>,
encrypted: usize,
authenticated_data_len: usize,
}
}
impl<T, C> AeadStreamWriter<T, C>
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<T, C> AsyncWrite for AeadStreamWriter<T, C>
where
T: AsyncWrite,
C: StreamCipher + StreamCipherSeek,
{
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
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::<Vec<_>>();
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<Result<(), std::io::Error>> {
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<Result<(), std::io::Error>> {
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<T, C>
where
T: AsyncRead,
C: StreamCipher,
C: StreamCipherSeek,
{
#[pin]
inner: T,
buf: Vec<u8>,
cipher: C,
mac: AeadStreamReaderMac<Poly1305>,
decrypted_data_len: usize,
authenticated_data_len: usize,
expected_data_len: Option<usize>,
tag: Tag,
}
}
impl<T, C> AeadStreamReader<T, C>
where
T: AsyncRead,
C: StreamCipher + StreamCipherSeek,
{
pub fn new(
inner: T,
mut cipher: C,
authenticated_data: &[u8],
tag: Tag,
expected_data_len: Option<usize>,
) -> 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<M> {
Running(M),
Finished,
Failed,
}
impl<T, C> AsyncRead for AeadStreamReader<T, C>
where
T: AsyncRead,
C: StreamCipher + StreamCipherSeek,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
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]);
}

View file

@ -1,13 +1,12 @@
#![deny(clippy::unwrap_used)] #![deny(clippy::unwrap_used)]
use aeadstream::AeadStreamReader;
use axum_extra::body::AsyncReadBody; use axum_extra::body::AsyncReadBody;
use axum_oidc::{ use axum_oidc::{
error::{ExtractorError, MiddlewareError}, error::{ExtractorError, MiddlewareError},
EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer,
}; };
use duration_str::parse_std;
use jwt::JwtApplication; use jwt::JwtApplication;
use pin_project_lite::pin_project; use poly1305::Tag;
use poly1305::{universal_hash::UniversalHash, Poly1305, Tag};
use prometheus_client::{ use prometheus_client::{
encoding::EncodeLabelSet, encoding::EncodeLabelSet,
metrics::{counter::Counter, family::Family, gauge::Gauge}, metrics::{counter::Counter, family::Family, gauge::Gauge},
@ -17,57 +16,49 @@ use sailfish::TemplateOnce;
use std::{ use std::{
borrow::Borrow, borrow::Borrow,
env, env,
io::{ErrorKind, SeekFrom}, io::SeekFrom,
pin::Pin,
str::FromStr, str::FromStr,
sync::Arc, sync::Arc,
task::{ready, Context, Poll},
time::{Duration, SystemTime, UNIX_EPOCH}, time::{Duration, SystemTime, UNIX_EPOCH},
}; };
use tower_http::services::ServeDir; use tower_http::{cors::CorsLayer, services::ServeDir};
use tower::ServiceBuilder; use tower::ServiceBuilder;
use tower_sessions::{cookie::SameSite, MemoryStore, SessionManagerLayer}; use tower_sessions::{cookie::SameSite, MemoryStore, SessionManagerLayer};
use axum::{ use axum::{
async_trait,
body::Body, body::Body,
error_handling::HandleErrorLayer, error_handling::HandleErrorLayer,
extract::{FromRef, FromRequest, Multipart, Path, Query, State}, extract::{FromRef, Path, Query, State},
http::{ http::{
header::{self, CONTENT_TYPE}, 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}, routing::{delete, get},
Router, Router,
}; };
use chacha20::{ use chacha20::{cipher::KeyIvInit, ChaCha20};
cipher::{generic_array::GenericArray, KeyInit, KeyIvInit, StreamCipher, StreamCipherSeek},
ChaCha20,
};
use futures_util::StreamExt; use futures_util::StreamExt;
use garbage_collector::GarbageCollector; use garbage_collector::GarbageCollector;
use log::{debug, info, warn}; use log::{debug, warn};
use serde::Deserialize; use serde::Deserialize;
use tokio::{ use tokio::{
fs::{self, File}, fs::{self, File},
io::{ io::{AsyncSeekExt, AsyncWriteExt, BufReader, BufWriter},
AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter,
ReadBuf,
},
net::TcpListener, net::TcpListener,
}; };
use util::{IdSalt, KeySalt}; use util::{IdSalt, KeySalt};
use zeroize::Zeroize;
use crate::{ use crate::{
aeadstream::AeadStreamWriter,
error::Error, error::Error,
jwt::Claims, jwt::Claims,
metadata::Metadata, metadata::Metadata,
util::{Id, Key, Nonce, Phrase}, util::{Id, Key, Nonce, Phrase},
}; };
mod aeadstream;
mod error; mod error;
mod garbage_collector; mod garbage_collector;
mod jwt; mod jwt;
@ -163,6 +154,14 @@ async fn main() {
.await .await
.expect("Jwt Authentication Client"); .expect("Jwt Authentication Client");
let cors = CorsLayer::new()
.allow_methods([Method::GET, Method::POST, Method::PUT])
.allow_origin(
application_base
.parse::<HeaderValue>()
.expect("valid APPLICATION_BASE"),
);
let data_path = env::var("DATA_PATH").expect("DATA_PATH env var"); let data_path = env::var("DATA_PATH").expect("DATA_PATH env var");
let mut registry = Registry::default(); let mut registry = Registry::default();
@ -218,7 +217,8 @@ async fn main() {
.nest_service("/static", ServeDir::new("static")) .nest_service("/static", ServeDir::new("static"))
.with_state(state) .with_state(state)
.layer(oidc_auth_service) .layer(oidc_auth_service)
.layer(session_service); .layer(session_service)
.layer(cors);
let listener = TcpListener::bind("[::]:8080") let listener = TcpListener::bind("[::]:8080")
.await .await
@ -322,7 +322,7 @@ async fn upload_bin(
Query(params): Query<PostQuery>, Query(params): Query<PostQuery>,
State(app_state): State<AppState>, State(app_state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
data: MultipartOrStream, body: Body,
) -> HandlerResult<impl IntoResponse> { ) -> HandlerResult<impl IntoResponse> {
let phrase = Phrase::from_str(&phrase)?; let phrase = Phrase::from_str(&phrase)?;
let id = Id::from_phrase(&phrase, &app_state.id_salt); let id = Id::from_phrase(&phrase, &app_state.id_salt);
@ -340,21 +340,24 @@ async fn upload_bin(
let file = File::create(&path).await?; let file = File::create(&path).await?;
let writer = BufWriter::new(file); let writer = BufWriter::new(file);
let mut ttl = params let ttl = params
.ttl .ttl
.map(|x| duration_str::parse(x).map_err(|_| Error::InvalidTtl)) .map(|x| duration_str::parse(x).map_err(|_| Error::InvalidTtl))
.transpose()? .transpose()?
.unwrap_or(Duration::from_secs(24 * 3600)); .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 { let mut stream = body.into_data_stream();
MultipartOrStream::Stream(stream) => {
let mut stream = stream.into_data_stream();
while let Some(chunk) = stream.next().await { while let Some(chunk) = stream.next().await {
writer.write_all(chunk.unwrap_or_default().as_ref()).await?; writer.write_all(chunk.unwrap_or_default().as_ref()).await?;
} }
writer.flush().await?;
writer.shutdown().await?; writer.shutdown().await?;
let tag = writer.tag().expect("valid tag");
metadata.content_type = match headers.get(CONTENT_TYPE) { metadata.content_type = match headers.get(CONTENT_TYPE) {
Some(content_type) => Some( Some(content_type) => Some(
content_type content_type
@ -365,40 +368,6 @@ async fn upload_bin(
), ),
None => Some("application/octet-stream".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;
}
}
}
}
}
writer.flush().await?;
writer.shutdown().await?;
let tag = writer.tag().expect("valid tag");
if let Some(expires_at) = SystemTime::now() if let Some(expires_at) = SystemTime::now()
.duration_since(UNIX_EPOCH)? .duration_since(UNIX_EPOCH)?
@ -466,41 +435,27 @@ async fn get_item(
&hex::decode(metadata.tag.as_deref().unwrap_or("")).unwrap_or_default(), &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?; 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(); let mut headers = HeaderMap::new();
headers.insert( // If send, the client directly disconnects after the CONTENT_LENGTH bytes are received.
header::CONTENT_LENGTH, // Because of that the MAC validation is skipped and the user may receive invalid data.
metadata.size.unwrap_or_default().into(), // 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() { if let Ok(subject) = metadata.subject.parse() {
headers.insert("CreatedBy", subject); headers.insert("CreatedBy", subject);
} }
@ -533,41 +488,6 @@ async fn metrics(State(app_state): State<AppState>) -> HandlerResult<impl IntoRe
Ok((headers, buffer)) Ok((headers, buffer))
} }
enum MultipartOrStream {
Multipart(Multipart),
Stream(Body),
}
#[async_trait]
impl<S: Sync + Send> FromRequest<S> for MultipartOrStream {
type Rejection = Response;
async fn from_request(req: Request<Body>, state: &S) -> Result<Self, Self::Rejection> {
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)] #[derive(TemplateOnce)]
#[template(path = "index.stpl")] #[template(path = "index.stpl")]
pub struct IndexTemplate<'a> { pub struct IndexTemplate<'a> {
@ -577,274 +497,3 @@ pub struct IndexTemplate<'a> {
#[derive(TemplateOnce)] #[derive(TemplateOnce)]
#[template(path = "delete.stpl")] #[template(path = "delete.stpl")]
pub struct DeleteTemplate; pub struct DeleteTemplate;
pin_project! {
struct EncWriter<T: AsyncWrite> {
#[pin]
inner: T,
buf: Vec<u8>,
to_mac: Vec<u8>,
cipher: ChaCha20,
mac: Poly1305,
tag: Option<Tag>,
encrypted: usize,
authenticated_data_len: usize,
}
}
impl<T: AsyncWrite> EncWriter<T> {
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<T: AsyncWrite> AsyncWrite for EncWriter<T> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
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::<Vec<_>>();
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<Result<(), std::io::Error>> {
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<Result<(), std::io::Error>> {
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<T: AsyncRead> {
#[pin]
inner: T,
buf: Vec<u8>,
cipher: ChaCha20,
mac: Poly1305,
decrypted: usize,
authenticated_data_len: usize,
tag: Tag,
state: DecReaderState,
}
}
impl<T: AsyncRead> DecReader<T> {
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<T: AsyncRead> AsyncRead for DecReader<T> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
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]);
}

View file

@ -1,5 +1,6 @@
use std::{io::ErrorKind, time::Instant}; use std::{io::ErrorKind, time::Instant};
use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::{fs::File, io::AsyncWriteExt}; use tokio::{fs::File, io::AsyncWriteExt};
@ -19,7 +20,10 @@ impl Metadata {
pub async fn from_file(path: &str) -> Result<Self, Error> { pub async fn from_file(path: &str) -> Result<Self, Error> {
let metadata = match tokio::fs::read_to_string(path).await { let metadata = match tokio::fs::read_to_string(path).await {
Ok(x) => Ok(x), 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()), Err(x) => Err(x.into()),
}?; }?;
Ok(toml::from_str::<Self>(&metadata)?) Ok(toml::from_str::<Self>(&metadata)?)

View file

@ -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<R> {
#[pin]
reader: Option<R>,
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<R: AsyncRead> DecryptingStream<R> {
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<R: AsyncRead> Stream for DecryptingStream<R> {
type Item = std::io::Result<Bytes>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
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<R: AsyncRead> DecryptingStream<R> {
/// 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,
}

View file

@ -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;
}

View file

@ -1,60 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="34mm"
height="15mm"
viewBox="0 0 34 15"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="zettoIT_new.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="3.659624"
inkscape:cx="94.95511"
inkscape:cy="25.002569"
inkscape:window-width="1916"
inkscape:window-height="2110"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid4316"
empspacing="4"
originx="0"
originy="0" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
d="M 1.0583328,3.175 H 17.991667 l -1e-6,-2.1166667 h 2.116667 V 3.175 h 2.116666 l -1e-6,-2.1166667 h 2.116667 V 3.175 h 2.116667 6.35 v 10.583333 h -6.35 V 6.35 l 2.116666,-10e-8 10e-7,5.2916661 h 2.116666 V 5.2916666 h -6.35 v 8.4666664 h -2.116667 l 1e-6,-8.4666664 h -2.116666 v 8.4666664 h -2.116667 l 1e-6,-8.4666664 -6.350001,-2e-7 v 2.1166667 l 3.175,2e-7 v 2.116667 c -2.245125,-3e-6 -1.763889,0 -3.175,0 v 2.1166657 h 4.233333 v 2.116667 H 9.5249988 V 5.2916664 l -8.466666,2e-7 z"
id="path13059"
sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 1.0583328,11.641666 v 2.116667 h 6.35 V 11.641666 H 3.1749995 L 5.2916662,6.35 l -2.1166667,-10e-8 c 0,0 -2.1166667,5.2916661 -2.1166667,5.2916661 z"
id="path13061"
sodipodi:nodetypes="cccccccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,23 @@
<script>
window.addEventListener("resize", paint);
paint();
function paint() {
var canvas = document.getElementById("matrix");
var ctx = canvas.getContext("2d");
ctx.reset();
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var characters = "01".split("");
var font_size = 10;
var font_spacing = 5;
var colums = canvas.width/font_size;
var rows = canvas.height/(font_size+font_spacing);
ctx.fillStyle = "rgba(255,255,255,0.1)";
for(var x = 0; x < colums; x++) {
for(var y = 0; y < rows; y++) {
ctx.fillText(characters[Math.round(Math.random())], x*font_size+5,y*(font_size+font_spacing));
}
}
}
</script>

View file

@ -1,12 +1,13 @@
<html> <html>
<head> <head>
<title>zettoit bin</title> <title>pfzetto bin</title>
<link rel="icon" type="image/svg" href="/static/zettoit.svg"/> <link rel="icon" type="image/svg" href="https://pfzetto.de/pfzetto.svg"/>
<link rel="stylesheet" href="/static/zettoit.css"/>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<% include!("./style.stpl"); %>
</head> </head>
<body> <body>
<canvas id="matrix"></canvas>
<main> <main>
<h1>Confirm Deletion</h1> <h1>Confirm Deletion</h1>
<p>The bin will be deleted. All data will be permanently lost.</p> <p>The bin will be deleted. All data will be permanently lost.</p>
@ -14,6 +15,7 @@
<button type="submit">Delete</button> <button type="submit">Delete</button>
</form> </form>
</main> </main>
<a class="watermark" href="https://git2.zettoit.eu/zettoit"><img src="/static/zettoit.svg" alt="zettoIT Logo"></a> <a class="watermark" href="https://pfzetto.de"><img src="https://pfzetto.de/pfzetto.svg" alt="pfzetto Logo"></a>
<% include!("./background.stpl"); %>
</body> </body>
</html> </html>

View file

@ -1,48 +1,57 @@
<html> <html>
<head> <head>
<title>zettoit bin</title> <title>pfzetto bin</title>
<link rel="icon" type="image/svg" href="/static/zettoit.svg"/> <link rel="icon" type="image/svg" href="https://pfzetto.de/pfzetto.svg"/>
<link rel="stylesheet" href="/static/zettoit.css"/>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<% include!("./style.stpl"); %>
</head> </head>
<body> <body>
<main> <canvas id="matrix"></canvas>
<h1>zettoIT bin</h1> <main id="main">
<h1>pfzetto bin</h1>
<p> <p>
An empty bin was created for you. The first HTTP POST or PUT request can upload data. 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. All following requests can only read the uploaded data.
</p> </p>
<p>To change the default expiration date, you can use the `?ttl=<seconds_to_live>` parameter.</p> <p>To change the default expiration date, you can use the <code>?ttl=<seconds_to_live></code> parameter.</p>
<p>After uploading data, you can access it by accessing <%= bin_url %> with an optional file extension that suits the data that you uploaded.</p> <p>Set the <code>Content-Type</code> header of the file you upload to make viewing easier.
<h1>Upload a image</h1> <p>After uploading data, you can access it by accessing <a href="<%= bin_url %>"><%= bin_url %></a> with an optional file extension that suits the data that you uploaded.</p>
<h1>web upload</h1>
<label class="file">
<input type="file" id="file" aria-label="File browser">
<span class="file-custom"></span>
</label>
<button type="button" onclick="upload()">Upload</button>
<h1>upload an image</h1>
<pre> <pre>
$ curl -H "Content-Type: image/png" -T my-image.png <%= bin_url %>` $ curl -H "Content-Type: image/png" -T my-image.png <%= bin_url %>`
</pre> </pre>
<h1>Upload a big file</h1> <h1>create and upload tar</h1>
<pre> <pre>
$ 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 %>
</pre> </pre>
<h1>Pipe into curl</h1>
<pre>
$ tar -cz my-files | curl -H "Content-Type: application/gzip" -T - <%= bin_url %>
</pre>
<h1>Encryption</h1>
<pre>
$ 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
</pre>
<h1>Browser upload</h1>
<form method="post" enctype="multipart/form-data">
<input type="file" name="file"></input>
<button type="submit">Upload</button>
</form>
</main> </main>
<a class="watermark" href="https://git2.zettoit.eu/zettoit"><img src="/static/zettoit.svg" alt="zettoIT Logo"></a> <a class="watermark" href="https://pfzetto.de"><img src="https://pfzetto.de/pfzetto.svg" alt="pfzetto Logo"></a>
<% include!("./background.stpl"); %>
<script>
function upload() {
const input = document.getElementById("file");
const body = document.getElementById("main");
body.innerHtml = "uploading file. please keep this page open.";
fetch(window.location.pathname, {
method: 'POST',
headers: {
"Content-Type": input.files[0].type
},
body: input.files[0]
})
.then((response) => response.text())
.then((response) => {body.innerText = response;});
}
</script>
</body> </body>
</html> </html>

View file

@ -0,0 +1,53 @@
<style>
body, html {
margin: 0;
background-color: black;
height: 100%;
width: 100%;
color: white;
font-family: monospace;
display: flex;
justify-content: center;
align-items: center;
z-index: -2;
}
header {
display: flex;
align-items: center;
flex-direction: column;
}
a {
color: white;
}
p {
margin-top: 2px;
margin-bottom: 2px;
}
h1 {
margin-bottom: 2px;
}
.watermark {
position: fixed;
bottom: 2px;
right: 2px;
}
.watermark img {
height: 32px;
}
#matrix {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: -1;
width: 100%;
height: 100%;
}
</style>