From ab25bc0559543d53ae1ddec8b9de7880347cd05c Mon Sep 17 00:00:00 2001 From: mikera Date: Wed, 4 Dec 2024 12:06:41 +0000 Subject: [PATCH] GUI updates for better key security and management --- .../convex/gui/components/ConnectPanel.java | 14 ++- .../java/convex/gui/keys/KeyRingPanel.java | 102 ++++++++++++++++-- .../java/convex/gui/keys/WalletComponent.java | 79 ++++++++------ .../main/java/convex/gui/utils/Toolkit.java | 9 ++ .../java/convex/gui/wallet/WalletApp.java | 6 ++ 5 files changed, 171 insertions(+), 39 deletions(-) diff --git a/convex-gui/src/main/java/convex/gui/components/ConnectPanel.java b/convex-gui/src/main/java/convex/gui/components/ConnectPanel.java index 006984aef..220d4ba32 100644 --- a/convex-gui/src/main/java/convex/gui/components/ConnectPanel.java +++ b/convex-gui/src/main/java/convex/gui/components/ConnectPanel.java @@ -19,6 +19,7 @@ import convex.core.init.Init; import convex.gui.components.account.AddressCombo; import convex.gui.keys.KeyRingPanel; +import convex.gui.keys.UnlockWalletDialog; import convex.gui.utils.SymbolIcon; import convex.gui.utils.Toolkit; import convex.net.IPUtils; @@ -79,8 +80,17 @@ public static Convex tryConnect(JComponent parent,String prompt) { HostCombo.registerGoodConnection(target); AWalletEntry we=KeyRingPanel.findWalletEntry(convex); - if ((we!=null)&&!we.isLocked()) { - convex.setKeyPair(we.getKeyPair()); + if ((we!=null)) { + if (!we.isLocked()) { + convex.setKeyPair(we.getKeyPair()); + } else { + boolean unlock=UnlockWalletDialog.offerUnlock(parent,we); + if (unlock) { + convex.setKeyPair(we.getKeyPair()); + } else { + JOptionPane.showMessageDialog(parent, "Wallet not unlocked. In read-only mode."); + } + } } return convex; } catch (ConnectException e) { diff --git a/convex-gui/src/main/java/convex/gui/keys/KeyRingPanel.java b/convex-gui/src/main/java/convex/gui/keys/KeyRingPanel.java index 14543ea36..cf54b9fb9 100644 --- a/convex-gui/src/main/java/convex/gui/keys/KeyRingPanel.java +++ b/convex-gui/src/main/java/convex/gui/keys/KeyRingPanel.java @@ -1,6 +1,8 @@ package convex.gui.keys; import java.awt.Color; +import java.awt.Component; +import java.awt.event.ActionEvent; import java.io.File; import java.io.IOException; import java.security.GeneralSecurityException; @@ -12,8 +14,10 @@ import javax.swing.DefaultListModel; import javax.swing.JButton; +import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.filechooser.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -111,13 +115,54 @@ public KeyRingPanel() { btnImportSeed.setToolTipText("Import a key pair using an Ed25519 seed"); toolBar.add(btnImportSeed); + // Import seed button + JButton btnLoadKeys = new ActionButton("Load Keys....",0xe890,this::loadStore); + btnLoadKeys.setToolTipText("Load keys from a Keystore file"); + toolBar.add(btnLoadKeys); + + add(toolBar, "dock south"); } + + private void loadStore(ActionEvent e) { + try { + File f=chooseKeyStore(this); + if (f==null) return; + if (f.exists()) { + KeyStore ks=PFXTools.loadStore(f, null); + int num=loadKeys(ks,"Loaded from "+f.getCanonicalPath()); + Toolkit.showMessge(this,num+" new keys imported."); + } + } catch (IOException | GeneralSecurityException e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } + } + + /** + * Shows dialog to choose a key store + * @param parent + * @return + */ + private static File chooseKeyStore(Component parent) { + JFileChooser chooser = new JFileChooser(); + FileNameExtensionFilter filter = new FileNameExtensionFilter("PKCS #12 Keystore", "p12", "pfx"); + chooser.setFileFilter(filter); + File defaultDir=FileUtils.getFile(Constants.DEFAULT_KEYSTORE_FILENAME); + if (defaultDir.isDirectory()) { + chooser.setCurrentDirectory(defaultDir); + } + int returnVal = chooser.showOpenDialog(parent); + if(returnVal != JFileChooser.APPROVE_OPTION) return null; + File f=chooser.getSelectedFile(); + return f; + } + /** - * Load keys from a file. Returns number of keys loaded, or -1 if file could not be opened + * Load keys from a File. Returns number of keys loaded, or -1 if file could not be opened * @param f * @return */ @@ -125,7 +170,7 @@ private static int loadKeys(File f) { if (!f.exists()) return -1; try { KeyStore ks=PFXTools.loadStore(f, null); - return loadKeys(ks, f.getCanonicalPath()); + return loadKeys(ks, "Default KeyStore: "+f.getCanonicalPath()); } catch (IOException e) { log.debug("Can't load key store: "+e.getMessage()); return -1; @@ -136,15 +181,19 @@ private static int loadKeys(File f) { } private static int loadKeys(KeyStore keyStore, String source) throws KeyStoreException { - int n=0; + int numImports=0; Enumeration aliases = keyStore.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); AWalletEntry we=KeystoreWalletEntry.create(keyStore, alias, source); - listModel.addElement(we); - n++; + we.tryUnlock(null); // if empty password, unlock by default + AWalletEntry existing=getKeyRingEntry(we.getPublicKey()); + if (existing==null) { + listModel.addElement(we); + numImports++; + } } - return n; + return numImports; } public static DefaultListModel getListModel() { @@ -188,4 +237,45 @@ public static AWalletEntry getKeyRingEntry(AccountKey publicKey) { } return null; } + + /** + * Save a key to a store, prompting as necessary + * @param parent + * @param walletEntry Wallet entry to save + * @return True if saved successfully, false otherwise + */ + public static boolean saveKey(Component parent, AWalletEntry walletEntry) { + boolean locked=walletEntry.isLocked(); + try { + File f=chooseKeyStore(parent); + if (f==null) return false; + KeyStore ks; + if (f.exists()) { + ks=PFXTools.loadStore(f, null); + } else { + ks=PFXTools.createStore(f, null); + } + String s=JOptionPane.showInputDialog(parent,"Enter key encryption password"); + if (s==null) return false; + + if (locked) { + boolean unlock=walletEntry.tryUnlock(s.toCharArray()); + if (!unlock) { + Toolkit.showMessge(parent, "Could not unlock key with this password"); + return false; + } + } + PFXTools.setKeyPair(ks, walletEntry.getKeyPair(), s.toCharArray()); + + // Lock wallet entry again + + PFXTools.saveStore(ks, f, null); + return true; + } catch (Exception e) { + Toolkit.showErrorMessage(parent,"Failed to save to KeyStore",e); + return false; + } finally { + if (locked) walletEntry.lock(); + } + } } diff --git a/convex-gui/src/main/java/convex/gui/keys/WalletComponent.java b/convex-gui/src/main/java/convex/gui/keys/WalletComponent.java index 3709951d9..b41f0e083 100644 --- a/convex-gui/src/main/java/convex/gui/keys/WalletComponent.java +++ b/convex-gui/src/main/java/convex/gui/keys/WalletComponent.java @@ -1,6 +1,7 @@ package convex.gui.keys; import java.awt.Color; +import java.awt.event.ActionEvent; import javax.swing.Icon; import javax.swing.JButton; @@ -92,38 +93,34 @@ public WalletComponent(AWalletEntry initialWalletEntry) { }); // Menu Button - JPopupMenu menu=new JPopupMenu(); - //JMenuItem m1=new JMenuItem("Edit..."); - //menu.add(m1); - JMenuItem m2=new JMenuItem("Show seed..."); - m2.addActionListener(e-> { - AKeyPair kp=walletEntry.getKeyPair(); - if (kp!=null) { - JPanel panel=new JPanel(); - panel.setLayout(new MigLayout("wrap 1","[200]")); - panel.add(new Identicon(kp.getAccountKey(),Toolkit.IDENTICON_SIZE_LARGE),"align center"); - - panel.add(Toolkit.withTitledBorder("Ed25519 Private Seed",new CodeLabel(kp.getSeed().toString()))); - panel.add(Toolkit.makeNote("WARNING: keep this private, it can be used to control your account(s)"),"grow"); - panel.setBorder(Toolkit.createDialogBorder()); - JOptionPane.showMessageDialog(WalletComponent.this, panel,"Ed25519 Private Seed",JOptionPane.INFORMATION_MESSAGE); - } else { - JOptionPane.showMessageDialog(WalletComponent.this, "Keypair is locked, cannot access seed","Warning",JOptionPane.WARNING_MESSAGE); - } - }); - menu.add(m2); - JMenuItem m3=new JMenuItem("Remove..."); - m3.addActionListener(e-> { - int confirm =JOptionPane.showConfirmDialog(WalletComponent.this, "Are you sure you want to delete this keypair from your keyring?","Confirm Delete",JOptionPane.WARNING_MESSAGE); - if (confirm==JOptionPane.OK_OPTION) { - KeyRingPanel.getListModel().removeElement(walletEntry); - } - }); - menu.add(m3); + { + JPopupMenu menu=new JPopupMenu(); + //JMenuItem m1=new JMenuItem("Edit..."); + //menu.add(m1); + JMenuItem m2=new JMenuItem("Show seed..."); + m2.addActionListener(this::showSeed); + menu.add(m2); + + JMenuItem m3=new JMenuItem("Remove..."); + m3.addActionListener(e-> { + int confirm =JOptionPane.showConfirmDialog(WalletComponent.this, "Are you sure you want to delete this keypair from your keyring?","Confirm Delete",JOptionPane.WARNING_MESSAGE); + if (confirm==JOptionPane.OK_OPTION) { + KeyRingPanel.getListModel().removeElement(walletEntry); + } + }); + menu.add(m3); + + JMenuItem m4=new JMenuItem("Save to KeyStore..."); + m4.addActionListener(e-> { + KeyRingPanel.saveKey(this,walletEntry); + }); + menu.add(m4); - DropdownMenu menuButton=new DropdownMenu(menu); - menuButton.setToolTipText("Settings and special actions for this key"); - buttons.add(menuButton); + + DropdownMenu menuButton=new DropdownMenu(menu); + menuButton.setToolTipText("Settings and special actions for this key"); + buttons.add(menuButton); + } // panel of buttons on right add(buttons,"dock east"); // add to MigLayout @@ -132,6 +129,26 @@ public WalletComponent(AWalletEntry initialWalletEntry) { } + private void showSeed(ActionEvent e) { + if (walletEntry.isLocked()) { + if (!UnlockWalletDialog.offerUnlock(this,walletEntry)) return;; + } + + AKeyPair kp=walletEntry.getKeyPair(); + if (kp!=null) { + JPanel panel=new JPanel(); + panel.setLayout(new MigLayout("wrap 1","[200]")); + panel.add(new Identicon(kp.getAccountKey(),Toolkit.IDENTICON_SIZE_LARGE),"align center"); + + panel.add(Toolkit.withTitledBorder("Ed25519 Private Seed",new CodeLabel(kp.getSeed().toString()))); + panel.add(Toolkit.makeNote("WARNING: keep this private, it can be used to control your account(s)"),"grow"); + panel.setBorder(Toolkit.createDialogBorder()); + } else { + JOptionPane.showMessageDialog(WalletComponent.this, "Keypair is locked, cannot access seed","Warning",JOptionPane.WARNING_MESSAGE); + } + } + + private void doUpdate() { // TODO Auto-generated method stub resetTooltipText(lockButton); diff --git a/convex-gui/src/main/java/convex/gui/utils/Toolkit.java b/convex-gui/src/main/java/convex/gui/utils/Toolkit.java index 0a775c925..1be3dfea3 100644 --- a/convex-gui/src/main/java/convex/gui/utils/Toolkit.java +++ b/convex-gui/src/main/java/convex/gui/utils/Toolkit.java @@ -31,6 +31,7 @@ import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenuItem; +import javax.swing.JOptionPane; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JTextArea; @@ -385,4 +386,12 @@ public static void scrollToBottom(JScrollPane scrollPane) { JScrollBar bar = scrollPane.getVerticalScrollBar(); bar.setValue(bar.getMaximum()); } + + public static void showMessge(Component parent, Object message) { + JOptionPane.showMessageDialog(parent, message); + } + + public static void showErrorMessage(Component parent, String attemptFailure,Exception e) { + JOptionPane.showMessageDialog(parent, attemptFailure+ "\n"+e.getMessage()); + } } diff --git a/convex-gui/src/main/java/convex/gui/wallet/WalletApp.java b/convex-gui/src/main/java/convex/gui/wallet/WalletApp.java index 67e45fcda..7f7a410a8 100644 --- a/convex-gui/src/main/java/convex/gui/wallet/WalletApp.java +++ b/convex-gui/src/main/java/convex/gui/wallet/WalletApp.java @@ -14,6 +14,7 @@ import convex.gui.components.Toast; import convex.gui.dlfs.DLFSPanel; import convex.gui.keys.KeyRingPanel; +import convex.gui.keys.UnlockWalletDialog; import convex.gui.panels.HomePanel; import convex.gui.peer.stake.PeerStakePanel; import convex.gui.utils.SymbolIcon; @@ -68,7 +69,12 @@ public void afterRun() { if (convex.getKeyPair()==null) { AWalletEntry we=KeyRingPanel.findWalletEntry(convex); if (we!=null) { + if (we.isLocked()) { + UnlockWalletDialog.offerUnlock(homePanel, we); + } convex.setKeyPair(we.getKeyPair()); + } else { + Toolkit.showMessge(this, "The key for this account is not in your key ring.\n\nWallet opened in read-only mode: transactions will fail."); } } }