From fba8738a8ab49d60b9f2f1f86fb4f4e7c21ad041 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sat, 29 Jun 2024 15:03:00 +0200 Subject: [PATCH 1/3] prompt user on non-whitelisted hosts Maintaining a whitelist in the binary does not scale and might put off third party devs. For a host that is not in the whitelist, we instead prompt the user if they want to accept the connection. Since we already run a server, we simply launch the browser on confirmation page served by our server. It is a bit weird UX, but it was the easiest solution I could find that works everywhere. Alternatives considered: - native dialogs using rfd - unfortunately does not work on macOS as the main process is windowless - native dialogs using SDL: looks extremely ugly, hard to figure out deployment - use Rust's Tauri or Qt or similar for native UIs: hard to deploy --- Cargo.lock | 201 ++++++++++++++++ bitbox-bridge/Cargo.toml | 1 + .../resources/confirmation_dialog.html | 20 ++ bitbox-bridge/src/web.rs | 217 +++++++++++++++--- 4 files changed, 409 insertions(+), 30 deletions(-) create mode 100644 bitbox-bridge/resources/confirmation_dialog.html diff --git a/Cargo.lock b/Cargo.lock index 4a42b21..a622144 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,6 +149,7 @@ dependencies = [ "tokio", "u2fframing", "warp", + "webbrowser", "windows-service", ] @@ -167,6 +168,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "bstr" version = "0.2.12" @@ -206,6 +216,12 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "0.1.10" @@ -285,6 +301,26 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes 1.4.0", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -616,6 +652,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.0" @@ -808,6 +853,28 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if 1.0.0", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "js-sys" version = "0.3.69" @@ -907,6 +974,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "num-traits" version = "0.2.11" @@ -926,6 +999,40 @@ dependencies = [ "libc", ] +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "object" version = "0.36.1" @@ -1741,6 +1848,34 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "425ba64c1e13b1c6e8c5d2541c8fac10022ca584f33da781db01b5756aef1f4e" +dependencies = [ + "block2", + "core-foundation", + "home", + "jni", + "log", + "ndk-context", + "objc2", + "objc2-foundation", + "url", + "web-sys", +] + [[package]] name = "widestring" version = "1.1.0" @@ -1798,6 +1933,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1816,6 +1960,21 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.0" @@ -1847,6 +2006,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.5", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" @@ -1859,6 +2024,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.0" @@ -1871,6 +2042,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.0" @@ -1889,6 +2066,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.0" @@ -1901,6 +2084,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.0" @@ -1913,6 +2102,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" @@ -1925,6 +2120,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.0" diff --git a/bitbox-bridge/Cargo.toml b/bitbox-bridge/Cargo.toml index 36a0a39..b5ae6e5 100644 --- a/bitbox-bridge/Cargo.toml +++ b/bitbox-bridge/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" license = "Apache-2.0" [dependencies] +webbrowser = "1.0" env_logger = "0.11" futures = { workspace = true } futures-util = { workspace = true } diff --git a/bitbox-bridge/resources/confirmation_dialog.html b/bitbox-bridge/resources/confirmation_dialog.html new file mode 100644 index 0000000..d302574 --- /dev/null +++ b/bitbox-bridge/resources/confirmation_dialog.html @@ -0,0 +1,20 @@ + + + + + BitBoxBridge + + +

BitBoxBridge

+

{{ message }}

