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:
parent
2adb74c971
commit
9d7799c814
@ -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 );
|
||||
}
|
||||
|
@ -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 )
|
||||
.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 );
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 ) );
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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." );
|
||||
}
|
||||
} );
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user