From a82ce7310d2a194248b099c473fd61572f6d3b09 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Wed, 3 Dec 2014 00:42:19 -0500 Subject: [PATCH] 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. --- .../Java/masterpassword-algorithm/pom.xml | 17 +- .../lyndir/masterpassword/MPElementType.java | 116 ++++++++----- .../masterpassword/MPElementVariant.java | 63 +++++++ .../com/lyndir/masterpassword/MPTemplate.java | 12 +- .../MPTemplateCharacterClass.java | 30 +++- .../lyndir/masterpassword/MPTemplates.java | 109 ------------ .../com/lyndir/masterpassword/MasterKey.java | 29 ++-- .../src/main/resources/ciphers.plist | 1 - .../lyndir/masterpassword/MasterKeyTest.java | 79 +++++++++ .../masterpassword/EmergencyActivity.java | 2 +- .../java/com/lyndir/masterpassword/CLI.java | 161 +++++++++++------- .../lyndir/masterpassword/PasswordFrame.java | 2 +- 12 files changed, 380 insertions(+), 241 deletions(-) create mode 100644 MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementVariant.java delete mode 100644 MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplates.java delete mode 120000 MasterPassword/Java/masterpassword-algorithm/src/main/resources/ciphers.plist create mode 100644 MasterPassword/Java/masterpassword-algorithm/src/test/java/com/lyndir/masterpassword/MasterKeyTest.java diff --git a/MasterPassword/Java/masterpassword-algorithm/pom.xml b/MasterPassword/Java/masterpassword-algorithm/pom.xml index 5e0b7ddb..94054f96 100644 --- a/MasterPassword/Java/masterpassword-algorithm/pom.xml +++ b/MasterPassword/Java/masterpassword-algorithm/pom.xml @@ -33,17 +33,24 @@ - - net.sf.plist - property-list - 2.0.0 - com.lambdaworks scrypt 1.4.0 + + + org.testng + testng + test + + + ch.qos.logback + logback-classic + test + + diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementType.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementType.java index aa0cf4ac..b2cc8338 100644 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementType.java +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementType.java @@ -3,7 +3,9 @@ package com.lyndir.masterpassword; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.lyndir.lhunath.opal.system.logging.Logger; +import java.util.List; import java.util.Set; +import javax.annotation.Generated; /** @@ -13,33 +15,72 @@ import java.util.Set; */ public enum MPElementType { - GeneratedMaximum( "Maximum Security Password", "Maximum", "20 characters, contains symbols.", MPElementTypeClass.Generated ), - GeneratedLong( "Long Password", "Long", "Copy-friendly, 14 characters, contains symbols.", MPElementTypeClass.Generated ), - GeneratedMedium( "Medium Password", "Medium", "Copy-friendly, 8 characters, contains symbols.", MPElementTypeClass.Generated ), - 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 ), + GeneratedMaximum( "20 characters, contains symbols.", // + ImmutableList.of( "x", "max", "maximum" ), MPElementTypeClass.Generated, // + ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ), new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ) ), - StoredPersonal( "Personal Password", "Personal", "AES-encrypted, exportable.", MPElementTypeClass.Stored, - MPElementFeature.ExportContent ), - StoredDevicePrivate( "Device Private Password", "Private", "AES-encrypted, not exported.", MPElementTypeClass.Stored, - MPElementFeature.DevicePrivate ); + GeneratedLong( "Copy-friendly, 14 characters, contains symbols.", // + ImmutableList.of( "l", "long" ), MPElementTypeClass.Generated, // + ImmutableList.of( new MPTemplate( "CvcvnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvnoCvcv" ), + 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.of(), MPElementFeature.ExportContent ), + + StoredDevicePrivate( "AES-encrypted, not exported.", // + ImmutableList.of( "device" ), MPElementTypeClass.Stored, // + ImmutableList.of(), MPElementFeature.DevicePrivate ); static final Logger logger = Logger.get( MPElementType.class ); - private final MPElementTypeClass typeClass; - private final Set typeFeatures; - private final String name; - private final String shortName; private final String description; + private final List options; + private final MPElementTypeClass typeClass; + private final List templates; + private final Set typeFeatures; - MPElementType(final String name, final String shortName, final String description, final MPElementTypeClass typeClass, - final MPElementFeature... typeFeatures) { + MPElementType(final String description, final List options, final MPElementTypeClass typeClass, + final List templates, final MPElementFeature... typeFeatures) { - this.name = name; - this.shortName = shortName; - this.typeClass = typeClass; this.description = description; + this.options = options; + this.typeClass = typeClass; + this.templates = templates; ImmutableSet.Builder typeFeaturesBuilder = ImmutableSet.builder(); for (final MPElementFeature typeFeature : typeFeatures) { @@ -48,6 +89,15 @@ public enum MPElementType { this.typeFeatures = typeFeaturesBuilder.build(); } + public String getDescription() { + + return description; + } + + public List getOptions() { + return options; + } + public MPElementTypeClass getTypeClass() { return typeClass; @@ -58,33 +108,18 @@ public enum MPElementType { 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()) - if (type.getName().equalsIgnoreCase( name ) || type.getShortName().equalsIgnoreCase( name )) + if (type.getOptions().contains( option.toLowerCase() )) 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(); } + public MPTemplate getTemplateAtRollingIndex(final int templateIndex) { + return templates.get( templateIndex % templates.size() ); + } } diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementVariant.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementVariant.java new file mode 100644 index 00000000..c212bd36 --- /dev/null +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPElementVariant.java @@ -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 options; + private final String scope; + + MPElementVariant(final String description, final String contextDescription, final List 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 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 ); + } +} diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplate.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplate.java index be28a17b..2ada4543 100644 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplate.java +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplate.java @@ -1,6 +1,7 @@ package com.lyndir.masterpassword; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.lyndir.lhunath.opal.system.util.MetaObject; import java.util.List; import java.util.Map; @@ -15,20 +16,15 @@ public class MPTemplate extends MetaObject { private final List template; - public MPTemplate(final String template, final Map characterClasses) { + MPTemplate(final String template) { - ImmutableList.Builder builder = ImmutableList.builder(); + ImmutableList.Builder builder = ImmutableList.builder(); 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(); } - public MPTemplate(final List template) { - - this.template = template; - } - public MPTemplateCharacterClass getCharacterClassAtIndex(final int index) { return template.get( index ); diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplateCharacterClass.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplateCharacterClass.java index d5116c11..2838034f 100644 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplateCharacterClass.java +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplateCharacterClass.java @@ -1,5 +1,6 @@ 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.ObjectMeta; @@ -9,16 +10,29 @@ import com.lyndir.lhunath.opal.system.util.ObjectMeta; * * @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; - @ObjectMeta(useFor = { }) private final char[] characters; - public MPTemplateCharacterClass(final char identifier, final char[] characters) { + MPTemplateCharacterClass(final char identifier, final String characters) { this.identifier = identifier; - this.characters = characters; + this.characters = characters.toCharArray(); } public char getIdentifier() { @@ -30,4 +44,12 @@ public class MPTemplateCharacterClass extends MetaObject { 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 ); + } } diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplates.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplates.java deleted file mode 100644 index f3378bef..00000000 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPTemplates.java +++ /dev/null @@ -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; - - -/** - * 07 04, 2012 - * - * @author lhunath - */ -public class MPTemplates extends MetaObject { - - static final Logger logger = Logger.get( MPTemplates.class ); - - private final Map> templates; - - public MPTemplates(final Map> 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 characterClassesBuilder = ImmutableMap.builder(); - for (final Map.Entry 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 characterClasses = characterClassesBuilder.build(); - - ImmutableMap.Builder> templatesBuilder = ImmutableMap.builder(); - for (final Map.Entry template : templatesDict.entrySet()) { - String key = template.getKey(); - NSObject value = template.getValue(); - Preconditions.checkState( NSArray.class.isAssignableFrom( value.getClass() ) ); - - MPElementType type = MPElementType.forName( key ); - List templateStrings = ((NSArray) value).getValue(); - - ImmutableList.Builder typeTemplatesBuilder = ImmutableList.builder(); - for (final NSObject templateString : templateStrings) - typeTemplatesBuilder.add( new MPTemplate( ((NSString) templateString).getValue(), characterClasses ) ); - - templatesBuilder.put( type, typeTemplatesBuilder.build() ); - } - ImmutableMap> 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 typeTemplates = templates.get( type ); - - return typeTemplates.get( templateIndex % typeTemplates.size() ); - } - - public static void main(final String... arguments) { - - load(); - } -} diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java index 500776ef..2dc259f2 100644 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java @@ -2,8 +2,6 @@ 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; @@ -14,7 +12,6 @@ import java.nio.ByteOrder; import java.nio.charset.Charset; import java.security.GeneralSecurityException; 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_p = 2; 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 ByteOrder MP_byteOrder = ByteOrder.BIG_ENDIAN; private static final MessageDigests MP_hash = MessageDigests.SHA256; private static final MessageAuthenticationDigests MP_mac = MessageAuthenticationDigests.HmacSHA256; - private static final MPTemplates templates = MPTemplates.load(); private final String userName; private final byte[] key; @@ -44,12 +41,13 @@ public class MasterKey { this.userName = userName; 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 ) - .putInt( userName.length() ) + .putInt( userNameBytes.length ) .array(); - byte[] salt = Bytes.concat( "com.lyndir.masterpassword".getBytes( MP_charset ), // - userNameLengthBytes, userName.getBytes( MP_charset ) ); + byte[] salt = Bytes.concat( MPElementVariant.Password.getScope().getBytes( MP_charset ), // + userNameLengthBytes, userNameBytes ); try { 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; } - 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.checkArgument( type.getTypeClass() == MPElementTypeClass.Generated ); @@ -92,19 +90,20 @@ public class MasterKey { if (counter == 0) counter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300; - byte[] nameLengthBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( MP_byteOrder ).putInt( name.length() ).array(); - byte[] counterBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( MP_byteOrder ).putInt( counter ).array(); - logger.trc( "seed from: hmac-sha256(%s, 'com.lyndir.masterpassword' | %s | %s | %s)", CryptUtils.encodeBase64( key ), + byte[] nameBytes = name.getBytes( MP_charset ); + byte[] nameLengthBytes = ByteBuffer.allocate( MP_intLen / Byte.SIZE ).order( MP_byteOrder ).putInt( nameBytes.length ).array(); + 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 ) ); - 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, // - name.getBytes( MP_charset ), // + nameBytes, // counterBytes ) ); logger.trc( "seed is: %s", CryptUtils.encodeBase64( seed ) ); Preconditions.checkState( seed.length > 0 ); 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 ); StringBuilder password = new StringBuilder( template.length() ); diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/resources/ciphers.plist b/MasterPassword/Java/masterpassword-algorithm/src/main/resources/ciphers.plist deleted file mode 120000 index 5258d70a..00000000 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/resources/ciphers.plist +++ /dev/null @@ -1 +0,0 @@ -../../../../../../MasterPassword/Resources/Data/ciphers.plist \ No newline at end of file diff --git a/MasterPassword/Java/masterpassword-algorithm/src/test/java/com/lyndir/masterpassword/MasterKeyTest.java b/MasterPassword/Java/masterpassword-algorithm/src/test/java/com/lyndir/masterpassword/MasterKeyTest.java new file mode 100644 index 00000000..5039e2ac --- /dev/null +++ b/MasterPassword/Java/masterpassword-algorithm/src/test/java/com/lyndir/masterpassword/MasterKeyTest.java @@ -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) { + } + } +} diff --git a/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java b/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java index 8376489f..0030eaca 100644 --- a/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java +++ b/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java @@ -184,7 +184,7 @@ public class EmergencyActivity extends Activity { @Override public void run() { try { - final String sitePassword = masterKeyFuture.get().encode( siteName, type, counter ); + final String sitePassword = masterKeyFuture.get().encode( siteName, type, counter, variant, context ); runOnUiThread( new Runnable() { @Override diff --git a/MasterPassword/Java/masterpassword-cli/src/main/java/com/lyndir/masterpassword/CLI.java b/MasterPassword/Java/masterpassword-cli/src/main/java/com/lyndir/masterpassword/CLI.java index 351a5112..270bcfe0 100644 --- a/MasterPassword/Java/masterpassword-cli/src/main/java/com/lyndir/masterpassword/CLI.java +++ b/MasterPassword/Java/masterpassword-cli/src/main/java/com/lyndir/masterpassword/CLI.java @@ -18,12 +18,16 @@ package com.lyndir.masterpassword; 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.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.util.ConversionUtils; +import com.lyndir.lhunath.opal.system.util.StringUtils; import java.io.*; import java.util.Arrays; +import java.util.Map; /** @@ -34,7 +38,6 @@ import java.util.Arrays; public class CLI { 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"; @@ -42,71 +45,115 @@ public class CLI { throws IOException { // 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 ), "" ); + String siteName = null, masterPassword, context = null; + String userName = System.getenv( ENV_USERNAME ); + String siteTypeName = ifNotNullElse( System.getenv( ENV_SITETYPE ), "" ); + MPElementType siteType = siteTypeName.isEmpty()? MPElementType.GeneratedLong: MPElementType.forOption( siteTypeName ); + MPElementVariant variant = MPElementVariant.Password; + String siteCounterName = ifNotNullElse( System.getenv( ENV_SITECOUNTER ), "" ); int siteCounter = siteCounterName.isEmpty()? 1: Integer.parseInt( siteCounterName ); // 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 )) - if ("-t".equals( arg ) || "--type".equals( arg )) - typeArg = true; - 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 )) + // Full Name + if ("-u".equals( arg ) || "--username".equals( arg )) userNameArg = true; else if (userNameArg) { userName = arg; 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( "\tMaster Password CLI" ); - System.out.println( "\t\tLyndir" ); + System.out.format( "Usage: mpw [-u name] [-t type] [-c counter] site\n\n" ); + 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 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 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( "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", siteType.getName() ); - System.out.println( "\t\tUse 'list' to see the available types." ); + System.out.format( " -c counter The value of the counter.\n" ); + System.out.format( " Defaults to %s in env or '1'.\n\n", ENV_SITECOUNTER ); + System.out.format( " -v variant The kind of content to generate.\n" ); + System.out.format( " Defaults to 'password'.\n" ); + optionsLength = 0; + Map 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 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( "\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 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( "\t-u | --username [user's name]" ); - System.out.println( "\t\tDefault: asked. The name of the user." ); - System.out.println(); - System.out.println( "Available environment variables:" ); - - System.out.format( "\t%s\n", ENV_USERNAME ); - 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(); + System.out.format( " ENVIRONMENT\n\n" ); + 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( " MP_SITECOUNTER | The default counter value.\n\n" ); return; } else siteName = arg; @@ -126,18 +173,16 @@ public class CLI { userName = lineReader.readLine(); } - if (masterPassword == null) { - if (console != null) - masterPassword = new String( console.readPassword( "%s's master password: ", userName ) ); + 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(); - } + 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 ) ); + System.out.println( new MasterKey( userName, masterPassword ).encode( siteName, siteType, siteCounter, variant, context ) ); } } diff --git a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/PasswordFrame.java b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/PasswordFrame.java index 65f6b85d..3dad0401 100644 --- a/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/PasswordFrame.java +++ b/MasterPassword/Java/masterpassword-gui/src/main/java/com/lyndir/masterpassword/PasswordFrame.java @@ -159,7 +159,7 @@ public class PasswordFrame extends JFrame implements DocumentListener { Res.execute( new Runnable() { @Override 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) callback.passwordGenerated( siteName, sitePassword );