From 9d7799c8147b119a175725a7058fa20dcdb3faef Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 30 Aug 2014 20:08:20 -0400 Subject: [PATCH] Improved Master Password algorithm API & GUI improvements. [IMPROVED] Read the master password using Console, not stdin. [IMPROVED] Clear the site password when the dialog closes. [IMPROVED] Make the site password selectable. --- .../lyndir/masterpassword/MPElementType.java | 6 +- .../{MasterPassword.java => MasterKey.java} | 85 ++++++++++--------- .../masterpassword/EmergencyActivity.java | 17 ++-- .../java/com/lyndir/masterpassword/CLI.java | 68 ++++++++------- .../ConfigAuthenticationPanel.java | 2 +- .../lyndir/masterpassword/PasswordFrame.java | 26 ++++-- .../lyndir/masterpassword/UnlockFrame.java | 2 +- .../java/com/lyndir/masterpassword/User.java | 24 +++--- 8 files changed, 120 insertions(+), 110 deletions(-) rename MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/{MasterPassword.java => MasterKey.java} (63%) diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementType.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementType.java index 213cf704..aa0cf4ac 100644 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementType.java +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementType.java @@ -80,11 +80,9 @@ public enum MPElementType { */ public static MPElementType forName(final String name) { - for (final MPElementType type : values()) { - if (type.getName().equalsIgnoreCase( name ) || type.getShortName().equalsIgnoreCase( name )) { + for (final MPElementType type : values()) + if (type.getName().equalsIgnoreCase( name ) || type.getShortName().equalsIgnoreCase( name )) return type; - } - } throw logger.bug( "Element type not known: %s", name ); } diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterPassword.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java similarity index 63% rename from MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterPassword.java rename to MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java index b1df9ce3..500776ef 100644 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterPassword.java +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java @@ -2,6 +2,8 @@ package com.lyndir.masterpassword; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; +import com.google.common.io.CharSource; +import com.google.common.io.CharStreams; import com.google.common.primitives.Bytes; import com.lambdaworks.crypto.SCrypt; import com.lyndir.lhunath.opal.crypto.CryptUtils; @@ -11,18 +13,17 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.Charset; import java.security.GeneralSecurityException; +import java.util.Arrays; +import javax.xml.stream.events.Characters; /** - * Implementation of the Master Password algorithm. - * - * 07 04, 2012 - * - * @author lhunath + * @author lhunath, 2014-08-30 */ -public abstract class MasterPassword { +public class MasterKey { - static final Logger logger = Logger.get( MasterPassword.class ); + @SuppressWarnings("UnusedDeclaration") + private static final Logger logger = Logger.get( MasterKey.class ); private static final int MP_N = 32768; private static final int MP_r = 8; private static final int MP_p = 2; @@ -33,52 +34,60 @@ public abstract class MasterPassword { private static final MessageAuthenticationDigests MP_mac = MessageAuthenticationDigests.HmacSHA256; private static final MPTemplates templates = MPTemplates.load(); - public static byte[] keyForPassword(final String password, final String username) { + private final String userName; + private final byte[] key; + + private boolean valid; + + public MasterKey(final String userName, final String masterPassword) { + + this.userName = userName; long start = System.currentTimeMillis(); - byte[] nusernameLengthBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ) - .order( MP_byteOrder ) - .putInt( username.length() ) - .array(); + byte[] userNameLengthBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ) + .order( MP_byteOrder ) + .putInt( userName.length() ) + .array(); byte[] salt = Bytes.concat( "com.lyndir.masterpassword".getBytes( MP_charset ), // - nusernameLengthBytes, // - username.getBytes( MP_charset ) ); + userNameLengthBytes, userName.getBytes( MP_charset ) ); try { - byte[] key = SCrypt.scrypt( password.getBytes( MP_charset ), salt, MP_N, MP_r, MP_p, MP_dkLen ); - logger.trc( "User: %s, password: %s derives to key ID: %s (took %.2fs)", username, password, - CodeUtils.encodeHex( keyIDForKey( key ) ), (double) (System.currentTimeMillis() - start) / 1000 ); + key = SCrypt.scrypt( masterPassword.getBytes( MP_charset ), salt, MP_N, MP_r, MP_p, MP_dkLen ); + valid = true; - return key; + logger.trc( "User: %s, master password derives to key ID: %s (took %.2fs)", // + userName, getKeyID(), (double) (System.currentTimeMillis() - start) / 1000 ); } catch (GeneralSecurityException e) { throw logger.bug( e ); } } - public static byte[] subkeyForKey(final byte[] key, final int subkeyLength) { + public String getUserName() { + return userName; + } + + public String getKeyID() { + + Preconditions.checkState( valid ); + return CodeUtils.encodeHex( MP_hash.of( key ) ); + } + + private byte[] getSubkey(final int subkeyLength) { + + Preconditions.checkState( valid ); byte[] subkey = new byte[Math.min( subkeyLength, key.length )]; System.arraycopy( key, 0, subkey, 0, subkey.length ); return subkey; } - public static byte[] keyIDForPassword(final String password, final String username) { - - return keyIDForKey( keyForPassword( password, username ) ); - } - - public static byte[] keyIDForKey(final byte[] key) { - - return MP_hash.of( key ); - } - - public static String generateContent(final MPElementType type, final String name, final byte[] key, int counter) { + public String encode(final String name, final MPElementType type, int counter) { + Preconditions.checkState( valid ); Preconditions.checkArgument( type.getTypeClass() == MPElementTypeClass.Generated ); Preconditions.checkArgument( !name.isEmpty() ); - Preconditions.checkArgument( key.length > 0 ); if (counter == 0) counter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300; @@ -112,17 +121,9 @@ public abstract class MasterPassword { return password.toString(); } - public static void main(final String... arguments) { + public void invalidate() { - String masterPassword = "test-mp"; - String username = "test-user"; - String siteName = "test-site"; - MPElementType siteType = MPElementType.GeneratedLong; - int siteCounter = 42; - - String sitePassword = generateContent( siteType, siteName, keyForPassword( masterPassword, username ), siteCounter ); - - logger.inf( "master password: %s, username: %s\nsite name: %s, site type: %s, site counter: %d\n => site password: %s", - masterPassword, username, siteName, siteType, siteCounter, sitePassword ); + valid = false; + Arrays.fill( key, (byte) 0 ); } } diff --git a/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java b/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java index 571ed493..8376489f 100644 --- a/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java +++ b/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java @@ -16,7 +16,6 @@ import com.google.common.base.Throwables; import com.google.common.util.concurrent.*; import com.lyndir.lhunath.opal.system.logging.Logger; import java.util.concurrent.*; -import java.util.prefs.Preferences; public class EmergencyActivity extends Activity { @@ -38,7 +37,7 @@ public class EmergencyActivity extends Activity { } }; - private ListenableFuture masterKeyFuture; + private ListenableFuture masterKeyFuture; @InjectView(R.id.progressView) ProgressBar progressView; @@ -142,16 +141,12 @@ public class EmergencyActivity extends Activity { } progressView.setVisibility( View.VISIBLE ); - (masterKeyFuture = executor.submit( new Callable() { + (masterKeyFuture = executor.submit( new Callable() { @Override - public byte[] call() + public MasterKey call() throws Exception { try { - long start = System.currentTimeMillis(); - byte[] masterKey = MasterPassword.keyForPassword( masterPassword, userName ); - logger.inf( "masterKey time: %d", System.currentTimeMillis() - start ); - - return masterKey; + return new MasterKey( userName, masterPassword ); } catch (RuntimeException e) { sitePasswordField.setText( "" ); @@ -189,9 +184,7 @@ public class EmergencyActivity extends Activity { @Override public void run() { try { - long start = System.currentTimeMillis(); - final String sitePassword = MasterPassword.generateContent( type, siteName, masterKeyFuture.get(), counter ); - logger.inf( "sitePassword time: %d", System.currentTimeMillis() - start ); + final String sitePassword = masterKeyFuture.get().encode( siteName, type, counter ); runOnUiThread( new Runnable() { @Override diff --git a/MasterPassword/Java/masterpassword-cli/src/main/java/com/lyndir/masterpassword/CLI.java b/MasterPassword/Java/masterpassword-cli/src/main/java/com/lyndir/masterpassword/CLI.java index 31042c62..351a5112 100644 --- a/MasterPassword/Java/masterpassword-cli/src/main/java/com/lyndir/masterpassword/CLI.java +++ b/MasterPassword/Java/masterpassword-cli/src/main/java/com/lyndir/masterpassword/CLI.java @@ -13,13 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + + package com.lyndir.masterpassword; +import static com.lyndir.lhunath.opal.system.util.ObjectUtils.ifNotNullElse; + import com.google.common.io.LineReader; import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.util.ConversionUtils; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.util.Arrays; @@ -30,22 +33,24 @@ import java.util.Arrays; */ public class CLI { - static final Logger logger = Logger.get( CLI.class ); - private static final String ENV_USERNAME = "MP_USERNAME"; - private static final String ENV_PASSWORD = "MP_PASSWORD"; + private static final String ENV_USERNAME = "MP_USERNAME"; + private static final String ENV_PASSWORD = "MP_PASSWORD"; + private static final String ENV_SITETYPE = "MP_SITETYPE"; + private static final String ENV_SITECOUNTER = "MP_SITECOUNTER"; public static void main(final String[] args) throws IOException { - String userName, masterPassword, siteName = null; + // Read information from the environment. + String siteName = null; + String userName = System.getenv().get( ENV_USERNAME ); + String masterPassword = System.getenv().get( ENV_PASSWORD ); + String siteTypeName = ifNotNullElse( System.getenv().get( ENV_SITETYPE ), "" ); + MPElementType siteType = siteTypeName.isEmpty()? MPElementType.GeneratedLong: MPElementType.forName( siteTypeName ); + String siteCounterName = ifNotNullElse( System.getenv().get( ENV_SITECOUNTER ), "" ); + int siteCounter = siteCounterName.isEmpty()? 1: Integer.parseInt( siteCounterName ); - /* Environment. */ - userName = System.getenv().get( ENV_USERNAME ); - masterPassword = System.getenv().get( ENV_PASSWORD ); - - /* Arguments. */ - int counter = 1; - MPElementType type = MPElementType.GeneratedLong; + // Parse information from option arguments. boolean typeArg = false, counterArg = false, userNameArg = false; for (final String arg : Arrays.asList( args )) if ("-t".equals( arg ) || "--type".equals( arg )) @@ -58,12 +63,12 @@ public class CLI { System.exit( 0 ); } - type = MPElementType.forName( arg ); + siteType = MPElementType.forName( arg ); typeArg = false; } else if ("-c".equals( arg ) || "--counter".equals( arg )) counterArg = true; else if (counterArg) { - counter = ConversionUtils.toIntegerNN( arg ); + siteCounter = ConversionUtils.toIntegerNN( arg ); counterArg = false; } else if ("-u".equals( arg ) || "--username".equals( arg )) userNameArg = true; @@ -80,12 +85,12 @@ public class CLI { System.out.println( "Available options:" ); System.out.println( "\t-t | --type [site password type]" ); - System.out.format( "\t\tDefault: %s. The password type to use for this site.\n", type.getName() ); + System.out.format( "\t\tDefault: %s. The password type to use for this site.\n", siteType.getName() ); System.out.println( "\t\tUse 'list' to see the available types." ); System.out.println(); System.out.println( "\t-c | --counter [site counter]" ); - System.out.format( "\t\tDefault: %d. The counter to use for this site.\n", counter ); + System.out.format( "\t\tDefault: %d. The counter to use for this site.\n", siteCounter ); System.out.println( "\t\tIncrement the counter if you need a new password." ); System.out.println(); @@ -106,28 +111,33 @@ public class CLI { } else siteName = arg; - InputStreamReader inReader = new InputStreamReader( System.in ); - try { + // Read missing information from the console. + Console console = System.console(); + try (InputStreamReader inReader = new InputStreamReader( System.in )) { LineReader lineReader = new LineReader( inReader ); + if (siteName == null) { System.err.format( "Site name: " ); siteName = lineReader.readLine(); } + if (userName == null) { System.err.format( "User's name: " ); userName = lineReader.readLine(); } - if (masterPassword == null) { - System.err.format( "%s's master password: ", userName ); - masterPassword = lineReader.readLine(); - } - byte[] masterKey = MasterPassword.keyForPassword( masterPassword, userName ); - String sitePassword = MasterPassword.generateContent( type, siteName, masterKey, counter ); - System.out.println( sitePassword ); - } - finally { - inReader.close(); + if (masterPassword == null) { + if (console != null) + masterPassword = new String( console.readPassword( "%s's master password: ", userName ) ); + + else { + System.err.format( "%s's master password: ", userName ); + masterPassword = lineReader.readLine(); + } + } } + + // Encode and write out the site password. + System.out.println( new MasterKey( userName, masterPassword ).encode( siteName, siteType, siteCounter ) ); } } diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/ConfigAuthenticationPanel.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/ConfigAuthenticationPanel.java index a5f3cf71..688a6b52 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/ConfigAuthenticationPanel.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/ConfigAuthenticationPanel.java @@ -89,7 +89,7 @@ public class ConfigAuthenticationPanel extends AuthenticationPanel implements It return selectedUser; } - return new User( selectedUser.getName(), new String( masterPasswordField.getPassword() ) ); + return new User( selectedUser.getUserName(), new String( masterPasswordField.getPassword() ) ); } public String getHelpText() { diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/PasswordFrame.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/PasswordFrame.java index a1554ef9..65f6b85d 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/PasswordFrame.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/PasswordFrame.java @@ -21,7 +21,7 @@ public class PasswordFrame extends JFrame implements DocumentListener { private final JTextField siteNameField; private final JComboBox siteTypeField; private final JSpinner siteCounterField; - private final JLabel passwordLabel; + private final JTextField passwordField; private final JLabel tipLabel; public PasswordFrame(User user) @@ -38,7 +38,7 @@ public class PasswordFrame extends JFrame implements DocumentListener { } ); // User - add( label = new JLabel( strf( "Generating passwords for: %s", user.getName() ) ), BorderLayout.NORTH ); + add( label = new JLabel( strf( "Generating passwords for: %s", user.getUserName() ) ), BorderLayout.NORTH ); label.setFont( Res.exoRegular().deriveFont( 12f ) ); label.setAlignmentX( LEFT_ALIGNMENT ); @@ -74,6 +74,9 @@ public class PasswordFrame extends JFrame implements DocumentListener { SwingUtilities.invokeLater( new Runnable() { @Override public void run() { + passwordField.setText( null ); + siteNameField.setText( null ); + if (getDefaultCloseOperation() == WindowConstants.EXIT_ON_CLOSE) System.exit( 0 ); else @@ -120,16 +123,18 @@ public class PasswordFrame extends JFrame implements DocumentListener { } ); // Password - passwordLabel = new JLabel( " ", JLabel.CENTER ); - passwordLabel.setFont( Res.sourceCodeProBlack().deriveFont( 40f ) ); - passwordLabel.setAlignmentX( Component.CENTER_ALIGNMENT ); + passwordField = new JTextField( " " ); + passwordField.setFont( Res.sourceCodeProBlack().deriveFont( 40f ) ); + passwordField.setHorizontalAlignment( JTextField.CENTER ); + passwordField.setAlignmentX( Component.CENTER_ALIGNMENT ); + passwordField.setEditable( false ); // Tip tipLabel = new JLabel( " ", JLabel.CENTER ); tipLabel.setFont( Res.exoThin().deriveFont( 9f ) ); tipLabel.setAlignmentX( Component.CENTER_ALIGNMENT ); - add( Components.boxLayout( BoxLayout.PAGE_AXIS, passwordLabel, tipLabel ), BorderLayout.SOUTH ); + add( Components.boxLayout( BoxLayout.PAGE_AXIS, passwordField, tipLabel ), BorderLayout.SOUTH ); pack(); setMinimumSize( getSize() ); @@ -146,7 +151,7 @@ public class PasswordFrame extends JFrame implements DocumentListener { final int siteCounter = (Integer) siteCounterField.getValue(); if (siteType.getTypeClass() != MPElementTypeClass.Generated || siteName == null || siteName.isEmpty() || !user.hasKey()) { - passwordLabel.setText( null ); + passwordField.setText( null ); tipLabel.setText( null ); return; } @@ -154,14 +159,17 @@ public class PasswordFrame extends JFrame implements DocumentListener { Res.execute( new Runnable() { @Override public void run() { - final String sitePassword = MasterPassword.generateContent( siteType, siteName, user.getKey(), siteCounter ); + final String sitePassword = user.getKey().encode( siteName, siteType, siteCounter ); if (callback != null) callback.passwordGenerated( siteName, sitePassword ); SwingUtilities.invokeLater( new Runnable() { @Override public void run() { - passwordLabel.setText( sitePassword ); + if (!siteName.equals( siteNameField.getText() )) + return; + + passwordField.setText( sitePassword ); tipLabel.setText( "Press [Enter] to copy the password." ); } } ); diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/UnlockFrame.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/UnlockFrame.java index 068b262c..4c3d796b 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/UnlockFrame.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/UnlockFrame.java @@ -126,7 +126,7 @@ public class UnlockFrame extends JFrame { } boolean checkSignIn() { - boolean enabled = user != null && !user.getName().isEmpty() && user.hasKey(); + boolean enabled = user != null && !user.getUserName().isEmpty() && user.hasKey(); signInButton.setEnabled( enabled ); return enabled; diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/User.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/User.java index 3f6298a2..52a7e1ec 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/User.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/User.java @@ -8,29 +8,29 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*; */ public class User { - private final String name; - private final String masterPassword; - private byte[] key; + private final String userName; + private final String masterPassword; + private MasterKey key; - public User(final String name, final String masterPassword) { - this.name = name; + public User(final String userName, final String masterPassword) { + this.userName = userName; this.masterPassword = masterPassword; } - public String getName() { - return name; + public String getUserName() { + return userName; } public boolean hasKey() { return key != null || (masterPassword != null && !masterPassword.isEmpty()); } - public byte[] getKey() { + public MasterKey getKey() { if (key == null) { if (!hasKey()) { - throw new IllegalStateException( strf( "Master password unknown for user: %s", name ) ); + throw new IllegalStateException( strf( "Master password unknown for user: %s", userName ) ); } else { - key = MasterPassword.keyForPassword( masterPassword, name ); + key = new MasterKey( userName, masterPassword ); } } @@ -39,11 +39,11 @@ public class User { @Override public int hashCode() { - return name.hashCode(); + return userName.hashCode(); } @Override public String toString() { - return name; + return userName; } }