Site security questions and copy login name.
This commit is contained in:
parent
10c6d203b8
commit
f41cdb8742
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,6 +7,7 @@ Thumbs.db
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
out
|
||||
|
||||
# Xcode IDE
|
||||
xcuserdata/
|
||||
|
@ -5,21 +5,31 @@
|
||||
<option name="myDefaultNotNull" value="javax.annotation.Nonnull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="4">
|
||||
<list size="9">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||
<item index="4" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
||||
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
|
||||
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
||||
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
||||
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="4">
|
||||
<list size="9">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||
<item index="4" class="java.lang.String" itemvalue="javax.validation.constraints.NotNull" />
|
||||
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
|
||||
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
||||
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
|
||||
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
|
@ -18,7 +18,9 @@
|
||||
|
||||
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;
|
||||
@ -238,8 +240,9 @@ public abstract class Components {
|
||||
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
|
||||
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
|
||||
final boolean isSelected, final boolean cellHasFocus) {
|
||||
String label = valueTransformer.apply( (E) value );
|
||||
super.getListCellRendererComponent(
|
||||
list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus );
|
||||
list, Strings.isNullOrEmpty( label )? " ": label, index, isSelected, cellHasFocus );
|
||||
setBorder( BorderFactory.createEmptyBorder( 2, 4, 2, 4 ) );
|
||||
|
||||
return this;
|
||||
@ -415,7 +418,7 @@ public abstract class Components {
|
||||
public static JLabel label(@Nullable final String label, final int horizontalAlignment) {
|
||||
return new JLabel( label, horizontalAlignment ) {
|
||||
{
|
||||
setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
|
||||
//setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
}
|
||||
|
||||
@ -466,8 +469,9 @@ public abstract class Components {
|
||||
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
|
||||
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
|
||||
final boolean isSelected, final boolean cellHasFocus) {
|
||||
String label = valueTransformer.apply( (E) value );
|
||||
super.getListCellRendererComponent(
|
||||
list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus );
|
||||
list, Strings.isNullOrEmpty( label )? " ": label, index, isSelected, cellHasFocus );
|
||||
setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) );
|
||||
|
||||
return this;
|
||||
|
@ -31,6 +31,8 @@ import java.io.IOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.joda.time.*;
|
||||
@ -72,6 +74,15 @@ public abstract class Res {
|
||||
return job( job, 0, TimeUnit.MILLISECONDS );
|
||||
}
|
||||
|
||||
public static <V> void job(final Callable<V> job, final Consumer<V> callback) {
|
||||
Futures.addCallback( job( job, 0, TimeUnit.MILLISECONDS ), new FailableCallback<V>( logger ) {
|
||||
@Override
|
||||
public void onSuccess(@Nullable final V result) {
|
||||
callback.accept( result );
|
||||
}
|
||||
}, uiExecutor() );
|
||||
}
|
||||
|
||||
public static <V> ListenableFuture<V> job(final Callable<V> job, final long delay, final TimeUnit timeUnit) {
|
||||
return jobExecutor.schedule( job, delay, timeUnit );
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import com.lyndir.masterpassword.gui.util.*;
|
||||
import com.lyndir.masterpassword.gui.util.Platform;
|
||||
import com.lyndir.masterpassword.model.*;
|
||||
import com.lyndir.masterpassword.model.impl.*;
|
||||
import com.lyndir.masterpassword.util.Utilities;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
@ -41,9 +42,11 @@ import javax.swing.event.DocumentListener;
|
||||
@SuppressWarnings("SerializableStoresNonSerializable")
|
||||
public class UserContentPanel extends JPanel implements MasterPassword.Listener, MPUser.Listener {
|
||||
|
||||
private static final Random random = new Random();
|
||||
private static final Logger logger = Logger.get( UserContentPanel.class );
|
||||
private static final JButton iconButton = Components.button( Res.icons().user(), null, null );
|
||||
private static final Random random = new Random();
|
||||
private static final int SIZE_RESULT = 48;
|
||||
private static final Logger logger = Logger.get( UserContentPanel.class );
|
||||
private static final JButton iconButton = Components.button( Res.icons().user(), null, null );
|
||||
private static final KeyStroke copyLoginKeyStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK );
|
||||
|
||||
private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(),
|
||||
"Add a new user to Master Password." );
|
||||
@ -459,8 +462,6 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
|
||||
private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener {
|
||||
|
||||
public static final int SIZE_RESULT = 48;
|
||||
|
||||
private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences(),
|
||||
"Show user preferences." );
|
||||
private final JButton logoutButton = Components.button( Res.icons().lock(), event -> logoutUser(),
|
||||
@ -476,6 +477,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
private final MPUser<?> user;
|
||||
private final JLabel passwordLabel;
|
||||
private final JLabel passwordField;
|
||||
private final JLabel answerLabel;
|
||||
private final JLabel answerField;
|
||||
private final JLabel queryLabel;
|
||||
private final JTextField queryField;
|
||||
@ -502,15 +504,17 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
questionsButton.setEnabled( false );
|
||||
deleteButton.setEnabled( false );
|
||||
|
||||
answerLabel = Components.label( "Answer:" );
|
||||
answerField = Components.heading( SwingConstants.CENTER );
|
||||
answerField.setForeground( Res.colors().highlightFg() );
|
||||
answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
|
||||
|
||||
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
|
||||
|
||||
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() );
|
||||
|
||||
@ -518,14 +522,22 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
queryLabel.setText( strf( "%s's password for:", user.getFullName() ) );
|
||||
add( queryField = Components.textField( null, this::updateSites ) );
|
||||
queryField.putClientProperty( "JTextField.variant", "search" );
|
||||
queryField.addActionListener( event -> useSite() );
|
||||
queryField.addActionListener( this::useSite );
|
||||
queryField.getInputMap().put( copyLoginKeyStroke, JTextField.notifyAction );
|
||||
queryField.addKeyListener( this );
|
||||
queryField.requestFocusInWindow();
|
||||
add( Components.strut() );
|
||||
|
||||
add( Components.scrollPane( sitesList = Components.list(
|
||||
sitesModel = new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ),
|
||||
this::getSiteDescription ) ) );
|
||||
add( Box.createGlue() );
|
||||
add( Components.strut() );
|
||||
|
||||
add( Components.label( strf(
|
||||
"Press %s to copy password, %s+%s to copy login name.",
|
||||
KeyEvent.getKeyText( KeyEvent.VK_ENTER ),
|
||||
InputEvent.getModifiersExText( copyLoginKeyStroke.getModifiers() ),
|
||||
KeyEvent.getKeyText( copyLoginKeyStroke.getKeyCode() ) ) ) );
|
||||
|
||||
addHierarchyListener( e -> {
|
||||
if (null != SwingUtilities.windowForComponent( this ))
|
||||
@ -600,13 +612,12 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
return;
|
||||
|
||||
CollectionListModel<MPQuestion> questionsModel = new CollectionListModel<MPQuestion>().selection( this::showQuestionResult );
|
||||
JList<MPQuestion> questionsList = Components.list( questionsModel, MPQuestion::getKeyword );
|
||||
JList<MPQuestion> questionsList = Components.list(
|
||||
questionsModel, question -> Strings.isNullOrEmpty( question.getKeyword() )? "<site>": question.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 ) );
|
||||
if (questions.stream().noneMatch( question -> question.getKeyword().equalsIgnoreCase( query ) ))
|
||||
questions.add( new MPNewQuestion( site, Utilities.ifNotNullElse( query, "" ) ) );
|
||||
|
||||
Res.ui( () -> questionsModel.set( questions ) );
|
||||
} ) );
|
||||
@ -630,7 +641,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
BoxLayout.PAGE_AXIS,
|
||||
Components.label( "Security Question Keyword:" ), queryField,
|
||||
Components.strut(),
|
||||
Components.label( "Answer:" ), answerField,
|
||||
answerLabel, answerField,
|
||||
Components.strut(),
|
||||
Components.scrollPane( questionsList ) ) ) {
|
||||
@Override
|
||||
@ -675,80 +686,61 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
Joiner.on( " - " ).skipNulls().join( parameters.build() ) );
|
||||
}
|
||||
|
||||
private void useSite() {
|
||||
private void useSite(final ActionEvent event) {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
if (site instanceof MPNewSite) {
|
||||
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
|
||||
this, strf( "<html>Remember the site <strong>%s</strong>?</html>", site.getSiteName() ),
|
||||
"New Site", JOptionPane.YES_NO_OPTION )) {
|
||||
sitesModel.setSelectedItem( user.addSite( site.getSiteName() ) );
|
||||
useSite();
|
||||
useSite( event );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
showSiteResult( site, result -> {
|
||||
boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
|
||||
showSiteResult( site, loginResult, result -> {
|
||||
if (result == null)
|
||||
return;
|
||||
|
||||
if (site instanceof MPFileSite)
|
||||
((MPFileSite) site).use();
|
||||
|
||||
Transferable clipboardContents = new StringSelection( result );
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
|
||||
|
||||
Res.ui( () -> {
|
||||
Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
|
||||
if (window instanceof Frame)
|
||||
((Frame) window).setExtendedState( Frame.ICONIFIED );
|
||||
} );
|
||||
copyResult( result );
|
||||
} );
|
||||
}
|
||||
|
||||
private void showSiteResult(@Nullable final MPSite<?> site) {
|
||||
showSiteResult( site, null );
|
||||
showSiteResult( site, false, result -> {
|
||||
} );
|
||||
}
|
||||
|
||||
private void showSiteResult(@Nullable final MPSite<?> site, @Nullable final Consumer<String> resultCallback) {
|
||||
if (site == null) {
|
||||
if (resultCallback != null)
|
||||
resultCallback.accept( null );
|
||||
Res.ui( () -> {
|
||||
passwordLabel.setText( " " );
|
||||
passwordField.setText( " " );
|
||||
settingsButton.setEnabled( false );
|
||||
questionsButton.setEnabled( false );
|
||||
deleteButton.setEnabled( false );
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
private void showSiteResult(@Nullable final MPSite<?> site, final boolean loginResult, final Consumer<String> resultCallback) {
|
||||
Res.job( () -> {
|
||||
try {
|
||||
String result = site.getResult();
|
||||
if (resultCallback != null)
|
||||
resultCallback.accept( result );
|
||||
|
||||
Res.ui( () -> {
|
||||
passwordLabel.setText( strf( "Your password for %s:", site.getSiteName() ) );
|
||||
passwordField.setText( result );
|
||||
settingsButton.setEnabled( true );
|
||||
questionsButton.setEnabled( true );
|
||||
deleteButton.setEnabled( true );
|
||||
} );
|
||||
if (site != null)
|
||||
return loginResult? site.getLogin(): site.getResult();
|
||||
}
|
||||
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
||||
logger.err( e, "While resolving password for: %s", site );
|
||||
}
|
||||
} );
|
||||
|
||||
return null;
|
||||
}, resultCallback.andThen( result -> Res.ui( () -> {
|
||||
passwordLabel.setText( ((result != null) && (site != null))? strf( "Your password for %s:", site.getSiteName() ): " " );
|
||||
passwordField.setText( (result != null)? result: " " );
|
||||
settingsButton.setEnabled( result != null );
|
||||
questionsButton.setEnabled( result != null );
|
||||
deleteButton.setEnabled( result != null );
|
||||
} ) ) );
|
||||
}
|
||||
|
||||
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() ),
|
||||
strf( "<html>Remember the security question with keyword <strong>%s</strong>?</html>",
|
||||
Strings.isNullOrEmpty( question.getKeyword() )? "<empty>": question.getKeyword() ),
|
||||
"New Question", JOptionPane.YES_NO_OPTION )) {
|
||||
useQuestion( question.getSite().addQuestion( question.getKeyword() ) );
|
||||
}
|
||||
@ -762,44 +754,51 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
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 );
|
||||
} );
|
||||
copyResult( result );
|
||||
} );
|
||||
}
|
||||
|
||||
private void showQuestionResult(@Nullable final MPQuestion question) {
|
||||
showQuestionResult( question, null );
|
||||
showQuestionResult( question, answer -> {
|
||||
} );
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void showQuestionResult(@Nullable final MPQuestion question, final Consumer<String> resultCallback) {
|
||||
Res.job( () -> {
|
||||
try {
|
||||
String answer = question.getAnswer();
|
||||
if (resultCallback != null)
|
||||
resultCallback.accept( answer );
|
||||
|
||||
Res.ui( () -> answerField.setText( answer ) );
|
||||
if (question != null)
|
||||
return question.getAnswer();
|
||||
}
|
||||
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
||||
logger.err( e, "While resolving answer for: %s", question );
|
||||
}
|
||||
|
||||
return null;
|
||||
}, resultCallback.andThen( answer -> Res.ui( () -> {
|
||||
if ((answer == null) || (question == null))
|
||||
answerLabel.setText( " " );
|
||||
else
|
||||
answerLabel.setText(
|
||||
Strings.isNullOrEmpty( question.getKeyword() )?
|
||||
strf( "<html>Answer for site <b>%s</b>:", question.getSite().getSiteName() ):
|
||||
strf( "<html>Answer for site <b>%s</b>, of question with keyword <b>%s</b>:",
|
||||
question.getSite().getSiteName(), question.getKeyword() ) );
|
||||
answerField.setText( (answer != null)? answer: " " );
|
||||
} ) ) );
|
||||
}
|
||||
|
||||
private void copyResult(final String result) {
|
||||
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 );
|
||||
} );
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
|
||||
|
||||
private final MPSite<?> site;
|
||||
private final String keyword;
|
||||
private final String keyword;
|
||||
|
||||
private MPResultType type;
|
||||
|
||||
|
@ -170,7 +170,7 @@ public class MPJSONFile extends MPJSONAnyObject {
|
||||
if (fileSite.questions != null)
|
||||
for (final Map.Entry<String, Site.Question> questionEntry : fileSite.questions.entrySet()) {
|
||||
Site.Question fileQuestion = questionEntry.getValue();
|
||||
MPFileQuestion question = new MPFileQuestion( site, questionEntry.getKey(),
|
||||
MPFileQuestion question = new MPFileQuestion( site, ifNotNullElse( questionEntry.getKey(), "" ),
|
||||
fileQuestion.type, export.redacted? fileQuestion.answer: null );
|
||||
|
||||
if (!export.redacted && (fileQuestion.answer != null))
|
||||
|
Loading…
Reference in New Issue
Block a user