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>
<groupId>com.lyndir.lhunath.opal</groupId>
<artifactId>opal-system</artifactId>
<version>1.6-p8</version>
<version>1.6-p9</version>
<exclusions>
<exclusion>
<groupId>joda-time</groupId>

View File

@ -1,6 +1,7 @@
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;
@ -74,7 +75,7 @@ public abstract class MasterKey {
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);
public boolean isValid() {
@ -106,7 +107,9 @@ public abstract class MasterKey {
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);

View File

@ -3,6 +3,7 @@ package com.lyndir.masterpassword;
import com.google.common.base.Charsets;
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;
@ -10,14 +11,15 @@ import java.nio.*;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* 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.
* - 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
*/
@ -75,18 +77,19 @@ public class MasterKeyV0 extends MasterKey {
}
}
public String encode(final String siteName, final MPSiteType siteType, int siteCounter, final MPSiteVariant siteVariant,
@Nullable final String siteContext) {
@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 );
logger.trc( "siteCounter: %d", siteCounter.longValue() );
logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant );
logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType );
if (siteCounter == 0)
siteCounter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300;
if (siteCounter.longValue() == 0)
siteCounter = UnsignedInteger.valueOf( (System.currentTimeMillis() / (300 * 1000)) * 300 );
String siteScope = siteVariant.getScope();
byte[] siteNameBytes = siteName.getBytes( MP_charset );
@ -108,7 +111,7 @@ public class MasterKeyV0 extends MasterKey {
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 );
Arrays.fill( buf.array(), sitePasswordSeedBytes[i] > 0? (byte) 0x00: (byte) 0xFF );
buf.position( 2 );
buf.put( sitePasswordSeedBytes[i] ).rewind();
sitePasswordSeed[i] = buf.getInt() & 0xFFFF;
@ -135,8 +138,13 @@ public class MasterKeyV0 extends MasterKey {
}
@Override
protected byte[] bytesForInt(final int integer) {
return ByteBuffer.allocate( MP_intLen / Byte.SIZE ).order( MP_byteOrder ).putInt( integer ).array();
protected byte[] bytesForInt(final int number) {
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

View File

@ -2,15 +2,17 @@ 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:
* - miscounted the byte-length fromInt multi-byte site names.
* - miscounted the byte-length fromInt multi-byte full names.
* - 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
*/
@ -29,18 +31,19 @@ public class MasterKeyV1 extends MasterKeyV0 {
return Version.V1;
}
public String encode(final String siteName, final MPSiteType siteType, int siteCounter, final MPSiteVariant siteVariant,
@Nullable final String siteContext) {
@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 );
logger.trc( "siteCounter: %d", siteCounter.longValue() );
logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant );
logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType );
if (siteCounter == 0)
siteCounter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300;
if (siteCounter.longValue() == 0)
siteCounter = UnsignedInteger.valueOf( (System.currentTimeMillis() / (300 * 1000)) * 300 );
String siteScope = siteVariant.getScope();
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.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:
* - miscounted the byte-length fromInt multi-byte full names.
* - V2: miscounted the byte-length fromInt multi-byte full names.
*
* @author lhunath, 2014-08-30
*/
@ -28,18 +30,19 @@ public class MasterKeyV2 extends MasterKeyV1 {
return Version.V2;
}
public String encode(final String siteName, final MPSiteType siteType, int siteCounter, final MPSiteVariant siteVariant,
@Nullable final String siteContext) {
@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 );
logger.trc( "siteCounter: %d", siteCounter.longValue() );
logger.trc( "siteVariant: %d (%s)", siteVariant.ordinal(), siteVariant );
logger.trc( "siteType: %d (%s)", siteType.ordinal(), siteType );
if (siteCounter == 0)
siteCounter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300;
if (siteCounter.longValue() == 0)
siteCounter = UnsignedInteger.valueOf( (System.currentTimeMillis() / (300 * 1000)) * 300 );
String siteScope = siteVariant.getScope();
byte[] siteNameBytes = siteName.getBytes( MP_charset );

View File

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

View File

@ -107,6 +107,13 @@
<version>GIT-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.lyndir.masterpassword</groupId>
<artifactId>masterpassword-tests</artifactId>
<version>GIT-SNAPSHOT</version>
</dependency>
<!-- EXTERNAL DEPENDENCIES -->
<dependency>
<groupId>com.jakewharton</groupId>
<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="siteVersion_hint">Algorithm</string>
<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>

View File

