Compare commits

..

16 commits

Author SHA1 Message Date
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
19 changed files with 905 additions and 657 deletions

637
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -113,5 +113,4 @@ 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 foreach git pull
git submodule update --remote
cd "$CUR_DIR"

View file

@ -9,10 +9,9 @@ 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 = "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"
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"

View file

@ -24,6 +24,7 @@ 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,
@ -31,11 +32,7 @@ use omegaupload_common::{
use reqwest::blocking::Client;
use reqwest::header::EXPIRES;
use reqwest::StatusCode;
use rpassword::prompt_password_stderr;
use crate::fragment::Builder;
mod fragment;
use rpassword::prompt_password;
#[derive(Parser)]
struct Opts {
@ -120,8 +117,7 @@ fn handle_upload(
}
let password = if password {
let maybe_password =
prompt_password_stderr("Please set the password for this paste: ")?;
let maybe_password = prompt_password("Please set the password for this paste: ")?;
Some(SecretVec::new(maybe_password.into_bytes()))
} else {
None
@ -200,8 +196,7 @@ 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_stderr("Please enter the password to access this paste: ")?;
let maybe_password = prompt_password("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"
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"
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"
# Wasm features
gloo-console = { version = "0.2", optional = true }
http = { version = "0.2", optional = true }
web-sys = { version = "0.3", features = ["Headers"], optional = true }
gloo-console = { version = "0.2.1", optional = true }
reqwasm = { version = "0.5.0", optional = true }
http = { version = "0.2.8", optional = true }
[features]
wasm = ["gloo-console", "http", "web-sys"]
wasm = ["gloo-console", "reqwasm", "http"]

View file

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

@ -1,4 +1,6 @@
#![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
@ -39,6 +41,7 @@ use crate::crypto::Key;
pub mod base64;
pub mod crypto;
pub mod fragment;
pub const API_ENDPOINT: &str = "/api";
@ -230,10 +233,10 @@ expiration_from_str! {
impl Display for Expiration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expiration::BurnAfterReading | Expiration::BurnAfterReadingWithDeadline(_) => {
Self::BurnAfterReading | Self::BurnAfterReadingWithDeadline(_) => {
write!(f, "This item has been burned. You now have the only copy.")
}
Expiration::UnixTime(time) => write!(
Self::UnixTime(time) => write!(
f,
"{}",
time.format("This item will expire on %A, %B %-d, %Y at %T %Z.")
@ -248,7 +251,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>
@ -282,6 +285,8 @@ 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()
}
@ -290,14 +295,12 @@ impl From<Expiration> for HeaderValue {
pub struct ParseHeaderValueError;
#[cfg(feature = "wasm")]
impl TryFrom<web_sys::Headers> for Expiration {
impl TryFrom<reqwasm::http::Headers> for Expiration {
type Error = ParseHeaderValueError;
fn try_from(headers: web_sys::Headers) -> Result<Self, Self::Error> {
fn try_from(headers: reqwasm::http::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,7 +17,8 @@
"highlight.js": "^11.4.0",
"highlightjs-line-numbers.js": "^2.8.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"source-map-loader": "^4.0.0"
},
"scripts": {
"build": "webpack --mode production",

View file

@ -7,24 +7,24 @@ edition = "2021"
[dependencies]
omegaupload-common = { path = "../common" }
anyhow = "1"
axum = { version = "0.4", features = ["http2", "headers"] }
bincode = "1"
anyhow = "1.0.58"
axum = { version = "0.5.14", features = ["http2", "headers"] }
bincode = "1.3.3"
# We don't care about which version (We want to match with axum), we just need
# to enable the feature
bytes = { version = "*", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] }
futures = "0.3"
bytes = { version = "1.2.0", features = ["serde"] }
chrono = { version = "0.4.19", features = ["serde"] }
futures = "0.3.21"
# We just need to pull in whatever axum is pulling in
headers = "*"
lazy_static = "1"
headers = "0.3.7"
lazy_static = "1.4.0"
# Disable `random()` and `thread_rng()`
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"
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"

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::{AddExtensionLayer, Router};
use axum::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(AddExtensionLayer::new(db))
.layer(axum::Extension(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 = "*", features = ["js"] }
getrandom = { version = "0.2.7", features = ["js"] }
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"
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"
tar = "0.4.38"
[dependencies.web-sys]
version = "0.3"
version = "0.3.59"
features = [
"BlobPropertyBag",
"TextDecoder",

View file

@ -25,9 +25,12 @@ 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::secrecy::{Secret, SecretVec};
use omegaupload_common::{Expiration, PartialParsedUrl};
use omegaupload_common::fragment::Builder;
use omegaupload_common::secrecy::{ExposeSecret, Secret, SecretString, SecretVec};
use omegaupload_common::{Expiration, PartialParsedUrl, Url};
use reqwasm::http::Request;
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue};
@ -50,6 +53,8 @@ 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 {
@ -75,7 +80,7 @@ pub fn start() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
if location().pathname().unwrap() == "/" {
render_message("Go away".into());
create_upload_ui();
return;
}
@ -166,6 +171,55 @@ 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,6 +95,14 @@ img, audio, video {
max-width: 75vw;
}
textarea {
width: 100%;
height: 100%;
min-width: 75vw;
min-height: 75vh;
box-sizing: border-box;
}
.primary {
@extend .hljs;
}

View file

@ -16,14 +16,64 @@
import './main.scss';
import ReactDom from 'react-dom';
import React from 'react';
import React, { useState } from 'react';
import { encrypt_string, encrypt_array_buffer } from '../pkg';
const hljs = require('highlight.js');
import hljs from '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;
let resolvedName: string;
if (name) {
resolvedName = name;
} else {
@ -287,4 +337,4 @@ function getObjectUrl(data, mimeType?: string) {
window.addEventListener("hashchange", () => location.reload());
export { renderMessage, loadFromDb };
export { renderMessage, createUploadUi, loadFromDb };

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

View file

@ -2,6 +2,7 @@ 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',
@ -21,6 +22,8 @@ module.exports = {
"css-loader",
// Compiles Sass to CSS
"sass-loader",
// source map for debugging
"source-map-loader"
],
},
],
@ -41,6 +44,7 @@ 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