Compare commits

..

No commits in common. "253fccaf7879f5dfa2ca34694513e851fec9096b" and "c73af508571dd71897f750a05669c765ac50ca2c" have entirely different histories.

19 changed files with 654 additions and 902 deletions

631
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -113,4 +113,5 @@ There are a few reasons to not use OmegaUpload:
- 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.
- Right now, you must upload via the CLI tool.
- The frontend uses WASM, which is a novel attack surface.

View file

@ -22,6 +22,6 @@ CUR_DIR=$(pwd)
PROJECT_TOP_LEVEL=$(git rev-parse --show-toplevel)
cd "$PROJECT_TOP_LEVEL" || exit 1
git submodule update --remote
git submodule foreach git pull
cd "$CUR_DIR"
cd "$CUR_DIR"

View file

@ -9,9 +9,10 @@ license = "GPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
omegaupload-common = { path = "../common" }
anyhow = "1.0.58"
atty = "0.2.14"
clap = { version = "3.2.15", features = ["derive"] }
reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls", "blocking"] }
rpassword = "7.0.0"
omegaupload-common = "0.2"
anyhow = "1"
atty = "0.2"
clap = { version = "3", features = ["derive"] }
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "blocking"] }
rpassword = "5"

View file

@ -1,4 +1,4 @@
use crate::secrecy::{ExposeSecret, SecretString};
use omegaupload_common::secrecy::{ExposeSecret, SecretString};
pub struct Builder {
decryption_key: SecretString,
@ -8,7 +8,6 @@ pub struct Builder {
}
impl Builder {
#[must_use]
pub fn new(decryption_key: SecretString) -> Self {
Self {
decryption_key,
@ -18,7 +17,6 @@ impl Builder {
}
}
#[must_use]
pub const fn needs_password(mut self) -> Self {
self.needs_password = true;
self
@ -26,7 +24,6 @@ impl Builder {
// 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
@ -34,13 +31,11 @@ impl Builder {
// 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;

View file

@ -24,7 +24,6 @@ use anyhow::{anyhow, bail, Context, Result};
use atty::Stream;
use clap::Parser;
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,
@ -32,7 +31,11 @@ use omegaupload_common::{
use reqwest::blocking::Client;
use reqwest::header::EXPIRES;
use reqwest::StatusCode;
use rpassword::prompt_password;
use rpassword::prompt_password_stderr;
use crate::fragment::Builder;
mod fragment;
#[derive(Parser)]
struct Opts {
@ -117,7 +120,8 @@ fn handle_upload(
}
let password = if password {
let maybe_password = prompt_password("Please set the password for this paste: ")?;
let maybe_password =
prompt_password_stderr("Please set the password for this paste: ")?;
Some(SecretVec::new(maybe_password.into_bytes()))
} else {
None
@ -196,7 +200,8 @@ fn handle_download(mut url: ParsedUrl) -> Result<()> {
let password = if url.needs_password {
// Only print prompt on interactive, else it messes with output
let maybe_password = prompt_password("Please enter the password to access this paste: ")?;
let maybe_password =
prompt_password_stderr("Please enter the password to access this paste: ")?;
Some(SecretVec::new(maybe_password.into_bytes()))
} else {
None

View file

@ -9,24 +9,24 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
base64 = "0.13.0"
bytes = { version = "1.2.0", features = ["serde"] }
chacha20poly1305 = { version = "0.9.1", features = ["stream", "std"] }
chrono = { version = "0.4.19", features = ["serde"] }
headers = "0.3.7"
lazy_static = "1.4.0"
rand = "0.8.5"
secrecy = "0.8.0"
serde = { version = "1.0.140", features = ["derive"] }
thiserror = "1.0.31"
typenum = "1.15.0"
url = "2.2.2"
argon2 = "0.4.1"
base64 = "0.13"
bytes = { version = "1", features = ["serde"] }
chacha20poly1305 = { version = "0.9", features = ["stream", "std"] }
chrono = { version = "0.4", features = ["serde"] }
headers = "0.3"
lazy_static = "1"
rand = "0.8"
secrecy = "0.8"
serde = { version = "1", features = ["derive"] }
thiserror = "1"
typenum = "1"
url = "2"
argon2 = "0.3.1"
# Wasm features
gloo-console = { version = "0.2.1", optional = true }
reqwasm = { version = "0.5.0", optional = true }
http = { version = "0.2.8", optional = true }
gloo-console = { version = "0.2", optional = true }
http = { version = "0.2", optional = true }
web-sys = { version = "0.3", features = ["Headers"], optional = true }
[features]
wasm = ["gloo-console", "reqwasm", "http"]
wasm = ["gloo-console", "http", "web-sys"]

View file

@ -294,43 +294,3 @@ fn get_argon2() -> Argon2<'static> {
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()))
);
}

View file

@ -1,6 +1,4 @@
#![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
@ -41,7 +39,6 @@ use crate::crypto::Key;
pub mod base64;
pub mod crypto;
pub mod fragment;
pub const API_ENDPOINT: &str = "/api";
@ -233,10 +230,10 @@ expiration_from_str! {
impl Display for Expiration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BurnAfterReading | Self::BurnAfterReadingWithDeadline(_) => {
Expiration::BurnAfterReading | Expiration::BurnAfterReadingWithDeadline(_) => {
write!(f, "This item has been burned. You now have the only copy.")
}
Self::UnixTime(time) => write!(
Expiration::UnixTime(time) => write!(
f,
"{}",
time.format("This item will expire on %A, %B %-d, %Y at %T %Z.")
@ -251,7 +248,7 @@ lazy_static! {
impl Header for Expiration {
fn name() -> &'static HeaderName {
&EXPIRATION_HEADER_NAME
&*EXPIRATION_HEADER_NAME
}
fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
@ -285,8 +282,6 @@ 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 {
(&expiration).into()
}
@ -295,12 +290,14 @@ impl From<Expiration> for HeaderValue {
pub struct ParseHeaderValueError;
#[cfg(feature = "wasm")]
impl TryFrom<reqwasm::http::Headers> for Expiration {
impl TryFrom<web_sys::Headers> for Expiration {
type Error = ParseHeaderValueError;
fn try_from(headers: reqwasm::http::Headers) -> Result<Self, Self::Error> {
fn try_from(headers: web_sys::Headers) -> Result<Self, Self::Error> {
headers
.get(http::header::EXPIRES.as_str())
.ok()
.flatten()
.as_deref()
.and_then(|v| Self::try_from(v).ok())
.ok_or(ParseHeaderValueError)

View file

@ -17,8 +17,7 @@
"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"
"react-dom": "^17.0.2"
},
"scripts": {
"build": "webpack --mode production",

View file

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

View file

@ -27,7 +27,7 @@ use axum::extract::{Extension, Path, TypedHeader};
use axum::http::header::EXPIRES;
use axum::http::StatusCode;
use axum::routing::{get, get_service, post};
use axum::Router;
use axum::{AddExtensionLayer, Router};
use chrono::Utc;
use futures::stream::StreamExt;
use headers::HeaderMap;
@ -103,7 +103,7 @@ async fn main() -> Result<()> {
&format!("{API_ENDPOINT}/:code"),
get(paste::<SHORT_CODE_SIZE>).delete(delete::<SHORT_CODE_SIZE>),
)
.layer(axum::Extension(db))
.layer(AddExtensionLayer::new(db))
.into_make_service()
})
.await?;

View file

@ -9,27 +9,27 @@ crate-type = ["cdylib"]
[dependencies]
omegaupload-common = { path = "../common", features = ["wasm"] }
# Enables wasm support
getrandom = { version = "0.2.7", features = ["js"] }
getrandom = { version = "*", features = ["js"] }
anyhow = "1.0.58"
bytes = "1.2.0"
byte-unit = "4.0.14"
console_error_panic_hook = "0.1.7"
gloo-console = "0.2.1"
http = "0.2.8"
js-sys = "0.3.59"
mime_guess = "2.0.4"
reqwasm = "0.5.0"
tree_magic_mini = { version = "3.0.3", features = ["with-gpl-data"] }
serde = { version = "1.0.140", features = ["derive"] }
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"
anyhow = "1"
bytes = "1"
byte-unit = "4"
console_error_panic_hook = "0.1"
gloo-console = "0.2"
http = "0.2"
js-sys = "0.3"
mime_guess = "2"
reqwasm = "0.4"
tree_magic_mini = { version = "3", features = ["with-gpl-data"] }
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2", features = ["serde-serialize"]}
wasm-bindgen-futures = "0.4"
zip = { version = "0.5", default-features = false, features = ["deflate"] }
flate2 = "1.0.22"
tar = "0.4.38"
[dependencies.web-sys]
version = "0.3.59"
version = "0.3"
features = [
"BlobPropertyBag",
"TextDecoder",

View file

@ -25,12 +25,9 @@ use gloo_console::{error, log};
use http::uri::PathAndQuery;
use http::{StatusCode, Uri};
use js_sys::{Array, JsString, Object, Uint8Array};
use omegaupload_common::base64;
use omegaupload_common::crypto::seal_in_place;
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 omegaupload_common::secrecy::{Secret, SecretVec};
use omegaupload_common::{Expiration, PartialParsedUrl};
use reqwasm::http::Request;
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue};
@ -53,8 +50,6 @@ extern "C" {
pub fn load_from_db(mime_type: JsString, name: Option<JsString>, language: Option<JsString>);
#[wasm_bindgen(js_name = renderMessage)]
pub fn render_message(message: JsString);
#[wasm_bindgen(js_name = createUploadUi)]
pub fn create_upload_ui();
}
fn window() -> Window {
@ -80,7 +75,7 @@ pub fn start() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
if location().pathname().unwrap() == "/" {
create_upload_ui();
render_message("Go away".into());
return;
}
@ -171,55 +166,6 @@ pub fn start() {
});
}
#[wasm_bindgen]
#[allow(clippy::future_not_send)]
pub fn encrypt_string(data: String) {
spawn_local(async move {
if let Err(e) = do_encrypt(data.into_bytes()).await {
log!(format!("[rs] Error encrypting string: {}", e));
}
});
}
#[wasm_bindgen]
#[allow(clippy::future_not_send)]
pub fn encrypt_array_buffer(data: Vec<u8>) {
spawn_local(async move {
if let Err(e) = do_encrypt(data).await {
log!(format!("[rs] Error encrypting array buffer: {}", e));
}
});
}
#[allow(clippy::future_not_send)]
async fn do_encrypt(mut data: Vec<u8>) -> Result<()> {
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 s: String = location().to_string().into();
let mut url = Url::from_str(&s)?;
let fragment = Builder::new(key);
let js_data = Uint8Array::new_with_length(u32::try_from(data.len()).expect("Data too large"));
js_data.copy_from(&data);
let short_code = Request::post(url.as_ref())
.body(js_data)
.send()
.await?
.text()
.await?;
url.set_path(&short_code);
url.set_fragment(Some(fragment.build().expose_secret()));
location()
.set_href(url.as_ref())
.expect("Unable to navigate to encrypted upload");
Ok(())
}
#[allow(clippy::future_not_send)]
async fn fetch_resources(
request_uri: Uri,

View file

@ -95,14 +95,6 @@ img, audio, video {
max-width: 75vw;
}
textarea {
width: 100%;
height: 100%;
min-width: 75vw;
min-height: 75vh;
box-sizing: border-box;
}
.primary {
@extend .hljs;
}
@ -116,4 +108,4 @@ textarea {
@extend .align-right;
padding-left: $padding;
}
}
}

View file

@ -16,64 +16,14 @@
import './main.scss';
import ReactDom from 'react-dom';
import React, { useState } from 'react';
import { encrypt_string, encrypt_array_buffer } from '../pkg';
import React from 'react';
import hljs from 'highlight.js'
const hljs = require('highlight.js');
(window as any).hljs = hljs;
require('highlightjs-line-numbers.js');
const FileForm = () => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const fr = new FileReader();
fr.onload = (e) => {
const { result } = e.target;
if (result instanceof ArrayBuffer) {
let data = new Uint8Array(result);
encrypt_array_buffer(data);
}
}
fr.readAsArrayBuffer((event.target as HTMLInputElement).files[0]);
}
return (
<input type="file" onChange={handleChange} />
)
}
const PasteForm = () => {
const [value, setValue] = useState("");
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
encrypt_string(value);
}
return (
<pre className='paste'>
<form className='hljs centered' onSubmit={handleSubmit}>
<textarea
placeholder="Sample text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<input type="submit" value="submit" />
</form>
</pre>
)
}
function createUploadUi() {
const html = <main className='hljs centered fullscreen'>
<FileForm />
<PasteForm />
</main>;
ReactDom.render(html, document.body);
}
function loadFromDb(mimeType: string, name?: string, language?: string) {
let resolvedName: string;
let resolvedName;
if (name) {
resolvedName = name;
} else {
@ -337,4 +287,4 @@ function getObjectUrl(data, mimeType?: string) {
window.addEventListener("hashchange", () => location.reload());
export { renderMessage, createUploadUi, loadFromDb };
export { renderMessage, loadFromDb };

@ -1 +1 @@
Subproject commit 8690be3625964d9992e7be4bc3e1a61a80161cc6
Subproject commit a1268635894c5ee23dfdece570418ca07b66c3fc

View file

@ -2,7 +2,6 @@ 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',
@ -22,8 +21,6 @@ module.exports = {
"css-loader",
// Compiles Sass to CSS
"sass-loader",
// source map for debugging
"source-map-loader"
],
},
],
@ -44,7 +41,6 @@ module.exports = {
crateDirectory: path.resolve(__dirname, "web"),
outDir: path.resolve(__dirname, "web/pkg"),
}),
new SourceMapDevToolPlugin({}),
],
experiments: {
asyncWebAssembly: true,

587
yarn.lock

File diff suppressed because it is too large Load diff