Make crypto even harder to fuck up
This commit is contained in:
parent
8a08e8e100
commit
bb35f710b2
8 changed files with 307 additions and 225 deletions
66
Cargo.lock
generated
66
Cargo.lock
generated
|
@ -41,6 +41,17 @@ version = "1.0.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
|
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34f8cda1a0ecf6f19d2bf64b9349d86900fa9bf98c979e655347a9e9dbe588c1"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"blake2",
|
||||||
|
"password-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.51"
|
version = "0.1.51"
|
||||||
|
@ -103,6 +114,12 @@ version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
|
@ -149,6 +166,17 @@ dependencies = [
|
||||||
"wyz",
|
"wyz",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-mac",
|
||||||
|
"digest",
|
||||||
|
"opaque-debug",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -336,6 +364,16 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-mac"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -922,17 +960,19 @@ dependencies = [
|
||||||
name = "omegaupload-common"
|
name = "omegaupload-common"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"argon2",
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"gloo-console",
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"typenum",
|
||||||
"url",
|
"url",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
@ -1001,6 +1041,17 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "peeking_take_while"
|
name = "peeking_take_while"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
@ -1377,19 +1428,6 @@ dependencies = [
|
||||||
"opaque-debug",
|
"opaque-debug",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sha2"
|
|
||||||
version = "0.9.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa"
|
|
||||||
dependencies = [
|
|
||||||
"block-buffer",
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"digest",
|
|
||||||
"opaque-debug",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
|
|
@ -6,9 +6,9 @@ use std::io::{Read, Write};
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use atty::Stream;
|
use atty::Stream;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use omegaupload_common::crypto::{gen_key_nonce, open_in_place, seal_in_place, Key};
|
use omegaupload_common::crypto::{open_in_place, seal_in_place};
|
||||||
use omegaupload_common::{
|
use omegaupload_common::{
|
||||||
base64, hash, Expiration, ParsedUrl, Url, API_ENDPOINT, EXPIRATION_HEADER_NAME,
|
base64, Expiration, ParsedUrl, Url, API_ENDPOINT, EXPIRATION_HEADER_NAME,
|
||||||
};
|
};
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use reqwest::header::EXPIRES;
|
use reqwest::header::EXPIRES;
|
||||||
|
@ -65,27 +65,13 @@ fn handle_upload(
|
||||||
bail!("This tool requires non interactive CLI. Pipe something in!");
|
bail!("This tool requires non interactive CLI. Pipe something in!");
|
||||||
}
|
}
|
||||||
|
|
||||||
let (data, nonce, key, pw_used) = {
|
let (data, key) = {
|
||||||
let (enc_key, nonce) = gen_key_nonce();
|
|
||||||
let mut container = Vec::new();
|
let mut container = Vec::new();
|
||||||
std::io::stdin().read_to_end(&mut container)?;
|
std::io::stdin().read_to_end(&mut container)?;
|
||||||
seal_in_place(&mut container, &nonce, &enc_key)
|
let password = password.as_ref().map(|v| v.expose_secret().as_ref());
|
||||||
.map_err(|_| anyhow!("Failed to encrypt data"))?;
|
let enc_key = seal_in_place(&mut container, password)?;
|
||||||
|
|
||||||
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 {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
let key = base64::encode(&enc_key);
|
let key = base64::encode(&enc_key);
|
||||||
let nonce = base64::encode(&nonce);
|
(container, key)
|
||||||
|
|
||||||
(container, nonce, key, pw_used)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut res = Client::new().post(url.as_ref());
|
let mut res = Client::new().post(url.as_ref());
|
||||||
|
@ -104,9 +90,9 @@ fn handle_upload(
|
||||||
.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 = format!("key:{}", key);
|
||||||
|
|
||||||
if pw_used {
|
if password.is_some() {
|
||||||
fragment.push_str("!pw");
|
fragment.push_str("!pw");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +127,7 @@ fn handle_download(mut url: ParsedUrl) -> Result<()> {
|
||||||
|
|
||||||
let mut data = res.bytes()?.as_ref().to_vec();
|
let mut data = res.bytes()?.as_ref().to_vec();
|
||||||
|
|
||||||
|
let mut password = None;
|
||||||
if url.needs_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) {
|
if atty::is(Stream::Stdout) {
|
||||||
|
@ -150,16 +137,10 @@ fn handle_download(mut url: ParsedUrl) -> Result<()> {
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
std::io::stdin().read_line(&mut input)?;
|
std::io::stdin().read_line(&mut input)?;
|
||||||
input.pop(); // last character is \n, we need to drop it.
|
input.pop(); // last character is \n, we need to drop it.
|
||||||
|
password = Some(input);
|
||||||
let pw_hash = hash(input.as_bytes());
|
|
||||||
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)
|
open_in_place(&mut data, &url.decryption_key, &password)?;
|
||||||
.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) {
|
||||||
|
|
|
@ -8,18 +8,21 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
bytes = { version = "*", features = ["serde"] }
|
bytes = { version = "*", features = ["serde"] }
|
||||||
chacha20poly1305 = "0.9"
|
chacha20poly1305 = { version = "0.9", features = ["stream"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
headers = "*"
|
headers = "*"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
sha2 = "0.9"
|
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
typenum = "1"
|
||||||
url = "2"
|
url = "2"
|
||||||
|
argon2 = "0.3.1"
|
||||||
|
|
||||||
web-sys = { version = "0.3", features = ["Headers"], optional = true }
|
# Wasm features
|
||||||
|
gloo-console = { version = "0.1", optional = true }
|
||||||
http = { version = "0.2", optional = true }
|
http = { version = "0.2", optional = true }
|
||||||
|
web-sys = { version = "0.3", features = ["Headers"], optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
wasm = ["web-sys", "http"]
|
wasm = ["gloo-console", "http", "web-sys"]
|
11
common/src/base64.rs
Normal file
11
common/src/base64.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use base64::{DecodeError, URL_SAFE};
|
||||||
|
|
||||||
|
/// URL-safe Base64 encoding.
|
||||||
|
pub fn encode(input: impl AsRef<[u8]>) -> String {
|
||||||
|
base64::encode_config(input, URL_SAFE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// URL-safe Base64 decoding.
|
||||||
|
pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
|
||||||
|
base64::decode_config(input, URL_SAFE)
|
||||||
|
}
|
196
common/src/crypto.rs
Normal file
196
common/src/crypto.rs
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
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 typenum::Unsigned;
|
||||||
|
|
||||||
|
pub use chacha20poly1305::Key;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
ChaCha20Poly1305(chacha20poly1305::aead::Error),
|
||||||
|
Argon2(argon2::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<chacha20poly1305::aead::Error> for Error {
|
||||||
|
fn from(err: chacha20poly1305::aead::Error) -> Self {
|
||||||
|
Error::ChaCha20Poly1305(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<argon2::Error> for Error {
|
||||||
|
fn from(err: argon2::Error) -> Self {
|
||||||
|
Error::Argon2(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::ChaCha20Poly1305(_) => write!(f, "Decryption failed"),
|
||||||
|
Error::Argon2(_) => write!(f, "KDF failed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<&str>) -> Result<Key, Error> {
|
||||||
|
let (key, nonce) = gen_key_nonce();
|
||||||
|
let cipher = XChaCha20Poly1305::new(&key);
|
||||||
|
cipher.encrypt_in_place(&nonce, &[], message)?;
|
||||||
|
|
||||||
|
let mut maybe_salt_string = None;
|
||||||
|
if let Some(password) = pw {
|
||||||
|
let (key, salt_string) = kdf(&password)?;
|
||||||
|
maybe_salt_string = Some(salt_string);
|
||||||
|
let cipher = XChaCha20Poly1305::new(&key);
|
||||||
|
cipher.encrypt_in_place(&nonce.increment(), &[], message)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: &Key, password: Option<&str>) -> Result<(), Error> {
|
||||||
|
let buffer_len = data.len();
|
||||||
|
let pw_key = if let Some(password) = password {
|
||||||
|
let salt_buf = data.split_off(buffer_len - Salt::SIZE);
|
||||||
|
let argon = Argon2::default();
|
||||||
|
let mut pw_key = Key::default();
|
||||||
|
argon.hash_password_into(password.as_bytes(), &salt_buf, &mut pw_key)?;
|
||||||
|
Some(pw_key)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let nonce = Nonce::from_slice(&data.split_off(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);
|
||||||
|
cipher.decrypt_in_place(&nonce.increment(), &[], data)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cipher = XChaCha20Poly1305::new(&key);
|
||||||
|
cipher.decrypt_in_place(&nonce, &[], data)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Securely generates a random key and nonce.
|
||||||
|
#[must_use]
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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; Salt::SIZE]);
|
||||||
|
|
||||||
|
impl Salt {
|
||||||
|
const SIZE: usize = argon2::password_hash::Salt::RECOMMENDED_LENGTH;
|
||||||
|
|
||||||
|
fn random() -> Self {
|
||||||
|
let mut salt = [0u8; Salt::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: &str) -> Result<(Key, Salt), argon2::Error> {
|
||||||
|
let salt = Salt::random();
|
||||||
|
let hasher = Argon2::default();
|
||||||
|
let mut key = Key::default();
|
||||||
|
hasher.hash_password_into(password.as_ref(), salt.as_ref(), &mut key)?;
|
||||||
|
|
||||||
|
Ok((*Key::from_slice(&key), salt))
|
||||||
|
}
|
|
@ -10,130 +10,25 @@ 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;
|
||||||
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 crypto;
|
||||||
|
|
||||||
pub const API_ENDPOINT: &str = "/api";
|
pub const API_ENDPOINT: &str = "/api";
|
||||||
|
|
||||||
pub mod base64 {
|
|
||||||
/// URL-safe Base64 encoding.
|
|
||||||
pub fn encode(input: impl AsRef<[u8]>) -> String {
|
|
||||||
base64::encode_config(input, base64::URL_SAFE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// URL-safe Base64 decoding.
|
|
||||||
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: Key,
|
||||||
pub nonce: Nonce,
|
|
||||||
pub needs_password: bool,
|
pub needs_password: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct PartialParsedUrl {
|
pub struct PartialParsedUrl {
|
||||||
pub decryption_key: Option<Key>,
|
pub decryption_key: Option<Key>,
|
||||||
pub nonce: Option<Nonce>,
|
|
||||||
pub needs_password: bool,
|
pub needs_password: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +45,6 @@ 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;
|
|
||||||
|
|
||||||
for (key, value) in args {
|
for (key, value) in args {
|
||||||
match (key, value) {
|
match (key, value) {
|
||||||
|
@ -160,16 +54,12 @@ impl From<&str> for PartialParsedUrl {
|
||||||
("pw", _) => {
|
("pw", _) => {
|
||||||
needs_password = true;
|
needs_password = true;
|
||||||
}
|
}
|
||||||
("nonce", Some(value)) => {
|
|
||||||
nonce = base64::decode(value).as_deref().map(Nonce::from_slice).ok();
|
|
||||||
}
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
decryption_key,
|
decryption_key,
|
||||||
nonce,
|
|
||||||
needs_password,
|
needs_password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,23 +90,19 @@ impl FromStr for ParsedUrl {
|
||||||
let PartialParsedUrl {
|
let PartialParsedUrl {
|
||||||
decryption_key,
|
decryption_key,
|
||||||
needs_password,
|
needs_password,
|
||||||
nonce,
|
|
||||||
} = PartialParsedUrl::from(fragment);
|
} = PartialParsedUrl::from(fragment);
|
||||||
|
|
||||||
url.set_fragment(None);
|
url.set_fragment(None);
|
||||||
|
|
||||||
let (decryption_key, nonce) = match (&decryption_key, nonce) {
|
let decryption_key = match &decryption_key {
|
||||||
(None, None) => Err(ParseUrlError::NeedKeyAndNonce),
|
Some(k) => Ok(*k),
|
||||||
(None, Some(_)) => Err(ParseUrlError::NeedKey),
|
None => 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,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ 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, Key};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::{Blob, BlobPropertyBag};
|
use web_sys::{Blob, BlobPropertyBag};
|
||||||
|
@ -36,31 +36,10 @@ fn now() -> f64 {
|
||||||
pub fn decrypt(
|
pub fn decrypt(
|
||||||
mut container: Vec<u8>,
|
mut container: Vec<u8>,
|
||||||
key: Key,
|
key: Key,
|
||||||
nonce: Nonce,
|
maybe_password: Option<&str>,
|
||||||
maybe_password: Option<Key>,
|
|
||||||
) -> Result<DecryptedData, PasteCompleteConstructionError> {
|
) -> Result<DecryptedData, PasteCompleteConstructionError> {
|
||||||
log!("Stage 1 decryption started.");
|
open_in_place(&mut container, &key, maybe_password)
|
||||||
let start = now();
|
.map_err(|_| PasteCompleteConstructionError::Decryption)?;
|
||||||
|
|
||||||
if let Some(password) = maybe_password {
|
|
||||||
crate::render_message("Decrypting Stage 1...".into());
|
|
||||||
open_in_place(&mut 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(&mut 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));
|
|
||||||
|
|
||||||
let mime_type = tree_magic_mini::from_u8(&container);
|
let mime_type = tree_magic_mini::from_u8(&container);
|
||||||
log!("Mimetype: ", mime_type);
|
log!("Mimetype: ", mime_type);
|
||||||
|
@ -79,6 +58,7 @@ pub fn decrypt(
|
||||||
Blob::new_with_u8_array_sequence_and_options(blob_chunks.dyn_ref().unwrap(), &blob_props)
|
Blob::new_with_u8_array_sequence_and_options(blob_chunks.dyn_ref().unwrap(), &blob_props)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
log!(format!("Blob conversion completed in {}ms", now() - start));
|
log!(format!("Blob conversion completed in {}ms", now() - start));
|
||||||
|
|
||||||
if mime_type.starts_with("text/") {
|
if mime_type.starts_with("text/") {
|
||||||
|
@ -125,8 +105,7 @@ pub fn decrypt(
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PasteCompleteConstructionError {
|
pub enum PasteCompleteConstructionError {
|
||||||
StageOneFailure,
|
Decryption,
|
||||||
StageTwoFailure,
|
|
||||||
InvalidEncoding,
|
InvalidEncoding,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,11 +114,8 @@ impl std::error::Error for PasteCompleteConstructionError {}
|
||||||
impl Display for PasteCompleteConstructionError {
|
impl Display for PasteCompleteConstructionError {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
PasteCompleteConstructionError::StageOneFailure => {
|
PasteCompleteConstructionError::Decryption => {
|
||||||
write!(f, "Failed to decrypt stage one.")
|
write!(f, "Failed to decrypt data.")
|
||||||
}
|
|
||||||
PasteCompleteConstructionError::StageTwoFailure => {
|
|
||||||
write!(f, "Failed to decrypt stage two.")
|
|
||||||
}
|
}
|
||||||
PasteCompleteConstructionError::InvalidEncoding => write!(
|
PasteCompleteConstructionError::InvalidEncoding => write!(
|
||||||
f,
|
f,
|
||||||
|
|
|
@ -8,9 +8,9 @@ use decrypt::DecryptedData;
|
||||||
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, Array};
|
use js_sys::{Array, JsString, Object, Uint8Array};
|
||||||
use omegaupload_common::crypto::{Key, Nonce};
|
use omegaupload_common::crypto::Key;
|
||||||
use omegaupload_common::{hash, Expiration, PartialParsedUrl};
|
use omegaupload_common::{Expiration, PartialParsedUrl};
|
||||||
use reqwasm::http::Request;
|
use reqwasm::http::Request;
|
||||||
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
|
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
|
||||||
use wasm_bindgen::{JsCast, JsValue};
|
use wasm_bindgen::{JsCast, JsValue};
|
||||||
|
@ -69,7 +69,7 @@ fn main() {
|
||||||
log!(&url);
|
log!(&url);
|
||||||
log!(&request_uri.to_string());
|
log!(&request_uri.to_string());
|
||||||
log!(&location().pathname().unwrap());
|
log!(&location().pathname().unwrap());
|
||||||
let (key, nonce, needs_pw) = {
|
let (key, needs_pw) = {
|
||||||
let partial_parsed_url = url
|
let partial_parsed_url = url
|
||||||
.split_once('#')
|
.split_once('#')
|
||||||
.map(|(_, fragment)| PartialParsedUrl::from(fragment))
|
.map(|(_, fragment)| PartialParsedUrl::from(fragment))
|
||||||
|
@ -81,14 +81,7 @@ fn main() {
|
||||||
render_message("Invalid paste link: Missing decryption key.".into());
|
render_message("Invalid paste link: Missing decryption key.".into());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let nonce = if let Some(nonce) = partial_parsed_url.nonce {
|
(key, partial_parsed_url.needs_password)
|
||||||
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 {
|
let password = if needs_pw {
|
||||||
|
@ -97,7 +90,7 @@ fn main() {
|
||||||
|
|
||||||
if let Ok(Some(password)) = pw {
|
if let Ok(Some(password)) = pw {
|
||||||
if !password.is_empty() {
|
if !password.is_empty() {
|
||||||
break Some(hash(password));
|
break Some(password);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +101,7 @@ fn main() {
|
||||||
if location().pathname().unwrap() == "/" {
|
if location().pathname().unwrap() == "/" {
|
||||||
} else {
|
} else {
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
if let Err(e) = fetch_resources(request_uri, key, nonce, password).await {
|
if let Err(e) = fetch_resources(request_uri, key, password.as_deref()).await {
|
||||||
log!(e.to_string());
|
log!(e.to_string());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -116,12 +109,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::future_not_send)]
|
#[allow(clippy::future_not_send)]
|
||||||
async fn fetch_resources(
|
async fn fetch_resources(request_uri: Uri, key: Key, password: Option<&str>) -> Result<()> {
|
||||||
request_uri: Uri,
|
|
||||||
key: Key,
|
|
||||||
nonce: Nonce,
|
|
||||||
password: Option<Key>,
|
|
||||||
) -> Result<()> {
|
|
||||||
match Request::get(&request_uri.to_string()).send().await {
|
match Request::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 = Expiration::try_from(resp.headers()).map_or_else(
|
||||||
|
@ -154,7 +142,7 @@ async fn fetch_resources(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let decrypted = decrypt(data, key, nonce, password)?;
|
let decrypted = decrypt(data, key, password)?;
|
||||||
let db_open_req = open_idb()?;
|
let db_open_req = open_idb()?;
|
||||||
|
|
||||||
// On success callback
|
// On success callback
|
||||||
|
@ -197,10 +185,13 @@ async fn fetch_resources(
|
||||||
.data(blob)
|
.data(blob)
|
||||||
.extra(
|
.extra(
|
||||||
"entries",
|
"entries",
|
||||||
JsValue::from(entries.into_iter()
|
JsValue::from(
|
||||||
.filter_map(|x| JsValue::from_serde(x).ok())
|
entries
|
||||||
.collect::<Array>())
|
.into_iter()
|
||||||
|
.filter_map(|x| JsValue::from_serde(x).ok())
|
||||||
|
.collect::<Array>(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let put_action = transaction
|
let put_action = transaction
|
||||||
|
|
Loading…
Reference in a new issue