diff --git a/bin/build.sh b/bin/build.sh index 0ce1446..e4048b3 100755 --- a/bin/build.sh +++ b/bin/build.sh @@ -17,7 +17,7 @@ cargo build --release --bin omegaupload-server # Prepare assets for upload to webserver mkdir -p dist/static # Move everything that's not index.html into a `static` subdir -find dist -type f -exec mv {} dist/static/ ";" +find dist -not -name index.html -type f -exec mv {} dist/static/ ";" strip target/release/omegaupload-server cp target/release/omegaupload-server dist/omegaupload-server diff --git a/common/src/base64.rs b/common/src/base64.rs index ebc9e9e..44f4130 100644 --- a/common/src/base64.rs +++ b/common/src/base64.rs @@ -6,6 +6,11 @@ pub fn encode(input: impl AsRef<[u8]>) -> String { } /// URL-safe Base64 decoding. +/// +/// # Errors +/// +/// Returns an error if a buffer cannot be decoded, such as if there's an +/// incorrect number of bytes. pub fn decode(input: impl AsRef<[u8]>) -> Result, DecodeError> { base64::decode_config(input, URL_SAFE) } diff --git a/common/src/crypto.rs b/common/src/crypto.rs index a1d7277..a347a36 100644 --- a/common/src/crypto.rs +++ b/common/src/crypto.rs @@ -6,7 +6,7 @@ use chacha20poly1305::aead::generic_array::GenericArray; use chacha20poly1305::aead::{AeadInPlace, NewAead}; use chacha20poly1305::XChaCha20Poly1305; use chacha20poly1305::XNonce; -use rand::{thread_rng, Rng}; +use rand::{CryptoRng, Rng}; use secrecy::{ExposeSecret, Secret, SecretVec, Zeroize}; use typenum::Unsigned; @@ -27,6 +27,7 @@ pub enum Error { pub struct Key(chacha20poly1305::Key); impl Key { + /// Encloses a secret key in a secret `Key` struct. pub fn new_secret(vec: Vec) -> Option> { chacha20poly1305::Key::from_exact_iter(vec.into_iter()) .map(Self) @@ -58,11 +59,14 @@ impl Zeroize for Key { } } -/// 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: +/// Seals the provided message with an optional password, returning the secret +/// key used to encrypt the message and mutating the buffer to contain necessary +/// metadata. +/// +/// 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 @@ -77,6 +81,17 @@ impl Zeroize for Key { /// `XChaCha20Poly1305`. /// - `rng_key` represents a randomly generated key. /// - `kdf(pw, salt)` represents a key derived from Argon2. +/// - `nonce` represents a randomly generated nonce. +/// +/// Note that the lengths for the nonce, key, and salt follow recommended +/// values. As of writing this doc (2021-10-31), the nonce size is 24 bytes, the +/// salt size is 16 bytes, and the key size is 32 bytes. +/// +/// # Errors +/// +/// This message will return an error if and only if there was a problem +/// encrypting the message or deriving a secret key from the password, if one +/// was provided. pub fn seal_in_place( message: &mut Vec, pw: Option>, @@ -104,6 +119,12 @@ pub fn seal_in_place( Ok(key) } +/// Opens a message that has been sealed with `seal_in_place`. +/// +/// # Errors +/// +/// Returns an error if there was a decryption failure or if there was a problem +/// deriving a secret key from the password. pub fn open_in_place( data: &mut Vec, key: &Secret, @@ -140,10 +161,9 @@ pub fn open_in_place( Ok(()) } -/// Securely generates a random key and nonce. #[must_use] fn gen_key_nonce() -> (Secret, Nonce) { - let mut rng = thread_rng(); + let mut rng = get_csrng(); let mut key = GenericArray::default(); rng.fill(key.as_mut_slice()); let mut nonce = Nonce::default(); @@ -154,15 +174,9 @@ fn gen_key_nonce() -> (Secret, Nonce) { // Type alias; to ensure that we're consistent on what the inner impl is. type NonceImpl = XNonce; -#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] struct Nonce(NonceImpl); -impl Default for Nonce { - fn default() -> Self { - Self(GenericArray::default()) - } -} - impl Deref for Nonce { type Target = NonceImpl; @@ -207,7 +221,7 @@ impl Salt { fn random() -> Self { let mut salt = [0_u8; Self::SIZE]; - thread_rng().fill(&mut salt); + get_csrng().fill(&mut salt); Self(salt) } } @@ -227,3 +241,12 @@ fn kdf(password: &SecretVec) -> Result<(Secret, Salt), argon2::Error> { Ok((Secret::new(key), salt)) } + +/// Fetches a cryptographically secure random number generator. This indirection +/// is used for better auditing the quality of rng. Notably, this function +/// returns a `Rng` with the `CryptoRng` marker trait, preventing +/// non-cryptographically secure RNGs from being used. +#[must_use] +pub fn get_csrng() -> impl CryptoRng + Rng { + rand::thread_rng() +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 21900dd..518346b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -18,7 +18,8 @@ futures = "0.3" # We just need to pull in whatever axum is pulling in headers = "*" lazy_static = "1" -rand = "0.8" +# Disable `random()` and `thread_rng()` +rand = { version = "0.8", default_features = false } rocksdb = { version = "0.17", default_features = false, features = ["zstd"] } serde = { version = "1", features = ["derive"] } signal-hook = "0.3" diff --git a/server/src/main.rs b/server/src/main.rs index aeaa4a7..2f51d6c 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -16,8 +16,8 @@ use chrono::Utc; use futures::stream::StreamExt; use headers::HeaderMap; use lazy_static::lazy_static; +use omegaupload_common::crypto::get_csrng; use omegaupload_common::{Expiration, API_ENDPOINT}; -use rand::thread_rng; use rand::Rng; use rocksdb::{ColumnFamilyDescriptor, IteratorMode}; use rocksdb::{Options, DB}; @@ -41,6 +41,7 @@ lazy_static! { #[tokio::main] async fn main() -> Result<()> { + const INDEX_PAGE: Html<&'static str> = Html(include_str!("../../dist/index.html")); const PASTE_DB_PATH: &str = "database"; const SHORT_CODE_SIZE: usize = 12; @@ -68,8 +69,6 @@ async fn main() -> Result<()> { let root_service = service::get(ServeDir::new("static")) .handle_error(|_| Ok::<_, Infallible>(StatusCode::NOT_FOUND)); - const INDEX_PAGE: Html<&'static str> = Html(include_str!("../../dist/index.html")); - axum::Server::bind(&"0.0.0.0:8080".parse()?) .serve( Router::new() @@ -206,7 +205,7 @@ async fn upload( // Try finding a code; give up after 1000 attempts // Statistics show that this is very unlikely to happen for i in 0..1000 { - let code: ShortCode = thread_rng().sample(short_code::Generator); + let code: ShortCode = get_csrng().sample(short_code::Generator); let db = Arc::clone(&db); let key = code.as_bytes(); let query = task::spawn_blocking(move || { diff --git a/server/src/short_code.rs b/server/src/short_code.rs index e9c4edc..dbe5ad9 100644 --- a/server/src/short_code.rs +++ b/server/src/short_code.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; use rand::prelude::Distribution; +use rand::Rng; use serde::de::{Unexpected, Visitor}; use serde::Deserialize; @@ -109,7 +110,7 @@ pub struct Generator; const ALPHABET: &[u8; 32] = b"23456789CFGHJMPQRVWXcfghjmpqrvwx"; impl Distribution for Generator { - fn sample(&self, rng: &mut R) -> ShortCodeChar { + fn sample(&self, rng: &mut R) -> ShortCodeChar { let value = rng.gen_range(0..32); assert!(value < 32); ShortCodeChar(ALPHABET[value] as char) @@ -117,7 +118,7 @@ impl Distribution for Generator { } impl Distribution> for Generator { - fn sample(&self, rng: &mut R) -> ShortCode { + fn sample(&self, rng: &mut R) -> ShortCode { let mut arr = [ShortCodeChar('\0'); N]; for c in arr.iter_mut() { diff --git a/web/src/decrypt.rs b/web/src/decrypt.rs index ab78078..a62fab8 100644 --- a/web/src/decrypt.rs +++ b/web/src/decrypt.rs @@ -12,7 +12,7 @@ use web_sys::{Blob, BlobPropertyBag}; #[derive(Clone, Serialize)] pub struct ArchiveMeta { name: String, - file_size: usize, + file_size: u64, } #[derive(Clone)] @@ -86,7 +86,7 @@ pub fn decrypt( match zip.by_index(i) { Ok(file) => entries.push(ArchiveMeta { name: file.name().to_string(), - file_size: file.size() as usize, + file_size: file.size(), }), Err(err) => match err { zip::result::ZipError::UnsupportedArchive(s) => { diff --git a/web/src/main.rs b/web/src/main.rs index c901052..e4a6b81 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -96,6 +96,8 @@ fn main() { Ok(Some(password)) if !password.is_empty() => { break Some(SecretVec::new(password.into_bytes())); } + // Empty message was entered. + Ok(Some(_)) => (), // Cancel button was entered. Ok(None) => { render_message("This paste requires a password.".into()); @@ -112,8 +114,6 @@ fn main() { None }; - log!(location().pathname().unwrap()); - spawn_local(async move { if let Err(e) = fetch_resources(request_uri, key, password).await { log!(e.to_string());