From d5551c8c8c8b713d329ebbaa71b1d06d2ac57c2f Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Fri, 24 Aug 2018 13:48:53 -0400 Subject: [PATCH] Key calculator and access to the full algorithm. --- platform-independent/java/gui/build.gradle | 2 + .../gui/util/CollectionListModel.java | 28 +++--- .../masterpassword/gui/util/Components.java | 57 +++++------ .../gui/util/ConsumingTrigger.java | 27 +++++ .../gui/util/DocumentModel.java | 95 ++++++++++++++++++ .../lyndir/masterpassword/gui/util/Res.java | 4 + .../masterpassword/gui/util/Selectable.java | 2 +- .../gui/util/UnsignedIntegerModel.java | 5 +- .../masterpassword/gui/view/FilesPanel.java | 2 +- .../gui/view/UserContentPanel.java | 82 ++++++++++++++- .../gui/src/main/resources/media/icon_key.png | Bin 0 -> 1672 bytes .../src/main/resources/media/icon_key@2x.png | Bin 0 -> 2532 bytes 12 files changed, 253 insertions(+), 51 deletions(-) create mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/ConsumingTrigger.java create mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/DocumentModel.java create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_key.png create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_key@2x.png diff --git a/platform-independent/java/gui/build.gradle b/platform-independent/java/gui/build.gradle index 6e3569cc..27809ee2 100644 --- a/platform-independent/java/gui/build.gradle +++ b/platform-independent/java/gui/build.gradle @@ -24,6 +24,8 @@ shadowJar { attributes 'Implementation-Version': version } doLast { + println("doLast: ${System.env.KEY_PW_DESKTOP}"); + println("doLast: "+System.getenv( 'KEY_PW_DESKTOP' )); if (System.getenv( 'KEY_PW_DESKTOP' ) != null) ant.signjar( jar: archivePath, alias: 'masterpassword-desktop', diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/CollectionListModel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/CollectionListModel.java index 17c67416..f4a99bb0 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/CollectionListModel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/CollectionListModel.java @@ -1,6 +1,5 @@ package com.lyndir.masterpassword.gui.util; -import com.google.common.collect.ImmutableList; import java.util.*; import java.util.function.Consumer; import javax.annotation.Nullable; @@ -18,25 +17,21 @@ public class CollectionListModel extends AbstractListModel private final List model = new LinkedList<>(); @Nullable - private E selectedItem; private JList list; @Nullable + private E selectedItem; + @Nullable private Consumer selectionConsumer; @SafeVarargs - public static CollectionListModel copy(final E... elements) { - return copy( Arrays.asList( elements ) ); + public CollectionListModel(final E... elements) { + this( Arrays.asList( elements ) ); } - public static CollectionListModel copy(final Collection elements) { - CollectionListModel model = new CollectionListModel<>(); - synchronized (model) { - model.model.addAll( elements ); - model.selectedItem = model.getElementAt( 0 ); - model.fireIntervalAdded( model, 0, model.model.size() ); - - return model; - } + public CollectionListModel(final Collection elements) { + model.addAll( elements ); + selectedItem = getElementAt( 0 ); + fireIntervalAdded( this, 0, model.size() ); } @Override @@ -44,8 +39,8 @@ public class CollectionListModel extends AbstractListModel return model.size(); } - @Override @Nullable + @Override public synchronized E getElementAt(final int index) { return (index < model.size())? model.get( index ): null; } @@ -109,6 +104,11 @@ public class CollectionListModel extends AbstractListModel return selectedItem; } + public CollectionListModel select(final E selectedItem) { + setSelectedItem( selectedItem ); + return this; + } + public synchronized void registerList(final JList list) { // TODO: This class should probably implement ListSelectionModel instead. if (this.list != null) diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java index ba7847e5..662f768f 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java @@ -20,7 +20,6 @@ package com.lyndir.masterpassword.gui.util; import com.google.common.base.Strings; import com.lyndir.lhunath.opal.system.logging.Logger; -import com.lyndir.masterpassword.util.Utilities; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -34,8 +33,8 @@ import javax.annotation.Nullable; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; -import javax.swing.event.*; -import javax.swing.text.DefaultFormatterFactory; +import javax.swing.event.HyperlinkEvent; +import javax.swing.text.*; import org.jetbrains.annotations.NonNls; @@ -114,8 +113,13 @@ public abstract class Components { if (options == null) return (selectedValue instanceof Integer)? (Integer) selectedValue: JOptionPane.CLOSED_OPTION; - int option = Arrays.binarySearch( options, selectedValue ); - return (option < 0)? JOptionPane.CLOSED_OPTION: option; + try { + int option = Arrays.binarySearch( options, selectedValue ); + return (option < 0)? JOptionPane.CLOSED_OPTION: option; + } + catch (final ClassCastException ignored) { + return JOptionPane.CLOSED_OPTION; + } } @Nullable @@ -164,7 +168,11 @@ public abstract class Components { } public static JTextField textField() { - return new JTextField() { + return textField( null ); + } + + public static JTextField textField(@Nullable final Document document) { + return new JTextField( document, null, 0 ) { { setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ), BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) ); @@ -174,43 +182,30 @@ public abstract class Components { @Override public Dimension getMaximumSize() { - return new Dimension( Integer.MAX_VALUE, getPreferredSize().height ); + return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE ); } }; } public static JTextField textField(@Nullable final String text, @Nullable final Consumer change) { - return new JTextField( text ) { + return textField( new DocumentModel( new PlainDocument() ).selection( text, change ).getDocument() ); + } + + public static JTextArea textArea() { + return new JTextArea() { { setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ), BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) ); setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); setAlignmentX( LEFT_ALIGNMENT ); - if (change != null) { - getDocument().addDocumentListener( new DocumentListener() { - @Override - public void insertUpdate(final DocumentEvent e) { - change.accept( getText() ); - } - - @Override - public void removeUpdate(final DocumentEvent e) { - change.accept( getText() ); - } - - @Override - public void changedUpdate(final DocumentEvent e) { - change.accept( getText() ); - } - } ); - change.accept( getText() ); - } + setLineWrap( true ); + setRows( 3 ); } @Override public Dimension getMaximumSize() { - return new Dimension( Integer.MAX_VALUE, getPreferredSize().height ); + return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE ); } }; } @@ -443,17 +438,17 @@ public abstract class Components { public static JComboBox comboBox(final E[] values, final Function valueTransformer, @Nullable final E selectedItem, @Nullable final Consumer selectionConsumer) { - return comboBox( CollectionListModel.copy( values ).selection( selectedItem, selectionConsumer ), valueTransformer ); + return comboBox( new CollectionListModel<>( values ).selection( selectedItem, selectionConsumer ), valueTransformer ); } public static JComboBox comboBox(final Collection values, final Function valueTransformer, @Nullable final Consumer selectionConsumer) { - return comboBox( CollectionListModel.copy( values ).selection( selectionConsumer ), valueTransformer ); + return comboBox( new CollectionListModel<>( values ).selection( selectionConsumer ), valueTransformer ); } public static JComboBox comboBox(final Collection values, final Function valueTransformer, @Nullable final E selectedItem, @Nullable final Consumer selectionConsumer) { - return comboBox( CollectionListModel.copy( values ).selection( selectedItem, selectionConsumer ), valueTransformer ); + return comboBox( new CollectionListModel<>( values ).selection( selectedItem, selectionConsumer ), valueTransformer ); } public static JComboBox comboBox(final ComboBoxModel model, final Function valueTransformer) { diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/ConsumingTrigger.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/ConsumingTrigger.java new file mode 100644 index 00000000..11c1c03f --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/ConsumingTrigger.java @@ -0,0 +1,27 @@ +package com.lyndir.masterpassword.gui.util; + +import java.util.function.Consumer; +import javax.annotation.Nullable; + + +/** + * @author lhunath, 2018-08-23 + */ +public class ConsumingTrigger implements Consumer { + + private final Runnable trigger; + + @Nullable + private T value; + + public ConsumingTrigger(final Runnable trigger) { + this.trigger = trigger; + } + + @Override + public void accept(final T t) { + value = t; + + trigger.run(); + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/DocumentModel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/DocumentModel.java new file mode 100644 index 00000000..78e99809 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/DocumentModel.java @@ -0,0 +1,95 @@ +package com.lyndir.masterpassword.gui.util; + +import com.lyndir.lhunath.opal.system.logging.Logger; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; + + +/** + * @author lhunath, 2018-08-24 + */ +public class DocumentModel implements Selectable { + + private static final Logger logger = Logger.get( DocumentModel.class ); + + private final Document document; + + @Nullable + private DocumentListener documentListener; + + public DocumentModel(final Document document) { + this.document = document; + } + + @Nonnull + public Document getDocument() { + return document; + } + + @Nullable + public String getText() { + try { + return (document.getLength() > 0)? document.getText( 0, document.getLength() ): null; + } + catch (final BadLocationException e) { + logger.wrn( "While getting text for model", e ); + return null; + } + } + + public void setText(@Nullable final String text) { + try { + if (document.getLength() > 0) + document.remove( 0, document.getLength() ); + + if (text != null) + document.insertString( 0, text, null ); + } + catch (final BadLocationException e) { + logger.err( "While setting text for model", e ); + } + } + + @Override + public DocumentModel selection(@Nullable final Consumer selectionConsumer) { + if (documentListener != null) + document.removeDocumentListener( documentListener ); + + if (selectionConsumer != null) + document.addDocumentListener( documentListener = new DocumentListener() { + @Override + public void insertUpdate(final DocumentEvent e) { + trigger(); + } + + @Override + public void removeUpdate(final DocumentEvent e) { + trigger(); + } + + @Override + public void changedUpdate(final DocumentEvent e) { + trigger(); + } + + private void trigger() { + selectionConsumer.accept( getText() ); + } + } ); + + return this; + } + + @Override + public DocumentModel selection(@Nullable final String selectedItem, @Nullable final Consumer selectionConsumer) { + selection( selectionConsumer ); + setText( selectedItem ); + + return this; + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java index 2a0241b7..80685b58 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java @@ -164,6 +164,10 @@ public abstract class Res { return icon( "media/icon_edit.png" ); } + public Icon key() { + return icon( "media/icon_key.png" ); + } + public Icon avatar(final int index) { return icon( strf( "media/avatar-%d.png", index % avatars() ) ); } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Selectable.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Selectable.java index f7f1fc40..00700bd7 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Selectable.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Selectable.java @@ -11,5 +11,5 @@ public interface Selectable { T selection(@Nullable Consumer selectionConsumer); - T selection(E selectedItem, @Nullable Consumer selectionConsumer); + T selection(@Nullable E selectedItem, @Nullable Consumer selectionConsumer); } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/UnsignedIntegerModel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/UnsignedIntegerModel.java index 68e24dc4..761393e5 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/UnsignedIntegerModel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/UnsignedIntegerModel.java @@ -109,13 +109,14 @@ public class UnsignedIntegerModel extends SpinnerNumberModel implements Selectab } @Override - public UnsignedIntegerModel selection(final UnsignedInteger selectedItem, @Nullable final Consumer selectionConsumer) { + public UnsignedIntegerModel selection(@Nullable final UnsignedInteger selectedItem, + @Nullable final Consumer selectionConsumer) { if (changeListener != null) { removeChangeListener( changeListener ); changeListener = null; } - setValue( selectedItem ); + setValue( (selectedItem != null)? selectedItem: getMinimum() ); return selection( selectionConsumer ); } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java index 1825b905..c2ddda77 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java @@ -23,7 +23,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma "Click to change the user's avatar." ); private final CollectionListModel> usersModel = - CollectionListModel.>copy( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser ); + new CollectionListModel>( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser ); protected FilesPanel() { setOpaque( false ); diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java index 1bc494a0..3500d0e3 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java @@ -34,6 +34,7 @@ import javax.annotation.Nullable; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import javax.swing.text.PlainDocument; /** @@ -470,8 +471,10 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, "Show site settings." ); private final JButton questionsButton = Components.button( Res.icons().question(), event -> showSiteQuestions(), "Show site recovery questions." ); - private final JButton editButton = Components.button( Res.icons().edit(), event -> showEditSite(), + private final JButton editButton = Components.button( Res.icons().edit(), event -> showSiteValues(), "Set/save personal password/login." ); + private final JButton keyButton = Components.button( Res.icons().key(), event -> showSiteKeys(), + "Cryptographic site keys." ); private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(), "Delete the site from the user." ); @@ -502,9 +505,12 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, siteToolbar.add( settingsButton ); siteToolbar.add( questionsButton ); siteToolbar.add( editButton ); + siteToolbar.add( keyButton ); siteToolbar.add( deleteButton ); settingsButton.setEnabled( false ); questionsButton.setEnabled( false ); + editButton.setEnabled( false ); + keyButton.setEnabled( false ); deleteButton.setEnabled( false ); answerLabel = Components.label( "Answer:" ); @@ -665,7 +671,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, } ); } - public void showEditSite() { + public void showSiteValues() { MPSite site = sitesModel.getSelectedItem(); if (site == null) return; @@ -710,6 +716,76 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, } } + public void showSiteKeys() { + MPSite site = sitesModel.getSelectedItem(); + if (site == null) + return; + + JTextArea resultField = Components.textArea(); + resultField.setEnabled( false ); + + CollectionListModel purposeModel = new CollectionListModel<>( MPKeyPurpose.values() ); + DocumentModel contextModel = new DocumentModel( new PlainDocument() ); + UnsignedIntegerModel counterModel = new UnsignedIntegerModel( UnsignedInteger.ONE ); + CollectionListModel typeModel = new CollectionListModel<>( MPResultType.values() ); + DocumentModel stateModel = new DocumentModel( new PlainDocument() ); + + Runnable trigger = () -> Res.job( () -> { + try { + MPKeyPurpose purpose = purposeModel.getSelectedItem(); + MPResultType type = typeModel.getSelectedItem(); + + String result = ((purpose == null) || (type == null))? null: + site.getResult( purpose, contextModel.getText(), counterModel.getNumber(), type, stateModel.getText() ); + + Res.ui( () -> resultField.setText( result ) ); + } + catch (final MPKeyUnavailableException | MPAlgorithmException e) { + logger.err( e, "While computing site edit results." ); + } + } ); + + purposeModel.selection( MPKeyPurpose.Authentication, p -> trigger.run() ); + contextModel.selection( c -> trigger.run() ); + counterModel.selection( c -> trigger.run() ); + typeModel.selection( MPResultType.DeriveKey, t -> { + switch (t) { + case DeriveKey: + stateModel.setText( Integer.toString( site.getAlgorithm().mpw_keySize_min() ) ); + break; + + default: + stateModel.setText( null ); + } + + trigger.run(); + } ); + stateModel.selection( c -> trigger.run() ); + + if (JOptionPane.OK_OPTION == Components.showDialog( this, site.getSiteName(), new JOptionPane( Components.panel( + BoxLayout.PAGE_AXIS, + Components.heading( "Key Calculator" ), + Components.label( "Purpose:" ), + Components.comboBox( purposeModel, MPKeyPurpose::getShortName ), + Components.strut(), + Components.label( "Context:" ), + Components.textField( contextModel.getDocument() ), + Components.label( "Counter:" ), + Components.spinner( counterModel ), + Components.label( "Type:" ), + Components.comboBox( typeModel, this::getTypeDescription ), + Components.label( "State:" ), + Components.scrollPane( Components.textField( stateModel.getDocument() ) ), + Components.strut(), + resultField ) ) { + { + setOptions( new Object[]{ "Copy", "Cancel" } ); + setInitialValue( getOptions()[0] ); + } + } )) + copyResult( resultField.getText() ); + } + public void deleteSite() { MPSite site = sitesModel.getSelectedItem(); if (site == null) @@ -790,6 +866,8 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, passwordField.setText( (result != null)? result: " " ); settingsButton.setEnabled( result != null ); questionsButton.setEnabled( result != null ); + editButton.setEnabled( result != null ); + keyButton.setEnabled( result != null ); deleteButton.setEnabled( result != null ); } ) ) ); } diff --git a/platform-independent/java/gui/src/main/resources/media/icon_key.png b/platform-independent/java/gui/src/main/resources/media/icon_key.png new file mode 100644 index 0000000000000000000000000000000000000000..25ea04cf4d87f838ed224eee22ca37857779b63d GIT binary patch literal 1672 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFsEgPM3hAM`dB6B=jtV<?;Zqle1Gx6p~WYGxKbf-tXS8q>!0ns}yePYv5bpoSKp8QB{;0T;&&% zT$P<{nWAKGr(jcI1vDTxwIorYA~z?m*s8)-32d$vkPQ;nS5g2gDap1~as*kZ5aAo3 z;GAESs$i;Tpqp%9W}skZsAp(wVs37(qhMrUXrOOkq;F`XYiMp|Y-D9%pa2C*K--E^ z(yW49+@N*=dA3R!B_#z``ugSN<$C4Ddih1^`i7R4mLM~XjC6r2bc-wVN)jt{^NN*W zCb*;)Cl_TFlw{`TDS%8&Ov*1Uu~h=P6yk;40$*Ra!Fk2dfC2`Yennz|zM-Cher_(v zUtrb6B|)hOXJA!b98y`3svneEoL^d$oC;K~4ATq@JNy=b6armi7u7M@JCVPIg| z;pyTS5^?zLwCD_BN0Bz++gV2fIyhasu1~Mx4C0B}pkOC)L`)=J_yxb?z{VnHI+^Y20Z@!v#QX@Y`U<@ z@j{K6KD)kF?Ok+o)fdL1^zneG)eb$)hQSrB~UVhKpg#|56 z^^b0^nQnLg+P62K!^_Xe^Axt7(>`y%>zHzJqeAtwH#?WF>-6O6kAM40s>P-!yyH{YA{_-uqgf1><6 z<`dH&YaQD@Wsd%R!3RnSOg($<9{T;{^w;PBfky9T3}&A>r7NNj{$4VvKIf6VP+)^b d!cmSNtTSpKESu}7APK5KJYD@<);T3K0RZP3dsqMf literal 0 HcmV?d00001 diff --git a/platform-independent/java/gui/src/main/resources/media/icon_key@2x.png b/platform-independent/java/gui/src/main/resources/media/icon_key@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..af6c44cdaf99ad95a7068a8e609a71f94de3c813 GIT binary patch literal 2532 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 K(~Ir=6aEb<;}*IA literal 0 HcmV?d00001