Compare commits

..

116 commits

Author SHA1 Message Date
b213253927
Update deps 2023-10-21 12:01:32 -07:00
b33dfac117
Add progress bar to CLI 2022-08-24 00:58:20 -07:00
909de7f2fa
Better logging message 2022-08-02 02:11:32 -07:00
9004c4ed29
Web workers 2022-08-02 02:10:58 -07:00
600d04eba9
Don't submit empty strings 2022-07-27 20:06:12 -07:00
50e76ae3a0
Update default text 2022-07-27 20:01:49 -07:00
72d41898cf
Update default text 2022-07-27 20:00:19 -07:00
170e23cba7
Update styling 2022-07-27 19:54:13 -07:00
Ninja3047
7b7d119831
Merge pull request #3 from Ninja3047/master
fix typescript errors, add tsconfig.json
2022-07-27 22:08:59 -04:00
Ninja3047
8d0abe0195
remove tsconfig 2022-07-27 22:06:53 -04:00
Ninja3047
7c383e8cd0
fix typescript errors, add tsconfig.json 2022-07-27 21:55:23 -04:00
Ninja3047
253fccaf78
Merge pull request #2 from Ninja3047/master
add support for encrypting text files from web
2022-07-27 21:35:08 -04:00
Ninja3047
b57298ffb2
update README 2022-07-27 18:10:34 -04:00
Ninja3047
4e7b3dfd3b
remove extra log 2022-07-27 17:45:55 -04:00
Ninja3047
a9e9a93493
support file upload, fix some lints 2022-07-27 17:36:15 -04:00
Ninja3047
37bdbae640
error context 2022-07-27 12:27:51 -04:00
Ninja3047
3ab01300b2
placeholder 2022-07-27 12:22:55 -04:00
Ninja3047
3c9c46da18
rewrite component to be functional 2022-07-27 12:18:00 -04:00
Ninja3047
37727bfd3d
update deps, fix clippy, format 2022-07-26 22:38:45 -04:00
Ninja3047
774b13e46c
add support for encrypting text files from web 2022-07-26 22:03:50 -04:00
Ninja3047
b793139a99 update axum again 2022-07-10 21:55:55 -07:00
Ninja3047
57bcd6371c update submodule and submodule script 2022-07-10 21:55:55 -07:00
Ninja3047
064fd749a4 update lock 2022-07-10 21:55:55 -07:00
Ninja3047
4694683b9a update axum 2022-07-10 21:55:55 -07:00
Ninja3047
c934b36b35 update headers to new type from reqwasm 2022-07-10 21:55:55 -07:00
Ninja3047
711f79a255 update rpassword dep 2022-07-10 21:55:55 -07:00
Ninja3047
9a7f13f8b9 add crypto tests 2022-07-10 21:55:55 -07:00
c73af50857
Update dependencies 2022-02-27 16:30:21 -08:00
7047be5ff1
Update README.md 2022-01-18 18:29:12 -08:00
111a9e9b98
use stdlib xchacha20poly1305 2022-01-18 18:09:28 -08:00
404d78c0ba
Update readme 2022-01-18 02:47:34 -08:00
5fa69be1d5
Use JSX 2022-01-18 02:46:07 -08:00
9a8658e744
Fix infra scripts 2022-01-18 01:52:44 -08:00
6859bffe05
Migrate to webpack 2022-01-18 01:39:56 -08:00
08f3e940e4
Update to axum 0.4 2022-01-18 00:36:02 -08:00
3393f40482
use correct declaration file name 2022-01-17 23:24:00 -08:00
e5519f7d3a
Update cli to 0.1.1 2022-01-17 21:30:32 -08:00
783028fc5d
Better CLI support 2022-01-17 21:29:58 -08:00
8504572b83
Bump common to 0.2.0 2022-01-17 21:27:25 -08:00
0eb8adc20e
implmeent macro for expiration from str 2022-01-17 21:26:34 -08:00
d6b2d53249
Only keep filename for cli filename hint 2022-01-16 19:40:57 -08:00
f1ad421777
Clippy 2022-01-16 19:40:14 -08:00
ae5965a7da
Fix image rendering in web ui 2022-01-16 19:34:28 -08:00
18ee415f46
Resolve filename in typescript 2022-01-16 01:01:38 -08:00
732e2bd2f3
Use fstrings where possible 2022-01-16 00:49:42 -08:00
4e2cfcfac6
Add mime type guessing from file ext for web ui 2022-01-16 00:49:07 -08:00
41d7feb4df
Better log on if language hint was provided 2022-01-16 00:03:34 -08:00
faac1936a9
ts nits 2022-01-15 23:59:39 -08:00
0dbc5fb44e
Anotate rust console messages 2022-01-15 23:53:25 -08:00
6337642b3c
Try to infer lang from name hint in web ui 2022-01-15 23:49:51 -08:00
ca71815d22
Add support for language and name hints to cli 2022-01-15 23:49:33 -08:00
b7d5425a73
Add support for language and name hints 2022-01-15 22:47:56 -08:00
9b2f53bddf
Update web deps script license 2022-01-15 21:41:14 -08:00
f7431ca6a4
Clippy 2022-01-15 21:40:36 -08:00
67aa009746
Move decryption handlers to functions 2022-01-15 21:38:33 -08:00
8a09da764e
Rename GzipArchive to Gzip 2022-01-15 21:34:52 -08:00
3cb95a9f04
Update hljs to 11.4.0 2022-01-15 21:29:41 -08:00
b45b2debe9
Add update script for web dependencies" 2022-01-15 21:29:16 -08:00
William Tan
468ce8a178
Add proper tar.gz support 2022-01-15 21:03:42 -08:00
9cc00b4e5b
update submodules 2022-01-15 20:55:04 -08:00
bccf84cfc1
Clippy 2022-01-15 16:55:47 -08:00
c2bdd089e9
Better error message for missing metadata 2022-01-11 23:54:45 -08:00
4c7ae4feb2
clippy 2022-01-11 23:50:07 -08:00
1fca1c51e4
Propogate invalid url errors to web frontend 2022-01-11 23:48:53 -08:00
83779545b9
Use TryFrom for PartialParsedUrl 2022-01-11 23:37:59 -08:00
e720007cbe
Add unit tests for fragment parsing 2022-01-11 23:24:47 -08:00
73b7b50ed4
Clarify order change 2022-01-11 22:45:03 -08:00
William Tan
2b6fc073fb
Better plaintext detection 2022-01-11 22:42:08 -08:00
247a5ad6f6
Pass mimetype to frontend 2022-01-11 22:19:15 -08:00
f1c40d64c7
Add web parsing tests; additional test media 2022-01-11 21:33:23 -08:00
112b75afae
Fix mistaken doctests 2022-01-11 21:33:05 -08:00
65c258bd0d
Add support for reading from stdin 2022-01-11 20:42:40 -08:00
a17f4d94e4
Add support for patch files 2022-01-11 20:42:30 -08:00
William Tan
2ebde58c86
Update dependencies 2022-01-11 20:31:21 -08:00
3079c33e45
Correctly specify clap version 2021-12-19 19:39:03 -08:00
b3bb1e0106
Update dependencies 2021-12-19 19:38:01 -08:00
67aff383a2
Add viewport tag 2021-12-19 19:28:30 -08:00
8630eaa21e
migrate to axum 0.3 2021-11-14 14:02:55 -08:00
a14e3e2afe
Clean up server deletion code 2021-11-14 13:52:40 -08:00
26950c8c62
Clean up readme 2021-11-05 21:28:28 -07:00
21c28a77f9
fixed readme 2021-10-31 16:10:00 -07:00
642a8973a4
Fix metadata for cli upload 2021-10-31 16:08:49 -07:00
8454607859
Add metadata for omegaupload-common 2021-10-31 16:06:23 -07:00
76e2494011
Rename cli without the -cli suffix 2021-10-31 15:54:27 -07:00
3732549873
Fix script import ordering 2021-10-31 15:52:34 -07:00
22e3ebed43
Update readme 2021-10-31 15:42:22 -07:00
5bb3ad2d0d
Use stronger argon2id params 2021-10-31 15:42:15 -07:00
3e2f608e27
Remove test text case 2021-10-31 14:02:12 -07:00
d6818e8237
Add License text to everything 2021-10-31 14:01:27 -07:00
8cbc1cd3f4
defer, not async, main.js 2021-10-31 12:53:05 -07:00
f748fbf265
Use Closure::once instead of Closure::wrap 2021-10-31 12:43:44 -07:00
1d4d37b6ea
move onsuccess to it's own function 2021-10-31 12:40:43 -07:00
3bc4541390
clippy lints 2021-10-31 12:34:26 -07:00
de4d4f90b7
format upload script 2021-10-31 11:44:46 -07:00
a9e994820c
Add shell scripts 2021-10-31 02:53:43 -07:00
4e0e08f3f0
Better pw prompting 2021-10-31 01:39:11 -07:00
4f5d1c46d3
"Support" index page fetch 2021-10-31 01:16:31 -07:00
05d736e88e
Fix arm guard 2021-10-31 01:01:05 -07:00
b833f97c55
clippy 2021-10-31 00:57:52 -07:00
24eff63a5e
Add support for querying db via SIGUSR1 2021-10-30 23:38:20 -07:00
9a85d13bd9
Fix length bug in nonce generation 2021-10-30 23:15:41 -07:00
8e05c622af
Make key material secret 2021-10-30 21:00:09 -07:00
ac52c20e3b
More flexible build script 2021-10-30 19:43:03 -07:00
d2084f369e
Fix split_off bug 2021-10-30 19:42:51 -07:00
a5b105f486
Fix option not derefing 2021-10-30 18:39:40 -07:00
bb35f710b2
Make crypto even harder to fuck up 2021-10-30 18:38:55 -07:00
8a08e8e100
support burns 2021-10-27 19:16:43 -07:00
c29340c93b
Sort entries 2021-10-27 17:45:36 -07:00
364a467626
Fix svg size 2021-10-27 02:27:44 -07:00
06a9522514
Document cli args 2021-10-27 02:23:37 -07:00
372a160558
Add support for svgs 2021-10-27 02:23:28 -07:00
d2755f82c9
Features 2021-10-27 01:49:06 -07:00
16bce1d11b
Have server serve web files 2021-10-26 23:51:05 -07:00
0d5f9f3288
Fix clap 3.0.0-beta.5 renaming 2021-10-26 22:30:37 -07:00
9bb265670e
Remove commented out dep 2021-10-26 22:25:28 -07:00
William Tan
6ef8d48db0
Add support for previewing zip files 2021-10-26 22:23:31 -07:00
72 changed files with 7032 additions and 6027 deletions

3
.gitignore vendored
View file

@ -3,3 +3,6 @@
**/dist/ **/dist/
**/node_modules **/node_modules
test.* test.*
dist.tar.zst
.env
web/pkg

3
.gitmodules vendored
View file

@ -4,9 +4,6 @@
[submodule "web/vendor/highlight.js"] [submodule "web/vendor/highlight.js"]
path = web/vendor/highlight.js path = web/vendor/highlight.js
url = git@github.com:highlightjs/highlight.js.git url = git@github.com:highlightjs/highlight.js.git
[submodule "web/vendor/text-fragments-polyfill"]
path = web/vendor/text-fragments-polyfill
url = git@github.com:GoogleChromeLabs/text-fragments-polyfill.git
[submodule "web/vendor/highlightjs-line-numbers.js"] [submodule "web/vendor/highlightjs-line-numbers.js"]
path = web/vendor/highlightjs-line-numbers.js path = web/vendor/highlightjs-line-numbers.js
url = git@github.com:wcoder/highlightjs-line-numbers.js.git url = git@github.com:wcoder/highlightjs-line-numbers.js.git

1
.shellcheckrc Normal file
View file

@ -0,0 +1 @@
external-sources=true

9
.swcrc Normal file
View file

@ -0,0 +1,9 @@
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true
},
"target": "es2021"
}
}

2186
Cargo.lock generated

File diff suppressed because it is too large Load diff

116
README.md Normal file
View file

@ -0,0 +1,116 @@
# OmegaUpload
OmegaUpload is a zero-knowledge temporary file hosting service.
## Examples
```bash
# Uploading a file:
$ omegaupload upload https://paste.example.com path/to/file
https://paste.example.com/PgRG8Hfrr9rR#I1FG2oejo2gSjB3Ym1mEmRfcN4X8GXc2pZtZeiSsWFo=
# Uploading a file with a password:
$ omegaupload upload -p https://paste.example.com path/to/file
Please set the password for this paste:
https://paste.example.com/862vhXVp3v9R#key:tbGxzHBNnXjS2eq89X9uvZKz_i8bvapLPEp8g0waQrc=!pw
# Downloading a file:
$ omegaupload download https://paste.example.com/PgRG8Hfrr9rR#I1FG2oejo2gSjB3Ym1mEmRfcN4X8GXc2pZtZeiSsWFo=
```
## Features
- Server has zero knowledge of uploaded data when uploading through a supported
frontend (Direct, plaintext upload is possible but unsupported).
- Only metadata stored on server is expiration time. This is a strong guarantee.
- All cryptographic functions are performed on the client side and are done via
a single common library, to minimize risk of programming error.
- Modern crypto functions are used with recommended parameters:
XChaCha20Poly1305 for encryption and Argon2id for KDF.
- Customizable expiration times, from burn-after-read to 1 day.
## Building from source
Prerequisites:
- `yarn` 1.22.17 or later (Earlier versions untested but likely to work)
- Cargo, with support for the latest Rust version
- _(Optional)_ zstd, for zipping up the file for distribution
First, run `git submodule update --init --recursive`.
Then, run `./bin/build.sh` for a `dist.tar.zst` to be generated, where you can
simply extract that folder and run the binary provided. The server will listen
on port `8080`.
### Running a local server
After running `./bin/build.sh`, you can cd into the `dist` and run
`./omegaupload-server`. It will run on port 8000, and will respond to HTTP
requests.
You can then point an omegaupload CLI instance (or run
`cargo run --bin omegaupload`) as an upload server.
If you're only changing the frontend (and not updating the server code), you can
run `yarn build` for faster iteration.
## Why OmegaUpload?
OmegaUpload's primary benefit is that the frontends use a unified common library
utilizing XChaCha20Poly1305 to encrypt and decrypt files.
### Security
The primary goal was to provide a unified library across both a CLI tool and
through the web frontend to minimize risk of compromise. As a result, the CLI
tool and the web frontend both utilize a Rust library whose crypto module
exposes two functions to encrypt and decrypt that only accept a message and
necessarily key material or return only necessary key material. This small API
effectively makes it impossible to have differences between the frontend, and
ensures that the attack surface is limited to these functions.
#### Password KDF
If a password is provided at encryption time, argon2 is used as a key derivation
function. Specifically, the library meets or exceeds OWASP recommended
parameters:
- Argon2id is used.
- Algorithm version is `0x13`.
- Parameters are `m = 15MiB`, `t = 2`, `p = 2`.
Additionally, a salt size of 16 bytes are used.
#### Blob Encryption
XChaCha20Poly1305 was used as the encryption method as it is becoming the
mainstream recommended method for encrypting messages. This was chosen over AES
primarily due to its strength in related-key attacks, as well as its widespread
recognition and usage in WireGuard, Quic, and TLS.
As this crate uses `XChaCha20`, a 24 byte nonce and a 32 bytes key are used.
#### Secrecy
Encryption and decryption functions offered by the common crate only accept or
return key material that will be properly zeroed on destruction. This is
enforced by the `secrecy` crate, which, on top of offering type wrappers that
zero the memory on drop, provide an easy way to audit when secrets are exposed.
This also means that to use these two functions necessarily requires the caller
to enclose key material in the wrapped type first, reducing possibility for key
material to remain in memory.
#### Memory Safety
Rust eliminates an entire class of memory-related bugs, and any `unsafe` block
is documented with a safety comment. This allows for easy auditing of memory
suspect code, and permits
## Why not OmegaUpload?
There are a few reasons to not use OmegaUpload:
- Limited to 3GB uploads—this is a soft limit of RocksDB.
- Cannot download files larger than 512 MiB through the web frontend—this
is a technical limitation of the current web frontend not using a web worker
in addition to the fact that browsers are not optimized for XChaCha20.
- The frontend uses WASM, which is a novel attack surface.

38
bin/build.sh Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
# OmegaUpload Build Script
# Copyright (C) 2021 Edward Shen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
set -euxo pipefail
cd "$(git rev-parse --show-toplevel)" || exit 1
# Clean resources
rm -rf dist
# Build frontend code
yarn
yarn build
mv dist/static/index.html dist
# Build server
cargo build --release --bin omegaupload-server
strip target/release/omegaupload-server
cp target/release/omegaupload-server dist/omegaupload-server
tar -cvf dist.tar dist
rm -rf dist.tar.zst
zstd -T0 --ultra --rm -22 dist.tar

27
bin/update_web_deps.sh Executable file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env bash
# OmegaUpload Update Web Dependencies Script
# Copyright (C) 2021 Edward Shen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
set -euxo pipefail
CUR_DIR=$(pwd)
PROJECT_TOP_LEVEL=$(git rev-parse --show-toplevel)
cd "$PROJECT_TOP_LEVEL" || exit 1
git submodule update --remote
cd "$CUR_DIR"

