2
0

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.
This commit is contained in:
Maarten Billemont 2014-08-30 20:08:20 -04:00
parent 2adb74c971
commit 9d7799c814
8 changed files with 120 additions and 110 deletions

View File

@ -80,11 +80,9 @@ public enum MPElementType {
*/ */
public static MPElementType forName(final String name) { public static MPElementType forName(final String name) {
for (final MPElementType type : values()) { for (final MPElementType type : values())
if (type.getName().equalsIgnoreCase( name ) || type.getShortName().equalsIgnoreCase( name )) { if (type.getName().equalsIgnoreCase( name ) || type.getShortName().equalsIgnoreCase( name ))
return type; return type;
}
}
throw logger.bug( "Element type not known: %s", name ); throw logger.bug( "Element type not known: %s", name );
} }

View File

@ -2,6 +2,8 @@ package com.lyndir.masterpassword;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.base.Preconditions; 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.google.common.primitives.Bytes;
import com.lambdaworks.crypto.SCrypt; import com.lambdaworks.crypto.SCrypt;
import com.lyndir.lhunath.opal.crypto.CryptUtils; import com.lyndir.lhunath.opal.crypto.CryptUtils;
@ -11,18 +13,17 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.Arrays;
import javax.xml.stream.events.Characters;
/** /**
* Implementation of the Master Password algorithm. * @author lhunath, 2014-08-30
*
* <i>07 04, 2012</i>
*
* @author lhunath
*/ */
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_N = 32768;
private static final int MP_r = 8; private static final int MP_r = 8;
private static final int MP_p = 2; 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 MessageAuthenticationDigests MP_mac = MessageAuthenticationDigests.HmacSHA256;
private static final MPTemplates templates = MPTemplates.load(); 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(); long start = System.currentTimeMillis();
byte[] nusernameLengthBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ) byte[] userNameLengthBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE )
.order( MP_byteOrder ) .order( MP_byteOrder )
.putInt( username.length() ) .putInt( userName.length() )
.array(); .array();
byte[] salt = Bytes.concat( "com.lyndir.masterpassword".getBytes( MP_charset ), // byte[] salt = Bytes.concat( "com.lyndir.masterpassword".getBytes( MP_charset ), //
nusernameLengthBytes, // userNameLengthBytes, userName.getBytes( MP_charset ) );
username.getBytes( MP_charset ) );
try { try {
byte[] key = SCrypt.scrypt( password.getBytes( MP_charset ), salt, MP_N, MP_r, MP_p, MP_dkLen ); key = SCrypt.scrypt( masterPassword.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, valid = true;
CodeUtils.encodeHex( keyIDForKey( key ) ), (double) (System.currentTimeMillis() - start) / 1000 );
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) { catch (GeneralSecurityException e) {
throw logger.bug( 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 )]; byte[] subkey = new byte[Math.min( subkeyLength, key.length )];
System.arraycopy( key, 0, subkey, 0, subkey.length ); System.arraycopy( key, 0, subkey, 0, subkey.length );
return subkey; return subkey;
} }
public static byte[] keyIDForPassword(final String password, final String username) { public String encode(final String name, final MPElementType type, int counter) {
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) {
Preconditions.checkState( valid );
Preconditions.checkArgument( type.getTypeClass() == MPElementTypeClass.Generated ); Preconditions.checkArgument( type.getTypeClass() == MPElementTypeClass.Generated );
Preconditions.checkArgument( !name.isEmpty() ); Preconditions.checkArgument( !name.isEmpty() );
Preconditions.checkArgument( key.length > 0 );
if (counter == 0) if (counter == 0)
counter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300; counter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300;
@ -112,17 +121,9 @@ public abstract class MasterPassword {
return password.toString(); return password.toString();
} }
public static void main(final String... arguments) { public void invalidate() {
String masterPassword = "test-mp"; valid = false;
String username = "test-user"; Arrays.fill( key, (byte) 0 );
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 );
} }
} }

View File

