2
0

Fixed counter type, added Android integrity testing.

[IMPROVED]  Type of Master Password counter is unsigned int; Guava's
            UnsignedInteger allows us to better enforce that in code,
            fixing a few places where ints were treated badly, limiting
            counter support.
[ADDED]     An integrity test activity to the Android app to ensure the
            Android device properly generates the expected passwords
            before allowing the user to rely on it.
[UPDATED]   Made standard test suite available for all without needing
            JAXB; implemented SAX reading of mpw_tests.xml + a good API
            for running the tests and getting feedback at runtime.
This commit is contained in:
Maarten Billemont 2015-12-24 22:26:17 -05:00
parent 188353d39b
commit f782b2ef62
23 changed files with 598 additions and 156 deletions

View File

@ -23,7 +23,7 @@
<dependency> <dependency>
<groupId>com.lyndir.lhunath.opal</groupId> <groupId>com.lyndir.lhunath.opal</groupId>
<artifactId>opal-system</artifactId> <artifactId>opal-system</artifactId>
<version>1.6-p8</version> <version>1.6-p9</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>joda-time</groupId> <groupId>joda-time</groupId>

View File

@ -1,6 +1,7 @@
package com.lyndir.masterpassword; package com.lyndir.masterpassword;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.lhunath.opal.system.*; import com.lyndir.lhunath.opal.system.*;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import java.util.Arrays; import java.util.Arrays;
@ -74,7 +75,7 @@ public abstract class MasterKey {
return idForBytes( getKey() ); return idForBytes( getKey() );
} }
public abstract String encode(@Nonnull final String siteName, final MPSiteType siteType, int siteCounter, public abstract String encode(@Nonnull final String siteName, final MPSiteType siteType, @Nonnull final UnsignedInteger siteCounter,
final MPSiteVariant siteVariant, @Nullable final String siteContext); final MPSiteVariant siteVariant, @Nullable final String siteContext);
public boolean isValid() { public boolean isValid() {
@ -106,7 +107,9 @@ public abstract class MasterKey {
return this; return this;
} }
protected abstract byte[] bytesForInt(final int integer); protected abstract byte[] bytesForInt(final int number);
protected abstract byte[] bytesForInt(@Nonnull final UnsignedInteger number);
protected abstract byte[] idForBytes(final byte[] bytes); protected abstract byte[] idForBytes(final byte[] bytes);

View File

@ -3,6 +3,7 @@ package com.lyndir.masterpassword;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
import com.google.common.primitives.UnsignedInteger;
import com.lambdaworks.crypto.SCrypt; import com.lambdaworks.crypto.SCrypt;
import com.lyndir.lhunath.opal.system.*; import com.lyndir.lhunath.opal.system.*;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
@ -10,14 +11,15 @@ import java.nio.*;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.Arrays; import java.util.Arrays;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
/** /**
* bugs: * bugs:
* - does math with chars whose signedness was platform-dependent. * - V2: miscounted the byte-length fromInt multi-byte full names.
* - miscounted the byte-length fromInt multi-byte site names. * - V1: miscounted the byte-length fromInt multi-byte site names.
* - miscounted the byte-length fromInt multi-byte full names. * - V0: does math with chars whose signedness was platform-dependent.
* *
* @author lhunath, 2014-08-30 * @author lhunath, 2014-08-30
*/ */
@ -75,18 +77,19 @@ public class MasterKeyV0 extends MasterKey {
} }
} }
public String encode(final String siteName, final MPSiteType siteType, int siteCounter, final MPSiteVariant siteVariant, @Override
@Nullable final String siteContext) { 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( siteType.getTypeClass() == MPSiteTypeClass.Generated );
Preconditions.checkArgument( !siteName.isEmpty() ); Preconditions.checkArgument( !siteName.isEmpty() );
logger.trc( "siteName: %s", siteName ); logger.trc( "siteName: %s", siteName );
logger.trc( "siteCounter: %d", siteCounter ); logger.trc( "siteCounter: %d", siteCounter.longValue() );
logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant ); logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant );
logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType ); logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType );
if (siteCounter == 0) if (siteCounter.longValue() == 0)
siteCounter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300; siteCounter = UnsignedInteger.valueOf( (System.currentTimeMillis() / (300 * 1000)) * 300 );
String siteScope = siteVariant.getScope(); String siteScope = siteVariant.getScope();
byte[] siteNameBytes = siteName.getBytes( MP_charset ); byte[] siteNameBytes = siteName.getBytes( MP_charset );
@ -108,7 +111,7 @@ public class MasterKeyV0 extends MasterKey {
int[] sitePasswordSeed = new int[sitePasswordSeedBytes.length]; int[] sitePasswordSeed = new int[sitePasswordSeedBytes.length];
for (int i = 0; i < sitePasswordSeedBytes.length; ++i) { for (int i = 0; i < sitePasswordSeedBytes.length; ++i) {
ByteBuffer buf = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( ByteOrder.BIG_ENDIAN ); ByteBuffer buf = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( ByteOrder.BIG_ENDIAN );
Arrays.fill( buf.array(), sitePasswordSeedBytes[i] > 0? (byte)0x00: (byte) 0xFF ); Arrays.fill( buf.array(), sitePasswordSeedBytes[i] > 0? (byte) 0x00: (byte) 0xFF );
buf.position( 2 ); buf.position( 2 );
buf.put( sitePasswordSeedBytes[i] ).rewind(); buf.put( sitePasswordSeedBytes[i] ).rewind();
sitePasswordSeed[i] = buf.getInt() & 0xFFFF; sitePasswordSeed[i] = buf.getInt() & 0xFFFF;
@ -135,8 +138,13 @@ public class MasterKeyV0 extends MasterKey {
} }
@Override @Override
protected byte[] bytesForInt(final int integer) { protected byte[] bytesForInt(final int number) {
return ByteBuffer.allocate( MP_intLen / Byte.SIZE ).order( MP_byteOrder ).putInt( integer ).array(); return ByteBuffer.allocate( MP_intLen / Byte.SIZE ).order( MP_byteOrder ).putInt( number ).array();
}
@Override
protected byte[] bytesForInt(@Nonnull final UnsignedInteger number) {
return ByteBuffer.allocate( MP_intLen / Byte.SIZE ).order( MP_byteOrder ).putInt( number.intValue() ).array();
} }
@Override @Override

View File

@ -2,15 +2,17 @@ package com.lyndir.masterpassword;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.lhunath.opal.system.*; import com.lyndir.lhunath.opal.system.*;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
/** /**
* bugs: * bugs:
* - miscounted the byte-length fromInt multi-byte site names. * - V2: miscounted the byte-length fromInt multi-byte full names.
* - miscounted the byte-length fromInt multi-byte full names. * - V1: miscounted the byte-length fromInt multi-byte site names.
* *
* @author lhunath, 2014-08-30 * @author lhunath, 2014-08-30
*/ */
@ -29,18 +31,19 @@ public class MasterKeyV1 extends MasterKeyV0 {
return Version.V1; return Version.V1;
} }
public String encode(final String siteName, final MPSiteType siteType, int siteCounter, final MPSiteVariant siteVariant, @Override
@Nullable final String siteContext) { 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( siteType.getTypeClass() == MPSiteTypeClass.Generated );
Preconditions.checkArgument( !siteName.isEmpty() ); Preconditions.checkArgument( !siteName.isEmpty() );
logger.trc( "siteName: %s", siteName ); logger.trc( "siteName: %s", siteName );
logger.trc( "siteCounter: %d", siteCounter ); logger.trc( "siteCounter: %d", siteCounter.longValue() );
logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant ); logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant );
logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType ); logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType );
if (siteCounter == 0) if (siteCounter.longValue() == 0)
siteCounter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300; siteCounter = UnsignedInteger.valueOf( (System.currentTimeMillis() / (300 * 1000)) * 300 );
String siteScope = siteVariant.getScope(); String siteScope = siteVariant.getScope();
byte[] siteNameBytes = siteName.getBytes( MP_charset ); byte[] siteNameBytes = siteName.getBytes( MP_charset );

