2
0

Implement security answers & immediate site lookup.

This commit is contained in:
Maarten Billemont 2018-08-07 00:07:16 -04:00
parent 7d1aa9c9f4
commit 10c6d203b8
20 changed files with 276 additions and 91 deletions

View File

@ -36,7 +36,7 @@ _needs() {
IFS=: read pkg tools <<< "$spec" IFS=: read pkg tools <<< "$spec"
IFS=, read -a tools <<< "${tools:-$pkg}" IFS=, read -a tools <<< "${tools:-$pkg}"
for tool in "${tools[@]}"; do for tool in "${tools[@]}"; do
hash "$tool" && continue 2 hash "$tool" 2>/dev/null && continue 2
done done
echo >&2 "Missing: $pkg. Please install this package." echo >&2 "Missing: $pkg. Please install this package."

View File

@ -31,17 +31,7 @@ import javax.annotation.Nullable;
*/ */
public class MPIncognitoQuestion extends MPBasicQuestion { public class MPIncognitoQuestion extends MPBasicQuestion {
private final MPIncognitoSite site;
public MPIncognitoQuestion(final MPIncognitoSite site, final String keyword, @Nullable final MPResultType type) { public MPIncognitoQuestion(final MPIncognitoSite site, final String keyword, @Nullable final MPResultType type) {
super( keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) ); super( site, keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
this.site = site;
}
@Nonnull
@Override
public MPIncognitoSite getSite() {
return site;
} }
} }

View File

@ -40,4 +40,10 @@ public class MPIncognitoSite extends MPBasicSite<MPIncognitoUser, MPIncognitoQue
@Nullable final MPResultType resultType, @Nullable final MPResultType loginType) { @Nullable final MPResultType resultType, @Nullable final MPResultType loginType) {
super( user, siteName, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType ); super( user, siteName, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType );
} }
@Nonnull
@Override
public MPIncognitoQuestion addQuestion(final String keyword) {
return addQuestion( new MPIncognitoQuestion( this, keyword, null ) );
}
} }

View File

@ -20,6 +20,7 @@ package com.lyndir.masterpassword.gui.model;
import com.lyndir.masterpassword.MPAlgorithm; import com.lyndir.masterpassword.MPAlgorithm;
import com.lyndir.masterpassword.model.impl.MPBasicUser; import com.lyndir.masterpassword.model.impl.MPBasicUser;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -38,6 +39,7 @@ public class MPIncognitoUser extends MPBasicUser<MPIncognitoSite> {
return null; return null;
} }
@Nonnull
@Override @Override
public MPIncognitoSite addSite(final String siteName) { public MPIncognitoSite addSite(final String siteName) {
return addSite( new MPIncognitoSite( this, siteName ) ); return addSite( new MPIncognitoSite( this, siteName ) );

View File

@ -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() );
}
}

View File

