diff --git a/.gitignore b/.gitignore index 1ce282b..092e5c3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ **/dist/ **/node_modules test.* -dist.tar.zst \ No newline at end of file +dist.tar.zst +.env \ No newline at end of file diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..256d0e6 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1 @@ +external-sources=true \ No newline at end of file diff --git a/build.sh b/bin/build.sh similarity index 88% rename from build.sh rename to bin/build.sh index 7064a7e..0ce1446 100755 --- a/build.sh +++ b/bin/build.sh @@ -2,6 +2,8 @@ set -euxo pipefail +cd "$(git rev-parse --show-toplevel)" || exit 1 + # Build frontend assets yarn trunk build --release diff --git a/bin/upload_test_files.sh b/bin/upload_test_files.sh new file mode 100755 index 0000000..80a4b1f --- /dev/null +++ b/bin/upload_test_files.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +source .env + +cd "$(git rev-parse --show-toplevel)" || exit 1 + +cargo build --release --bin omegaupload-cli + +TEST_PATH="test/*" + +PADDING=0 + +for file in $TEST_PATH; do + if [ $PADDING -lt ${#file} ]; then + PADDING=${#file} + fi +done + +for file in $TEST_PATH; do + printf "%${PADDING}s: " "${file#$TEST_PATH}" + ./target/release/omegaupload-cli upload "$PASTE_URL" "$file" +done \ No newline at end of file diff --git a/test/archive.zip b/test/archive.zip new file mode 100644 index 0000000..b65a00d Binary files /dev/null and b/test/archive.zip differ diff --git a/test/code.rs b/test/code.rs new file mode 100644 index 0000000..a1d7277 --- /dev/null +++ b/test/code.rs @@ -0,0 +1,229 @@ +use std::ops::{Deref, DerefMut}; + +use argon2::Argon2; +use chacha20poly1305::aead::generic_array::sequence::GenericSequence; +use chacha20poly1305::aead::generic_array::GenericArray; +use chacha20poly1305::aead::{AeadInPlace, NewAead}; +use chacha20poly1305::XChaCha20Poly1305; +use chacha20poly1305::XNonce; +use rand::{thread_rng, Rng}; +use secrecy::{ExposeSecret, Secret, SecretVec, Zeroize}; +use typenum::Unsigned; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Invalid password.")] + Password, + #[error("Invalid secret key.")] + SecretKey, + #[error("An error occurred while trying to decrypt the blob.")] + Encryption, + #[error("An error occurred while trying to derive a secret key.")] + Kdf, +} + +// This struct intentionally prevents implement Clone or Copy +#[derive(Default)] +pub struct Key(chacha20poly1305::Key); + +impl Key { + pub fn new_secret(vec: Vec) -> Option> { + chacha20poly1305::Key::from_exact_iter(vec.into_iter()) + .map(Self) + .map(Secret::new) + } +} + +impl AsRef for Key { + fn as_ref(&self) -> &chacha20poly1305::Key { + &self.0 + } +} + +impl Deref for Key { + type Target = chacha20poly1305::Key; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for Key { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Zeroize for Key { + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +/// Seals the provided message with an optional message. The resulting sealed +/// message has the nonce used to encrypt the message appended to it as well as +/// a salt string used to derive the key. In other words, the modified buffer is +/// one of the following to possibilities, depending if there was a password +/// provided: +/// +/// ``` +/// modified = C(message, rng_key, nonce) || nonce +/// ``` +/// or +/// ``` +/// modified = C(C(message, rng_key, nonce), kdf(pw, salt), nonce + 1) || nonce || salt +/// ``` +/// +/// Where: +/// - `C(message, key, nonce)` represents encrypting a provided message with +/// `XChaCha20Poly1305`. +/// - `rng_key` represents a randomly generated key. +/// - `kdf(pw, salt)` represents a key derived from Argon2. +pub fn seal_in_place( + message: &mut Vec, + pw: Option>, +) -> Result, Error> { + let (key, nonce) = gen_key_nonce(); + let cipher = XChaCha20Poly1305::new(key.expose_secret()); + cipher + .encrypt_in_place(&nonce, &[], message) + .map_err(|_| Error::Encryption)?; + + let mut maybe_salt_string = None; + if let Some(password) = pw { + let (key, salt_string) = kdf(&password).map_err(|_| Error::Kdf)?; + maybe_salt_string = Some(salt_string); + let cipher = XChaCha20Poly1305::new(key.expose_secret()); + cipher + .encrypt_in_place(&nonce.increment(), &[], message) + .map_err(|_| Error::Encryption)?; + } + + message.extend_from_slice(nonce.as_slice()); + if let Some(maybe_salted_string) = maybe_salt_string { + message.extend_from_slice(maybe_salted_string.as_ref()); + } + Ok(key) +} + +pub fn open_in_place( + data: &mut Vec, + key: &Secret, + password: Option>, +) -> Result<(), Error> { + let pw_key = if let Some(password) = password { + let salt_buf = data.split_off(data.len() - Salt::SIZE); + let argon = Argon2::default(); + let mut pw_key = Key::default(); + argon + .hash_password_into(password.expose_secret(), &salt_buf, &mut pw_key) + .map_err(|_| Error::Kdf)?; + Some(Secret::new(pw_key)) + } else { + None + }; + + let nonce = Nonce::from_slice(&data.split_off(data.len() - Nonce::SIZE)); + + // At this point we should have a buffer that's only the ciphertext. + + if let Some(key) = pw_key { + let cipher = XChaCha20Poly1305::new(key.expose_secret()); + cipher + .decrypt_in_place(&nonce.increment(), &[], data) + .map_err(|_| Error::Password)?; + } + + let cipher = XChaCha20Poly1305::new(key.expose_secret()); + cipher + .decrypt_in_place(&nonce, &[], data) + .map_err(|_| Error::SecretKey)?; + + Ok(()) +} + +/// Securely generates a random key and nonce. +#[must_use] +fn gen_key_nonce() -> (Secret, Nonce) { + let mut rng = thread_rng(); + let mut key = GenericArray::default(); + rng.fill(key.as_mut_slice()); + let mut nonce = Nonce::default(); + rng.fill(nonce.as_mut_slice()); + (Secret::new(Key(key)), nonce) +} + +// Type alias; to ensure that we're consistent on what the inner impl is. +type NonceImpl = XNonce; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +struct Nonce(NonceImpl); + +impl Default for Nonce { + fn default() -> Self { + Self(GenericArray::default()) + } +} + +impl Deref for Nonce { + type Target = NonceImpl; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Nonce { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl AsRef<[u8]> for Nonce { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Nonce { + const SIZE: usize = >::Length::USIZE; + + #[must_use] + pub fn increment(&self) -> Self { + let mut inner = self.0; + inner.as_mut_slice()[0] += 1; + Self(inner) + } + + #[must_use] + pub fn from_slice(slice: &[u8]) -> Self { + Self(*NonceImpl::from_slice(slice)) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +struct Salt([u8; Self::SIZE]); + +impl Salt { + const SIZE: usize = argon2::password_hash::Salt::RECOMMENDED_LENGTH; + + fn random() -> Self { + let mut salt = [0_u8; Self::SIZE]; + thread_rng().fill(&mut salt); + Self(salt) + } +} + +impl AsRef<[u8]> for Salt { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +/// Hashes an input to output a usable key. +fn kdf(password: &SecretVec) -> Result<(Secret, Salt), argon2::Error> { + let salt = Salt::random(); + let hasher = Argon2::default(); + let mut key = Key::default(); + hasher.hash_password_into(password.expose_secret().as_ref(), salt.as_ref(), &mut key)?; + + Ok((Secret::new(key), salt)) +} diff --git a/test/image.png b/test/image.png new file mode 100644 index 0000000..2aa6227 Binary files /dev/null and b/test/image.png differ diff --git a/test/image.svg b/test/image.svg new file mode 100644 index 0000000..a194b30 --- /dev/null +++ b/test/image.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/test/movie.mp4 b/test/movie.mp4 new file mode 100644 index 0000000..aa7e45f Binary files /dev/null and b/test/movie.mp4 differ diff --git a/test/music.mp3 b/test/music.mp3 new file mode 100644 index 0000000..d84fdb5 Binary files /dev/null and b/test/music.mp3 differ diff --git a/test/text.txt b/test/text.txt new file mode 100644 index 0000000..eb265ef --- /dev/null +++ b/test/text.txt @@ -0,0 +1,19 @@ +Copyright (c) 2021 Edward Shen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file