From db793bc13d2aaab6818128f41b4aab629542d336 Mon Sep 17 00:00:00 2001 From: 2ro <17595647+2ro@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:03:32 -0400 Subject: [PATCH] goblin: log in to nostr apps with your nsec (Advanced -> Nostr key) Advanced gains a password-gated Nostr key card: reveal the wallet's nsec, Copy it, or show it as a QR. Scanning that QR (or pasting the copied nsec) into a nostr app's private-key login - e.g. magick.market - signs you in with the same identity the wallet uses. The nsec is derived on demand behind the wallet password and never persisted; wrong password cannot leak it. Six advanced.* strings added across all six locales. --- locales/de.yml | 6 +++ locales/en.yml | 6 +++ locales/fr.yml | 6 +++ locales/ru.yml | 6 +++ locales/tr.yml | 6 +++ locales/zh-CN.yml | 6 +++ src/gui/views/goblin/mod.rs | 91 +++++++++++++++++++++++++++++++++++++ src/wallet/wallet.rs | 21 +++++++++ 8 files changed, 148 insertions(+) diff --git a/locales/de.yml b/locales/de.yml index a3934e4..2e55b43 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -609,6 +609,12 @@ goblin: repair_confirm: "Ja, jetzt reparieren" repair_confirm_note: "Die Reparatur scannt die Chain neu und kann einige Minuten dauern." restore_confirm_note: "Dies löscht lokale Daten und baut sie aus deinem Seed neu auf — das kann einige Minuten dauern." + nostr_key: "Nostr-Schlüssel" + nostr_key_desc: "Dein nsec, der geheime Schlüssel deiner Nostr-Identität. Kopiere ihn oder zeige den QR-Code, um dich bei Nostr-Apps wie magick.market anzumelden. Wer ihn hat, kontrolliert deine Identität, also halte ihn geheim." + reveal_nsec: "Schlüssel anzeigen" + copy_nsec: "nsec kopieren" + show_qr: "QR anzeigen" + hide_qr: "QR ausblenden" privacy: title: "Netzwerk-Privatsphäre" intro: "Goblin sendet seinen privaten Datenverkehr durch das Nym mixnet — ein Netzwerk mit fünf Sprüngen, das verbirgt, wer mit wem kommuniziert, sodass ein Relay eine Zahlung nicht zu dir zurückverfolgen kann." diff --git a/locales/en.yml b/locales/en.yml index e43a9ec..49336e4 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -609,6 +609,12 @@ goblin: repair_confirm: "Yes, repair now" repair_confirm_note: "Repair re-scans the chain and can take a few minutes." restore_confirm_note: "This erases local data and rebuilds it from your seed — it can take several minutes." + nostr_key: "Nostr key" + nostr_key_desc: "Your nsec, the secret key to your nostr identity. Copy it or show its QR to log in to nostr apps like magick.market. Anyone who has it controls your identity, so keep it private." + reveal_nsec: "Show key" + copy_nsec: "Copy nsec" + show_qr: "Show QR" + hide_qr: "Hide QR" privacy: title: "Network privacy" intro: "Goblin sends its private traffic through the Nym mixnet — a five-hop network that hides who is talking to whom, so a relay can't link a payment back to you." diff --git a/locales/fr.yml b/locales/fr.yml index 0dd7d61..7853173 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -609,6 +609,12 @@ goblin: repair_confirm: "Oui, réparer maintenant" repair_confirm_note: "La réparation réanalyse la chaîne et peut prendre quelques minutes." restore_confirm_note: "Cela efface les données locales et les reconstruit depuis votre seed — cela peut prendre plusieurs minutes." + nostr_key: "Clé Nostr" + nostr_key_desc: "Votre nsec, la clé secrète de votre identité Nostr. Copiez-la ou affichez son QR pour vous connecter à des applis Nostr comme magick.market. Quiconque la possède contrôle votre identité, gardez-la privée." + reveal_nsec: "Afficher la clé" + copy_nsec: "Copier le nsec" + show_qr: "Afficher le QR" + hide_qr: "Masquer le QR" privacy: title: "Confidentialité réseau" intro: "Goblin envoie son trafic privé via le mixnet Nym — un réseau à cinq sauts qui masque qui parle à qui, afin qu'un relais ne puisse pas relier un paiement à vous." diff --git a/locales/ru.yml b/locales/ru.yml index 0933ebd..8ea3dbb 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -609,6 +609,12 @@ goblin: repair_confirm: "Да, восстановить сейчас" repair_confirm_note: "Восстановление повторно сканирует цепочку и может занять несколько минут." restore_confirm_note: "Это стирает локальные данные и восстанавливает их из seed-фразы — может занять несколько минут." + nostr_key: "Ключ Nostr" + nostr_key_desc: "Ваш nsec, секретный ключ вашей личности Nostr. Скопируйте его или покажите QR-код, чтобы войти в приложения Nostr, такие как magick.market. Любой, у кого он есть, управляет вашей личностью, держите его в секрете." + reveal_nsec: "Показать ключ" + copy_nsec: "Копировать nsec" + show_qr: "Показать QR" + hide_qr: "Скрыть QR" privacy: title: "Сетевая приватность" intro: "Goblin отправляет приватный трафик через mixnet Nym — сеть из пяти переходов, скрывающую, кто с кем общается, чтобы реле не могло связать платёж с вами." diff --git a/locales/tr.yml b/locales/tr.yml index c558b10..6e8d4fe 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -609,6 +609,12 @@ goblin: repair_confirm: "Evet, şimdi onar" repair_confirm_note: "Onarım zinciri yeniden tarar ve birkaç dakika sürebilir." restore_confirm_note: "Bu, yerel verileri siler ve seed'inizden yeniden oluşturur — birkaç dakika sürebilir." + nostr_key: "Nostr anahtarı" + nostr_key_desc: "nsec'iniz, Nostr kimliğinizin gizli anahtarı. magick.market gibi Nostr uygulamalarında oturum açmak için kopyalayın veya QR kodunu gösterin. Ona sahip olan herkes kimliğinizi kontrol eder, gizli tutun." + reveal_nsec: "Anahtarı göster" + copy_nsec: "nsec'i kopyala" + show_qr: "QR göster" + hide_qr: "QR gizle" privacy: title: "Ağ gizliliği" intro: "Goblin özel trafiğini Nym mixnet üzerinden gönderir — kimin kiminle konuştuğunu gizleyen beş atlamalı bir ağ, böylece bir relay bir ödemeyi sana bağlayamaz." diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index a0cb526..0b132ff 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -609,6 +609,12 @@ goblin: repair_confirm: "是的,立即修复" repair_confirm_note: "修复会重新扫描链,可能需要几分钟。" restore_confirm_note: "这会清除本地数据并从助记词重建——可能需要几分钟。" + nostr_key: "Nostr 密钥" + nostr_key_desc: "您的 nsec,即 Nostr 身份的私钥。复制它或显示二维码,即可登录 magick.market 等 Nostr 应用。持有它的人即可控制您的身份,请妥善保管。" + reveal_nsec: "显示密钥" + copy_nsec: "复制 nsec" + show_qr: "显示二维码" + hide_qr: "隐藏二维码" privacy: title: "网络隐私" intro: "Goblin 通过 Nym mixnet 发送其私密流量 — 这是一个五跳网络,可隐藏通信双方的身份,使中继无法将付款关联到你。" diff --git a/src/gui/views/goblin/mod.rs b/src/gui/views/goblin/mod.rs index bc09822..0011297 100644 --- a/src/gui/views/goblin/mod.rs +++ b/src/gui/views/goblin/mod.rs @@ -153,6 +153,15 @@ struct AdvancedState { revealed: Option, /// Set when the entered password didn't decrypt the seed. wrong_pass: bool, + /// Password typed to reveal the nostr secret key (nsec). + nsec_pass: String, + /// The revealed nsec, held only while shown (cleared on hide/back). + nsec_revealed: Option, + /// Set when the entered password didn't unlock the nostr identity. + nsec_wrong: bool, + /// Whether the nsec QR is expanded (so it can be scanned to log in + /// elsewhere, e.g. magick.market's private-key login). + nsec_qr: bool, /// Armed "really restore?" confirm. confirm_restore: bool, /// Armed "really repair?" confirm (repair takes a few minutes). @@ -3139,6 +3148,88 @@ impl GoblinWalletView { }); ui.add_space(12.0); + // Nostr key (nsec). Password-gated reveal, then Copy + a QR + // so it can be carried into a nostr app's private-key login + // (e.g. magick.market) without retyping. Same gate as the + // recovery phrase above. + w::card(ui, |ui| { + ui.set_min_width(ui.available_width()); + advanced_head(ui, &t!("goblin.advanced.nostr_key"), t.surface_text); + advanced_desc(ui, &t!("goblin.advanced.nostr_key_desc")); + ui.add_space(10.0); + if let Some(nsec) = adv.nsec_revealed.clone() { + w::field_well(ui, |ui| { + ui.label( + RichText::new(&nsec) + .font(FontId::new(14.0, fonts::medium())) + .color(t.surface_text), + ); + }); + ui.add_space(10.0); + if w::big_action_on_card(ui, &t!("goblin.advanced.copy_nsec")).clicked() + { + cb.copy_string_to_buffer(nsec.clone()); + } + ui.add_space(8.0); + let qr_label = if adv.nsec_qr { + t!("goblin.advanced.hide_qr") + } else { + t!("goblin.advanced.show_qr") + }; + if w::big_action_on_card(ui, &qr_label).clicked() { + adv.nsec_qr = !adv.nsec_qr; + } + if adv.nsec_qr { + ui.add_space(10.0); + ui.vertical_centered(|ui| { + w::qr_code(ui, &nsec, 220.0); + }); + } + ui.add_space(10.0); + if w::big_action_on_card(ui, &t!("goblin.advanced.hide")).clicked() { + adv.nsec_revealed = None; + adv.nsec_qr = false; + adv.nsec_pass.clear(); + } + } else { + w::field_well(ui, |ui| { + TextEdit::new(egui::Id::from("advanced_nsec_pass")) + .focus(false) + .hint_text(t!("goblin.advanced.password")) + .password() + .text_color(t.surface_text) + .body() + .ui(ui, &mut adv.nsec_pass, cb); + }); + if adv.nsec_wrong { + ui.add_space(6.0); + ui.label( + RichText::new(t!("goblin.advanced.wrong_password")) + .font(FontId::new(13.0, fonts::medium())) + .color(t.neg), + ); + } + ui.add_space(10.0); + ui.add_enabled_ui(!adv.nsec_pass.is_empty(), |ui| { + if w::big_action_on_card(ui, &t!("goblin.advanced.reveal_nsec")) + .clicked() + { + match wallet.get_nostr_nsec(adv.nsec_pass.clone()) { + Ok(nsec) => { + adv.nsec_revealed = Some(nsec); + adv.nsec_wrong = false; + adv.nsec_pass.clear(); + } + Err(_) => { + adv.nsec_wrong = true; + } + } + } + }); + } + }); + ui.add_space(12.0); + // Delete. w::card(ui, |ui| { ui.set_min_width(ui.available_width()); diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index b96350f..51bbf23 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -488,6 +488,27 @@ impl Wallet { /// shares nothing with it), atomically moving the registered username /// (if any) to the new key via the name server. Blocking (network I/O): /// call from a worker thread. Returns the new bech32 npub. + /// The nostr secret key (nsec, bech32) for this wallet, gated on the wallet + /// password. Used by Advanced → "Nostr key" so the user can copy it or show + /// it as a QR to log in to nostr apps (e.g. magick.market). Unlocking the + /// stored identity both verifies the password and yields the keys, so a + /// wrong password can never leak the key. The value is derived on demand and + /// never persisted. + pub fn get_nostr_nsec(&self, password: String) -> Result { + let svc = self + .nostr_service() + .ok_or_else(|| "nostr identity not ready".to_string())?; + use nostr_sdk::ToBech32; + let keys = svc + .identity + .read() + .unlock(&password) + .map_err(|_| "wrong password".to_string())?; + keys.secret_key() + .to_bech32() + .map_err(|e| format!("nsec encode failed: {e}")) + } + pub fn rotate_nostr_identity(&self, password: String) -> Result { let svc = self .nostr_service()