@ -2,6 +2,7 @@ package com.lyndir.masterpassword.gui.model;
import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.*;
import com.lyndir.masterpassword.model.impl.*; import com.lyndir.masterpassword.model.impl.*;
import javax.annotation.Nonnull;
/** /**
@ -12,4 +13,10 @@ public class MPNewSite extends MPBasicSite<MPUser<?>, MPQuestion> {
public MPNewSite(final MPUser<?> user, final String siteName) { public MPNewSite(final MPUser<?> user, final String siteName) {
super( user, 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." );
}
} }

View File

@ -100,6 +100,7 @@ public abstract class Components {
public static int showDialog(@Nullable final Component owner, @Nullable final String title, final JOptionPane pane) { public static int showDialog(@Nullable final Component owner, @Nullable final String title, final JOptionPane pane) {
JDialog dialog = pane.createDialog( owner, title ); JDialog dialog = pane.createDialog( owner, title );
dialog.setMinimumSize( new Dimension( 520, 0 ) );
dialog.setModalityType( Dialog.ModalityType.DOCUMENT_MODAL ); dialog.setModalityType( Dialog.ModalityType.DOCUMENT_MODAL );
showDialog( dialog ); showDialog( dialog );
@ -143,7 +144,6 @@ public abstract class Components {
title, Dialog.ModalityType.DOCUMENT_MODAL ); title, Dialog.ModalityType.DOCUMENT_MODAL );
dialog.setMinimumSize( new Dimension( 320, 0 ) ); dialog.setMinimumSize( new Dimension( 320, 0 ) );
dialog.setLocationRelativeTo( owner ); dialog.setLocationRelativeTo( owner );
dialog.setLocationByPlatform( true );
dialog.setContentPane( content ); dialog.setContentPane( content );
return showDialog( dialog ); return showDialog( dialog );
@ -155,6 +155,7 @@ public abstract class Components {
dialog.getRootPane().putClientProperty( "Window.style", "small" ); dialog.getRootPane().putClientProperty( "Window.style", "small" );
dialog.pack(); dialog.pack();
dialog.setLocationByPlatform( true );
dialog.setVisible( true ); dialog.setVisible( true );
return dialog; return dialog;
@ -176,7 +177,7 @@ public abstract class Components {
}; };
} }
public static JTextField textField(@Nullable final String text, @Nullable final Consumer<String> selection) { public static JTextField textField(@Nullable final String text, @Nullable final Consumer<String> change) {
return new JTextField( text ) { return new JTextField( text ) {
{ {
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ), 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 ) ); setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
setAlignmentX( LEFT_ALIGNMENT ); setAlignmentX( LEFT_ALIGNMENT );
if (selection != null) if (change != null) {
getDocument().addDocumentListener( new DocumentListener() { getDocument().addDocumentListener( new DocumentListener() {
@Override @Override
public void insertUpdate(final DocumentEvent e) { public void insertUpdate(final DocumentEvent e) {
selection.accept( getText() ); change.accept( getText() );
} }
@Override @Override
public void removeUpdate(final DocumentEvent e) { public void removeUpdate(final DocumentEvent e) {
selection.accept( getText() ); change.accept( getText() );
} }
@Override @Override
public void changedUpdate(final DocumentEvent e) { public void changedUpdate(final DocumentEvent e) {
selection.accept( getText() ); change.accept( getText() );
} }
} ); } );
change.accept( getText() );
}
} }
@Override @Override
@ -228,6 +231,7 @@ public abstract class Components {
public static <E> JList<E> list(final ListModel<E> model, final Function<E, String> valueTransformer) { public static <E> JList<E> list(final ListModel<E> model, final Function<E, String> valueTransformer) {
return new JList<E>( model ) { return new JList<E>( model ) {
{ {
setAlignmentX( LEFT_ALIGNMENT );
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
setCellRenderer( new DefaultListCellRenderer() { setCellRenderer( new DefaultListCellRenderer() {
@Override @Override
@ -241,7 +245,9 @@ public abstract class Components {
return this; return this;
} }
} ); } );
setAlignmentX( LEFT_ALIGNMENT );
if (model instanceof CollectionListModel)
((CollectionListModel<E>) model).registerList( this );
} }
@Override @Override

View File

@ -9,8 +9,6 @@ import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.MPFileUser; import com.lyndir.masterpassword.model.impl.MPFileUser;
import com.lyndir.masterpassword.model.impl.MPFileUserManager; import com.lyndir.masterpassword.model.impl.MPFileUserManager;
import java.awt.*; import java.awt.*;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
@ -26,8 +24,6 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma
private final CollectionListModel<MPUser<?>> usersModel = private final CollectionListModel<MPUser<?>> usersModel =
CollectionListModel.<MPUser<?>>copy( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser ); CollectionListModel.<MPUser<?>>copy( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser );
private final JComboBox<? extends MPUser<?>> userField =
Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) );
protected FilesPanel() { protected FilesPanel() {
setOpaque( false ); setOpaque( false );
@ -46,7 +42,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma
add( Components.strut( Components.margin() ) ); add( Components.strut( Components.margin() ) );
// User Selection // User Selection
add( userField ); add( Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) ) );
MPFileUserManager.get().addListener( this ); MPFileUserManager.get().addListener( this );
MasterPassword.get().addListener( this ); MasterPassword.get().addListener( this );

View File

@ -3,12 +3,10 @@ package com.lyndir.masterpassword.gui.view;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.gui.util.Res; import com.lyndir.masterpassword.gui.util.Res;
import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.MPFileUserManager; import com.lyndir.masterpassword.model.impl.MPFileUserManager;
import java.awt.*; import java.awt.*;
import java.awt.event.ComponentAdapter; import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent; import java.awt.event.ComponentEvent;
import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.BevelBorder; import javax.swing.border.BevelBorder;
@ -23,8 +21,6 @@ public class MasterPasswordFrame extends JFrame {
@SuppressWarnings("FieldCanBeLocal") @SuppressWarnings("FieldCanBeLocal")
private final Components.GradientPanel root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS ); 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(); private final UserContentPanel userContent = new UserContentPanel();
@SuppressWarnings("MagicNumber") @SuppressWarnings("MagicNumber")
@ -32,15 +28,16 @@ public class MasterPasswordFrame extends JFrame {
super( "Master Password" ); super( "Master Password" );
setContentPane( root ); setContentPane( root );
root.add( filesPanel ); root.add( new FilesPanel() );
root.add( Components.strut() ); 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.getUserToolbar(), BorderLayout.LINE_START );
userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END ); userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END );
userPanel.add( Components.borderPanel( userPanel.add( Components.borderPanel(
BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ), BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ),
Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent ), BorderLayout.CENTER ); Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent ), BorderLayout.CENTER );
root.add( userPanel );
addComponentListener( new ComponentHandler() ); addComponentListener( new ComponentHandler() );
setPreferredSize( new Dimension( 800, 560 ) ); setPreferredSize( new Dimension( 800, 560 ) );

View File

@ -10,8 +10,7 @@ import com.lyndir.lhunath.opal.system.util.ObjectUtils;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.gui.MPGuiConstants; import com.lyndir.masterpassword.gui.MPGuiConstants;
import com.lyndir.masterpassword.gui.MasterPassword; import com.lyndir.masterpassword.gui.MasterPassword;
import com.lyndir.masterpassword.gui.model.MPIncognitoUser; import com.lyndir.masterpassword.gui.model.*;
import com.lyndir.masterpassword.gui.model.MPNewSite;
import com.lyndir.masterpassword.gui.util.*; import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.gui.util.Platform; import com.lyndir.masterpassword.gui.util.Platform;
import com.lyndir.masterpassword.model.*; 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(), private final JButton resetButton = Components.button( Res.icons().reset(), event -> resetUser(),
"Change the master password for this user." ); "Change the master password for this user." );
private final JPasswordField masterPasswordField = Components.passwordField(); private final JPasswordField masterPasswordField;
private final JLabel errorLabel = Components.label(); private final JLabel errorLabel;
private final JLabel identiconLabel = Components.label( SwingConstants.CENTER ); private final JLabel identiconLabel;
private Future<?> identiconJob; private Future<?> identiconJob;
@ -312,16 +311,16 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
add( Components.strut() ); add( Components.strut() );
add( identiconLabel ); add( identiconLabel = Components.label( SwingConstants.CENTER ) );
identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) ); identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) );
add( Box.createGlue() ); add( Box.createGlue() );
add( Components.label( "Master Password:" ) ); add( Components.label( "Master Password:" ) );
add( Components.strut() ); add( Components.strut() );
add( masterPasswordField ); add( masterPasswordField = Components.passwordField() );
masterPasswordField.addActionListener( this ); masterPasswordField.addActionListener( this );
masterPasswordField.getDocument().addDocumentListener( this ); masterPasswordField.getDocument().addDocumentListener( this );
add( errorLabel ); add( errorLabel = Components.label() );
errorLabel.setForeground( Res.colors().errorFg() ); errorLabel.setForeground( Res.colors().errorFg() );
add( Box.createGlue() ); add( Box.createGlue() );
} }
@ -468,21 +467,20 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
"Sign out and lock user." ); "Sign out and lock user." );
private final JButton settingsButton = Components.button( Res.icons().settings(), event -> showSiteSettings(), private final JButton settingsButton = Components.button( Res.icons().settings(), event -> showSiteSettings(),
"Show site settings." ); "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." ); "Show site recovery questions." );
private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(), private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(),
"Delete the site from the user." ); "Delete the site from the user." );
@Nonnull @Nonnull
private final MPUser<?> user; private final MPUser<?> user;
private final JLabel passwordLabel = Components.label( SwingConstants.CENTER ); private final JLabel passwordLabel;
private final JLabel passwordField = Components.heading( SwingConstants.CENTER ); private final JLabel passwordField;
private final JLabel queryLabel = Components.label(); private final JLabel answerField;
private final JTextField queryField = Components.textField( null, this::updateSites ); private final JLabel queryLabel;
private final CollectionListModel<MPSite<?>> sitesModel = private final JTextField queryField;
new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ); private final CollectionListModel<MPSite<?>> sitesModel;
private final JList<MPSite<?>> sitesList = private final JList<MPSite<?>> sitesList;
Components.list( sitesModel, this::getSiteDescription );
private Future<?> updateSitesJob; private Future<?> updateSitesJob;
@ -501,27 +499,32 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
siteToolbar.add( questionsButton ); siteToolbar.add( questionsButton );
siteToolbar.add( deleteButton ); siteToolbar.add( deleteButton );
settingsButton.setEnabled( false ); settingsButton.setEnabled( false );
questionsButton.setEnabled( false );
deleteButton.setEnabled( false ); deleteButton.setEnabled( false );
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
add( passwordLabel ); add( passwordLabel = Components.label( SwingConstants.CENTER ) );
add( passwordField ); add( passwordField = Components.heading( SwingConstants.CENTER ) );
passwordField.setForeground( Res.colors().highlightFg() ); passwordField.setForeground( Res.colors().highlightFg() );
passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) ); 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( Box.createGlue() );
add( Components.strut() ); add( Components.strut() );
add( queryLabel ); add( queryLabel = Components.label() );
queryLabel.setText( strf( "%s's password for:", user.getFullName() ) ); queryLabel.setText( strf( "%s's password for:", user.getFullName() ) );
add( queryField ); add( queryField = Components.textField( null, this::updateSites ) );
queryField.putClientProperty( "JTextField.variant", "search" ); queryField.putClientProperty( "JTextField.variant", "search" );
queryField.addActionListener( event -> useSite() ); queryField.addActionListener( event -> useSite() );
queryField.addKeyListener( this ); queryField.addKeyListener( this );
queryField.requestFocusInWindow(); queryField.requestFocusInWindow();
add( Components.strut() ); add( Components.strut() );
add( Components.scrollPane( sitesList ) ); add( Components.scrollPane( sitesList = Components.list(
sitesModel.registerList( sitesList ); sitesModel = new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ),
this::getSiteDescription ) ) );
add( Box.createGlue() ); add( Box.createGlue() );
addHierarchyListener( e -> { addHierarchyListener( e -> {
@ -587,10 +590,56 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
Components.textField( fileSite.getUrl(), fileSite::setUrl ), Components.textField( fileSite.getUrl(), fileSite::setUrl ),
Components.strut() ); 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] ) ) ) ); BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
} }
public void showSiteQuestions() {
MPSite<?> site = sitesModel.getSelectedItem();
if (site == null)
return;
CollectionListModel<MPQuestion> questionsModel = new CollectionListModel<MPQuestion>().selection( this::showQuestionResult );
JList<MPQuestion> questionsList = Components.list( questionsModel, MPQuestion::getKeyword );
JTextField queryField = Components.textField( null, query -> Res.job( () -> {
Collection<MPQuestion> 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() { public void deleteSite() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = sitesModel.getSelectedItem();
if (site == null) if (site == null)
@ -668,6 +717,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
passwordLabel.setText( " " ); passwordLabel.setText( " " );
passwordField.setText( " " ); passwordField.setText( " " );
settingsButton.setEnabled( false ); settingsButton.setEnabled( false );
questionsButton.setEnabled( false );
deleteButton.setEnabled( false ); deleteButton.setEnabled( false );
} ); } );
return; return;
@ -683,6 +733,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
passwordLabel.setText( strf( "Your password for %s:", site.getSiteName() ) ); passwordLabel.setText( strf( "Your password for %s:", site.getSiteName() ) );
passwordField.setText( result ); passwordField.setText( result );
settingsButton.setEnabled( true ); settingsButton.setEnabled( true );
questionsButton.setEnabled( true );
deleteButton.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( "<html>Remember the answer for the security question with keyword <strong>%s</strong>?</html>",
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<String> 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 @Override
public void keyTyped(final KeyEvent event) { public void keyTyped(final KeyEvent event) {
} }
@ -713,13 +824,11 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
updateSitesJob.cancel( true ); updateSitesJob.cancel( true );
updateSitesJob = Res.job( () -> { updateSitesJob = Res.job( () -> {
Collection<MPSite<?>> sites = new LinkedList<>(); Collection<MPSite<?>> sites = new LinkedList<>( user.findSites( query ) );
if (!Strings.isNullOrEmpty( query )) {
sites.addAll( new LinkedList<>( user.findSites( query ) ) );
if (!Strings.isNullOrEmpty( query ))
if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) )) if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) ))
sites.add( new MPNewSite( user, query ) ); sites.add( new MPNewSite( user, query ) );
}
Res.ui( () -> sitesModel.set( sites ) ); Res.ui( () -> sitesModel.set( sites ) );
} ); } );

View File

@ -40,6 +40,13 @@ public interface MPQuestion extends Comparable<MPQuestion> {
void setType(MPResultType type); void setType(MPResultType type);
@Nonnull
default String getAnswer()
throws MPKeyUnavailableException, MPAlgorithmException {
return getAnswer( null );
}
@Nonnull @Nonnull
String getAnswer(@Nullable String state) String getAnswer(@Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException; throws MPKeyUnavailableException, MPAlgorithmException;

View File

@ -18,6 +18,7 @@
package com.lyndir.masterpassword.model; package com.lyndir.masterpassword.model;
import com.google.common.collect.ImmutableCollection;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import java.util.Collection; import java.util.Collection;
@ -57,6 +58,7 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
void setLoginType(@Nullable MPResultType loginType); void setLoginType(@Nullable MPResultType loginType);
@Nonnull
default String getResult() default String getResult()
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@ -79,6 +81,16 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state) String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException; 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 @Nonnull
default String getLogin() default String getLogin()
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@ -94,10 +106,17 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
@Nonnull @Nonnull
MPUser<?> getUser(); MPUser<?> getUser();
boolean addQuestion(Q question); @Nonnull
Q addQuestion(String keyword);
@Nonnull
Q addQuestion(Q question);
boolean deleteQuestion(Q question); boolean deleteQuestion(Q question);
@Nonnull @Nonnull
Collection<Q> getQuestions(); Collection<Q> getQuestions();
@Nonnull
ImmutableCollection<Q> findQuestions(@Nullable String query);
} }

View File

@ -100,6 +100,7 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
// - Relations // - Relations
@Nonnull
S addSite(String siteName); S addSite(String siteName);
@Nonnull @Nonnull

View File

@ -22,6 +22,7 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPQuestion; import com.lyndir.masterpassword.model.MPQuestion;
import com.lyndir.masterpassword.model.MPSite;
import java.util.Objects; import java.util.Objects;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -33,14 +34,23 @@ import org.jetbrains.annotations.NotNull;
*/ */
public abstract class MPBasicQuestion extends Changeable implements MPQuestion { public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
private final MPSite<?> site;
private final String keyword; private final String keyword;
private MPResultType type; private MPResultType type;
protected MPBasicQuestion(final String keyword, final MPResultType type) { protected MPBasicQuestion(final MPSite<?> site, final String keyword, final MPResultType type) {
this.site = site;
this.keyword = keyword; this.keyword = keyword;
this.type = type; this.type = type;
} }
@Nonnull
@Override
public MPSite<?> getSite() {
return site;
}
@Nonnull @Nonnull
@Override @Override
public String getKeyword() { public String getKeyword() {
@ -55,7 +65,7 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
@Override @Override
public void setType(final MPResultType type) { public void setType(final MPResultType type) {
if (Objects.equals(this.type, type)) if (this.type == type)
return; return;
this.type = type; this.type = type;
@ -70,15 +80,12 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
return getSite().getResult( MPKeyPurpose.Recovery, getKeyword(), null, getType(), state ); return getSite().getResult( MPKeyPurpose.Recovery, getKeyword(), null, getType(), state );
} }
@Nonnull
@Override
public abstract MPBasicSite<?, ?> getSite();
@Override @Override
protected void onChanged() { protected void onChanged() {
super.onChanged(); super.onChanged();
getSite().setChanged(); if (site instanceof Changeable)
((Changeable) site).setChanged();
} }
@Override @Override

View File

@ -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.ObjectUtils.*;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; 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.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.*;
@ -35,9 +38,9 @@ import javax.annotation.Nullable;
public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> extends Changeable public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> extends Changeable
implements MPSite<Q> { implements MPSite<Q> {
private final Collection<Q> questions = new LinkedHashSet<>();
private final U user; private final U user;
private final String siteName; private final String siteName;
private final Collection<Q> questions = new LinkedHashSet<>();
private MPAlgorithm algorithm; private MPAlgorithm algorithm;
private UnsignedInteger counter; private UnsignedInteger counter;
@ -104,7 +107,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
@Override @Override
public void setResultType(final MPResultType resultType) { public void setResultType(final MPResultType resultType) {
if (Objects.equals( this.resultType, resultType )) if (this.resultType == resultType)
return; return;
this.resultType = resultType; this.resultType = resultType;
@ -119,7 +122,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
@Override @Override
public void setLoginType(@Nullable final MPResultType loginType) { public void setLoginType(@Nullable final MPResultType loginType) {
if (Objects.equals( this.loginType, loginType )) if (this.loginType == loginType)
return; return;
this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() ); this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() );
@ -134,7 +137,9 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
return getResult( keyPurpose, keyContext, getCounter(), getResultType(), state ); return getResult( keyPurpose, keyContext, getCounter(), getResultType(), state );
} }
protected String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nonnull
@Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state) @Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@ -143,7 +148,9 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
keyPurpose, keyContext, type, state ); keyPurpose, keyContext, type, state );
} }
protected String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nonnull
@Override
public String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, final String state) @Nullable final UnsignedInteger counter, final MPResultType type, final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@ -160,13 +167,13 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
return getResult( MPKeyPurpose.Identification, null, null, getLoginType(), state ); return getResult( MPKeyPurpose.Identification, null, null, getLoginType(), state );
} }
@Nonnull
@Override @Override
public boolean addQuestion(final Q question) { public Q addQuestion(final Q question) {
if (!questions.add( question )) questions.add( question );
return false;
setChanged(); setChanged();
return true; return question;
} }
@Override @Override
@ -184,6 +191,17 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
return Collections.unmodifiableCollection( questions ); return Collections.unmodifiableCollection( questions );
} }
@Nonnull
@Override
public ImmutableCollection<Q> findQuestions(@Nullable final String query) {
ImmutableSortedSet.Builder<Q> results = ImmutableSortedSet.naturalOrder();
for (final Q question : getQuestions())
if (Strings.isNullOrEmpty( query ) || question.getKeyword().startsWith( query ))
results.add( question );
return results.build();
}
@Nonnull @Nonnull
@Override @Override
public U getUser() { public U getUser() {

View File

@ -20,6 +20,7 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; 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.ImmutableCollection;
import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ImmutableSortedSet;
import com.lyndir.lhunath.opal.system.CodeUtils; import com.lyndir.lhunath.opal.system.CodeUtils;
@ -202,9 +203,8 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
@Override @Override
public ImmutableCollection<S> findSites(@Nullable final String query) { public ImmutableCollection<S> findSites(@Nullable final String query) {
ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder(); ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
if (query != null)
for (final S site : getSites()) for (final S site : getSites())
if (site.getSiteName().startsWith( query )) if (Strings.isNullOrEmpty( query ) || site.getSiteName().startsWith( query ))
results.add( site ); results.add( site );
return results.build(); return results.build();

View File

@ -30,16 +30,13 @@ import javax.annotation.Nullable;
*/ */
public class MPFileQuestion extends MPBasicQuestion { public class MPFileQuestion extends MPBasicQuestion {
private final MPFileSite site;
@Nullable @Nullable
private String answerState; private String answerState;
public MPFileQuestion(final MPFileSite site, final String keyword, public MPFileQuestion(final MPFileSite site, final String keyword,
@Nullable final MPResultType type, @Nullable final String answerState) { @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; this.answerState = answerState;
} }
@ -48,6 +45,8 @@ public class MPFileQuestion extends MPBasicQuestion {
return answerState; return answerState;
} }
@Nonnull
@Override
public String getAnswer() public String getAnswer()
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getAnswer( answerState ); return getAnswer( answerState );
@ -66,9 +65,8 @@ public class MPFileQuestion extends MPBasicQuestion {
setChanged(); setChanged();
} }
@Nonnull public void use() {
@Override if (getSite() instanceof MPFileSite)
public MPFileSite getSite() { ((MPFileSite) getSite()).use();
return site;
} }
} }

View File

@ -145,6 +145,12 @@ public class MPFileSite extends MPBasicSite<MPFileUser, MPFileQuestion> {
setChanged(); setChanged();
} }
@Nonnull
@Override
public MPFileQuestion addQuestion(final String keyword) {
return addQuestion( new MPFileQuestion( this, keyword, null, null ) );
}
@Override @Override
public int compareTo(@Nonnull final MPSite<?> o) { public int compareTo(@Nonnull final MPSite<?> o) {
int comparison = (o instanceof MPFileSite)? ((MPFileSite) o).getLastUsed().compareTo( getLastUsed() ): 0; int comparison = (o instanceof MPFileSite)? ((MPFileSite) o).getLastUsed().compareTo( getLastUsed() ): 0;

View File

@ -207,6 +207,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
super.reset(); super.reset();
} }
@Nonnull
@Override @Override
public MPFileSite addSite(final String siteName) { public MPFileSite addSite(final String siteName) {
return addSite( new MPFileSite( this, siteName ) ); return addSite( new MPFileSite( this, siteName ) );

@ -1 +1 @@
Subproject commit b190b45756666da71fa8037501530b14c609be24 Subproject commit 0623231ad5614d5e6541199fd755589f04e214cb