View File

@ -2,14 +2,16 @@ package com.lyndir.masterpassword;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.primitives.Bytes; 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.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
/** /**
* bugs: * bugs:
* - miscounted the byte-length fromInt multi-byte full names. * - V2: miscounted the byte-length fromInt multi-byte full names.
* *
* @author lhunath, 2014-08-30 * @author lhunath, 2014-08-30
*/ */
@ -28,18 +30,19 @@ public class MasterKeyV2 extends MasterKeyV1 {
return Version.V2; return Version.V2;
} }
public String encode(final String siteName, final MPSiteType siteType, int siteCounter, final MPSiteVariant siteVariant, @Override
@Nullable final String siteContext) { 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( siteType.getTypeClass() == MPSiteTypeClass.Generated );
Preconditions.checkArgument( !siteName.isEmpty() ); Preconditions.checkArgument( !siteName.isEmpty() );
logger.trc( "siteName: %s", siteName ); logger.trc( "siteName: %s", siteName );
logger.trc( "siteCounter: %d", siteCounter ); logger.trc( "siteCounter: %d", siteCounter.longValue() );
logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant ); logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant );
logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType ); logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType );
if (siteCounter == 0) if (siteCounter.longValue() == 0)
siteCounter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300; siteCounter = UnsignedInteger.valueOf( (System.currentTimeMillis() / (300 * 1000)) * 300 );
String siteScope = siteVariant.getScope(); String siteScope = siteVariant.getScope();
byte[] siteNameBytes = siteName.getBytes( MP_charset ); byte[] siteNameBytes = siteName.getBytes( MP_charset );

View File

@ -12,13 +12,13 @@
android:icon="@drawable/icon" android:icon="@drawable/icon"
android:label="@string/app_name" android:label="@string/app_name"
android:allowBackup="true"> android:allowBackup="true">
<activity android:name=".EmergencyActivity" android:theme="@style/MPTheme"> <activity android:name=".TestActivity" android:theme="@style/MPTheme">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".UsersActivity" /> <activity android:name=".EmergencyActivity" android:theme="@style/MPTheme" />
</application> </application>
</manifest> </manifest>

View File

@ -107,6 +107,13 @@
<version>GIT-SNAPSHOT</version> <version>GIT-SNAPSHOT</version>
</dependency> </dependency>
<dependency>
<groupId>com.lyndir.masterpassword</groupId>
<artifactId>masterpassword-tests</artifactId>
<version>GIT-SNAPSHOT</version>
</dependency>
<!-- EXTERNAL DEPENDENCIES -->
<dependency> <dependency>
<groupId>com.jakewharton</groupId> <groupId>com.jakewharton</groupId>
<artifactId>butterknife</artifactId> <artifactId>butterknife</artifactId>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:background="@drawable/background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp"
android:orientation="vertical"
android:gravity="center">
<View
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ProgressBar
android:id="@+id/progressView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="8dp"
style="@android:style/Widget.ProgressBar.Horizontal" />
<TextView
android:id="@+id/statusView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:background="@android:color/transparent"
android:textSize="14sp"
android:textColor="@android:color/secondary_text_dark"
android:text="@string/tests_testing" />
<TextView
android:id="@+id/logView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="20dp"
android:gravity="bottom"
android:background="@android:color/transparent"
android:textSize="9sp"
android:textColor="@android:color/tertiary_text_dark" />
<Button
android:id="@+id/actionButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:enabled="false"
android:text="@string/tests_btn_testing"
android:onClick="onAction"/>
</LinearLayout>
</ScrollView>

View File

@ -11,4 +11,12 @@
<string name="siteCounter_hint">Password #</string> <string name="siteCounter_hint">Password #</string>
<string name="siteVersion_hint">Algorithm</string> <string name="siteVersion_hint">Algorithm</string>
<string name="empty" /> <string name="empty" />
<string name="tests_unavailable">Test suite unavailable.</string>
<string name="tests_btn_unavailable">Exit</string>
<string name="tests_testing">Testing device\'s password generation integrity…</string>
<string name="tests_btn_testing">Please Stand By…</string>
<string name="tests_failed">Incompatible device or OS.</string>
<string name="tests_btn_failed">Exit</string>
<string name="tests_passed">Integrity checks passed!</string>
<string name="tests_btn_passed">Continue</string>
</resources> </resources>

View File

