1
0
forked from GRIN/grim

fix: route login/authorize callback POSTs over Tor, drop goblin-wallet UA

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).
This commit is contained in:
2ro
2026-07-10 14:55:23 -04:00
parent d47a71a03b
commit 5466f687ac
2 changed files with 33 additions and 31 deletions
+18 -17
View File
@@ -432,12 +432,16 @@ pub fn build_authorize_event(keys: &Keys, template: &Template) -> Result<Event,
}
/// POST the signed authorize event to the callback as
/// `{"c": <nonce>, "d": <domain>, "event": <event-json>}`. 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": <nonce>, "d": <domain>, "event": <event-json>}`. 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 {
+15 -14
View File
@@ -241,22 +241,23 @@ pub fn build_login_event(keys: &Keys, challenge: &str, domain: &str) -> Result<E
}
/// POST the signed login event to the callback as `{"event": <event-json>}`.
/// 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 {