Re-organize the project into a better hierarchy.
This commit is contained in:
12
core/java/algorithm/build.gradle
Normal file
12
core/java/algorithm/build.gradle
Normal file
@@ -0,0 +1,12 @@
|
||||
apply plugin: 'java'
|
||||
|
||||
description = 'Master Password Algorithm Implementation'
|
||||
|
||||
dependencies {
|
||||
compile( 'com.lyndir.lhunath.opal:opal-system:1.6-p10' ) {
|
||||
exclude( module: 'joda-time' )
|
||||
}
|
||||
compile 'com.lambdaworks:scrypt:1.4.0'
|
||||
compile 'org.jetbrains:annotations:13.0'
|
||||
compile 'com.google.code.findbugs:jsr305:3.0.1'
|
||||
}
|
44
core/java/algorithm/pom.xml
Normal file
44
core/java/algorithm/pom.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<!-- PROJECT METADATA -->
|
||||
<parent>
|
||||
<groupId>com.lyndir.masterpassword</groupId>
|
||||
<artifactId>masterpassword</artifactId>
|
||||
<version>GIT-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<name>Master Password Algorithm Implementation</name>
|
||||
<description>The implementation of the Master Password algorithm</description>
|
||||
|
||||
<artifactId>masterpassword-algorithm</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<!-- DEPENDENCY MANAGEMENT -->
|
||||
<dependencies>
|
||||
|
||||
<!-- PROJECT REFERENCES -->
|
||||
<dependency>
|
||||
<groupId>com.lyndir.lhunath.opal</groupId>
|
||||
<artifactId>opal-system</artifactId>
|
||||
<version>1.6-p9</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>joda-time</groupId>
|
||||
<artifactId>joda-time</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- EXTERNAL DEPENDENCIES -->
|
||||
<dependency>
|
||||
<groupId>com.lambdaworks</groupId>
|
||||
<artifactId>scrypt</artifactId>
|
||||
<version>1.4.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,74 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.lyndir.lhunath.opal.system.MessageAuthenticationDigests;
|
||||
import com.lyndir.lhunath.opal.system.MessageDigests;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2016-10-29
|
||||
*/
|
||||
public class MPConstant {
|
||||
|
||||
/* Environment */
|
||||
|
||||
/**
|
||||
* mpw: default user name if one is not provided.
|
||||
*/
|
||||
public static final String env_userName = "MP_USERNAME";
|
||||
/**
|
||||
* mpw: default site type if one is not provided.
|
||||
*
|
||||
* @see MPSiteType#forOption(String)
|
||||
*/
|
||||
public static final String env_siteType = "MP_SITETYPE";
|
||||
/**
|
||||
* mpw: default site counter value if one is not provided.
|
||||
*/
|
||||
public static final String env_siteCounter = "MP_SITECOUNTER";
|
||||
/**
|
||||
* mpw: default path to look for run configuration files if the platform default is not desired.
|
||||
*/
|
||||
public static final String env_rcDir = "MP_RCDIR";
|
||||
/**
|
||||
* mpw: permit automatic update checks.
|
||||
*/
|
||||
public static final String env_checkUpdates = "MP_CHECKUPDATES";
|
||||
|
||||
/* Algorithm */
|
||||
|
||||
/**
|
||||
* scrypt: CPU cost parameter.
|
||||
*/
|
||||
public static final int scrypt_N = 32768;
|
||||
/**
|
||||
* scrypt: Memory cost parameter.
|
||||
*/
|
||||
public static final int scrypt_r = 8;
|
||||
/**
|
||||
* scrypt: Parallelization parameter.
|
||||
*/
|
||||
public static final int scrypt_p = 2;
|
||||
/**
|
||||
* mpw: Master key size (byte).
|
||||
*/
|
||||
public static final int mpw_dkLen = 64;
|
||||
/**
|
||||
* mpw: Input character encoding.
|
||||
*/
|
||||
public static final Charset mpw_charset = Charsets.UTF_8;
|
||||
/**
|
||||
* mpw: Platform-agnostic byte order.
|
||||
*/
|
||||
public static final ByteOrder mpw_byteOrder = ByteOrder.BIG_ENDIAN;
|
||||
/**
|
||||
* mpw: Site digest.
|
||||
*/
|
||||
public static final MessageAuthenticationDigests mpw_digest = MessageAuthenticationDigests.HmacSHA256;
|
||||
/**
|
||||
* mpw: Key ID hash.
|
||||
*/
|
||||
public static final MessageDigests mpw_hash = MessageDigests.SHA256;
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.lyndir.lhunath.opal.system.MessageAuthenticationDigests;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.nio.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 15-03-29
|
||||
*/
|
||||
public class MPIdenticon {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MPIdenticon.class );
|
||||
|
||||
private static final Charset charset = Charsets.UTF_8;
|
||||
private static final Color[] colors = new Color[]{
|
||||
Color.RED, Color.GREEN, Color.YELLOW, Color.BLUE, Color.MAGENTA, Color.CYAN, Color.MONO };
|
||||
private static final char[] leftArm = new char[]{ '╔', '╚', '╰', '═' };
|
||||
private static final char[] rightArm = new char[]{ '╗', '╝', '╯', '═' };
|
||||
private static final char[] body = new char[]{ '█', '░', '▒', '▓', '☺', '☻' };
|
||||
private static final char[] accessory = new char[]{
|
||||
'◈', '◎', '◐', '◑', '◒', '◓', '☀', '☁', '☂', '☃', '☄', '★', '☆', '☎', '☏', '⎈', '⌂', '☘', '☢', '☣', '☕', '⌚', '⌛', '⏰', '⚡',
|
||||
'⛄', '⛅', '☔', '♔', '♕', '♖', '♗', '♘', '♙', '♚', '♛', '♜', '♝', '♞', '♟', '♨', '♩', '♪', '♫', '⚐', '⚑', '⚔', '⚖', '⚙', '⚠',
|
||||
'⌘', '⏎', '✄', '✆', '✈', '✉', '✌' };
|
||||
|
||||
private final String fullName;
|
||||
private final Color color;
|
||||
private final String text;
|
||||
|
||||
public MPIdenticon(String fullName, String masterPassword) {
|
||||
this( fullName, masterPassword.toCharArray() );
|
||||
}
|
||||
|
||||
public MPIdenticon(String fullName, char[] masterPassword) {
|
||||
this.fullName = fullName;
|
||||
|
||||
byte[] masterPasswordBytes = charset.encode( CharBuffer.wrap( masterPassword ) ).array();
|
||||
ByteBuffer identiconSeedBytes = ByteBuffer.wrap(
|
||||
MessageAuthenticationDigests.HmacSHA256.of( masterPasswordBytes, fullName.getBytes( charset ) ) );
|
||||
Arrays.fill( masterPasswordBytes, (byte) 0 );
|
||||
|
||||
IntBuffer identiconSeedBuffer = IntBuffer.allocate( identiconSeedBytes.capacity() );
|
||||
while (identiconSeedBytes.hasRemaining())
|
||||
identiconSeedBuffer.put( identiconSeedBytes.get() & 0xFF );
|
||||
int[] identiconSeed = identiconSeedBuffer.array();
|
||||
|
||||
color = colors[identiconSeed[4] % colors.length];
|
||||
text = strf( "%c%c%c%c", leftArm[identiconSeed[0] % leftArm.length], body[identiconSeed[1] % body.length],
|
||||
rightArm[identiconSeed[2] % rightArm.length], accessory[identiconSeed[3] % accessory.length] );
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public enum Color {
|
||||
RED,
|
||||
GREEN,
|
||||
YELLOW,
|
||||
BLUE,
|
||||
MAGENTA,
|
||||
CYAN,
|
||||
MONO
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
/**
|
||||
* <i>07 04, 2012</i>
|
||||
*
|
||||
* @author lhunath
|
||||
*/
|
||||
public enum MPSiteFeature {
|
||||
|
||||
/**
|
||||
* Export the key-protected content data.
|
||||
*/
|
||||
ExportContent( 1 << 10 ),
|
||||
|
||||
/**
|
||||
* Never export content.
|
||||
*/
|
||||
DevicePrivate( 1 << 11 );
|
||||
|
||||
MPSiteFeature(final int mask) {
|
||||
this.mask = mask;
|
||||
}
|
||||
|
||||
private final int mask;
|
||||
|
||||
public int getMask() {
|
||||
return mask;
|
||||
}
|
||||
}
|
@@ -0,0 +1,218 @@
|
||||
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.Nullable;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
|
||||
|
||||
/**
|
||||
* <i>07 04, 2012</i>
|
||||
*
|
||||
* @author lhunath
|
||||
*/
|
||||
public enum MPSiteType {
|
||||
|
||||
GeneratedMaximum( "Max", "20 characters, contains symbols.", //
|
||||
ImmutableList.of( "x", "max", "maximum" ), //
|
||||
ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ), new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ), //
|
||||
MPSiteTypeClass.Generated, 0x0 ),
|
||||
|
||||
GeneratedLong( "Long", "Copy-friendly, 14 characters, contains symbols.", //
|
||||
ImmutableList.of( "l", "long" ), //
|
||||
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" ) ), //
|
||||
MPSiteTypeClass.Generated, 0x1 ),
|
||||
|
||||
GeneratedMedium( "Medium", "Copy-friendly, 8 characters, contains symbols.", //
|
||||
ImmutableList.of( "m", "med", "medium" ), //
|
||||
ImmutableList.of( new MPTemplate( "CvcnoCvc" ), new MPTemplate( "CvcCvcno" ) ), //
|
||||
MPSiteTypeClass.Generated, 0x2 ),
|
||||
|
||||
GeneratedBasic( "Basic", "8 characters, no symbols.", //
|
||||
ImmutableList.of( "b", "basic" ), //
|
||||
ImmutableList.of( new MPTemplate( "aaanaaan" ), new MPTemplate( "aannaaan" ), new MPTemplate( "aaannaaa" ) ), //
|
||||
MPSiteTypeClass.Generated, 0x3 ),
|
||||
|
||||
GeneratedShort( "Short", "Copy-friendly, 4 characters, no symbols.", //
|
||||
ImmutableList.of( "s", "short" ), //
|
||||
ImmutableList.of( new MPTemplate( "Cvcn" ) ), //
|
||||
MPSiteTypeClass.Generated, 0x4 ),
|
||||
|
||||
GeneratedPIN( "PIN", "4 numbers.", //
|
||||
ImmutableList.of( "i", "pin" ), //
|
||||
ImmutableList.of( new MPTemplate( "nnnn" ) ), //
|
||||
MPSiteTypeClass.Generated, 0x5 ),
|
||||
|
||||
GeneratedName( "Name", "9 letter name.", //
|
||||
ImmutableList.of( "n", "name" ), //
|
||||
ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ), //
|
||||
MPSiteTypeClass.Generated, 0xE ),
|
||||
|
||||
GeneratedPhrase( "Phrase", "20 character sentence.", //
|
||||
ImmutableList.of( "p", "phrase" ), //
|
||||
ImmutableList.of( new MPTemplate( "cvcc cvc cvccvcv cvc" ), new MPTemplate( "cvc cvccvcvcv cvcv" ),
|
||||
new MPTemplate( "cv cvccv cvc cvcvccv" ) ), //
|
||||
MPSiteTypeClass.Generated, 0xF ),
|
||||
|
||||
StoredPersonal( "Personal", "AES-encrypted, exportable.", //
|
||||
ImmutableList.of( "personal" ), //
|
||||
ImmutableList.<MPTemplate>of(), //
|
||||
MPSiteTypeClass.Stored, 0x0, MPSiteFeature.ExportContent ),
|
||||
|
||||
StoredDevicePrivate( "Device", "AES-encrypted, not exported.", //
|
||||
ImmutableList.of( "device" ), //
|
||||
ImmutableList.<MPTemplate>of(), //
|
||||
MPSiteTypeClass.Stored, 0x1, MPSiteFeature.DevicePrivate );
|
||||
|
||||
static final Logger logger = Logger.get( MPSiteType.class );
|
||||
|
||||
private final String shortName;
|
||||
private final String description;
|
||||
private final List<String> options;
|
||||
private final List<MPTemplate> templates;
|
||||
private final MPSiteTypeClass typeClass;
|
||||
private final int typeIndex;
|
||||
private final Set<MPSiteFeature> typeFeatures;
|
||||
|
||||
MPSiteType(final String shortName, final String description, final List<String> options, final List<MPTemplate> templates,
|
||||
final MPSiteTypeClass typeClass, final int typeIndex, final MPSiteFeature... typeFeatures) {
|
||||
|
||||
this.shortName = shortName;
|
||||
this.description = description;
|
||||
this.options = options;
|
||||
this.templates = templates;
|
||||
this.typeClass = typeClass;
|
||||
this.typeIndex = typeIndex;
|
||||
|
||||
ImmutableSet.Builder<MPSiteFeature> typeFeaturesBuilder = ImmutableSet.builder();
|
||||
for (final MPSiteFeature typeFeature : typeFeatures) {
|
||||
typeFeaturesBuilder.add( typeFeature );
|
||||
}
|
||||
this.typeFeatures = typeFeaturesBuilder.build();
|
||||
}
|
||||
|
||||
public String getShortName() {
|
||||
return shortName;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
public List<String> getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
public MPSiteTypeClass getTypeClass() {
|
||||
|
||||
return typeClass;
|
||||
}
|
||||
|
||||
public Set<MPSiteFeature> getTypeFeatures() {
|
||||
|
||||
return typeFeatures;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
int mask = typeIndex | typeClass.getMask();
|
||||
for (MPSiteFeature typeFeature : typeFeatures)
|
||||
mask |= typeFeature.getMask();
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param option The option to select a type with. It is matched case insensitively.
|
||||
*
|
||||
* @return The type registered for the given option.
|
||||
*/
|
||||
public static MPSiteType forOption(final String option) {
|
||||
|
||||
for (final MPSiteType type : values())
|
||||
if (type.getOptions().contains( option.toLowerCase() ))
|
||||
return type;
|
||||
|
||||
throw logger.bug( "No type for option: %s", option );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name The name fromInt the type to look up. It is matched case insensitively.
|
||||
*
|
||||
* @return The type registered with the given name.
|
||||
*/
|
||||
@Contract("!null -> !null, null -> null")
|
||||
public static MPSiteType forName(@Nullable final String name) {
|
||||
|
||||
if (name == null)
|
||||
return null;
|
||||
|
||||
for (final MPSiteType type : values())
|
||||
if (type.name().equalsIgnoreCase( name ))
|
||||
return type;
|
||||
|
||||
throw logger.bug( "No type for name: %s", name );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param typeClass The class for which we look up types.
|
||||
*
|
||||
* @return All types that support the given class.
|
||||
*/
|
||||
public static ImmutableList<MPSiteType> forClass(final MPSiteTypeClass typeClass) {
|
||||
|
||||
ImmutableList.Builder<MPSiteType> types = ImmutableList.builder();
|
||||
for (final MPSiteType type : values())
|
||||
if (type.getTypeClass() == typeClass)
|
||||
types.add( type );
|
||||
|
||||
return types.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type The type for which we look up types.
|
||||
*
|
||||
* @return The type registered with the given type.
|
||||
*/
|
||||
public static MPSiteType forType(final int type) {
|
||||
|
||||
for (MPSiteType siteType : values())
|
||||
if (siteType.getType() == type)
|
||||
return siteType;
|
||||
|
||||
throw logger.bug( "No type: %s", type );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mask The mask for which we look up types.
|
||||
*
|
||||
* @return All types that support the given mask.
|
||||
*/
|
||||
public static ImmutableList<MPSiteType> forMask(final int mask) {
|
||||
|
||||
int typeMask = mask & ~0xF;
|
||||
ImmutableList.Builder<MPSiteType> types = ImmutableList.builder();
|
||||
for (MPSiteType siteType : values())
|
||||
if (((siteType.getType() & ~0xF) & typeMask) != 0)
|
||||
types.add( siteType );
|
||||
|
||||
return types.build();
|
||||
}
|
||||
|
||||
public MPTemplate getTemplateAtRollingIndex(final int templateIndex) {
|
||||
return templates.get( templateIndex % templates.size() );
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
/**
|
||||
* <i>07 04, 2012</i>
|
||||
*
|
||||
* @author lhunath
|
||||
*/
|
||||
public enum MPSiteTypeClass {
|
||||
Generated( 1 << 4 ),
|
||||
Stored( 1 << 5 );
|
||||
|
||||
private final int mask;
|
||||
|
||||
MPSiteTypeClass(final int mask) {
|
||||
this.mask = mask;
|
||||
}
|
||||
|
||||
public int getMask() {
|
||||
return mask;
|
||||
}
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 14-12-02
|
||||
*/
|
||||
public enum MPSiteVariant {
|
||||
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( MPSiteType.class );
|
||||
|
||||
private final String description;
|
||||
private final String contextDescription;
|
||||
private final List<String> options;
|
||||
private final String scope;
|
||||
|
||||
MPSiteVariant(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 MPSiteVariant forOption(final String option) {
|
||||
|
||||
for (final MPSiteVariant variant : values())
|
||||
if (variant.getOptions().contains( option.toLowerCase() ))
|
||||
return variant;
|
||||
|
||||
throw logger.bug( "No variant for option: %s", option );
|
||||
}
|
||||
/**
|
||||
* @param name The name fromInt the variant to look up. It is matched case insensitively.
|
||||
*
|
||||
* @return The variant registered with the given name.
|
||||
*/
|
||||
@Contract("!null -> !null, null -> null")
|
||||
public static MPSiteVariant forName(@Nullable final String name) {
|
||||
|
||||
if (name == null)
|
||||
return null;
|
||||
|
||||
for (final MPSiteVariant type : values())
|
||||
if (type.name().equalsIgnoreCase( name ))
|
||||
return type;
|
||||
|
||||
throw logger.bug( "No variant for name: %s", name );
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.lyndir.lhunath.opal.system.util.MetaObject;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* <i>07 04, 2012</i>
|
||||
*
|
||||
* @author lhunath
|
||||
*/
|
||||
public class MPTemplate extends MetaObject {
|
||||
|
||||
private final String templateString;
|
||||
private final List<MPTemplateCharacterClass> template;
|
||||
|
||||
MPTemplate(final String templateString) {
|
||||
|
||||
ImmutableList.Builder<MPTemplateCharacterClass> builder = ImmutableList.builder();
|
||||
for (int i = 0; i < templateString.length(); ++i)
|
||||
builder.add( MPTemplateCharacterClass.forIdentifier( templateString.charAt( i ) ) );
|
||||
|
||||
this.templateString = templateString;
|
||||
template = builder.build();
|
||||
}
|
||||
|
||||
public String getTemplateString() {
|
||||
return templateString;
|
||||
}
|
||||
|
||||
public MPTemplateCharacterClass getCharacterClassAtIndex(final int index) {
|
||||
|
||||
return template.get( index );
|
||||
}
|
||||
|
||||
public int length() {
|
||||
|
||||
return template.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return strf( "{MPTemplate: %s}", templateString );
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* <i>07 04, 2012</i>
|
||||
*
|
||||
* @author lhunath
|
||||
*/
|
||||
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[] characters;
|
||||
|
||||
MPTemplateCharacterClass(final char identifier, final String characters) {
|
||||
|
||||
this.identifier = identifier;
|
||||
this.characters = characters.toCharArray();
|
||||
}
|
||||
|
||||
public char getIdentifier() {
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
public char getCharacterAtRollingIndex(final int index) {
|
||||
|
||||
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 );
|
||||
}
|
||||
}
|
@@ -0,0 +1,196 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lyndir.lhunath.opal.system.*;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2014-08-30
|
||||
*/
|
||||
public abstract class MasterKey {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MasterKey.class );
|
||||
private static boolean allowNativeByDefault = true;
|
||||
|
||||
@Nonnull
|
||||
private final String fullName;
|
||||
private boolean allowNative = allowNativeByDefault;
|
||||
|
||||
@Nullable
|
||||
private byte[] masterKey;
|
||||
|
||||
public static MasterKey create(final String fullName, final char[] masterPassword) {
|
||||
|
||||
return create( Version.CURRENT, fullName, masterPassword );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static MasterKey create(Version version, final String fullName, final char[] masterPassword) {
|
||||
|
||||
switch (version) {
|
||||
case V0:
|
||||
return new MasterKeyV0( fullName ).revalidate( masterPassword );
|
||||
case V1:
|
||||
return new MasterKeyV1( fullName ).revalidate( masterPassword );
|
||||
case V2:
|
||||
return new MasterKeyV2( fullName ).revalidate( masterPassword );
|
||||
case V3:
|
||||
return new MasterKeyV3( fullName ).revalidate( masterPassword );
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException( "Unsupported version: " + version );
|
||||
}
|
||||
|
||||
public static boolean isAllowNativeByDefault() {
|
||||
return allowNativeByDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Native libraries are useful for speeding up the performance of cryptographical functions.
|
||||
* Sometimes, however, we may prefer to use Java-only code.
|
||||
* For instance, for auditability / trust or because the native code doesn't work on our CPU/platform.
|
||||
* <p/>
|
||||
* This setter affects the default setting for any newly created {@link MasterKey}s.
|
||||
*
|
||||
* @param allowNative false to disallow the use of native libraries.
|
||||
*/
|
||||
public static void setAllowNativeByDefault(final boolean allowNative) {
|
||||
allowNativeByDefault = allowNative;
|
||||
}
|
||||
|
||||
protected MasterKey(@NotNull final String fullName) {
|
||||
|
||||
this.fullName = fullName;
|
||||
logger.trc( "fullName: %s", fullName );
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected abstract byte[] deriveKey(final char[] masterPassword);
|
||||
|
||||
public abstract Version getAlgorithmVersion();
|
||||
|
||||
@NotNull
|
||||
public String getFullName() {
|
||||
|
||||
return fullName;
|
||||
}
|
||||
|
||||
public boolean isAllowNative() {
|
||||
return allowNative;
|
||||
}
|
||||
|
||||
public MasterKey setAllowNative(final boolean allowNative) {
|
||||
this.allowNative = allowNative;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected byte[] getKey() {
|
||||
|
||||
Preconditions.checkState( isValid() );
|
||||
return Preconditions.checkNotNull( masterKey );
|
||||
}
|
||||
|
||||
public byte[] getKeyID() {
|
||||
|
||||
return idForBytes( getKey() );
|
||||
}
|
||||
|
||||
public abstract String encode(@Nonnull final String siteName, final MPSiteType siteType, @Nonnull final UnsignedInteger siteCounter,
|
||||
final MPSiteVariant siteVariant, @Nullable final String siteContext);
|
||||
|
||||
public boolean isValid() {
|
||||
return masterKey != null;
|
||||
}
|
||||
|
||||
public void invalidate() {
|
||||
|
||||
if (masterKey != null) {
|
||||
Arrays.fill( masterKey, (byte) 0 );
|
||||
masterKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
public MasterKey revalidate(final char[] masterPassword) {
|
||||
invalidate();
|
||||
|
||||
logger.trc( "masterPassword: %s", new String( masterPassword ) );
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
masterKey = deriveKey( masterPassword );
|
||||
|
||||
if (masterKey == null)
|
||||
logger.dbg( "masterKey calculation failed after %.2fs.", (System.currentTimeMillis() - start) / 1000D );
|
||||
else
|
||||
logger.trc( "masterKey ID: %s (derived in %.2fs)", CodeUtils.encodeHex( idForBytes( masterKey ) ),
|
||||
(System.currentTimeMillis() - start) / 1000D );
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
protected abstract byte[] bytesForInt(final int number);
|
||||
|
||||
protected abstract byte[] bytesForInt(@Nonnull final UnsignedInteger number);
|
||||
|
||||
protected abstract byte[] idForBytes(final byte[] bytes);
|
||||
|
||||
public enum Version {
|
||||
/**
|
||||
* bugs:
|
||||
* - does math with chars whose signedness was platform-dependent.
|
||||
* - miscounted the byte-length fromInt multi-byte site names.
|
||||
* - miscounted the byte-length fromInt multi-byte full names.
|
||||
*/
|
||||
V0,
|
||||
/**
|
||||
* bugs:
|
||||
* - miscounted the byte-length fromInt multi-byte site names.
|
||||
* - miscounted the byte-length fromInt multi-byte full names.
|
||||
*/
|
||||
V1,
|
||||
/**
|
||||
* bugs:
|
||||
* - miscounted the byte-length fromInt multi-byte full names.
|
||||
*/
|
||||
V2,
|
||||
/**
|
||||
* bugs:
|
||||
* - no known issues.
|
||||
*/
|
||||
V3;
|
||||
|
||||
public static final Version CURRENT = V3;
|
||||
|
||||
public static Version fromInt(final int algorithmVersion) {
|
||||
|
||||
return values()[algorithmVersion];
|
||||
}
|
||||
|
||||
public int toInt() {
|
||||
|
||||
return ordinal();
|
||||
}
|
||||
|
||||
public String toBundleVersion() {
|
||||
switch (this) {
|
||||
case V0:
|
||||
return "1.0";
|
||||
case V1:
|
||||
return "2.0";
|
||||
case V2:
|
||||
return "2.1";
|
||||
case V3:
|
||||
return "2.2";
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException( "Unsupported version: " + this );
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,151 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lambdaworks.crypto.SCrypt;
|
||||
import com.lyndir.lhunath.opal.system.*;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.nio.*;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* bugs:
|
||||
* - V2: miscounted the byte-length fromInt multi-byte full names.
|
||||
* - V1: miscounted the byte-length fromInt multi-byte site names.
|
||||
* - V0: does math with chars whose signedness was platform-dependent.
|
||||
*
|
||||
* @author lhunath, 2014-08-30
|
||||
*/
|
||||
public class MasterKeyV0 extends MasterKey {
|
||||
|
||||
private static final int MP_intLen = 32;
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MasterKeyV0.class );
|
||||
|
||||
public MasterKeyV0(final String fullName) {
|
||||
super( fullName );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getAlgorithmVersion() {
|
||||
|
||||
return Version.V0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected byte[] deriveKey(final char[] masterPassword) {
|
||||
String fullName = getFullName();
|
||||
byte[] fullNameBytes = fullName.getBytes( MPConstant.mpw_charset );
|
||||
byte[] fullNameLengthBytes = bytesForInt( fullName.length() );
|
||||
|
||||
String mpKeyScope = MPSiteVariant.Password.getScope();
|
||||
byte[] masterKeySalt = Bytes.concat( mpKeyScope.getBytes( MPConstant.mpw_charset ), fullNameLengthBytes, fullNameBytes );
|
||||
logger.trc( "key scope: %s", mpKeyScope );
|
||||
logger.trc( "masterKeySalt ID: %s", CodeUtils.encodeHex( idForBytes( masterKeySalt ) ) );
|
||||
|
||||
ByteBuffer mpBytesBuf = MPConstant.mpw_charset.encode( CharBuffer.wrap( masterPassword ) );
|
||||
byte[] mpBytes = new byte[mpBytesBuf.remaining()];
|
||||
mpBytesBuf.get( mpBytes, 0, mpBytes.length );
|
||||
Arrays.fill( mpBytesBuf.array(), (byte) 0 );
|
||||
|
||||
return scrypt( masterKeySalt, mpBytes );
|
||||
}
|
||||
|
||||
protected byte[] scrypt(final byte[] masterKeySalt, final byte[] mpBytes) {
|
||||
try {
|
||||
if (isAllowNative())
|
||||
return SCrypt.scrypt( mpBytes, masterKeySalt, MPConstant.scrypt_N, MPConstant.scrypt_r, MPConstant.scrypt_p, MPConstant.mpw_dkLen );
|
||||
else
|
||||
return SCrypt.scryptJ( mpBytes, masterKeySalt, MPConstant.scrypt_N, MPConstant.scrypt_r, MPConstant.scrypt_p, MPConstant.mpw_dkLen );
|
||||
}
|
||||
catch (GeneralSecurityException e) {
|
||||
logger.bug( e );
|
||||
return null;
|
||||
}
|
||||
finally {
|
||||
Arrays.fill( mpBytes, (byte) 0 );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encode(@Nonnull final String siteName, final MPSiteType siteType, @Nonnull UnsignedInteger siteCounter,
|
||||
final MPSiteVariant siteVariant, @Nullable final String siteContext) {
|
||||
Preconditions.checkArgument( siteType.getTypeClass() == MPSiteTypeClass.Generated );
|
||||
Preconditions.checkArgument( !siteName.isEmpty() );
|
||||
|
||||
logger.trc( "siteName: %s", siteName );
|
||||
logger.trc( "siteCounter: %d", siteCounter.longValue() );
|
||||
logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant );
|
||||
logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType );
|
||||
|
||||
if (siteCounter.longValue() == 0)
|
||||
siteCounter = UnsignedInteger.valueOf( (System.currentTimeMillis() / (300 * 1000)) * 300 );
|
||||
|
||||
String siteScope = siteVariant.getScope();
|
||||
byte[] siteNameBytes = siteName.getBytes( MPConstant.mpw_charset );
|
||||
byte[] siteNameLengthBytes = bytesForInt( siteName.length() );
|
||||
byte[] siteCounterBytes = bytesForInt( siteCounter );
|
||||
byte[] siteContextBytes = siteContext == null || siteContext.isEmpty()? null: siteContext.getBytes( MPConstant.mpw_charset );
|
||||
byte[] siteContextLengthBytes = bytesForInt( siteContextBytes == null? 0: siteContextBytes.length );
|
||||
logger.trc( "site scope: %s, context: %s", siteScope, siteContextBytes == null? "<empty>": siteContext );
|
||||
logger.trc( "seed from: hmac-sha256(masterKey, %s | %s | %s | %s | %s | %s)", siteScope, CodeUtils.encodeHex( siteNameLengthBytes ),
|
||||
siteName, CodeUtils.encodeHex( siteCounterBytes ), CodeUtils.encodeHex( siteContextLengthBytes ),
|
||||
siteContextBytes == null? "(null)": siteContext );
|
||||
|
||||
byte[] sitePasswordInfo = Bytes.concat( siteScope.getBytes( MPConstant.mpw_charset ), siteNameLengthBytes, siteNameBytes, siteCounterBytes );
|
||||
if (siteContextBytes != null)
|
||||
sitePasswordInfo = Bytes.concat( sitePasswordInfo, siteContextLengthBytes, siteContextBytes );
|
||||
logger.trc( "sitePasswordInfo ID: %s", CodeUtils.encodeHex( idForBytes( sitePasswordInfo ) ) );
|
||||
|
||||
byte[] sitePasswordSeedBytes = MPConstant.mpw_digest.of( getKey(), sitePasswordInfo );
|
||||
int[] sitePasswordSeed = new int[sitePasswordSeedBytes.length];
|
||||
for (int i = 0; i < sitePasswordSeedBytes.length; ++i) {
|
||||
ByteBuffer buf = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( ByteOrder.BIG_ENDIAN );
|
||||
Arrays.fill( buf.array(), sitePasswordSeedBytes[i] > 0? (byte) 0x00: (byte) 0xFF );
|
||||
buf.position( 2 );
|
||||
buf.put( sitePasswordSeedBytes[i] ).rewind();
|
||||
sitePasswordSeed[i] = buf.getInt() & 0xFFFF;
|
||||
}
|
||||
logger.trc( "sitePasswordSeed ID: %s", CodeUtils.encodeHex( idForBytes( sitePasswordSeedBytes ) ) );
|
||||
|
||||
Preconditions.checkState( sitePasswordSeed.length > 0 );
|
||||
int templateIndex = sitePasswordSeed[0];
|
||||
MPTemplate template = siteType.getTemplateAtRollingIndex( templateIndex );
|
||||
logger.trc( "type %s, template: %s", siteType, template.getTemplateString() );
|
||||
|
||||
StringBuilder password = new StringBuilder( template.length() );
|
||||
for (int i = 0; i < template.length(); ++i) {
|
||||
int characterIndex = sitePasswordSeed[i + 1];
|
||||
MPTemplateCharacterClass characterClass = template.getCharacterClassAtIndex( i );
|
||||
char passwordCharacter = characterClass.getCharacterAtRollingIndex( characterIndex );
|
||||
logger.trc( "class %c, index %d (0x%02X) -> character: %c", characterClass.getIdentifier(), characterIndex,
|
||||
sitePasswordSeed[i + 1], passwordCharacter );
|
||||
|
||||
password.append( passwordCharacter );
|
||||
}
|
||||
|
||||
return password.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] bytesForInt(final int number) {
|
||||
return ByteBuffer.allocate( MP_intLen / Byte.SIZE ).order( MPConstant.mpw_byteOrder ).putInt( number ).array();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] bytesForInt(@Nonnull final UnsignedInteger number) {
|
||||
return ByteBuffer.allocate( MP_intLen / Byte.SIZE ).order( MPConstant.mpw_byteOrder ).putInt( number.intValue() ).array();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] idForBytes(final byte[] bytes) {
|
||||
return MPConstant.mpw_hash.of( bytes );
|
||||
}
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lyndir.lhunath.opal.system.*;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* bugs:
|
||||
* - V2: miscounted the byte-length fromInt multi-byte full names.
|
||||
* - V1: miscounted the byte-length fromInt multi-byte site names.
|
||||
*
|
||||
* @author lhunath, 2014-08-30
|
||||
*/
|
||||
public class MasterKeyV1 extends MasterKeyV0 {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MasterKeyV1.class );
|
||||
|
||||
public MasterKeyV1(final String fullName) {
|
||||
super( fullName );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getAlgorithmVersion() {
|
||||
|
||||
return Version.V1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encode(@Nonnull final String siteName, final MPSiteType siteType, @Nonnull UnsignedInteger siteCounter,
|
||||
final MPSiteVariant siteVariant, @Nullable final String siteContext) {
|
||||
Preconditions.checkArgument( siteType.getTypeClass() == MPSiteTypeClass.Generated );
|
||||
Preconditions.checkArgument( !siteName.isEmpty() );
|
||||
|
||||
logger.trc( "siteName: %s", siteName );
|
||||
logger.trc( "siteCounter: %d", siteCounter.longValue() );
|
||||
logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant );
|
||||
logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType );
|
||||
|
||||
if (siteCounter.longValue() == 0)
|
||||
siteCounter = UnsignedInteger.valueOf( (System.currentTimeMillis() / (300 * 1000)) * 300 );
|
||||
|
||||
String siteScope = siteVariant.getScope();
|
||||
byte[] siteNameBytes = siteName.getBytes( MPConstant.mpw_charset );
|
||||
byte[] siteNameLengthBytes = bytesForInt( siteName.length() );
|
||||
byte[] siteCounterBytes = bytesForInt( siteCounter );
|
||||
byte[] siteContextBytes = siteContext == null || siteContext.isEmpty()? null: siteContext.getBytes( MPConstant.mpw_charset );
|
||||
byte[] siteContextLengthBytes = bytesForInt( siteContextBytes == null? 0: siteContextBytes.length );
|
||||
logger.trc( "site scope: %s, context: %s", siteScope, siteContextBytes == null? "<empty>": siteContext );
|
||||
logger.trc( "seed from: hmac-sha256(masterKey, %s | %s | %s | %s | %s | %s)", siteScope, CodeUtils.encodeHex( siteNameLengthBytes ),
|
||||
siteName, CodeUtils.encodeHex( siteCounterBytes ), CodeUtils.encodeHex( siteContextLengthBytes ),
|
||||
siteContextBytes == null? "(null)": siteContext );
|
||||
|
||||
byte[] sitePasswordInfo = Bytes.concat( siteScope.getBytes( MPConstant.mpw_charset ), siteNameLengthBytes, siteNameBytes, siteCounterBytes );
|
||||
if (siteContextBytes != null)
|
||||
sitePasswordInfo = Bytes.concat( sitePasswordInfo, siteContextLengthBytes, siteContextBytes );
|
||||
logger.trc( "sitePasswordInfo ID: %s", CodeUtils.encodeHex( idForBytes( sitePasswordInfo ) ) );
|
||||
|
||||
byte[] sitePasswordSeed = MPConstant.mpw_digest.of( getKey(), sitePasswordInfo );
|
||||
logger.trc( "sitePasswordSeed ID: %s", CodeUtils.encodeHex( idForBytes( sitePasswordSeed ) ) );
|
||||
|
||||
Preconditions.checkState( sitePasswordSeed.length > 0 );
|
||||
int templateIndex = sitePasswordSeed[0] & 0xFF; // Mask the integer's sign.
|
||||
MPTemplate template = siteType.getTemplateAtRollingIndex( templateIndex );
|
||||
logger.trc( "type %s, template: %s", siteType, template.getTemplateString() );
|
||||
|
||||
StringBuilder password = new StringBuilder( template.length() );
|
||||
for (int i = 0; i < template.length(); ++i) {
|
||||
int characterIndex = sitePasswordSeed[i + 1] & 0xFF; // Mask the integer's sign.
|
||||
MPTemplateCharacterClass characterClass = template.getCharacterClassAtIndex( i );
|
||||
char passwordCharacter = characterClass.getCharacterAtRollingIndex( characterIndex );
|
||||
logger.trc( "class %c, index %d (0x%02X) -> character: %c", characterClass.getIdentifier(), characterIndex,
|
||||
sitePasswordSeed[i + 1], passwordCharacter );
|
||||
|
||||
password.append( passwordCharacter );
|
||||
}
|
||||
|
||||
return password.toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* bugs:
|
||||
* - V2: miscounted the byte-length fromInt multi-byte full names.
|
||||
*
|
||||
* @author lhunath, 2014-08-30
|
||||
*/
|
||||
public class MasterKeyV2 extends MasterKeyV1 {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MasterKeyV2.class );
|
||||
|
||||
public MasterKeyV2(final String fullName) {
|
||||
super( fullName );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getAlgorithmVersion() {
|
||||
|
||||
return Version.V2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encode(@Nonnull final String siteName, final MPSiteType siteType, @Nonnull UnsignedInteger siteCounter,
|
||||
final MPSiteVariant siteVariant, @Nullable final String siteContext) {
|
||||
Preconditions.checkArgument( siteType.getTypeClass() == MPSiteTypeClass.Generated );
|
||||
Preconditions.checkArgument( !siteName.isEmpty() );
|
||||
|
||||
logger.trc( "siteName: %s", siteName );
|
||||
logger.trc( "siteCounter: %d", siteCounter.longValue() );
|
||||
logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant );
|
||||
logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType );
|
||||
|
||||
if (siteCounter.longValue() == 0)
|
||||
siteCounter = UnsignedInteger.valueOf( (System.currentTimeMillis() / (300 * 1000)) * 300 );
|
||||
|
||||
String siteScope = siteVariant.getScope();
|
||||
byte[] siteNameBytes = siteName.getBytes( MPConstant.mpw_charset );
|
||||
byte[] siteNameLengthBytes = bytesForInt( siteNameBytes.length );
|
||||
byte[] siteCounterBytes = bytesForInt( siteCounter );
|
||||
byte[] siteContextBytes = siteContext == null || siteContext.isEmpty()? null: siteContext.getBytes( MPConstant.mpw_charset );
|
||||
byte[] siteContextLengthBytes = bytesForInt( siteContextBytes == null? 0: siteContextBytes.length );
|
||||
logger.trc( "site scope: %s, context: %s", siteScope, siteContextBytes == null? "<empty>": siteContext );
|
||||
logger.trc( "seed from: hmac-sha256(masterKey, %s | %s | %s | %s | %s | %s)", siteScope, CodeUtils.encodeHex( siteNameLengthBytes ),
|
||||
siteName, CodeUtils.encodeHex( siteCounterBytes ), CodeUtils.encodeHex( siteContextLengthBytes ),
|
||||
siteContextBytes == null? "(null)": siteContext );
|
||||
|
||||
byte[] sitePasswordInfo = Bytes.concat( siteScope.getBytes( MPConstant.mpw_charset ), siteNameLengthBytes, siteNameBytes, siteCounterBytes );
|
||||
if (siteContextBytes != null)
|
||||
sitePasswordInfo = Bytes.concat( sitePasswordInfo, siteContextLengthBytes, siteContextBytes );
|
||||
logger.trc( "sitePasswordInfo ID: %s", CodeUtils.encodeHex( idForBytes( sitePasswordInfo ) ) );
|
||||
|
||||
byte[] sitePasswordSeed = MPConstant.mpw_digest.of( getKey(), sitePasswordInfo );
|
||||
logger.trc( "sitePasswordSeed ID: %s", CodeUtils.encodeHex( idForBytes( sitePasswordSeed ) ) );
|
||||
|
||||
Preconditions.checkState( sitePasswordSeed.length > 0 );
|
||||
int templateIndex = sitePasswordSeed[0] & 0xFF; // Mask the integer's sign.
|
||||
MPTemplate template = siteType.getTemplateAtRollingIndex( templateIndex );
|
||||
logger.trc( "type %s, template: %s", siteType, template.getTemplateString() );
|
||||
|
||||
StringBuilder password = new StringBuilder( template.length() );
|
||||
for (int i = 0; i < template.length(); ++i) {
|
||||
int characterIndex = sitePasswordSeed[i + 1] & 0xFF; // Mask the integer's sign.
|
||||
MPTemplateCharacterClass characterClass = template.getCharacterClassAtIndex( i );
|
||||
char passwordCharacter = characterClass.getCharacterAtRollingIndex( characterIndex );
|
||||
logger.trc( "class %c, index %d (0x%02X) -> character: %c", characterClass.getIdentifier(), characterIndex,
|
||||
sitePasswordSeed[i + 1], passwordCharacter );
|
||||
|
||||
password.append( passwordCharacter );
|
||||
}
|
||||
|
||||
return password.toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* bugs:
|
||||
* - no known issues.
|
||||
*
|
||||
* @author lhunath, 2014-08-30
|
||||
*/
|
||||
public class MasterKeyV3 extends MasterKeyV2 {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MasterKeyV3.class );
|
||||
|
||||
public MasterKeyV3(final String fullName) {
|
||||
super( fullName );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getAlgorithmVersion() {
|
||||
|
||||
return Version.V3;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected byte[] deriveKey(final char[] masterPassword) {
|
||||
byte[] fullNameBytes = getFullName().getBytes( MPConstant.mpw_charset );
|
||||
byte[] fullNameLengthBytes = bytesForInt( fullNameBytes.length );
|
||||
|
||||
String mpKeyScope = MPSiteVariant.Password.getScope();
|
||||
byte[] masterKeySalt = Bytes.concat( mpKeyScope.getBytes( MPConstant.mpw_charset ), fullNameLengthBytes, fullNameBytes );
|
||||
logger.trc( "key scope: %s", mpKeyScope );
|
||||
logger.trc( "masterKeySalt ID: %s", CodeUtils.encodeHex( idForBytes( masterKeySalt ) ) );
|
||||
|
||||
ByteBuffer mpBytesBuf = MPConstant.mpw_charset.encode( CharBuffer.wrap( masterPassword ) );
|
||||
byte[] mpBytes = new byte[mpBytesBuf.remaining()];
|
||||
mpBytesBuf.get( mpBytes, 0, mpBytes.length );
|
||||
Arrays.fill( mpBytesBuf.array(), (byte) 0 );
|
||||
|
||||
return scrypt( masterKeySalt, mpBytes );
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
*
|
||||
* @author lhunath, 15-02-04
|
||||
*/
|
||||
|
||||
|
||||
@ParametersAreNonnullByDefault package com.lyndir.masterpassword;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
19
core/java/model/build.gradle
Normal file
19
core/java/model/build.gradle
Normal file
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'net.ltgt.apt' version '0.9'
|
||||
}
|
||||
|
||||
description = 'Master Password Site Model'
|
||||
|
||||
dependencies {
|
||||
compile project(':masterpassword-algorithm')
|
||||
|
||||
compile group: 'joda-time', name: 'joda-time', version:'2.4'
|
||||
|
||||
compileOnly group: 'com.google.auto.value', name: 'auto-value', version: '1.2'
|
||||
apt group: 'com.google.auto.value', name: 'auto-value', version: '1.2'
|
||||
|
||||
testCompile group: 'org.testng', name: 'testng', version:'6.8.5'
|
||||
testCompile group: 'ch.qos.logback', name: 'logback-classic', version:'1.1.2'
|
||||
}
|
||||
test.useTestNG()
|
55
core/java/model/pom.xml
Normal file
55
core/java/model/pom.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<!-- PROJECT METADATA -->
|
||||
<parent>
|
||||
<groupId>com.lyndir.masterpassword</groupId>
|
||||
<artifactId>masterpassword</artifactId>
|
||||
<version>GIT-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<name>Master Password Site Model</name>
|
||||
<description>A persistence model for Master Password sites.</description>
|
||||
|
||||
<artifactId>masterpassword-model</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<!-- DEPENDENCY MANAGEMENT -->
|
||||
<dependencies>
|
||||
|
||||
<!-- PROJECT REFERENCES -->
|
||||
<dependency>
|
||||
<groupId>com.lyndir.masterpassword</groupId>
|
||||
<artifactId>masterpassword-algorithm</artifactId>
|
||||
<version>GIT-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<!-- EXTERNAL DEPENDENCIES -->
|
||||
<dependency>
|
||||
<groupId>joda-time</groupId>
|
||||
<artifactId>joda-time</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.value</groupId>
|
||||
<artifactId>auto-value</artifactId>
|
||||
<version>1.0-rc1</version>
|
||||
<scope>provided</scope>
|
||||
</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>
|
||||
|
||||
</project>
|
@@ -0,0 +1,19 @@
|
||||
package com.lyndir.masterpassword.model;
|
||||
|
||||
/**
|
||||
* @author lhunath, 14-12-17
|
||||
*/
|
||||
public class IncorrectMasterPasswordException extends Exception {
|
||||
|
||||
private final MPUser user;
|
||||
|
||||
public IncorrectMasterPasswordException(final MPUser user) {
|
||||
super( "Incorrect master password for user: " + user.getFullName() );
|
||||
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public MPUser getUser() {
|
||||
return user;
|
||||
}
|
||||
}
|
@@ -0,0 +1,143 @@
|
||||
package com.lyndir.masterpassword.model;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
|
||||
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lyndir.masterpassword.*;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.Nullable;
|
||||
import org.joda.time.Instant;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 14-12-05
|
||||
*/
|
||||
public class MPSite {
|
||||
|
||||
public static final MPSiteType DEFAULT_TYPE = MPSiteType.GeneratedLong;
|
||||
public static final UnsignedInteger DEFAULT_COUNTER = UnsignedInteger.valueOf( 1 );
|
||||
|
||||
private final MPUser user;
|
||||
private MasterKey.Version algorithmVersion;
|
||||
private Instant lastUsed;
|
||||
private String siteName;
|
||||
private MPSiteType siteType;
|
||||
private UnsignedInteger siteCounter;
|
||||
private int uses;
|
||||
private String loginName;
|
||||
|
||||
public MPSite(final MPUser user, final String siteName) {
|
||||
this( user, siteName, DEFAULT_TYPE, DEFAULT_COUNTER );
|
||||
}
|
||||
|
||||
public MPSite(final MPUser user, final String siteName, final MPSiteType siteType, final UnsignedInteger siteCounter) {
|
||||
this.user = user;
|
||||
this.algorithmVersion = MasterKey.Version.CURRENT;
|
||||
this.lastUsed = new Instant();
|
||||
this.siteName = siteName;
|
||||
this.siteType = siteType;
|
||||
this.siteCounter = siteCounter;
|
||||
}
|
||||
|
||||
protected MPSite(final MPUser user, final MasterKey.Version algorithmVersion, final Instant lastUsed, final String siteName,
|
||||
final MPSiteType siteType, final UnsignedInteger siteCounter, final int uses, @Nullable final String loginName,
|
||||
@Nullable final String importContent) {
|
||||
this.user = user;
|
||||
this.algorithmVersion = algorithmVersion;
|
||||
this.lastUsed = lastUsed;
|
||||
this.siteName = siteName;
|
||||
this.siteType = siteType;
|
||||
this.siteCounter = siteCounter;
|
||||
this.uses = uses;
|
||||
this.loginName = loginName;
|
||||
}
|
||||
|
||||
public String resultFor(final MasterKey masterKey) {
|
||||
return resultFor( masterKey, MPSiteVariant.Password, null );
|
||||
}
|
||||
|
||||
public String resultFor(final MasterKey masterKey, final MPSiteVariant variant, @Nullable final String context) {
|
||||
return masterKey.encode( siteName, siteType, siteCounter, variant, context );
|
||||
}
|
||||
|
||||
public MPUser getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String exportContent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public MasterKey.Version getAlgorithmVersion() {
|
||||
return algorithmVersion;
|
||||
}
|
||||
|
||||
public void setAlgorithmVersion(final MasterKey.Version mpVersion) {
|
||||
this.algorithmVersion = mpVersion;
|
||||
}
|
||||
|
||||
public Instant getLastUsed() {
|
||||
return lastUsed;
|
||||
}
|
||||
|
||||
public void updateLastUsed() {
|
||||
lastUsed = new Instant();
|
||||
user.updateLastUsed();
|
||||
}
|
||||
|
||||
public String getSiteName() {
|
||||
return siteName;
|
||||
}
|
||||
|
||||
public void setSiteName(final String siteName) {
|
||||
this.siteName = siteName;
|
||||
}
|
||||
|
||||
public MPSiteType getSiteType() {
|
||||
return siteType;
|
||||
}
|
||||
|
||||
public void setSiteType(final MPSiteType siteType) {
|
||||
this.siteType = siteType;
|
||||
}
|
||||
|
||||
public UnsignedInteger getSiteCounter() {
|
||||
return siteCounter;
|
||||
}
|
||||
|
||||
public void setSiteCounter(final UnsignedInteger siteCounter) {
|
||||
this.siteCounter = siteCounter;
|
||||
}
|
||||
|
||||
public int getUses() {
|
||||
return uses;
|
||||
}
|
||||
|
||||
public void setUses(final int uses) {
|
||||
this.uses = uses;
|
||||
}
|
||||
|
||||
public String getLoginName() {
|
||||
return loginName;
|
||||
}
|
||||
|
||||
public void setLoginName(final String loginName) {
|
||||
this.loginName = loginName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
return this == obj || obj instanceof MPSite && Objects.equals( siteName, ((MPSite) obj).siteName );
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode( siteName );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return strf( "{MPSite: %s}", siteName );
|
||||
}
|
||||
}
|
@@ -0,0 +1,131 @@
|
||||
package com.lyndir.masterpassword.model;
|
||||
|
||||
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.Preconditions;
|
||||
import com.lyndir.masterpassword.MasterKey;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import org.joda.time.Instant;
|
||||
import org.joda.time.format.DateTimeFormatter;
|
||||
import org.joda.time.format.ISODateTimeFormat;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 14-12-07
|
||||
*/
|
||||
public class MPSiteMarshaller {
|
||||
|
||||
private static final DateTimeFormatter rfc3339 = ISODateTimeFormat.dateTimeNoMillis();
|
||||
|
||||
private final StringBuilder export = new StringBuilder();
|
||||
private ContentMode contentMode = ContentMode.PROTECTED;
|
||||
private MasterKey masterKey;
|
||||
|
||||
public static MPSiteMarshaller marshallSafe(final MPUser user) {
|
||||
MPSiteMarshaller marshaller = new MPSiteMarshaller();
|
||||
marshaller.marshallHeaderForSafeContent( user );
|
||||
for (MPSite site : user.getSites())
|
||||
marshaller.marshallSite( site );
|
||||
|
||||
return marshaller;
|
||||
}
|
||||
|
||||
public static MPSiteMarshaller marshallVisible(final MPUser user, final MasterKey masterKey) {
|
||||
MPSiteMarshaller marshaller = new MPSiteMarshaller();
|
||||
marshaller.marshallHeaderForVisibleContentWithKey( user, masterKey );
|
||||
for (MPSite site : user.getSites())
|
||||
marshaller.marshallSite( site );
|
||||
|
||||
return marshaller;
|
||||
}
|
||||
|
||||
private String marshallHeaderForSafeContent(final MPUser user) {
|
||||
return marshallHeader( ContentMode.PROTECTED, user, null );
|
||||
}
|
||||
|
||||
private String marshallHeaderForVisibleContentWithKey(final MPUser user, final MasterKey masterKey) {
|
||||
return marshallHeader( ContentMode.VISIBLE, user, masterKey );
|
||||
}
|
||||
|
||||
private String marshallHeader(final ContentMode contentMode, final MPUser user, @Nullable final MasterKey masterKey) {
|
||||
this.contentMode = contentMode;
|
||||
this.masterKey = masterKey;
|
||||
|
||||
StringBuilder header = new StringBuilder();
|
||||
header.append( "# Master Password site export\n" );
|
||||
header.append( "# " ).append( this.contentMode.description() ).append( '\n' );
|
||||
header.append( "# \n" );
|
||||
header.append( "##\n" );
|
||||
header.append( "# Format: 1\n" );
|
||||
header.append( "# Date: " ).append( rfc3339.print( new Instant() ) ).append( '\n' );
|
||||
header.append( "# User Name: " ).append( user.getFullName() ).append( '\n' );
|
||||
header.append( "# Full Name: " ).append( user.getFullName() ).append( '\n' );
|
||||
header.append( "# Avatar: " ).append( user.getAvatar() ).append( '\n' );
|
||||
header.append( "# Key ID: " ).append( user.exportKeyID() ).append( '\n' );
|
||||
header.append( "# Version: " ).append( MasterKey.Version.CURRENT.toBundleVersion() ).append( '\n' );
|
||||
header.append( "# Algorithm: " ).append( MasterKey.Version.CURRENT.toInt() ).append( '\n' );
|
||||
header.append( "# Default Type: " ).append( user.getDefaultType().getType() ).append( '\n' );
|
||||
header.append( "# Passwords: " ).append( this.contentMode.name() ).append( '\n' );
|
||||
header.append( "##\n" );
|
||||
header.append( "#\n" );
|
||||
header.append( "# Last Times Password Login\t Site\tSite\n" );
|
||||
header.append( "# used used type name\t name\tpassword\n" );
|
||||
|
||||
export.append( header );
|
||||
return header.toString();
|
||||
}
|
||||
|
||||
public String marshallSite(MPSite site) {
|
||||
String exportLine = strf( "%s %8d %8s %25s\t%25s\t%s", //
|
||||
rfc3339.print( site.getLastUsed() ), // lastUsed
|
||||
site.getUses(), // uses
|
||||
strf( "%d:%d:%d", //
|
||||
site.getSiteType().getType(), // type
|
||||
site.getAlgorithmVersion().toInt(), // algorithm
|
||||
site.getSiteCounter().intValue() ), // counter
|
||||
ifNotNullElse( site.getLoginName(), "" ), // loginName
|
||||
site.getSiteName(), // siteName
|
||||
ifNotNullElse( contentMode.contentForSite( site, masterKey ), "" ) // password
|
||||
);
|
||||
export.append( exportLine ).append( '\n' );
|
||||
|
||||
return exportLine;
|
||||
}
|
||||
|
||||
public String getExport() {
|
||||
return export.toString();
|
||||
}
|
||||
|
||||
public ContentMode getContentMode() {
|
||||
return contentMode;
|
||||
}
|
||||
|
||||
public enum ContentMode {
|
||||
PROTECTED( "Export of site names and stored passwords (unless device-private) encrypted with the master key." ) {
|
||||
@Override
|
||||
public String contentForSite(final MPSite site, @Nullable final MasterKey masterKey) {
|
||||
return site.exportContent();
|
||||
}
|
||||
},
|
||||
VISIBLE( "Export of site names and passwords in clear-text." ) {
|
||||
@Override
|
||||
public String contentForSite(final MPSite site, @Nonnull final MasterKey masterKey) {
|
||||
return site.resultFor( Preconditions.checkNotNull( masterKey, "Master key is required when content mode is VISIBLE." ) );
|
||||
}
|
||||
};
|
||||
|
||||
private final String description;
|
||||
|
||||
ContentMode(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String description() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public abstract String contentForSite(final MPSite site, final MasterKey masterKey);
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package com.lyndir.masterpassword.model;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 14-12-07
|
||||
*/
|
||||
public class MPSiteResult {
|
||||
|
||||
private final MPSite site;
|
||||
|
||||
public MPSiteResult(final MPSite site) {
|
||||
this.site = site;
|
||||
}
|
||||
|
||||
public MPSite getSite() {
|
||||
return site;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
return this == obj || obj instanceof MPSiteResult && Objects.equals( site, ((MPSiteResult) obj).site );
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode( site );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return strf( "{MPSiteResult: %s}", site );
|
||||
}
|
||||
}
|
@@ -0,0 +1,160 @@
|
||||
package com.lyndir.masterpassword.model;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
||||
import com.lyndir.lhunath.opal.system.util.NNOperation;
|
||||
import com.lyndir.masterpassword.MPSiteType;
|
||||
import com.lyndir.masterpassword.MasterKey;
|
||||
import java.io.*;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.format.DateTimeFormatter;
|
||||
import org.joda.time.format.ISODateTimeFormat;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 14-12-07
|
||||
*/
|
||||
public class MPSiteUnmarshaller {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MPSite.class );
|
||||
private static final DateTimeFormatter rfc3339 = ISODateTimeFormat.dateTimeNoMillis();
|
||||
private static final Pattern[] unmarshallFormats = new Pattern[]{
|
||||
Pattern.compile( "^([^ ]+) +(\\d+) +(\\d+)(:\\d+)? +([^\t]+)\t(.*)" ),
|
||||
Pattern.compile( "^([^ ]+) +(\\d+) +(\\d+)(:\\d+)?(:\\d+)? +([^\t]*)\t *([^\t]+)\t(.*)" ) };
|
||||
private static final Pattern headerFormat = Pattern.compile( "^#\\s*([^:]+): (.*)" );
|
||||
|
||||
private final int importFormat;
|
||||
private final int mpVersion;
|
||||
private final boolean clearContent;
|
||||
private final MPUser user;
|
||||
|
||||
@Nonnull
|
||||
public static MPSiteUnmarshaller unmarshall(@Nonnull File file)
|
||||
throws IOException {
|
||||
try (Reader reader = new FileReader( file )) {
|
||||
return unmarshall( CharStreams.readLines( reader ) );
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static MPSiteUnmarshaller unmarshall(@Nonnull List<String> lines) {
|
||||
byte[] keyID = null;
|
||||
String fullName = null;
|
||||
int mpVersion = 0, importFormat = 0, avatar = 0;
|
||||
boolean clearContent = false, headerStarted = false;
|
||||
MPSiteType defaultType = MPSiteType.GeneratedLong;
|
||||
MPSiteUnmarshaller marshaller = null;
|
||||
final ImmutableList.Builder<MPSite> sites = ImmutableList.builder();
|
||||
|
||||
for (String line : lines)
|
||||
// Header delimitor.
|
||||
if (line.startsWith( "##" ))
|
||||
if (!headerStarted)
|
||||
// Starts the header.
|
||||
headerStarted = true;
|
||||
else
|
||||
// Ends the header.
|
||||
marshaller = new MPSiteUnmarshaller( importFormat, mpVersion, fullName, keyID, avatar, defaultType, clearContent );
|
||||
|
||||
// Comment.
|
||||
else if (line.startsWith( "#" )) {
|
||||
if (headerStarted && marshaller == null) {
|
||||
// In header.
|
||||
Matcher headerMatcher = headerFormat.matcher( line );
|
||||
if (headerMatcher.matches()) {
|
||||
String name = headerMatcher.group( 1 ), value = headerMatcher.group( 2 );
|
||||
if ("Full Name".equalsIgnoreCase( name ) || "User Name".equalsIgnoreCase( name ))
|
||||
fullName = value;
|
||||
else if ("Key ID".equalsIgnoreCase( name ))
|
||||
keyID = CodeUtils.decodeHex( value );
|
||||
else if ("Algorithm".equalsIgnoreCase( name ))
|
||||
mpVersion = ConversionUtils.toIntegerNN( value );
|
||||
else if ("Format".equalsIgnoreCase( name ))
|
||||
importFormat = ConversionUtils.toIntegerNN( value );
|
||||
else if ("Avatar".equalsIgnoreCase( name ))
|
||||
avatar = ConversionUtils.toIntegerNN( value );
|
||||
else if ("Passwords".equalsIgnoreCase( name ))
|
||||
clearContent = value.equalsIgnoreCase( "visible" );
|
||||
else if ("Default Type".equalsIgnoreCase( name ))
|
||||
defaultType = MPSiteType.forType( ConversionUtils.toIntegerNN( value ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No comment.
|
||||
else if (marshaller != null)
|
||||
ifNotNull( marshaller.unmarshallSite( line ), new NNOperation<MPSite>() {
|
||||
@Override
|
||||
public void apply(@Nonnull final MPSite site) {
|
||||
sites.add( site );
|
||||
}
|
||||
} );
|
||||
|
||||
return Preconditions.checkNotNull( marshaller, "No full header found in import file." );
|
||||
}
|
||||
|
||||
protected MPSiteUnmarshaller(final int importFormat, final int mpVersion, final String fullName, final byte[] keyID, final int avatar,
|
||||
final MPSiteType defaultType, final boolean clearContent) {
|
||||
this.importFormat = importFormat;
|
||||
this.mpVersion = mpVersion;
|
||||
this.clearContent = clearContent;
|
||||
|
||||
user = new MPUser( fullName, keyID, MasterKey.Version.fromInt( mpVersion ), avatar, defaultType, new DateTime( 0 ) );
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public MPSite unmarshallSite(@Nonnull String siteLine) {
|
||||
Matcher siteMatcher = unmarshallFormats[importFormat].matcher( siteLine );
|
||||
if (!siteMatcher.matches())
|
||||
return null;
|
||||
|
||||
MPSite site;
|
||||
switch (importFormat) {
|
||||
case 0:
|
||||
site = new MPSite( user, //
|
||||
MasterKey.Version.fromInt( ConversionUtils.toIntegerNN( siteMatcher.group( 4 ).replace( ":", "" ) ) ), //
|
||||
rfc3339.parseDateTime( siteMatcher.group( 1 ) ).toInstant(), //
|
||||
siteMatcher.group( 5 ), //
|
||||
MPSiteType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ), MPSite.DEFAULT_COUNTER, //
|
||||
ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), //
|
||||
null, //
|
||||
siteMatcher.group( 6 ) );
|
||||
break;
|
||||
|
||||
case 1:
|
||||
site = new MPSite( user, //
|
||||
MasterKey.Version.fromInt( ConversionUtils.toIntegerNN( siteMatcher.group( 4 ).replace( ":", "" ) ) ), //
|
||||
rfc3339.parseDateTime( siteMatcher.group( 1 ) ).toInstant(), //
|
||||
siteMatcher.group( 7 ), //
|
||||
MPSiteType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
|
||||
UnsignedInteger.valueOf( siteMatcher.group( 5 ).replace( ":", "" ) ), //
|
||||
ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), //
|
||||
siteMatcher.group( 6 ), //
|
||||
siteMatcher.group( 8 ) );
|
||||
break;
|
||||
|
||||
default:
|
||||
throw logger.bug( "Unexpected format: %d", importFormat );
|
||||
}
|
||||
|
||||
user.addSite( site );
|
||||
return site;
|
||||
}
|
||||
|
||||
public MPUser getUser() {
|
||||
return user;
|
||||
}
|
||||
}
|
152
core/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java
Executable file
152
core/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java
Executable file
@@ -0,0 +1,152 @@
|
||||
package com.lyndir.masterpassword.model;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||
import com.lyndir.masterpassword.MPSiteType;
|
||||
import com.lyndir.masterpassword.MasterKey;
|
||||
import java.util.*;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import org.joda.time.*;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 14-12-07
|
||||
*/
|
||||
public class MPUser implements Comparable<MPUser> {
|
||||
|
||||
private final String fullName;
|
||||
private final Collection<MPSite> sites = Sets.newHashSet();
|
||||
|
||||
@Nullable
|
||||
private byte[] keyID;
|
||||
private final MasterKey.Version algorithmVersion;
|
||||
private int avatar;
|
||||
private MPSiteType defaultType;
|
||||
private ReadableInstant lastUsed;
|
||||
|
||||
public MPUser(final String fullName) {
|
||||
this( fullName, null );
|
||||
}
|
||||
|
||||
public MPUser(final String fullName, @Nullable final byte[] keyID) {
|
||||
this( fullName, keyID, MasterKey.Version.CURRENT, 0, MPSiteType.GeneratedLong, new DateTime() );
|
||||
}
|
||||
|
||||
public MPUser(final String fullName, @Nullable final byte[] keyID, final MasterKey.Version algorithmVersion, final int avatar,
|
||||
final MPSiteType defaultType, final ReadableInstant lastUsed) {
|
||||
this.fullName = fullName;
|
||||
this.keyID = keyID;
|
||||
this.algorithmVersion = algorithmVersion;
|
||||
this.avatar = avatar;
|
||||
this.defaultType = defaultType;
|
||||
this.lastUsed = lastUsed;
|
||||
}
|
||||
|
||||
public Collection<MPSiteResult> findSitesByName(String query) {
|
||||
ImmutableList.Builder<MPSiteResult> results = ImmutableList.builder();
|
||||
for (MPSite site : getSites())
|
||||
if (site.getSiteName().startsWith( query ))
|
||||
results.add( new MPSiteResult( site ) );
|
||||
|
||||
return results.build();
|
||||
}
|
||||
|
||||
public void addSite(final MPSite site) {
|
||||
sites.add( site );
|
||||
}
|
||||
|
||||
public void deleteSite(final MPSite site) {
|
||||
sites.remove( site );
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
public boolean hasKeyID() {
|
||||
return keyID != null;
|
||||
}
|
||||
|
||||
public String exportKeyID() {
|
||||
return CodeUtils.encodeHex( keyID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an authentication attempt against the keyID for this user.
|
||||
*
|
||||
* Note: If this user doesn't have a keyID set yet, authentication will always succeed and the key ID will be set as a result.
|
||||
*
|
||||
* @param masterPassword The password to authenticate with.
|
||||
*
|
||||
* @return The master key for the user if authentication was successful.
|
||||
*
|
||||
* @throws IncorrectMasterPasswordException If authentication fails due to the given master password not matching the user's keyID.
|
||||
*/
|
||||
@Nonnull
|
||||
public MasterKey authenticate(final char[] masterPassword)
|
||||
throws IncorrectMasterPasswordException {
|
||||
MasterKey masterKey = MasterKey.create( algorithmVersion, getFullName(), masterPassword );
|
||||
if (keyID == null || keyID.length == 0)
|
||||
keyID = masterKey.getKeyID();
|
||||
else if (!Arrays.equals( masterKey.getKeyID(), keyID ))
|
||||
throw new IncorrectMasterPasswordException( this );
|
||||
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
public int getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public void setAvatar(final int avatar) {
|
||||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
public MPSiteType getDefaultType() {
|
||||
return defaultType;
|
||||
}
|
||||
|
||||
public void setDefaultType(final MPSiteType defaultType) {
|
||||
this.defaultType = defaultType;
|
||||
}
|
||||
|
||||
public ReadableInstant getLastUsed() {
|
||||
return lastUsed;
|
||||
}
|
||||
|
||||
public void updateLastUsed() {
|
||||
this.lastUsed = new Instant();
|
||||
}
|
||||
|
||||
public Iterable<MPSite> getSites() {
|
||||
return sites;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
return this == obj || obj instanceof MPUser && Objects.equals( fullName, ((MPUser) obj).fullName );
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode( fullName );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return strf( "{MPUser: %s}", fullName );
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(final MPUser o) {
|
||||
int comparison = lastUsed.compareTo( o.lastUsed );
|
||||
if (comparison == 0)
|
||||
comparison = fullName.compareTo( o.fullName );
|
||||
|
||||
return comparison;
|
||||
}
|
||||
}
|
@@ -0,0 +1,123 @@
|
||||
package com.lyndir.masterpassword.model;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||
|
||||
import com.google.common.base.*;
|
||||
import com.google.common.collect.*;
|
||||
import com.google.common.io.CharSink;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.masterpassword.MPConstant;
|
||||
import java.io.*;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* Manages user data stored in user-specific {@code .mpsites} files under {@code .mpw.d}.
|
||||
* @author lhunath, 14-12-07
|
||||
*/
|
||||
public class MPUserFileManager extends MPUserManager {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MPUserFileManager.class );
|
||||
private static final MPUserFileManager instance;
|
||||
|
||||
static {
|
||||
String rcDir = System.getenv( MPConstant.env_rcDir );
|
||||
if (rcDir != null)
|
||||
instance = create( new File( rcDir ) );
|
||||
else
|
||||
instance = create( new File( ifNotNullElseNullable( System.getProperty( "user.home" ), System.getenv( "HOME" ) ), ".mpw.d" ) );
|
||||
}
|
||||
|
||||
private final File userFilesDirectory;
|
||||
|
||||
public static MPUserFileManager get() {
|
||||
MPUserManager.instance = instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static MPUserFileManager create(final File userFilesDirectory) {
|
||||
return new MPUserFileManager( userFilesDirectory );
|
||||
}
|
||||
|
||||
protected MPUserFileManager(final File userFilesDirectory) {
|
||||
|
||||
super( unmarshallUsers( userFilesDirectory ) );
|
||||
this.userFilesDirectory = userFilesDirectory;
|
||||
}
|
||||
|
||||
private static Iterable<MPUser> unmarshallUsers(final File userFilesDirectory) {
|
||||
if (!userFilesDirectory.mkdirs() && !userFilesDirectory.isDirectory()) {
|
||||
logger.err( "Couldn't create directory for user files: %s", userFilesDirectory );
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
return FluentIterable.from( listUserFiles( userFilesDirectory ) ).transform( new Function<File, MPUser>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public MPUser apply(@Nullable final File file) {
|
||||
try {
|
||||
return MPSiteUnmarshaller.unmarshall( Preconditions.checkNotNull( file ) ).getUser();
|
||||
}
|
||||
catch (IOException e) {
|
||||
logger.err( e, "Couldn't read user from: %s", file );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} ).filter( Predicates.notNull() );
|
||||
}
|
||||
|
||||
private static ImmutableList<File> listUserFiles(final File userFilesDirectory) {
|
||||
return ImmutableList.copyOf( ifNotNullElse( userFilesDirectory.listFiles( new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(final File dir, final String name) {
|
||||
return name.endsWith( ".mpsites" );
|
||||
}
|
||||
} ), new File[0] ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addUser(final MPUser user) {
|
||||
super.addUser( user );
|
||||
save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUser(final MPUser user) {
|
||||
super.deleteUser( user );
|
||||
save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the current user state to disk.
|
||||
*/
|
||||
public void save() {
|
||||
// Save existing users.
|
||||
for (final MPUser user : getUsers())
|
||||
try {
|
||||
new CharSink() {
|
||||
@Override
|
||||
public Writer openStream()
|
||||
throws IOException {
|
||||
return new FileWriter( new File( userFilesDirectory, user.getFullName() + ".mpsites" ) );
|
||||
}
|
||||
}.write( MPSiteMarshaller.marshallSafe( user ).getExport() );
|
||||
}
|
||||
catch (IOException e) {
|
||||
logger.err( e, "Unable to save sites for user: %s", user );
|
||||
}
|
||||
|
||||
// Remove deleted users.
|
||||
for (File userFile : listUserFiles( userFilesDirectory ))
|
||||
if (getUserNamed( userFile.getName().replaceFirst( "\\.mpsites$", "" ) ) == null)
|
||||
if (!userFile.delete())
|
||||
logger.err( "Couldn't delete file: %s", userFile );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The location on the file system where the user models are stored.
|
||||
*/
|
||||
public File getPath() {
|
||||
return userFilesDirectory;
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package com.lyndir.masterpassword.model;
|
||||
|
||||
import com.google.common.collect.*;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 14-12-05
|
||||
*/
|
||||
public abstract class MPUserManager {
|
||||
|
||||
private final Map<String, MPUser> usersByName = Maps.newHashMap();
|
||||
static MPUserManager instance;
|
||||
|
||||
public static MPUserManager get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
protected MPUserManager(final Iterable<MPUser> users) {
|
||||
for (MPUser user : users)
|
||||
usersByName.put( user.getFullName(), user );
|
||||
}
|
||||
|
||||
public SortedSet<MPUser> getUsers() {
|
||||
return FluentIterable.from( usersByName.values() ).toSortedSet( Ordering.natural() );
|
||||
}
|
||||
|
||||
public MPUser getUserNamed(String fullName) {
|
||||
return usersByName.get( fullName );
|
||||
}
|
||||
|
||||
public void addUser(final MPUser user) {
|
||||
usersByName.put( user.getFullName(), user );
|
||||
}
|
||||
|
||||
public void deleteUser(final MPUser user) {
|
||||
usersByName.remove( user.getFullName() );
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
*
|
||||
* @author lhunath, 15-02-04
|
||||
*/
|
||||
|
||||
@ParametersAreNonnullByDefault
|
||||
package com.lyndir.masterpassword.model;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
11
core/java/tests/build.gradle
Normal file
11
core/java/tests/build.gradle
Normal file
@@ -0,0 +1,11 @@
|
||||
apply plugin: 'java'
|
||||
|
||||
description = 'Master Password Test Suite'
|
||||
|
||||
dependencies {
|
||||
compile project(':masterpassword-algorithm')
|
||||
|
||||
testCompile group: 'org.testng', name: 'testng', version:'6.8.5'
|
||||
testCompile group: 'ch.qos.logback', name: 'logback-classic', version:'1.1.2'
|
||||
}
|
||||
test.useTestNG()
|
43
core/java/tests/pom.xml
Normal file
43
core/java/tests/pom.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<!-- PROJECT METADATA -->
|
||||
<parent>
|
||||
<groupId>com.lyndir.masterpassword</groupId>
|
||||
<artifactId>masterpassword</artifactId>
|
||||
<version>GIT-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<name>Master Password Test Suite</name>
|
||||
<description>The standard test suite to ensure the Master Password algorithm is operating as it should</description>
|
||||
|
||||
<artifactId>masterpassword-tests</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<!-- DEPENDENCY MANAGEMENT -->
|
||||
<dependencies>
|
||||
|
||||
<!-- PROJECT REFERENCES -->
|
||||
<dependency>
|
||||
<groupId>com.lyndir.masterpassword</groupId>
|
||||
<artifactId>masterpassword-algorithm</artifactId>
|
||||
<version>GIT-SNAPSHOT</version>
|
||||
</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>
|
||||
|
||||
</project>
|
@@ -0,0 +1,177 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
||||
import com.lyndir.lhunath.opal.system.util.NNFunctionNN;
|
||||
import java.io.IOException;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.xml.parsers.*;
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.ext.DefaultHandler2;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2015-12-22
|
||||
*/
|
||||
public class MPTestSuite implements Callable<Boolean> {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MPTestSuite.class );
|
||||
private static final String DEFAULT_RESOURCE_NAME = "mpw_tests.xml";
|
||||
|
||||
private MPTests tests;
|
||||
private Listener listener;
|
||||
|
||||
public MPTestSuite()
|
||||
throws UnavailableException {
|
||||
this( DEFAULT_RESOURCE_NAME );
|
||||
}
|
||||
|
||||
public MPTestSuite(String resourceName)
|
||||
throws UnavailableException {
|
||||
try {
|
||||
tests = new MPTests();
|
||||
tests.cases = Lists.newLinkedList();
|
||||
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
|
||||
parser.parse( Thread.currentThread().getContextClassLoader().getResourceAsStream( resourceName ), new DefaultHandler2() {
|
||||
private Deque<String> currentTags = Lists.newLinkedList();
|
||||
private Deque<StringBuilder> currentTexts = Lists.newLinkedList();
|
||||
private MPTests.Case currentCase;
|
||||
|
||||
@Override
|
||||
public void startElement(final String uri, final String localName, final String qName, final Attributes attributes)
|
||||
throws SAXException {
|
||||
super.startElement( uri, localName, qName, attributes );
|
||||
currentTags.push( qName );
|
||||
currentTexts.push( new StringBuilder() );
|
||||
|
||||
if ("case".equals( qName )) {
|
||||
currentCase = new MPTests.Case();
|
||||
currentCase.identifier = attributes.getValue( "id" );
|
||||
currentCase.parent = attributes.getValue( "parent" );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endElement(final String uri, final String localName, final String qName)
|
||||
throws SAXException {
|
||||
super.endElement( uri, localName, qName );
|
||||
Preconditions.checkState( qName.equals( currentTags.pop() ) );
|
||||
String text = currentTexts.pop().toString();
|
||||
|
||||
if ("case".equals( qName ))
|
||||
tests.cases.add( currentCase );
|
||||
if ("algorithm".equals( qName ))
|
||||
currentCase.algorithm = ConversionUtils.toInteger( text ).orNull();
|
||||
if ("fullName".equals( qName ))
|
||||
currentCase.fullName = text;
|
||||
if ("masterPassword".equals( qName ))
|
||||
currentCase.masterPassword = text;
|
||||
if ("keyID".equals( qName ))
|
||||
currentCase.keyID = text;
|
||||
if ("siteName".equals( qName ))
|
||||
currentCase.siteName = text;
|
||||
if ("siteCounter".equals( qName ))
|
||||
currentCase.siteCounter = text.isEmpty()? null: UnsignedInteger.valueOf( text );
|
||||
if ("siteType".equals( qName ))
|
||||
currentCase.siteType = text;
|
||||
if ("siteVariant".equals( qName ))
|
||||
currentCase.siteVariant = text;
|
||||
if ("siteContext".equals( qName ))
|
||||
currentCase.siteContext = text;
|
||||
if ("result".equals( qName ))
|
||||
currentCase.result = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void characters(final char[] ch, final int start, final int length)
|
||||
throws SAXException {
|
||||
super.characters( ch, start, length );
|
||||
|
||||
currentTexts.peek().append( ch, start, length );
|
||||
}
|
||||
} );
|
||||
}
|
||||
catch (IllegalArgumentException | ParserConfigurationException | SAXException | IOException e) {
|
||||
throw new UnavailableException( e );
|
||||
}
|
||||
|
||||
for (MPTests.Case testCase : tests.getCases())
|
||||
testCase.initializeParentHierarchy( tests );
|
||||
}
|
||||
|
||||
public void setListener(final Listener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public MPTests getTests() {
|
||||
return tests;
|
||||
}
|
||||
|
||||
public boolean forEach(String testName, NNFunctionNN<MPTests.Case, Boolean> testFunction) {
|
||||
List<MPTests.Case> cases = tests.getCases();
|
||||
for (int c = 0; c < cases.size(); c++) {
|
||||
MPTests.Case testCase = cases.get( c );
|
||||
if (testCase.getResult().isEmpty())
|
||||
continue;
|
||||
|
||||
progress( Logger.Target.INFO, c, cases.size(), //
|
||||
"[%s] on %s...", testName, testCase.getIdentifier() );
|
||||
|
||||
if (!testFunction.apply( testCase )) {
|
||||
progress( Logger.Target.ERROR, cases.size(), cases.size(), //
|
||||
"[%s] on %s: FAILED!", testName, testCase.getIdentifier() );
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
progress( Logger.Target.INFO, c + 1, cases.size(), //
|
||||
"[%s] on %s: passed!", testName, testCase.getIdentifier() );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void progress(final Logger.Target target, final int current, final int max, final String format, final Object... args) {
|
||||
logger.log( target, format, args );
|
||||
|
||||
if (listener != null)
|
||||
listener.progress( current, max, format, args );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean call()
|
||||
throws Exception {
|
||||
return forEach( "mpw", new NNFunctionNN<MPTests.Case, Boolean>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public Boolean apply(@Nonnull final MPTests.Case testCase) {
|
||||
MasterKey masterKey = MasterKey.create( testCase.getAlgorithm(), testCase.getFullName(), testCase.getMasterPassword() );
|
||||
String sitePassword = masterKey.encode( testCase.getSiteName(), testCase.getSiteType(), testCase.getSiteCounter(),
|
||||
testCase.getSiteVariant(), testCase.getSiteContext() );
|
||||
|
||||
return testCase.getResult().equals( sitePassword );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
public static class UnavailableException extends Exception {
|
||||
|
||||
public UnavailableException(final Throwable cause) {
|
||||
super( cause );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface Listener {
|
||||
|
||||
void progress(int current, int max, String messageFormat, Object... args);
|
||||
}
|
||||
}
|
@@ -0,0 +1,208 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.lhunath.opal.system.util.*;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 14-12-05
|
||||
*/
|
||||
public class MPTests {
|
||||
|
||||
private static final String ID_DEFAULT = "default";
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MPTests.class );
|
||||
|
||||
List<Case> cases;
|
||||
|
||||
@Nonnull
|
||||
public List<Case> getCases() {
|
||||
return checkNotNull( cases );
|
||||
}
|
||||
|
||||
public Case getCase(String identifier) {
|
||||
for (Case testCase : getCases())
|
||||
if (identifier.equals( testCase.getIdentifier() ))
|
||||
return testCase;
|
||||
|
||||
throw new IllegalArgumentException( "No case for identifier: " + identifier );
|
||||
}
|
||||
|
||||
public Case getDefaultCase() {
|
||||
try {
|
||||
return getCase( ID_DEFAULT );
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw new IllegalStateException( "Missing default case in test suite. Add a case with id: " + ID_DEFAULT, e );
|
||||
}
|
||||
}
|
||||
|
||||
public static class Case {
|
||||
|
||||
String identifier;
|
||||
String parent;
|
||||
Integer algorithm;
|
||||
String fullName;
|
||||
String masterPassword;
|
||||
String keyID;
|
||||
String siteName;
|
||||
UnsignedInteger siteCounter;
|
||||
String siteType;
|
||||
String siteVariant;
|
||||
String siteContext;
|
||||
String result;
|
||||
|
||||
private transient Case parentCase;
|
||||
|
||||
public void initializeParentHierarchy(MPTests tests) {
|
||||
|
||||
if (parent != null) {
|
||||
parentCase = tests.getCase( parent );
|
||||
parentCase.initializeParentHierarchy( tests );
|
||||
}
|
||||
|
||||
algorithm = ifNotNullElse( algorithm, new NNSupplier<Integer>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public Integer get() {
|
||||
return checkNotNull( parentCase.algorithm );
|
||||
}
|
||||
} );
|
||||
fullName = ifNotNullElse( fullName, new NNSupplier<String>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public String get() {
|
||||
return checkNotNull( parentCase.fullName );
|
||||
}
|
||||
} );
|
||||
masterPassword = ifNotNullElse( masterPassword, new NNSupplier<String>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public String get() {
|
||||
return checkNotNull( parentCase.masterPassword );
|
||||
}
|
||||
} );
|
||||
keyID = ifNotNullElse( keyID, new NNSupplier<String>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public String get() {
|
||||
return checkNotNull( parentCase.keyID );
|
||||
}
|
||||
} );
|
||||
siteName = ifNotNullElse( siteName, new NNSupplier<String>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public String get() {
|
||||
return checkNotNull( parentCase.siteName );
|
||||
}
|
||||
} );
|
||||
siteCounter = ifNotNullElse( siteCounter, new NNSupplier<UnsignedInteger>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public UnsignedInteger get() {
|
||||
return checkNotNull( parentCase.siteCounter );
|
||||
}
|
||||
} );
|
||||
siteType = ifNotNullElse( siteType, new NNSupplier<String>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public String get() {
|
||||
return checkNotNull( parentCase.siteType );
|
||||
}
|
||||
} );
|
||||
siteVariant = ifNotNullElse( siteVariant, new NNSupplier<String>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public String get() {
|
||||
return checkNotNull( parentCase.siteVariant );
|
||||
}
|
||||
} );
|
||||
siteContext = ifNotNullElse( siteContext, new NNSupplier<String>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public String get() {
|
||||
return parentCase == null? "": checkNotNull( parentCase.siteContext );
|
||||
}
|
||||
} );
|
||||
result = ifNotNullElse( result, new NNSupplier<String>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public String get() {
|
||||
return parentCase == null? "": checkNotNull( parentCase.result );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Case getParentCase() {
|
||||
return parentCase;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public MasterKey.Version getAlgorithm() {
|
||||
return MasterKey.Version.fromInt( checkNotNull( algorithm ) );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getFullName() {
|
||||
return checkNotNull( fullName );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public char[] getMasterPassword() {
|
||||
return checkNotNull( masterPassword ).toCharArray();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getKeyID() {
|
||||
return checkNotNull( keyID );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getSiteName() {
|
||||
return checkNotNull( siteName );
|
||||
}
|
||||
|
||||
public UnsignedInteger getSiteCounter() {
|
||||
return ifNotNullElse( siteCounter, UnsignedInteger.valueOf( 1 ) );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public MPSiteType getSiteType() {
|
||||
return MPSiteType.forName( checkNotNull( siteType ) );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public MPSiteVariant getSiteVariant() {
|
||||
return MPSiteVariant.forName( checkNotNull( siteVariant ) );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getSiteContext() {
|
||||
return checkNotNull( siteContext );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getResult() {
|
||||
return checkNotNull( result );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return identifier;
|
||||
}
|
||||
}
|
||||
}
|
279
core/java/tests/src/main/resources/mpw_tests.xml
Normal file
279
core/java/tests/src/main/resources/mpw_tests.xml
Normal file
@@ -0,0 +1,279 @@
|
||||
<tests>
|
||||
<!-- Default values for all parameters. -->
|
||||
<case id="default">
|
||||
<algorithm>-1</algorithm>
|
||||
<fullName>Robert Lee Mitchell</fullName>
|
||||
<masterPassword>banana colored duckling</masterPassword>
|
||||
<keyID>98EEF4D1DF46D849574A82A03C3177056B15DFFCA29BB3899DE4628453675302</keyID>
|
||||
<siteName>masterpasswordapp.com</siteName>
|
||||
<siteCounter>1</siteCounter>
|
||||
<siteType>GeneratedLong</siteType>
|
||||
<siteVariant>Password</siteVariant>
|
||||
<result><!-- abstract --></result>
|
||||
</case>
|
||||
|
||||
<!-- Algorithm 3 -->
|
||||
<case id="v3" parent="default">
|
||||
<algorithm>3</algorithm>
|
||||
<result>Jejr5[RepuSosp</result>
|
||||
</case>
|
||||
<case id="v3_mb_fullName" parent="v3">
|
||||
<fullName>⛄</fullName>
|
||||
<keyID>1717AA1F9BF5BA56CD0965CDA3D78E6D2E6A1EA8C067A8EA621F3DDAD4A87EB8</keyID>
|
||||
<result>NopaDajh8=Fene</result>
|
||||
</case>
|
||||
<case id="v3_mb_masterPassword" parent="v3">
|
||||
<masterPassword>⛄</masterPassword>
|
||||
<keyID>351432B8528A5ABECAB768CA95015097DE76FE14C41E10AF36C67DCFB8917E08</keyID>
|
||||
<result>QesuHirv5-Xepl</result>
|
||||
</case>
|
||||
<case id="v3_mb_siteName" parent="v3">
|
||||
<siteName>⛄</siteName>
|
||||
<result>LiheCuwhSerz6)</result>
|
||||
</case>
|
||||
<case id="v3_loginName" parent="v3">
|
||||
<siteVariant>Login</siteVariant>
|
||||
<siteType>GeneratedName</siteType>
|
||||
<result>wohzaqage</result>
|
||||
</case>
|
||||
<case id="v3_securityAnswer" parent="v3">
|
||||
<siteVariant>Answer</siteVariant>
|
||||
<siteType>GeneratedPhrase</siteType>
|
||||
<result>xin diyjiqoja hubu</result>
|
||||
</case>
|
||||
<case id="v3_securityAnswer_context" parent="v3_securityAnswer">
|
||||
<siteContext>question</siteContext>
|
||||
<result>xogx tem cegyiva jab</result>
|
||||
</case>
|
||||
<case id="v3_type_maximum" parent="v3">
|
||||
<siteType>GeneratedMaximum</siteType>
|
||||
<result>W6@692^B1#&@gVdSdLZ@</result>
|
||||
</case>
|
||||
<case id="v3_type_medium" parent="v3">
|
||||
<siteType>GeneratedMedium</siteType>
|
||||
<result>Jej2$Quv</result>
|
||||
</case>
|
||||
<case id="v3_type_basic" parent="v3">
|
||||
<siteType>GeneratedBasic</siteType>
|
||||
<result>WAo2xIg6</result>
|
||||
</case>
|
||||
<case id="v3_type_short" parent="v3">
|
||||
<siteType>GeneratedShort</siteType>
|
||||
<result>Jej2</result>
|
||||
</case>
|
||||
<case id="v3_type_pin" parent="v3">
|
||||
<siteType>GeneratedPIN</siteType>
|
||||
<result>7662</result>
|
||||
</case>
|
||||
<case id="v3_type_name" parent="v3">
|
||||
<siteType>GeneratedName</siteType>
|
||||
<result>jejraquvo</result>
|
||||
</case>
|
||||
<case id="v3_type_phrase" parent="v3">
|
||||
<siteType>GeneratedPhrase</siteType>
|
||||
<result>jejr quv cabsibu tam</result>
|
||||
</case>
|
||||
<case id="v3_counter_ceiling" parent="v3">
|
||||
<siteCounter>4294967295</siteCounter>
|
||||
<result>XambHoqo6[Peni</result>
|
||||
</case>
|
||||
|
||||
<!-- Algorithm 2 -->
|
||||
<case id="v2" parent="default">
|
||||
<algorithm>2</algorithm>
|
||||
<result>Jejr5[RepuSosp</result>
|
||||
</case>
|
||||
<case id="v2_mb_fullName" parent="v2">
|
||||
<fullName>⛄</fullName>
|
||||
<keyID>1717AA1F9BF5BA56CD0965CDA3D78E6D2E6A1EA8C067A8EA621F3DDAD4A87EB8</keyID>
|
||||
<result>WaqoGuho2[Xaxw</result>
|
||||
</case>
|
||||
<case id="v2_mb_masterPassword" parent="v2">
|
||||
<masterPassword>⛄</masterPassword>
|
||||
<keyID>351432B8528A5ABECAB768CA95015097DE76FE14C41E10AF36C67DCFB8917E08</keyID>
|
||||
<result>QesuHirv5-Xepl</result>
|
||||
</case>
|
||||
<case id="v2_mb_siteName" parent="v2">
|
||||
<siteName>⛄</siteName>
|
||||
<result>LiheCuwhSerz6)</result>
|
||||
</case>
|
||||
<case id="v2_loginName" parent="v2">
|
||||
<siteVariant>Login</siteVariant>
|
||||
<siteType>GeneratedName</siteType>
|
||||
<result>wohzaqage</result>
|
||||
</case>
|
||||
<case id="v2_securityAnswer" parent="v2">
|
||||
<siteVariant>Answer</siteVariant>
|
||||
<siteType>GeneratedPhrase</siteType>
|
||||
<result>xin diyjiqoja hubu</result>
|
||||
</case>
|
||||
<case id="v2_securityAnswer_context" parent="v2_securityAnswer">
|
||||
<siteContext>question</siteContext>
|
||||
<result>xogx tem cegyiva jab</result>
|
||||
</case>
|
||||
<case id="v2_type_maximum" parent="v2">
|
||||
<siteType>GeneratedMaximum</siteType>
|
||||
<result>W6@692^B1#&@gVdSdLZ@</result>
|
||||
</case>
|
||||
<case id="v2_type_medium" parent="v2">
|
||||
<siteType>GeneratedMedium</siteType>
|
||||
<result>Jej2$Quv</result>
|
||||
</case>
|
||||
<case id="v2_type_basic" parent="v2">
|
||||
<siteType>GeneratedBasic</siteType>
|
||||
<result>WAo2xIg6</result>
|
||||
</case>
|
||||
<case id="v2_type_short" parent="v2">
|
||||
<siteType>GeneratedShort</siteType>
|
||||
<result>Jej2</result>
|
||||
</case>
|
||||
<case id="v2_type_pin" parent="v2">
|
||||
<siteType>GeneratedPIN</siteType>
|
||||
<result>7662</result>
|
||||
</case>
|
||||
<case id="v2_type_name" parent="v2">
|
||||
<siteType>GeneratedName</siteType>
|
||||
<result>jejraquvo</result>
|
||||
</case>
|
||||
<case id="v2_type_phrase" parent="v2">
|
||||
<siteType>GeneratedPhrase</siteType>
|
||||
<result>jejr quv cabsibu tam</result>
|
||||
</case>
|
||||
<case id="v2_counter_ceiling" parent="v2">
|
||||
<siteCounter>4294967295</siteCounter>
|
||||
<result>XambHoqo6[Peni</result>
|
||||
</case>
|
||||
|
||||
<!-- Algorithm 1 -->
|
||||
<case id="v1" parent="default">
|
||||
<algorithm>1</algorithm>
|
||||
<result>Jejr5[RepuSosp</result>
|
||||
</case>
|
||||
<case id="v1_mb_fullName" parent="v1">
|
||||
<fullName>⛄</fullName>
|
||||
<keyID>1717AA1F9BF5BA56CD0965CDA3D78E6D2E6A1EA8C067A8EA621F3DDAD4A87EB8</keyID>
|
||||
<result>WaqoGuho2[Xaxw</result>
|
||||
</case>
|
||||
<case id="v1_mb_masterPassword" parent="v1">
|
||||
<masterPassword>⛄</masterPassword>
|
||||
<keyID>351432B8528A5ABECAB768CA95015097DE76FE14C41E10AF36C67DCFB8917E08</keyID>
|
||||
<result>QesuHirv5-Xepl</result>
|
||||
</case>
|
||||
<case id="v1_mb_siteName" parent="v1">
|
||||
<siteName>⛄</siteName>
|
||||
<result>WawiYarp2@Kodh</result>
|
||||
</case>
|
||||
<case id="v1_loginName" parent="v1">
|
||||
<siteVariant>Login</siteVariant>
|
||||
<siteType>GeneratedName</siteType>
|
||||
<result>wohzaqage</result>
|
||||
</case>
|
||||
<case id="v1_securityAnswer" parent="v1">
|
||||
<siteVariant>Answer</siteVariant>
|
||||
<siteType>GeneratedPhrase</siteType>
|
||||
<result>xin diyjiqoja hubu</result>
|
||||
</case>
|
||||
<case id="v1_securityAnswer_context" parent="v1_securityAnswer">
|
||||
<siteContext>question</siteContext>
|
||||
<result>xogx tem cegyiva jab</result>
|
||||
</case>
|
||||
<case id="v1_type_maximum" parent="v1">
|
||||
<siteType>GeneratedMaximum</siteType>
|
||||
<result>W6@692^B1#&@gVdSdLZ@</result>
|
||||
</case>
|
||||
<case id="v1_type_medium" parent="v1">
|
||||
<siteType>GeneratedMedium</siteType>
|
||||
<result>Jej2$Quv</result>
|
||||
</case>
|
||||
<case id="v1_type_basic" parent="v1">
|
||||
<siteType>GeneratedBasic</siteType>
|
||||
<result>WAo2xIg6</result>
|
||||
</case>
|
||||
<case id="v1_type_short" parent="v1">
|
||||
<siteType>GeneratedShort</siteType>
|
||||
<result>Jej2</result>
|
||||
</case>
|
||||
<case id="v1_type_pin" parent="v1">
|
||||
<siteType>GeneratedPIN</siteType>
|
||||
<result>7662</result>
|
||||
</case>
|
||||
<case id="v1_type_name" parent="v1">
|
||||
<siteType>GeneratedName</siteType>
|
||||
<result>jejraquvo</result>
|
||||
</case>
|
||||
<case id="v1_type_phrase" parent="v1">
|
||||
<siteType>GeneratedPhrase</siteType>
|
||||
<result>jejr quv cabsibu tam</result>
|
||||
</case>
|
||||
<case id="v1_counter_ceiling" parent="v1">
|
||||
<siteCounter>4294967295</siteCounter>
|
||||
<result>XambHoqo6[Peni</result>
|
||||
</case>
|
||||
|
||||
<!-- Algorithm 0 -->
|
||||
<case id="v0" parent="default">
|
||||
<algorithm>0</algorithm>
|
||||
<result>Feji5@ReduWosh</result>
|
||||
</case>
|
||||
<case id="v0_mb_fullName" parent="v0">
|
||||
<fullName>⛄</fullName>
|
||||
<keyID>1717AA1F9BF5BA56CD0965CDA3D78E6D2E6A1EA8C067A8EA621F3DDAD4A87EB8</keyID>
|
||||
<result>HajrYudo7@Mamh</result>
|
||||
</case>
|
||||
<case id="v0_mb_masterPassword" parent="v0">
|
||||
<masterPassword>⛄</masterPassword>
|
||||
<keyID>351432B8528A5ABECAB768CA95015097DE76FE14C41E10AF36C67DCFB8917E08</keyID>
|
||||
<result>MewmDini0]Meho</result>
|
||||
</case>
|
||||
<case id="v0_mb_siteName" parent="v0">
|
||||
<siteName>⛄</siteName>
|
||||
<result>HahiVana2@Nole</result>
|
||||
</case>
|
||||
<case id="v0_loginName" parent="v0">
|
||||
<siteVariant>Login</siteVariant>
|
||||
<siteType>GeneratedName</siteType>
|
||||
<result>lozwajave</result>
|
||||
</case>
|
||||
<case id="v0_securityAnswer" parent="v0">
|
||||
<siteVariant>Answer</siteVariant>
|
||||
<siteType>GeneratedPhrase</siteType>
|
||||
<result>miy lirfijoja dubu</result>
|
||||
</case>
|
||||
<case id="v0_securityAnswer_context" parent="v0_securityAnswer">
|
||||
<siteContext>question</siteContext>
|
||||
<result>movm bex gevrica jaf</result>
|
||||
</case>
|
||||
<case id="v0_type_maximum" parent="v0">
|
||||
<siteType>GeneratedMaximum</siteType>
|
||||
<result>w1!3bA3icmRAc)SS@lwl</result>
|
||||
</case>
|
||||
<case id="v0_type_medium" parent="v0">
|
||||
<siteType>GeneratedMedium</siteType>
|
||||
<result>Fej7]Jug</result>
|
||||
</case>
|
||||
<case id="v0_type_basic" parent="v0">
|
||||
<siteType>GeneratedBasic</siteType>
|
||||
<result>wvH7irC1</result>
|
||||
</case>
|
||||
<case id="v0_type_short" parent="v0">
|
||||
<siteType>GeneratedShort</siteType>
|
||||
<result>Fej7</result>
|
||||
</case>
|
||||
<case id="v0_type_pin" parent="v0">
|
||||
<siteType>GeneratedPIN</siteType>
|
||||
<result>2117</result>
|
||||
</case>
|
||||
<case id="v0_type_name" parent="v0">
|
||||
<siteType>GeneratedName</siteType>
|
||||
<result>fejrajugo</result>
|
||||
</case>
|
||||
<case id="v0_type_phrase" parent="v0">
|
||||
<siteType>GeneratedPhrase</siteType>
|
||||
<result>fejr jug gabsibu bax</result>
|
||||
</case>
|
||||
<case id="v0_counter_ceiling" parent="v0">
|
||||
<siteCounter>4294967295</siteCounter>
|
||||
<result>QateDojh1@Hecn</result>
|
||||
</case>
|
||||
</tests>
|
||||
|
@@ -0,0 +1,96 @@
|
||||
package com.lyndir.masterpassword;
|
||||
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.lhunath.opal.system.util.NNFunctionNN;
|
||||
import com.lyndir.lhunath.opal.system.util.StringUtils;
|
||||
import java.net.URL;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import org.testng.annotations.BeforeMethod;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
|
||||
public class MasterKeyTest {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MasterKeyTest.class );
|
||||
|
||||
private MPTestSuite testSuite;
|
||||
|
||||
@BeforeMethod
|
||||
public void setUp()
|
||||
throws Exception {
|
||||
|
||||
testSuite = new MPTestSuite();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncode()
|
||||
throws Exception {
|
||||
|
||||
testSuite.forEach( "testEncode", new NNFunctionNN<MPTests.Case, Boolean>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public Boolean apply(@Nonnull final MPTests.Case testCase) {
|
||||
MasterKey masterKey = MasterKey.create( testCase.getAlgorithm(), testCase.getFullName(), testCase.getMasterPassword() );
|
||||
|
||||
assertEquals(
|
||||
masterKey.encode( testCase.getSiteName(), testCase.getSiteType(), testCase.getSiteCounter(),
|
||||
testCase.getSiteVariant(), testCase.getSiteContext() ),
|
||||
testCase.getResult(), "[testEncode] Failed test case: " + testCase );
|
||||
|
||||
return true;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUserName()
|
||||
throws Exception {
|
||||
|
||||
MPTests.Case defaultCase = testSuite.getTests().getDefaultCase();
|
||||
|
||||
assertEquals( MasterKey.create( defaultCase.getFullName(), defaultCase.getMasterPassword() ).getFullName(),
|
||||
defaultCase.getFullName(), "[testGetUserName] Failed test case: " + defaultCase );
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetKeyID()
|
||||
throws Exception {
|
||||
|
||||
testSuite.forEach( "testGetKeyID", new NNFunctionNN<MPTests.Case, Boolean>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public Boolean apply(@Nonnull final MPTests.Case testCase) {
|
||||
MasterKey masterKey = MasterKey.create( testCase.getFullName(), testCase.getMasterPassword() );
|
||||
|
||||
assertEquals( CodeUtils.encodeHex( masterKey.getKeyID() ),
|
||||
testCase.getKeyID(), "[testGetKeyID] Failed test case: " + testCase );
|
||||
|
||||
return true;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidate()
|
||||
throws Exception {
|
||||
|
||||
try {
|
||||
MPTests.Case defaultCase = testSuite.getTests().getDefaultCase();
|
||||
|
||||
MasterKey masterKey = MasterKey.create( defaultCase.getFullName(), defaultCase.getMasterPassword() );
|
||||
masterKey.invalidate();
|
||||
masterKey.encode( defaultCase.getSiteName(), defaultCase.getSiteType(), defaultCase.getSiteCounter(),
|
||||
defaultCase.getSiteVariant(), defaultCase.getSiteContext() );
|
||||
|
||||
assertTrue( false, "[testInvalidate] Master key should have been invalidated, but was still usable." );
|
||||
}
|
||||
catch (IllegalStateException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
15
core/java/tests/src/test/resources/logback.xml
Normal file
15
core/java/tests/src/test/resources/logback.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<configuration scan="false">
|
||||
|
||||
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%-8relative %22c{0} [%-5level] %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="com.lyndir" level="${mp.log.level:-INFO}" />
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="stdout" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
Reference in New Issue
Block a user