2
0

Remove plist dependency, fix length bug, import ciphers.plist.

[REMOVED]   Java code no longer depends on ciphers.plist and net.sf.plist.
[ADDED]     Java code now explicitly defines the algorithm's templates.
[FIXED]     Java code now properly counts the site name and user name's byte length.
[FIXED]     Java code now explicitly uses 32-bit integers.
This commit is contained in:
Maarten Billemont 2014-12-03 00:42:19 -05:00
parent ae08cb62c5
commit a82ce7310d
12 changed files with 380 additions and 241 deletions

View File

@ -33,17 +33,24 @@
</dependency> </dependency>
<!-- EXTERNAL DEPENDENCIES --> <!-- EXTERNAL DEPENDENCIES -->
<dependency>
<groupId>net.sf.plist</groupId>
<artifactId>property-list</artifactId>
<version>2.0.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.lambdaworks</groupId> <groupId>com.lambdaworks</groupId>
<artifactId>scrypt</artifactId> <artifactId>scrypt</artifactId>
<version>1.4.0</version> <version>1.4.0</version>
</dependency> </dependency>
<!-- TESTING -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -3,7 +3,9 @@ package com.lyndir.masterpassword;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import java.util.List;
import java.util.Set; import java.util.Set;
import javax.annotation.Generated;
/** /**
@ -13,33 +15,72 @@ import java.util.Set;
*/ */
public enum MPElementType { public enum MPElementType {
GeneratedMaximum( "Maximum Security Password", "Maximum", "20 characters, contains symbols.", MPElementTypeClass.Generated ), GeneratedMaximum( "20 characters, contains symbols.", //
GeneratedLong( "Long Password", "Long", "Copy-friendly, 14 characters, contains symbols.", MPElementTypeClass.Generated ), ImmutableList.of( "x", "max", "maximum" ), MPElementTypeClass.Generated, //
GeneratedMedium( "Medium Password", "Medium", "Copy-friendly, 8 characters, contains symbols.", MPElementTypeClass.Generated ), ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ), new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ) ),
GeneratedBasic( "Basic Password", "Basic", "8 characters, no symbols.", MPElementTypeClass.Generated ),
GeneratedShort( "Short Password", "Short", "Copy-friendly, 4 characters, no symbols.", MPElementTypeClass.Generated ),
GeneratedPIN( "PIN", "PIN", "4 numbers.", MPElementTypeClass.Generated ),
StoredPersonal( "Personal Password", "Personal", "AES-encrypted, exportable.", MPElementTypeClass.Stored, GeneratedLong( "Copy-friendly, 14 characters, contains symbols.", //
MPElementFeature.ExportContent ), ImmutableList.of( "l", "long" ), MPElementTypeClass.Generated, //
StoredDevicePrivate( "Device Private Password", "Private", "AES-encrypted, not exported.", MPElementTypeClass.Stored, ImmutableList.of( new MPTemplate( "CvcvnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvnoCvcv" ),
MPElementFeature.DevicePrivate ); new MPTemplate( "CvcvCvcvCvcvno" ), new MPTemplate( "CvccnoCvcvCvcv" ),
new MPTemplate( "CvccCvcvnoCvcv" ), new MPTemplate( "CvccCvcvCvcvno" ),
new MPTemplate( "CvcvnoCvccCvcv" ), new MPTemplate( "CvcvCvccnoCvcv" ),
new MPTemplate( "CvcvCvccCvcvno" ), new MPTemplate( "CvcvnoCvcvCvcc" ),
new MPTemplate( "CvcvCvcvnoCvcc" ), new MPTemplate( "CvcvCvcvCvccno" ),
new MPTemplate( "CvccnoCvccCvcv" ), new MPTemplate( "CvccCvccnoCvcv" ),
new MPTemplate( "CvccCvccCvcvno" ), new MPTemplate( "CvcvnoCvccCvcc" ),
new MPTemplate( "CvcvCvccnoCvcc" ), new MPTemplate( "CvcvCvccCvccno" ),
new MPTemplate( "CvccnoCvcvCvcc" ), new MPTemplate( "CvccCvcvnoCvcc" ),
new MPTemplate( "CvccCvcvCvccno" ) ) ),
GeneratedMedium( "Copy-friendly, 8 characters, contains symbols.", //
ImmutableList.of( "m", "med", "medium" ), MPElementTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "CvcnoCvc" ), new MPTemplate( "CvcCvcno" ) ) ),
GeneratedBasic( "8 characters, no symbols.", //
ImmutableList.of( "b", "basic" ), MPElementTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "aaanaaan" ), new MPTemplate( "aannaaan" ), new MPTemplate( "aaannaaa" ) ) ),
GeneratedShort( "Copy-friendly, 4 characters, no symbols.", //
ImmutableList.of( "s", "short" ), MPElementTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "Cvcn" ) ) ),
GeneratedPIN( "4 numbers.", //
ImmutableList.of( "i", "pin" ), MPElementTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "nnnn" ) ) ),
GeneratedName( "9 letter name.", //
ImmutableList.of( "n", "name" ), MPElementTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ) ),
GeneratedPhrase( "20 character sentence.", //
ImmutableList.of( "p", "phrase" ), MPElementTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "cvcc cvc cvccvcv cvc" ), new MPTemplate( "cvc cvccvcvcv cvcv" ),
new MPTemplate( "cv cvccv cvc cvcvccv" ) ) ),
StoredPersonal( "AES-encrypted, exportable.", //
ImmutableList.of( "personal" ), MPElementTypeClass.Stored, //
ImmutableList.<MPTemplate>of(), MPElementFeature.ExportContent ),
StoredDevicePrivate( "AES-encrypted, not exported.", //
ImmutableList.of( "device" ), MPElementTypeClass.Stored, //
ImmutableList.<MPTemplate>of(), MPElementFeature.DevicePrivate );
static final Logger logger = Logger.get( MPElementType.class ); static final Logger logger = Logger.get( MPElementType.class );
private final MPElementTypeClass typeClass;
private final Set<MPElementFeature> typeFeatures;
private final String name;
private final String shortName;
private final String description; private final String description;
private final List<String> options;
private final MPElementTypeClass typeClass;
private final List<MPTemplate> templates;
private final Set<MPElementFeature> typeFeatures;
MPElementType(final String name, final String shortName, final String description, final MPElementTypeClass typeClass, MPElementType(final String description, final List<String> options, final MPElementTypeClass typeClass,
final MPElementFeature... typeFeatures) { final List<MPTemplate> templates, final MPElementFeature... typeFeatures) {
this.name = name;
this.shortName = shortName;
this.typeClass = typeClass;
this.description = description; this.description = description;
this.options = options;
this.typeClass = typeClass;
this.templates = templates;
ImmutableSet.Builder<MPElementFeature> typeFeaturesBuilder = ImmutableSet.builder(); ImmutableSet.Builder<MPElementFeature> typeFeaturesBuilder = ImmutableSet.builder();
for (final MPElementFeature typeFeature : typeFeatures) { for (final MPElementFeature typeFeature : typeFeatures) {
@ -48,6 +89,15 @@ public enum MPElementType {
this.typeFeatures = typeFeaturesBuilder.build(); this.typeFeatures = typeFeaturesBuilder.build();
} }
public String getDescription() {
return description;
}
public List<String> getOptions() {
return options;
}
public MPElementTypeClass getTypeClass() { public MPElementTypeClass getTypeClass() {
return typeClass; return typeClass;
@ -58,33 +108,18 @@ public enum MPElementType {
return typeFeatures; return typeFeatures;
} }
public String getName() {
return name;
}
public String getShortName() {
return shortName;
}
public String getDescription() {
return description;
}
/** /**
* @param name The full or short name of the type we want to look up. It is matched case insensitively. * @param option The option to select a type with. It is matched case insensitively.
* *
* @return The type with the given name. * @return The type registered for the given option.
*/ */
public static MPElementType forName(final String name) { public static MPElementType forOption(final String option) {
for (final MPElementType type : values()) for (final MPElementType type : values())
if (type.getName().equalsIgnoreCase( name ) || type.getShortName().equalsIgnoreCase( name )) if (type.getOptions().contains( option.toLowerCase() ))
return type; return type;
throw logger.bug( "Element type not known: %s", name ); throw logger.bug( "No type for option: %s", option );
} }
/** /**
@ -102,4 +137,7 @@ public enum MPElementType {
return types.build(); return types.build();
} }
public MPTemplate getTemplateAtRollingIndex(final int templateIndex) {
return templates.get( templateIndex % templates.size() );
}
} }

View File

@ -0,0 +1,63 @@
package com.lyndir.masterpassword;
import com.google.common.collect.ImmutableList;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.util.List;
/**
* @author lhunath, 14-12-02
*/
public enum MPElementVariant {
Password( "The password to log in with.", "Doesn't currently use a context.", //
ImmutableList.of( "p", "password" ), "com.lyndir.masterpassword" ),
Login( "The username to log in as.", "Doesn't currently use a context.", //
ImmutableList.of( "l", "login" ), "com.lyndir.masterpassword.login" ),
Answer( "The answer to a security question.", "Empty for a universal site answer or\nthe most significant word(s) of the question.", //
ImmutableList.of( "a", "answer" ), "com.lyndir.masterpassword.answer" );
static final Logger logger = Logger.get( MPElementType.class );
private final String description;
private final String contextDescription;
private final List<String> options;
private final String scope;
MPElementVariant(final String description, final String contextDescription, final List<String> options, final String scope) {
this.contextDescription = contextDescription;
this.options = options;
this.description = description;
this.scope = scope;
}
public String getDescription() {
return description;
}
public String getContextDescription() {
return contextDescription;
}
public List<String> getOptions() {
return options;
}
public String getScope() {
return scope;
}
/**
* @param option The option to select a variant with. It is matched case insensitively.
*
* @return The variant registered for the given option.
*/
public static MPElementVariant forOption(final String option) {
for (final MPElementVariant variant : values())
if (variant.getOptions().contains( option.toLowerCase() ))
return variant;
throw logger.bug( "No variant for option: %s", option );
}
}

View File

@ -1,6 +1,7 @@
package com.lyndir.masterpassword; package com.lyndir.masterpassword;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.lyndir.lhunath.opal.system.util.MetaObject; import com.lyndir.lhunath.opal.system.util.MetaObject;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -15,20 +16,15 @@ public class MPTemplate extends MetaObject {
private final List<MPTemplateCharacterClass> template; private final List<MPTemplateCharacterClass> template;
public MPTemplate(final String template, final Map<Character, MPTemplateCharacterClass> characterClasses) { MPTemplate(final String template) {
ImmutableList.Builder<MPTemplateCharacterClass> builder = ImmutableList.<MPTemplateCharacterClass>builder(); ImmutableList.Builder<MPTemplateCharacterClass> builder = ImmutableList.builder();
for (int i = 0; i < template.length(); ++i) for (int i = 0; i < template.length(); ++i)
builder.add( characterClasses.get( template.charAt( i ) ) ); builder.add( MPTemplateCharacterClass.forIdentifier( template.charAt( i ) ) );
this.template = builder.build(); this.template = builder.build();
} }
public MPTemplate(final List<MPTemplateCharacterClass> template) {
this.template = template;
}
public MPTemplateCharacterClass getCharacterClassAtIndex(final int index) { public MPTemplateCharacterClass getCharacterClassAtIndex(final int index) {
return template.get( index ); return template.get( index );

View File

@ -1,5 +1,6 @@
package com.lyndir.masterpassword; package com.lyndir.masterpassword;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.MetaObject; import com.lyndir.lhunath.opal.system.util.MetaObject;
import com.lyndir.lhunath.opal.system.util.ObjectMeta; import com.lyndir.lhunath.opal.system.util.ObjectMeta;
@ -9,16 +10,29 @@ import com.lyndir.lhunath.opal.system.util.ObjectMeta;
* *
* @author lhunath * @author lhunath
*/ */
public class MPTemplateCharacterClass extends MetaObject { public enum MPTemplateCharacterClass {
UpperVowel( 'V', "AEIOU" ),
UpperConsonant( 'C', "BCDFGHJKLMNPQRSTVWXYZ" ),
LowerVowel( 'v', "aeiou" ),
LowerConsonant( 'c', "bcdfghjklmnpqrstvwxyz" ),
UpperAlphanumeric( 'A', "AEIOUBCDFGHJKLMNPQRSTVWXYZ" ),
Alphanumeric( 'a', "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz" ),
Numeric( 'n', "0123456789" ),
Other( 'o', "@&%?,=[]_:-+*$#!'^~;()/." ),
Any( 'x', "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz0123456789!@#$%^&*()" ),
Space( ' ', " " );
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPTemplateCharacterClass.class );
private final char identifier; private final char identifier;
@ObjectMeta(useFor = { })
private final char[] characters; private final char[] characters;
public MPTemplateCharacterClass(final char identifier, final char[] characters) { MPTemplateCharacterClass(final char identifier, final String characters) {
this.identifier = identifier; this.identifier = identifier;
this.characters = characters; this.characters = characters.toCharArray();
} }
public char getIdentifier() { public char getIdentifier() {
@ -30,4 +44,12 @@ public class MPTemplateCharacterClass extends MetaObject {
return characters[index % characters.length]; return characters[index % characters.length];
} }
public static MPTemplateCharacterClass forIdentifier(final char identifier) {
for (MPTemplateCharacterClass characterClass : values())
if (characterClass.getIdentifier() == identifier)
return characterClass;
throw logger.bug( "No character class defined for identifier: %s", identifier );
}
} }

View File

@ -1,109 +0,0 @@
package com.lyndir.masterpassword;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Closeables;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.MetaObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import net.sf.plist.*;
import net.sf.plist.io.PropertyListException;
import net.sf.plist.io.PropertyListParser;
/**
* <i>07 04, 2012</i>
*
* @author lhunath
*/
public class MPTemplates extends MetaObject {
static final Logger logger = Logger.get( MPTemplates.class );
private final Map<MPElementType, List<MPTemplate>> templates;
public MPTemplates(final Map<MPElementType, List<MPTemplate>> templates) {
this.templates = templates;
}
public static MPTemplates load() {
return loadFromPList( "ciphers.plist" );
}
public static MPTemplates loadFromPList(final String templateResource) {
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
InputStream templateStream = Thread.currentThread().getContextClassLoader().getResourceAsStream( templateResource );
Preconditions.checkNotNull( templateStream, "Not found: %s", templateResource );
try {
NSObject plistObject = PropertyListParser.parse( templateStream );
Preconditions.checkState( NSDictionary.class.isAssignableFrom( plistObject.getClass() ) );
NSDictionary plist = (NSDictionary) plistObject;
NSDictionary characterClassesDict = (NSDictionary) plist.get( "MPCharacterClasses" );
NSDictionary templatesDict = (NSDictionary) plist.get( "MPElementGeneratedEntity" );
ImmutableMap.Builder<Character, MPTemplateCharacterClass> characterClassesBuilder = ImmutableMap.builder();
for (final Map.Entry<String, NSObject> characterClassEntry : characterClassesDict.entrySet()) {
String key = characterClassEntry.getKey();
NSObject value = characterClassEntry.getValue();
Preconditions.checkState( key.length() == 1 );
Preconditions.checkState( NSString.class.isAssignableFrom( value.getClass() ));
char character = key.charAt( 0 );
char[] characterClass = ((NSString)value).getValue().toCharArray();
characterClassesBuilder.put( character, new MPTemplateCharacterClass( character, characterClass ) );
}
ImmutableMap<Character, MPTemplateCharacterClass> characterClasses = characterClassesBuilder.build();
ImmutableMap.Builder<MPElementType, List<MPTemplate>> templatesBuilder = ImmutableMap.builder();
for (final Map.Entry<String, NSObject> template : templatesDict.entrySet()) {
String key = template.getKey();
NSObject value = template.getValue();
Preconditions.checkState( NSArray.class.isAssignableFrom( value.getClass() ) );
MPElementType type = MPElementType.forName( key );
List<NSObject> templateStrings = ((NSArray) value).getValue();
ImmutableList.Builder<MPTemplate> typeTemplatesBuilder = ImmutableList.<MPTemplate>builder();
for (final NSObject templateString : templateStrings)
typeTemplatesBuilder.add( new MPTemplate( ((NSString) templateString).getValue(), characterClasses ) );
templatesBuilder.put( type, typeTemplatesBuilder.build() );
}
ImmutableMap<MPElementType, List<MPTemplate>> templates = templatesBuilder.build();
return new MPTemplates( templates );
}
catch (PropertyListException e) {
logger.err( e, "Could not parse templates from: %s", templateResource );
throw Throwables.propagate( e );
}
catch (IOException e) {
logger.err( e, "Could not read templates from: %s", templateResource );
throw Throwables.propagate( e );
}
finally {
Closeables.closeQuietly( templateStream );
}
}
public MPTemplate getTemplateForTypeAtRollingIndex(final MPElementType type, final int templateIndex) {
List<MPTemplate> typeTemplates = templates.get( type );
return typeTemplates.get( templateIndex % typeTemplates.size() );
}
public static void main(final String... arguments) {
load();
}
}

View File

@ -2,8 +2,6 @@ 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;
@ -14,7 +12,6 @@ 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 java.util.Arrays;
import javax.xml.stream.events.Characters;
/** /**
@ -28,11 +25,11 @@ public class MasterKey {
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;
private static final int MP_dkLen = 64; private static final int MP_dkLen = 64;
private static final int MP_intLen = 32;
private static final Charset MP_charset = Charsets.UTF_8; private static final Charset MP_charset = Charsets.UTF_8;
private static final ByteOrder MP_byteOrder = ByteOrder.BIG_ENDIAN; private static final ByteOrder MP_byteOrder = ByteOrder.BIG_ENDIAN;
private static final MessageDigests MP_hash = MessageDigests.SHA256; private static final MessageDigests MP_hash = MessageDigests.SHA256;
private static final MessageAuthenticationDigests MP_mac = MessageAuthenticationDigests.HmacSHA256; private static final MessageAuthenticationDigests MP_mac = MessageAuthenticationDigests.HmacSHA256;
private static final MPTemplates templates = MPTemplates.load();
private final String userName; private final String userName;
private final byte[] key; private final byte[] key;
@ -44,12 +41,13 @@ public class MasterKey {
this.userName = userName; this.userName = userName;
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
byte[] userNameLengthBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ) byte[] userNameBytes = userName.getBytes( MP_charset );
byte[] userNameLengthBytes = ByteBuffer.allocate( MP_intLen / Byte.SIZE )
.order( MP_byteOrder ) .order( MP_byteOrder )
.putInt( userName.length() ) .putInt( userNameBytes.length )
.array(); .array();
byte[] salt = Bytes.concat( "com.lyndir.masterpassword".getBytes( MP_charset ), // byte[] salt = Bytes.concat( MPElementVariant.Password.getScope().getBytes( MP_charset ), //
userNameLengthBytes, userName.getBytes( MP_charset ) ); userNameLengthBytes, userNameBytes );
try { try {
key = SCrypt.scrypt( masterPassword.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 );
@ -83,7 +81,7 @@ public class MasterKey {
return subkey; return subkey;
} }
public String encode(final String name, final MPElementType type, int counter) { public String encode(final String name, final MPElementType type, int counter, final MPElementVariant variant, final String context) {
Preconditions.checkState( valid ); Preconditions.checkState( valid );
Preconditions.checkArgument( type.getTypeClass() == MPElementTypeClass.Generated ); Preconditions.checkArgument( type.getTypeClass() == MPElementTypeClass.Generated );
@ -92,19 +90,20 @@ public class MasterKey {
if (counter == 0) if (counter == 0)
counter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300; counter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300;
byte[] nameLengthBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( MP_byteOrder ).putInt( name.length() ).array(); byte[] nameBytes = name.getBytes( MP_charset );
byte[] counterBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( MP_byteOrder ).putInt( counter ).array(); byte[] nameLengthBytes = ByteBuffer.allocate( MP_intLen / Byte.SIZE ).order( MP_byteOrder ).putInt( nameBytes.length ).array();
logger.trc( "seed from: hmac-sha256(%s, 'com.lyndir.masterpassword' | %s | %s | %s)", CryptUtils.encodeBase64( key ), byte[] counterBytes = ByteBuffer.allocate( MP_intLen / Byte.SIZE ).order( MP_byteOrder ).putInt( counter ).array();
logger.trc( "seed from: hmac-sha256(%s, %s | %s | %s | %s)", variant.getScope(), CryptUtils.encodeBase64( key ),
CodeUtils.encodeHex( nameLengthBytes ), name, CodeUtils.encodeHex( counterBytes ) ); CodeUtils.encodeHex( nameLengthBytes ), name, CodeUtils.encodeHex( counterBytes ) );
byte[] seed = MP_mac.of( key, Bytes.concat( "com.lyndir.masterpassword".getBytes( MP_charset ), // byte[] seed = MP_mac.of( key, Bytes.concat( variant.getScope().getBytes( MP_charset ), //
nameLengthBytes, // nameLengthBytes, //
name.getBytes( MP_charset ), // nameBytes, //
counterBytes ) ); counterBytes ) );
logger.trc( "seed is: %s", CryptUtils.encodeBase64( seed ) ); logger.trc( "seed is: %s", CryptUtils.encodeBase64( seed ) );
Preconditions.checkState( seed.length > 0 ); Preconditions.checkState( seed.length > 0 );
int templateIndex = seed[0] & 0xFF; // Mask the integer's sign. int templateIndex = seed[0] & 0xFF; // Mask the integer's sign.
MPTemplate template = templates.getTemplateForTypeAtRollingIndex( type, templateIndex ); MPTemplate template = type.getTemplateAtRollingIndex( templateIndex );
logger.trc( "type: %s, template: %s", type, template ); logger.trc( "type: %s, template: %s", type, template );
StringBuilder password = new StringBuilder( template.length() ); StringBuilder password = new StringBuilder( template.length() );

View File

@ -1 +0,0 @@
../../../../../../MasterPassword/Resources/Data/ciphers.plist

View File

@ -0,0 +1,79 @@
package com.lyndir.masterpassword;
import static org.testng.Assert.*;
import org.testng.annotations.Test;
public class MasterKeyTest {
private static final String FULL_NAME = "Robert Lee Mitchell";
private static final String MASTER_PASSWORD = "banana colored duckling";
private static final String SITE_NAME = "masterpasswordapp.com";
@Test
public void testEncode()
throws Exception {
MasterKey masterKey = new MasterKey( FULL_NAME, MASTER_PASSWORD );
assertEquals( masterKey.encode( SITE_NAME, MPElementType.GeneratedLong, 1, MPElementVariant.Password, null ), //
"Jejr5[RepuSosp" );
assertEquals( masterKey.encode( "\u26C4", MPElementType.GeneratedMaximum, 1, MPElementVariant.Password, null ), //
"b9]1#2g*suJ^E@OJXZTQ" );
assertEquals( masterKey.encode( "\u26C4", MPElementType.GeneratedLong, 1, MPElementVariant.Password, null ), //
"LiheCuwhSerz6)" );
assertEquals( masterKey.encode( "\u26C4", MPElementType.GeneratedMedium, 1, MPElementVariant.Password, null ), //
"Xep8'Cav" );
assertEquals( masterKey.encode( "\u26C4", MPElementType.GeneratedBasic, 1, MPElementVariant.Password, null ), //
"bpW62jmW" );
assertEquals( masterKey.encode( "\u26C4", MPElementType.GeneratedShort, 1, MPElementVariant.Password, null ), //
"Puw2" );
assertEquals( masterKey.encode( "\u26C4", MPElementType.GeneratedPIN, 1, MPElementVariant.Password, null ), //
"3258" );
assertEquals( masterKey.encode( "\u26C4", MPElementType.GeneratedName, 1, MPElementVariant.Password, null ), //
"cujtebona" );
assertEquals( masterKey.encode( "\u26C4", MPElementType.GeneratedPhrase, 1, MPElementVariant.Password, null ), //
"ri durzu jid kalebho" );
assertEquals( masterKey.encode( "\u26C4", MPElementType.GeneratedMaximum, (int) (1L << 32 - 1), MPElementVariant.Password, null ),
"y4=s&D6)ao(xcBS)AgBT" );
}
@Test
public void testGetUserName()
throws Exception {
assertEquals( new MasterKey( FULL_NAME, "banana colored duckling" ).getUserName(), FULL_NAME );
}
@Test
public void testGetKeyID()
throws Exception {
assertEquals( new MasterKey( FULL_NAME, "banana colored duckling" ).getKeyID(),
"98EEF4D1DF46D849574A82A03C3177056B15DFFCA29BB3899DE4628453675302" );
}
@Test
public void testInvalidate()
throws Exception {
try {
MasterKey masterKey = new MasterKey( FULL_NAME, MASTER_PASSWORD );
masterKey.invalidate();
masterKey.encode( SITE_NAME, MPElementType.GeneratedLong, 1, MPElementVariant.Password, null );
assertFalse( true, "Master key was not invalidated." );
}
catch (IllegalStateException ignored) {
}
}
}

View File

@ -184,7 +184,7 @@ public class EmergencyActivity extends Activity {
@Override @Override
public void run() { public void run() {
try { try {
final String sitePassword = masterKeyFuture.get().encode( siteName, type, counter ); final String sitePassword = masterKeyFuture.get().encode( siteName, type, counter, variant, context );
runOnUiThread( new Runnable() { runOnUiThread( new Runnable() {
@Override @Override

View File

@ -18,12 +18,16 @@
package com.lyndir.masterpassword; package com.lyndir.masterpassword;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.ifNotNullElse; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.ifNotNullElse;
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
import com.google.common.base.Joiner;
import com.google.common.collect.Maps;
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.util.ConversionUtils; import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import com.lyndir.lhunath.opal.system.util.StringUtils;
import java.io.*; import java.io.*;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map;
/** /**
@ -34,7 +38,6 @@ import java.util.Arrays;
public class CLI { public class CLI {
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_SITETYPE = "MP_SITETYPE"; private static final String ENV_SITETYPE = "MP_SITETYPE";
private static final String ENV_SITECOUNTER = "MP_SITECOUNTER"; private static final String ENV_SITECOUNTER = "MP_SITECOUNTER";
@ -42,71 +45,115 @@ public class CLI {
throws IOException { throws IOException {
// Read information from the environment. // Read information from the environment.
String siteName = null; String siteName = null, masterPassword, context = null;
String userName = System.getenv().get( ENV_USERNAME ); String userName = System.getenv( ENV_USERNAME );
String masterPassword = System.getenv().get( ENV_PASSWORD ); String siteTypeName = ifNotNullElse( System.getenv( ENV_SITETYPE ), "" );
String siteTypeName = ifNotNullElse( System.getenv().get( ENV_SITETYPE ), "" ); MPElementType siteType = siteTypeName.isEmpty()? MPElementType.GeneratedLong: MPElementType.forOption( siteTypeName );
MPElementType siteType = siteTypeName.isEmpty()? MPElementType.GeneratedLong: MPElementType.forName( siteTypeName ); MPElementVariant variant = MPElementVariant.Password;
String siteCounterName = ifNotNullElse( System.getenv().get( ENV_SITECOUNTER ), "" ); String siteCounterName = ifNotNullElse( System.getenv( ENV_SITECOUNTER ), "" );
int siteCounter = siteCounterName.isEmpty()? 1: Integer.parseInt( siteCounterName ); int siteCounter = siteCounterName.isEmpty()? 1: Integer.parseInt( siteCounterName );
// Parse information from option arguments. // Parse information from option arguments.
boolean typeArg = false, counterArg = false, userNameArg = false; boolean userNameArg = false, typeArg = false, counterArg = false, variantArg = false, contextArg = false;
for (final String arg : Arrays.asList( args )) for (final String arg : Arrays.asList( args ))
if ("-t".equals( arg ) || "--type".equals( arg )) // Full Name
typeArg = true; if ("-u".equals( arg ) || "--username".equals( arg ))
else if (typeArg) {
if ("list".equalsIgnoreCase( arg )) {
System.out.format( "%30s | %s\n", "type", "description" );
for (final MPElementType aType : MPElementType.values())
System.out.format( "%30s | %s\n", aType.getName(), aType.getDescription() );
System.exit( 0 );
}
siteType = MPElementType.forName( arg );
typeArg = false;
} else if ("-c".equals( arg ) || "--counter".equals( arg ))
counterArg = true;
else if (counterArg) {
siteCounter = ConversionUtils.toIntegerNN( arg );
counterArg = false;
} else if ("-u".equals( arg ) || "--username".equals( arg ))
userNameArg = true; userNameArg = true;
else if (userNameArg) { else if (userNameArg) {
userName = arg; userName = arg;
userNameArg = false; userNameArg = false;
} else if ("-h".equals( arg ) || "--help".equals( arg )) { }
// Type
else if ("-t".equals( arg ) || "--type".equals( arg ))
typeArg = true;
else if (typeArg) {
siteType = MPElementType.forOption( arg );
typeArg = false;
}
// Counter
else if ("-c".equals( arg ) || "--counter".equals( arg ))
counterArg = true;
else if (counterArg) {
siteCounter = ConversionUtils.toIntegerNN( arg );
counterArg = false;
}
// Variant
else if ("-v".equals( arg ) || "--variant".equals( arg ))
variantArg = true;
else if (variantArg) {
variant = MPElementVariant.forOption( arg );
variantArg = false;
}
// Context
else if ("-C".equals( arg ) || "--context".equals( arg ))
contextArg = true;
else if (contextArg) {
context = arg;
contextArg = false;
}
// Help
else if ("-h".equals( arg ) || "--help".equals( arg )) {
System.out.println(); System.out.println();
System.out.println( "\tMaster Password CLI" ); System.out.format( "Usage: mpw [-u name] [-t type] [-c counter] site\n\n" );
System.out.println( "\t\tLyndir" ); System.out.format( " -u name Specify the full name of the user.\n" );
System.out.format( " Defaults to %s in env.\n\n", ENV_USERNAME );
System.out.format( " -t type Specify the password's template.\n" );
System.out.format( " Defaults to %s in env or 'long' for password, 'name' for login.\n", ENV_SITETYPE );
System.out.println( "[options] [site name]" ); int optionsLength = 0;
Map<String, MPElementType> typeMap = Maps.newLinkedHashMap();
for (MPElementType elementType : MPElementType.values()) {
String options = Joiner.on( ", " ).join( elementType.getOptions() );
typeMap.put( options, elementType );
optionsLength = Math.max( optionsLength, options.length() );
}
for (Map.Entry<String, MPElementType> entry : typeMap.entrySet()) {
String infoString = strf( " -v %" + optionsLength + "s | ", entry.getKey() );
String infoNewline = "\n" + StringUtils.repeat( " ", infoString.length() - 3 ) + " | ";
infoString += entry.getValue().getDescription().replaceAll( "\n", infoNewline );
System.out.println( infoString );
}
System.out.println(); System.out.println();
System.out.println( "Available options:" );
System.out.println( "\t-t | --type [site password type]" ); System.out.format( " -c counter The value of the counter.\n" );
System.out.format( "\t\tDefault: %s. The password type to use for this site.\n", siteType.getName() ); System.out.format( " Defaults to %s in env or '1'.\n\n", ENV_SITECOUNTER );
System.out.println( "\t\tUse 'list' to see the available types." ); System.out.format( " -v variant The kind of content to generate.\n" );
System.out.format( " Defaults to 'password'.\n" );
optionsLength = 0;
Map<String, MPElementVariant> variantMap = Maps.newLinkedHashMap();
for (MPElementVariant elementVariant : MPElementVariant.values()) {
String options = Joiner.on( ", " ).join( elementVariant.getOptions() );
variantMap.put( options, elementVariant );
optionsLength = Math.max( optionsLength, options.length() );
}
for (Map.Entry<String, MPElementVariant> entry : variantMap.entrySet()) {
String infoString = strf( " -v %" + optionsLength + "s | ", entry.getKey() );
String infoNewline = "\n" + StringUtils.repeat( " ", infoString.length() - 3 ) + " | ";
infoString += entry.getValue().getDescription().replaceAll( "\n", infoNewline );
System.out.println( infoString );
}
System.out.println(); 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", siteCounter );
System.out.println( "\t\tIncrement the counter if you need a new password." );
System.out.format( " -C context A variant-specific context.\n" );
System.out.format( " Defaults to empty.\n" );
for (Map.Entry<String, MPElementVariant> entry : variantMap.entrySet()) {
String infoString = strf( " -v %" + optionsLength + "s | ", entry.getKey() );
String infoNewline = "\n" + StringUtils.repeat( " ", infoString.length() - 3 ) + " | ";
infoString += entry.getValue().getContextDescription().replaceAll( "\n", infoNewline );
System.out.println( infoString );
}
System.out.println(); System.out.println();
System.out.println( "\t-u | --username [user's name]" );
System.out.println( "\t\tDefault: asked. The name of the user." );
System.out.println(); System.out.format( " ENVIRONMENT\n\n" );
System.out.println( "Available environment variables:" ); System.out.format( " MP_USERNAME | The full name of the user.\n" );
System.out.format( " MP_SITETYPE | The default password template.\n" );
System.out.format( "\t%s\n", ENV_USERNAME ); System.out.format( " MP_SITECOUNTER | The default counter value.\n\n" );
System.out.println( "\t\tThe name of the user." );
System.out.format( "\t%s\n", ENV_PASSWORD );
System.out.println( "\t\tThe master password of the user." );
System.out.println();
return; return;
} else } else
siteName = arg; siteName = arg;
@ -126,18 +173,16 @@ public class CLI {
userName = lineReader.readLine(); userName = lineReader.readLine();
} }
if (masterPassword == null) { if (console != null)
if (console != null) masterPassword = new String( console.readPassword( "%s's master password: ", userName ) );
masterPassword = new String( console.readPassword( "%s's master password: ", userName ) );
else { else {
System.err.format( "%s's master password: ", userName ); System.err.format( "%s's master password: ", userName );
masterPassword = lineReader.readLine(); masterPassword = lineReader.readLine();
}
} }
} }
// Encode and write out the site password. // Encode and write out the site password.
System.out.println( new MasterKey( userName, masterPassword ).encode( siteName, siteType, siteCounter ) ); System.out.println( new MasterKey( userName, masterPassword ).encode( siteName, siteType, siteCounter, variant, context ) );
} }
} }

View File

@ -159,7 +159,7 @@ 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 = user.getKey().encode( siteName, siteType, siteCounter ); final String sitePassword = user.getKey().encode( siteName, siteType, siteCounter, MPElementVariant.Password, null );
if (callback != null) if (callback != null)
callback.passwordGenerated( siteName, sitePassword ); callback.passwordGenerated( siteName, sitePassword );