From 5466f687ac6cd5d8070bb4f3b72db64e0abc27f2 Mon Sep 17 00:00:00 2001 From: 2ro <17595647+2ro@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:55:23 -0400 Subject: [PATCH] fix: route login/authorize callback POSTs over Tor, drop goblin-wallet UA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The login (kind 22242) and authorize callback POSTs carry a signed Nostr event containing the user's identity pubkey. They went out via crate::http::HttpClient::send (clearnet even when Tor is on) with an identifying "User-Agent: goblin-wallet" header, so the callback server saw the user's real IP tied to their Nostr identity — a deanonymization leak. Reroute both post_login_event and post_authorize_event through crate::tor::http_request_bytes, which honors route_over_tor(): Tor when the open wallet routes over Tor, clearnet otherwise. Drop the goblin-wallet UA; the helper sets a browser-like default User-Agent, so the traffic is not trivially fingerprintable as Goblin at the destination. Content-Type is preserved. Behavior with Tor off is unchanged (clearnet POST via the helper's clearnet branch). --- src/nostr/authuri.rs | 35 ++++++++++++++++++----------------- src/nostr/loginuri.rs | 29 +++++++++++++++-------------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/nostr/authuri.rs b/src/nostr/authuri.rs index 8b3fd258..8630805f 100644 --- a/src/nostr/authuri.rs +++ b/src/nostr/authuri.rs @@ -432,12 +432,16 @@ pub fn build_authorize_event(keys: &Keys, template: &Template) -> Result, "d": , "event": }`. Goes through the -/// app's shared [`crate::http::HttpClient`], so it follows the exact same -/// transport policy (proxy settings included) as every other clearnet call. -/// The `c` and `d` fields correlate the delivery to the request the server -/// minted, since the arbitrary event carries no challenge or domain tag of its -/// own. The caller wraps this in its own timeout. +/// `{"c": , "d": , "event": }`. Goes through +/// [`crate::tor::http_request_bytes`], the transport-aware helper: over Tor when +/// the open wallet routes over Tor, clearnet otherwise. Routing over Tor when it +/// is on keeps the callback server from tying the user's real IP to the identity +/// pubkey in the signed event. The helper sets a browser-like default +/// `User-Agent`, so the request is not trivially fingerprintable as Goblin at the +/// destination (no identifying `goblin-wallet` header). The `c` and `d` fields +/// correlate the delivery to the request the server minted, since the arbitrary +/// event carries no challenge or domain tag of its own. The caller wraps this in +/// its own timeout. pub async fn post_authorize_event( callback: &str, challenge: &str, @@ -445,17 +449,14 @@ pub async fn post_authorize_event( event: &Event, ) -> Result<(), String> { let body = serde_json::json!({ "c": challenge, "d": domain, "event": event }).to_string(); - let req = hyper::Request::builder() - .method(hyper::Method::POST) - .uri(callback) - .header("Content-Type", "application/json") - .header("User-Agent", "goblin-wallet") - .body(http_body_util::Full::new(bytes::Bytes::from(body))) - .map_err(|e| e.to_string())?; - let resp = crate::http::HttpClient::send(req) - .await - .map_err(|e| e.to_string())?; - let status = resp.status().as_u16(); + let (status, _) = crate::tor::http_request_bytes( + "POST", + callback.to_string(), + Some(body.into_bytes()), + vec![("Content-Type".to_string(), "application/json".to_string())], + ) + .await + .ok_or_else(|| "callback request failed".to_string())?; if (200..300).contains(&status) { Ok(()) } else { diff --git a/src/nostr/loginuri.rs b/src/nostr/loginuri.rs index d1ef013f..e90b9ac9 100644 --- a/src/nostr/loginuri.rs +++ b/src/nostr/loginuri.rs @@ -241,22 +241,23 @@ pub fn build_login_event(keys: &Keys, challenge: &str, domain: &str) -> Result}`. -/// Goes through the app's shared [`crate::http::HttpClient`], so it follows -/// the exact same transport policy (proxy settings included) as every other -/// clearnet call. The caller wraps this in its own timeout. +/// Goes through [`crate::tor::http_request_bytes`], the transport-aware helper: +/// over Tor when the open wallet routes over Tor, clearnet otherwise. Routing +/// over Tor when it is on keeps the callback server from tying the user's real +/// IP to the identity pubkey in the signed event. The helper sets a browser-like +/// default `User-Agent`, so the request is not trivially fingerprintable as +/// Goblin at the destination (no identifying `goblin-wallet` header). The caller +/// wraps this in its own timeout. pub async fn post_login_event(callback: &str, event: &Event) -> Result<(), String> { let body = serde_json::json!({ "event": event }).to_string(); - let req = hyper::Request::builder() - .method(hyper::Method::POST) - .uri(callback) - .header("Content-Type", "application/json") - .header("User-Agent", "goblin-wallet") - .body(http_body_util::Full::new(bytes::Bytes::from(body))) - .map_err(|e| e.to_string())?; - let resp = crate::http::HttpClient::send(req) - .await - .map_err(|e| e.to_string())?; - let status = resp.status().as_u16(); + let (status, _) = crate::tor::http_request_bytes( + "POST", + callback.to_string(), + Some(body.into_bytes()), + vec![("Content-Type".to_string(), "application/json".to_string())], + ) + .await + .ok_or_else(|| "callback request failed".to_string())?; if (200..300).contains(&status) { Ok(()) } else {