From 70f7fa1345c9638f820997a47cd79dbcf4a99a06 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sun, 29 Mar 2015 20:30:57 -0400 Subject: [PATCH] Java identicon support. --- .../lyndir/masterpassword/MPIdenticon.java | 106 ++++++++++++++++++ .../com/lyndir/masterpassword/MasterKey.java | 1 + .../gui/AuthenticationPanel.java | 9 ++ .../gui/IncognitoAuthenticationPanel.java | 2 + .../gui/ModelAuthenticationPanel.java | 6 +- .../masterpassword/gui/PasswordFrame.java | 4 +- .../com/lyndir/masterpassword/gui/Res.java | 8 ++ .../masterpassword/gui/UnlockFrame.java | 62 ++++++---- .../masterpassword/gui/util/Components.java | 41 ++++--- 9 files changed, 200 insertions(+), 39 deletions(-) create mode 100644 MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPIdenticon.java diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPIdenticon.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPIdenticon.java new file mode 100644 index 00000000..b1b799e1 --- /dev/null +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPIdenticon.java @@ -0,0 +1,106 @@ +package com.lyndir.masterpassword; + +import static com.lyndir.lhunath.opal.system.util.StringUtils.strf; + +import com.google.common.collect.ImmutableMap; +import com.lyndir.lhunath.opal.system.MessageAuthenticationDigests; +import com.lyndir.lhunath.opal.system.logging.Logger; +import java.awt.*; +import java.nio.*; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; + + +/** + * @author lhunath, 15-03-29 + */ +public class MPIdenticon { + + @SuppressWarnings("UnusedDeclaration") + private static final Logger logger = Logger.get( MPIdenticon.class ); + + private static final Charset charset = StandardCharsets.UTF_8; + private static final Color[] colors = new Color[]{ + Color.RED, Color.GREEN, Color.YELLOW, Color.BLUE, Color.MAGENTA, Color.CYAN, Color.MONO }; + private static final char[] leftArm = new char[]{ '╔', '╚', '╰', '═' }; + private static final char[] rightArm = new char[]{ '╗', '╝', '╯', '═' }; + private static final char[] body = new char[]{ '█', '░', '▒', '▓', '☺', '☻' }; + private static final char[] accessory = new char[]{ + '◈', '◎', '◐', '◑', '◒', '◓', '☀', '☁', '☂', '☃', '☄', '★', '☆', '☎', '☏', '⎈', '⌂', '☘', '☢', '☣', '☕', '⌚', '⌛', '⏰', '⚡', + '⛄', '⛅', '☔', '♔', '♕', '♖', '♗', '♘', '♙', '♚', '♛', '♜', '♝', '♞', '♟', '♨', '♩', '♪', '♫', '⚐', '⚑', '⚔', '⚖', '⚙', '⚠', + '⌘', '⏎', '✄', '✆', '✈', '✉', '✌' }; + + private final String fullName; + private final Color color; + private final String text; + + public MPIdenticon(String fullName, String masterPassword) { + this( fullName, masterPassword.toCharArray() ); + } + + public MPIdenticon(String fullName, char[] masterPassword) { + this.fullName = fullName; + + byte[] masterPasswordBytes = charset.encode( CharBuffer.wrap( masterPassword ) ).array(); + ByteBuffer identiconSeedBytes = ByteBuffer.wrap( + MessageAuthenticationDigests.HmacSHA256.of( masterPasswordBytes, fullName.getBytes( charset ) ) ); + Arrays.fill( masterPasswordBytes, (byte) 0 ); + + IntBuffer identiconSeedBuffer = IntBuffer.allocate( identiconSeedBytes.capacity() ); + while (identiconSeedBytes.hasRemaining()) + identiconSeedBuffer.put( identiconSeedBytes.get() & 0xFF ); + int[] identiconSeed = identiconSeedBuffer.array(); + + color = colors[identiconSeed[4] % colors.length]; + text = strf( "%c%c%c%c", leftArm[identiconSeed[0] % leftArm.length], body[identiconSeed[1] % body.length], + rightArm[identiconSeed[2] % rightArm.length], accessory[identiconSeed[3] % accessory.length] ); + } + + public String getFullName() { + return fullName; + } + + public String getText() { + return text; + } + + public Color getColor() { + return color; + } + + public enum BackgroundMode { + DARK, LIGHT + } + + + public enum Color { + RED( "#dc322f", "#dc322f" ), + GREEN( "#859900", "#859900" ), + YELLOW( "#b58900", "#b58900" ), + BLUE( "#268bd2", "#268bd2" ), + MAGENTA( "#d33682", "#d33682" ), + CYAN( "#2aa198", "#2aa198" ), + MONO( "#93a1a1", "#586e75" ); + + private final String rgbDark; + private final String rgbLight; + + Color(final String rgbDark, final String rgbLight) { + this.rgbDark = rgbDark; + this.rgbLight = rgbLight; + } + + public java.awt.Color getAWTColor(BackgroundMode backgroundMode) { + switch (backgroundMode) { + case DARK: + return new java.awt.Color( Integer.decode( rgbDark ) ); + case LIGHT: + return new java.awt.Color( Integer.decode( rgbLight ) ); + } + + throw new UnsupportedOperationException( "Unsupported background mode: " + backgroundMode ); + } + } +} diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java index 84390db9..6e152180 100644 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java @@ -65,6 +65,7 @@ public abstract class MasterKey { @Nonnull protected byte[] getKey() { + Preconditions.checkState( isValid() ); return Preconditions.checkNotNull( masterKey ); } diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/AuthenticationPanel.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/AuthenticationPanel.java index fd7a15fc..055cddf1 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/AuthenticationPanel.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/AuthenticationPanel.java @@ -1,9 +1,13 @@ package com.lyndir.masterpassword.gui; import com.google.common.collect.ImmutableList; +import com.lyndir.masterpassword.MPIdenticon; import com.lyndir.masterpassword.gui.util.Components; import java.awt.*; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.swing.*; +import org.jetbrains.annotations.NotNull; /** @@ -29,6 +33,8 @@ public abstract class AuthenticationPanel extends Components.GradientPanel { } } ); add( Box.createVerticalGlue() ); + + avatarLabel.setToolTipText( "The avatar for your user. Click to change it." ); } protected void updateUser(boolean repack) { @@ -39,8 +45,11 @@ public abstract class AuthenticationPanel extends Components.GradientPanel { unlockFrame.repack(); } + @Nullable protected abstract User getSelectedUser(); + @NotNull + @Nonnull public abstract char[] getMasterPassword(); public Component getFocusComponent() { diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/IncognitoAuthenticationPanel.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/IncognitoAuthenticationPanel.java index 54633a22..8c114bc4 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/IncognitoAuthenticationPanel.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/IncognitoAuthenticationPanel.java @@ -7,6 +7,7 @@ import java.awt.event.ActionListener; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import org.jetbrains.annotations.NotNull; /** @@ -58,6 +59,7 @@ public class IncognitoAuthenticationPanel extends AuthenticationPanel implements return new IncognitoUser( fullNameField.getText() ); } + @NotNull @Override public char[] getMasterPassword() { return masterPasswordField.getPassword(); diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/ModelAuthenticationPanel.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/ModelAuthenticationPanel.java index b78aac31..0f37d704 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/ModelAuthenticationPanel.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/ModelAuthenticationPanel.java @@ -14,6 +14,7 @@ import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.plaf.metal.MetalComboBoxEditor; +import org.jetbrains.annotations.NotNull; /** @@ -104,6 +105,7 @@ public class ModelAuthenticationPanel extends AuthenticationPanel implements Ite return userField.getModel().getElementAt( selectedIndex ); } + @NotNull @Override public char[] getMasterPassword() { return masterPasswordField.getPassword(); @@ -124,6 +126,7 @@ public class ModelAuthenticationPanel extends AuthenticationPanel implements Ite updateUser( true ); } } ); + setToolTipText( "Add a new user to the list." ); } }, new JButton( Res.iconQuestion() ) { { @@ -131,10 +134,11 @@ public class ModelAuthenticationPanel extends AuthenticationPanel implements Ite @Override public void actionPerformed(final ActionEvent e) { JOptionPane.showMessageDialog( ModelAuthenticationPanel.this, // - "Reads users and sites from the directory at ~/.mpw.", // + "Reads users and sites from the directory at ~/.mpw.d.", // "Help", JOptionPane.INFORMATION_MESSAGE ); } } ); + setToolTipText( "More information." ); } } ); } diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/PasswordFrame.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/PasswordFrame.java index c6fd071e..54ea2583 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/PasswordFrame.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/PasswordFrame.java @@ -53,7 +53,7 @@ public class PasswordFrame extends JFrame implements DocumentListener { add( Components.borderPanel( sitePanel, BorderFactory.createRaisedBevelBorder(), Res.colors().frameBg() ), BorderLayout.CENTER ); // User - sitePanel.add( Components.label( strf( "Generating passwords for: %s", user.getFullName() ), JLabel.CENTER ) ); + sitePanel.add( Components.label( strf( "Generating passwords for: %s", user.getFullName() ), SwingConstants.CENTER ) ); sitePanel.add( Components.stud() ); // Site Name @@ -161,7 +161,7 @@ public class PasswordFrame extends JFrame implements DocumentListener { updateMask(); // Tip - tipLabel = Components.label( " ", JLabel.CENTER ); + tipLabel = Components.label( " ", SwingConstants.CENTER ); tipLabel.setAlignmentX( Component.CENTER_ALIGNMENT ); JPanel passwordContainer = Components.boxLayout( BoxLayout.PAGE_AXIS, maskPasswordField, passwordField, tipLabel ); passwordContainer.setOpaque( true ); diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/Res.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/Res.java index 74fb5c84..a76b1a9f 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/Res.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/Res.java @@ -97,6 +97,10 @@ public abstract class Res { return 19; } + public static Font emoticonsFont() { + return emoticonsRegular(); + } + public static Font controlFont() { return arimoRegular(); } @@ -109,6 +113,10 @@ public abstract class Res { return sourceSansProBlack(); } + public static Font emoticonsRegular() { + return font( "fonts/Emoticons-Regular.otf" ); + } + public static Font sourceCodeProRegular() { return font( "fonts/SourceCodePro-Regular.otf" ); } diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/UnlockFrame.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/UnlockFrame.java index 5df63855..a3f3c115 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/UnlockFrame.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/UnlockFrame.java @@ -2,10 +2,12 @@ package com.lyndir.masterpassword.gui; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; +import com.lyndir.masterpassword.MPIdenticon; import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.model.IncorrectMasterPasswordException; import java.awt.*; import java.awt.event.*; +import javax.annotation.Nullable; import javax.swing.*; @@ -16,6 +18,7 @@ public class UnlockFrame extends JFrame { private final SignInCallback signInCallback; private final Components.GradientPanel root; + private final JLabel identiconLabel; private final JButton signInButton; private final JPanel authenticationContainer; private AuthenticationPanel authenticationPanel; @@ -27,33 +30,40 @@ public class UnlockFrame extends JFrame { super( "Unlock Master Password" ); this.signInCallback = signInCallback; - setDefaultCloseOperation(DISPOSE_ON_CLOSE); - setContentPane(root = Components.gradientPanel(new BorderLayout(20, 20), Res.colors().frameBg())); - root.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); - - addWindowFocusListener(new WindowAdapter() { + setDefaultCloseOperation( DISPOSE_ON_CLOSE ); + addWindowFocusListener( new WindowAdapter() { @Override public void windowGainedFocus(WindowEvent e) { - root.setGradientColor(Res.colors().frameBg()); + root.setGradientColor( Res.colors().frameBg() ); } @Override public void windowLostFocus(WindowEvent e) { - root.setGradientColor(Color.RED); + root.setGradientColor( Color.RED ); } - }); - - authenticationContainer = Components.boxLayout( BoxLayout.PAGE_AXIS ); - authenticationContainer.setOpaque( true ); - authenticationContainer.setBackground( Res.colors().controlBg() ); - authenticationContainer.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) ); - add( Components.borderPanel( authenticationContainer, BorderFactory.createRaisedBevelBorder(), Res.colors().frameBg() ) ); + } ); // Sign In JPanel signInBox = Components.boxLayout( BoxLayout.LINE_AXIS, Box.createGlue(), signInButton = Components.button( "Sign In" ), Box.createGlue() ); signInBox.setBackground( null ); - root.add( signInBox, BorderLayout.SOUTH ); + + setContentPane( root = Components.gradientPanel( new FlowLayout(), Res.colors().frameBg() ) ); + root.setLayout( new BoxLayout( root, BoxLayout.PAGE_AXIS ) ); + root.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) ); + root.add( Components.borderPanel( authenticationContainer = Components.boxLayout( BoxLayout.PAGE_AXIS ), + BorderFactory.createRaisedBevelBorder(), Res.colors().frameBg() ) ); + root.add( Box.createVerticalStrut( 8 ) ); + root.add( identiconLabel = Components.label( " ", SwingConstants.CENTER ) ); + root.add( Box.createVerticalStrut( 8 ) ); + root.add( signInBox ); + + authenticationContainer.setOpaque( true ); + authenticationContainer.setBackground( Res.colors().controlBg() ); + authenticationContainer.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) ); + identiconLabel.setFont( Res.emoticonsFont().deriveFont( 14.f ) ); + identiconLabel.setToolTipText( + "A representation of your identity across all Master Password apps.\nIt should always be the same." ); signInButton.addActionListener( new AbstractAction() { @Override public void actionPerformed(final ActionEvent e) { @@ -86,6 +96,7 @@ public class UnlockFrame extends JFrame { authenticationContainer.add( Components.stud() ); final JCheckBox incognitoCheckBox = Components.checkBox( "Incognito" ); + incognitoCheckBox.setToolTipText( "Log in without saving any information." ); incognitoCheckBox.setSelected( incognito ); incognitoCheckBox.addItemListener( new ItemListener() { @Override @@ -104,9 +115,10 @@ public class UnlockFrame extends JFrame { authenticationContainer.add( toolsPanel ); for (JButton button : authenticationPanel.getButtons()) { toolsPanel.add( button ); + button.setBorder( BorderFactory.createEmptyBorder() ); button.setMargin( new Insets( 0, 0, 0, 0 ) ); - button.setAlignmentX(RIGHT_ALIGNMENT); - button.setContentAreaFilled(false); + button.setAlignmentX( RIGHT_ALIGNMENT ); + button.setContentAreaFilled( false ); } checkSignIn(); @@ -121,13 +133,24 @@ public class UnlockFrame extends JFrame { } ); } - void updateUser(User user) { + void updateUser(@Nullable User user) { this.user = user; checkSignIn(); } boolean checkSignIn() { - boolean enabled = user != null && !user.getFullName().isEmpty() && authenticationPanel.getMasterPassword().length > 0; + String fullName = user == null? "": user.getFullName(); + char[] masterPassword = authenticationPanel.getMasterPassword(); + boolean enabled = !fullName.isEmpty() && masterPassword.length > 0; + + if (fullName.isEmpty() || masterPassword.length == 0) + identiconLabel.setText( " " ); + else { + MPIdenticon identicon = new MPIdenticon( fullName, masterPassword ); + identiconLabel.setText( identicon.getText() ); + identiconLabel.setForeground( identicon.getColor().getAWTColor( MPIdenticon.BackgroundMode.DARK ) ); + } + signInButton.setEnabled( enabled ); return enabled; @@ -170,7 +193,6 @@ public class UnlockFrame extends JFrame { } } ); } - } } ); } diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java index 1874349e..1110a354 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java @@ -70,10 +70,10 @@ public abstract class Components { public static JPasswordField passwordField() { return new JPasswordField() { { - setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Res.colors().controlBorder(), 1, true), - BorderFactory.createEmptyBorder(4, 4, 4, 4))); - setAlignmentX(LEFT_ALIGNMENT); - setAlignmentY(BOTTOM_ALIGNMENT); + setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ), + BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) ); + setAlignmentX( LEFT_ALIGNMENT ); + setAlignmentY( BOTTOM_ALIGNMENT ); } @Override @@ -116,7 +116,7 @@ public abstract class Components { ((DefaultEditor) getEditor()).getTextField().setBorder( editorBorder ); setAlignmentX( LEFT_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT ); - setBorder(null); + setBorder( null ); } @Override @@ -126,12 +126,21 @@ public abstract class Components { }; } - public static JLabel label(final String label) { - return label( label, JLabel.LEADING ); + public static JLabel label(@Nullable String label) { + return label( label, SwingConstants.LEADING ); } - public static JLabel label(final String label, final int alignment) { - return new JLabel( label, alignment ) { + /** + * @param horizontalAlignment One of the following constants + * defined in SwingConstants: + * LEFT, + * CENTER, + * RIGHT, + * LEADING or + * TRAILING. + */ + public static JLabel label(@Nullable final String label, final int horizontalAlignment) { + return new JLabel( label, horizontalAlignment ) { { setFont( Res.controlFont().deriveFont( 12f ) ); setAlignmentX( LEFT_ALIGNMENT ); @@ -148,8 +157,8 @@ public abstract class Components { public static JCheckBox checkBox(final String label) { return new JCheckBox( label ) { { - setFont(Res.controlFont().deriveFont(12f)); - setBackground(null); + setFont( Res.controlFont().deriveFont( 12f ) ); + setBackground( null ); setAlignmentX( LEFT_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT ); } @@ -164,14 +173,14 @@ public abstract class Components { public static JComboBox comboBox(final ComboBoxModel model) { return new JComboBox( model ) { { -// CompoundBorder editorBorder = BorderFactory.createCompoundBorder( -// BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ), -// BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ); -// ((JComponent) ((BasicComboBoxEditor) getEditor()).getEditorComponent()).setBorder(editorBorder); + // CompoundBorder editorBorder = BorderFactory.createCompoundBorder( + // BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ), + // BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ); + // ((JComponent) ((BasicComboBoxEditor) getEditor()).getEditorComponent()).setBorder(editorBorder); setFont( Res.controlFont().deriveFont( 12f ) ); setAlignmentX( LEFT_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT ); -// setBorder(null); + // setBorder(null); } @Override