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] 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] 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] Post root-level 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
- [ ] 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)
- [ ] Setting your metadata and syncing it 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] dark/light mode
- [ ] semi-secure handling of private keys by zeroing memory and marking them Weak if displayed or exported (partial)
- [ ] exporting/importing of private keys with a passphrase (partial, no export yet)
- [x] secure handling of private keys by zeroing memory and marking them Weak if displayed or exported
- [x] exporting/importing of private keys with a passphrase
- [ ] multiple identities
- [ ] user management of relays (read/write), including ranking (partial, no ranking ui yet)
- [ ] choose to load from another relay with a button press

View File

@ -1,10 +1,12 @@
use crate::error::Error;
use crate::globals::GLOBALS;
use nostr_types::{EncryptedPrivateKey, Event, KeySecurity, PreEvent, PrivateKey, PublicKey};
use tokio::task;
pub enum Signer {
Fresh,
Encrypted(EncryptedPrivateKey),
Ready(PrivateKey),
Ready(PrivateKey, EncryptedPrivateKey),
}
impl Default for Signer {
@ -20,7 +22,7 @@ impl Signer {
pub fn unlock_encrypted_private_key(&mut self, pass: &str) -> Result<(), Error> {
if let Signer::Encrypted(epk) = self {
*self = Signer::Ready(epk.decrypt(pass)?);
*self = Signer::Ready(epk.decrypt(pass)?, epk.clone());
Ok(())
} else {
Err(Error::NoPrivateKey)
@ -28,32 +30,38 @@ impl Signer {
}
pub fn generate_private_key(&mut self, pass: &str) -> Result<EncryptedPrivateKey, Error> {
*self = Signer::Ready(PrivateKey::generate());
if let Signer::Ready(pk) = self {
Ok(pk.export_encrypted(pass)?)
} else {
Err(Error::NoPrivateKey)
}
let pk = PrivateKey::generate();
let epk = pk.export_encrypted(pass)?;
*self = Signer::Ready(pk, epk.clone());
Ok(epk)
}
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 {
matches!(self, Signer::Ready(_))
matches!(self, Signer::Ready(_, _))
}
pub fn public_key(&self) -> Option<PublicKey> {
if let Signer::Ready(pk) = self {
if let Signer::Ready(pk, _) = self {
Some(pk.public_key())
} else {
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> {
if let Signer::Ready(pk) = self {
if let Signer::Ready(pk, _) = self {
Some(pk.key_security())
} else {
None
@ -62,11 +70,63 @@ impl Signer {
pub fn sign_preevent(&self, preevent: PreEvent, pow: Option<u8>) -> Result<Event, Error> {
match self {
Signer::Ready(pk) => match pow {
Signer::Ready(pk, _) => match pow {
Some(pow) => Ok(Event::new_with_pow(preevent, pk, pow)?),
None => Ok(Event::new(preevent, pk)?),
},
_ => 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.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);

View File

@ -1,6 +1,7 @@
use super::GossipUi;
use crate::comms::BusMessage;
use crate::globals::GLOBALS;
use crate::ui::widgets::CopyButton;
use eframe::egui;
use egui::{Context, TextEdit, Ui};
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() {
ui.heading("Ready to sign events");
ui.add_space(10.0);
let key_security = GLOBALS.signer.blocking_read().key_security().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();
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() {
ui.heading("Password Needed");