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"
|
||||
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]]
|
||||
name = "async-trait"
|
||||
version = "0.1.51"
|
||||
|
@ -103,6 +114,12 @@ version = "0.13.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
|
@ -149,6 +166,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "block-buffer"
|
||||
version = "0.9.0"
|
||||
|
@ -336,6 +364,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
|
@ -922,17 +960,19 @@ dependencies = [
|
|||
name = "omegaupload-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"base64",
|
||||
"bytes",
|
||||
"chacha20poly1305",
|
||||
"chrono",
|
||||
"gloo-console",
|
||||
"headers",
|
||||
"http",
|
||||
"lazy_static",
|
||||
"rand",
|
||||
"serde",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
"typenum",
|
||||
"url",
|
||||
"web-sys",
|
||||
]
|
||||
|
@ -1001,6 +1041,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "peeking_take_while"
|
||||
version = "0.1.2"
|
||||
|
@ -1377,19 +1428,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.4"
|
||||
|
|
|
@ -6,9 +6,9 @@ use std::io::{Read, Write};
|
|||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use atty::Stream;
|
||||
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::{
|
||||
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::header::EXPIRES;
|
||||
|
@ -65,27 +65,13 @@ fn handle_upload(
|
|||
bail!("This tool requires non interactive CLI. Pipe something in!");
|
||||
}
|
||||
|
||||
let (data, nonce, key, pw_used) = {
|
||||
let (enc_key, nonce) = gen_key_nonce();
|
||||
let (data, key) = {
|
||||
let mut container = Vec::new();
|
||||
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 {
|
||||
false
|
||||
};
|
||||
|
||||
let password = password.as_ref().map(|v| v.expose_secret().as_ref());
|
||||
let enc_key = seal_in_place(&mut container, password)?;
|
||||
let key = base64::encode(&enc_key);
|
||||
let nonce = base64::encode(&nonce);
|
||||
|
||||
(container, nonce, key, pw_used)
|
||||
(container, key)
|
||||
};
|
||||
|
||||
let mut res = Client::new().post(url.as_ref());
|
||||
|
@ -104,9 +90,9 @@ fn handle_upload(
|
|||
.map_err(|_| anyhow!("Failed to get base URL"))?
|
||||
.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");
|
||||
}
|
||||
|
||||
|
@ -141,6 +127,7 @@ fn handle_download(mut url: ParsedUrl) -> Result<()> {
|
|||
|
||||
let mut data = res.bytes()?.as_ref().to_vec();
|
||||
|
||||
let mut password = None;
|
||||
if url.needs_password {
|
||||
// Only print prompt on interactive, else it messes with output
|
||||
if atty::is(Stream::Stdout) {
|
||||
|
@ -150,16 +137,10 @@ fn handle_download(mut url: ParsedUrl) -> Result<()> {
|
|||
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());
|
||||
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?"))?;
|
||||
password = Some(input);
|
||||
}
|
||||
|
||||
open_in_place(&mut data, &url.nonce, &url.decryption_key)
|
||||
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect decryption key?"))?;
|
||||
open_in_place(&mut data, &url.decryption_key, &password)?;
|
||||
|
||||
if atty::is(Stream::Stdout) {
|
||||
if let Ok(data) = String::from_utf8(data) {
|
||||
|
|
|
@ -8,18 +8,21 @@ edition = "2021"
|
|||
[dependencies]
|
||||
base64 = "0.13"
|
||||
bytes = { version = "*", features = ["serde"] }
|
||||
chacha20poly1305 = "0.9"
|
||||
chacha20poly1305 = { version = "0.9", features = ["stream"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
headers = "*"
|
||||
lazy_static = "1"
|
||||
rand = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
sha2 = "0.9"
|
||||
thiserror = "1"
|
||||
typenum = "1"
|
||||
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 }
|
||||
web-sys = { version = "0.3", features = ["Headers"], optional = true }
|
||||
|
||||
[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 lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use thiserror::Error;
|
||||
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 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 sanitized_url: Url,
|
||||
pub decryption_key: Key,
|
||||
pub nonce: Nonce,
|
||||
pub needs_password: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PartialParsedUrl {
|
||||
pub decryption_key: Option<Key>,
|
||||
pub nonce: Option<Nonce>,
|
||||
pub needs_password: bool,
|
||||
}
|
||||
|
||||
|
@ -150,7 +45,6 @@ impl From<&str> for PartialParsedUrl {
|
|||
|
||||
let mut decryption_key = None;
|
||||
let mut needs_password = false;
|
||||
let mut nonce = None;
|
||||
|
||||
for (key, value) in args {
|
||||
match (key, value) {
|
||||
|
@ -160,16 +54,12 @@ impl From<&str> for PartialParsedUrl {
|
|||
("pw", _) => {
|
||||
needs_password = true;
|
||||
}
|
||||
("nonce", Some(value)) => {
|
||||
nonce = base64::decode(value).as_deref().map(Nonce::from_slice).ok();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
decryption_key,
|
||||
nonce,
|
||||
needs_password,
|
||||
}
|
||||
}
|
||||
|
@ -200,23 +90,19 @@ impl FromStr for ParsedUrl {
|
|||
let PartialParsedUrl {
|
||||
decryption_key,
|
||||
needs_password,
|
||||
nonce,
|
||||
} = PartialParsedUrl::from(fragment);
|
||||
|
||||
url.set_fragment(None);
|
||||
|
||||
let (decryption_key, nonce) = match (&decryption_key, nonce) {
|
||||
(None, None) => Err(ParseUrlError::NeedKeyAndNonce),
|
||||
(None, Some(_)) => Err(ParseUrlError::NeedKey),
|
||||
(Some(_), None) => Err(ParseUrlError::NeedNonce),
|
||||
(Some(k), Some(v)) => Ok((*k, v)),
|
||||
let decryption_key = match &decryption_key {
|
||||
Some(k) => Ok(*k),
|
||||
None => Err(ParseUrlError::NeedKey),
|
||||
}?;
|
||||
|
||||
Ok(Self {
|
||||
sanitized_url: url,
|
||||
decryption_key,
|
||||
needs_password,
|
||||
nonce,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::sync::Arc;
|
|||
|
||||
use gloo_console::log;
|
||||
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 wasm_bindgen::JsCast;
|
||||
use web_sys::{Blob, BlobPropertyBag};
|
||||
|
@ -36,31 +36,10 @@ fn now() -> f64 {
|
|||
pub fn decrypt(
|
||||
mut container: Vec<u8>,
|
||||
key: Key,
|
||||
nonce: Nonce,
|
||||
maybe_password: Option<Key>,
|
||||
maybe_password: Option<&str>,
|
||||
) -> Result<DecryptedData, PasteCompleteConstructionError> {
|
||||
log!("Stage 1 decryption started.");
|
||||
let start = now();
|
||||
|
||||
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));
|
||||
open_in_place(&mut container, &key, maybe_password)
|
||||
.map_err(|_| PasteCompleteConstructionError::Decryption)?;
|
||||
|
||||
let mime_type = tree_magic_mini::from_u8(&container);
|
||||
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)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
log!(format!("Blob conversion completed in {}ms", now() - start));
|
||||
|
||||
if mime_type.starts_with("text/") {
|
||||
|
@ -125,8 +105,7 @@ pub fn decrypt(
|
|||
|
||||
#[derive(Debug)]
|
||||
pub enum PasteCompleteConstructionError {
|
||||
StageOneFailure,
|
||||
StageTwoFailure,
|
||||
Decryption,
|
||||
InvalidEncoding,
|
||||
}
|
||||
|
||||
|
@ -135,11 +114,8 @@ 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.")
|
||||
PasteCompleteConstructionError::Decryption => {
|
||||
write!(f, "Failed to decrypt data.")
|
||||
}
|
||||
PasteCompleteConstructionError::InvalidEncoding => write!(
|
||||
f,
|
||||
|
|
|
@ -8,9 +8,9 @@ use decrypt::DecryptedData;
|
|||
use gloo_console::{error, log};
|
||||
use http::uri::PathAndQuery;
|
||||
use http::{StatusCode, Uri};
|
||||
use js_sys::{JsString, Object, Uint8Array, Array};
|
||||
use omegaupload_common::crypto::{Key, Nonce};
|
||||
use omegaupload_common::{hash, Expiration, PartialParsedUrl};
|
||||
use js_sys::{Array, JsString, Object, Uint8Array};
|
||||
use omegaupload_common::crypto::Key;
|
||||
use omegaupload_common::{Expiration, PartialParsedUrl};
|
||||
use reqwasm::http::Request;
|
||||
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
|
@ -69,7 +69,7 @@ fn main() {
|
|||
log!(&url);
|
||||
log!(&request_uri.to_string());
|
||||
log!(&location().pathname().unwrap());
|
||||
let (key, nonce, needs_pw) = {
|
||||
let (key, needs_pw) = {
|
||||
let partial_parsed_url = url
|
||||
.split_once('#')
|
||||
.map(|(_, fragment)| PartialParsedUrl::from(fragment))
|
||||
|
@ -81,14 +81,7 @@ fn main() {
|
|||
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)
|
||||
(key, partial_parsed_url.needs_password)
|
||||
};
|
||||
|
||||
let password = if needs_pw {
|
||||
|
@ -97,7 +90,7 @@ fn main() {
|
|||
|
||||
if let Ok(Some(password)) = pw {
|
||||
if !password.is_empty() {
|
||||
break Some(hash(password));
|
||||
break Some(password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +101,7 @@ fn main() {
|
|||
if location().pathname().unwrap() == "/" {
|
||||
} else {
|
||||
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());
|
||||
}
|
||||
});
|
||||
|
@ -116,12 +109,7 @@ fn main() {
|
|||
}
|
||||
|
||||
#[allow(clippy::future_not_send)]
|
||||
async fn fetch_resources(
|
||||
request_uri: Uri,
|
||||
key: Key,
|
||||
nonce: Nonce,
|
||||
password: Option<Key>,
|
||||
) -> Result<()> {
|
||||
async fn fetch_resources(request_uri: Uri, key: Key, password: Option<&str>) -> 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(
|
||||
|
@ -154,7 +142,7 @@ async fn fetch_resources(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let decrypted = decrypt(data, key, nonce, password)?;
|
||||
let decrypted = decrypt(data, key, password)?;
|
||||
let db_open_req = open_idb()?;
|
||||
|
||||
// On success callback
|
||||
|
@ -197,10 +185,13 @@ async fn fetch_resources(
|
|||
.data(blob)
|
||||
.extra(
|
||||
"entries",
|
||||
JsValue::from(entries.into_iter()
|
||||
.filter_map(|x| JsValue::from_serde(x).ok())
|
||||
.collect::<Array>())
|
||||
JsValue::from(
|
||||
entries
|
||||
.into_iter()
|
||||
.filter_map(|x| JsValue::from_serde(x).ok())
|
||||
.collect::<Array>(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
let put_action = transaction
|
||||
|
|
Loading…
Reference in a new issue