From 10c6d203b8ada112401f499b5d2852fd4a4c1042 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Tue, 7 Aug 2018 00:07:16 -0400 Subject: [PATCH] Implement security answers & immediate site lookup. --- lib/bin/build_lib | 2 +- .../gui/model/MPIncognitoQuestion.java | 12 +- .../gui/model/MPIncognitoSite.java | 6 + .../gui/model/MPIncognitoUser.java | 2 + .../gui/model/MPNewQuestion.java | 15 ++ .../masterpassword/gui/model/MPNewSite.java | 7 + .../masterpassword/gui/util/Components.java | 20 ++- .../masterpassword/gui/view/FilesPanel.java | 6 +- .../gui/view/MasterPasswordFrame.java | 9 +- .../gui/view/UserContentPanel.java | 165 +++++++++++++++--- .../masterpassword/model/MPQuestion.java | 7 + .../lyndir/masterpassword/model/MPSite.java | 21 ++- .../lyndir/masterpassword/model/MPUser.java | 1 + .../model/impl/MPBasicQuestion.java | 23 ++- .../model/impl/MPBasicSite.java | 40 +++-- .../model/impl/MPBasicUser.java | 8 +- .../model/impl/MPFileQuestion.java | 14 +- .../masterpassword/model/impl/MPFileSite.java | 6 + .../masterpassword/model/impl/MPFileUser.java | 1 + public/site | 2 +- 20 files changed, 276 insertions(+), 91 deletions(-) create mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPNewQuestion.java diff --git a/lib/bin/build_lib b/lib/bin/build_lib index 5a3e6ff3..015650da 100755 --- a/lib/bin/build_lib +++ b/lib/bin/build_lib @@ -36,7 +36,7 @@ _needs() { IFS=: read pkg tools <<< "$spec" IFS=, read -a tools <<< "${tools:-$pkg}" for tool in "${tools[@]}"; do - hash "$tool" && continue 2 + hash "$tool" 2>/dev/null && continue 2 done echo >&2 "Missing: $pkg. Please install this package." diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoQuestion.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoQuestion.java index 63b8e3d9..301c90ab 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoQuestion.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoQuestion.java @@ -31,17 +31,7 @@ import javax.annotation.Nullable; */ public class MPIncognitoQuestion extends MPBasicQuestion { - private final MPIncognitoSite site; - public MPIncognitoQuestion(final MPIncognitoSite site, final String keyword, @Nullable final MPResultType type) { - super( keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) ); - - this.site = site; - } - - @Nonnull - @Override - public MPIncognitoSite getSite() { - return site; + super( site, keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) ); } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java index 32d47662..1ad317de 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java @@ -40,4 +40,10 @@ public class MPIncognitoSite extends MPBasicSite { return null; } + @Nonnull @Override public MPIncognitoSite addSite(final String siteName) { return addSite( new MPIncognitoSite( this, siteName ) ); diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPNewQuestion.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPNewQuestion.java new file mode 100644 index 00000000..5022ff2d --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPNewQuestion.java @@ -0,0 +1,15 @@ +package com.lyndir.masterpassword.gui.model; + +import com.lyndir.masterpassword.model.MPSite; +import com.lyndir.masterpassword.model.impl.MPBasicQuestion; + + +/** + * @author lhunath, 2018-07-27 + */ +public class MPNewQuestion extends MPBasicQuestion { + + public MPNewQuestion(final MPSite site, final String keyword) { + super( site, keyword, site.getAlgorithm().mpw_default_answer_type() ); + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPNewSite.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPNewSite.java index 0f0396ed..07274538 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPNewSite.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPNewSite.java @@ -2,6 +2,7 @@ package com.lyndir.masterpassword.gui.model; import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.impl.*; +import javax.annotation.Nonnull; /** @@ -12,4 +13,10 @@ public class MPNewSite extends MPBasicSite, MPQuestion> { public MPNewSite(final MPUser user, final String siteName) { super( user, siteName ); } + + @Nonnull + @Override + public MPQuestion addQuestion(final String keyword) { + throw new UnsupportedOperationException( "Cannot add a question to a site that hasn't been created yet." ); + } } 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 314da5ce..bf249642 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 @@ -100,6 +100,7 @@ public abstract class Components { public static int showDialog(@Nullable final Component owner, @Nullable final String title, final JOptionPane pane) { JDialog dialog = pane.createDialog( owner, title ); + dialog.setMinimumSize( new Dimension( 520, 0 ) ); dialog.setModalityType( Dialog.ModalityType.DOCUMENT_MODAL ); showDialog( dialog ); @@ -143,7 +144,6 @@ public abstract class Components { title, Dialog.ModalityType.DOCUMENT_MODAL ); dialog.setMinimumSize( new Dimension( 320, 0 ) ); dialog.setLocationRelativeTo( owner ); - dialog.setLocationByPlatform( true ); dialog.setContentPane( content ); return showDialog( dialog ); @@ -155,6 +155,7 @@ public abstract class Components { dialog.getRootPane().putClientProperty( "Window.style", "small" ); dialog.pack(); + dialog.setLocationByPlatform( true ); dialog.setVisible( true ); return dialog; @@ -176,7 +177,7 @@ public abstract class Components { }; } - public static JTextField textField(@Nullable final String text, @Nullable final Consumer selection) { + public static JTextField textField(@Nullable final String text, @Nullable final Consumer change) { return new JTextField( text ) { { setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ), @@ -184,23 +185,25 @@ public abstract class Components { setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); setAlignmentX( LEFT_ALIGNMENT ); - if (selection != null) + if (change != null) { getDocument().addDocumentListener( new DocumentListener() { @Override public void insertUpdate(final DocumentEvent e) { - selection.accept( getText() ); + change.accept( getText() ); } @Override public void removeUpdate(final DocumentEvent e) { - selection.accept( getText() ); + change.accept( getText() ); } @Override public void changedUpdate(final DocumentEvent e) { - selection.accept( getText() ); + change.accept( getText() ); } } ); + change.accept( getText() ); + } } @Override @@ -228,6 +231,7 @@ public abstract class Components { public static JList list(final ListModel model, final Function valueTransformer) { return new JList( model ) { { + setAlignmentX( LEFT_ALIGNMENT ); setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); setCellRenderer( new DefaultListCellRenderer() { @Override @@ -241,7 +245,9 @@ public abstract class Components { return this; } } ); - setAlignmentX( LEFT_ALIGNMENT ); + + if (model instanceof CollectionListModel) + ((CollectionListModel) model).registerList( this ); } @Override 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 c42a102c..1825b905 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 @@ -9,8 +9,6 @@ import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.impl.MPFileUser; import com.lyndir.masterpassword.model.impl.MPFileUserManager; import java.awt.*; -import java.util.Collection; -import java.util.concurrent.CopyOnWriteArraySet; import javax.annotation.Nullable; import javax.swing.*; @@ -26,8 +24,6 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma private final CollectionListModel> usersModel = CollectionListModel.>copy( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser ); - private final JComboBox> userField = - Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) ); protected FilesPanel() { setOpaque( false ); @@ -46,7 +42,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma add( Components.strut( Components.margin() ) ); // User Selection - add( userField ); + add( Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) ) ); MPFileUserManager.get().addListener( this ); MasterPassword.get().addListener( this ); diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java index baf3a66e..a48e06e4 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java @@ -3,12 +3,10 @@ package com.lyndir.masterpassword.gui.view; import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Res; -import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.impl.MPFileUserManager; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; -import javax.annotation.Nullable; import javax.swing.*; import javax.swing.border.BevelBorder; @@ -23,8 +21,6 @@ public class MasterPasswordFrame extends JFrame { @SuppressWarnings("FieldCanBeLocal") private final Components.GradientPanel root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS ); - private final FilesPanel filesPanel = new FilesPanel(); - private final JPanel userPanel = Components.panel( new BorderLayout( 0, 0 ) ); private final UserContentPanel userContent = new UserContentPanel(); @SuppressWarnings("MagicNumber") @@ -32,15 +28,16 @@ public class MasterPasswordFrame extends JFrame { super( "Master Password" ); setContentPane( root ); - root.add( filesPanel ); + root.add( new FilesPanel() ); root.add( Components.strut() ); - root.add( userPanel ); + JPanel userPanel = Components.panel( new BorderLayout( 0, 0 ) ); userPanel.add( userContent.getUserToolbar(), BorderLayout.LINE_START ); userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END ); userPanel.add( Components.borderPanel( BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ), Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent ), BorderLayout.CENTER ); + root.add( userPanel ); addComponentListener( new ComponentHandler() ); setPreferredSize( new Dimension( 800, 560 ) ); 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 d3e68cc0..65bf7b4a 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 @@ -10,8 +10,7 @@ import com.lyndir.lhunath.opal.system.util.ObjectUtils; import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.gui.MPGuiConstants; import com.lyndir.masterpassword.gui.MasterPassword; -import com.lyndir.masterpassword.gui.model.MPIncognitoUser; -import com.lyndir.masterpassword.gui.model.MPNewSite; +import com.lyndir.masterpassword.gui.model.*; import com.lyndir.masterpassword.gui.util.*; import com.lyndir.masterpassword.gui.util.Platform; import com.lyndir.masterpassword.model.*; @@ -290,9 +289,9 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, private final JButton resetButton = Components.button( Res.icons().reset(), event -> resetUser(), "Change the master password for this user." ); - private final JPasswordField masterPasswordField = Components.passwordField(); - private final JLabel errorLabel = Components.label(); - private final JLabel identiconLabel = Components.label( SwingConstants.CENTER ); + private final JPasswordField masterPasswordField; + private final JLabel errorLabel; + private final JLabel identiconLabel; private Future identiconJob; @@ -312,16 +311,16 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); add( Components.strut() ); - add( identiconLabel ); + add( identiconLabel = Components.label( SwingConstants.CENTER ) ); identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) ); add( Box.createGlue() ); add( Components.label( "Master Password:" ) ); add( Components.strut() ); - add( masterPasswordField ); + add( masterPasswordField = Components.passwordField() ); masterPasswordField.addActionListener( this ); masterPasswordField.getDocument().addDocumentListener( this ); - add( errorLabel ); + add( errorLabel = Components.label() ); errorLabel.setForeground( Res.colors().errorFg() ); add( Box.createGlue() ); } @@ -468,21 +467,20 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, "Sign out and lock user." ); private final JButton settingsButton = Components.button( Res.icons().settings(), event -> showSiteSettings(), "Show site settings." ); - private final JButton questionsButton = Components.button( Res.icons().question(), null, + private final JButton questionsButton = Components.button( Res.icons().question(), event -> showSiteQuestions(), "Show site recovery questions." ); private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(), "Delete the site from the user." ); @Nonnull private final MPUser user; - private final JLabel passwordLabel = Components.label( SwingConstants.CENTER ); - private final JLabel passwordField = Components.heading( SwingConstants.CENTER ); - private final JLabel queryLabel = Components.label(); - private final JTextField queryField = Components.textField( null, this::updateSites ); - private final CollectionListModel> sitesModel = - new CollectionListModel>().selection( this::showSiteResult ); - private final JList> sitesList = - Components.list( sitesModel, this::getSiteDescription ); + private final JLabel passwordLabel; + private final JLabel passwordField; + private final JLabel answerField; + private final JLabel queryLabel; + private final JTextField queryField; + private final CollectionListModel> sitesModel; + private final JList> sitesList; private Future updateSitesJob; @@ -501,27 +499,32 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, siteToolbar.add( questionsButton ); siteToolbar.add( deleteButton ); settingsButton.setEnabled( false ); + questionsButton.setEnabled( false ); deleteButton.setEnabled( false ); add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); - add( passwordLabel ); - add( passwordField ); + add( passwordLabel = Components.label( SwingConstants.CENTER ) ); + add( passwordField = Components.heading( SwingConstants.CENTER ) ); passwordField.setForeground( Res.colors().highlightFg() ); passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) ); + answerField = Components.heading( SwingConstants.CENTER ); + answerField.setForeground( Res.colors().highlightFg() ); + answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) ); add( Box.createGlue() ); add( Components.strut() ); - add( queryLabel ); + add( queryLabel = Components.label() ); queryLabel.setText( strf( "%s's password for:", user.getFullName() ) ); - add( queryField ); + add( queryField = Components.textField( null, this::updateSites ) ); queryField.putClientProperty( "JTextField.variant", "search" ); queryField.addActionListener( event -> useSite() ); queryField.addKeyListener( this ); queryField.requestFocusInWindow(); add( Components.strut() ); - add( Components.scrollPane( sitesList ) ); - sitesModel.registerList( sitesList ); + add( Components.scrollPane( sitesList = Components.list( + sitesModel = new CollectionListModel>().selection( this::showSiteResult ), + this::getSiteDescription ) ) ); add( Box.createGlue() ); addHierarchyListener( e -> { @@ -587,10 +590,56 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, Components.textField( fileSite.getUrl(), fileSite::setUrl ), Components.strut() ); - Components.showDialog( this, site.getSiteName(), new JOptionPane( Components.panel( + Components.showDialog( this, strf( "Settings for %s", site.getSiteName() ), new JOptionPane( Components.panel( BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) ); } + public void showSiteQuestions() { + MPSite site = sitesModel.getSelectedItem(); + if (site == null) + return; + + CollectionListModel questionsModel = new CollectionListModel().selection( this::showQuestionResult ); + JList questionsList = Components.list( questionsModel, MPQuestion::getKeyword ); + JTextField queryField = Components.textField( null, query -> Res.job( () -> { + Collection questions = new LinkedList<>( site.findQuestions( query ) ); + + if (!Strings.isNullOrEmpty( query )) + if (questions.stream().noneMatch( question -> question.getKeyword().equalsIgnoreCase( query ) )) + questions.add( new MPNewQuestion( site, query ) ); + + Res.ui( () -> questionsModel.set( questions ) ); + } ) ); + queryField.putClientProperty( "JTextField.variant", "search" ); + queryField.addActionListener( event -> useQuestion( questionsModel.getSelectedItem() ) ); + queryField.addKeyListener( new KeyAdapter() { + @Override + public void keyPressed(final KeyEvent event) { + if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN)) + questionsList.dispatchEvent( event ); + } + + @Override + public void keyReleased(final KeyEvent event) { + if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN)) + questionsList.dispatchEvent( event ); + } + } ); + + Components.showDialog( this, strf( "Recovery answers for %s", site.getSiteName() ), new JOptionPane( Components.panel( + BoxLayout.PAGE_AXIS, + Components.label( "Security Question Keyword:" ), queryField, + Components.strut(), + Components.label( "Answer:" ), answerField, + Components.strut(), + Components.scrollPane( questionsList ) ) ) { + @Override + public void selectInitialValue() { + queryField.requestFocusInWindow(); + } + } ); + } + public void deleteSite() { MPSite site = sitesModel.getSelectedItem(); if (site == null) @@ -668,6 +717,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, passwordLabel.setText( " " ); passwordField.setText( " " ); settingsButton.setEnabled( false ); + questionsButton.setEnabled( false ); deleteButton.setEnabled( false ); } ); return; @@ -683,6 +733,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, passwordLabel.setText( strf( "Your password for %s:", site.getSiteName() ) ); passwordField.setText( result ); settingsButton.setEnabled( true ); + questionsButton.setEnabled( true ); deleteButton.setEnabled( true ); } ); } @@ -692,6 +743,66 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, } ); } + private void useQuestion(@Nullable final MPQuestion question) { + if (question instanceof MPNewQuestion) { + if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog( + this, + strf( "Remember the answer for the security question with keyword %s?", + question.getKeyword() ), + "New Question", JOptionPane.YES_NO_OPTION )) { + useQuestion( question.getSite().addQuestion( question.getKeyword() ) ); + } + return; + } + + showQuestionResult( question, result -> { + if (result == null) + return; + + if (question instanceof MPFileQuestion) + ((MPFileQuestion) question).use(); + + Transferable clipboardContents = new StringSelection( result ); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null ); + + Res.ui( () -> { + Window answerDialog = SwingUtilities.windowForComponent( answerField ); + if (answerDialog instanceof Dialog) + answerDialog.setVisible( false ); + + Window window = SwingUtilities.windowForComponent( UserContentPanel.this ); + if (window instanceof Frame) + ((Frame) window).setExtendedState( Frame.ICONIFIED ); + } ); + } ); + } + + private void showQuestionResult(@Nullable final MPQuestion question) { + showQuestionResult( question, null ); + } + + private void showQuestionResult(@Nullable final MPQuestion question, @Nullable final Consumer resultCallback) { + if (question == null) { + if (resultCallback != null) + resultCallback.accept( null ); + Res.ui( () -> answerField.setText( " " ) ); + return; + } + + Res.job( () -> { + try { + String answer = question.getAnswer(); + if (resultCallback != null) + resultCallback.accept( answer ); + + Res.ui( () -> answerField.setText( answer ) ); + } + catch (final MPKeyUnavailableException | MPAlgorithmException e) { + logger.err( e, "While resolving answer for: %s", question ); + } + } ); + } + @Override public void keyTyped(final KeyEvent event) { } @@ -713,13 +824,11 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, updateSitesJob.cancel( true ); updateSitesJob = Res.job( () -> { - Collection> sites = new LinkedList<>(); - if (!Strings.isNullOrEmpty( query )) { - sites.addAll( new LinkedList<>( user.findSites( query ) ) ); + Collection> sites = new LinkedList<>( user.findSites( query ) ); + if (!Strings.isNullOrEmpty( query )) if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) )) sites.add( new MPNewSite( user, query ) ); - } Res.ui( () -> sitesModel.set( sites ) ); } ); diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPQuestion.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPQuestion.java index 478a8f7d..29687f54 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPQuestion.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPQuestion.java @@ -40,6 +40,13 @@ public interface MPQuestion extends Comparable { void setType(MPResultType type); + @Nonnull + default String getAnswer() + throws MPKeyUnavailableException, MPAlgorithmException { + + return getAnswer( null ); + } + @Nonnull String getAnswer(@Nullable String state) throws MPKeyUnavailableException, MPAlgorithmException; diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java index 110adbac..645e160c 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java @@ -18,6 +18,7 @@ package com.lyndir.masterpassword.model; +import com.google.common.collect.ImmutableCollection; import com.google.common.primitives.UnsignedInteger; import com.lyndir.masterpassword.*; import java.util.Collection; @@ -57,6 +58,7 @@ public interface MPSite extends Comparable> { void setLoginType(@Nullable MPResultType loginType); + @Nonnull default String getResult() throws MPKeyUnavailableException, MPAlgorithmException { @@ -79,6 +81,16 @@ public interface MPSite extends Comparable> { String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state) throws MPKeyUnavailableException, MPAlgorithmException; + @Nonnull + String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, + @Nullable UnsignedInteger counter, MPResultType type, @Nullable String state) + throws MPKeyUnavailableException, MPAlgorithmException; + + @Nonnull + String getState(MPKeyPurpose keyPurpose, @Nullable String keyContext, + @Nullable UnsignedInteger counter, MPResultType type, String state) + throws MPKeyUnavailableException, MPAlgorithmException; + @Nonnull default String getLogin() throws MPKeyUnavailableException, MPAlgorithmException { @@ -94,10 +106,17 @@ public interface MPSite extends Comparable> { @Nonnull MPUser getUser(); - boolean addQuestion(Q question); + @Nonnull + Q addQuestion(String keyword); + + @Nonnull + Q addQuestion(Q question); boolean deleteQuestion(Q question); @Nonnull Collection getQuestions(); + + @Nonnull + ImmutableCollection findQuestions(@Nullable String query); } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java index 834dae1e..ae3b6fd5 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java @@ -100,6 +100,7 @@ public interface MPUser> extends Comparable> { // - Relations + @Nonnull S addSite(String siteName); @Nonnull diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java index d22f576d..b2c9e502 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java @@ -22,6 +22,7 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.model.MPQuestion; +import com.lyndir.masterpassword.model.MPSite; import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -33,14 +34,23 @@ import org.jetbrains.annotations.NotNull; */ public abstract class MPBasicQuestion extends Changeable implements MPQuestion { + private final MPSite site; private final String keyword; - private MPResultType type; - protected MPBasicQuestion(final String keyword, final MPResultType type) { + private MPResultType type; + + protected MPBasicQuestion(final MPSite site, final String keyword, final MPResultType type) { + this.site = site; this.keyword = keyword; this.type = type; } + @Nonnull + @Override + public MPSite getSite() { + return site; + } + @Nonnull @Override public String getKeyword() { @@ -55,7 +65,7 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion { @Override public void setType(final MPResultType type) { - if (Objects.equals(this.type, type)) + if (this.type == type) return; this.type = type; @@ -70,15 +80,12 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion { return getSite().getResult( MPKeyPurpose.Recovery, getKeyword(), null, getType(), state ); } - @Nonnull - @Override - public abstract MPBasicSite getSite(); - @Override protected void onChanged() { super.onChanged(); - getSite().setChanged(); + if (site instanceof Changeable) + ((Changeable) site).setChanged(); } @Override diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java index 8a7d9b68..e7dce0c3 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java @@ -21,6 +21,9 @@ package com.lyndir.masterpassword.model.impl; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; import static com.lyndir.lhunath.opal.system.util.StringUtils.*; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSortedSet; import com.google.common.primitives.UnsignedInteger; import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.model.*; @@ -35,9 +38,9 @@ import javax.annotation.Nullable; public abstract class MPBasicSite, Q extends MPQuestion> extends Changeable implements MPSite { - private final Collection questions = new LinkedHashSet<>(); private final U user; private final String siteName; + private final Collection questions = new LinkedHashSet<>(); private MPAlgorithm algorithm; private UnsignedInteger counter; @@ -104,7 +107,7 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext @Override public void setResultType(final MPResultType resultType) { - if (Objects.equals( this.resultType, resultType )) + if (this.resultType == resultType) return; this.resultType = resultType; @@ -119,7 +122,7 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext @Override public void setLoginType(@Nullable final MPResultType loginType) { - if (Objects.equals( this.loginType, loginType )) + if (this.loginType == loginType) return; this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() ); @@ -134,8 +137,10 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext return getResult( keyPurpose, keyContext, getCounter(), getResultType(), state ); } - protected String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, - @Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state) + @Nonnull + @Override + public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, + @Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state) throws MPKeyUnavailableException, MPAlgorithmException { return getUser().getMasterKey().siteResult( @@ -143,8 +148,10 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext keyPurpose, keyContext, type, state ); } - protected String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, - @Nullable final UnsignedInteger counter, final MPResultType type, final String state) + @Nonnull + @Override + public String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, + @Nullable final UnsignedInteger counter, final MPResultType type, final String state) throws MPKeyUnavailableException, MPAlgorithmException { return getUser().getMasterKey().siteState( @@ -160,13 +167,13 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext return getResult( MPKeyPurpose.Identification, null, null, getLoginType(), state ); } + @Nonnull @Override - public boolean addQuestion(final Q question) { - if (!questions.add( question )) - return false; + public Q addQuestion(final Q question) { + questions.add( question ); setChanged(); - return true; + return question; } @Override @@ -184,6 +191,17 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext return Collections.unmodifiableCollection( questions ); } + @Nonnull + @Override + public ImmutableCollection findQuestions(@Nullable final String query) { + ImmutableSortedSet.Builder results = ImmutableSortedSet.naturalOrder(); + for (final Q question : getQuestions()) + if (Strings.isNullOrEmpty( query ) || question.getKeyword().startsWith( query )) + results.add( question ); + + return results.build(); + } + @Nonnull @Override public U getUser() { diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java index 6821eeaf..9f40c02c 100755 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java @@ -20,6 +20,7 @@ package com.lyndir.masterpassword.model.impl; import static com.lyndir.lhunath.opal.system.util.StringUtils.*; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableSortedSet; import com.lyndir.lhunath.opal.system.CodeUtils; @@ -202,10 +203,9 @@ public abstract class MPBasicUser> extends Changeabl @Override public ImmutableCollection findSites(@Nullable final String query) { ImmutableSortedSet.Builder results = ImmutableSortedSet.naturalOrder(); - if (query != null) - for (final S site : getSites()) - if (site.getSiteName().startsWith( query )) - results.add( site ); + for (final S site : getSites()) + if (Strings.isNullOrEmpty( query ) || site.getSiteName().startsWith( query )) + results.add( site ); return results.build(); } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileQuestion.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileQuestion.java index 60f49bff..05817c30 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileQuestion.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileQuestion.java @@ -30,16 +30,13 @@ import javax.annotation.Nullable; */ public class MPFileQuestion extends MPBasicQuestion { - private final MPFileSite site; - @Nullable private String answerState; public MPFileQuestion(final MPFileSite site, final String keyword, @Nullable final MPResultType type, @Nullable final String answerState) { - super( keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) ); + super( site, keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) ); - this.site = site; this.answerState = answerState; } @@ -48,6 +45,8 @@ public class MPFileQuestion extends MPBasicQuestion { return answerState; } + @Nonnull + @Override public String getAnswer() throws MPKeyUnavailableException, MPAlgorithmException { return getAnswer( answerState ); @@ -66,9 +65,8 @@ public class MPFileQuestion extends MPBasicQuestion { setChanged(); } - @Nonnull - @Override - public MPFileSite getSite() { - return site; + public void use() { + if (getSite() instanceof MPFileSite) + ((MPFileSite) getSite()).use(); } } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java index 8437e96a..1de501f0 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java @@ -145,6 +145,12 @@ public class MPFileSite extends MPBasicSite { setChanged(); } + @Nonnull + @Override + public MPFileQuestion addQuestion(final String keyword) { + return addQuestion( new MPFileQuestion( this, keyword, null, null ) ); + } + @Override public int compareTo(@Nonnull final MPSite o) { int comparison = (o instanceof MPFileSite)? ((MPFileSite) o).getLastUsed().compareTo( getLastUsed() ): 0; diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java index 6e979561..2d825b49 100755 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java @@ -207,6 +207,7 @@ public class MPFileUser extends MPBasicUser { super.reset(); } + @Nonnull @Override public MPFileSite addSite(final String siteName) { return addSite( new MPFileSite( this, siteName ) ); diff --git a/public/site b/public/site index b190b457..0623231a 160000 --- a/public/site +++ b/public/site @@ -1 +1 @@ -Subproject commit b190b45756666da71fa8037501530b14c609be24 +Subproject commit 0623231ad5614d5e6541199fd755589f04e214cb