+ + + + + diff --git a/bitbox-bridge/src/web.rs b/bitbox-bridge/src/web.rs index b9890c6..43cc98b 100644 --- a/bitbox-bridge/src/web.rs +++ b/bitbox-bridge/src/web.rs @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use futures::channel::mpsc; +use futures::channel::{mpsc, oneshot}; use futures::prelude::*; use futures_util::sink::SinkExt; use percent_encoding::percent_decode_str; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; -use warp::{self, Filter, Rejection}; +use std::sync::atomic::AtomicU32; +use std::sync::{Arc, Mutex}; +use warp::{self, Filter, Rejection, Reply}; use crate::error::WebError; use crate::usb::UsbDevices; @@ -155,7 +157,144 @@ async fn ws_upgrade( })) } +// Global state to store the current oneshot sender +struct ConfirmState { + counter: AtomicU32, + sender: Mutex, String)>>, +} + +impl ConfirmState { + fn new() -> Arc { + Arc::new(ConfirmState { + counter: AtomicU32::new(0), + sender: Mutex::new(HashMap::new()), + }) + } +} + +fn with_state( + state: T, +) -> impl Filter + Clone { + warp::any().map(move || state.clone()) +} + +#[derive(Clone)] +struct AllowedHosts(Arc>>); + +impl AllowedHosts { + fn new() -> AllowedHosts { + AllowedHosts(Arc::new(Mutex::new(HashSet::new()))) + } +} + +async fn user_confirm( + confirm_state: Arc, + message: String, + base_url: &str, +) -> Result { + let (tx, rx) = oneshot::channel(); + let counter = confirm_state + .counter + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + { + let mut sender = confirm_state.sender.lock().unwrap(); + sender.insert(counter, (tx, message)); + } + + // Launch the web browser to show the dialog + let dialog_url = format!("{}/confirm/{}", base_url, counter); + if webbrowser::open(&dialog_url).is_err() { + return Err(()); + } + + // Wait for the user's response from the HTTP handler + rx.await.map_err(|_| ()) +} + +async fn user_confirm_origin( + confirm_state: Arc, + allowed_hosts: AllowedHosts, + host: &str, + base_url: &str, +) -> Result { + // Early return for whitelisted hosts. + if is_valid_origin(host) { + return Ok(true); + } + { + // Early return if the origin was previously allowed/accepted by the user. + if allowed_hosts.0.lock().unwrap().contains(host) { + return Ok(true); + } + } + let result = user_confirm( + confirm_state, + format!("Allow {} to connect to your BitBox?", host), + base_url, + ) + .await?; + if result { + allowed_hosts.0.lock().unwrap().insert(host.into()); + } + Ok(result) +} + +fn setup_confirm_routes( + confirm_state: Arc, +) -> impl Filter + Clone { + let confirm_dialog = warp::path!("confirm" / u32) + .and(warp::get()) + .and(with_state(confirm_state.clone())) + .map(|counter: u32, confirm_state: Arc| { + let sender_locked = confirm_state.sender.lock().unwrap(); + if let Some((_, message)) = sender_locked.get(&counter) { + let html = include_str!("../resources/confirmation_dialog.html"); + let ctx = { + let mut ctx = tera::Context::new(); + ctx.insert("counter", &counter); + ctx.insert("message", &message); + ctx + }; + let body = match tera::Tera::one_off(html, &ctx, true) { + Ok(reply) => reply, + Err(_) => "Could not render tera template".into(), + }; + warp::reply::html(body).into_response() + } else { + // No user confirmation active. + warp::reply::with_status("", warp::http::StatusCode::BAD_REQUEST).into_response() + } + }); + + async fn handle_user_response( + counter: u32, + choice: bool, + confirm_state: Arc, + ) -> Result { + if let Some((sender, _)) = confirm_state.sender.lock().unwrap().remove(&counter) { + let _ = sender.send(choice); + + Ok(warp::reply::with_status("", warp::http::StatusCode::OK)) + } else { + Ok(warp::reply::with_status( + "", + warp::http::StatusCode::BAD_REQUEST, + )) + } + } + + let handle_response = warp::path!("confirm" / "response" / u32 / bool) + .and(warp::post()) + .and(with_state(confirm_state.clone())) + .and_then(handle_user_response); + + handle_response.or(confirm_dialog) +} + pub async fn create(usb_devices: UsbDevices, notify_tx: mpsc::Sender<()>, addr: SocketAddr) { + let confirm_state = ConfirmState::new(); + let allowed_hosts = AllowedHosts::new(); + // create a warp filter out of "usb_devices" to pass it into our handlers later let usb_devices = warp::any().map(move || (usb_devices.clone(), notify_tx.clone())); @@ -197,34 +336,51 @@ pub async fn create(usb_devices: UsbDevices, notify_tx: mpsc::Sender<()>, addr: // Only accept some origin // Use untuple_one at the end to get rid of the "unit" return value let check_origin = warp::header::optional("origin") - .and_then(|origin: Option| { - debug!("Origin: {:?}", origin); - async move { - if let Some(origin) = origin { - let scheme_str = origin.scheme_str(); - if scheme_str == Some("chrome-extension") || scheme_str == Some("moz-extension") - { - debug!("Allow Chrome/Firefox extension"); - return Ok(()); - } - match origin.host() { - Some(host) => { - if !is_valid_origin(host) { - warn!("Not whitelisted origin tried to connect: {}", host); + .and(with_state(confirm_state.clone())) + .and(with_state(allowed_hosts)) + .and(with_state(addr)) + .and_then( + |origin: Option, + confirm_state: Arc, + allowed_hosts: AllowedHosts, + addr| { + debug!("Origin: {:?}", origin); + async move { + if let Some(origin) = origin { + let scheme_str = origin.scheme_str(); + if scheme_str == Some("chrome-extension") + || scheme_str == Some("moz-extension") + { + debug!("Allow Chrome/Firefox extension"); + return Ok(()); + } + match origin.host() { + Some(host) => { + if !user_confirm_origin( + confirm_state, + allowed_hosts, + host, + &format!("http://{}", addr), + ) + .await + .unwrap() + { + warn!("Not whitelisted origin tried to connect: {}", host); + return Err(warp::reject::custom(WebError::NonLocalIp)); + } + } + None => { + warn!("Not whitelisted origin tried to connect"); return Err(warp::reject::custom(WebError::NonLocalIp)); } } - None => { - warn!("Not whitelisted origin tried to connect"); - return Err(warp::reject::custom(WebError::NonLocalIp)); - } } + // If there is no `origin` header, it must mean that the connection is from + // a website hosted by ourselves. Which is fine. + Ok(()) } - // If there is no `origin` header, it must mean that the connection is from - // a website hosted by ourselves. Which is fine. - Ok(()) - } - }) + }, + ) .untuple_one(); let opt_origin = warp::header::optional("origin"); @@ -266,7 +422,7 @@ pub async fn create(usb_devices: UsbDevices, notify_tx: mpsc::Sender<()>, addr: let websocket = warp::get() .and(v1_root) .and(websocket) - .and(check_origin) + .and(check_origin.clone()) .and(warp::ws()) .and(usb_devices.clone()) .and_then(ws_upgrade); @@ -275,7 +431,7 @@ pub async fn create(usb_devices: UsbDevices, notify_tx: mpsc::Sender<()>, addr: let devices = warp::get() .and(v1_root) .and(devices) - .and(check_origin) + .and(check_origin.clone()) .and(usb_devices) .and_then(list_devices) .and(opt_origin) @@ -285,15 +441,16 @@ pub async fn create(usb_devices: UsbDevices, notify_tx: mpsc::Sender<()>, addr: let info = warp::get() .and(api) .and(info) - .and(check_origin) + .and(check_origin.clone()) .and_then(show_info) .and(opt_origin) .map(add_origin); + let confirm_routes = setup_confirm_routes(confirm_state.clone()); // combine routes let routes = only_local_ip .and(only_local_vhost) - .and(websocket.or(devices).or(root).or(info)) + .and(websocket.or(devices).or(root).or(info).or(confirm_routes)) .recover(|err: warp::Rejection| { async { if let Some(err) = err.find::() { From 4f67f34bcd7705aead6ec3471f27cf89037b0651 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 11 Jul 2024 14:38:36 +0200 Subject: [PATCH 2/3] bridge: user confirmation styling Added basic BitBox styling. The HTML file is self contained and only uses inline styles and inline svg graphics. --- .../resources/confirmation_dialog.html | 108 +++++++++++++++++- 1 file changed, 104 insertions(+), 4 deletions(-) diff --git a/bitbox-bridge/resources/confirmation_dialog.html b/bitbox-bridge/resources/confirmation_dialog.html index d302574..fbddfd2 100644 --- a/bitbox-bridge/resources/confirmation_dialog.html +++ b/bitbox-bridge/resources/confirmation_dialog.html @@ -3,12 +3,112 @@ BitBoxBridge + -

BitBoxBridge

-

{{ message }}

- - + + + +
+ +

BitBoxBridge

+

{{ message }}

+ + +