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"
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"] }

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)]
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::<HeaderValue>()
.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<PostQuery>,
State(app_state): State<AppState>,
headers: HeaderMap,
data: MultipartOrStream,
body: Body,
) -> HandlerResult<impl IntoResponse> {
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<AppState>) -> HandlerResult<impl IntoRe
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)]
#[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<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 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<Self, Error> {
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::<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>
<head>
<title>zettoit bin</title>
<link rel="icon" type="image/svg" href="/static/zettoit.svg"/>
<link rel="stylesheet" href="/static/zettoit.css"/>
<title>pfzetto bin</title>
<link rel="icon" type="image/svg" href="https://pfzetto.de/pfzetto.svg"/>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<% include!("./style.stpl"); %>
</head>
<body>
<canvas id="matrix"></canvas>
<main>
<h1>Confirm Deletion</h1>
<p>The bin will be deleted. All data will be permanently lost.</p>
@ -14,6 +15,7 @@
<button type="submit">Delete</button>
</form>
</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>
</html>

View File

@ -1,48 +1,57 @@
<html>
<head>
<title>zettoit bin</title>
<link rel="icon" type="image/svg" href="/static/zettoit.svg"/>
<link rel="stylesheet" href="/static/zettoit.css"/>
<title>pfzetto bin</title>
<link rel="icon" type="image/svg" href="https://pfzetto.de/pfzetto.svg"/>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<% include!("./style.stpl"); %>
</head>
<body>
<main>
<h1>zettoIT bin</h1>
<canvas id="matrix"></canvas>
<main id="main">
<h1>pfzetto bin</h1>
<p>
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.
</p>
<p>To change the default expiration date, you can use the `?ttl=<seconds_to_live>` 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>
<h1>Upload a image</h1>
<p>To change the default expiration date, you can use the <code>?ttl=<seconds_to_live></code> parameter.</p>
<p>Set the <code>Content-Type</code> header of the file you upload to make viewing easier.
<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>
$ curl -H "Content-Type: image/png" -T my-image.png <%= bin_url %>`
</pre>
<h1>Upload a big file</h1>
<h1>create and upload tar</h1>
<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>
<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>
<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>
</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>