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) {
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 );
}

View File

@ -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.
*
* <i>07 04, 2012</i>
*
* @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 )
byte[] userNameLengthBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE )
.order( MP_byteOrder )
.putInt( username.length() )
.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 );
}
}

View File

@ -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<byte[]> masterKeyFuture;
private ListenableFuture<MasterKey> 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<byte[]>() {
(masterKeyFuture = executor.submit( new Callable<MasterKey>() {
@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

View File

@ -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_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) {
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();
}
}
}
byte[] masterKey = MasterPassword.keyForPassword( masterPassword, userName );
String sitePassword = MasterPassword.generateContent( type, siteName, masterKey, counter );
System.out.println( sitePassword );
}
finally {
inReader.close();
}
// 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 new User( selectedUser.getName(), new String( masterPasswordField.getPassword() ) );
return new User( selectedUser.getUserName(), new String( masterPasswordField.getPassword() ) );
}
public String getHelpText() {

View File

@ -21,7 +21,7 @@ public class PasswordFrame extends JFrame implements DocumentListener {
private final JTextField siteNameField;
private final JComboBox<MPElementType> 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." );
}
} );

View File

@ -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;

View File

@ -8,29 +8,29 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
*/
public class User {
private final String name;
private final String userName;
private final String masterPassword;
private byte[] key;
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;
}
}