@ -16,7 +16,6 @@ import com.google.common.base.Throwables;
import com.google.common.util.concurrent.*; import com.google.common.util.concurrent.*;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.prefs.Preferences;
public class EmergencyActivity extends Activity { public class EmergencyActivity extends Activity {
@ -38,7 +37,7 @@ public class EmergencyActivity extends Activity {
} }
}; };
private ListenableFuture<byte[]> masterKeyFuture; private ListenableFuture<MasterKey> masterKeyFuture;
@InjectView(R.id.progressView) @InjectView(R.id.progressView)
ProgressBar progressView; ProgressBar progressView;
@ -142,16 +141,12 @@ public class EmergencyActivity extends Activity {
} }
progressView.setVisibility( View.VISIBLE ); progressView.setVisibility( View.VISIBLE );
(masterKeyFuture = executor.submit( new Callable<byte[]>() { (masterKeyFuture = executor.submit( new Callable<MasterKey>() {
@Override @Override
public byte[] call() public MasterKey call()
throws Exception { throws Exception {
try { try {
long start = System.currentTimeMillis(); return new MasterKey( userName, masterPassword );
byte[] masterKey = MasterPassword.keyForPassword( masterPassword, userName );
logger.inf( "masterKey time: %d", System.currentTimeMillis() - start );
return masterKey;
} }
catch (RuntimeException e) { catch (RuntimeException e) {
sitePasswordField.setText( "" ); sitePasswordField.setText( "" );
@ -189,9 +184,7 @@ public class EmergencyActivity extends Activity {
@Override @Override
public void run() { public void run() {
try { try {
long start = System.currentTimeMillis(); final String sitePassword = masterKeyFuture.get().encode( siteName, type, counter );
final String sitePassword = MasterPassword.generateContent( type, siteName, masterKeyFuture.get(), counter );
logger.inf( "sitePassword time: %d", System.currentTimeMillis() - start );
runOnUiThread( new Runnable() { runOnUiThread( new Runnable() {
@Override @Override

View File

@ -13,13 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.lyndir.masterpassword; package com.lyndir.masterpassword;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.ifNotNullElse;
import com.google.common.io.LineReader; import com.google.common.io.LineReader;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ConversionUtils; import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import java.io.IOException; import java.io.*;
import java.io.InputStreamReader;
import java.util.Arrays; import java.util.Arrays;
@ -30,22 +33,24 @@ import java.util.Arrays;
*/ */
public class CLI { public class CLI {
static final Logger logger = Logger.get( CLI.class ); private static final String ENV_USERNAME = "MP_USERNAME";
private static final String ENV_USERNAME = "MP_USERNAME"; private static final String ENV_PASSWORD = "MP_PASSWORD";
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) public static void main(final String[] args)
throws IOException { 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. */ // Parse information from option arguments.
userName = System.getenv().get( ENV_USERNAME );
masterPassword = System.getenv().get( ENV_PASSWORD );
/* Arguments. */
int counter = 1;
MPElementType type = MPElementType.GeneratedLong;
boolean typeArg = false, counterArg = false, userNameArg = false; boolean typeArg = false, counterArg = false, userNameArg = false;
for (final String arg : Arrays.asList( args )) for (final String arg : Arrays.asList( args ))
if ("-t".equals( arg ) || "--type".equals( arg )) if ("-t".equals( arg ) || "--type".equals( arg ))
@ -58,12 +63,12 @@ public class CLI {
System.exit( 0 ); System.exit( 0 );
} }
type = MPElementType.forName( arg ); siteType = MPElementType.forName( arg );
typeArg = false; typeArg = false;
} else if ("-c".equals( arg ) || "--counter".equals( arg )) } else if ("-c".equals( arg ) || "--counter".equals( arg ))
counterArg = true; counterArg = true;
else if (counterArg) { else if (counterArg) {
counter = ConversionUtils.toIntegerNN( arg ); siteCounter = ConversionUtils.toIntegerNN( arg );
counterArg = false; counterArg = false;
} else if ("-u".equals( arg ) || "--username".equals( arg )) } else if ("-u".equals( arg ) || "--username".equals( arg ))
userNameArg = true; userNameArg = true;
@ -80,12 +85,12 @@ public class CLI {
System.out.println( "Available options:" ); System.out.println( "Available options:" );
System.out.println( "\t-t | --type [site password type]" ); 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( "\t\tUse 'list' to see the available types." );
System.out.println(); System.out.println();
System.out.println( "\t-c | --counter [site counter]" ); 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( "\t\tIncrement the counter if you need a new password." );
System.out.println(); System.out.println();
@ -106,28 +111,33 @@ public class CLI {
} else } else
siteName = arg; siteName = arg;
InputStreamReader inReader = new InputStreamReader( System.in ); // Read missing information from the console.
try { Console console = System.console();
try (InputStreamReader inReader = new InputStreamReader( System.in )) {
LineReader lineReader = new LineReader( inReader ); LineReader lineReader = new LineReader( inReader );
if (siteName == null) { if (siteName == null) {
System.err.format( "Site name: " ); System.err.format( "Site name: " );
siteName = lineReader.readLine(); siteName = lineReader.readLine();
} }
if (userName == null) { if (userName == null) {
System.err.format( "User's name: " ); System.err.format( "User's name: " );
userName = lineReader.readLine(); userName = lineReader.readLine();
} }
if (masterPassword == null) {
System.err.format( "%s's master password: ", userName );
masterPassword = lineReader.readLine();
}
byte[] masterKey = MasterPassword.keyForPassword( masterPassword, userName ); if (masterPassword == null) {
String sitePassword = MasterPassword.generateContent( type, siteName, masterKey, counter ); if (console != null)
System.out.println( sitePassword ); masterPassword = new String( console.readPassword( "%s's master password: ", userName ) );
}
finally { else {
inReader.close(); 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 ) );
} }
} }

View File

@ -89,7 +89,7 @@ public class ConfigAuthenticationPanel extends AuthenticationPanel implements It
return selectedUser; return selectedUser;
} }
return new User( selectedUser.getName(), new String( masterPasswordField.getPassword() ) ); return new User( selectedUser.getUserName(), new String( masterPasswordField.getPassword() ) );
} }
public String getHelpText() { public String getHelpText() {

View File

@ -21,7 +21,7 @@ public class PasswordFrame extends JFrame implements DocumentListener {
private final JTextField siteNameField; private final JTextField siteNameField;
private final JComboBox<MPElementType> siteTypeField; private final JComboBox<MPElementType> siteTypeField;
private final JSpinner siteCounterField; private final JSpinner siteCounterField;
private final JLabel passwordLabel; private final JTextField passwordField;
private final JLabel tipLabel; private final JLabel tipLabel;
public PasswordFrame(User user) public PasswordFrame(User user)
@ -38,7 +38,7 @@ public class PasswordFrame extends JFrame implements DocumentListener {
} ); } );
// User // 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.setFont( Res.exoRegular().deriveFont( 12f ) );
label.setAlignmentX( LEFT_ALIGNMENT ); label.setAlignmentX( LEFT_ALIGNMENT );
@ -74,6 +74,9 @@ public class PasswordFrame extends JFrame implements DocumentListener {
SwingUtilities.invokeLater( new Runnable() { SwingUtilities.invokeLater( new Runnable() {
@Override @Override
public void run() { public void run() {
passwordField.setText( null );
siteNameField.setText( null );
if (getDefaultCloseOperation() == WindowConstants.EXIT_ON_CLOSE) if (getDefaultCloseOperation() == WindowConstants.EXIT_ON_CLOSE)
System.exit( 0 ); System.exit( 0 );
else else
@ -120,16 +123,18 @@ public class PasswordFrame extends JFrame implements DocumentListener {
} ); } );
// Password // Password
passwordLabel = new JLabel( " ", JLabel.CENTER ); passwordField = new JTextField( " " );
passwordLabel.setFont( Res.sourceCodeProBlack().deriveFont( 40f ) ); passwordField.setFont( Res.sourceCodeProBlack().deriveFont( 40f ) );
passwordLabel.setAlignmentX( Component.CENTER_ALIGNMENT ); passwordField.setHorizontalAlignment( JTextField.CENTER );
passwordField.setAlignmentX( Component.CENTER_ALIGNMENT );
passwordField.setEditable( false );
// Tip // Tip
tipLabel = new JLabel( " ", JLabel.CENTER ); tipLabel = new JLabel( " ", JLabel.CENTER );
tipLabel.setFont( Res.exoThin().deriveFont( 9f ) ); tipLabel.setFont( Res.exoThin().deriveFont( 9f ) );
tipLabel.setAlignmentX( Component.CENTER_ALIGNMENT ); 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(); pack();
setMinimumSize( getSize() ); setMinimumSize( getSize() );
@ -146,7 +151,7 @@ public class PasswordFrame extends JFrame implements DocumentListener {
final int siteCounter = (Integer) siteCounterField.getValue(); final int siteCounter = (Integer) siteCounterField.getValue();
if (siteType.getTypeClass() != MPElementTypeClass.Generated || siteName == null || siteName.isEmpty() || !user.hasKey()) { if (siteType.getTypeClass() != MPElementTypeClass.Generated || siteName == null || siteName.isEmpty() || !user.hasKey()) {
passwordLabel.setText( null ); passwordField.setText( null );
tipLabel.setText( null ); tipLabel.setText( null );
return; return;
} }
@ -154,14 +159,17 @@ public class PasswordFrame extends JFrame implements DocumentListener {
Res.execute( new Runnable() { Res.execute( new Runnable() {
@Override @Override
public void run() { 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) if (callback != null)
callback.passwordGenerated( siteName, sitePassword ); callback.passwordGenerated( siteName, sitePassword );
SwingUtilities.invokeLater( new Runnable() { SwingUtilities.invokeLater( new Runnable() {
@Override @Override
public void run() { public void run() {
passwordLabel.setText( sitePassword ); if (!siteName.equals( siteNameField.getText() ))
return;
passwordField.setText( sitePassword );
tipLabel.setText( "Press [Enter] to copy the password." ); tipLabel.setText( "Press [Enter] to copy the password." );
} }
} ); } );

View File

@ -126,7 +126,7 @@ public class UnlockFrame extends JFrame {
} }
boolean checkSignIn() { boolean checkSignIn() {
boolean enabled = user != null && !user.getName().isEmpty() && user.hasKey(); boolean enabled = user != null && !user.getUserName().isEmpty() && user.hasKey();
signInButton.setEnabled( enabled ); signInButton.setEnabled( enabled );
return enabled; return enabled;

View File

@ -8,29 +8,29 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
*/ */
public class User { public class User {
private final String name; private final String userName;
private final String masterPassword; private final String masterPassword;
private byte[] key; private MasterKey key;
public User(final String name, final String masterPassword) { public User(final String userName, final String masterPassword) {
this.name = name; this.userName = userName;
this.masterPassword = masterPassword; this.masterPassword = masterPassword;
} }
public String getName() { public String getUserName() {
return name; return userName;
} }
public boolean hasKey() { public boolean hasKey() {
return key != null || (masterPassword != null && !masterPassword.isEmpty()); return key != null || (masterPassword != null && !masterPassword.isEmpty());
} }
public byte[] getKey() { public MasterKey getKey() {
if (key == null) { if (key == null) {
if (!hasKey()) { 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 { } else {
key = MasterPassword.keyForPassword( masterPassword, name ); key = new MasterKey( userName, masterPassword );
} }
} }
@ -39,11 +39,11 @@ public class User {
@Override @Override
public int hashCode() { public int hashCode() {
return name.hashCode(); return userName.hashCode();
} }
@Override @Override
public String toString() { public String toString() {
return name; return userName;
} }
} }