Export of private keys is now available

This commit is contained in:
Mike Dilger 2023-01-04 15:23:17 +13:00
parent 5ebc0a9139
commit fb72af07a6
4 changed files with 155 additions and 18 deletions

View File

@ -23,6 +23,7 @@ As of right now you can (if you aren't stopped by some bug):
- [x] **Creting content** - [x] **Creting content**
- [x] Generating a key that is kept securely within the client encrypted under a password that you need to unlock it every time you start the client. - [x] Generating a key that is kept securely within the client encrypted under a password that you need to unlock it every time you start the client.
- [x] Generate or import (hex or bech32) a private key (your identity) (also kept under a password) - [x] Generate or import (hex or bech32) a private key (your identity) (also kept under a password)
- [x] Exporting your private key encrypted, or decrypted as bech32 or hex
- [x] Choose relays to post to (from among some starting relays, plus ones the client has seen in events), including entering a relay URL directly. - [x] Choose relays to post to (from among some starting relays, plus ones the client has seen in events), including entering a relay URL directly.
- [x] Post root-level text messages - [x] Post root-level text messages
- [x] Post replies to other people's text messages - [x] Post replies to other people's text messages
@ -30,7 +31,6 @@ As of right now you can (if you aren't stopped by some bug):
### Missing Critical Features ### Missing Critical Features
- [ ] Exporting your private key in any format (encrypted or unencrypted in bech32/hex format), although you can pull the encrypted private key from the database with an SQL command.
- [ ] An inbox of replies to your posts (they are loaded, but you have to go into each feed to find them, they are not collated into an inbox) - [ ] An inbox of replies to your posts (they are loaded, but you have to go into each feed to find them, they are not collated into an inbox)
- [ ] Setting your metadata and syncing it with the network. - [ ] Setting your metadata and syncing it with the network.
- [ ] Syncing the relays you use with the network - [ ] Syncing the relays you use with the network
@ -96,8 +96,8 @@ We do not intend to support the following features/NIPs:
- [x] configurable look-back time - [x] configurable look-back time
- [x] dark/light mode - [x] dark/light mode
- [ ] semi-secure handling of private keys by zeroing memory and marking them Weak if displayed or exported (partial) - [x] secure handling of private keys by zeroing memory and marking them Weak if displayed or exported
- [ ] exporting/importing of private keys with a passphrase (partial, no export yet) - [x] exporting/importing of private keys with a passphrase
- [ ] multiple identities - [ ] multiple identities
- [ ] user management of relays (read/write), including ranking (partial, no ranking ui yet) - [ ] user management of relays (read/write), including ranking (partial, no ranking ui yet)
- [ ] choose to load from another relay with a button press - [ ] choose to load from another relay with a button press

View File

@ -1,10 +1,12 @@
use crate::error::Error; use crate::error::Error;
use crate::globals::GLOBALS;
use nostr_types::{EncryptedPrivateKey, Event, KeySecurity, PreEvent, PrivateKey, PublicKey}; use nostr_types::{EncryptedPrivateKey, Event, KeySecurity, PreEvent, PrivateKey, PublicKey};
use tokio::task;
pub enum Signer { pub enum Signer {
Fresh, Fresh,
Encrypted(EncryptedPrivateKey), Encrypted(EncryptedPrivateKey),
Ready(PrivateKey), Ready(PrivateKey, EncryptedPrivateKey),
} }
impl Default for Signer { impl Default for Signer {
@ -20,7 +22,7 @@ impl Signer {
pub fn unlock_encrypted_private_key(&mut self, pass: &str) -> Result<(), Error> { pub fn unlock_encrypted_private_key(&mut self, pass: &str) -> Result<(), Error> {
if let Signer::Encrypted(epk) = self { if let Signer::Encrypted(epk) = self {
*self = Signer::Ready(epk.decrypt(pass)?); *self = Signer::Ready(epk.decrypt(pass)?, epk.clone());
Ok(()) Ok(())
} else { } else {
Err(Error::NoPrivateKey) Err(Error::NoPrivateKey)
@ -28,32 +30,38 @@ impl Signer {
} }
pub fn generate_private_key(&mut self, pass: &str) -> Result<EncryptedPrivateKey, Error> { pub fn generate_private_key(&mut self, pass: &str) -> Result<EncryptedPrivateKey, Error> {
*self = Signer::Ready(PrivateKey::generate()); let pk = PrivateKey::generate();
if let Signer::Ready(pk) = self { let epk = pk.export_encrypted(pass)?;
Ok(pk.export_encrypted(pass)?) *self = Signer::Ready(pk, epk.clone());
} else { Ok(epk)
Err(Error::NoPrivateKey)
}
} }
pub fn is_loaded(&self) -> bool { pub fn is_loaded(&self) -> bool {
matches!(self, Signer::Encrypted(_)) || matches!(self, Signer::Ready(_)) matches!(self, Signer::Encrypted(_)) || matches!(self, Signer::Ready(_, _))
} }
pub fn is_ready(&self) -> bool { pub fn is_ready(&self) -> bool {
matches!(self, Signer::Ready(_)) matches!(self, Signer::Ready(_, _))
} }
pub fn public_key(&self) -> Option<PublicKey> { pub fn public_key(&self) -> Option<PublicKey> {
if let Signer::Ready(pk) = self { if let Signer::Ready(pk, _) = self {
Some(pk.public_key()) Some(pk.public_key())
} else { } else {
None None
} }
} }
pub fn encrypted_private_key(&self) -> Option<EncryptedPrivateKey> {
if let Signer::Ready(_, epk) = self {
Some(epk.clone())
} else {
None
}
}
pub fn key_security(&self) -> Option<KeySecurity> { pub fn key_security(&self) -> Option<KeySecurity> {
if let Signer::Ready(pk) = self { if let Signer::Ready(pk, _) = self {
Some(pk.key_security()) Some(pk.key_security())
} else { } else {
None None
@ -62,11 +70,63 @@ impl Signer {
pub fn sign_preevent(&self, preevent: PreEvent, pow: Option<u8>) -> Result<Event, Error> { pub fn sign_preevent(&self, preevent: PreEvent, pow: Option<u8>) -> Result<Event, Error> {
match self { match self {
Signer::Ready(pk) => match pow { Signer::Ready(pk, _) => match pow {
Some(pow) => Ok(Event::new_with_pow(preevent, pk, pow)?), Some(pow) => Ok(Event::new_with_pow(preevent, pk, pow)?),
None => Ok(Event::new(preevent, pk)?), None => Ok(Event::new(preevent, pk)?),
}, },
_ => Err(Error::NoPrivateKey), _ => Err(Error::NoPrivateKey),
} }
} }
pub fn export_private_key_bech32(&mut self, pass: &str) -> Result<String, Error> {
match self {
Signer::Ready(_, epk) => {
let mut pk = epk.decrypt(pass)?;
let output = pk.try_as_bech32_string()?;
// We have to regenerate encrypted private key because it may have fallen from
// medium to weak security.
let epk = pk.export_encrypted(pass)?;
// And then we have to save that
let mut settings = GLOBALS.settings.blocking_write();
settings.encrypted_private_key = Some(epk.clone());
task::spawn(async move {
if let Err(e) = settings.save().await {
tracing::error!("{}", e);
}
});
*self = Signer::Ready(pk, epk);
Ok(output)
}
_ => Err(Error::NoPrivateKey),
}
}
pub fn export_private_key_hex(&mut self, pass: &str) -> Result<String, Error> {
match self {
Signer::Ready(_, epk) => {
let mut pk = epk.decrypt(pass)?;
let output = pk.as_hex_string();
// We have to regenerate encrypted private key because it may have fallen from
// medium to weak security.
let epk = pk.export_encrypted(pass)?;
// And then we have to save that
let mut settings = GLOBALS.settings.blocking_write();
settings.encrypted_private_key = Some(epk.clone());
task::spawn(async move {
if let Err(e) = settings.save().await {
tracing::error!("{}", e);
}
});
*self = Signer::Ready(pk, epk);
Ok(output)
}
_ => Err(Error::NoPrivateKey),
}
}
} }

View File

@ -79,7 +79,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
}); });
ui.add_space(10.0); ui.add_space(10.0);
ui.label("After generating or importing your key, gossip will save it encrypted under a password. You will need this password to unlock it every time you start gossip. Gossip handles keys securely by never displaying them and zeroing memory used for private keys and passwords before freeing it."); ui.label("After generating or importing your key, gossip will save it encrypted under a password. You will need this password to unlock it every time you start gossip. Gossip handles keys securely by never displaying them and zeroing memory used for private keys and passwords before freeing it (unless you explicitly request it to be exported).");
ui.add_space(10.0); ui.add_space(10.0);
ui.add_space(10.0); ui.add_space(10.0);

View File

@ -1,6 +1,7 @@
use super::GossipUi; use super::GossipUi;
use crate::comms::BusMessage; use crate::comms::BusMessage;
use crate::globals::GLOBALS; use crate::globals::GLOBALS;
use crate::ui::widgets::CopyButton;
use eframe::egui; use eframe::egui;
use egui::{Context, TextEdit, Ui}; use egui::{Context, TextEdit, Ui};
use nostr_types::{KeySecurity, PublicKeyHex}; use nostr_types::{KeySecurity, PublicKeyHex};
@ -18,6 +19,8 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
if GLOBALS.signer.blocking_read().is_ready() { if GLOBALS.signer.blocking_read().is_ready() {
ui.heading("Ready to sign events"); ui.heading("Ready to sign events");
ui.add_space(10.0);
let key_security = GLOBALS.signer.blocking_read().key_security().unwrap(); let key_security = GLOBALS.signer.blocking_read().key_security().unwrap();
let public_key = GLOBALS.signer.blocking_read().public_key().unwrap(); let public_key = GLOBALS.signer.blocking_read().public_key().unwrap();
@ -30,7 +33,81 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
)); ));
let pkhex: PublicKeyHex = public_key.into(); let pkhex: PublicKeyHex = public_key.into();
ui.label(&format!("Public Key: {}", pkhex.0)); ui.horizontal(|ui| {
ui.label(&format!("Public Key (Hex): {}", pkhex.0));
if ui.add(CopyButton {}).clicked() {
ui.output().copied_text = pkhex.0;
}
});
if let Ok(bech32) = public_key.try_as_bech32_string() {
ui.horizontal(|ui| {
ui.label(&format!("Public Key (bech32): {}", bech32));
if ui.add(CopyButton {}).clicked() {
ui.output().copied_text = bech32;
}
});
}
ui.add_space(10.0);
if let Some(epk) = GLOBALS.signer.blocking_read().encrypted_private_key() {
ui.horizontal(|ui| {
ui.label(&format!("Encrypted Private Key: {}", epk));
if ui.add(CopyButton {}).clicked() {
ui.output().copied_text = epk.to_string();
}
});
}
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading("Raw Export");
if key_security == KeySecurity::Medium {
ui.label("WARNING: This will downgrade your key security to WEAK");
}
ui.horizontal(|ui| {
ui.add_space(10.0);
ui.label("Enter Password To Export: ");
ui.add(TextEdit::singleline(&mut app.password).password(true));
});
if ui.button("Export Private Key as bech32").clicked() {
match GLOBALS
.signer
.blocking_write()
.export_private_key_bech32(&app.password)
{
Ok(mut bech32) => {
println!("Exported private key (bech32): {}", bech32);
bech32.zeroize();
app.status =
"Exported key has been printed to the console standard output.".to_owned();
}
Err(e) => app.status = format!("{}", e),
}
app.password.zeroize();
app.password = "".to_owned();
}
if ui.button("Export Private Key as hex").clicked() {
match GLOBALS
.signer
.blocking_write()
.export_private_key_hex(&app.password)
{
Ok(mut hex) => {
println!("Exported private key (hex): {}", hex);
hex.zeroize();
app.status =
"Exported key has been printed to the console standard output.".to_owned();
}
Err(e) => app.status = format!("{}", e),
}
app.password.zeroize();
app.password = "".to_owned();
}
} else if GLOBALS.signer.blocking_read().is_loaded() { } else if GLOBALS.signer.blocking_read().is_loaded() {
ui.heading("Password Needed"); ui.heading("Password Needed");