Compare commits

..

No commits in common. "aead" and "master" have entirely different histories.
aead ... master

15 changed files with 578 additions and 677 deletions

123
Cargo.lock generated
View file

@ -57,9 +57,9 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.5"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
[[package]]
name = "anstyle-parse"
@ -218,29 +218,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f"
dependencies = [
"axum 0.7.4",
"axum-core 0.4.3",
"bytes",
"futures-util",
"http 1.0.0",
"http-body 1.0.0",
"http-body-util",
"mime",
"pin-project-lite",
"serde",
"tokio",
"tokio-util",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-macros"
version = "0.4.1"
@ -317,7 +294,6 @@ name = "bin"
version = "0.1.0"
dependencies = [
"axum 0.7.4",
"axum-extra",
"axum-oidc",
"bytes",
"chacha20",
@ -330,7 +306,6 @@ dependencies = [
"log",
"markdown",
"pin-project-lite",
"poly1305",
"prometheus-client",
"rand",
"render",
@ -345,7 +320,6 @@ dependencies = [
"tower",
"tower-http",
"tower-sessions",
"zeroize",
]
[[package]]
@ -433,9 +407,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.33"
version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a"
dependencies = [
"android-tzdata",
"iana-time-zone",
@ -1028,7 +1002,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.11",
"indexmap 2.2.1",
"indexmap 2.1.0",
"slab",
"tokio",
"tokio-util",
@ -1047,7 +1021,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 1.0.0",
"indexmap 2.2.1",
"indexmap 2.1.0",
"slab",
"tokio",
"tokio-util",
@ -1318,9 +1292,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.2.1"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
@ -1678,12 +1652,6 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "open"
version = "5.0.1"
@ -1822,18 +1790,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
version = "1.1.4"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0"
checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.4"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
@ -1885,17 +1853,6 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c"
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -2046,9 +2003,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.5"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a"
dependencies = [
"aho-corasick",
"memchr",
@ -2323,9 +2280,9 @@ checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
[[package]]
name = "serde"
version = "1.0.196"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
dependencies = [
"serde_derive",
]
@ -2342,9 +2299,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.196"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
dependencies = [
"proc-macro2",
"quote",
@ -2353,9 +2310,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.113"
version = "1.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
dependencies = [
"itoa",
"ryu",
@ -2404,15 +2361,15 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.5.1"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5c9fdb6b00a489875b22efd4b78fe2b363b72265cc5f6eb2e2b9ee270e6140c"
checksum = "f58c3a1b3e418f61c25b2aeb43fc6c95eaa252b8cecdda67f401943e9e08d33f"
dependencies = [
"base64 0.21.7",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.2.1",
"indexmap 2.1.0",
"serde",
"serde_json",
"serde_with_macros",
@ -2421,9 +2378,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.5.1"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbff351eb4b33600a2e138dfa0b10b65a238ea8ff8fb2387c422c5022a3e8298"
checksum = "d2068b437a31fc68f25dd7edc296b078f04b45145c199d8eed9866e45f1ff274"
dependencies = [
"darling",
"proc-macro2",
@ -2754,7 +2711,7 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [
"indexmap 2.2.1",
"indexmap 2.1.0",
"serde",
"serde_spanned",
"toml_datetime",
@ -2950,16 +2907,6 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.9.0"
@ -3284,9 +3231,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
version = "0.5.35"
version = "0.5.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1931d78a9c73861da0134f453bb1f790ce49b2e30eba8410b4b79bac72b46a2d"
checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16"
dependencies = [
"memchr",
]
@ -3306,17 +3253,3 @@ name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]

View file

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1706473964,
"narHash": "sha256-Fq6xleee/TsX6NbtoRuI96bBuDHMU57PrcK9z1QEKbk=",
"lastModified": 1705974079,
"narHash": "sha256-HyC3C2esW57j6bG0MKwX4kQi25ltslRnr6z2uvpadJo=",
"owner": "ipetkov",
"repo": "crane",
"rev": "c798790eabec3e3da48190ae3698ac227aab770c",
"rev": "0b4e511fe6e346381e31d355e03de52aa43e8cb2",
"type": "github"
},
"original": {
@ -40,11 +40,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1706371002,
"narHash": "sha256-dwuorKimqSYgyu8Cw6ncKhyQjUDOyuXoxDTVmAXq88s=",
"lastModified": 1705856552,
"narHash": "sha256-JXfnuEf5Yd6bhMs/uvM67/joxYKoysyE3M2k6T3eWbg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c002c6aa977ad22c60398daaa9be52f2203d0006",
"rev": "612f97239e2cc474c13c9dafa0df378058c5ad8d",
"type": "github"
},
"original": {
@ -72,11 +72,11 @@
]
},
"locked": {
"lastModified": 1706494265,
"narHash": "sha256-4ilEUJEwNaY9r/8BpL3VmZiaGber0j09lvvx0e/bosA=",
"lastModified": 1705976279,
"narHash": "sha256-Zx97bJ3+O8IP70uJPD//rRsr8bcxICISMTZUT/L9eFk=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "246ba7102553851af60e0382f558f6bc5f63fa13",
"rev": "f889dc31ef97835834bdc3662394ebdb3c96b974",
"type": "github"
},
"original": {

View file

@ -54,6 +54,11 @@
bin = craneLib.buildPackage (commonArgs // {
inherit cargoArtifacts;
pname = "bin";
installPhaseCommand = ''
mkdir -p $out/bin
cp target/release/bin $out/bin/bin
cp -r server/static $out/static
'';
});
binctl = craneLib.buildPackage (commonArgs // {
inherit cargoArtifacts;

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", "cors"], default-features=false }
tower-http = { version="0.5", features=["fs"], default-features=false }
prometheus-client = "0.22.0"
chacha20 = "0.9"
@ -32,9 +32,6 @@ sha3 = "0.10"
hex = "0.4"
bytes = "1.5"
pin-project-lite = "0.2"
poly1305 = "0.8.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"] }
jsonwebtoken = "9.2.0"

View file

@ -1,322 +0,0 @@
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

@ -21,9 +21,6 @@ pub enum Error {
#[error("file exists")]
DataFileExists,
#[error("bin verification failed")]
BinVerificationFailed,
#[error("hex error: {:?}", 0)]
Hex(#[from] hex::FromHexError),
@ -62,11 +59,6 @@ impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
debug!("{:?}", &self);
match self {
Self::BinVerificationFailed => (
StatusCode::INTERNAL_SERVER_ERROR,
"bin verification failed\n",
)
.into_response(),
Self::PhraseInvalid => (StatusCode::BAD_REQUEST, "url is not valid\n").into_response(),
Self::BinNotFound => (StatusCode::NOT_FOUND, "bin does not exist\n").into_response(),
Self::DataFileExists => {

View file

@ -1,12 +1,10 @@
#![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::{deserialize_option_duration, parse_std};
use jwt::JwtApplication;
use poly1305::Tag;
use prometheus_client::{
encoding::EncodeLabelSet,
metrics::{counter::Counter, family::Family, gauge::Gauge},
@ -16,54 +14,58 @@ use sailfish::TemplateOnce;
use std::{
borrow::Borrow,
env,
io::SeekFrom,
str::FromStr,
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tower_http::{cors::CorsLayer, services::ServeDir};
use tower_http::services::ServeDir;
use tower::ServiceBuilder;
use tower_sessions::{cookie::SameSite, MemoryStore, SessionManagerLayer};
use axum::{
async_trait,
body::Body,
error_handling::HandleErrorLayer,
extract::{FromRef, Path, Query, State},
extract::{FromRef, FromRequest, Multipart, Path, Query, State},
http::{
header::{self, CONTENT_TYPE},
HeaderMap, HeaderValue, Method, StatusCode, Uri,
HeaderMap, HeaderValue, Request, StatusCode, Uri,
},
response::{Html, IntoResponse, Redirect},
routing::{delete, get},
Router,
response::{Html, IntoResponse, Redirect, Response},
routing::{delete, get, post},
BoxError, Router,
};
use chacha20::{cipher::KeyIvInit, ChaCha20};
use chacha20::{
cipher::{KeyIvInit, StreamCipher},
ChaCha20,
};
use futures_util::StreamExt;
use garbage_collector::GarbageCollector;
use log::{debug, warn};
use serde::Deserialize;
use sha3::{Digest, Sha3_256};
use tokio::{
fs::{self, File},
io::{AsyncSeekExt, AsyncWriteExt, BufReader, BufWriter},
io::{AsyncWriteExt, BufReader, BufWriter},
net::TcpListener,
};
use util::{IdSalt, KeySalt};
use crate::{
aeadstream::AeadStreamWriter,
error::Error,
jwt::Claims,
metadata::Metadata,
util::{Id, Key, Nonce, Phrase},
web_util::DecryptingStream,
};
mod aeadstream;
mod error;
mod garbage_collector;
mod jwt;
mod metadata;
mod util;
mod web_util;
/// length of the "phrase" that is used to access the bin (https://example.com/<phrase>)
const PHRASE_LENGTH: usize = 16;
@ -154,14 +156,6 @@ 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();
@ -217,8 +211,7 @@ async fn main() {
.nest_service("/static", ServeDir::new("static"))
.with_state(state)
.layer(oidc_auth_service)
.layer(session_service)
.layer(cors);
.layer(session_service);
let listener = TcpListener::bind("[::]:8080")
.await
@ -249,7 +242,7 @@ async fn get_index(
let metadata = Metadata {
subject,
nonce: nonce.to_hex(),
tag: None,
etag: None,
size: None,
content_type: None,
expires_at,
@ -322,7 +315,7 @@ async fn upload_bin(
Query(params): Query<PostQuery>,
State(app_state): State<AppState>,
headers: HeaderMap,
body: Body,
data: MultipartOrStream,
) -> HandlerResult<impl IntoResponse> {
let phrase = Phrase::from_str(&phrase)?;
let id = Id::from_phrase(&phrase, &app_state.id_salt);
@ -336,38 +329,75 @@ async fn upload_bin(
if !path.exists() {
let key = Key::from_phrase(&phrase, &app_state.key_salt);
let nonce = Nonce::from_hex(&metadata.nonce)?;
let mut cipher = ChaCha20::new(key.borrow(), nonce.borrow());
let file = File::create(&path).await?;
let writer = BufWriter::new(file);
let mut writer = BufWriter::new(file);
let ttl = params
let mut etag_hasher = Sha3_256::new();
let mut size: u64 = 0;
let mut ttl = params
.ttl
.map(|x| duration_str::parse(x).map_err(|_| Error::InvalidTtl))
.map(|x| duration_str::parse(&x).map_err(|_| Error::InvalidTtl))
.transpose()?
.unwrap_or(Duration::from_secs(24 * 3600));
let cipher = ChaCha20::new(key.borrow(), nonce.borrow());
let mut writer = AeadStreamWriter::new(writer, cipher, &[]);
let mut stream = body.into_data_stream();
while let Some(chunk) = stream.next().await {
writer.write_all(chunk.unwrap_or_default().as_ref()).await?;
match data {
MultipartOrStream::Stream(stream) => {
let mut stream = stream.into_data_stream();
while let Some(chunk) = stream.next().await {
let mut buf = chunk.unwrap_or_default().to_vec();
etag_hasher.update(&buf);
size += buf.len() as u64;
cipher.apply_keystream(&mut buf);
writer.write_all(&buf).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() {
let mut buf = chunk.to_vec();
etag_hasher.update(&buf);
size += buf.len() as u64;
cipher.apply_keystream(&mut buf);
writer.write_all(&buf).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");
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)?
@ -377,8 +407,8 @@ async fn upload_bin(
metadata.expires_at = expires_at;
}
metadata.tag = Some(hex::encode(tag));
metadata.size = Some(writer.size() as u64);
metadata.etag = Some(hex::encode(etag_hasher.finalize()));
metadata.size = Some(size);
metadata.to_file(&metadata_path).await?;
@ -429,33 +459,21 @@ async fn get_item(
} else {
//TODO(pfz4): Maybe add link handling
let file = File::open(&path).await?;
let mut reader = BufReader::new(file);
let reader = BufReader::new(file);
let tag = *Tag::from_slice(
&hex::decode(metadata.tag.as_deref().unwrap_or("")).unwrap_or_default(),
);
reader.seek(SeekFrom::Start(0)).await?;
let cipher = ChaCha20::new(key.borrow(), nonce.borrow());
let body = AsyncReadBody::new(AeadStreamReader::new(
let body = Body::from_stream(DecryptingStream::new(
reader,
cipher,
&[],
tag,
metadata.size.map(|x| x as usize),
id.clone(),
&metadata,
&key,
&nonce,
));
let mut headers = HeaderMap::new();
// 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(),
//);
headers.insert(
header::CONTENT_LENGTH,
metadata.size.unwrap_or_default().into(),
);
if let Ok(subject) = metadata.subject.parse() {
headers.insert("CreatedBy", subject);
}
@ -463,6 +481,15 @@ async fn get_item(
if let Some(content_type) = metadata.content_type.and_then(|x| x.parse().ok()) {
headers.insert(header::CONTENT_TYPE, content_type);
}
if let Some(etag) = metadata.etag.clone().and_then(|x| x.parse().ok()) {
headers.insert(header::ETAG, etag);
}
if let Some(digest) = metadata
.etag
.and_then(|x| format!("sha3-256={x}").parse().ok())
{
headers.insert("Digest", digest);
}
app_state
.metrics
@ -488,6 +515,41 @@ 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> {

View file

@ -1,6 +1,5 @@
use std::{io::ErrorKind, time::Instant};
use log::debug;
use serde::{Deserialize, Serialize};
use tokio::{fs::File, io::AsyncWriteExt};
@ -10,7 +9,7 @@ use crate::Error;
pub struct Metadata {
pub subject: String,
pub nonce: String,
pub tag: Option<String>,
pub etag: Option<String>,
pub size: Option<u64>,
pub content_type: Option<String>,
pub expires_at: u64, // seconds since UNIX_EPOCH
@ -20,10 +19,7 @@ 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 => {
debug!("{} {:?}", path, err);
Err(Error::BinNotFound)
}
Err(err) if err.kind() == ErrorKind::NotFound => Err(Error::BinNotFound),
Err(x) => Err(x.into()),
}?;
Ok(toml::from_str::<Self>(&metadata)?)

133
server/src/web_util.rs Normal file
View file

@ -0,0 +1,133 @@
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,
// hasher to verify file integrity
hasher: Sha3_256,
// 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,
hasher: Sha3_256::new(),
target_hash: metadata.etag.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
this.hasher.update(&chunk);
// 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,
}

172
server/static/zettoit.css Normal file
View file

@ -0,0 +1,172 @@
/*
* 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;
}

60
server/static/zettoit.svg Normal file
View file

@ -0,0 +1,60 @@
<?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>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1,23 +0,0 @@
<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,13 +1,12 @@
<html>
<head>
<title>pfzetto bin</title>
<link rel="icon" type="image/svg" href="https://pfzetto.de/pfzetto.svg"/>
<title>zettoit bin</title>
<link rel="icon" type="image/svg" href="/static/zettoit.svg"/>
<link rel="stylesheet" href="/static/zettoit.css"/>
<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>
@ -15,7 +14,6 @@
<button type="submit">Delete</button>
</form>
</main>
<a class="watermark" href="https://pfzetto.de"><img src="https://pfzetto.de/pfzetto.svg" alt="pfzetto Logo"></a>
<% include!("./background.stpl"); %>
<a class="watermark" href="https://git2.zettoit.eu/zettoit"><img src="/static/zettoit.svg" alt="zettoIT Logo"></a>
</body>
</html>

View file

@ -1,57 +1,48 @@
<html>
<head>
<title>pfzetto bin</title>
<link rel="icon" type="image/svg" href="https://pfzetto.de/pfzetto.svg"/>
<title>zettoit bin</title>
<link rel="icon" type="image/svg" href="/static/zettoit.svg"/>
<link rel="stylesheet" href="/static/zettoit.css"/>
<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 id="main">
<h1>pfzetto bin</h1>
<main>
<h1>zettoIT 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 <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>
<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>
<pre>
$ curl -H "Content-Type: image/png" -T my-image.png <%= bin_url %>`
</pre>
<h1>create and upload tar</h1>
<h1>Upload a big file</h1>
<pre>
$ tar -cz . | curl -T - -H "Content-Type: application/gzip" <%= bin_url %>
$ curl -X POST -H "Content-Type: application/gzip" -T my-file.tar.gz <%= 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://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>
<a class="watermark" href="https://git2.zettoit.eu/zettoit"><img src="/static/zettoit.svg" alt="zettoIT Logo"></a>
</body>
</html>

View file

@ -1,93 +0,0 @@
<style>
html {
font-family: monospace;
font-site: 12px;
text-align: justify;
}
body {
background-color: black;
color: white;
margin: 0;
height: 100%;
width: 100%;
max-width: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: -2;
}
header {
display: flex;
align-items: center;
flex-direction: column;
}
main {
max-width: calc(100% - 8px);
padding: 4px;
}
pre {
max-width: 100%;
overflow-x: scroll;
}
a {
color: white;
}
p, input, button {
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%;
}
button, input[type="file"]::file-selector-button {
cursor: pointer;
text-decoration-line: underline;
background-color: transparent;
color: white;
padding: 0;
border: none;
}
button:active, input[type="file"]::file-selector-button:active {
background-color: transparent;
color: white;
padding: 0;
}
input[type="file"] {
display: block;
border: none;
cursor: pointer;
padding: 0;
}
</style>