@ -1,5 +1,6 @@
package com.lyndir.masterpassword; package com.lyndir.masterpassword;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.ifNotNullElse;
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf; import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
import android.app.*; import android.app.*;
@ -16,9 +17,9 @@ import android.widget.*;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.InjectView; import butterknife.InjectView;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.primitives.UnsignedInteger;
import com.google.common.util.concurrent.*; import com.google.common.util.concurrent.*;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -87,6 +88,10 @@ public class EmergencyActivity extends Activity {
private int hc_masterPassword; private int hc_masterPassword;
private String sitePassword; private String sitePassword;
public static void start(Context context) {
context.startActivity( new Intent( context, EmergencyActivity.class ) );
}
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate( savedInstanceState ); super.onCreate( savedInstanceState );
@ -261,7 +266,7 @@ public class EmergencyActivity extends Activity {
private void updateSitePassword() { private void updateSitePassword() {
final String siteName = siteNameField.getText().toString(); final String siteName = siteNameField.getText().toString();
final MPSiteType type = (MPSiteType) siteTypeField.getSelectedItem(); final MPSiteType type = (MPSiteType) siteTypeField.getSelectedItem();
final int counter = ConversionUtils.toIntegerNN( counterField.getText() ); final UnsignedInteger counter = UnsignedInteger.valueOf( ifNotNullElse( counterField.getText(), "1" ).toString() );
if (masterKeyFuture == null || siteName.isEmpty() || type == null) { if (masterKeyFuture == null || siteName.isEmpty() || type == null) {
sitePasswordField.setText( "" ); sitePasswordField.setText( "" );

View File

@ -0,0 +1,82 @@
package com.lyndir.masterpassword;
import android.os.Handler;
import android.os.Looper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import java.util.*;
import java.util.concurrent.*;
/**
* @author lhunath, 2015-12-22
*/
public class MainThreadExecutor extends AbstractExecutorService {
private final Handler mHandler = new Handler( Looper.getMainLooper() );
private final Set<Runnable> commands = Sets.newLinkedHashSet();
private boolean shutdown;
@Override
public void execute(final Runnable command) {
if (shutdown)
throw new RejectedExecutionException( "This executor has been shut down" );
synchronized (commands) {
commands.add( command );
mHandler.post( new Runnable() {
@Override
public void run() {
synchronized (commands) {
if (!commands.remove( command ))
// Command was removed, not executing.
return;
}
command.run();
}
} );
}
}
@Override
public void shutdown() {
shutdown = true;
}
@Override
public List<Runnable> shutdownNow() {
shutdown = true;
mHandler.removeCallbacksAndMessages( null );
synchronized (commands) {
ImmutableList<Runnable> pendingTasks = ImmutableList.copyOf( commands );
commands.clear();
commands.notify();
return pendingTasks;
}
}
@Override
public boolean isShutdown() {
return shutdown;
}
@Override
public boolean isTerminated() {
synchronized (commands) {
return shutdown && commands.isEmpty();
}
}
@Override
public boolean awaitTermination(final long timeout, final TimeUnit unit)
throws InterruptedException {
if (isTerminated())
return true;
commands.wait( unit.toMillis( timeout ) );
return isTerminated();
}
}

View File

@ -0,0 +1,171 @@
package com.lyndir.masterpassword;
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
import android.app.*;
import android.os.*;
import android.view.View;
import android.view.WindowManager;
import android.widget.*;
import butterknife.ButterKnife;
import butterknife.InjectView;
import com.google.common.base.*;
import com.google.common.collect.*;
import com.google.common.util.concurrent.*;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.util.Set;
import java.util.concurrent.*;
import javax.annotation.Nullable;
public class TestActivity extends Activity implements MPTestSuite.Listener {
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( TestActivity.class );
private final ListeningExecutorService backgroundExecutor = MoreExecutors.listeningDecorator( Executors.newSingleThreadExecutor() );
private final ListeningExecutorService mainExecutor = MoreExecutors.listeningDecorator( new MainThreadExecutor() );
@InjectView(R.id.progressView)
ProgressBar progressView;
@InjectView(R.id.statusView)
TextView statusView;
@InjectView(R.id.logView)
TextView logView;
@InjectView(R.id.actionButton)
Button actionButton;
private MPTestSuite testSuite;
private ListenableFuture<Boolean> testFuture;
private Runnable action;
private ImmutableSet<String> testNames;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate( savedInstanceState );
Res.init( getResources() );
getWindow().setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE );
setContentView( R.layout.activity_test );
ButterKnife.inject( this );
try {
setStatus( 0, 0, null );
testSuite = new MPTestSuite();
testSuite.setListener( this );
testNames = FluentIterable.from( testSuite.getTests().getCases() ).transform(
new Function<MPTests.Case, String>() {
@Nullable
@Override
public String apply(@Nullable final MPTests.Case input) {
return input == null? null: input.identifier;
}
} ).filter( Predicates.notNull() ).toSet();
}
catch (MPTestSuite.UnavailableException e) {
logger.err( e, "While loading test suite" );
setStatus( R.string.tests_unavailable, R.string.tests_btn_unavailable, new Runnable() {
@Override
public void run() {
finish();
}
} );
}
}
@Override
protected void onStart() {
super.onStart();
final Set<String> integrityTestsPassed = getPreferences( MODE_PRIVATE ).getStringSet( "integrityTestsPassed",
ImmutableSet.<String>of() );
if (!FluentIterable.from( testNames ).anyMatch( new Predicate<String>() {
@Override
public boolean apply(@Nullable final String testName) {
return !integrityTestsPassed.contains( testName );
}
} )) {
// None of the tests we need to perform were missing from the tests that have already been passed on this device.
finish();
EmergencyActivity.start( TestActivity.this );
}
}
@Override
protected void onResume() {
super.onResume();
if (testFuture == null) {
setStatus( R.string.tests_testing, R.string.tests_btn_testing, null );
Futures.addCallback( testFuture = backgroundExecutor.submit( testSuite ), new FutureCallback<Boolean>() {
@Override
public void onSuccess(@Nullable final Boolean result) {
if (result != null && result)
setStatus( R.string.tests_passed, R.string.tests_btn_passed, new Runnable() {
@Override
public void run() {
getPreferences( MODE_PRIVATE ).edit().putStringSet( "integrityTestsPassed", testNames ).apply();
finish();
EmergencyActivity.start( TestActivity.this );
}
} );
else
setStatus( R.string.tests_failed, R.string.tests_btn_failed, new Runnable() {
@Override
public void run() {
finish();
}
} );
}
@Override
public void onFailure(final Throwable t) {
logger.err( t, "While running test suite" );
setStatus( R.string.tests_failed, R.string.tests_btn_failed, new Runnable() {
@Override
public void run() {
finish();
}
} );
}
}, mainExecutor );
}
}
public void onAction(View v) {
if (action != null)
action.run();
}
private void setStatus(int statusId, int buttonId, @Nullable Runnable action) {
this.action = action;
if (statusId == 0)
statusView.setText( null );
else
statusView.setText( statusId );
if (buttonId == 0)
actionButton.setText( null );
else
actionButton.setText( buttonId );
actionButton.setEnabled( action != null );
}
@Override
public void progress(final int current, final int max, final String messageFormat, final Object... args) {
runOnUiThread( new Runnable() {
@Override
public void run() {
logView.append( strf( '\n' + messageFormat, args ) );
progressView.setMax( max );
progressView.setProgress( current );
}
} );
}
}

View File

@ -14,7 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
package com.lyndir.masterpassword; package com.lyndir.masterpassword;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.ifNotNullElse; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.ifNotNullElse;
@ -23,6 +22,7 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.io.LineReader; import com.google.common.io.LineReader;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.lhunath.opal.system.util.ConversionUtils; import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import com.lyndir.lhunath.opal.system.util.StringUtils; import com.lyndir.lhunath.opal.system.util.StringUtils;
import java.io.*; import java.io.*;
@ -52,7 +52,7 @@ public class CLI {
MPSiteType siteType = siteTypeName.isEmpty()? MPSiteType.GeneratedLong: MPSiteType.forOption( siteTypeName ); MPSiteType siteType = siteTypeName.isEmpty()? MPSiteType.GeneratedLong: MPSiteType.forOption( siteTypeName );
MPSiteVariant variant = MPSiteVariant.Password; MPSiteVariant variant = MPSiteVariant.Password;
String siteCounterName = ifNotNullElse( System.getenv( ENV_SITECOUNTER ), "" ); String siteCounterName = ifNotNullElse( System.getenv( ENV_SITECOUNTER ), "" );
int siteCounter = siteCounterName.isEmpty()? 1: Integer.parseInt( siteCounterName ); UnsignedInteger siteCounter = siteCounterName.isEmpty()? UnsignedInteger.valueOf( 1 ): UnsignedInteger.valueOf( siteCounterName );
// Parse information from option arguments. // Parse information from option arguments.
boolean userNameArg = false, typeArg = false, counterArg = false, variantArg = false, contextArg = false; boolean userNameArg = false, typeArg = false, counterArg = false, variantArg = false, contextArg = false;
@ -77,7 +77,7 @@ public class CLI {
else if ("-c".equals( arg ) || "--counter".equals( arg )) else if ("-c".equals( arg ) || "--counter".equals( arg ))
counterArg = true; counterArg = true;
else if (counterArg) { else if (counterArg) {
siteCounter = ConversionUtils.toIntegerNN( arg ); siteCounter = UnsignedInteger.valueOf( arg );
counterArg = false; counterArg = false;
} }

View File

@ -1,5 +1,6 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.MPSiteType; import com.lyndir.masterpassword.MPSiteType;
import com.lyndir.masterpassword.MasterKey; import com.lyndir.masterpassword.MasterKey;
@ -11,10 +12,10 @@ public class IncognitoSite extends Site {
private String siteName; private String siteName;
private MPSiteType siteType; private MPSiteType siteType;
private int siteCounter; private UnsignedInteger siteCounter;
private MasterKey.Version algorithmVersion; private MasterKey.Version algorithmVersion;
public IncognitoSite(final String siteName, final MPSiteType siteType, final int siteCounter, public IncognitoSite(final String siteName, final MPSiteType siteType, final UnsignedInteger siteCounter,
final MasterKey.Version algorithmVersion) { final MasterKey.Version algorithmVersion) {
this.siteName = siteName; this.siteName = siteName;
this.siteType = siteType; this.siteType = siteType;
@ -48,11 +49,11 @@ public class IncognitoSite extends Site {
this.algorithmVersion = algorithmVersion; this.algorithmVersion = algorithmVersion;
} }
public int getSiteCounter() { public UnsignedInteger getSiteCounter() {
return siteCounter; return siteCounter;
} }
public void setSiteCounter(final int siteCounter) { public void setSiteCounter(final UnsignedInteger siteCounter) {
this.siteCounter = siteCounter; this.siteCounter = siteCounter;
} }
} }

View File

@ -1,5 +1,6 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.MPSiteType; import com.lyndir.masterpassword.MPSiteType;
import com.lyndir.masterpassword.MasterKey; import com.lyndir.masterpassword.MasterKey;
import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.*;
@ -55,13 +56,13 @@ public class ModelSite extends Site {
} }
} }
public int getSiteCounter() { public UnsignedInteger getSiteCounter() {
return model.getSiteCounter(); return model.getSiteCounter();
} }
@Override @Override
public void setSiteCounter(final int siteCounter) { public void setSiteCounter(final UnsignedInteger siteCounter) {
if (siteCounter != getSiteCounter()) { if (siteCounter.equals( getSiteCounter() )) {
model.setSiteCounter( siteCounter ); model.setSiteCounter( siteCounter );
MPUserFileManager.get().save(); MPUserFileManager.get().save();
} }

View File

@ -4,6 +4,7 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.FluentIterable; import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.primitives.UnsignedInteger;
import com.google.common.util.concurrent.*; import com.google.common.util.concurrent.*;
import com.lyndir.lhunath.opal.system.util.PredicateNN; import com.lyndir.lhunath.opal.system.util.PredicateNN;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
@ -26,7 +27,7 @@ public class PasswordFrame extends JFrame implements DocumentListener {
private final User user; private final User user;
private final Components.GradientPanel root; private final Components.GradientPanel root;
private final JTextField siteNameField; private final JTextField siteNameField;
private final JButton siteActionButton; private final JButton siteActionButton;
private final JComboBox<MPSiteType> siteTypeField; private final JComboBox<MPSiteType> siteTypeField;
private final JComboBox<MasterKey.Version> siteVersionField; private final JComboBox<MasterKey.Version> siteVersionField;
private final JSpinner siteCounterField; private final JSpinner siteCounterField;
@ -37,8 +38,8 @@ public class PasswordFrame extends JFrame implements DocumentListener {
private final Font passwordEchoFont; private final Font passwordEchoFont;
@Nullable @Nullable
private Site currentSite; private Site currentSite;
private boolean updatingUI; private boolean updatingUI;
public PasswordFrame(User user) public PasswordFrame(User user)
throws HeadlessException { throws HeadlessException {
@ -62,70 +63,70 @@ public class PasswordFrame extends JFrame implements DocumentListener {
sitePanel.add( Components.stud() ); sitePanel.add( Components.stud() );
// Site Name // Site Name
sitePanel.add(Components.label("Site Name:")); sitePanel.add( Components.label( "Site Name:" ) );
JComponent siteControls = Components.boxLayout( BoxLayout.LINE_AXIS, // JComponent siteControls = Components.boxLayout( BoxLayout.LINE_AXIS, //
siteNameField = Components.textField(), Components.stud(), siteNameField = Components.textField(), Components.stud(),
siteActionButton = Components.button( "Add Site" ) ); siteActionButton = Components.button( "Add Site" ) );
siteNameField.getDocument().addDocumentListener(this); siteNameField.getDocument().addDocumentListener( this );
siteNameField.addActionListener(new ActionListener() { siteNameField.addActionListener( new ActionListener() {
@Override @Override
public void actionPerformed(final ActionEvent e) { public void actionPerformed(final ActionEvent e) {
Futures.addCallback(updatePassword(true), new FutureCallback<String>() { Futures.addCallback( updatePassword( true ), new FutureCallback<String>() {
@Override @Override
public void onSuccess(@Nullable final String sitePassword) { public void onSuccess(@Nullable final String sitePassword) {
StringSelection clipboardContents = new StringSelection(sitePassword); StringSelection clipboardContents = new StringSelection( sitePassword );
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(clipboardContents, null); Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
SwingUtilities.invokeLater(new Runnable() { SwingUtilities.invokeLater( new Runnable() {
@Override @Override
public void run() { public void run() {
passwordField.setText(null); passwordField.setText( null );
siteNameField.setText(null); siteNameField.setText( null );
dispatchEvent(new WindowEvent(PasswordFrame.this, WindowEvent.WINDOW_CLOSING)); dispatchEvent( new WindowEvent( PasswordFrame.this, WindowEvent.WINDOW_CLOSING ) );
} }
}); } );
} }
@Override @Override
public void onFailure(final Throwable t) { public void onFailure(final Throwable t) {
} }
}); } );
} }
}); } );
siteActionButton.addActionListener(new ActionListener() { siteActionButton.addActionListener( new ActionListener() {
@Override @Override
public void actionPerformed(final ActionEvent e) { public void actionPerformed(final ActionEvent e) {
if (currentSite == null) if (currentSite == null)
return; return;
else if (currentSite instanceof ModelSite) else if (currentSite instanceof ModelSite)
PasswordFrame.this.user.deleteSite(currentSite); PasswordFrame.this.user.deleteSite( currentSite );
else else
PasswordFrame.this.user.addSite(currentSite); PasswordFrame.this.user.addSite( currentSite );
siteNameField.requestFocus(); siteNameField.requestFocus();
updatePassword( true ); updatePassword( true );
} }
}); } );
sitePanel.add( siteControls ); sitePanel.add( siteControls );
sitePanel.add( Components.stud() ); sitePanel.add( Components.stud() );
// Site Type & Counter // Site Type & Counter
MPSiteType[] types = Iterables.toArray( MPSiteType.forClass( MPSiteTypeClass.Generated ), MPSiteType.class ); MPSiteType[] types = Iterables.toArray( MPSiteType.forClass( MPSiteTypeClass.Generated ), MPSiteType.class );
JComponent siteSettings = Components.boxLayout( BoxLayout.LINE_AXIS, // JComponent siteSettings = Components.boxLayout( BoxLayout.LINE_AXIS, //
siteTypeField = Components.comboBox( types ), // siteTypeField = Components.comboBox( types ), //
Components.stud(), // Components.stud(), //
siteVersionField = Components.comboBox( MasterKey.Version.values() ), // siteVersionField = Components.comboBox( MasterKey.Version.values() ), //
Components.stud(), // Components.stud(), //
siteCounterField = Components.spinner( siteCounterField = Components.spinner(
new SpinnerNumberModel( 1, 1, Integer.MAX_VALUE, 1 ) ) ); new SpinnerNumberModel( 1L, 1L, UnsignedInteger.MAX_VALUE, 1L ) ) );
sitePanel.add( siteSettings ); sitePanel.add( siteSettings );
siteTypeField.setFont( Res.valueFont().deriveFont( 12f ) ); siteTypeField.setFont( Res.valueFont().deriveFont( 12f ) );
siteTypeField.setSelectedItem( MPSiteType.GeneratedLong ); siteTypeField.setSelectedItem( MPSiteType.GeneratedLong );
siteTypeField.addItemListener( new ItemListener() { siteTypeField.addItemListener( new ItemListener() {
@Override @Override
public void itemStateChanged(final ItemEvent e) { public void itemStateChanged(final ItemEvent e) {
updatePassword(true); updatePassword( true );
} }
} ); } );
@ -135,7 +136,7 @@ public class PasswordFrame extends JFrame implements DocumentListener {
siteVersionField.addItemListener( new ItemListener() { siteVersionField.addItemListener( new ItemListener() {
@Override @Override
public void itemStateChanged(final ItemEvent e) { public void itemStateChanged(final ItemEvent e) {
updatePassword(true); updatePassword( true );
} }
} ); } );
@ -144,7 +145,7 @@ public class PasswordFrame extends JFrame implements DocumentListener {
siteCounterField.addChangeListener( new ChangeListener() { siteCounterField.addChangeListener( new ChangeListener() {
@Override @Override
public void stateChanged(final ChangeEvent e) { public void stateChanged(final ChangeEvent e) {
updatePassword(true); updatePassword( true );
} }
} ); } );
@ -161,11 +162,11 @@ public class PasswordFrame extends JFrame implements DocumentListener {
// Password // Password
passwordField = Components.passwordField(); passwordField = Components.passwordField();
passwordField.setAlignmentX(Component.CENTER_ALIGNMENT); passwordField.setAlignmentX( Component.CENTER_ALIGNMENT );
passwordField.setHorizontalAlignment(JTextField.CENTER); passwordField.setHorizontalAlignment( JTextField.CENTER );
passwordField.putClientProperty("JPasswordField.cutCopyAllowed", true); passwordField.putClientProperty( "JPasswordField.cutCopyAllowed", true );
passwordField.setEditable(false); passwordField.setEditable( false );
passwordField.setBackground(null); passwordField.setBackground( null );
passwordField.setBorder( null ); passwordField.setBorder( null );
passwordEchoChar = passwordField.getEchoChar(); passwordEchoChar = passwordField.getEchoChar();
passwordEchoFont = passwordField.getFont().deriveFont( 40f ); passwordEchoFont = passwordField.getFont().deriveFont( 40f );
@ -174,7 +175,8 @@ public class PasswordFrame extends JFrame implements DocumentListener {
// Tip // Tip
tipLabel = Components.label( " ", SwingConstants.CENTER ); tipLabel = Components.label( " ", SwingConstants.CENTER );
tipLabel.setAlignmentX( Component.CENTER_ALIGNMENT ); tipLabel.setAlignmentX( Component.CENTER_ALIGNMENT );
JPanel passwordContainer = Components.boxLayout( BoxLayout.PAGE_AXIS, maskPasswordField, Box.createGlue(), passwordField, Box.createGlue(), tipLabel ); JPanel passwordContainer = Components.boxLayout( BoxLayout.PAGE_AXIS, maskPasswordField, Box.createGlue(), passwordField,
Box.createGlue(), tipLabel );
passwordContainer.setOpaque( true ); passwordContainer.setOpaque( true );
passwordContainer.setBackground( Color.white ); passwordContainer.setBackground( Color.white );
passwordContainer.setBorder( BorderFactory.createEmptyBorder( 8, 8, 8, 8 ) ); passwordContainer.setBorder( BorderFactory.createEmptyBorder( 8, 8, 8, 8 ) );
@ -202,27 +204,27 @@ public class PasswordFrame extends JFrame implements DocumentListener {
if (updatingUI) if (updatingUI)
return Futures.immediateCancelledFuture(); return Futures.immediateCancelledFuture();
if (siteNameQuery == null || siteNameQuery.isEmpty() || !user.isKeyAvailable()) { if (siteNameQuery == null || siteNameQuery.isEmpty() || !user.isKeyAvailable()) {
siteActionButton.setVisible(false); siteActionButton.setVisible( false );
tipLabel.setText( null ); tipLabel.setText( null );
passwordField.setText( null ); passwordField.setText( null );
return Futures.immediateCancelledFuture(); return Futures.immediateCancelledFuture();
} }
final MPSiteType siteType = siteTypeField.getModel().getElementAt(siteTypeField.getSelectedIndex()); final MPSiteType siteType = siteTypeField.getModel().getElementAt( siteTypeField.getSelectedIndex() );
final MasterKey.Version siteVersion = siteVersionField.getItemAt(siteVersionField.getSelectedIndex()); final MasterKey.Version siteVersion = siteVersionField.getItemAt( siteVersionField.getSelectedIndex() );
final int siteCounter = (Integer) siteCounterField.getValue(); final UnsignedInteger siteCounter = UnsignedInteger.valueOf( ((Number) siteCounterField.getValue()).longValue() );
Iterable<Site> siteResults = user.findSitesByName(siteNameQuery); Iterable<Site> siteResults = user.findSitesByName( siteNameQuery );
if (!allowNameCompletion) if (!allowNameCompletion)
siteResults = FluentIterable.from(siteResults).filter(new PredicateNN<Site>() { siteResults = FluentIterable.from( siteResults ).filter( new PredicateNN<Site>() {
@Override @Override
public boolean apply(Site input) { public boolean apply(Site input) {
return siteNameQuery.equals(input.getSiteName()); return siteNameQuery.equals( input.getSiteName() );
} }
}); } );
final Site site = Iterables.getFirst(siteResults, final Site site = Iterables.getFirst( siteResults,
new IncognitoSite(siteNameQuery, siteType, siteCounter, siteVersion) ); new IncognitoSite( siteNameQuery, siteType, siteCounter, siteVersion ) );
if (currentSite != null && site.getSiteName().equals(currentSite.getSiteName())) { if (currentSite != null && currentSite.getSiteName().equals( site.getSiteName() )) {
site.setSiteType( siteType ); site.setSiteType( siteType );
site.setAlgorithmVersion( siteVersion ); site.setAlgorithmVersion( siteVersion );
site.setSiteCounter( siteCounter ); site.setSiteCounter( siteCounter );
@ -244,12 +246,12 @@ public class PasswordFrame extends JFrame implements DocumentListener {
public void run() { public void run() {
updatingUI = true; updatingUI = true;
currentSite = site; currentSite = site;
siteActionButton.setVisible(user instanceof ModelUser); siteActionButton.setVisible( user instanceof ModelUser );
if (currentSite instanceof ModelSite) if (currentSite instanceof ModelSite)
siteActionButton.setText("Delete Site"); siteActionButton.setText( "Delete Site" );
else else
siteActionButton.setText("Add Site"); siteActionButton.setText( "Add Site" );
siteTypeField.setSelectedItem(currentSite.getSiteType()); siteTypeField.setSelectedItem( currentSite.getSiteType() );
siteVersionField.setSelectedItem( currentSite.getAlgorithmVersion() ); siteVersionField.setSelectedItem( currentSite.getAlgorithmVersion() );
siteCounterField.setValue( currentSite.getSiteCounter() ); siteCounterField.setValue( currentSite.getSiteCounter() );
siteNameField.setText( currentSite.getSiteName() ); siteNameField.setText( currentSite.getSiteName() );
@ -273,16 +275,16 @@ public class PasswordFrame extends JFrame implements DocumentListener {
@Override @Override
public void insertUpdate(final DocumentEvent e) { public void insertUpdate(final DocumentEvent e) {
updatePassword(true); updatePassword( true );
} }
@Override @Override
public void removeUpdate(final DocumentEvent e) { public void removeUpdate(final DocumentEvent e) {
updatePassword(false); updatePassword( false );
} }
@Override @Override
public void changedUpdate(final DocumentEvent e) { public void changedUpdate(final DocumentEvent e) {
updatePassword(true); updatePassword( true );
} }
} }

View File

@ -2,6 +2,7 @@ package com.lyndir.masterpassword.gui;
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf; import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.MPSiteType; import com.lyndir.masterpassword.MPSiteType;
import com.lyndir.masterpassword.MasterKey; import com.lyndir.masterpassword.MasterKey;
@ -23,9 +24,9 @@ public abstract class Site {
public abstract void setAlgorithmVersion(final MasterKey.Version algorithmVersion); public abstract void setAlgorithmVersion(final MasterKey.Version algorithmVersion);
public abstract int getSiteCounter(); public abstract UnsignedInteger getSiteCounter();
public abstract void setSiteCounter(final int siteCounter); public abstract void setSiteCounter(final UnsignedInteger siteCounter);
@Override @Override
public String toString() { public String toString() {

View File

@ -26,6 +26,11 @@
<version>GIT-SNAPSHOT</version> <version>GIT-SNAPSHOT</version>
</dependency> </dependency>
<!-- EXTERNAL DEPENDENCIES -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.google.auto.value</groupId> <groupId>com.google.auto.value</groupId>
<artifactId>auto-value</artifactId> <artifactId>auto-value</artifactId>

View File

@ -2,6 +2,7 @@ package com.lyndir.masterpassword.model;
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf; import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import java.util.Objects; import java.util.Objects;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -13,15 +14,15 @@ import org.joda.time.Instant;
*/ */
public class MPSite { public class MPSite {
public static final MPSiteType DEFAULT_TYPE = MPSiteType.GeneratedLong; public static final MPSiteType DEFAULT_TYPE = MPSiteType.GeneratedLong;
public static final int DEFAULT_COUNTER = 1; public static final UnsignedInteger DEFAULT_COUNTER = UnsignedInteger.valueOf( 1 );
private final MPUser user; private final MPUser user;
private MasterKey.Version algorithmVersion; private MasterKey.Version algorithmVersion;
private Instant lastUsed; private Instant lastUsed;
private String siteName; private String siteName;
private MPSiteType siteType; private MPSiteType siteType;
private int siteCounter; private UnsignedInteger siteCounter;
private int uses; private int uses;
private String loginName; private String loginName;
@ -29,7 +30,7 @@ public class MPSite {
this( user, siteName, DEFAULT_TYPE, DEFAULT_COUNTER ); this( user, siteName, DEFAULT_TYPE, DEFAULT_COUNTER );
} }
public MPSite(final MPUser user, final String siteName, final MPSiteType siteType, final int siteCounter) { public MPSite(final MPUser user, final String siteName, final MPSiteType siteType, final UnsignedInteger siteCounter) {
this.user = user; this.user = user;
this.algorithmVersion = MasterKey.Version.CURRENT; this.algorithmVersion = MasterKey.Version.CURRENT;
this.lastUsed = new Instant(); this.lastUsed = new Instant();
@ -39,7 +40,7 @@ public class MPSite {
} }
protected MPSite(final MPUser user, final MasterKey.Version algorithmVersion, final Instant lastUsed, final String siteName, protected MPSite(final MPUser user, final MasterKey.Version algorithmVersion, final Instant lastUsed, final String siteName,
final MPSiteType siteType, final int siteCounter, final int uses, @Nullable final String loginName, final MPSiteType siteType, final UnsignedInteger siteCounter, final int uses, @Nullable final String loginName,
@Nullable final String importContent) { @Nullable final String importContent) {
this.user = user; this.user = user;
this.algorithmVersion = algorithmVersion; this.algorithmVersion = algorithmVersion;
@ -101,11 +102,11 @@ public class MPSite {
this.siteType = siteType; this.siteType = siteType;
} }
public int getSiteCounter() { public UnsignedInteger getSiteCounter() {
return siteCounter; return siteCounter;
} }
public void setSiteCounter(final int siteCounter) { public void setSiteCounter(final UnsignedInteger siteCounter) {
this.siteCounter = siteCounter; this.siteCounter = siteCounter;
} }

View File

@ -5,6 +5,7 @@ import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams; 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.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ConversionUtils; import com.lyndir.lhunath.opal.system.util.ConversionUtils;
@ -139,7 +140,7 @@ public class MPSiteUnmarshaller {
rfc3339.parseDateTime( siteMatcher.group( 1 ) ).toInstant(), // rfc3339.parseDateTime( siteMatcher.group( 1 ) ).toInstant(), //
siteMatcher.group( 7 ), // siteMatcher.group( 7 ), //
MPSiteType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ), MPSiteType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
ConversionUtils.toIntegerNN( siteMatcher.group( 5 ).replace( ":", "" ) ), // UnsignedInteger.valueOf( siteMatcher.group( 5 ).replace( ":", "" ) ), //
ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), // ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), //
siteMatcher.group( 6 ), // siteMatcher.group( 6 ), //
siteMatcher.group( 8 ) ); siteMatcher.group( 8 ) );

View File

@ -1,24 +1,33 @@
package com.lyndir.masterpassword; package com.lyndir.masterpassword;
import com.google.common.io.Resources; 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.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import com.lyndir.lhunath.opal.system.util.NNFunctionNN; import com.lyndir.lhunath.opal.system.util.NNFunctionNN;
import java.net.URL; import java.io.IOException;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.Callable;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.xml.bind.JAXBContext; import javax.xml.parsers.*;
import javax.xml.bind.JAXBException; import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.ext.DefaultHandler2;
/** /**
* @author lhunath, 2015-12-22 * @author lhunath, 2015-12-22
*/ */
public class MPTestSuite { public class MPTestSuite implements Callable<Boolean> {
@SuppressWarnings("UnusedDeclaration") @SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPTestSuite.class ); private static final Logger logger = Logger.get( MPTestSuite.class );
private static final String DEFAULT_RESOURCE_NAME = "mpw_tests.xml"; private static final String DEFAULT_RESOURCE_NAME = "mpw_tests.xml";
private MPTests tests; private MPTests tests;
private Listener listener;
public MPTestSuite() public MPTestSuite()
throws UnavailableException { throws UnavailableException {
@ -28,15 +37,78 @@ public class MPTestSuite {
public MPTestSuite(String resourceName) public MPTestSuite(String resourceName)
throws UnavailableException { throws UnavailableException {
try { try {
URL testCasesResource = Resources.getResource( resourceName ); tests = new MPTests();
tests = (MPTests) JAXBContext.newInstance( MPTests.class ).createUnmarshaller().unmarshal( testCasesResource ); 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;
for (MPTests.Case testCase : tests.getCases()) @Override
testCase.initializeParentHierarchy( tests ); 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 | JAXBException e) { catch (IllegalArgumentException | ParserConfigurationException | SAXException | IOException e) {
throw new UnavailableException( 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() { public MPTests getTests() {
@ -44,22 +116,39 @@ public class MPTestSuite {
} }
public boolean forEach(String testName, NNFunctionNN<MPTests.Case, Boolean> testFunction) { public boolean forEach(String testName, NNFunctionNN<MPTests.Case, Boolean> testFunction) {
for (MPTests.Case testCase : tests.getCases()) { List<MPTests.Case> cases = tests.getCases();
for (int c = 0; c < cases.size(); c++) {
MPTests.Case testCase = cases.get( c );
if (testCase.getResult().isEmpty()) if (testCase.getResult().isEmpty())
continue; continue;
logger.inf( "[%s] on %s...", testName, testCase.getIdentifier() ); progress( Logger.Target.INFO, c, cases.size(), //
"[%s] on %s...", testName, testCase.getIdentifier() );
if (!testFunction.apply( testCase )) { if (!testFunction.apply( testCase )) {
logger.err( "[%s] on %s: FAILED!", testName, testCase.getIdentifier() ); progress( Logger.Target.ERROR, cases.size(), cases.size(), //
"[%s] on %s: FAILED!", testName, testCase.getIdentifier() );
return false; return false;
} }
logger.inf( "[%s] on %s: passed!", testName, testCase.getIdentifier() );
progress( Logger.Target.INFO, c + 1, cases.size(), //
"[%s] on %s: passed!", testName, testCase.getIdentifier() );
} }
return true; return true;
} }
public boolean run() { 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>() { return forEach( "mpw", new NNFunctionNN<MPTests.Case, Boolean>() {
@Nonnull @Nonnull
@Override @Override
@ -79,4 +168,10 @@ public class MPTestSuite {
super( cause ); super( cause );
} }
} }
public interface Listener {
void progress(int current, int max, String messageFormat, Object... args);
}
} }

View File

@ -3,18 +3,17 @@ package com.lyndir.masterpassword;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; 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.logging.Logger;
import com.lyndir.lhunath.opal.system.util.*; import com.lyndir.lhunath.opal.system.util.*;
import java.util.List; import java.util.List;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.xml.bind.annotation.*;
/** /**
* @author lhunath, 14-12-05 * @author lhunath, 14-12-05
*/ */
@XmlRootElement(name = "tests")
public class MPTests { public class MPTests {
private static final String ID_DEFAULT = "default"; private static final String ID_DEFAULT = "default";
@ -22,8 +21,7 @@ public class MPTests {
@SuppressWarnings("UnusedDeclaration") @SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPTests.class ); private static final Logger logger = Logger.get( MPTests.class );
@XmlElement(name = "case") List<Case> cases;
private List<Case> cases;
@Nonnull @Nonnull
public List<Case> getCases() { public List<Case> getCases() {
@ -47,33 +45,20 @@ public class MPTests {
} }
} }
@XmlRootElement(name = "case")
public static class Case { public static class Case {
@XmlAttribute(name = "id") String identifier;
private String identifier; String parent;
@XmlAttribute Integer algorithm;
private String parent; String fullName;
@XmlElement String masterPassword;
private String algorithm; String keyID;
@XmlElement String siteName;
private String fullName; UnsignedInteger siteCounter;
@XmlElement String siteType;
private String masterPassword; String siteVariant;
@XmlElement String siteContext;
private String keyID; String result;
@XmlElement
private String siteName;
@XmlElement
private Integer siteCounter;
@XmlElement
private String siteType;
@XmlElement
private String siteVariant;
@XmlElement
private String siteContext;
@XmlElement
private String result;
private transient Case parentCase; private transient Case parentCase;
@ -84,10 +69,10 @@ public class MPTests {
parentCase.initializeParentHierarchy( tests ); parentCase.initializeParentHierarchy( tests );
} }
algorithm = ifNotNullElse( algorithm, new NNSupplier<String>() { algorithm = ifNotNullElse( algorithm, new NNSupplier<Integer>() {
@Nonnull @Nonnull
@Override @Override
public String get() { public Integer get() {
return checkNotNull( parentCase.algorithm ); return checkNotNull( parentCase.algorithm );
} }
} ); } );
@ -119,10 +104,10 @@ public class MPTests {
return checkNotNull( parentCase.siteName ); return checkNotNull( parentCase.siteName );
} }
} ); } );
siteCounter = ifNotNullElse( siteCounter, new NNSupplier<Integer>() { siteCounter = ifNotNullElse( siteCounter, new NNSupplier<UnsignedInteger>() {
@Nonnull @Nonnull
@Override @Override
public Integer get() { public UnsignedInteger get() {
return checkNotNull( parentCase.siteCounter ); return checkNotNull( parentCase.siteCounter );
} }
} ); } );
@ -168,7 +153,7 @@ public class MPTests {
@Nonnull @Nonnull
public MasterKey.Version getAlgorithm() { public MasterKey.Version getAlgorithm() {
return MasterKey.Version.fromInt( ConversionUtils.toIntegerNN( algorithm ) ); return MasterKey.Version.fromInt( checkNotNull( algorithm ) );
} }
@Nonnull @Nonnull
@ -191,8 +176,8 @@ public class MPTests {
return checkNotNull( siteName ); return checkNotNull( siteName );
} }
public int getSiteCounter() { public UnsignedInteger getSiteCounter() {
return ifNotNullElse( siteCounter, 1 ); return ifNotNullElse( siteCounter, UnsignedInteger.valueOf( 1 ) );
} }
@Nonnull @Nonnull

View File

@ -1,7 +1,7 @@
<tests> <tests>
<!-- Default values for all parameters. --> <!-- Default values for all parameters. -->
<case id="default"> <case id="default">
<algorithm><!-- current --></algorithm> <algorithm>-1</algorithm>
<fullName>Robert Lee Mitchell</fullName> <fullName>Robert Lee Mitchell</fullName>
<masterPassword>banana colored duckling</masterPassword> <masterPassword>banana colored duckling</masterPassword>
<keyID>98EEF4D1DF46D849574A82A03C3177056B15DFFCA29BB3899DE4628453675302</keyID> <keyID>98EEF4D1DF46D849574A82A03C3177056B15DFFCA29BB3899DE4628453675302</keyID>