mirror of
https://code.gri.mw/GUI/grim.git
synced 2026-07-11 17:28:55 +00:00
ce1c071f3c
Wallet: - identity.json (NIP-49 ncryptsec) now written 0600 in a 0700 dir so a local user can't grind the wallet password offline (+ regression test). - Wallet password held as a ZeroingString through init_nostr so it's scrubbed on drop instead of lingering in a plain String for the session. - Replaced 4 .unwrap() on re-read tx_meta with graceful guards (archive wipe mid-send could otherwise panic the nostr/task thread). - Tor::http_request/post: bind the client once via let-else and propagate TLS builder errors, fixing a TOCTOU unwrap panic on concurrent Tor restart. goblin-nip05d server (redeployed to goblin.st, verified live): - One-name-per-pubkey now enforced by a partial UNIQUE index (closes the check-then-insert race); INSERT rows-affected==0 returns 409 not a false 201. - NIP-98 replay protection: one-time auth event-id enforcement within the freshness window; tightened forward skew to +5s. - Rate-limited the unauthenticated GET endpoints; SQLite in WAL mode. - Verified live: replay rejected, second name for a pubkey blocked. Audit verdict: fund-safety invariants (never auto-pay Invoice1; S2/I2 finalization bound to counterparty npub) and Tor-from-day-one all hold. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
106 lines
2.9 KiB
Rust
106 lines
2.9 KiB
Rust
use base64::Engine;
|
|
use nostr_sdk::prelude::*;
|
|
use sha2::{Digest, Sha256};
|
|
use std::process::Command;
|
|
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn replay_and_double_name_rejected() {
|
|
let keys = Keys::generate();
|
|
let pk = keys.public_key().to_hex();
|
|
let server = "https://goblin.st";
|
|
|
|
// Build a register POST for name A.
|
|
let name_a = format!("r{}", &pk[..7]);
|
|
let url = format!("{server}/api/v1/register");
|
|
let body = serde_json::json!({"name": name_a, "pubkey": pk}).to_string();
|
|
let ph = hex::encode(Sha256::digest(body.as_bytes()));
|
|
let ev = EventBuilder::new(Kind::HttpAuth, "")
|
|
.tag(Tag::custom(TagKind::custom("u"), [url.clone()]))
|
|
.tag(Tag::custom(TagKind::custom("method"), ["POST".to_string()]))
|
|
.tag(Tag::custom(TagKind::custom("payload"), [ph]))
|
|
.sign_with_keys(&keys)
|
|
.unwrap();
|
|
let auth = format!(
|
|
"Nostr {}",
|
|
base64::engine::general_purpose::STANDARD.encode(ev.as_json())
|
|
);
|
|
let post = |a: &str, b: &str| {
|
|
String::from_utf8_lossy(
|
|
&Command::new("curl")
|
|
.args([
|
|
"-s",
|
|
"-X",
|
|
"POST",
|
|
&url,
|
|
"-H",
|
|
&format!("Authorization: {a}"),
|
|
"-H",
|
|
"Content-Type: application/json",
|
|
"-d",
|
|
b,
|
|
])
|
|
.output()
|
|
.unwrap()
|
|
.stdout,
|
|
)
|
|
.to_string()
|
|
};
|
|
let r1 = post(&auth, &body);
|
|
let r2 = post(&auth, &body); // exact replay (same auth event id)
|
|
println!("first: {r1}");
|
|
println!("replay: {r2}");
|
|
assert!(r1.contains("nip05"), "first register should succeed");
|
|
assert!(
|
|
r2.contains("replayed"),
|
|
"replay should be rejected, got: {r2}"
|
|
);
|
|
|
|
// Second DISTINCT name with a FRESH signature but same pubkey -> one-name rule.
|
|
let name_b = format!("s{}", &pk[..7]);
|
|
let body_b = serde_json::json!({"name": name_b, "pubkey": pk}).to_string();
|
|
let ph_b = hex::encode(Sha256::digest(body_b.as_bytes()));
|
|
let ev_b = EventBuilder::new(Kind::HttpAuth, "")
|
|
.tag(Tag::custom(TagKind::custom("u"), [url.clone()]))
|
|
.tag(Tag::custom(TagKind::custom("method"), ["POST".to_string()]))
|
|
.tag(Tag::custom(TagKind::custom("payload"), [ph_b]))
|
|
.sign_with_keys(&keys)
|
|
.unwrap();
|
|
let auth_b = format!(
|
|
"Nostr {}",
|
|
base64::engine::general_purpose::STANDARD.encode(ev_b.as_json())
|
|
);
|
|
let r3 = post(&auth_b, &body_b);
|
|
println!("2nd name: {r3}");
|
|
assert!(
|
|
r3.contains("already has a name") || r3.contains("pubkey already"),
|
|
"one-name rule should block, got: {r3}"
|
|
);
|
|
|
|
// Cleanup name A.
|
|
let del_url = format!("{server}/api/v1/register/{name_a}");
|
|
let ev_d = EventBuilder::new(Kind::HttpAuth, "")
|
|
.tag(Tag::custom(TagKind::custom("u"), [del_url.clone()]))
|
|
.tag(Tag::custom(
|
|
TagKind::custom("method"),
|
|
["DELETE".to_string()],
|
|
))
|
|
.sign_with_keys(&keys)
|
|
.unwrap();
|
|
let auth_d = format!(
|
|
"Nostr {}",
|
|
base64::engine::general_purpose::STANDARD.encode(ev_d.as_json())
|
|
);
|
|
let _ = Command::new("curl")
|
|
.args([
|
|
"-s",
|
|
"-X",
|
|
"DELETE",
|
|
&del_url,
|
|
"-H",
|
|
&format!("Authorization: {auth_d}"),
|
|
])
|
|
.output();
|
|
println!("✓ replay + one-name-per-pubkey enforced");
|
|
}
|