@ -1,5 +1,6 @@
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 android.app.*;
@ -16,9 +17,9 @@ import android.widget.*;
import butterknife.ButterKnife;
import butterknife.InjectView;
import com.google.common.base.Throwables;
import com.google.common.primitives.UnsignedInteger;
import com.google.common.util.concurrent.*;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import java.util.*;
import java.util.concurrent.*;
import javax.annotation.Nullable;
@ -87,6 +88,10 @@ public class EmergencyActivity extends Activity {
private int hc_masterPassword;
private String sitePassword;
public static void start(Context context) {
context.startActivity( new Intent( context, EmergencyActivity.class ) );
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate( savedInstanceState );
@ -261,7 +266,7 @@ public class EmergencyActivity extends Activity {
private void updateSitePassword() {
final String siteName = siteNameField.getText().toString();
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) {
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.
*/
package com.lyndir.masterpassword;
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.collect.Maps;
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.StringUtils;
import java.io.*;
@ -52,7 +52,7 @@ public class CLI {
MPSiteType siteType = siteTypeName.isEmpty()? MPSiteType.GeneratedLong: MPSiteType.forOption( siteTypeName );
MPSiteVariant variant = MPSiteVariant.Password;
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.
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 ))
counterArg = true;
else if (counterArg) {
siteCounter = ConversionUtils.toIntegerNN( arg );
siteCounter = UnsignedInteger.valueOf( arg );
counterArg = false;
}

View File

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

View File

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

View File

@ -26,6 +26,11 @@
<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>

View File

@ -2,6 +2,7 @@ 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;
@ -13,15 +14,15 @@ import org.joda.time.Instant;
*/
public class MPSite {
public static final MPSiteType DEFAULT_TYPE = MPSiteType.GeneratedLong;
public static final int DEFAULT_COUNTER = 1;
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 int siteCounter;
private UnsignedInteger siteCounter;
private int uses;
private String loginName;
@ -29,7 +30,7 @@ public class MPSite {
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.algorithmVersion = MasterKey.Version.CURRENT;
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,
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) {
this.user = user;
this.algorithmVersion = algorithmVersion;
@ -101,11 +102,11 @@ public class MPSite {
this.siteType = siteType;
}
public int getSiteCounter() {
public UnsignedInteger getSiteCounter() {
return siteCounter;
}
public void setSiteCounter(final int siteCounter) {
public void setSiteCounter(final UnsignedInteger 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.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;
@ -139,7 +140,7 @@ public class MPSiteUnmarshaller {
rfc3339.parseDateTime( siteMatcher.group( 1 ) ).toInstant(), //
siteMatcher.group( 7 ), //
MPSiteType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
ConversionUtils.toIntegerNN( siteMatcher.group( 5 ).replace( ":", "" ) ), //
UnsignedInteger.valueOf( siteMatcher.group( 5 ).replace( ":", "" ) ), //
ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), //
siteMatcher.group( 6 ), //
siteMatcher.group( 8 ) );

View File

@ -1,24 +1,33 @@
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.util.ConversionUtils;
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.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
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 {
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 MPTests tests;
private Listener listener;
public MPTestSuite()
throws UnavailableException {
@ -28,15 +37,78 @@ public class MPTestSuite {
public MPTestSuite(String resourceName)
throws UnavailableException {
try {
URL testCasesResource = Resources.getResource( resourceName );
tests = (MPTests) JAXBContext.newInstance( MPTests.class ).createUnmarshaller().unmarshal( testCasesResource );
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;
for (MPTests.Case testCase : tests.getCases())
testCase.initializeParentHierarchy( tests );
@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 | JAXBException e) {
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() {
@ -44,22 +116,39 @@ public class MPTestSuite {
}
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())
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 )) {
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;
}
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;
}
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>() {
@Nonnull
@Override
@ -79,4 +168,10 @@ public class MPTestSuite {
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.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;
import javax.xml.bind.annotation.*;
/**
* @author lhunath, 14-12-05
*/
@XmlRootElement(name = "tests")
public class MPTests {
private static final String ID_DEFAULT = "default";
@ -22,8 +21,7 @@ public class MPTests {
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPTests.class );
@XmlElement(name = "case")
private List<Case> cases;
List<Case> cases;
@Nonnull
public List<Case> getCases() {
@ -47,33 +45,20 @@ public class MPTests {
}
}
@XmlRootElement(name = "case")
public static class Case {
@XmlAttribute(name = "id")
private String identifier;
@XmlAttribute
private String parent;
@XmlElement
private String algorithm;
@XmlElement
private String fullName;
@XmlElement
private String masterPassword;
@XmlElement
private String keyID;
@XmlElement
private String siteName;
@XmlElement
private Integer siteCounter;
@XmlElement
private String siteType;
@XmlElement
private String siteVariant;
@XmlElement
private String siteContext;
@XmlElement
private String result;
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;
@ -84,10 +69,10 @@ public class MPTests {
parentCase.initializeParentHierarchy( tests );
}
algorithm = ifNotNullElse( algorithm, new NNSupplier<String>() {
algorithm = ifNotNullElse( algorithm, new NNSupplier<Integer>() {
@Nonnull
@Override
public String get() {
public Integer get() {
return checkNotNull( parentCase.algorithm );
}
} );
@ -119,10 +104,10 @@ public class MPTests {
return checkNotNull( parentCase.siteName );
}
} );
siteCounter = ifNotNullElse( siteCounter, new NNSupplier<Integer>() {
siteCounter = ifNotNullElse( siteCounter, new NNSupplier<UnsignedInteger>() {
@Nonnull
@Override
public Integer get() {
public UnsignedInteger get() {
return checkNotNull( parentCase.siteCounter );
}
} );
@ -168,7 +153,7 @@ public class MPTests {
@Nonnull
public MasterKey.Version getAlgorithm() {
return MasterKey.Version.fromInt( ConversionUtils.toIntegerNN( algorithm ) );
return MasterKey.Version.fromInt( checkNotNull( algorithm ) );
}
@Nonnull
@ -191,8 +176,8 @@ public class MPTests {
return checkNotNull( siteName );
}
public int getSiteCounter() {
return ifNotNullElse( siteCounter, 1 );
public UnsignedInteger getSiteCounter() {
return ifNotNullElse( siteCounter, UnsignedInteger.valueOf( 1 ) );
}
@Nonnull

View File

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