38
bin/upload_test_files.sh Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
# OmegaUpload Upload Test Script
# Copyright (C) 2021 Edward Shen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
source .env
cd "$(git rev-parse --show-toplevel)" || exit 1
cargo build --release --bin omegaupload
TEST_PATH="test/*"
PADDING=0
for file in $TEST_PATH; do
if [ $PADDING -lt ${#file} ]; then
PADDING=${#file}
fi
done
for file in $TEST_PATH; do
printf "%$((PADDING - ${#TEST_PATH} + 1))s: " "${file#$TEST_PATH}"
./target/release/omegaupload upload "$PASTE_URL" "$file"
done

View file

@ -1,15 +1,19 @@
[package] [package]
name = "omegaupload-cli" name = "omegaupload"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
description = "OmegaUpload CLI tool"
repository = "https://git.eddie.sh/edward/omegaupload"
license = "GPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
omegaupload-common = { path = "../common" } omegaupload-common = { path = "../common" }
anyhow = "1.0.58"
anyhow = "1" atty = "0.2.14"
atty = "0.2" bytes = "1"
clap = "3.0.0-beta.4" clap = { version = "3.2.15", features = ["derive"] }
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "blocking"] } indicatif = "0.17"
secrecy = { version = "0.8", features = ["serde"] } reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls", "blocking"] }
rpassword = "7.0.0"

View file

@ -1,32 +1,74 @@
#![warn(clippy::nursery, clippy::pedantic)] #![warn(clippy::nursery, clippy::pedantic)]
#![deny(unsafe_code)] #![deny(unsafe_code)]
use std::io::{Read, Write}; // OmegaUpload CLI Client
// Copyright (C) 2021 Edward Shen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::io::{Cursor, Read, Write};
use std::path::PathBuf;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use atty::Stream; use atty::Stream;
use clap::Clap; use bytes::Bytes;
use omegaupload_common::crypto::{gen_key_nonce, open_in_place, seal_in_place, Key}; use clap::Parser;
use omegaupload_common::{base64, hash, Expiration, ParsedUrl, Url}; use indicatif::{ProgressBar, ProgressStyle};
use reqwest::blocking::Client; use omegaupload_common::crypto::{open_in_place, seal_in_place};
use omegaupload_common::fragment::Builder;
use omegaupload_common::secrecy::{ExposeSecret, SecretString, SecretVec};
use omegaupload_common::{
base64, Expiration, ParsedUrl, Url, API_ENDPOINT, EXPIRATION_HEADER_NAME,
};
use reqwest::blocking::{Body, Client};
use reqwest::header::EXPIRES; use reqwest::header::EXPIRES;
use reqwest::StatusCode; use reqwest::StatusCode;
use secrecy::{ExposeSecret, SecretString}; use rpassword::prompt_password;
#[derive(Clap)] #[derive(Parser)]
struct Opts { struct Opts {
#[clap(subcommand)] #[clap(subcommand)]
action: Action, action: Action,
} }
#[derive(Clap)] #[derive(Parser)]
enum Action { enum Action {
/// Upload a paste to an omegaupload server.
Upload { Upload {
/// The OmegaUpload instance to upload data to.
url: Url, url: Url,
/// Encrypt the uploaded paste with the provided password, preventing
/// public access.
#[clap(short, long)] #[clap(short, long)]
password: Option<SecretString>, password: bool,
/// How long for the paste to last, or until someone has read it.
#[clap(short, long, possible_values = Expiration::variants())]
duration: Option<Expiration>,
/// The path to the file to upload. If none is provided, then reads
/// stdin instead.
path: Option<PathBuf>,
/// Hint that the uploaded file should be syntax highlighted with a
/// specific language.
#[clap(short, long)]
language: Option<String>,
/// Don't provide a file name hint.
#[clap(short = 'F', long)]
no_file_name_hint: bool,
}, },
/// Download a paste from an omegaupload server.
Download { Download {
/// The paste to download.
url: ParsedUrl, url: ParsedUrl,
}, },
} }
@ -35,47 +77,87 @@ fn main() -> Result<()> {
let opts = Opts::parse(); let opts = Opts::parse();
match opts.action { match opts.action {
Action::Upload { url, password } => handle_upload(url, password), Action::Upload {
url,
password,
duration,
path,
language,
no_file_name_hint,
} => handle_upload(url, password, duration, path, language, no_file_name_hint),
Action::Download { url } => handle_download(url), Action::Download { url } => handle_download(url),
}?; }?;
Ok(()) Ok(())
} }
fn handle_upload(mut url: Url, password: Option<SecretString>) -> Result<()> { fn handle_upload(
mut url: Url,
password: bool,
duration: Option<Expiration>,
path: Option<PathBuf>,
language: Option<String>,
no_file_name_hint: bool,
) -> Result<()> {
url.set_fragment(None); url.set_fragment(None);
if atty::is(Stream::Stdin) { if password && path.is_none() {
bail!("This tool requires non interactive CLI. Pipe something in!"); bail!("Reading data from stdin is incompatible with a password. Provide a path to a file to upload.");
} }
let (data, nonce, key, pw_used) = { let (data, key) = {
let (enc_key, nonce) = gen_key_nonce(); let mut container = if let Some(ref path) = path {
let mut container = Vec::new(); std::fs::read(path)?
std::io::stdin().read_to_end(&mut container)?;
seal_in_place(&mut container, &nonce, &enc_key)
.map_err(|_| anyhow!("Failed to encrypt data"))?;
let pw_used = if let Some(password) = password {
let pw_hash = hash(password.expose_secret().as_bytes());
let pw_key = Key::from_slice(pw_hash.as_ref());
seal_in_place(&mut container, &nonce.increment(), pw_key)
.map_err(|_| anyhow!("Failed to encrypt data"))?;
true
} else { } else {
false let mut container = vec![];
std::io::stdin().lock().read_to_end(&mut container)?;
container
}; };
let key = base64::encode(&enc_key); if container.is_empty() {
let nonce = base64::encode(&nonce); bail!("Nothing to upload.");
}
(container, nonce, key, pw_used) let password = if password {
let maybe_password = prompt_password("Please set the password for this paste: ")?;
Some(SecretVec::new(maybe_password.into_bytes()))
} else {
None
};
let enc_key = seal_in_place(&mut container, password)?;
let key = SecretString::new(base64::encode(&enc_key.expose_secret().as_ref()));
(container, key)
}; };
let res = Client::new() let mut req = Client::new().post(url.as_ref());
.post(url.as_ref())
.body(data) if let Some(duration) = duration {
.send() req = req.header(&*EXPIRATION_HEADER_NAME, duration);
}
let data_size = data.len() as u64;
let progress_style = ProgressStyle::with_template(
"[{elapsed_precise}] {bar:40} {bytes}/{total_bytes} {eta_precise}",
)
.unwrap();
let progress_bar = ProgressBar::new(data_size).with_style(progress_style);
let res = req
.body(Body::sized(
WrappedBody::new(
move |amt| {
progress_bar.inc(amt as u64);
},
data,
),
data_size,
))
.build()
.expect("Failed to build body");
let res = reqwest::blocking::ClientBuilder::new()
.timeout(None)
.build()?
.execute(res)
.context("Request to server failed")?; .context("Request to server failed")?;
if res.status() != StatusCode::OK { if res.status() != StatusCode::OK {
@ -86,20 +168,59 @@ fn handle_upload(mut url: Url, password: Option<SecretString>) -> Result<()> {
.map_err(|_| anyhow!("Failed to get base URL"))? .map_err(|_| anyhow!("Failed to get base URL"))?
.extend(std::iter::once(res.text()?)); .extend(std::iter::once(res.text()?));
let mut fragment = format!("key:{}!nonce:{}", key, nonce); let mut fragment = Builder::new(key);
if password {
if pw_used { fragment = fragment.needs_password();
fragment.push_str("!pw");
} }
url.set_fragment(Some(&fragment)); if !no_file_name_hint {
let file_name = path.and_then(|path| {
path.file_name()
.map(|str| str.to_string_lossy().to_string())
});
if let Some(file_name) = file_name {
fragment = fragment.file_name(file_name);
}
}
println!("{}", url); if let Some(language) = language {
fragment = fragment.language(language);
}
url.set_fragment(Some(fragment.build().expose_secret()));
println!("{url}");
Ok(()) Ok(())
} }
fn handle_download(url: ParsedUrl) -> Result<()> { struct WrappedBody<Callback> {
callback: Callback,
inner: Cursor<Bytes>,
}
impl<Callback> WrappedBody<Callback> {
fn new(callback: Callback, data: Vec<u8>) -> Self {
Self {
callback,
inner: Cursor::new(Bytes::from(data)),
}
}
}
impl<Callback: FnMut(usize)> Read for WrappedBody<Callback> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let res = self.inner.read(buf);
if let Ok(size) = res {
(self.callback)(size);
}
res
}
}
fn handle_download(mut url: ParsedUrl) -> Result<()> {
url.sanitized_url
.set_path(&format!("{API_ENDPOINT}{}", url.sanitized_url.path()));
let res = Client::new() let res = Client::new()
.get(url.sanitized_url) .get(url.sanitized_url)
.send() .send()
@ -109,7 +230,8 @@ fn handle_download(url: ParsedUrl) -> Result<()> {
bail!("Got bad response from server: {}", res.status()); bail!("Got bad response from server: {}", res.status());
} }
let expiration_text = dbg!(res.headers()) let expiration_text = res
.headers()
.get(EXPIRES) .get(EXPIRES)
.and_then(|v| Expiration::try_from(v).ok()) .and_then(|v| Expiration::try_from(v).ok())
.as_ref() .as_ref()
@ -120,25 +242,15 @@ fn handle_download(url: ParsedUrl) -> Result<()> {
let mut data = res.bytes()?.as_ref().to_vec(); let mut data = res.bytes()?.as_ref().to_vec();
if url.needs_password { let password = if url.needs_password {
// Only print prompt on interactive, else it messes with output // Only print prompt on interactive, else it messes with output
if atty::is(Stream::Stdout) { let maybe_password = prompt_password("Please enter the password to access this paste: ")?;
print!("Please enter the password to access this document: "); Some(SecretVec::new(maybe_password.into_bytes()))
std::io::stdout().flush()?; } else {
} None
let mut input = String::new(); };
std::io::stdin().read_line(&mut input)?;
input.pop(); // last character is \n, we need to drop it.
let pw_hash = hash(input.as_bytes()); open_in_place(&mut data, &url.decryption_key, password)?;
let pw_key = Key::from_slice(pw_hash.as_ref());
open_in_place(&mut data, &url.nonce.increment(), pw_key)
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect password?"))?;
}
open_in_place(&mut data, &url.nonce, &url.decryption_key)
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect decryption key?"))?;
if atty::is(Stream::Stdout) { if atty::is(Stream::Stdout) {
if let Ok(data) = String::from_utf8(data) { if let Ok(data) = String::from_utf8(data) {
@ -150,7 +262,7 @@ fn handle_download(url: ParsedUrl) -> Result<()> {
std::io::stdout().write_all(&data)?; std::io::stdout().write_all(&data)?;
} }
eprintln!("{}", expiration_text); eprintln!("{expiration_text}");
Ok(()) Ok(())
} }

View file

@ -1,27 +1,32 @@
[package] [package]
name = "omegaupload-common" name = "omegaupload-common"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
description = "Common library for OmegaUpload"
repository = "https://git.eddie.sh/edward/omegaupload"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
base64 = "0.13" base64 = "0.21.0"
bytes = { version = "*", features = ["serde"] } bytes = { version = "1.2.0", features = ["serde"] }
chacha20poly1305 = "0.9" chacha20poly1305 = { version = "0.10", features = ["stream", "std"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }
headers = "*" headers = "0.3.7"
lazy_static = "1" lazy_static = "1.4.0"
rand = "0.8" rand = "0.8.5"
serde = { version = "1", features = ["derive"] } secrecy = "0.8.0"
sha2 = "0.9" serde = { version = "1.0.140", features = ["derive"] }
thiserror = "1" thiserror = "1.0.31"
url = "2" typenum = "1.15.0"
url = "2.2.2"
argon2 = "0.5"
# Wasm deps # Wasm features
web-sys = { version = "0.3", features = ["Headers"], optional = true } gloo-console = { version = "0.3", optional = true }
http = { version = "0.2", optional = true } reqwasm = { version = "0.5.0", optional = true }
wasm-bindgen = { version = "0.2", optional = true } http = { version = "0.2.8", optional = true }
[features] [features]
wasm = ["web-sys", "http", "wasm-bindgen"] wasm = ["gloo-console", "reqwasm", "http"]

42
common/src/base64.rs Normal file
View file

@ -0,0 +1,42 @@
// Copyright (c) 2021 Edward Shen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use base64::alphabet::URL_SAFE;
use base64::engine::general_purpose::GeneralPurpose;
use base64::engine::general_purpose::GeneralPurposeConfig;
use base64::DecodeError;
use base64::Engine;
const URL_BASE64: GeneralPurpose = GeneralPurpose::new(&URL_SAFE, GeneralPurposeConfig::new());
/// URL-safe Base64 encoding.
pub fn encode(input: impl AsRef<[u8]>) -> String {
URL_BASE64.encode(input)
}
/// URL-safe Base64 decoding.
///
/// # Errors
///
/// Returns an error if a buffer cannot be decoded, such as if there's an
/// incorrect number of bytes.
pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
URL_BASE64.decode(input)
}

334
common/src/crypto.rs Normal file
View file

@ -0,0 +1,334 @@
// Copyright (c) 2021 Edward Shen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use std::ops::{Deref, DerefMut};
use argon2::{Argon2, ParamsBuilder};
use chacha20poly1305::aead::generic_array::sequence::GenericSequence;
use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::aead::{AeadInPlace};
use chacha20poly1305::XChaCha20Poly1305;
use chacha20poly1305::XNonce;
use rand::{CryptoRng, Rng};
use secrecy::{DebugSecret, ExposeSecret, Secret, SecretVec, Zeroize};
use typenum::Unsigned;
use chacha20poly1305::KeyInit;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Invalid password.")]
Password,
#[error("Invalid secret key.")]
SecretKey,
#[error("An error occurred while trying to decrypt the blob.")]
Encryption,
#[error("An error occurred while trying to derive a secret key.")]
Kdf,
}
// This struct intentionally prevents implement Clone or Copy
#[derive(Default, PartialEq, Eq)]
pub struct Key(chacha20poly1305::Key);
impl Key {
/// Encloses a secret key in a secret `Key` struct.
pub fn new_secret(vec: Vec<u8>) -> Option<Secret<Self>> {
chacha20poly1305::Key::from_exact_iter(vec.into_iter())
.map(Self)
.map(Secret::new)
}
}
impl DebugSecret for Key {}
impl AsRef<chacha20poly1305::Key> for Key {
fn as_ref(&self) -> &chacha20poly1305::Key {
&self.0
}
}
impl Deref for Key {
type Target = chacha20poly1305::Key;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Key {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Zeroize for Key {
fn zeroize(&mut self) {
self.0.zeroize();
}
}
/// Seals the provided message with an optional password, returning the secret
/// key used to encrypt the message and mutating the buffer to contain necessary
/// metadata.
///
/// The resulting sealed message has the nonce used to encrypt the message
/// appended to it as well as a salt string used to derive the key. In other
/// words, the modified buffer is one of the following to possibilities,
/// depending if there was a password provided:
///
/// ```text
/// modified = C(message, rng_key, nonce) || nonce
/// ```
/// or
/// ```text
/// modified = C(C(message, rng_key, nonce), kdf(pw, salt), nonce + 1) || nonce || salt
/// ```
///
/// Where:
/// - `C(message, key, nonce)` represents encrypting a provided message with
/// `XChaCha20Poly1305`.
/// - `rng_key` represents a randomly generated key.
/// - `kdf(pw, salt)` represents a key derived from Argon2.
/// - `nonce` represents a randomly generated nonce.
///
/// Note that the lengths for the nonce, key, and salt follow recommended
/// values. As of writing this doc (2021-10-31), the nonce size is 24 bytes, the
/// salt size is 16 bytes, and the key size is 32 bytes.
///
/// # Errors
///
/// This message will return an error if and only if there was a problem
/// encrypting the message or deriving a secret key from the password, if one
/// was provided.
pub fn seal_in_place(
message: &mut Vec<u8>,
pw: Option<SecretVec<u8>>,
) -> Result<Secret<Key>, Error> {
let (key, nonce) = gen_key_nonce();
let cipher = XChaCha20Poly1305::new(key.expose_secret());
cipher
.encrypt_in_place(&nonce, &[], message)
.map_err(|_| Error::Encryption)?;
let mut maybe_salt_string = None;
if let Some(password) = pw {
let (key, salt_string) = kdf(&password).map_err(|_| Error::Kdf)?;
maybe_salt_string = Some(salt_string);
let cipher = XChaCha20Poly1305::new(key.expose_secret());
cipher
.encrypt_in_place(&nonce.increment(), &[], message)
.map_err(|_| Error::Encryption)?;
}
message.extend_from_slice(nonce.as_slice());
if let Some(maybe_salted_string) = maybe_salt_string {
message.extend_from_slice(maybe_salted_string.as_ref());
}
Ok(key)
}
/// Opens a message that has been sealed with `seal_in_place`.
///
/// # Errors
///
/// Returns an error if there was a decryption failure or if there was a problem
/// deriving a secret key from the password.
pub fn open_in_place(
data: &mut Vec<u8>,
key: &Secret<Key>,
password: Option<SecretVec<u8>>,
) -> Result<(), Error> {
let pw_key = if let Some(password) = password {
let salt_buf = data.split_off(data.len() - Salt::SIZE);
let argon = get_argon2();
let mut pw_key = Key::default();
argon
.hash_password_into(password.expose_secret(), &salt_buf, &mut pw_key)
.map_err(|_| Error::Kdf)?;
Some(Secret::new(pw_key))
} else {
None
};
let nonce = Nonce::from_slice(&data.split_off(data.len() - Nonce::SIZE));
// At this point we should have a buffer that's only the ciphertext.
if let Some(key) = pw_key {
let cipher = XChaCha20Poly1305::new(key.expose_secret());
cipher
.decrypt_in_place(&nonce.increment(), &[], data)
.map_err(|_| Error::Password)?;
}
let cipher = XChaCha20Poly1305::new(key.expose_secret());
cipher
.decrypt_in_place(&nonce, &[], data)
.map_err(|_| Error::SecretKey)?;
Ok(())
}
#[must_use]
fn gen_key_nonce() -> (Secret<Key>, Nonce) {
let mut rng = get_csrng();
let mut key = GenericArray::default();
rng.fill(key.as_mut_slice());
let mut nonce = Nonce::default();
rng.fill(nonce.as_mut_slice());
(Secret::new(Key(key)), nonce)
}
// Type alias; to ensure that we're consistent on what the inner impl is.
type NonceImpl = XNonce;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
struct Nonce(NonceImpl);
impl Deref for Nonce {
type Target = NonceImpl;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Nonce {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl AsRef<[u8]> for Nonce {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl Nonce {
const SIZE: usize = <NonceImpl as GenericSequence<_>>::Length::USIZE;
#[must_use]
pub fn increment(&self) -> Self {
let mut inner = self.0;
inner.as_mut_slice()[0] += 1;
Self(inner)
}
#[must_use]
pub fn from_slice(slice: &[u8]) -> Self {
Self(*NonceImpl::from_slice(slice))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Salt([u8; Self::SIZE]);
impl Salt {
const SIZE: usize = argon2::password_hash::Salt::RECOMMENDED_LENGTH;
fn random() -> Self {
let mut salt = [0_u8; Self::SIZE];
get_csrng().fill(&mut salt);
Self(salt)
}
}
impl AsRef<[u8]> for Salt {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
/// Hashes an input to output a usable key.
fn kdf(password: &SecretVec<u8>) -> Result<(Secret<Key>, Salt), argon2::Error> {
let salt = Salt::random();
let hasher = get_argon2();
let mut key = Key::default();
hasher.hash_password_into(password.expose_secret().as_ref(), salt.as_ref(), &mut key)?;
Ok((Secret::new(key), salt))
}
/// Returns Argon2id configured as follows:
/// - 15MiB of memory (`m`),
/// - an iteration count of 2 (`t`),
/// - and 2 degrees of parallelism (`p`).
///
/// This follows the [minimum recommended parameters suggested by OWASP][rec].
///
/// [rec]: https://link.eddie.sh/vaQ6a.
fn get_argon2() -> Argon2<'static> {
let mut params = ParamsBuilder::new();
params
.m_cost(15 * 1024) // 15 MiB
.t_cost(2)
.p_cost(2);
let params = params.build().expect("Hard coded params to work");
Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params)
}
/// Fetches a cryptographically secure random number generator. This indirection
/// is used for better auditing the quality of rng. Notably, this function
/// returns a `Rng` with the `CryptoRng` marker trait, preventing
/// non-cryptographically secure RNGs from being used.
#[must_use]
pub fn get_csrng() -> impl CryptoRng + Rng {
rand::thread_rng()
}
#[cfg(test)]
mod test {
use super::open_in_place;
use super::seal_in_place;
use crate::crypto::SecretVec;
macro_rules! test_encryption {
($($name:ident, $content:expr, $password:expr),*) => {
$(
#[test]
fn $name() {
let mut m = $content;
let n: Vec<u8> = $content;
let key = seal_in_place(&mut m, $password).unwrap();
assert_ne!(m, n);
assert!(open_in_place(&mut m, &key, $password).is_ok());
assert_eq!(m, n);
}
)*
};
}
test_encryption!(empty, vec![], None);
test_encryption!(
empty_password,
vec![],
Some(SecretVec::from(b"password".to_vec()))
);
test_encryption!(
normal,
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
None
);
test_encryption!(
normal_password,
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
Some(SecretVec::from(b"password".to_vec()))
);
}

66
common/src/fragment.rs Normal file
View file

@ -0,0 +1,66 @@
use crate::secrecy::{ExposeSecret, SecretString};
pub struct Builder {
decryption_key: SecretString,
needs_password: bool,
file_name: Option<String>,
language: Option<String>,
}
impl Builder {
#[must_use]
pub fn new(decryption_key: SecretString) -> Self {
Self {
decryption_key,
needs_password: false,
file_name: None,
language: None,
}
}
#[must_use]
pub const fn needs_password(mut self) -> Self {
self.needs_password = true;
self
}
// False positive
#[allow(clippy::missing_const_for_fn)]
#[must_use]
pub fn file_name(mut self, name: String) -> Self {
self.file_name = Some(name);
self
}
// False positive
#[allow(clippy::missing_const_for_fn)]
#[must_use]
pub fn language(mut self, language: String) -> Self {
self.language = Some(language);
self
}
#[must_use]
pub fn build(self) -> SecretString {
if !self.needs_password && self.file_name.is_none() && self.language.is_none() {
return self.decryption_key;
}
let mut args = String::new();
if self.needs_password {
args.push_str("!pw");
}
if let Some(file_name) = self.file_name {
args.push_str("!name:");
args.push_str(&file_name);
}
if let Some(language) = self.language {
args.push_str("!lang:");
args.push_str(&language);
}
SecretString::new(format!(
"key:{}{}",
self.decryption_key.expose_secret(),
args
))
}
}

View file

@ -1,7 +1,29 @@
#![warn(clippy::nursery, clippy::pedantic)] #![warn(clippy::nursery, clippy::pedantic)]
// False positive: https://github.com/rust-lang/rust-clippy/issues/6902
#![allow(clippy::use_self)]
//! Contains common functions and structures used by multiple projects //! Contains common functions and structures used by multiple projects
// Copyright (c) 2021 Edward Shen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use std::fmt::Display; use std::fmt::Display;
use std::str::FromStr; use std::str::FromStr;
@ -9,134 +31,75 @@ use bytes::Bytes;
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use headers::{Header, HeaderName, HeaderValue}; use headers::{Header, HeaderName, HeaderValue};
use lazy_static::lazy_static; use lazy_static::lazy_static;
pub use secrecy;
use secrecy::Secret;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error; use thiserror::Error;
pub use url::Url; pub use url::Url;
use crate::crypto::{Key, Nonce}; use crate::crypto::Key;
pub mod base64 { pub mod base64;
/// URL-safe Base64 encoding. pub mod crypto;
pub fn encode(input: impl AsRef<[u8]>) -> String { pub mod fragment;
base64::encode_config(input, base64::URL_SAFE)
}
/// URL-safe Base64 decoding. pub const API_ENDPOINT: &str = "/api";
pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, base64::DecodeError> {
base64::decode_config(input, base64::URL_SAFE)
}
}
/// Hashes an input to output a usable key.
pub fn hash(data: impl AsRef<[u8]>) -> crypto::Key {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize()
}
pub mod crypto {
use std::ops::{Deref, DerefMut};
use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::aead::{Aead, AeadInPlace, Buffer, Error, NewAead};
use chacha20poly1305::XChaCha20Poly1305;
use chacha20poly1305::XNonce;
use rand::{thread_rng, Rng};
pub use chacha20poly1305::Key;
/// Securely generates a random key and nonce.
#[must_use]
pub fn gen_key_nonce() -> (Key, Nonce) {
let mut rng = thread_rng();
let mut key: Key = GenericArray::default();
rng.fill(key.as_mut_slice());
let mut nonce = Nonce::default();
rng.fill(nonce.as_mut_slice());
(key, nonce)
}
pub fn seal(plaintext: &[u8], nonce: &Nonce, key: &Key) -> Result<Vec<u8>, Error> {
let cipher = XChaCha20Poly1305::new(key);
cipher.encrypt(nonce, plaintext)
}
pub fn seal_in_place(buffer: &mut impl Buffer, nonce: &Nonce, key: &Key) -> Result<(), Error> {
let cipher = XChaCha20Poly1305::new(key);
cipher.encrypt_in_place(nonce, &[], buffer)
}
pub fn open(encrypted: &[u8], nonce: &Nonce, key: &Key) -> Result<Vec<u8>, Error> {
let cipher = XChaCha20Poly1305::new(key);
cipher.decrypt(nonce, encrypted)
}
pub fn open_in_place(buffer: &mut impl Buffer, nonce: &Nonce, key: &Key) -> Result<(), Error> {
let cipher = XChaCha20Poly1305::new(key);
cipher.decrypt_in_place(nonce, &[], buffer)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Nonce(XNonce);
impl Default for Nonce {
fn default() -> Self {
Self(GenericArray::default())
}
}
impl Deref for Nonce {
type Target = XNonce;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Nonce {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl AsRef<[u8]> for Nonce {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl Nonce {
#[must_use]
pub fn increment(&self) -> Self {
let mut inner = self.0;
inner.as_mut_slice()[0] += 1;
Self(inner)
}
#[must_use]
pub fn from_slice(slice: &[u8]) -> Self {
Self(*XNonce::from_slice(slice))
}
}
}
pub struct ParsedUrl { pub struct ParsedUrl {
pub sanitized_url: Url, pub sanitized_url: Url,
pub decryption_key: Key, pub decryption_key: Secret<Key>,
pub nonce: Nonce,
pub needs_password: bool, pub needs_password: bool,
} }
#[derive(Default)] #[derive(Default, Debug)]
pub struct PartialParsedUrl { pub struct PartialParsedUrl {
pub decryption_key: Option<Key>, pub decryption_key: Option<Secret<Key>>,
pub nonce: Option<Nonce>,
pub needs_password: bool, pub needs_password: bool,
pub name: Option<String>,
pub language: Option<String>,
} }
impl From<&str> for PartialParsedUrl { #[cfg(test)]
fn from(fragment: &str) -> Self { impl PartialEq for PartialParsedUrl {
fn eq(&self, other: &Self) -> bool {
use secrecy::ExposeSecret;
let decryption_key_matches = {
match (self.decryption_key.as_ref(), other.decryption_key.as_ref()) {
(Some(key), Some(other)) => key.expose_secret() == other.expose_secret(),
(None, None) => true,
_ => false,
}
};
decryption_key_matches && self.needs_password == other.needs_password
}
}
#[derive(Error, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum PartialParsedUrlParseError {
#[error("A decryption key that was not valid web base64 was provided.")]
InvalidDecryptionKey,
}
impl TryFrom<&str> for PartialParsedUrl {
type Error = PartialParsedUrlParseError;
fn try_from(fragment: &str) -> Result<Self, Self::Error> {
// Short circuit if the fragment only contains the key.
// Base64 has an interesting property that the length of an encoded text
// is always 4/3rds larger than the original data.
if !fragment.contains("key:") {
let decryption_key = base64::decode(fragment)
.map_err(|_| PartialParsedUrlParseError::InvalidDecryptionKey)?;
let decryption_key = Key::new_secret(decryption_key);
return Ok(Self {
decryption_key,
..Self::default()
});
}
let args = fragment.split('!').filter_map(|kv| { let args = fragment.split('!').filter_map(|kv| {
let (k, v) = { let (k, v) = {
let mut iter = kv.split(':'); let mut iter = kv.split(':');
@ -148,28 +111,39 @@ impl From<&str> for PartialParsedUrl {
let mut decryption_key = None; let mut decryption_key = None;
let mut needs_password = false; let mut needs_password = false;
let mut nonce = None; let mut name = None;
let mut language = None;
for (key, value) in args { for (key, value) in args {
match (key, value) { match (key, value) {
("key", Some(value)) => { ("key", Some(value)) => {
decryption_key = dbg!(base64::decode(value).map(|k| *Key::from_slice(&k)).ok()); let key = base64::decode(value)
.map_err(|_| PartialParsedUrlParseError::InvalidDecryptionKey)?;
decryption_key = Key::new_secret(key);
} }
("pw", _) => { ("pw", _) => {
needs_password = true; needs_password = true;
} }
("nonce", Some(value)) => { ("name", Some(provided_name)) => name = Some(provided_name.to_owned()),
nonce = dbg!(base64::decode(value).as_deref().map(Nonce::from_slice).ok()); ("lang", Some(provided_lang)) => language = Some(provided_lang.to_owned()),
}
_ => (), _ => (),
} }
} }
Self { Ok(Self {
decryption_key, decryption_key,
nonce,
needs_password, needs_password,
} name,
language,
})
}
}
impl FromStr for PartialParsedUrl {
type Err = PartialParsedUrlParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
} }
} }
@ -179,10 +153,8 @@ pub enum ParseUrlError {
BadUrl, BadUrl,
#[error("Missing decryption key")] #[error("Missing decryption key")]
NeedKey, NeedKey,
#[error("Missing nonce")] #[error(transparent)]
NeedNonce, InvalidKey(#[from] PartialParsedUrlParseError),
#[error("Missing decryption key and nonce")]
NeedKeyAndNonce,
} }
impl FromStr for ParsedUrl { impl FromStr for ParsedUrl {
@ -190,31 +162,25 @@ impl FromStr for ParsedUrl {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut url = Url::from_str(s).map_err(|_| ParseUrlError::BadUrl)?; let mut url = Url::from_str(s).map_err(|_| ParseUrlError::BadUrl)?;
let fragment = url.fragment().ok_or(ParseUrlError::NeedKeyAndNonce)?; let fragment = url.fragment().ok_or(ParseUrlError::NeedKey)?;
if fragment.is_empty() { if fragment.is_empty() {
return Err(ParseUrlError::NeedKeyAndNonce); return Err(ParseUrlError::NeedKey);
} }
let PartialParsedUrl { let PartialParsedUrl {
decryption_key, mut decryption_key,
needs_password, needs_password,
nonce, ..
} = PartialParsedUrl::from(fragment); } = PartialParsedUrl::try_from(fragment)?;
url.set_fragment(None); url.set_fragment(None);
let (decryption_key, nonce) = match (&decryption_key, nonce) { let decryption_key = decryption_key.take().ok_or(ParseUrlError::NeedKey)?;
(None, None) => Err(ParseUrlError::NeedKeyAndNonce),
(None, Some(_)) => Err(ParseUrlError::NeedKey),
(Some(_), None) => Err(ParseUrlError::NeedNonce),
(Some(k), Some(v)) => Ok((*k, v)),
}?;
Ok(Self { Ok(Self {
sanitized_url: url, sanitized_url: url,
decryption_key, decryption_key,
needs_password, needs_password,
nonce,
}) })
} }
} }
@ -222,16 +188,55 @@ impl FromStr for ParsedUrl {
#[derive(Serialize, Deserialize, Clone, Copy, Debug)] #[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub enum Expiration { pub enum Expiration {
BurnAfterReading, BurnAfterReading,
BurnAfterReadingWithDeadline(DateTime<Utc>),
UnixTime(DateTime<Utc>), UnixTime(DateTime<Utc>),
} }
// This impl is used for the CLI. We use a macro here to ensure that possible
// expressed by the CLI are the same supported by the server.
macro_rules! expiration_from_str {
{
$($str_repr:literal => $duration:expr),* $(,)?
} => {
impl FromStr for Expiration {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
$($str_repr => Ok($duration),)*
_ => Err(s.to_owned()),
}
}
}
impl Expiration {
#[must_use]
pub const fn variants() -> &'static [&'static str] {
&[
$($str_repr,)*
]
}
}
};
}
expiration_from_str! {
"read" => Self::BurnAfterReading,
"5m" => Self::UnixTime(Utc::now() + Duration::minutes(5)),
"10m" => Self::UnixTime(Utc::now() + Duration::minutes(10)),
"1h" => Self::UnixTime(Utc::now() + Duration::hours(1)),
"1d" => Self::UnixTime(Utc::now() + Duration::days(1)),
"3d" => Self::UnixTime(Utc::now() + Duration::days(1)),
"1w" => Self::UnixTime(Utc::now() + Duration::weeks(1)),
}
impl Display for Expiration { impl Display for Expiration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Expiration::BurnAfterReading => { Self::BurnAfterReading | Self::BurnAfterReadingWithDeadline(_) => {
write!(f, "This item has been burned. You now have the only copy.") write!(f, "This item has been burned. You now have the only copy.")
} }
Expiration::UnixTime(time) => write!( Self::UnixTime(time) => write!(
f, f,
"{}", "{}",
time.format("This item will expire on %A, %B %-d, %Y at %T %Z.") time.format("This item will expire on %A, %B %-d, %Y at %T %Z.")
@ -246,7 +251,7 @@ lazy_static! {
impl Header for Expiration { impl Header for Expiration {
fn name() -> &'static HeaderName { fn name() -> &'static HeaderName {
&*EXPIRATION_HEADER_NAME &EXPIRATION_HEADER_NAME
} }
fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error> fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
@ -254,19 +259,9 @@ impl Header for Expiration {
Self: Sized, Self: Sized,
I: Iterator<Item = &'i HeaderValue>, I: Iterator<Item = &'i HeaderValue>,
{ {
match values let bytes = values.next().ok_or_else(headers::Error::invalid)?;
.next()
.ok_or_else(headers::Error::invalid)? Self::try_from(bytes).map_err(|_| headers::Error::invalid())
.as_bytes()
{
b"read" => Ok(Self::BurnAfterReading),
b"5m" => Ok(Self::UnixTime(Utc::now() + Duration::minutes(5))),
b"10m" => Ok(Self::UnixTime(Utc::now() + Duration::minutes(10))),
b"1h" => Ok(Self::UnixTime(Utc::now() + Duration::hours(1))),
b"1d" => Ok(Self::UnixTime(Utc::now() + Duration::days(1))),
// We disallow permanent pastes
_ => Err(headers::Error::invalid()),
}
} }
fn encode<E: Extend<HeaderValue>>(&self, container: &mut E) { fn encode<E: Extend<HeaderValue>>(&self, container: &mut E) {
@ -280,7 +275,9 @@ impl From<&Expiration> for HeaderValue {
// so we don't need the extra check. // so we don't need the extra check.
unsafe { unsafe {
Self::from_maybe_shared_unchecked(match expiration { Self::from_maybe_shared_unchecked(match expiration {
Expiration::BurnAfterReading => Bytes::from_static(b"0"), Expiration::BurnAfterReadingWithDeadline(_) | Expiration::BurnAfterReading => {
Bytes::from_static(b"0")
}
Expiration::UnixTime(duration) => Bytes::from(duration.to_rfc3339()), Expiration::UnixTime(duration) => Bytes::from(duration.to_rfc3339()),
}) })
} }
@ -288,34 +285,41 @@ impl From<&Expiration> for HeaderValue {
} }
impl From<Expiration> for HeaderValue { impl From<Expiration> for HeaderValue {
// False positive: https://github.com/rust-lang/rust-clippy/issues/9095
#[allow(clippy::needless_borrow)]
fn from(expiration: Expiration) -> Self { fn from(expiration: Expiration) -> Self {
(&expiration).into() (&expiration).into()
} }
} }
#[cfg(feature = "wasm")] pub struct ParseHeaderValueError;
impl TryFrom<web_sys::Headers> for Expiration {
// #[cfg(feature = "wasm")]
// impl TryFrom<reqwest::header::HeaderMap<&str>> for Expiration {
// type Error = ParseHeaderValueError;
// fn try_from(headers: reqwest::header::HeaderMap) -> Result<Self, Self::Error> {
// headers
// .get(http::header::EXPIRES.as_str())
// .as_deref()
// .and_then(|v| Self::try_from(v).ok())
// .ok_or(ParseHeaderValueError)
// }
// }
impl TryFrom<HeaderValue> for Expiration {
type Error = ParseHeaderValueError; type Error = ParseHeaderValueError;
fn try_from(headers: web_sys::Headers) -> Result<Self, Self::Error> { fn try_from(value: HeaderValue) -> Result<Self, Self::Error> {
headers Self::try_from(&value)
.get(http::header::EXPIRES.as_str())
.ok()
.flatten()
.as_deref()
.and_then(|v| Self::try_from(v).ok())
.ok_or(ParseHeaderValueError)
} }
} }
pub struct ParseHeaderValueError;
impl TryFrom<&HeaderValue> for Expiration { impl TryFrom<&HeaderValue> for Expiration {
type Error = ParseHeaderValueError; type Error = ParseHeaderValueError;
fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> { fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
value std::str::from_utf8(value.as_bytes())
.to_str()
.map_err(|_| ParseHeaderValueError) .map_err(|_| ParseHeaderValueError)
.and_then(Self::try_from) .and_then(Self::try_from)
} }
@ -325,6 +329,10 @@ impl TryFrom<&str> for Expiration {
type Error = ParseHeaderValueError; type Error = ParseHeaderValueError;
fn try_from(value: &str) -> Result<Self, Self::Error> { fn try_from(value: &str) -> Result<Self, Self::Error> {
if value == "0" {
return Ok(Self::BurnAfterReading);
}
value value
.parse::<DateTime<Utc>>() .parse::<DateTime<Utc>>()
.map_err(|_| ParseHeaderValueError) .map_err(|_| ParseHeaderValueError)
@ -337,3 +345,120 @@ impl Default for Expiration {
Self::UnixTime(Utc::now() + Duration::days(1)) Self::UnixTime(Utc::now() + Duration::days(1))
} }
} }
#[cfg(test)]
mod partial_parsed_url_parsing {
use secrecy::Secret;
use crate::base64;
use crate::crypto::Key;
use crate::PartialParsedUrl;
#[test]
fn empty() {
assert_eq!("".parse(), Ok(PartialParsedUrl::default()));
}
const DECRYPTION_KEY_STRING: &str = "ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=";
fn decryption_key() -> Option<Secret<Key>> {
Key::new_secret(base64::decode(DECRYPTION_KEY_STRING).unwrap())
}
#[test]
fn clean_no_password() {
assert_eq!(
DECRYPTION_KEY_STRING.parse(),
Ok(PartialParsedUrl {
decryption_key: decryption_key(),
..Default::default()
})
);
}
#[test]
fn no_password() {
let input = "key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=";
assert_eq!(
input.parse(),
Ok(PartialParsedUrl {
decryption_key: decryption_key(),
..Default::default()
})
);
}
#[test]
fn with_password() {
let input = "key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=!pw";
assert_eq!(
input.parse(),
Ok(PartialParsedUrl {
decryption_key: decryption_key(),
needs_password: true,
..Default::default()
})
);
}
#[test]
fn with_name() {
let input = "key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=!name:test_file.rs";
assert_eq!(
input.parse(),
Ok(PartialParsedUrl {
decryption_key: decryption_key(),
name: Some("test_file.rs".to_owned()),
..Default::default()
})
);
}
#[test]
fn with_lang() {
let input = "key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=!lang:rust";
assert_eq!(
input.parse(),
Ok(PartialParsedUrl {
decryption_key: decryption_key(),
language: Some("rust".to_owned()),
..Default::default()
})
);
}
#[test]
fn order_does_not_matter() {
let input = "pw!key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=";
assert_eq!(
input.parse(),
Ok(PartialParsedUrl {
decryption_key: decryption_key(),
needs_password: true,
..Default::default()
})
);
}
#[test]
fn empty_key_pair_gracefully_fails() {
let input = "!!!key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=!!!";
assert_eq!(
input.parse(),
Ok(PartialParsedUrl {
decryption_key: decryption_key(),
..Default::default()
})
);
}
#[test]
fn invalid_decryption_key_fails() {
assert!("invalid key".parse::<PartialParsedUrl>().is_err());
}
#[test]
fn unknown_fields_fail() {
assert!("!!a!!b!!c".parse::<PartialParsedUrl>().is_err());
}
}

27
package.json Normal file
View file

@ -0,0 +1,27 @@
{
"devDependencies": {
"@swc/cli": "^0.1.51",
"@swc/core": "^1.2.102",
"@types/react-dom": "^17.0.11",
"@wasm-tool/wasm-pack-plugin": "^1.6.0",
"css-loader": "^6.5.1",
"html-webpack-plugin": "^5.5.0",
"sass": "^1.48.0",
"sass-loader": "^12.4.0",
"style-loader": "^3.3.1",
"swc-loader": "^0.1.15",
"webpack": "^5.66.0",
"webpack-cli": "^4.9.1"
},
"dependencies": {
"highlight.js": "^11.4.0",
"highlightjs-line-numbers.js": "^2.8.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"source-map-loader": "^4.0.0"
},
"scripts": {
"build": "webpack --mode production",
"clean": "(git rev-parse --show-toplevel && rm -rf node_modules dist web/pkg)"
}
}

View file

@ -7,18 +7,24 @@ edition = "2021"
[dependencies] [dependencies]
omegaupload-common = { path = "../common" } omegaupload-common = { path = "../common" }
anyhow = "1" anyhow = "1.0.58"
axum = { version = "0.2", features = ["http2", "headers"] } axum = { version = "0.6", features = ["http2", "headers"] }
bincode = "1" bincode = "1.3.3"
# We don't care about which version (We want to match with axum), we just need # We don't care about which version (We want to match with axum), we just need
# to enable the feature # to enable the feature
bytes = { version = "*", features = ["serde"] } bytes = { version = "1.2.0", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }
futures = "0.3.21"
# We just need to pull in whatever axum is pulling in # We just need to pull in whatever axum is pulling in
headers = "*" headers = "0.3.7"
rand = "0.8" lazy_static = "1.4.0"
rocksdb = { version = "0.17", default_features = false, features = ["zstd"] } # Disable `random()` and `thread_rng()`
serde = { version = "1", features = ["derive"] } rand = { version = "0.8.5", default-features = false }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } rocksdb = { version = "0.21", default-features = false, features = ["zstd"] }
tracing = { version = "0.1" } serde = { version = "1.0.140", features = ["derive"] }
tracing-subscriber = "0.2" signal-hook = "0.3.14"
signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] }
tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.4", features = ["fs"] }
tracing = "0.1.35"
tracing-subscriber = "0.3.15"

3
server/README.md Normal file
View file

@ -0,0 +1,3 @@
This server responds to four paths:
`GET /`

View file

@ -1,23 +1,46 @@
#![warn(clippy::nursery, clippy::pedantic)] #![warn(clippy::nursery, clippy::pedantic)]
// OmegaUpload Zero Knowledge File Hosting
// Copyright (C) 2021 Edward Shen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::convert::Infallible;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use axum::body::Bytes; use axum::body::Bytes;
use axum::error_handling::HandleError;
use axum::extract::{Extension, Path, TypedHeader}; use axum::extract::{Extension, Path, TypedHeader};
use axum::handler::{get, post};
use axum::http::header::EXPIRES; use axum::http::header::EXPIRES;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::{AddExtensionLayer, Router}; use axum::routing::{get, get_service, post};
use axum::Router;
use chrono::Utc; use chrono::Utc;
use futures::stream::StreamExt;
use headers::HeaderMap; use headers::HeaderMap;
use omegaupload_common::Expiration; use lazy_static::lazy_static;
use rand::thread_rng; use omegaupload_common::crypto::get_csrng;
use omegaupload_common::{Expiration, API_ENDPOINT};
use rand::Rng; use rand::Rng;
use rocksdb::{ColumnFamilyDescriptor, IteratorMode}; use rocksdb::{ColumnFamilyDescriptor, IteratorMode};
use rocksdb::{Options, DB}; use rocksdb::{Options, DB};
use tokio::task; use signal_hook::consts::SIGUSR1;
use signal_hook_tokio::Signals;
use tokio::task::{self, JoinHandle};
use tower_http::services::{ServeDir, ServeFile};
use tracing::{error, instrument, trace}; use tracing::{error, instrument, trace};
use tracing::{info, warn}; use tracing::{info, warn};
@ -28,6 +51,10 @@ mod short_code;
const BLOB_CF_NAME: &str = "blob"; const BLOB_CF_NAME: &str = "blob";
const META_CF_NAME: &str = "meta"; const META_CF_NAME: &str = "meta";
lazy_static! {
static ref MAX_PASTE_AGE: chrono::Duration = chrono::Duration::days(1);
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
const PASTE_DB_PATH: &str = "database"; const PASTE_DB_PATH: &str = "database";
@ -48,27 +75,50 @@ async fn main() -> Result<()> {
], ],
)?); )?);
set_up_expirations(&db); set_up_expirations::<SHORT_CODE_SIZE>(&db);
axum::Server::bind(&"0.0.0.0:8081".parse()?) let signals = Signals::new(&[SIGUSR1])?;
.serve( let signals_handle = signals.handle();
let signals_task = tokio::spawn(handle_signals(signals, Arc::clone(&db)));
let root_service = HandleError::new(get_service(ServeDir::new("static")), |_| async {
Ok::<_, Infallible>(StatusCode::NOT_FOUND)
});
let index_service = HandleError::new(get_service(ServeFile::new("index.html")), |_| async {
Ok::<_, Infallible>(StatusCode::NOT_FOUND)
});
axum::Server::bind(&"0.0.0.0:8080".parse()?)
.serve({
info!("Now serving on 0.0.0.0:8080");
Router::new() Router::new()
.route("/", post(upload::<SHORT_CODE_SIZE>))
.route( .route(
"/:code", "/",
post(upload::<SHORT_CODE_SIZE>).get_service(index_service.clone()),
)
.route_service("/:code", index_service)
.nest_service("/static", root_service)
.route(
&format!("{API_ENDPOINT}/:code"),
get(paste::<SHORT_CODE_SIZE>).delete(delete::<SHORT_CODE_SIZE>), get(paste::<SHORT_CODE_SIZE>).delete(delete::<SHORT_CODE_SIZE>),
) )
.layer(AddExtensionLayer::new(db)) .layer(axum::Extension(db))
.into_make_service(), .into_make_service()
) })
.await?; .await?;
// Must be called for correct shutdown // Must be called for correct shutdown
DB::destroy(&Options::default(), PASTE_DB_PATH)?; DB::destroy(&Options::default(), PASTE_DB_PATH)?;
signals_handle.close();
signals_task.await?;
Ok(()) Ok(())
} }
fn set_up_expirations(db: &Arc<DB>) { // See https://link.eddie.sh/5JHlD
#[allow(clippy::cognitive_complexity)]
fn set_up_expirations<const N: usize>(db: &Arc<DB>) {
let mut corrupted = 0; let mut corrupted = 0;
let mut expired = 0; let mut expired = 0;
let mut pending = 0; let mut pending = 0;
@ -79,43 +129,37 @@ fn set_up_expirations(db: &Arc<DB>) {
let db_ref = Arc::clone(db); let db_ref = Arc::clone(db);
let delete_entry = move |key: &[u8]| { for item in db.iterator_cf(meta_cf, IteratorMode::Start) {
let blob_cf = db_ref.cf_handle(BLOB_CF_NAME).unwrap(); let (key, value) = item.unwrap();
let meta_cf = db_ref.cf_handle(META_CF_NAME).unwrap(); let key: [u8; N] = (*key).try_into().unwrap();
if let Err(e) = db_ref.delete_cf(blob_cf, &key) {
warn!("{}", e);
}
if let Err(e) = db_ref.delete_cf(meta_cf, &key) {
warn!("{}", e);
}
};
for (key, value) in db.iterator_cf(meta_cf, IteratorMode::Start) {
let expiration = if let Ok(value) = bincode::deserialize::<Expiration>(&value) { let expiration = if let Ok(value) = bincode::deserialize::<Expiration>(&value) {
value value
} else { } else {
corrupted += 1; corrupted += 1;
delete_entry(&key); delete_entry(Arc::clone(&db_ref), key);
continue; continue;
}; };
let expiration_time = match expiration { let expiration_time = match expiration {
Expiration::BurnAfterReading => { Expiration::BurnAfterReading => {
panic!("Got burn after reading expiration time? Invariant violated"); warn!("Found unbounded burn after reading. Defaulting to max age");
Utc::now() + *MAX_PASTE_AGE
} }
Expiration::BurnAfterReadingWithDeadline(deadline) => deadline,
Expiration::UnixTime(time) => time, Expiration::UnixTime(time) => time,
}; };
let sleep_duration = (expiration_time - Utc::now()).to_std().unwrap_or_default(); let sleep_duration = (expiration_time - Utc::now()).to_std().unwrap_or_default();
if sleep_duration == Duration::default() { if sleep_duration == Duration::default() {
expired += 1; expired += 1;
delete_entry(&key); delete_entry(Arc::clone(&db_ref), key);
} else { } else {
pending += 1; pending += 1;
let delete_entry_ref = delete_entry.clone(); let db = Arc::clone(&db_ref);
task::spawn_blocking(move || async move { task::spawn(async move {
tokio::time::sleep(sleep_duration).await; tokio::time::sleep(sleep_duration).await;
delete_entry_ref(&key); delete_entry(db, key);
}); });
} }
} }
@ -123,14 +167,26 @@ fn set_up_expirations(db: &Arc<DB>) {
if corrupted == 0 { if corrupted == 0 {
info!("No corrupted pastes found."); info!("No corrupted pastes found.");
} else { } else {
warn!("Found {} corrupted pastes.", corrupted); warn!("Found {corrupted} corrupted pastes.");
} }
info!("Found {} expired pastes.", expired); info!("Found {expired} expired pastes.");
info!("Found {} active pastes.", pending); info!("Found {pending} active pastes.");
info!("Cleanup timers have been initialized."); info!("Cleanup timers have been initialized.");
} }
async fn handle_signals(mut signals: Signals, db: Arc<DB>) {
while let Some(signal) = signals.next().await {
if signal == SIGUSR1 {
let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
info!(
"Active paste count: {}",
db.iterator_cf(meta_cf, IteratorMode::Start).count()
);
}
}
}
#[instrument(skip(db, body), err)] #[instrument(skip(db, body), err)]
async fn upload<const N: usize>( async fn upload<const N: usize>(
Extension(db): Extension<Arc<DB>>, Extension(db): Extension<Arc<DB>>,
@ -141,6 +197,15 @@ async fn upload<const N: usize>(
return Err(StatusCode::BAD_REQUEST); return Err(StatusCode::BAD_REQUEST);
} }
if let Some(header) = maybe_expires {
if let Expiration::UnixTime(time) = header.0 {
if (time - Utc::now()) > *MAX_PASTE_AGE {
warn!("{time} exceeds allowed paste lifetime");
return Err(StatusCode::BAD_REQUEST);
}
}
}
// 3GB max; this is a soft-limit of RocksDb // 3GB max; this is a soft-limit of RocksDb
if body.len() >= 3_221_225_472 { if body.len() >= 3_221_225_472 {
return Err(StatusCode::PAYLOAD_TOO_LARGE); return Err(StatusCode::PAYLOAD_TOO_LARGE);
@ -153,7 +218,7 @@ async fn upload<const N: usize>(
// Try finding a code; give up after 1000 attempts // Try finding a code; give up after 1000 attempts
// Statistics show that this is very unlikely to happen // Statistics show that this is very unlikely to happen
for i in 0..1000 { for i in 0..1000 {
let code: ShortCode<N> = thread_rng().sample(short_code::Generator); let code: ShortCode<N> = get_csrng().sample(short_code::Generator);
let db = Arc::clone(&db); let db = Arc::clone(&db);
let key = code.as_bytes(); let key = code.as_bytes();
let query = task::spawn_blocking(move || { let query = task::spawn_blocking(move || {
@ -162,7 +227,7 @@ async fn upload<const N: usize>(
.await; .await;
if matches!(query, Ok(false)) { if matches!(query, Ok(false)) {
new_key = Some(key); new_key = Some(key);
trace!("Found new key after {} attempts.", i); trace!("Found new key after {i} attempts.");
break; break;
} }
} }
@ -174,10 +239,6 @@ async fn upload<const N: usize>(
return Err(StatusCode::INTERNAL_SERVER_ERROR); return Err(StatusCode::INTERNAL_SERVER_ERROR);
}; };
trace!("Serializing paste...");
trace!("Finished serializing paste.");
let db_ref = Arc::clone(&db); let db_ref = Arc::clone(&db);
match task::spawn_blocking(move || { match task::spawn_blocking(move || {
let blob_cf = db_ref.cf_handle(BLOB_CF_NAME).unwrap(); let blob_cf = db_ref.cf_handle(BLOB_CF_NAME).unwrap();
@ -185,6 +246,11 @@ async fn upload<const N: usize>(
let data = bincode::serialize(&body).expect("bincode to serialize"); let data = bincode::serialize(&body).expect("bincode to serialize");
db_ref.put_cf(blob_cf, key, data)?; db_ref.put_cf(blob_cf, key, data)?;
let expires = maybe_expires.map(|v| v.0).unwrap_or_default(); let expires = maybe_expires.map(|v| v.0).unwrap_or_default();
let expires = if let Expiration::BurnAfterReading = expires {
Expiration::BurnAfterReadingWithDeadline(Utc::now() + *MAX_PASTE_AGE)
} else {
expires
};
let meta = bincode::serialize(&expires).expect("bincode to serialize"); let meta = bincode::serialize(&expires).expect("bincode to serialize");
if db_ref.put_cf(meta_cf, key, meta).is_err() { if db_ref.put_cf(meta_cf, key, meta).is_err() {
// try and roll back on metadata write failure // try and roll back on metadata write failure
@ -196,26 +262,20 @@ async fn upload<const N: usize>(
{ {
Ok(Ok(_)) => { Ok(Ok(_)) => {
if let Some(expires) = maybe_expires { if let Some(expires) = maybe_expires {
if let Expiration::UnixTime(expiration_time) = expires.0 { if let Expiration::UnixTime(expiration_time)
| Expiration::BurnAfterReadingWithDeadline(expiration_time) = expires.0
{
let sleep_duration = let sleep_duration =
(expiration_time - Utc::now()).to_std().unwrap_or_default(); (expiration_time - Utc::now()).to_std().unwrap_or_default();
task::spawn(async move {
task::spawn_blocking(move || async move {
tokio::time::sleep(sleep_duration).await; tokio::time::sleep(sleep_duration).await;
let blob_cf = db.cf_handle(BLOB_CF_NAME).unwrap(); delete_entry(db, key);
let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
if let Err(e) = db.delete_cf(blob_cf, key) {
warn!("{}", e);
}
if let Err(e) = db.delete_cf(meta_cf, key) {
warn!("{}", e);
}
}); });
} }
} }
} }
e => { e => {
error!("Failed to insert paste into db: {:?}", e); error!("Failed to insert paste into db: {e:?}");
return Err(StatusCode::INTERNAL_SERVER_ERROR); return Err(StatusCode::INTERNAL_SERVER_ERROR);
} }
} }
@ -233,7 +293,7 @@ async fn paste<const N: usize>(
let metadata: Expiration = { let metadata: Expiration = {
let meta_cf = db.cf_handle(META_CF_NAME).unwrap(); let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
let query_result = db.get_cf(meta_cf, key).map_err(|e| { let query_result = db.get_cf(meta_cf, key).map_err(|e| {
error!("Failed to fetch initial query: {}", e); error!("Failed to fetch initial query: {e}");
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })?;
@ -251,22 +311,10 @@ async fn paste<const N: usize>(
// Check if paste has expired. // Check if paste has expired.
if let Expiration::UnixTime(expires) = metadata { if let Expiration::UnixTime(expires) = metadata {
if expires < Utc::now() { if expires < Utc::now() {
task::spawn_blocking(move || { delete_entry(db, url.as_bytes()).await.map_err(|e| {
let blob_cf = db.cf_handle(BLOB_CF_NAME).unwrap(); error!("Failed to join handle: {e}");
let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
if let Err(e) = db.delete_cf(blob_cf, &key) {
warn!("{}", e);
}
if let Err(e) = db.delete_cf(meta_cf, &key) {
warn!("{}", e);
}
})
.await
.map_err(|e| {
error!("Failed to join handle: {}", e);
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })??;
return Err(StatusCode::NOT_FOUND); return Err(StatusCode::NOT_FOUND);
} }
} }
@ -275,7 +323,7 @@ async fn paste<const N: usize>(
// not sure if perf of get_pinned is better than spawn_blocking // not sure if perf of get_pinned is better than spawn_blocking
let blob_cf = db.cf_handle(BLOB_CF_NAME).unwrap(); let blob_cf = db.cf_handle(BLOB_CF_NAME).unwrap();
let query_result = db.get_pinned_cf(blob_cf, key).map_err(|e| { let query_result = db.get_pinned_cf(blob_cf, key).map_err(|e| {
error!("Failed to fetch initial query: {}", e); error!("Failed to fetch initial query: {e}");
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })?;
@ -291,18 +339,14 @@ async fn paste<const N: usize>(
}; };
// Check if we need to burn after read // Check if we need to burn after read
if matches!(metadata, Expiration::BurnAfterReading) { if matches!(
let join_handle = task::spawn_blocking(move || db.delete(key)) metadata,
.await Expiration::BurnAfterReading | Expiration::BurnAfterReadingWithDeadline(_)
.map_err(|e| { ) {
error!("Failed to join handle: {}", e); delete_entry(db, key).await.map_err(|e| {
StatusCode::INTERNAL_SERVER_ERROR error!("Failed to join handle: {e}");
})?;
join_handle.map_err(|e| {
error!("Failed to burn paste after read: {}", e);
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })??;
} }
let mut map = HeaderMap::new(); let mut map = HeaderMap::new();
@ -316,24 +360,24 @@ async fn delete<const N: usize>(
Extension(db): Extension<Arc<DB>>, Extension(db): Extension<Arc<DB>>,
Path(url): Path<ShortCode<N>>, Path(url): Path<ShortCode<N>>,
) -> StatusCode { ) -> StatusCode {
match task::spawn_blocking(move || { match delete_entry(db, url.as_bytes()).await {
let blob_cf = db.cf_handle(BLOB_CF_NAME).unwrap();
let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
if let Err(e) = db.delete_cf(blob_cf, url.as_bytes()) {
warn!("{}", e);
return Err(());
}
if let Err(e) = db.delete_cf(meta_cf, url.as_bytes()) {
warn!("{}", e);
return Err(());
}
Ok(())
})
.await
{
Ok(_) => StatusCode::OK, Ok(_) => StatusCode::OK,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
} }
} }
fn delete_entry<const N: usize>(db: Arc<DB>, key: [u8; N]) -> JoinHandle<Result<(), StatusCode>> {
task::spawn_blocking(move || {
let blob_cf = db.cf_handle(BLOB_CF_NAME).unwrap();
let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
if let Err(e) = db.delete_cf(blob_cf, &key) {
warn!("{e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
if let Err(e) = db.delete_cf(meta_cf, &key) {
warn!("{e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
Ok(())
})
}

View file

@ -1,6 +1,23 @@
// OmegaUpload Zero Knowledge File Hosting
// Copyright (C) 2021 Edward Shen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fmt::Debug; use std::fmt::Debug;
use rand::prelude::Distribution; use rand::prelude::Distribution;
use rand::Rng;
use serde::de::{Unexpected, Visitor}; use serde::de::{Unexpected, Visitor};
use serde::Deserialize; use serde::Deserialize;
@ -109,7 +126,7 @@ pub struct Generator;
const ALPHABET: &[u8; 32] = b"23456789CFGHJMPQRVWXcfghjmpqrvwx"; const ALPHABET: &[u8; 32] = b"23456789CFGHJMPQRVWXcfghjmpqrvwx";
impl Distribution<ShortCodeChar> for Generator { impl Distribution<ShortCodeChar> for Generator {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> ShortCodeChar { fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> ShortCodeChar {
let value = rng.gen_range(0..32); let value = rng.gen_range(0..32);
assert!(value < 32); assert!(value < 32);
ShortCodeChar(ALPHABET[value] as char) ShortCodeChar(ALPHABET[value] as char)
@ -117,7 +134,7 @@ impl Distribution<ShortCodeChar> for Generator {
} }
impl<const N: usize> Distribution<ShortCode<N>> for Generator { impl<const N: usize> Distribution<ShortCode<N>> for Generator {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> ShortCode<N> { fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> ShortCode<N> {
let mut arr = [ShortCodeChar('\0'); N]; let mut arr = [ShortCodeChar('\0'); N];
for c in arr.iter_mut() { for c in arr.iter_mut() {

442
test/0000-test-patch.patch Normal file
View file

@ -0,0 +1,442 @@
From 960344b240161b36cca35c22b6a685162b0f217e Mon Sep 17 00:00:00 2001
From: William Tan <code@wtan.me>
Date: Tue, 11 Jan 2022 22:31:18 -0500
Subject: [PATCH] Update dependencies
---
Cargo.lock | 144 ++++++++++++++++++++++---------------------------
cli/Cargo.toml | 4 +-
2 files changed, 65 insertions(+), 83 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index fc97ae7..a16f56f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -28,9 +28,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.51"
+version = "1.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
+checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3"
[[package]]
name = "argon2"
@@ -149,9 +149,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blake2"
-version = "0.10.0"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a58bdf5134c5beae6fc382002c4d88950bad1feea20f8f7165494b6b43b049de"
+checksum = "b94ba84325db59637ffc528bbe8c7f86c02c57cff5c0e2b9b00f9a851f42f309"
dependencies = [
"digest 0.10.1",
]
@@ -176,9 +176,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.8.0"
+version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
+checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]]
name = "byte-unit"
@@ -295,9 +295,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "3.0.0-rc.7"
+version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9468f8012246b0836c6fd11725102b0844254985f2462b6c637d50040ef49df0"
+checksum = "1957aa4a5fb388f0a0a73ce7556c5b42025b874e5cdc2c670775e346e97adec0"
dependencies = [
"atty",
"bitflags",
@@ -312,9 +312,9 @@ dependencies = [
[[package]]
name = "clap_derive"
-version = "3.0.0-rc.7"
+version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b72e1af32a4de4d21a43d26de33fe69c00e895371bd8b1523d674f011b610467"
+checksum = "517358c28fcef6607bf6f76108e02afad7e82297d132a6b846dcc1fc3efcd153"
dependencies = [
"heck",
"proc-macro-error",
@@ -392,9 +392,9 @@ dependencies = [
[[package]]
name = "fixedbitset"
-version = "0.4.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "398ea4fabe40b9b0d885340a2a991a44c8a645624075ad966d21f88688e2b69e"
+checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e"
[[package]]
name = "flate2"
@@ -515,9 +515,9 @@ dependencies = [
[[package]]
name = "generic-array"
-version = "0.14.4"
+version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
"typenum",
"version_check",
@@ -556,9 +556,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.3.9"
+version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd"
+checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689"
dependencies = [
"bytes",
"fnv",
@@ -606,12 +606,9 @@ dependencies = [
[[package]]
name = "heck"
-version = "0.3.3"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
-dependencies = [
- "unicode-segmentation",
-]
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hermit-abi"
@@ -624,13 +621,13 @@ dependencies = [
[[package]]
name = "http"
-version = "0.2.5"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
+checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
dependencies = [
"bytes",
"fnv",
- "itoa 0.4.8",
+ "itoa 1.0.1",
]
[[package]]
@@ -706,9 +703,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "1.7.0"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
dependencies = [
"autocfg",
"hashbrown",
@@ -921,9 +918,9 @@ dependencies = [
[[package]]
name = "num_cpus"
-version = "1.13.0"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [
"hermit-abi",
"libc",
@@ -1086,18 +1083,18 @@ dependencies = [
[[package]]
name = "pin-project"
-version = "1.0.8"
+version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08"
+checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
-version = "1.0.8"
+version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389"
+checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb"
dependencies = [
"proc-macro2",
"quote",
@@ -1106,9 +1103,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
-version = "0.2.7"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
[[package]]
name = "pin-utils"
@@ -1129,9 +1126,9 @@ dependencies = [
[[package]]
name = "ppv-lite86"
-version = "0.2.15"
+version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "proc-macro-error"
@@ -1159,18 +1156,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.34"
+version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
-version = "1.0.10"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
+checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d"
dependencies = [
"proc-macro2",
]
@@ -1258,15 +1255,16 @@ dependencies = [
[[package]]
name = "reqwest"
-version = "0.11.7"
+version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5"
+checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525"
dependencies = [
"base64",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
+ "h2",
"http",
"http-body",
"hyper",
@@ -1343,7 +1341,7 @@ dependencies = [
"log",
"ring",
"sct",
- "webpki 0.22.0",
+ "webpki",
]
[[package]]
@@ -1382,18 +1380,18 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.132"
+version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"
+checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.132"
+version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276"
+checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537"
dependencies = [
"proc-macro2",
"quote",
@@ -1402,9 +1400,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.73"
+version = "1.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
+checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142"
dependencies = [
"itoa 1.0.1",
"ryu",
@@ -1453,9 +1451,9 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
[[package]]
name = "signal-hook"
-version = "0.3.12"
+version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c35dfd12afb7828318348b8c408383cf5071a086c1d4ab1c0f9840ec92dbb922"
+checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d"
dependencies = [
"libc",
"signal-hook-registry",
@@ -1472,9 +1470,9 @@ dependencies = [
[[package]]
name = "signal-hook-tokio"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6c5d32165ff8b94e68e7b3bdecb1b082e958c22434b363482cfb89dcd6f3ff8"
+checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e"
dependencies = [
"futures-core",
"libc",
@@ -1524,9 +1522,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
-version = "1.0.82"
+version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
+checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7"
dependencies = [
"proc-macro2",
"quote",
@@ -1643,7 +1641,7 @@ checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b"
dependencies = [
"rustls",
"tokio",
- "webpki 0.22.0",
+ "webpki",
]
[[package]]
@@ -1794,9 +1792,9 @@ checksum = "e73fc24a5427b3b15e2b0bcad8ef61b5affb1da8ac89c8bf3f196c8692d57f02"
[[package]]
name = "tree_magic_mini"
-version = "3.0.2"
+version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a7581560dc616314f7d73e81419c783d93a92e7fc7331b3041ff57bab240ea6"
+checksum = "91adfd0607cacf6e4babdb870e9bec4037c1c4b151cfd279ccefc5e0c7feaa6d"
dependencies = [
"bytecount",
"fnv",
@@ -1815,9 +1813,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "typenum"
-version = "1.14.0"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicase"
@@ -1843,12 +1841,6 @@ dependencies = [
"tinyvec",
]
-[[package]]
-name = "unicode-segmentation"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
-
[[package]]
name = "unicode-xid"
version = "0.2.2"
@@ -1891,9 +1883,9 @@ checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b"
[[package]]
name = "version_check"
-version = "0.9.3"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "want"
@@ -1989,16 +1981,6 @@ dependencies = [
"wasm-bindgen",
]
-[[package]]
-name = "webpki"
-version = "0.21.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
-dependencies = [
- "ring",
- "untrusted",
-]
-
[[package]]
name = "webpki"
version = "0.22.0"
@@ -2011,11 +1993,11 @@ dependencies = [
[[package]]
name = "webpki-roots"
-version = "0.21.1"
+version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
+checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449"
dependencies = [
- "webpki 0.21.4",
+ "webpki",
]
[[package]]
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 6d3ffe3..b47554d 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -13,6 +13,6 @@ omegaupload-common = "0.1"
anyhow = "1"
atty = "0.2"
-clap = { version = "3.0.0-rc.7", features = ["derive"] }
+clap = { version = "3", features = ["derive"] }
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "blocking"] }
-rpassword = "5"
\ No newline at end of file
+rpassword = "5"
--
2.34.1

41
test/LICENSE.md Normal file
View file

@ -0,0 +1,41 @@
This folder contains mixed media that are under different licenses.
- `music.mp3`, written by Kevin McLeod, was sourced at https://freepd.com under
the CC0 1.0 Universal license. The file was trimmed to the first 30 seconds.
- `movie.mp4` is a 10 second snippet of Big Buck Bunny sourced at
http://bbb3d.renderfarming.net/download.html under the Attribution 3.0
Unported (CC BY 3.0) license.
- `movie.mkv` is identical to `movie.mp4` but transcoded as a `.mkv` file. It is
under the same license as `movie.mp4`.
- `image.png` has all rights reserved, with the sole exception of copying and
distribution for testing purposes for this project only.
- `image.png.gz` is under the same license as `image.png`.
- `image.webp` is under the same license as `image.png`.
- All other files are dual-licensed under the CC0 1.0 Universal License or MIT
No Attribution License, at your convenience.
The CC BY 3.0 License may be found at
https://creativecommons.org/licenses/by/3.0/.
The CC0 1.0 Universal License is may be found at
https://creativecommons.org/publicdomain/zero/1.0/legalcode.
The MIT No Attribution is displayed below:
```
MIT No Attribution
Copyright 2021 Edward Shen
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

BIN
test/archive.zip Normal file

Binary file not shown.

229
test/code.rs Normal file
View file

@ -0,0 +1,229 @@
use std::ops::{Deref, DerefMut};
use argon2::Argon2;
use chacha20poly1305::aead::generic_array::sequence::GenericSequence;
use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::aead::{AeadInPlace, NewAead};
use chacha20poly1305::XChaCha20Poly1305;
use chacha20poly1305::XNonce;
use rand::{thread_rng, Rng};
use secrecy::{ExposeSecret, Secret, SecretVec, Zeroize};
use typenum::Unsigned;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Invalid password.")]
Password,
#[error("Invalid secret key.")]
SecretKey,
#[error("An error occurred while trying to decrypt the blob.")]
Encryption,
#[error("An error occurred while trying to derive a secret key.")]
Kdf,
}
// This struct intentionally prevents implement Clone or Copy
#[derive(Default)]
pub struct Key(chacha20poly1305::Key);
impl Key {
pub fn new_secret(vec: Vec<u8>) -> Option<Secret<Self>> {
chacha20poly1305::Key::from_exact_iter(vec.into_iter())
.map(Self)
.map(Secret::new)
}
}
impl AsRef<chacha20poly1305::Key> for Key {
fn as_ref(&self) -> &chacha20poly1305::Key {
&self.0
}
}
impl Deref for Key {
type Target = chacha20poly1305::Key;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Key {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Zeroize for Key {
fn zeroize(&mut self) {
self.0.zeroize();
}
}
/// Seals the provided message with an optional message. The resulting sealed
/// message has the nonce used to encrypt the message appended to it as well as
/// a salt string used to derive the key. In other words, the modified buffer is
/// one of the following to possibilities, depending if there was a password
/// provided:
///
/// ```
/// modified = C(message, rng_key, nonce) || nonce
/// ```
/// or
/// ```
/// modified = C(C(message, rng_key, nonce), kdf(pw, salt), nonce + 1) || nonce || salt
/// ```
///
/// Where:
/// - `C(message, key, nonce)` represents encrypting a provided message with
/// `XChaCha20Poly1305`.
/// - `rng_key` represents a randomly generated key.
/// - `kdf(pw, salt)` represents a key derived from Argon2.
pub fn seal_in_place(
message: &mut Vec<u8>,
pw: Option<SecretVec<u8>>,
) -> Result<Secret<Key>, Error> {
let (key, nonce) = gen_key_nonce();
let cipher = XChaCha20Poly1305::new(key.expose_secret());
cipher
.encrypt_in_place(&nonce, &[], message)
.map_err(|_| Error::Encryption)?;
let mut maybe_salt_string = None;
if let Some(password) = pw {
let (key, salt_string) = kdf(&password).map_err(|_| Error::Kdf)?;
maybe_salt_string = Some(salt_string);
let cipher = XChaCha20Poly1305::new(key.expose_secret());
cipher
.encrypt_in_place(&nonce.increment(), &[], message)
.map_err(|_| Error::Encryption)?;
}
message.extend_from_slice(nonce.as_slice());
if let Some(maybe_salted_string) = maybe_salt_string {
message.extend_from_slice(maybe_salted_string.as_ref());
}
Ok(key)
}
pub fn open_in_place(
data: &mut Vec<u8>,
key: &Secret<Key>,
password: Option<SecretVec<u8>>,
) -> Result<(), Error> {
let pw_key = if let Some(password) = password {
let salt_buf = data.split_off(data.len() - Salt::SIZE);
let argon = Argon2::default();
let mut pw_key = Key::default();
argon
.hash_password_into(password.expose_secret(), &salt_buf, &mut pw_key)
.map_err(|_| Error::Kdf)?;
Some(Secret::new(pw_key))
} else {
None
};
let nonce = Nonce::from_slice(&data.split_off(data.len() - Nonce::SIZE));
// At this point we should have a buffer that's only the ciphertext.
if let Some(key) = pw_key {
let cipher = XChaCha20Poly1305::new(key.expose_secret());
cipher
.decrypt_in_place(&nonce.increment(), &[], data)
.map_err(|_| Error::Password)?;
}
let cipher = XChaCha20Poly1305::new(key.expose_secret());
cipher
.decrypt_in_place(&nonce, &[], data)
.map_err(|_| Error::SecretKey)?;
Ok(())
}
/// Securely generates a random key and nonce.
#[must_use]
fn gen_key_nonce() -> (Secret<Key>, Nonce) {
let mut rng = thread_rng();
let mut key = GenericArray::default();
rng.fill(key.as_mut_slice());
let mut nonce = Nonce::default();
rng.fill(nonce.as_mut_slice());
(Secret::new(Key(key)), nonce)
}
// Type alias; to ensure that we're consistent on what the inner impl is.
type NonceImpl = XNonce;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Nonce(NonceImpl);
impl Default for Nonce {
fn default() -> Self {
Self(GenericArray::default())
}
}
impl Deref for Nonce {
type Target = NonceImpl;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Nonce {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl AsRef<[u8]> for Nonce {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl Nonce {
const SIZE: usize = <NonceImpl as GenericSequence<_>>::Length::USIZE;
#[must_use]
pub fn increment(&self) -> Self {
let mut inner = self.0;
inner.as_mut_slice()[0] += 1;
Self(inner)
}
#[must_use]
pub fn from_slice(slice: &[u8]) -> Self {
Self(*NonceImpl::from_slice(slice))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Salt([u8; Self::SIZE]);
impl Salt {
const SIZE: usize = argon2::password_hash::Salt::RECOMMENDED_LENGTH;
fn random() -> Self {
let mut salt = [0_u8; Self::SIZE];
thread_rng().fill(&mut salt);
Self(salt)
}
}
impl AsRef<[u8]> for Salt {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
/// Hashes an input to output a usable key.
fn kdf(password: &SecretVec<u8>) -> Result<(Secret<Key>, Salt), argon2::Error> {
let salt = Salt::random();
let hasher = Argon2::default();
let mut key = Key::default();
hasher.hash_password_into(password.expose_secret().as_ref(), salt.as_ref(), &mut key)?;
Ok((Secret::new(key), salt))
}

BIN
test/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
test/image.png.gz Normal file

Binary file not shown.

21
test/image.svg Normal file
View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 4417 3259" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<g transform="matrix(4.16667,0,0,4.16667,0,0)">
<path d="M525.403,293.05C393.77,293.05 274.175,308.875 185.633,334.665L185.633,554.963C274.175,580.753 393.77,596.577 525.403,596.577C676.06,596.577 810.938,575.848 901.537,543.175L901.537,346.457C810.938,313.781 676.06,293.05 525.403,293.05Z" style="fill:rgb(143,30,28);fill-rule:nonzero;"/>
<path d="M907.423,492.442C903.566,481.779 902.794,468.288 906.062,455.28C911.912,431.991 928.483,419.082 943.075,426.447C946.693,428.274 949.849,431.178 952.462,434.865C952.701,434.864 952.94,434.865 953.177,434.881C953.177,434.881 997.729,487.987 956.49,550.884C955.595,554.453 879.956,642.602 862.447,645.408C850.987,647.244 877.338,555.41 907.423,492.442Z" style="fill:rgb(143,30,28);fill-rule:nonzero;"/>
<path d="M176.479,482.021C181.779,472.391 183.637,459.233 180.696,445.596C175.43,421.18 156.786,404.486 139.054,408.311C134.656,409.259 130.729,411.383 127.388,414.409C127.106,414.351 126.824,414.296 126.543,414.256C126.543,414.256 70.251,456.208 114.486,528.18C115.291,531.921 198.337,637.018 218.797,643.943C232.188,648.475 207.55,551.418 176.479,482.021Z" style="fill:rgb(143,30,28);fill-rule:nonzero;"/>
<path d="M97.467,488.066L97.474,488.081C97.659,488.226 97.831,488.357 97.467,488.066Z" style="fill:rgb(227,58,37);fill-rule:nonzero;"/>
<path d="M993.119,412.903C992.239,409.839 991.363,406.777 990.457,403.741L1021.14,359.29C1024.27,354.768 1024.91,348.892 1022.87,343.735C1020.83,338.605 1016.38,334.925 1011.11,334.025L959.224,325.22C957.216,321.118 955.108,317.078 952.994,313.07L974.791,263.167C977.034,258.08 976.56,252.172 973.588,247.559C970.627,242.923 965.598,240.215 960.239,240.426L907.583,242.339C904.856,238.789 902.087,235.271 899.261,231.818L911.362,178.328C912.587,172.895 911.04,167.21 907.259,163.264C903.497,159.332 898.03,157.705 892.833,158.981L841.544,171.589C838.223,168.654 834.845,165.756 831.43,162.916L833.278,108.002C833.476,102.443 830.885,97.161 826.434,94.077C821.988,90.973 816.341,90.504 811.478,92.811L763.631,115.558C759.777,113.348 755.903,111.158 751.987,109.041L743.532,54.926C742.675,49.444 739.147,44.788 734.206,42.661C729.283,40.523 723.638,41.213 719.315,44.469L676.656,76.476C672.456,75.08 668.237,73.743 663.964,72.465L645.578,21.148C643.708,15.919 639.397,12.077 634.14,10.997C628.901,9.926 623.51,11.74 619.877,15.799L583.97,55.971C579.628,55.471 575.285,55.015 570.927,54.639L543.204,7.926C540.394,3.194 535.434,0.314 530.088,0.314C524.754,0.314 519.784,3.194 516.998,7.926L489.265,54.639C484.907,55.015 480.543,55.471 476.209,55.971L440.299,15.799C436.663,11.74 431.252,9.926 426.031,10.997C420.776,12.089 416.458,15.919 414.598,21.148L396.196,72.465C391.936,73.743 387.715,75.092 383.505,76.476L340.861,44.469C336.525,41.203 330.881,40.514 325.945,42.661C321.026,44.788 317.484,49.444 316.632,54.926L308.171,109.041C304.257,111.158 300.382,113.335 296.518,115.558L248.676,92.811C243.818,90.496 238.147,90.973 233.722,94.077C229.277,97.161 226.68,102.443 226.882,108.002L228.717,162.916C225.312,165.756 221.943,168.654 218.605,171.589L167.326,158.981C162.115,157.716 156.656,159.332 152.885,163.264C149.09,167.21 147.553,172.895 148.772,178.328L160.851,231.818C158.049,235.285 155.276,238.789 152.558,242.339L99.903,240.426C94.588,240.269 89.516,242.923 86.547,247.559C83.572,252.172 83.122,258.08 85.336,263.167L107.15,313.07C105.031,317.078 102.926,321.118 100.901,325.22L49.018,334.025C43.747,334.913 39.304,338.591 37.254,343.735C35.217,348.892 35.878,354.768 38.989,359.29L69.679,403.741C69.442,404.525 69.224,405.317 68.989,406.105L52.126,424.017L97.467,488.066C97.467,488.066 532.619,688.798 936.264,491.462C982.372,483.189 993.119,412.903 993.119,412.903Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
<path d="M608.303,376.759C608.303,376.759 656.46,324.03 704.618,376.759C704.618,376.759 742.458,447.071 704.618,482.222C704.618,482.222 642.701,531.439 608.303,482.222C608.303,482.222 567.024,443.55 608.303,376.759Z" style="fill:rgb(3,4,4);fill-rule:nonzero;"/>
<path d="M664.057,396.32C664.057,416.853 651.954,433.499 637.027,433.499C622.103,433.499 610,416.853 610,396.32C610,375.788 622.103,359.14 637.027,359.14C651.954,359.14 664.057,375.788 664.057,396.32Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M393.365,362.361C393.365,362.361 475.973,325.785 498.519,407.423C498.519,407.423 522.137,502.577 430.682,507.948C430.682,507.948 314.06,485.486 393.365,362.361Z" style="fill:rgb(3,4,4);fill-rule:nonzero;"/>
<path d="M434.855,397.668C434.855,418.841 422.375,436.014 406.978,436.014C391.587,436.014 379.104,418.841 379.104,397.668C379.104,376.49 391.587,359.322 406.978,359.322C422.375,359.322 434.855,376.49 434.855,397.668Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M111.602,499.216C122.569,486.753 149.213,471.659 147.172,452.934C143.519,419.407 115.716,394.935 85.073,398.275C77.473,399.103 70.415,401.567 64.149,405.311C63.687,405.204 63.224,405.1 62.761,405.017C62.761,405.017 -40.87,455.89 18.197,557.674C18.754,562.811 136.045,713.342 168.985,724.805C190.544,732.307 149.074,596.165 111.602,499.216Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
<path d="M953.549,494.673C940.856,483.973 907.387,474.255 906.629,455.435C905.273,421.737 929.141,393.414 959.941,392.175C967.579,391.867 974.925,393.258 981.676,396.032C982.118,395.858 982.56,395.686 983.005,395.535C983.005,395.535 1093.03,430.486 1049.7,539.901C1049.91,545.064 956.232,711.317 925.355,727.536C905.146,738.151 930.861,596.105 953.549,494.673Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
<path d="M191.142,495.558C191.142,495.558 189.759,632.854 324.308,663.49L352.362,607.127C352.362,607.127 254.867,616.558 247.367,495.558L191.142,495.558Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
<path d="M876.362,495.558C876.362,495.558 877.744,632.854 743.195,663.49L715.141,607.127C715.141,607.127 812.636,616.558 820.136,495.558L876.362,495.558Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
<path d="M779.167,635.591C758.917,586.649 693.572,567.218 633.216,592.191C580.09,614.172 548.579,663.223 555.592,708.036C597.538,707.384 642.532,704.665 686.328,698.318C686.328,698.318 660.491,740.081 622.471,776.529C648.037,783.128 677.854,781.297 706.547,769.425C766.904,744.452 799.417,684.532 779.167,635.591Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
<path d="M404.746,695.984C404.746,695.984 459.949,703.279 535.416,705.14C542.026,657.629 506.036,607.348 448.615,587.897C385.177,566.409 319.626,590.689 302.201,642.129C284.776,693.569 322.077,752.689 385.515,774.178C413.636,783.704 442.168,784.227 466.744,777.385C429.833,740.88 404.746,695.984 404.746,695.984Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
test/image.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
test/movie.mkv Normal file

Binary file not shown.

BIN
test/movie.mp4 Normal file

Binary file not shown.

BIN
test/music.mp3 Normal file

Binary file not shown.

BIN
test/omegaupload Executable file

Binary file not shown.

3
test/text.pgp Normal file
View file

@ -0,0 +1,3 @@
-----BEGIN PGP MESSAGE-----
-----END PGP MESSAGE-----

6
web/.gitignore vendored
View file

@ -1,6 +0,0 @@
node_modules
/dist
/target
/pkg
/wasm-pack.log
yarn-error.log

View file

@ -1,11 +1,6 @@
# You must change these to your own details.
[package] [package]
name = "rust-webpack-template" name = "omegaupload-web"
description = "My super awesome Rust, WebAssembly, and Webpack project!"
version = "0.1.0" version = "0.1.0"
authors = ["You <you@example.com>"]
categories = ["wasm"]
readme = "README.md"
edition = "2021" edition = "2021"
[lib] [lib]
@ -14,23 +9,30 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
omegaupload-common = { path = "../common", features = ["wasm"] } omegaupload-common = { path = "../common", features = ["wasm"] }
# Enables wasm support # Enables wasm support
getrandom = { version = "*", features = ["js"] } getrandom = { version = "0.2.7", features = ["js"] }
anyhow = "1" anyhow = "1.0.58"
bytes = "1" bytes = "1.2.0"
byte-unit = "4" byte-unit = "4.0.14"
console_error_panic_hook = "0.1" console_error_panic_hook = "0.1.7"
gloo-console = "0.1" gloo-console = "0.3"
http = "0.2" http = "0.2.8"
js-sys = "0.3" js-sys = "0.3.59"
reqwasm = "0.2" mime_guess = "2.0.4"
tree_magic_mini = { version = "3", features = ["with-gpl-data"] } tree_magic_mini = { version = "3.0.3", features = ["with-gpl-data"] }
wasm-bindgen = "0.2" serde = { version = "1.0.140", features = ["derive"] }
wasm-bindgen-futures = "0.4" serde-wasm-bindgen = { version = "0.6" }
wasm-bindgen = { version = "0.2.82", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4.32"
zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
flate2 = "1.0.24"
tar = "0.4.38"
reqwest = "0.11"
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3" version = "0.3.59"
features = [ features = [
"BlobPropertyBag",
"TextDecoder", "TextDecoder",
"IdbFactory", "IdbFactory",
"IdbOpenDbRequest", "IdbOpenDbRequest",
@ -47,6 +49,3 @@ features = [
"Performance", "Performance",
"Location", "Location",
] ]
[dev-dependencies]
wasm-bindgen-test = "0.2.45"

674
web/LICENSE Normal file
View file

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View file

@ -1,48 +1,10 @@
## How to install Contains the codebase used for the frontend
```sh Notes on licensing:
npm install
```
## How to run in debug mode https://www.gnu.org/licenses/gpl-faq.html#AllCompatibility
```sh Because there is a statically linked in dependency on `shared-mime-types`, this
# Builds the project and opens it in a new browser tab. Auto-reloads when the project changes. crate MUST be under a GPLv2 or later license. This has been confirmed as of
npm start 2021-10-24.
```
## How to build in release mode
```sh
# Builds the project and places it into the `dist` folder.
npm run build
```
## How to run unit tests
```sh
# Runs tests in Firefox
npm test -- --firefox
# Runs tests in Chrome
npm test -- --chrome
# Runs tests in Safari
npm test -- --safari
```
## What does each file do?
* `Cargo.toml` contains the standard Rust metadata. You put your Rust dependencies in here. You must change this file with your details (name, description, version, authors, categories)
* `package.json` contains the standard npm metadata. You put your JavaScript dependencies in here. You must change this file with your details (author, name, version)
* `webpack.config.js` contains the Webpack configuration. You shouldn't need to change this, unless you have very special needs.
* The `js` folder contains your JavaScript code (`index.js` is used to hook everything into Webpack, you don't need to change it).
* The `src` folder contains your Rust code.
* The `static` folder contains any files that you want copied as-is into the final build. It contains an `index.html` file which loads the `index.js` file.
* The `tests` folder contains your Rust unit tests.

View file

@ -1,4 +0,0 @@
import { renderMessage } from './ui';
import * as index from "../pkg/index.js";
renderMessage("wtf");
console.log(index);

View file

@ -1,196 +0,0 @@
import hljs from 'hljs';
window.addEventListener("hashchange", () => location.reload());
function loadFromDb() {
const dbReq = window.indexedDB.open("omegaupload", 1);
dbReq.onsuccess = (evt) => {
const db = evt.target.result;
const obj_store = db
.transaction("decrypted data")
.objectStore("decrypted data");
let fetchReq = obj_store.get(window.location.pathname);
fetchReq.onsuccess = (evt) => {
const data = evt.target.result;
switch (data.type) {
case "string":
createStringPasteUi(data);
break;
case "blob":
createBlobPasteUi(data);
break;
case "image":
createImagePasteUi(data);
break;
case "audio":
createAudioPasteUi(data);
break;
case "video":
createVideoPasteUi(data);
break;
default:
renderMessage("Something went wrong. Try clearing local data.");
break;
}
// IDB was only used as a temporary medium;
window.onbeforeunload = (e) => {
// See https://link.eddie.sh/NrIIq on why .commit is necessary.
const transaction = db.transaction("decrypted data", "readwrite");
transaction
.objectStore("decrypted data")
.delete(window.location.pathname);
transaction.commit();
transaction.oncomplete = () => {
console.log("Item deleted from cache");
}
};
};
fetchReq.onerror = (evt) => {
console.log("err");
console.log(evt);
};
};
}
function createStringPasteUi(data) {
let bodyEle = document.getElementsByTagName("body")[0];
bodyEle.textContent = '';
let mainEle = document.createElement("main");
let preEle = document.createElement("pre");
preEle.classList.add("paste");
let headerEle = document.createElement("header");
headerEle.classList.add("unselectable");
headerEle.textContent = data.expiration;
preEle.appendChild(headerEle);
preEle.appendChild(document.createElement("hr"));
let codeEle = document.createElement("code");
codeEle.textContent = data.data;
preEle.appendChild(codeEle);
mainEle.appendChild(preEle);
bodyEle.appendChild(mainEle);
hljs.highlightAll();
hljs.initLineNumbersOnLoad();
}
function createBlobPasteUi(data) {
let bodyEle = document.getElementsByTagName("body")[0];
bodyEle.textContent = '';
let mainEle = document.createElement("main");
mainEle.classList.add("hljs");
mainEle.classList.add("centered");
mainEle.classList.add("fullscreen");
let divEle = document.createElement("div");
divEle.classList.add("centered");
let expirationEle = document.createElement("p");
expirationEle.textContent = data.expiration;
divEle.appendChild(expirationEle);
let downloadEle = document.createElement("a");
downloadEle.href = URL.createObjectURL(data.data);
downloadEle.download = window.location.pathname;
downloadEle.classList.add("hljs-meta");
downloadEle.textContent = "Download binary file.";
divEle.appendChild(downloadEle);
mainEle.appendChild(divEle);
let displayAnywayEle = document.createElement("p");
displayAnywayEle.classList.add("display-anyways");
displayAnywayEle.classList.add("hljs-comment");
displayAnywayEle.textContent = "Display anyways?";
displayAnywayEle.onclick = () => {
data.data.text().then(text => {
data.data = text;
createStringPasteUi(data);
})
};
mainEle.appendChild(displayAnywayEle);
bodyEle.appendChild(mainEle);
}
function createImagePasteUi({ expiration, data, file_size }) {
createMultiMediaPasteUi("img", expiration, data, (downloadEle, imgEle) => {
imgEle.onload = () => {
downloadEle.textContent = "Download " + file_size + " \u2014 " + imgEle.naturalWidth + " by " + imgEle.naturalHeight;
}
});
}
function createAudioPasteUi({ expiration, data }) {
createMultiMediaPasteUi("audio", expiration, data, "Download");
}
function createVideoPasteUi({ expiration, data }) {
createMultiMediaPasteUi("video", expiration, data, "Download");
}
function createMultiMediaPasteUi(tag, expiration, data, on_create) {
let bodyEle = document.getElementsByTagName("body")[0];
bodyEle.textContent = '';
let mainEle = document.createElement("main");
mainEle.classList.add("hljs");
mainEle.classList.add("centered");
mainEle.classList.add("fullscreen");
const downloadLink = URL.createObjectURL(data);
let expirationEle = document.createElement("p");
expirationEle.textContent = expiration;
mainEle.appendChild(expirationEle);
let mediaEle = document.createElement(tag);
mediaEle.src = downloadLink;
mediaEle.controls = true;
mainEle.appendChild(mediaEle);
let downloadEle = document.createElement("a");
downloadEle.href = downloadLink;
downloadEle.download = window.location.pathname;
downloadEle.classList.add("hljs-meta");
mainEle.appendChild(downloadEle);
bodyEle.appendChild(mainEle);
if (on_create instanceof Function) {
on_create(downloadEle, mediaEle);
} else {
downloadEle.textContent = on_create;
}
}
function renderMessage(message) {
let body = document.getElementsByTagName("body")[0];
body.textContent = '';
let mainEle = document.createElement("main");
mainEle.classList.add("hljs");
mainEle.classList.add("centered");
mainEle.classList.add("fullscreen");
mainEle.textContent = message;
body.appendChild(mainEle);
}
function renderIndex() {
console.log("index");
// TODO: find a way to not hard code this.
// https://docs.rs/chacha20poly1305/0.9.0/chacha20poly1305/type.Key.html
// https://docs.rs/chacha20poly1305/0.9.0/chacha20poly1305/type.XNonce.html
const key = crypto.getRandomValues(new Uint8Array(32));
const nonce = crypto.getRandomValues(new Uint8Array(24));
console.log(key, nonce);
}
export { renderIndex, renderMessage, loadFromDb };

View file

@ -1,23 +0,0 @@
{
"author": "Edward Shen <code@eddie.sh>",
"name": "omegaupload-web",
"version": "0.1.0",
"scripts": {
"build": "rimraf dist pkg && webpack",
"start": "rimraf dist pkg && webpack-dev-server --open",
"test": "cargo test && wasm-pack test --headless"
},
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.1.0",
"copy-webpack-plugin": "^9.0.1",
"rimraf": "^3.0.0",
"ts-loader": "^9.2.6",
"webpack": "^5.60.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.3.1"
},
"dependencies": {
"hljs": "^6.2.3",
"typescript": "^4.4.4"
}
}

33
web/src/bg_encrypt.ts Normal file
View file

@ -0,0 +1,33 @@
// OmegaUpload Web Frontend
// Copyright (C) 2021 Edward Shen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { encrypt_array_buffer } from '../pkg';
interface BgData {
location: string,
data: any
}
addEventListener('message', (event: MessageEvent<BgData>) => {
let { location, data } = event.data;
console.log('[js-worker] Sending data to rust in a worker thread...');
encrypt_array_buffer(location, data).then(url => {
console.log("[js-worker] Encryption done.");
postMessage(url);
}).catch(e => console.error(e));
})
postMessage("init");

View file

@ -1,11 +1,35 @@
use std::fmt::{Display, Formatter}; // OmegaUpload Web Frontend
// Copyright (C) 2021 Edward Shen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::io::Cursor;
use std::sync::Arc; use std::sync::Arc;
use gloo_console::log; use gloo_console::log;
use js_sys::{Array, Uint8Array}; use js_sys::{Array, Uint8Array};
use omegaupload_common::crypto::{open_in_place, Key, Nonce}; use omegaupload_common::crypto::{open_in_place, Error, Key};
use omegaupload_common::secrecy::{Secret, SecretVec};
use serde::Serialize;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use web_sys::Blob; use web_sys::{Blob, BlobPropertyBag};
#[derive(Clone, Serialize)]
pub struct ArchiveMeta {
name: String,
file_size: u64,
}
#[derive(Clone)] #[derive(Clone)]
pub enum DecryptedData { pub enum DecryptedData {
@ -14,6 +38,7 @@ pub enum DecryptedData {
Image(Arc<Blob>, usize), Image(Arc<Blob>, usize),
Audio(Arc<Blob>), Audio(Arc<Blob>),
Video(Arc<Blob>), Video(Arc<Blob>),
Archive(Arc<Blob>, Vec<ArchiveMeta>),
} }
fn now() -> f64 { fn now() -> f64 {
@ -24,82 +49,201 @@ fn now() -> f64 {
.now() .now()
} }
pub struct MimeType(pub String);
pub fn decrypt( pub fn decrypt(
mut container: Vec<u8>, mut container: Vec<u8>,
key: Key, key: &Secret<Key>,
nonce: Nonce, maybe_password: Option<SecretVec<u8>>,
maybe_password: Option<Key>, name_hint: Option<&str>,
) -> Result<DecryptedData, PasteCompleteConstructionError> { ) -> Result<(DecryptedData, MimeType), Error> {
let container = &mut container; open_in_place(&mut container, key, maybe_password)?;
log!("Stage 1 decryption started.");
let start = now();
if let Some(password) = maybe_password { let mime_type = guess_mime_type(name_hint, &container);
crate::render_message("Decrypting Stage 1...".into()); log!("[rs] Mime type:", mime_type);
open_in_place(container, &nonce.increment(), &password).map_err(|_| {
crate::render_message("Unable to decrypt paste with the provided password.".into()); log!("[rs] Blob conversion started.");
PasteCompleteConstructionError::StageOneFailure let start = now();
})?; let blob_chunks = Array::new_with_length(container.chunks(65536).len().try_into().unwrap());
for (i, chunk) in container.chunks(65536).enumerate() {
let array = Uint8Array::new_with_length(chunk.len().try_into().unwrap());
array.copy_from(chunk);
blob_chunks.set(i.try_into().unwrap(), array.dyn_into().unwrap());
} }
log!(format!("Stage 1 completed in {}ms", now() - start)); let mut blob_props = BlobPropertyBag::new();
blob_props.type_(mime_type);
let blob = Arc::new(
Blob::new_with_u8_array_sequence_and_options(blob_chunks.dyn_ref().unwrap(), &blob_props)
.unwrap(),
);
log!("Stage 2 decryption started."); log!(format!(
let start = now(); "[rs] Blob conversion completed in {}ms",
crate::render_message("Decrypting Stage 2...".into()); now() - start
open_in_place(container, &nonce, &key).map_err(|_| { ));
crate::render_message(
"Unable to decrypt paste with the provided encryption key and nonce.".into(),
);
PasteCompleteConstructionError::StageTwoFailure
})?;
log!(format!("Stage 2 completed in {}ms", now() - start));
if let Ok(decrypted) = std::str::from_utf8(container) { let data = match container.content_type() {
Ok(DecryptedData::String(Arc::new(decrypted.to_owned()))) ContentType::Text => DecryptedData::String(Arc::new(
// SAFETY: ContentType::Text is guaranteed to be valid UTF-8.
unsafe { String::from_utf8_unchecked(container) },
)),
ContentType::Image => DecryptedData::Image(blob, container.len()),
ContentType::Audio => DecryptedData::Audio(blob),
ContentType::Video => DecryptedData::Video(blob),
ContentType::ZipArchive => handle_zip_archive(blob, container),
ContentType::Gzip => handle_gzip(blob, container),
ContentType::Unknown => DecryptedData::Blob(blob),
};
Ok((data, MimeType(mime_type.to_owned())))
}
fn handle_zip_archive(blob: Arc<Blob>, container: Vec<u8>) -> DecryptedData {
let mut entries = vec![];
let cursor = Cursor::new(container);
if let Ok(mut zip) = zip::ZipArchive::new(cursor) {
for i in 0..zip.len() {
match zip.by_index(i) {
Ok(file) => entries.push(ArchiveMeta {
name: file.name().to_string(),
file_size: file.size(),
}),
Err(err) => match err {
zip::result::ZipError::UnsupportedArchive(s) => {
log!("Unsupported: ", s.to_string());
}
_ => {
log!(format!("Error: {err}"));
}
},
}
}
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
DecryptedData::Archive(blob, entries)
}
fn handle_gzip(blob: Arc<Blob>, container: Vec<u8>) -> DecryptedData {
let mut entries = vec![];
let cursor = Cursor::new(container);
let gzip_dec = flate2::read::GzDecoder::new(cursor);
let mut archive = tar::Archive::new(gzip_dec);
if let Ok(files) = archive.entries() {
for file in files.flatten() {
let file_path = if let Ok(file_path) = file.path() {
file_path.display().to_string()
} else {
"<Invalid utf-8 path>".to_string()
};
entries.push(ArchiveMeta {
name: file_path,
file_size: file.size(),
});
}
}
if entries.is_empty() {
DecryptedData::Blob(blob)
} else { } else {
log!("Blob conversion started."); DecryptedData::Archive(blob, entries)
let start = now(); }
let blob_chunks = Array::new_with_length(container.chunks(65536).len().try_into().unwrap()); }
for (i, chunk) in container.chunks(65536).enumerate() {
let array = Uint8Array::new_with_length(chunk.len().try_into().unwrap()); fn guess_mime_type(name_hint: Option<&str>, data: &[u8]) -> &'static str {
array.copy_from(chunk); if let Some(name) = name_hint {
blob_chunks.set(i.try_into().unwrap(), array.dyn_into().unwrap()); let guesses = mime_guess::from_path(name);
if let Some(mime_type) = guesses.first_raw() {
// Found at least one, but generally speaking this crate only
// uses authoritative sources (RFCs), so generally speaking
// there's only one association, and multiple are due to legacy
// support. As a result, we can probably just get the first one.
log!("[rs] Mime type inferred from extension.");
return mime_type;
} }
let blob = log!("[rs] No mime type found for extension, falling back to introspection.");
Arc::new(Blob::new_with_u8_array_sequence(blob_chunks.dyn_ref().unwrap()).unwrap()); }
log!(format!("Blob conversion completed in {}ms", now() - start)); tree_magic_mini::from_u8(data)
}
let mime_type = tree_magic_mini::from_u8(container); #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum ContentType {
Text,
Image,
Audio,
Video,
ZipArchive,
Gzip,
Unknown,
}
if mime_type.starts_with("image/") || mime_type == "application/x-riff" { trait ContentTypeExt {
Ok(DecryptedData::Image(blob, container.len())) fn mime_type(&self) -> &str;
fn content_type(&self) -> ContentType;
}
impl<T: AsRef<[u8]>> ContentTypeExt for T {
fn mime_type(&self) -> &str {
tree_magic_mini::from_u8(self.as_ref())
}
fn content_type(&self) -> ContentType {
let mime_type = self.mime_type();
// check image first; tree magic match_u8 matches SVGs as plain text
if mime_type.starts_with("image/")
// application/x-riff is WebP
|| mime_type == "application/x-riff"
{
ContentType::Image
} else if tree_magic_mini::match_u8("text/plain", self.as_ref()) {
if std::str::from_utf8(self.as_ref()).is_ok() {
ContentType::Text
} else {
ContentType::Unknown
}
} else if mime_type.starts_with("audio/") { } else if mime_type.starts_with("audio/") {
Ok(DecryptedData::Audio(blob)) ContentType::Audio
} else if mime_type.starts_with("video/") || mime_type == "application/x-matroska" { } else if mime_type.starts_with("video/")
Ok(DecryptedData::Video(blob)) // application/x-matroska is mkv
|| mime_type == "application/x-matroska"
{
ContentType::Video
} else if mime_type == "application/zip" {
ContentType::ZipArchive
} else if mime_type == "application/gzip" {
ContentType::Gzip
} else { } else {
Ok(DecryptedData::Blob(blob)) ContentType::Unknown
} }
} }
} }
#[derive(Debug)] #[cfg(test)]
pub enum PasteCompleteConstructionError { mod content_type {
StageOneFailure, use super::*;
StageTwoFailure,
}
impl std::error::Error for PasteCompleteConstructionError {} macro_rules! test_content_type {
($($name:ident, $path:literal, $type:expr),*) => {
impl Display for PasteCompleteConstructionError { $(
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { #[test]
match self { fn $name() {
PasteCompleteConstructionError::StageOneFailure => { let data = include_bytes!(concat!("../../test/", $path));
write!(f, "Failed to decrypt stage one.") assert_eq!(data.content_type(), $type);
} }
PasteCompleteConstructionError::StageTwoFailure => { )*
write!(f, "Failed to decrypt stage two.") };
}
}
} }
test_content_type!(license_is_text, "LICENSE.md", ContentType::Text);
test_content_type!(code_is_text, "code.rs", ContentType::Text);
test_content_type!(patch_is_text, "0000-test-patch.patch", ContentType::Text);
test_content_type!(png_is_image, "image.png", ContentType::Image);
test_content_type!(webp_is_image, "image.webp", ContentType::Image);
test_content_type!(svg_is_image, "image.svg", ContentType::Image);
test_content_type!(mp3_is_audio, "music.mp3", ContentType::Audio);
test_content_type!(mp4_is_video, "movie.mp4", ContentType::Video);
test_content_type!(mkv_is_video, "movie.mkv", ContentType::Video);
test_content_type!(zip_is_zip, "archive.zip", ContentType::ZipArchive);
test_content_type!(gzip_is_gzip, "image.png.gz", ContentType::Gzip);
test_content_type!(binary_is_unknown, "omegaupload", ContentType::Unknown);
test_content_type!(pgp_is_text, "text.pgp", ContentType::Text);
} }

View file

@ -1,5 +1,22 @@
// OmegaUpload Web Frontend
// Copyright (C) 2021 Edward Shen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{hint::unreachable_unchecked, marker::PhantomData}; use std::{hint::unreachable_unchecked, marker::PhantomData};
use gloo_console::log;
use js_sys::{Array, JsString, Object}; use js_sys::{Array, JsString, Object};
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
@ -21,7 +38,10 @@ impl From<IdbObject<Ready>> for Object {
Ok(o) => o, Ok(o) => o,
// SAFETY: IdbObject maintains the invariant that it can eventually // SAFETY: IdbObject maintains the invariant that it can eventually
// be constructed into a JS object. // be constructed into a JS object.
_ => unsafe { unreachable_unchecked() }, _ => {
log!("IdbObject invariant violated?!");
unsafe { unreachable_unchecked() }
}
} }
} }
} }
@ -31,6 +51,10 @@ impl IdbObject<NeedsType> {
Self(Array::new(), PhantomData) Self(Array::new(), PhantomData)
} }
pub fn archive(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("archive"))
}
pub fn video(self) -> IdbObject<NeedsExpiration> { pub fn video(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("video")) self.add_tuple("type", &JsString::from("video"))
} }

10
web/src/index.html Normal file
View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Omegaupload</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
</html>

6
web/src/index.js Normal file
View file

@ -0,0 +1,6 @@
import { start } from '../pkg';
import './main.scss';
start();
window.addEventListener("hashchange", () => location.reload());

View file

@ -1,20 +1,39 @@
#![warn(clippy::nursery, clippy::pedantic)] #![warn(clippy::nursery, clippy::pedantic)]
// OmegaUpload Web Frontend
// Copyright (C) 2021 Edward Shen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::str::FromStr; use std::str::FromStr;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use byte_unit::{n_mib_bytes, Byte}; use byte_unit::{n_mib_bytes, Byte};
use decrypt::DecryptedData; use decrypt::{DecryptedData, MimeType};
use gloo_console::{error, log}; use gloo_console::{error, log};
use http::uri::PathAndQuery; use http::uri::PathAndQuery;
use http::{StatusCode, Uri}; use http::{StatusCode, Uri};
use js_sys::{JsString, Object, Uint8Array}; use js_sys::{Array, JsString, Object};
use omegaupload_common::crypto::{Key, Nonce}; use omegaupload_common::base64;
use omegaupload_common::{hash, Expiration, PartialParsedUrl}; use omegaupload_common::crypto::seal_in_place;
use reqwasm::http::Request; use omegaupload_common::crypto::{Error as CryptoError, Key};
use omegaupload_common::fragment::Builder;
use omegaupload_common::secrecy::{ExposeSecret, Secret, SecretString, SecretVec};
use omegaupload_common::{Expiration, PartialParsedUrl, Url};
use wasm_bindgen::prelude::{wasm_bindgen, Closure}; use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::{spawn_local, JsFuture}; use wasm_bindgen_futures::spawn_local;
use web_sys::{Event, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode, Location, Window}; use web_sys::{Event, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode, Location, Window};
use crate::decrypt::decrypt; use crate::decrypt::decrypt;
@ -27,14 +46,14 @@ mod util;
const DOWNLOAD_SIZE_LIMIT: u128 = n_mib_bytes!(500); const DOWNLOAD_SIZE_LIMIT: u128 = n_mib_bytes!(500);
#[wasm_bindgen(raw_module = "../js/ui.js")] #[wasm_bindgen(raw_module = "../src/render")]
extern "C" { extern "C" {
#[wasm_bindgen(js_name = renderIndex)]
pub fn render_index();
#[wasm_bindgen(js_name = loadFromDb)] #[wasm_bindgen(js_name = loadFromDb)]
pub fn load_from_db(); pub fn load_from_db(mime_type: JsString, name: Option<JsString>, language: Option<JsString>);
#[wasm_bindgen(js_name = renderMessage)] #[wasm_bindgen(js_name = renderMessage)]
pub fn render_message(message: JsString); pub fn render_message(message: JsString);
#[wasm_bindgen(js_name = createUploadUi)]
pub fn create_upload_ui();
} }
fn window() -> Window { fn window() -> Window {
@ -54,185 +73,203 @@ fn open_idb() -> Result<IdbOpenDbRequest> {
.map_err(|_| anyhow!("Failed to open idb")) .map_err(|_| anyhow!("Failed to open idb"))
} }
// This is like the `main` function, except for JavaScript. #[wasm_bindgen]
#[wasm_bindgen(start)] #[allow(clippy::missing_panics_doc)]
pub fn js_main() { pub fn start() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook)); std::panic::set_hook(Box::new(console_error_panic_hook::hook));
if location().pathname().unwrap() == "/" { if location().pathname().unwrap() == "/" {
render_index(); create_upload_ui();
} else { return;
render_message("Loading paste...".into()); }
let url = String::from(location().to_string()); render_message("Loading paste...".into());
let request_uri = {
let mut uri_parts = url.parse::<Uri>().unwrap().into_parts();
if let Some(parts) = uri_parts.path_and_query.as_mut() {
*parts = PathAndQuery::from_str(&format!("/api{}", parts.path())).unwrap();
}
Uri::from_parts(uri_parts).unwrap()
};
log!(&url); let url = String::from(location().to_string());
log!(&request_uri.to_string()); let request_uri = {
log!(&location().pathname().unwrap()); let mut uri_parts = url.parse::<Uri>().unwrap().into_parts();
let (key, nonce, needs_pw) = { if let Some(parts) = uri_parts.path_and_query.as_mut() {
let partial_parsed_url = url *parts = PathAndQuery::from_str(&format!("/api{}", parts.path())).unwrap();
.split_once('#') }
.map(|(_, fragment)| PartialParsedUrl::from(fragment)) Uri::from_parts(uri_parts).unwrap()
.unwrap_or_default(); };
let key = if let Some(key) = partial_parsed_url.decryption_key {
key let (
} else { key,
PartialParsedUrl {
needs_password,
name,
language,
..
},
) = {
let fragment = if let Some(fragment) = url.split_once('#').map(|(_, fragment)| fragment) {
if fragment.is_empty() {
error!("Key is missing in url; bailing."); error!("Key is missing in url; bailing.");
render_message("Invalid paste link: Missing decryption key.".into()); render_message("Invalid paste link: Missing metadata.".into());
return; return;
}; }
let nonce = if let Some(nonce) = partial_parsed_url.nonce { fragment
nonce } else {
} else { error!("Key is missing in url; bailing.");
error!("Nonce is missing in url; bailing."); render_message("Invalid paste link: Missing metadata.".into());
render_message("Invalid paste link: Missing nonce.".into()); return;
return;
};
(key, nonce, partial_parsed_url.needs_password)
}; };
let password = if needs_pw { let mut partial_parsed_url = match PartialParsedUrl::try_from(fragment) {
loop { Ok(partial_parsed_url) => partial_parsed_url,
let pw = Err(e) => {
window().prompt_with_message("A password is required to decrypt this paste:"); error!("Failed to parse text fragment; bailing.");
render_message(format!("Invalid paste link: {e}").into());
return;
}
};
if let Ok(Some(password)) = pw { let key = if let Some(key) = partial_parsed_url.decryption_key.take() {
if !password.is_empty() { key
break Some(hash(password)); } else {
} error!("Key is missing in url; bailing.");
render_message("Invalid paste link: Missing decryption key.".into());
return;
};
(key, partial_parsed_url)
};
let password = if needs_password {
loop {
let pw = window().prompt_with_message("A password is required to decrypt this paste:");
match pw {
// Ok button was entered.
Ok(Some(password)) if !password.is_empty() => {
break Some(SecretVec::new(password.into_bytes()));
}
// Empty message was entered.
Ok(Some(_)) => (),
// Cancel button was entered.
Ok(None) => {
render_message("This paste requires a password.".into());
return;
}
e => {
render_message("Internal error occurred.".into());
error!(format!("Error occurred at pw prompt: {e:?}"));
return;
} }
} }
} else { }
None } else {
}; None
spawn_local(async move { };
if let Err(e) = fetch_resources(request_uri, key, nonce, password).await {
log!(e.to_string()); spawn_local(async move {
} if let Err(e) = fetch_resources(request_uri, key, password, name, language).await {
}); log!(e.to_string());
} }
});
}
#[wasm_bindgen]
#[allow(clippy::future_not_send)]
pub async fn encrypt_array_buffer(location: String, data: Vec<u8>) -> Result<JsString, JsString> {
do_encrypt(location, data).await.map_err(|e| {
log!(format!("[rs] Error encrypting array buffer: {}", e));
JsString::from(e.to_string())
})
}
#[allow(clippy::future_not_send)]
async fn do_encrypt(location: String, mut data: Vec<u8>) -> Result<JsString> {
let (data, key) = {
let enc_key = seal_in_place(&mut data, None)?;
let key = SecretString::new(base64::encode(&enc_key.expose_secret().as_ref()));
(data, key)
};
let mut url = Url::from_str(&location)?;
let fragment = Builder::new(key);
let short_code = reqwest::Client::new()
.post(url.as_ref())
.body(data)
.send()
.await?
.text()
.await?;
url.set_path(&short_code);
url.set_fragment(Some(fragment.build().expose_secret()));
Ok(JsString::from(url.as_ref()))
} }
#[allow(clippy::future_not_send)] #[allow(clippy::future_not_send)]
async fn fetch_resources( async fn fetch_resources(
request_uri: Uri, request_uri: Uri,
key: Key, key: Secret<Key>,
nonce: Nonce, password: Option<SecretVec<u8>>,
password: Option<Key>, name: Option<String>,
language: Option<String>,
) -> Result<()> { ) -> Result<()> {
match Request::get(&request_uri.to_string()).send().await { match reqwest::Client::new()
.get(&request_uri.to_string())
.send()
.await
{
Ok(resp) if resp.status() == StatusCode::OK => { Ok(resp) if resp.status() == StatusCode::OK => {
let expires = Expiration::try_from(resp.headers()).map_or_else( let expires = resp
|_| "This item does not expire.".to_string(), .headers()
|expires| expires.to_string(), .get(http::header::EXPIRES)
); .and_then(|header| Expiration::try_from(header).ok())
.map_or_else(
|| "This item does not expire.".to_string(),
|expires| expires.to_string(),
);
let data = { let data = resp
let data_fut = resp .bytes()
.as_raw() .await
.array_buffer() .expect("to get raw bytes from a response")
.expect("to get raw bytes from a response"); .to_vec();
let data = match JsFuture::from(data_fut).await {
Ok(data) => data,
Err(e) => {
render_message(
"Network failure: Failed to completely read encryption paste.".into(),
);
bail!(format!(
"JsFuture returned an error while fetching resp buffer: {:?}",
e
));
}
};
Uint8Array::new(&data).to_vec()
};
if data.len() as u128 > DOWNLOAD_SIZE_LIMIT { if data.len() as u128 > DOWNLOAD_SIZE_LIMIT {
render_message("The paste is too large to decrypt from the web browser. You must use the CLI tool to download this paste.".into()); render_message("The paste is too large to decrypt from the web browser. You must use the CLI tool to download this paste.".into());
return Ok(()); return Ok(());
} }
let decrypted = decrypt(data, key, nonce, password)?; let (decrypted, mimetype) = match decrypt(data, &key, password, name.as_deref()) {
Ok(data) => data,
Err(e) => {
let msg = match e {
CryptoError::Password => "The provided password was incorrect.",
CryptoError::SecretKey => "The secret key in the URL was incorrect.",
ref e => {
log!(format!("Bad kdf or corrupted blob: {e}"));
"An internal error occurred."
}
};
render_message(JsString::from(msg));
bail!(e);
}
};
let db_open_req = open_idb()?; let db_open_req = open_idb()?;
// On success callback let on_success = Closure::once(Box::new(move |event| {
let on_success = Closure::once(Box::new(move |event: Event| { on_success(&event, &decrypted, mimetype, &expires, name, language);
let transaction: IdbObjectStore = as_idb_db(&event) }));
.transaction_with_str_and_mode("decrypted data", IdbTransactionMode::Readwrite)
.unwrap()
.object_store("decrypted data")
.unwrap();
let decrypted_object = match &decrypted {
DecryptedData::String(s) => IdbObject::new()
.string()
.expiration_text(&expires)
.data(&JsValue::from_str(s)),
DecryptedData::Blob(blob) => {
IdbObject::new().blob().expiration_text(&expires).data(blob)
}
DecryptedData::Image(blob, size) => IdbObject::new()
.image()
.expiration_text(&expires)
.data(blob)
.extra(
"file_size",
Byte::from_bytes(*size as u128)
.get_appropriate_unit(true)
.to_string(),
),
DecryptedData::Audio(blob) => IdbObject::new()
.audio()
.expiration_text(&expires)
.data(blob),
DecryptedData::Video(blob) => IdbObject::new()
.video()
.expiration_text(&expires)
.data(blob),
};
let put_action = transaction
.put_with_key(
&Object::from(decrypted_object),
&JsString::from(location().pathname().unwrap()),
)
.unwrap();
put_action.set_onsuccess(Some(
Closure::wrap(Box::new(|| {
log!("success");
load_from_db();
}) as Box<dyn Fn()>)
.into_js_value()
.unchecked_ref(),
));
put_action.set_onerror(Some(
Closure::wrap(Box::new(|e| {
log!(e);
}) as Box<dyn Fn(Event)>)
.into_js_value()
.unchecked_ref(),
));
}) as Box<dyn FnOnce(Event)>);
db_open_req.set_onsuccess(Some(on_success.into_js_value().unchecked_ref())); db_open_req.set_onsuccess(Some(on_success.into_js_value().unchecked_ref()));
db_open_req.set_onerror(Some( db_open_req.set_onerror(Some(
Closure::wrap(Box::new(|e| { Closure::once(Box::new(|e: Event| log!(e)))
log!(e); .into_js_value()
}) as Box<dyn Fn(Event)>) .unchecked_ref(),
.into_js_value()
.unchecked_ref(),
)); ));
let on_upgrade = Closure::wrap(Box::new(move |event: Event| { let on_upgrade = Closure::once(Box::new(move |event: Event| {
let db = as_idb_db(&event); let db = as_idb_db(&event);
let _obj_store = db.create_object_store("decrypted data").unwrap(); let _obj_store = db.create_object_store("decrypted data").unwrap();
}) as Box<dyn FnMut(Event)>); }));
db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().unchecked_ref())); db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().unchecked_ref()));
} }
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => { Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
@ -242,12 +279,82 @@ async fn fetch_resources(
render_message("Invalid paste URL.".into()); render_message("Invalid paste URL.".into());
} }
Ok(err) => { Ok(err) => {
render_message(err.status_text().into()); render_message(err.status().as_str().into());
} }
Err(err) => { Err(err) => {
render_message(format!("{}", err).into()); render_message(format!("{err}").into());
} }
} }
Ok(()) Ok(())
} }
fn on_success(
event: &Event,
decrypted: &DecryptedData,
mimetype: MimeType,
expires: &str,
name: Option<String>,
language: Option<String>,
) {
let transaction: IdbObjectStore = as_idb_db(event)
.transaction_with_str_and_mode("decrypted data", IdbTransactionMode::Readwrite)
.unwrap()
.object_store("decrypted data")
.unwrap();
let decrypted_object = match decrypted {
DecryptedData::String(s) => IdbObject::new()
.string()
.expiration_text(expires)
.data(&JsValue::from_str(s)),
DecryptedData::Blob(blob) => IdbObject::new().blob().expiration_text(expires).data(blob),
DecryptedData::Image(blob, size) => IdbObject::new()
.image()
.expiration_text(expires)
.data(blob)
.extra(
"file_size",
Byte::from_bytes(*size as u128)
.get_appropriate_unit(true)
.to_string(),
),
DecryptedData::Audio(blob) => IdbObject::new().audio().expiration_text(expires).data(blob),
DecryptedData::Video(blob) => IdbObject::new().video().expiration_text(expires).data(blob),
DecryptedData::Archive(blob, entries) => IdbObject::new()
.archive()
.expiration_text(expires)
.data(blob)
.extra(
"entries",
JsValue::from(
entries
.iter()
.filter_map(|x| serde_wasm_bindgen::to_value(x).ok())
.collect::<Array>(),
),
),
};
let put_action = transaction
.put_with_key(
&Object::from(decrypted_object),
&JsString::from(location().pathname().unwrap()),
)
.unwrap();
put_action.set_onsuccess(Some(
Closure::once(Box::new(|| {
log!("[rs] Successfully inserted encrypted item into storage.");
let name = name.map(JsString::from);
let language = language.map(JsString::from);
load_from_db(JsString::from(mimetype.0), name, language);
}))
.into_js_value()
.unchecked_ref(),
));
put_action.set_onerror(Some(
Closure::once(Box::new(|e: Event| log!(e)))
.into_js_value()
.unchecked_ref(),
));
}

144
web/src/main.scss Normal file
View file

@ -0,0 +1,144 @@
// OmegaUpload Web Frontend
// Copyright (C) 2021 Edward Shen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
@use 'node_modules/highlight.js/styles/github-dark.css';
$padding: 1em;
@font-face {
font-family: "Mplus Code";
src: url("../vendor/MPLUS_FONTS/fonts/ttf/MPLUSCodeLatin[wdth,wght].ttf") format("truetype");
}
body {
background-color: #404040;
margin: 0;
}
.unselectable {
user-select: none;
}
hr {
@extend .hljs;
margin: $padding 0;
}
main {
display: inline-flex;
min-width: 100%;
max-width: 100%;
justify-content: center;
}
th {
font-weight: normal;
}
.paste {
@extend .hljs;
border-radius: $padding;
margin: $padding;
padding: 2 * $padding;
box-shadow: 0 0 $padding black;
min-width: 120ch;
}
.hljs {
font-family: 'Mplus Code', sans-serif;
}
.align-right {
text-align: right;
}
.hljs-ln td.hljs-ln-numbers {
@extend .align-right;
padding-right: $padding;
}
.centered {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.fullscreen {
min-height: 100vh;
min-width: 100vw;
margin: 0;
}
.display-anyways {
margin-top: 4em;
text-decoration: underline;
cursor: pointer;
}
img,
audio,
video {
border-radius: $padding;
margin-bottom: $padding;
max-height: 75vh;
max-width: 75vw;
}
textarea {
@extend .paste;
height: 75vh;
border-color: white;
}
.primary {
@extend .hljs;
}
.archive {
&-table {
width: 100%;
}
&-file-size {
@extend .align-right;
padding-left: $padding;
}
}
.button {
@extend .hljs;
font-size: 16px;
text-decoration: underline;
border: none;
&:hover {
cursor: pointer;
}
}
.file-upload {
@extend .button;
input {
display: none;
}
}
.text-upload {
@extend .button;
}

363
web/src/render.tsx Normal file
View file

@ -0,0 +1,363 @@
// OmegaUpload Web Frontend
// Copyright (C) 2021 Edward Shen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import ReactDom from 'react-dom';
import React, { useState } from 'react';
let hljs;
if (typeof WorkerGlobalScope === 'undefined' || !(self instanceof WorkerGlobalScope)) {
hljs = require('highlight.js');
(window as any).hljs = hljs;
require('highlightjs-line-numbers.js');
}
const FileForm = () => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
let file = event.target.files![0];
const fr = new FileReader();
fr.onload = (_e) => {
encryptMessage(new Uint8Array(fr.result as ArrayBuffer));
}
fr.readAsArrayBuffer(file);
}
return <>
<label className="file-upload hljs-meta" >
Select a file
<input type="file" onChange={handleChange} />
</label>
</>
}
const PasteForm = () => {
const [data, setValue] = useState("");
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (data.trim() !== "") {
encryptMessage(new TextEncoder().encode(data));
} else {
console.log("[js] Not sending string because it was empty.");
}
}
return (
<form className='hljs centered' onSubmit={handleSubmit}>
<textarea
placeholder="すいちゃんは~ 今日もかわい~!!"
value={data}
onChange={(e) => setValue(e.target.value)}
/>
<input className="text-upload hljs-meta" type="submit" value="Submit" />
</form>
)
}
function encryptMessage(data: Uint8Array) {
const worker = new Worker(new URL('./bg_encrypt.ts', import.meta.url));
worker.onmessage = (event: MessageEvent<string>) => {
console.log(event);
if (event.data === 'init') {
console.log("[js] Sending data to worker");
const message = { data, location: window.location.toString() };
worker.postMessage(message, [message.data.buffer]);
} else {
window.location.assign(event.data);
}
}
}
function createUploadUi() {
const html = <main className='hljs centered fullscreen'>
<FileForm />
<p>or paste your data below</p>
<PasteForm />
</main>;
ReactDom.render(html, document.body);
}
function loadFromDb(mimeType: string, name?: string, language?: string) {
let resolvedName: string;
if (name) {
resolvedName = name;
} else {
const pathName = window.location.pathname;
const leafIndex = pathName.indexOf("/");
resolvedName = pathName.slice(leafIndex + 1);
}
console.log("[js] Resolved name:", resolvedName);
console.log("[js] Got language:", language);
console.log("[js] Got mime type:", mimeType);
const dbReq = window.indexedDB.open("omegaupload", 1);
dbReq.onsuccess = (evt) => {
const db = (evt.target as IDBRequest).result;
const obj_store = db
.transaction("decrypted data")
.objectStore("decrypted data");
const fetchReq = obj_store.get(window.location.pathname);
fetchReq.onsuccess = (evt) => {
const data = (evt.target as IDBRequest).result;
switch (data.type) {
case "string":
console.info("[js] Rendering string UI.");
createStringPasteUi(data, mimeType, resolvedName, language);
break;
case "blob":
console.info("[js] Rendering blob UI.");
createBlobPasteUi(data, resolvedName);
break;
case "image":
console.info("[js] Rendering image UI.");
createImagePasteUi(data, resolvedName, mimeType);
break;
case "audio":
console.info("[js] Rendering audio UI.");
createAudioPasteUi(data, resolvedName, mimeType);
break;
case "video":
console.info("[js] Rendering video UI.");
createVideoPasteUi(data, resolvedName, mimeType);
break;
case "archive":
console.info("[js] Rendering archive UI.");
createArchivePasteUi(data, resolvedName);
break;
default:
console.info("[js] Rendering unknown UI.");
renderMessage("Something went wrong. Try clearing local data.");
break;
}
// IDB was only used as a temporary medium;
window.onbeforeunload = (_e) => {
// See https://link.eddie.sh/NrIIq on why .commit is necessary.
const transaction = db.transaction("decrypted data", "readwrite");
transaction
.objectStore("decrypted data")
.delete(window.location.pathname);
transaction.commit();
transaction.oncomplete = () => {
console.log("Item deleted from cache");
}
};
};
fetchReq.onerror = (evt) => {
console.log("err");
console.log(evt);
};
};
}
function createStringPasteUi(data, mimeType: string, name: string, lang?: string, skipSyntaxHighlight?: boolean) {
const html = <main>
<pre className='paste'>
<p className='unselectable centered'>{data.expiration}</p>
<a href={getObjectUrl([data.data], mimeType)} download={name} className='hljs-meta centered'>
Download file.
</a>
<hr />
<code>
{data.data}
</code>
</pre>
</main>;
ReactDom.render(html, document.body);
if (skipSyntaxHighlight) {
return;
}
let languages = undefined;
if (!hljs.getLanguage(lang)) {
if (lang) {
console.warn(`[js] User provided language (${lang}) is not known.`);
} else {
console.info(`[js] Language hint not provided.`);
}
} else {
languages = [lang];
}
// If a language wasn't provided, see if we can use the file extension to give
// us a better hint for hljs
if (!languages) {
if (name) {
console.log("[js] Trying to infer from file name...");
const periodIndex = name.indexOf(".");
if (periodIndex === -1) {
console.warn("[js] Did not find file extension.")
} else {
let extension = name.slice(periodIndex + 1);
console.info(`[js] Found extension ${extension}.`);
if (!hljs.getLanguage(extension)) {
console.warn(`[js] Extension was not recognized by hljs. Giving up.`);
} else {
console.info("[js] Successfully inferred language from file extension.");
languages = [extension];
}
}
} else {
console.log("[js] No file name hint provided.");
}
} else {
console.info(`[js] Selecting user provided language ${languages[0]} for highlighting.`);
}
// If we still haven't set languages here, then we're leaving it up to the
// library
if (!languages) {
console.log("[js] Deferring to hljs inference for syntax highlighting.");
} else {
hljs.configure({ languages });
}
hljs.highlightAll();
(hljs as any).initLineNumbersOnLoad();
}
function createBlobPasteUi(data, name: string) {
const html = <main className='hljs centered fullscreen'>
<div className='centered'>
<p>{data.expiration}</p>
<a href={getObjectUrl(data.data, name)} download={name} className='hljs-meta'>
Download binary file.
</a>
</div>
<p className='display-anyways hljs-comment' onClick={() => {
data.data.text().then(text => {
data.data = text;
createStringPasteUi(data, "application/octet-stream", name, undefined, true);
})
}}>Display anyways?</p>
</main>;
ReactDom.render(html, document.body);
}
function createImagePasteUi({ expiration, data, file_size }, name: string, mimeType: string) {
createMultiMediaPasteUi("img", expiration, data, name, mimeType, (downloadEle, imgEle) => {
imgEle.onload = () => {
const width = imgEle.naturalWidth || imgEle.width;
const height = imgEle.naturalHeight || imgEle.height;
downloadEle.textContent = "Download " + file_size + " \u2014 " + width + " by " + height;
}
});
}
function createAudioPasteUi({ expiration, data }, name: string, mimeType: string) {
createMultiMediaPasteUi("audio", expiration, data, name, mimeType, "Download");
}
function createVideoPasteUi({ expiration, data }, name: string, mimeType: string) {
createMultiMediaPasteUi("video", expiration, data, name, mimeType, "Download");
}
function createArchivePasteUi({ expiration, data, entries }, name: string) {
// Because it's a stable sort, we can first sort by name (to get all folder
// items grouped together) and then sort by if there's a / or not.
entries.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
// This doesn't get sub directories and their folders, but hey it's close
// enough
entries.sort((a, b) => {
return b.name.includes("/") - a.name.includes("/");
});
const html = <main>
<section className='paste'>
<p className='centered'>{expiration}</p>
<a href={getObjectUrl(data)} download={name} className='hljs-meta centered'>Download</a>
<hr />
<table className='archive-table'>
<thead>
<tr className='hljs-title'><th>Name</th><th className='align-right'>File Size</th></tr>
</thead>
<tbody>
{
entries.map(({ name, file_size }) => {
return <tr><td>{name}</td><td className='align-right hljs-number'>{file_size}</td></tr>;
})
}
</tbody>
</table>
</section>
</main>;
ReactDom.render(html, document.body);
}
function createMultiMediaPasteUi(tag, expiration, data, name: string, mimeType: string, on_create?: Function | string) {
const bodyEle = document.body;
bodyEle.textContent = '';
const mainEle = document.createElement("main");
mainEle.classList.add("hljs");
mainEle.classList.add("centered");
mainEle.classList.add("fullscreen");
const downloadLink = getObjectUrl(data, mimeType);
const expirationEle = document.createElement("p");
expirationEle.textContent = expiration;
mainEle.appendChild(expirationEle);
const mediaEle = document.createElement(tag);
mediaEle.src = downloadLink;
mediaEle.controls = true;
mainEle.appendChild(mediaEle);
const downloadEle = document.createElement("a");
downloadEle.href = downloadLink;
downloadEle.download = name;
downloadEle.classList.add("hljs-meta");
mainEle.appendChild(downloadEle);
bodyEle.appendChild(mainEle);
if (on_create instanceof Function) {
on_create(downloadEle, mediaEle);
} else {
downloadEle.textContent = on_create;
}
}
function renderMessage(message) {
ReactDom.render(
<main className='hljs centered fullscreen'>
{message}
</main>,
document.body,
);
}
function getObjectUrl(data, mimeType?: string) {
return URL.createObjectURL(new Blob([data], { type: mimeType }));
}
export { renderMessage, createUploadUi, loadFromDb };

View file

@ -1,3 +1,19 @@
// OmegaUpload Web Frontend
// Copyright (C) 2021 Edward Shen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use web_sys::{Event, IdbDatabase, IdbOpenDbRequest}; use web_sys::{Event, IdbDatabase, IdbOpenDbRequest};

View file

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Omegaupload</title>
<!-- <script src="highlightjs-line-numbers.min.js" defer></script> -->
</head>
<body>
<script src="index.js"></script>
</body>
</html>

View file

@ -1,30 +0,0 @@
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
wasm_bindgen_test_configure!(run_in_browser);
// This runs a unit test in native Rust, so it can only use Rust APIs.
#[test]
fn rust_test() {
assert_eq!(1, 1);
}
// This runs a unit test in the browser, so it can use browser APIs.
#[wasm_bindgen_test]
fn web_test() {
assert_eq!(1, 1);
}
// This runs a unit test in the browser, and in addition it supports asynchronous Future APIs.
#[wasm_bindgen_test(async)]
async fn async_test() -> Result<(), JsValue> {
// Creates a JavaScript Promise which will asynchronously resolve with the value 42.
let promise = js_sys::Promise::resolve(&JsValue::from(42));
// Converts that Promise into a Future.
// The unit test will wait for the Future to resolve.
JsFuture::from(promise).map(|x| {
assert_eq!(x, 42);
})
}

View file

@ -1,11 +0,0 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": false,
"module": "esnext",
"target": "es5",
"allowJs": true,
"moduleResolution": "node",
"sourceMap": true,
}
}

1
web/vendor/MPLUS_FONTS vendored Submodule

@ -0,0 +1 @@
Subproject commit a1268635894c5ee23dfdece570418ca07b66c3fc

View file

@ -1,51 +0,0 @@
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const dist = path.resolve(__dirname, "dist");
module.exports = {
mode: "development",
entry: {
index: "./js/index.js"
},
devtool: 'inline-source-map',
output: {
path: dist,
filename: "[name].js"
},
devServer: {
static: {
directory: dist,
},
proxy: {
'/api': {
target: 'http://localhost:8081',
pathRewrite: { '^/api': '' }
}
},
watchFiles: ['src/**', 'js/**'],
},
plugins: [
new CopyPlugin({
patterns: [
path.resolve(__dirname, "static")
]
}),
new WasmPackPlugin({
crateDirectory: __dirname,
}),
],
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
}
]
},
experiments: {
asyncWebAssembly: true,
},
};

File diff suppressed because it is too large Load diff

View file

@ -1,43 +0,0 @@
[package]
name = "omegaupload-web"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
omegaupload-common = { path = "../common", features = ["wasm"] }
# Enables wasm support
getrandom = { version = "*", features = ["js"] }
anyhow = "1"
bytes = "1"
byte-unit = "4"
console_error_panic_hook = "0.1"
gloo-console = "0.1"
http = "0.2"
js-sys = "0.3"
reqwasm = "0.2"
tree_magic_mini = { version = "3", features = ["with-gpl-data"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
[dependencies.web-sys]
version = "0.3"
features = [
"TextDecoder",
"IdbFactory",
"IdbOpenDbRequest",
"IdbRequest",
"IdbDatabase",
"IdbObjectStore",
"IdbTransaction",
"IdbTransactionMode",
"IdbIndex",
"IdbIndexParameters",
"Event",
"EventTarget",
"Window",
"Performance",
"Location",
]

View file

@ -1,10 +0,0 @@
Contains the codebase used for the frontend
Notes on licensing:
https://www.gnu.org/licenses/gpl-faq.html#AllCompatibility
Because there is a statically linked in dependency on `shared-mime-types`, this
crate MUST be under a GPLv2 or later license. This has been confirmed as of
2021-10-24.

View file

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Omegaupload</title>
<link data-trunk rel="rust" data-wasm-opt="0" data-keep-debug="true" data-no-mangle="true" />
<link data-trunk rel="copy-file" href="vendor/MPLUS_FONTS/fonts/ttf/MplusCodeLatin[wdth,wght].ttf" dest="/" />
<link data-trunk rel="copy-file" href="vendor/highlight.min.js" dest="/" />
<link data-trunk rel="copy-file" href="vendor/highlightjs-line-numbers.js/dist/highlightjs-line-numbers.min.js"
dest="/" />
<link data-trunk rel="scss" href="src/main.scss" />
<script src="main.js" async></script>
<script src="highlight.min.js" defer></script>
<script src="highlightjs-line-numbers.min.js" defer></script>
</head>
</html>

View file

@ -1,105 +0,0 @@
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use gloo_console::log;
use js_sys::{Array, Uint8Array};
use omegaupload_common::crypto::{open_in_place, Key, Nonce};
use wasm_bindgen::JsCast;
use web_sys::Blob;
#[derive(Clone)]
pub enum DecryptedData {
String(Arc<String>),
Blob(Arc<Blob>),
Image(Arc<Blob>, usize),
Audio(Arc<Blob>),
Video(Arc<Blob>),
}
fn now() -> f64 {
web_sys::window()
.expect("should have a Window")
.performance()
.expect("should have a Performance")
.now()
}
pub fn decrypt(
mut container: Vec<u8>,
key: Key,
nonce: Nonce,
maybe_password: Option<Key>,
) -> Result<DecryptedData, PasteCompleteConstructionError> {
let container = &mut container;
log!("Stage 1 decryption started.");
let start = now();
if let Some(password) = maybe_password {
crate::render_message("Decrypting Stage 1...".into());
open_in_place(container, &nonce.increment(), &password).map_err(|_| {
crate::render_message("Unable to decrypt paste with the provided password.".into());
PasteCompleteConstructionError::StageOneFailure
})?;
}
log!(format!("Stage 1 completed in {}ms", now() - start));
log!("Stage 2 decryption started.");
let start = now();
crate::render_message("Decrypting Stage 2...".into());
open_in_place(container, &nonce, &key).map_err(|_| {
crate::render_message(
"Unable to decrypt paste with the provided encryption key and nonce.".into(),
);
PasteCompleteConstructionError::StageTwoFailure
})?;
log!(format!("Stage 2 completed in {}ms", now() - start));
if let Ok(decrypted) = std::str::from_utf8(container) {
Ok(DecryptedData::String(Arc::new(decrypted.to_owned())))
} else {
log!("Blob conversion started.");
let start = now();
let blob_chunks = Array::new_with_length(container.chunks(65536).len().try_into().unwrap());
for (i, chunk) in container.chunks(65536).enumerate() {
let array = Uint8Array::new_with_length(chunk.len().try_into().unwrap());
array.copy_from(chunk);
blob_chunks.set(i.try_into().unwrap(), array.dyn_into().unwrap());
}
let blob =
Arc::new(Blob::new_with_u8_array_sequence(blob_chunks.dyn_ref().unwrap()).unwrap());
log!(format!("Blob conversion completed in {}ms", now() - start));
let mime_type = tree_magic_mini::from_u8(container);
if mime_type.starts_with("image/") || mime_type == "application/x-riff" {
Ok(DecryptedData::Image(blob, container.len()))
} else if mime_type.starts_with("audio/") {
Ok(DecryptedData::Audio(blob))
} else if mime_type.starts_with("video/") || mime_type == "application/x-matroska" {
Ok(DecryptedData::Video(blob))
} else {
Ok(DecryptedData::Blob(blob))
}
}
}
#[derive(Debug)]
pub enum PasteCompleteConstructionError {
StageOneFailure,
StageTwoFailure,
}
impl std::error::Error for PasteCompleteConstructionError {}
impl Display for PasteCompleteConstructionError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
PasteCompleteConstructionError::StageOneFailure => {
write!(f, "Failed to decrypt stage one.")
}
PasteCompleteConstructionError::StageTwoFailure => {
write!(f, "Failed to decrypt stage two.")
}
}
}
}

View file

@ -1,95 +0,0 @@
use std::{hint::unreachable_unchecked, marker::PhantomData};
use js_sys::{Array, JsString, Object};
use wasm_bindgen::JsValue;
pub struct IdbObject<State>(Array, PhantomData<State>);
impl<State: IdbObjectState> IdbObject<State> {
fn add_tuple<NextState>(self, key: &str, value: &JsValue) -> IdbObject<NextState> {
let array = Array::new();
array.push(&JsString::from(key));
array.push(value);
self.0.push(&array);
IdbObject(self.0, PhantomData)
}
}
impl From<IdbObject<Ready>> for Object {
fn from(db_object: IdbObject<Ready>) -> Self {
match Self::from_entries(db_object.as_ref()) {
Ok(o) => o,
// SAFETY: IdbObject maintains the invariant that it can eventually
// be constructed into a JS object.
_ => unsafe { unreachable_unchecked() },
}
}
}
impl IdbObject<NeedsType> {
pub fn new() -> Self {
Self(Array::new(), PhantomData)
}
pub fn video(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("video"))
}
pub fn audio(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("audio"))
}
pub fn image(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("image"))
}
pub fn blob(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("blob"))
}
pub fn string(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("string"))
}
}
impl Default for IdbObject<NeedsType> {
fn default() -> Self {
Self::new()
}
}
impl IdbObject<NeedsExpiration> {
pub fn expiration_text(self, expires: &str) -> IdbObject<NeedsData> {
self.add_tuple("expiration", &JsString::from(expires))
}
}
impl IdbObject<NeedsData> {
pub fn data(self, value: &JsValue) -> IdbObject<Ready> {
self.add_tuple("data", value)
}
}
impl IdbObject<Ready> {
pub fn extra(self, key: &str, value: impl Into<JsValue>) -> Self {
self.add_tuple(key, &value.into())
}
}
impl AsRef<JsValue> for IdbObject<Ready> {
fn as_ref(&self) -> &JsValue {
self.0.as_ref()
}
}
macro_rules! impl_idb_object_state {
($($ident:ident),*) => {
pub trait IdbObjectState {}
$(
pub enum $ident {}
impl IdbObjectState for $ident {}
)*
};
}
impl_idb_object_state!(NeedsType, NeedsExpiration, NeedsData, Ready);

View file

@ -1,251 +0,0 @@
#![warn(clippy::nursery, clippy::pedantic)]
use std::str::FromStr;
use anyhow::{anyhow, bail, Context, Result};
use byte_unit::{n_mib_bytes, Byte};
use decrypt::DecryptedData;
use gloo_console::{error, log};
use http::uri::PathAndQuery;
use http::{StatusCode, Uri};
use js_sys::{JsString, Object, Uint8Array};
use omegaupload_common::crypto::{Key, Nonce};
use omegaupload_common::{hash, Expiration, PartialParsedUrl};
use reqwasm::http::Request;
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{Event, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode, Location, Window};
use crate::decrypt::decrypt;
use crate::idb_object::IdbObject;
use crate::util::as_idb_db;
mod decrypt;
mod idb_object;
mod util;
const DOWNLOAD_SIZE_LIMIT: u128 = n_mib_bytes!(500);
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = renderIndex)]
pub fn render_index();
#[wasm_bindgen(js_name = loadFromDb)]
pub fn load_from_db();
#[wasm_bindgen(js_name = renderMessage)]
pub fn render_message(message: JsString);
}
fn window() -> Window {
web_sys::window().expect("Failed to get a reference of the window")
}
fn location() -> Location {
window().location()
}
fn open_idb() -> Result<IdbOpenDbRequest> {
window()
.indexed_db()
.unwrap()
.context("Missing browser idb impl")?
.open("omegaupload")
.map_err(|_| anyhow!("Failed to open idb"))
}
fn main() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
if location().pathname().unwrap() == "/" {
render_index();
} else {
render_message("Loading paste...".into());
let url = String::from(location().to_string());
let request_uri = {
let mut uri_parts = url.parse::<Uri>().unwrap().into_parts();
if let Some(parts) = uri_parts.path_and_query.as_mut() {
*parts = PathAndQuery::from_str(&format!("/api{}", parts.path())).unwrap();
}
Uri::from_parts(uri_parts).unwrap()
};
log!(&url);
log!(&request_uri.to_string());
log!(&location().pathname().unwrap());
let (key, nonce, needs_pw) = {
let partial_parsed_url = url
.split_once('#')
.map(|(_, fragment)| PartialParsedUrl::from(fragment))
.unwrap_or_default();
let key = if let Some(key) = partial_parsed_url.decryption_key {
key
} else {
error!("Key is missing in url; bailing.");
render_message("Invalid paste link: Missing decryption key.".into());
return;
};
let nonce = if let Some(nonce) = partial_parsed_url.nonce {
nonce
} else {
error!("Nonce is missing in url; bailing.");
render_message("Invalid paste link: Missing nonce.".into());
return;
};
(key, nonce, partial_parsed_url.needs_password)
};
let password = if needs_pw {
loop {
let pw =
window().prompt_with_message("A password is required to decrypt this paste:");
if let Ok(Some(password)) = pw {
if !password.is_empty() {
break Some(hash(password));
}
}
}
} else {
None
};
spawn_local(async move {
if let Err(e) = fetch_resources(request_uri, key, nonce, password).await {
log!(e.to_string());
}
});
}
}
#[allow(clippy::future_not_send)]
async fn fetch_resources(
request_uri: Uri,
key: Key,
nonce: Nonce,
password: Option<Key>,
) -> Result<()> {
match Request::get(&request_uri.to_string()).send().await {
Ok(resp) if resp.status() == StatusCode::OK => {
let expires = Expiration::try_from(resp.headers()).map_or_else(
|_| "This item does not expire.".to_string(),
|expires| expires.to_string(),
);
let data = {
let data_fut = resp
.as_raw()
.array_buffer()
.expect("to get raw bytes from a response");
let data = match JsFuture::from(data_fut).await {
Ok(data) => data,
Err(e) => {
render_message(
"Network failure: Failed to completely read encryption paste.".into(),
);
bail!(format!(
"JsFuture returned an error while fetching resp buffer: {:?}",
e
));
}
};
Uint8Array::new(&data).to_vec()
};
if data.len() as u128 > DOWNLOAD_SIZE_LIMIT {
render_message("The paste is too large to decrypt from the web browser. You must use the CLI tool to download this paste.".into());
return Ok(());
}
let decrypted = decrypt(data, key, nonce, password)?;
let db_open_req = open_idb()?;
// On success callback
let on_success = Closure::once(Box::new(move |event: Event| {
let transaction: IdbObjectStore = as_idb_db(&event)
.transaction_with_str_and_mode("decrypted data", IdbTransactionMode::Readwrite)
.unwrap()
.object_store("decrypted data")
.unwrap();
let decrypted_object = match &decrypted {
DecryptedData::String(s) => IdbObject::new()
.string()
.expiration_text(&expires)
.data(&JsValue::from_str(s)),
DecryptedData::Blob(blob) => {
IdbObject::new().blob().expiration_text(&expires).data(blob)
}
DecryptedData::Image(blob, size) => IdbObject::new()
.image()
.expiration_text(&expires)
.data(blob)
.extra(
"file_size",
Byte::from_bytes(*size as u128)
.get_appropriate_unit(true)
.to_string(),
),
DecryptedData::Audio(blob) => IdbObject::new()
.audio()
.expiration_text(&expires)
.data(blob),
DecryptedData::Video(blob) => IdbObject::new()
.video()
.expiration_text(&expires)
.data(blob),
};
let put_action = transaction
.put_with_key(
&Object::from(decrypted_object),
&JsString::from(location().pathname().unwrap()),
)
.unwrap();
put_action.set_onsuccess(Some(
Closure::wrap(Box::new(|| {
log!("success");
load_from_db();
}) as Box<dyn Fn()>)
.into_js_value()
.unchecked_ref(),
));
put_action.set_onerror(Some(
Closure::wrap(Box::new(|e| {
log!(e);
}) as Box<dyn Fn(Event)>)
.into_js_value()
.unchecked_ref(),
));
}) as Box<dyn FnOnce(Event)>);
db_open_req.set_onsuccess(Some(on_success.into_js_value().unchecked_ref()));
db_open_req.set_onerror(Some(
Closure::wrap(Box::new(|e| {
log!(e);
}) as Box<dyn Fn(Event)>)
.into_js_value()
.unchecked_ref(),
));
let on_upgrade = Closure::wrap(Box::new(move |event: Event| {
let db = as_idb_db(&event);
let _obj_store = db.create_object_store("decrypted data").unwrap();
}) as Box<dyn FnMut(Event)>);
db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().unchecked_ref()));
}
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
render_message("Either the paste was burned or it never existed.".into());
}
Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => {
render_message("Invalid paste URL.".into());
}
Ok(err) => {
render_message(err.status_text().into());
}
Err(err) => {
render_message(format!("{}", err).into());
}
}
Ok(())
}

View file

@ -1,76 +0,0 @@
@use '../vendor/highlight.js/src/styles/github-dark.css';
$padding: 1em;
@font-face {
font-family: "Mplus Code";
src: url("./MplusCodeLatin[wdth,wght].ttf") format("truetype");
}
body {
background-color: #404040;
margin: 0;
}
.unselectable {
user-select: none;
}
hr {
@extend .hljs;
margin: $padding 0;
}
main {
display: inline-flex;
min-width: 100%;
justify-content: center;
}
.paste {
@extend .hljs;
border-radius: $padding;
margin: $padding;
padding: 2 * $padding;
box-shadow: 0 0 $padding black;
min-width: 120ch;
}
.hljs {
font-family: 'Mplus Code', sans-serif;
}
.hljs-ln td.hljs-ln-numbers {
text-align: right;
padding-right: $padding;
}
.centered {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.fullscreen {
min-height: 100vh;
min-width: 100vw;
margin: 0;
}
.display-anyways {
margin-top: 4em;
text-decoration: underline;
cursor: pointer;
}
img, audio, video {
border-radius: $padding;
margin-bottom: $padding;
max-height: 75vh;
max-width: 75vw;
}
.primary {
@extend .hljs;
}

View file

@ -1,194 +0,0 @@
window.addEventListener("hashchange", () => location.reload());
// Exported to main.rs
function loadFromDb() {
const dbReq = window.indexedDB.open("omegaupload", 1);
dbReq.onsuccess = (evt) => {
const db = (evt.target as IDBRequest).result;
const obj_store = db
.transaction("decrypted data")
.objectStore("decrypted data");
let fetchReq = obj_store.get(window.location.pathname);
fetchReq.onsuccess = (evt) => {
const data = (evt.target as IDBRequest).result;
switch (data.type) {
case "string":
createStringPasteUi(data);
break;
case "blob":
createBlobPasteUi(data);
break;
case "image":
createImagePasteUi(data);
break;
case "audio":
createAudioPasteUi(data);
break;
case "video":
createVideoPasteUi(data);
break;
default:
renderMessage("Something went wrong. Try clearing local data.");
break;
}
// IDB was only used as a temporary medium;
window.onbeforeunload = (e) => {
// See https://link.eddie.sh/NrIIq on why .commit is necessary.
const transaction = db.transaction("decrypted data", "readwrite");
transaction
.objectStore("decrypted data")
.delete(window.location.pathname);
transaction.commit();
transaction.oncomplete = () => {
console.log("Item deleted from cache");
}
};
};
fetchReq.onerror = (evt) => {
console.log("err");
console.log(evt);
};
};
}
function createStringPasteUi(data) {
let bodyEle = document.getElementsByTagName("body")[0];
bodyEle.textContent = '';
let mainEle = document.createElement("main");
let preEle = document.createElement("pre");
preEle.classList.add("paste");
let headerEle = document.createElement("header");
headerEle.classList.add("unselectable");
headerEle.textContent = data.expiration;
preEle.appendChild(headerEle);
preEle.appendChild(document.createElement("hr"));
let codeEle = document.createElement("code");
codeEle.textContent = data.data;
preEle.appendChild(codeEle);
mainEle.appendChild(preEle);
bodyEle.appendChild(mainEle);
hljs.highlightAll();
hljs.initLineNumbersOnLoad();
}
function createBlobPasteUi(data) {
let bodyEle = document.getElementsByTagName("body")[0];
bodyEle.textContent = '';
let mainEle = document.createElement("main");
mainEle.classList.add("hljs");
mainEle.classList.add("centered");
mainEle.classList.add("fullscreen");
let divEle = document.createElement("div");
divEle.classList.add("centered");
let expirationEle = document.createElement("p");
expirationEle.textContent = data.expiration;
divEle.appendChild(expirationEle);
let downloadEle = document.createElement("a");
downloadEle.href = URL.createObjectURL(data.data);
downloadEle.download = window.location.pathname;
downloadEle.classList.add("hljs-meta");
downloadEle.textContent = "Download binary file.";
divEle.appendChild(downloadEle);
mainEle.appendChild(divEle);
let displayAnywayEle = document.createElement("p");
displayAnywayEle.classList.add("display-anyways");
displayAnywayEle.classList.add("hljs-comment");
displayAnywayEle.textContent = "Display anyways?";
displayAnywayEle.onclick = () => {
data.data.text().then(text => {
data.data = text;
createStringPasteUi(data);
})
};
mainEle.appendChild(displayAnywayEle);
bodyEle.appendChild(mainEle);
}
function createImagePasteUi({ expiration, data, file_size }) {
createMultiMediaPasteUi("img", expiration, data, (downloadEle, imgEle) => {
imgEle.onload = () => {
downloadEle.textContent = "Download " + file_size + " \u2014 " + imgEle.naturalWidth + " by " + imgEle.naturalHeight;
}
});
}
function createAudioPasteUi({ expiration, data }) {
createMultiMediaPasteUi("audio", expiration, data, "Download");
}
function createVideoPasteUi({ expiration, data }) {
createMultiMediaPasteUi("video", expiration, data, "Download");
}
function createMultiMediaPasteUi(tag, expiration, data, on_create?) {
let bodyEle = document.getElementsByTagName("body")[0];
bodyEle.textContent = '';
let mainEle = document.createElement("main");
mainEle.classList.add("hljs");
mainEle.classList.add("centered");
mainEle.classList.add("fullscreen");
const downloadLink = URL.createObjectURL(data);
let expirationEle = document.createElement("p");
expirationEle.textContent = expiration;
mainEle.appendChild(expirationEle);
let mediaEle = document.createElement(tag);
mediaEle.src = downloadLink;
mediaEle.controls = true;
mainEle.appendChild(mediaEle);
let downloadEle = document.createElement("a");
downloadEle.href = downloadLink;
downloadEle.download = window.location.pathname;
downloadEle.classList.add("hljs-meta");
mainEle.appendChild(downloadEle);
bodyEle.appendChild(mainEle);
if (on_create instanceof Function) {
on_create(downloadEle, mediaEle);
} else {
downloadEle.textContent = on_create;
}
}
function renderMessage(message) {
let body = document.getElementsByTagName("body")[0];
body.textContent = '';
let mainEle = document.createElement("main");
mainEle.classList.add("hljs");
mainEle.classList.add("centered");
mainEle.classList.add("fullscreen");
mainEle.textContent = message;
body.appendChild(mainEle);
}
// Export to main.rs
function renderIndex() {
console.log("index");
// TODO: find a way to not hard code this.
// https://docs.rs/chacha20poly1305/0.9.0/chacha20poly1305/type.Key.html
// https://docs.rs/chacha20poly1305/0.9.0/chacha20poly1305/type.XNonce.html
const key = crypto.getRandomValues(new Uint8Array(32));
const nonce = crypto.getRandomValues(new Uint8Array(24));
console.log(key, nonce);
}

View file

@ -1,10 +0,0 @@
use wasm_bindgen::JsCast;
use web_sys::{Event, IdbDatabase, IdbOpenDbRequest};
/// # Panics
///
/// This will panic if event is not an event from the IDB API.
pub fn as_idb_db(event: &Event) -> IdbDatabase {
let target: IdbOpenDbRequest = event.target().map(JsCast::unchecked_into).unwrap();
target.result().map(JsCast::unchecked_into).unwrap()
}

@ -1 +0,0 @@
Subproject commit 6ee9e7ca06f40f2303d839ccac8bfb8b56d2b3cd

@ -1 +0,0 @@
Subproject commit 257cfee803426333af25b68da17601aec2663172

File diff suppressed because one or more lines are too long

@ -1 +0,0 @@
Subproject commit 8480334a29f01ad8b7fb0497c65285872781ee96

53
webpack.config.js Normal file
View file

@ -0,0 +1,53 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const { SourceMapDevToolPlugin } = require('webpack');
module.exports = {
entry: './web/src/index.js',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'swc-loader',
exclude: /node_modules/,
},
{
test: /\.scss$/i,
use: [
// Creates `style` nodes from JS strings
"style-loader",
// Translates CSS into CommonJS
"css-loader",
// Compiles Sass to CSS
"sass-loader",
// source map for debugging
"source-map-loader"
],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
path: path.resolve(__dirname, 'dist/static'),
filename: 'index.js',
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'web/src/index.html'),
publicPath: "/static",
}),
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, "web"),
outDir: path.resolve(__dirname, "web/pkg"),
}),
new SourceMapDevToolPlugin({}),
],
experiments: {
asyncWebAssembly: true,
},
mode: 'development'
};

1631
yarn.lock Normal file

File diff suppressed because it is too large Load diff