Compare commits

..

4 commits
master ... aead

Author SHA1 Message Date
998fefb5d8
flake update 2024-01-29 21:34:40 +01:00
a81a29df55
style improvement 2024-01-29 21:29:09 +01:00
793463b826
aead umbau 2024-01-25 22:36:52 +01:00
5fb711bd85
aead 2024-01-25 17:03:38 +01:00
15 changed files with 677 additions and 578 deletions

123
Cargo.lock generated
View file

@ -57,9 +57,9 @@ dependencies = [
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.4" version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220"
[[package]] [[package]]
name = "anstyle-parse" name = "anstyle-parse"
@ -218,6 +218,29 @@ dependencies = [
"tracing", "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]] [[package]]
name = "axum-macros" name = "axum-macros"
version = "0.4.1" version = "0.4.1"
@ -294,6 +317,7 @@ name = "bin"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"axum 0.7.4", "axum 0.7.4",
"axum-extra",
"axum-oidc", "axum-oidc",
"bytes", "bytes",
"chacha20", "chacha20",
@ -306,6 +330,7 @@ dependencies = [
"log", "log",
"markdown", "markdown",
"pin-project-lite", "pin-project-lite",
"poly1305",
"prometheus-client", "prometheus-client",
"rand", "rand",
"render", "render",
@ -320,6 +345,7 @@ dependencies = [
"tower", "tower",
"tower-http", "tower-http",
"tower-sessions", "tower-sessions",
"zeroize",
] ]
[[package]] [[package]]
@ -407,9 +433,9 @@ dependencies = [
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.32" version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
@ -1002,7 +1028,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http 0.2.11", "http 0.2.11",
"indexmap 2.1.0", "indexmap 2.2.1",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -1021,7 +1047,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http 1.0.0", "http 1.0.0",
"indexmap 2.1.0", "indexmap 2.2.1",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -1292,9 +1318,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.1.0" version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.3", "hashbrown 0.14.3",
@ -1652,6 +1678,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "open" name = "open"
version = "5.0.1" version = "5.0.1"
@ -1790,18 +1822,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.3" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0"
dependencies = [ dependencies = [
"pin-project-internal", "pin-project-internal",
] ]
[[package]] [[package]]
name = "pin-project-internal" name = "pin-project-internal"
version = "1.1.3" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1853,6 +1885,17 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" 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]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@ -2003,9 +2046,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.4" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -2280,9 +2323,9 @@ checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.195" version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -2299,9 +2342,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.195" version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2310,9 +2353,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.111" version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -2361,15 +2404,15 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.5.0" version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f58c3a1b3e418f61c25b2aeb43fc6c95eaa252b8cecdda67f401943e9e08d33f" checksum = "f5c9fdb6b00a489875b22efd4b78fe2b363b72265cc5f6eb2e2b9ee270e6140c"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.21.7",
"chrono", "chrono",
"hex", "hex",
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.1.0", "indexmap 2.2.1",
"serde", "serde",
"serde_json", "serde_json",
"serde_with_macros", "serde_with_macros",
@ -2378,9 +2421,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_with_macros" name = "serde_with_macros"
version = "3.5.0" version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2068b437a31fc68f25dd7edc296b078f04b45145c199d8eed9866e45f1ff274" checksum = "dbff351eb4b33600a2e138dfa0b10b65a238ea8ff8fb2387c422c5022a3e8298"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
@ -2711,7 +2754,7 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [ dependencies = [
"indexmap 2.1.0", "indexmap 2.2.1",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
@ -2907,6 +2950,16 @@ dependencies = [
"tinyvec", "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]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@ -3231,9 +3284,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.34" version = "0.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" checksum = "1931d78a9c73861da0134f453bb1f790ce49b2e30eba8410b4b79bac72b46a2d"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -3253,3 +3306,17 @@ name = "zeroize"
version = "1.7.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" 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": { "locked": {
"lastModified": 1705974079, "lastModified": 1706473964,
"narHash": "sha256-HyC3C2esW57j6bG0MKwX4kQi25ltslRnr6z2uvpadJo=", "narHash": "sha256-Fq6xleee/TsX6NbtoRuI96bBuDHMU57PrcK9z1QEKbk=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "0b4e511fe6e346381e31d355e03de52aa43e8cb2", "rev": "c798790eabec3e3da48190ae3698ac227aab770c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -40,11 +40,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1705856552, "lastModified": 1706371002,
"narHash": "sha256-JXfnuEf5Yd6bhMs/uvM67/joxYKoysyE3M2k6T3eWbg=", "narHash": "sha256-dwuorKimqSYgyu8Cw6ncKhyQjUDOyuXoxDTVmAXq88s=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "612f97239e2cc474c13c9dafa0df378058c5ad8d", "rev": "c002c6aa977ad22c60398daaa9be52f2203d0006",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -72,11 +72,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1705976279, "lastModified": 1706494265,
"narHash": "sha256-Zx97bJ3+O8IP70uJPD//rRsr8bcxICISMTZUT/L9eFk=", "narHash": "sha256-4ilEUJEwNaY9r/8BpL3VmZiaGber0j09lvvx0e/bosA=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "f889dc31ef97835834bdc3662394ebdb3c96b974", "rev": "246ba7102553851af60e0382f558f6bc5f63fa13",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

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

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"
@ -32,6 +32,9 @@ sha3 = "0.10"
hex = "0.4" hex = "0.4"
bytes = "1.5" bytes = "1.5"
pin-project-lite = "0.2" 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"] } reqwest = { version="0.11", default_features=false, features=["rustls-tls", "json"] }
jsonwebtoken = "9.2.0" jsonwebtoken = "9.2.0"

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

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

View file

@ -1,10 +1,12 @@
#![deny(clippy::unwrap_used)] #![deny(clippy::unwrap_used)]
use aeadstream::AeadStreamReader;
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::{deserialize_option_duration, parse_std};
use jwt::JwtApplication; use jwt::JwtApplication;
use 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},
@ -14,58 +16,54 @@ use sailfish::TemplateOnce;
use std::{ use std::{
borrow::Borrow, borrow::Borrow,
env, env,
io::SeekFrom,
str::FromStr, str::FromStr,
sync::Arc, sync::Arc,
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, post}, routing::{delete, get},
BoxError, Router, Router,
}; };
use chacha20::{ use chacha20::{cipher::KeyIvInit, ChaCha20};
cipher::{KeyIvInit, StreamCipher},
ChaCha20,
};
use futures_util::StreamExt; use futures_util::StreamExt;
use garbage_collector::GarbageCollector; use garbage_collector::GarbageCollector;
use log::{debug, warn}; use log::{debug, warn};
use serde::Deserialize; use serde::Deserialize;
use sha3::{Digest, Sha3_256};
use tokio::{ use tokio::{
fs::{self, File}, fs::{self, File},
io::{AsyncWriteExt, BufReader, BufWriter}, io::{AsyncSeekExt, AsyncWriteExt, BufReader, BufWriter},
net::TcpListener, net::TcpListener,
}; };
use util::{IdSalt, KeySalt}; use util::{IdSalt, KeySalt};
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},
web_util::DecryptingStream,
}; };
mod aeadstream;
mod error; mod error;
mod garbage_collector; mod garbage_collector;
mod jwt; mod jwt;
mod metadata; mod metadata;
mod util; mod util;
mod web_util;
/// length of the "phrase" that is used to access the bin (https://example.com/<phrase>) /// length of the "phrase" that is used to access the bin (https://example.com/<phrase>)
const PHRASE_LENGTH: usize = 16; const PHRASE_LENGTH: usize = 16;
@ -156,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();
@ -211,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
@ -242,7 +249,7 @@ async fn get_index(
let metadata = Metadata { let metadata = Metadata {
subject, subject,
nonce: nonce.to_hex(), nonce: nonce.to_hex(),
etag: None, tag: None,
size: None, size: None,
content_type: None, content_type: None,
expires_at, expires_at,
@ -315,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);
@ -329,30 +336,28 @@ async fn upload_bin(
if !path.exists() { if !path.exists() {
let key = Key::from_phrase(&phrase, &app_state.key_salt); let key = Key::from_phrase(&phrase, &app_state.key_salt);
let nonce = Nonce::from_hex(&metadata.nonce)?; let nonce = Nonce::from_hex(&metadata.nonce)?;
let mut cipher = ChaCha20::new(key.borrow(), nonce.borrow());
let file = File::create(&path).await?; let file = File::create(&path).await?;
let mut writer = BufWriter::new(file); let writer = BufWriter::new(file);
let mut etag_hasher = Sha3_256::new(); let ttl = params
let mut size: u64 = 0;
let mut 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));
match data { let cipher = ChaCha20::new(key.borrow(), nonce.borrow());
MultipartOrStream::Stream(stream) => { let mut writer = AeadStreamWriter::new(writer, cipher, &[]);
let mut stream = stream.into_data_stream();
let mut stream = body.into_data_stream();
while let Some(chunk) = stream.next().await { while let Some(chunk) = stream.next().await {
let mut buf = chunk.unwrap_or_default().to_vec(); writer.write_all(chunk.unwrap_or_default().as_ref()).await?;
etag_hasher.update(&buf);
size += buf.len() as u64;
cipher.apply_keystream(&mut buf);
writer.write_all(&buf).await?;
} }
writer.flush().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
@ -363,41 +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() {
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?;
if let Some(expires_at) = SystemTime::now() if let Some(expires_at) = SystemTime::now()
.duration_since(UNIX_EPOCH)? .duration_since(UNIX_EPOCH)?
@ -407,8 +377,8 @@ async fn upload_bin(
metadata.expires_at = expires_at; metadata.expires_at = expires_at;
} }
metadata.etag = Some(hex::encode(etag_hasher.finalize())); metadata.tag = Some(hex::encode(tag));
metadata.size = Some(size); metadata.size = Some(writer.size() as u64);
metadata.to_file(&metadata_path).await?; metadata.to_file(&metadata_path).await?;
@ -459,21 +429,33 @@ async fn get_item(
} else { } else {
//TODO(pfz4): Maybe add link handling //TODO(pfz4): Maybe add link handling
let file = File::open(&path).await?; let file = File::open(&path).await?;
let reader = BufReader::new(file); let mut reader = BufReader::new(file);
let body = Body::from_stream(DecryptingStream::new( 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(
reader, reader,
id.clone(), cipher,
&metadata, &[],
&key, tag,
&nonce, 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);
} }
@ -481,15 +463,6 @@ async fn get_item(
if let Some(content_type) = metadata.content_type.and_then(|x| x.parse().ok()) { if let Some(content_type) = metadata.content_type.and_then(|x| x.parse().ok()) {
headers.insert(header::CONTENT_TYPE, content_type); 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 app_state
.metrics .metrics
@ -515,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> {

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};
@ -9,7 +10,7 @@ use crate::Error;
pub struct Metadata { pub struct Metadata {
pub subject: String, pub subject: String,
pub nonce: String, pub nonce: String,
pub etag: Option<String>, pub tag: Option<String>,
pub size: Option<u64>, pub size: Option<u64>,
pub content_type: Option<String>, pub content_type: Option<String>,
pub expires_at: u64, // seconds since UNIX_EPOCH pub expires_at: u64, // seconds since UNIX_EPOCH
@ -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,133 +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,
// 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,
}

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,93 @@
<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>