diff --git a/Cargo.lock b/Cargo.lock index c7aeef8..7ba6598 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -776,6 +776,12 @@ dependencies = [ "tiff", ] +[[package]] +name = "imagesize" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b3b62f4e783e38afa07b51eaaa789be2fba03dbe29a05a1a906eb64feb987d" + [[package]] name = "indexmap" version = "1.7.0" @@ -1103,6 +1109,7 @@ dependencies = [ "gloo-console", "http", "image", + "imagesize", "js-sys", "omegaupload-common", "reqwasm", diff --git a/web/Cargo.toml b/web/Cargo.toml index ae29957..7c1c7f1 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -18,6 +18,7 @@ gloo-console = "0.1" http = "0.2" image = "0.23" js-sys = "0.3" +imagesize = "0.9" reqwasm = "0.2" tree_magic_mini = { version = "3", features = ["with-gpl-data"] } wasm-bindgen = "0.2" diff --git a/web/src/decrypt.rs b/web/src/decrypt.rs index c89b699..1ad9d1b 100644 --- a/web/src/decrypt.rs +++ b/web/src/decrypt.rs @@ -3,7 +3,7 @@ use std::io::Cursor; use std::sync::Arc; use gloo_console::log; -use image::{EncodableLayout, GenericImageView, ImageDecoder}; +use image::io::Reader; use js_sys::{Array, Uint8Array}; use omegaupload_common::crypto::{open_in_place, Key, Nonce}; use wasm_bindgen::JsCast; @@ -13,7 +13,7 @@ use web_sys::Blob; pub enum DecryptedData { String(Arc), Blob(Arc), - Image(Arc, (u32, u32), usize), + Image(Arc, (usize, usize), usize), Audio(Arc), Video(Arc), } @@ -35,16 +35,25 @@ pub fn decrypt( let container = &mut container; log!("Stage 1 decryption started."); let start = now(); + if let Some(password) = maybe_password { - open_in_place(container, &nonce.increment(), &password) - .map_err(|_| PasteCompleteConstructionError::StageOneFailure)?; + crate::render_message("Decrypting Stage 1...".into()); + open_in_place(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(); - open_in_place(container, &nonce, &key) - .map_err(|_| PasteCompleteConstructionError::StageTwoFailure)?; + crate::render_message("Decrypting Stage 2...".into()); + open_in_place(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)); if let Ok(decrypted) = std::str::from_utf8(container) { @@ -64,15 +73,18 @@ pub fn decrypt( log!("Image introspection started"); let start = now(); - let res = image::guess_format(&container); + let dimensions = imagesize::blob_size(&container).ok(); log!(format!( "Image introspection completed in {}ms", now() - start )); - // let image_reader = image::io::Reader::new(Cursor::new(container.as_bytes())); - if let Ok(dimensions) = res { - log!(format!("{:?}", dimensions)); - Ok(DecryptedData::Image(blob, (0, 0), container.len())) + + if let Some(dimensions) = dimensions { + Ok(DecryptedData::Image( + blob, + (dimensions.width, dimensions.height), + container.len(), + )) } else { let mime_type = tree_magic_mini::from_u8(container); log!(mime_type); diff --git a/web/src/idb_object.rs b/web/src/idb_object.rs index d8fb0fe..e7847b7 100644 --- a/web/src/idb_object.rs +++ b/web/src/idb_object.rs @@ -21,7 +21,7 @@ impl From> for Object { Ok(o) => o, // SAFETY: IdbObject maintains the invariant that it can eventually // be constructed into a JS object. - _ => unsafe { panic!() }, + _ => unsafe { unreachable_unchecked() }, } } } diff --git a/web/src/main.rs b/web/src/main.rs index b46dba0..c5d68d0 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -2,14 +2,15 @@ use std::str::FromStr; -use anyhow::{anyhow, Context, Result}; -use byte_unit::Byte; +use anyhow::{anyhow, bail, Context, Result}; +use byte_unit::{n_mib_bytes, Byte}; use decrypt::DecryptedData; -use gloo_console::log; +use gloo_console::{error, log}; use http::uri::PathAndQuery; use http::{StatusCode, Uri}; use js_sys::{JsString, Object, Uint8Array}; -use omegaupload_common::{Expiration, PartialParsedUrl}; +use omegaupload_common::crypto::{Key, Nonce}; +use omegaupload_common::{hash, Expiration, PartialParsedUrl}; use reqwasm::http::Request; use wasm_bindgen::prelude::{wasm_bindgen, Closure}; use wasm_bindgen::{JsCast, JsValue}; @@ -24,12 +25,14 @@ mod decrypt; mod idb_object; mod util; +const DOWNLOAD_SIZE_LIMIT: u128 = n_mib_bytes!(500); + #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_name = loadFromDb)] - fn load_from_db(); - #[wasm_bindgen(js_name = createNotFoundUi)] - fn create_not_found_ui(); + pub fn load_from_db(); + #[wasm_bindgen(js_name = renderMessage)] + pub fn render_message(message: JsString); } fn window() -> Window { @@ -51,6 +54,9 @@ fn open_idb() -> Result { fn main() { std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + + render_message("Loading paste...".into()); + let url = String::from(location().to_string()); let request_uri = { let mut uri_parts = url.parse::().unwrap().into_parts(); @@ -63,10 +69,48 @@ fn main() { log!(&url); log!(&request_uri.to_string()); log!(&location().pathname().unwrap()); + let (key, nonce, needs_pw) = { + let partial_parsed_url = url + .split_once('#') + .map(|(_, fragment)| PartialParsedUrl::from(fragment)) + .unwrap_or_default(); + let key = match partial_parsed_url.decryption_key { + Some(key) => key, + None => { + error!("Key is missing in url; bailing."); + render_message("Invalid paste link: Missing decryption key.".into()); + return; + } + }; + let nonce = match partial_parsed_url.nonce { + Some(nonce) => nonce, + None => { + 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 { + loop { + let pw = window().prompt_with_message("A password is required to decrypt this paste:"); + + if let Ok(Some(password)) = pw { + if !password.is_empty() { + break Some(hash(password)); + } + } + } + } else { + None + }; + if location().pathname().unwrap() == "/" { } else { - spawn_local(async { - if let Err(e) = fetch_resources(request_uri, url).await { + spawn_local(async move { + if let Err(e) = fetch_resources(request_uri, key, nonce, password).await { log!(e.to_string()); } }); @@ -74,7 +118,12 @@ fn main() { } #[allow(clippy::future_not_send)] -async fn fetch_resources(request_uri: Uri, url: String) -> Result<()> { +async fn fetch_resources( + request_uri: Uri, + key: Key, + nonce: Nonce, + password: Option, +) -> 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( @@ -86,28 +135,28 @@ async fn fetch_resources(request_uri: Uri, url: String) -> Result<()> { let data_fut = resp .as_raw() .array_buffer() - .expect("Failed to get raw bytes from response"); - let data = JsFuture::from(data_fut) - .await - .expect("Failed to result array buffer future"); + .expect("to get raw bytes from a response"); + let data = match JsFuture::from(data_fut).await { + Ok(data) => data, + Err(e) => { + render_message( + "Network failure: Failed to completely read encryption paste.".into(), + ); + bail!(format!( + "JsFuture returned an error while fetching resp buffer: {:?}", + e + )); + } + }; Uint8Array::new(&data).to_vec() }; - let (key, nonce) = { - let partial_parsed_url = url - .split_once('#') - .map(|(_, fragment)| PartialParsedUrl::from(fragment)) - .unwrap_or_default(); - let key = partial_parsed_url - .decryption_key - .context("missing key should be handled in the future")?; - let nonce = partial_parsed_url - .nonce - .context("missing nonce be handled in the future")?; - (key, nonce) - }; + if data.len() as u128 > DOWNLOAD_SIZE_LIMIT { + render_message("The paste is too large to decrypt from the web browser. You must use the CLI tool to download this paste.".into()); + return Ok(()); + } - let decrypted = decrypt(data, key, nonce, None)?; + let decrypted = decrypt(data, key, nonce, password)?; let db_open_req = open_idb()?; // On success callback @@ -189,11 +238,17 @@ async fn fetch_resources(request_uri: Uri, url: String) -> Result<()> { db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().unchecked_ref())); } Ok(resp) if resp.status() == StatusCode::NOT_FOUND => { - create_not_found_ui(); + render_message("Either the paste was burned or it never existed.".into()); + } + Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => { + render_message("Invalid paste URL.".into()); + } + Ok(err) => { + render_message(format!("{}", err.status_text()).into()); + } + Err(err) => { + render_message(format!("{}", err).into()); } - Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => {} - Ok(err) => {} - Err(err) => {} } Ok(()) diff --git a/web/src/main.ts b/web/src/main.ts index e156f47..83ed4a7 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -26,7 +26,7 @@ function loadFromDb() { createVideoPasteUi(data); break; default: - createBrokenStateUi(); + renderMessage("Something went wrong. Try clearing local data."); break; } @@ -159,26 +159,15 @@ function createMultiMediaPasteUi(tag, expiration, data, downloadMessage) { bodyEle.appendChild(mainEle); } -// Exported to main.rs -function createNotFoundUi() { +function renderMessage(message) { let body = document.getElementsByTagName("body")[0]; body.textContent = ''; - body.appendChild(createGenericError("Either the paste has been burned or one never existed.")); -} - -function createBrokenStateUi() { - let body = document.getElementsByTagName("body")[0]; - body.textContent = ''; - body.appendChild(createGenericError("Something went wrong. Try clearing local data.")); -} - -function createGenericError(message) { let mainEle = document.createElement("main"); mainEle.classList.add("hljs"); mainEle.classList.add("centered"); mainEle.classList.add("fullscreen"); mainEle.textContent = message; - return mainEle; + body.appendChild(mainEle); } window.addEventListener("hashchange", () => location.reload()); \ No newline at end of file