2
0

Fixes #138- delays identicon update to avoid leaking interactive keyboard input.

This commit is contained in:
Maarten Billemont 2017-03-10 11:31:22 -05:00
parent c3f4d148a4
commit 77bee803b8
24 changed files with 188 additions and 70 deletions

View File

@ -10,7 +10,7 @@ To skip the intro and go straight to the information on how to use the code, [cl
Master Password is available for [📲 iOS](https://itunes.apple.com/app/id510296984), [🖥 macOS](https://ssl.masterpasswordapp.com/masterpassword-mac.zip), [📲 Android](https://ssl.masterpasswordapp.com/masterpassword-android.apk), [🖥 Desktop](https://ssl.masterpasswordapp.com/masterpassword-gui.jar), and [⌨ Console](https://ssl.masterpasswordapp.com/masterpassword-cli.tar.gz). Master Password is available for [📲 iOS](https://itunes.apple.com/app/id510296984), [🖥 macOS](https://ssl.masterpasswordapp.com/masterpassword-mac.zip), [📲 Android](https://ssl.masterpasswordapp.com/masterpassword-android.apk), [🖥 Desktop](https://ssl.masterpasswordapp.com/masterpassword-gui.jar), and [⌨ Console](https://ssl.masterpasswordapp.com/masterpassword-cli.tar.gz).
Master Password is also available from the following package managers: [mac OS: Homebrew](https://brew.sh/). Get in touch if you are interested in adding Master Password to any other package managers. Master Password is also available from the following package managers: [macOS: Homebrew](https://brew.sh/). Get in touch if you are interested in adding Master Password to any other package managers.
## What is a password? ## What is a password?

View File

@ -1,12 +1,15 @@
apply plugin: 'java' plugins {
id 'java'
}
description = 'Master Password Algorithm Implementation' description = 'Master Password Algorithm Implementation'
dependencies { dependencies {
compile( 'com.lyndir.lhunath.opal:opal-system:1.6-p10' ) { compile (group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.6-p10') {
exclude( module: 'joda-time' ) exclude( module: 'joda-time' )
} }
compile 'com.lambdaworks:scrypt:1.4.0'
compile 'org.jetbrains:annotations:13.0' compile group: 'com.lambdaworks', name: 'scrypt', version: '1.4.0'
compile 'com.google.code.findbugs:jsr305:3.0.1' compile group: 'org.jetbrains', name: 'annotations', version: '13.0'
compile group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.1'
} }

View File

@ -9,7 +9,6 @@ dependencies {
compile project(':masterpassword:algorithm') compile project(':masterpassword:algorithm')
compile group: 'joda-time', name: 'joda-time', version:'2.4' compile group: 'joda-time', name: 'joda-time', version:'2.4'
compileOnly group: 'com.google.auto.value', name: 'auto-value', version: '1.2' compileOnly group: 'com.google.auto.value', name: 'auto-value', version: '1.2'
apt group: 'com.google.auto.value', name: 'auto-value', version: '1.2' apt group: 'com.google.auto.value', name: 'auto-value', version: '1.2'

View File

@ -1,4 +1,6 @@
apply plugin: 'java' plugins {
id 'java'
}
description = 'Master Password Test Suite' description = 'Master Password Test Suite'

View File

@ -2,14 +2,10 @@ package com.lyndir.masterpassword;
import static org.testng.Assert.*; import static org.testng.Assert.*;
import com.google.common.io.Resources;
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.NNFunctionNN; import com.lyndir.lhunath.opal.system.util.NNFunctionNN;
import com.lyndir.lhunath.opal.system.util.StringUtils;
import java.net.URL;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.xml.bind.JAXBContext;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test; import org.testng.annotations.Test;
@ -88,7 +84,7 @@ public class MasterKeyTest {
masterKey.encode( defaultCase.getSiteName(), defaultCase.getSiteType(), defaultCase.getSiteCounter(), masterKey.encode( defaultCase.getSiteName(), defaultCase.getSiteType(), defaultCase.getSiteCounter(),
defaultCase.getSiteVariant(), defaultCase.getSiteContext() ); defaultCase.getSiteVariant(), defaultCase.getSiteContext() );
assertTrue( false, "[testInvalidate] Master key should have been invalidated, but was still usable." ); fail( "[testInvalidate] Master key should have been invalidated, but was still usable." );
} }
catch (IllegalStateException ignored) { catch (IllegalStateException ignored) {
} }

View File

@ -0,0 +1,30 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Android" type="AndroidRunConfigurationType" factoryName="Android App">
<module name="android" />
<option name="DEPLOY" value="true" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="TARGET_SELECTION_MODE" value="SHOW_DIALOG" />
<option name="PREFERRED_AVD" value="" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="DEBUGGER_TYPE" value="Java" />
<option name="USE_LAST_SELECTED_DEVICE" value="false" />
<option name="PREFERRED_AVD" value="" />
<Java />
<Profilers>
<option name="ENABLE_ADVANCED_PROFILING" value="false" />
<option name="GAPID_ENABLED" value="false" />
<option name="GAPID_DISABLE_PCS" value="false" />
<option name="SUPPORT_LIB_ENABLED" value="true" />
<option name="INSTRUMENTATION_ENABLED" value="true" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY_CLASS" value="" />
<method />
</configuration>
</component>

View File

@ -0,0 +1,16 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="GUI" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="com.lyndir.masterpassword.gui.GUI" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="ENABLE_SWING_INSPECTOR" value="false" />
<option name="ENV_VARIABLES" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="gui" />
<envs />
<method />
</configuration>
</component>

View File

@ -0,0 +1,29 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Tests" type="TestNG" factoryName="TestNG">
<module name="tests" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="SUITE_NAME" value="" />
<option name="PACKAGE_NAME" value="com.lyndir.masterpassword" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="GROUP_NAME" value="" />
<option name="TEST_OBJECT" value="PACKAGE" />
<option name="VM_PARAMETERS" value="-ea" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$/../core/java/tests" />
<option name="OUTPUT_DIRECTORY" value="" />
<option name="ANNOTATION_TYPE" />
<option name="ENV_VARIABLES" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="singleModule" />
</option>
<option name="USE_DEFAULT_REPORTERS" value="false" />
<option name="PROPERTIES_FILE" value="" />
<envs />
<properties />
<listeners />
<method />
</configuration>
</component>

View File

@ -5,8 +5,8 @@ allprojects {
version = 'GIT-SNAPSHOT' version = 'GIT-SNAPSHOT'
tasks.withType(JavaCompile) { tasks.withType(JavaCompile) {
sourceCompatibility = "1.7" sourceCompatibility = '1.7'
targetCompatibility = "1.7" targetCompatibility = '1.7'
} }
tasks.withType(FindBugs) { tasks.withType(FindBugs) {
reports { reports {
@ -29,6 +29,6 @@ buildscript {
subprojects { subprojects {
repositories { repositories {
mavenCentral() mavenCentral()
maven { url "http://maven.lyndir.com" } maven { url 'http://maven.lyndir.com' }
} }
} }

View File

@ -2,32 +2,34 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion 25 compileSdkVersion 25
buildToolsVersion "25.0.0" buildToolsVersion '25.0.0'
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7 sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7
} }
defaultConfig { defaultConfig {
applicationId "com.lyndir.masterpassword" applicationId 'com.lyndir.masterpassword'
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 25 targetSdkVersion 25
versionCode 20401 versionCode 20401
versionName "2.4.1" versionName '2.4.1'
} }
// release with: STORE_PW=$(mpw masterpassword.keystore) KEY_PW=$(mpw masterpassword-android) gradle assembleRelease // release with: STORE_PW=$(mpw masterpassword.keystore) KEY_PW=$(mpw masterpassword-android) gradle assembleRelease
signingConfigs { signingConfigs {
release { release {
storeFile file('masterpassword.keystore') storeFile file( 'masterpassword.keystore' )
storePassword System.getenv('STORE_PW') storePassword System.getenv( 'STORE_PW' )
keyAlias 'masterpassword-android' keyAlias 'masterpassword-android'
keyPassword System.getenv('KEY_PW') keyPassword System.getenv( 'KEY_PW' )
} }
} }
buildTypes { buildTypes {
release { release {
if (System.getenv('STORE_PW') != null) if (System.getenv( 'STORE_PW' ) != null)
signingConfig signingConfigs.release signingConfig signingConfigs.release
} }
} }
@ -37,9 +39,8 @@ dependencies {
compile project( ':masterpassword:algorithm' ) compile project( ':masterpassword:algorithm' )
compile project( ':masterpassword:tests' ) compile project( ':masterpassword:tests' )
// Android dependencies compile group: 'org.slf4j', name: 'slf4j-android', version:'1.7.13-underscore'
compile 'org.slf4j:slf4j-android:1.7.13-underscore' compile group: 'com.jakewharton', name: 'butterknife', version:'8.5.1'
compile 'com.jakewharton:butterknife:8.5.1' annotationProcessor group: 'com.jakewharton', name: 'butterknife-compiler', version:'8.5.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1' compile files( 'libs/scrypt-1.4.0-native.jar' )
compile files('libs/scrypt-1.4.0-native.jar')
} }

View File

@ -22,6 +22,9 @@ import com.google.common.io.*;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.TypeUtils; import com.lyndir.lhunath.opal.system.util.TypeUtils;
import com.lyndir.masterpassword.gui.model.User;
import com.lyndir.masterpassword.gui.view.PasswordFrame;
import com.lyndir.masterpassword.gui.view.UnlockFrame;
import java.io.*; import java.io.*;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;

View File

@ -30,12 +30,16 @@ import javax.swing.*;
*/ */
public abstract class Res { public abstract class Res {
private static final WeakHashMap<Window, ExecutorService> executorByWindow = new WeakHashMap<>(); private static final WeakHashMap<Window, ScheduledExecutorService> executorByWindow = new WeakHashMap<>();
private static final Logger logger = Logger.get( Res.class ); private static final Logger logger = Logger.get( Res.class );
private static final Colors colors = new Colors(); private static final Colors colors = new Colors();
public static Future<?> execute(final Window host, final Runnable job) { public static Future<?> execute(final Window host, final Runnable job) {
return getExecutor( host ).submit( new Runnable() { return schedule( host, job, 0, TimeUnit.MILLISECONDS );
}
public static Future<?> schedule(final Window host, final Runnable job, final long delay, final TimeUnit timeUnit) {
return getExecutor( host ).schedule( new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
@ -45,12 +49,16 @@ public abstract class Res {
logger.err( t, "Unexpected: %s", t.getLocalizedMessage() ); logger.err( t, "Unexpected: %s", t.getLocalizedMessage() );
} }
} }
} ); }, delay, timeUnit );
} }
public static <V> ListenableFuture<V> execute(final Window host, final Callable<V> job) { public static <V> ListenableFuture<V> execute(final Window host, final Callable<V> job) {
ExecutorService executor = getExecutor( host ); return schedule( host, job, 0, TimeUnit.MILLISECONDS );
return JdkFutureAdapters.listenInPoolThread( executor.submit( new Callable<V>() { }
public static <V> ListenableFuture<V> schedule(final Window host, final Callable<V> job, final long delay, final TimeUnit timeUnit) {
ScheduledExecutorService executor = getExecutor( host );
return JdkFutureAdapters.listenInPoolThread( executor.schedule( new Callable<V>() {
@Override @Override
public V call() public V call()
throws Exception { throws Exception {
@ -62,14 +70,14 @@ public abstract class Res {
throw t; throw t;
} }
} }
} ), executor ); }, delay, timeUnit ), executor );
} }
private static ExecutorService getExecutor(final Window host) { private static ScheduledExecutorService getExecutor(final Window host) {
ExecutorService executor = executorByWindow.get( host ); ScheduledExecutorService executor = executorByWindow.get( host );
if (executor == null) { if (executor == null) {
executorByWindow.put( host, executor = Executors.newSingleThreadExecutor() ); executorByWindow.put( host, executor = Executors.newSingleThreadScheduledExecutor() );
host.addWindowListener( new WindowAdapter() { host.addWindowListener( new WindowAdapter() {
@Override @Override

View File

@ -1,4 +1,4 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.model;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.MPSiteType; import com.lyndir.masterpassword.MPSiteType;

View File

@ -1,4 +1,4 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.model;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.lyndir.masterpassword.model.IncorrectMasterPasswordException; import com.lyndir.masterpassword.model.IncorrectMasterPasswordException;

View File

@ -1,4 +1,4 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.model;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.MPSiteType; import com.lyndir.masterpassword.MPSiteType;

View File

@ -1,8 +1,9 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.model;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.FluentIterable; import com.google.common.collect.FluentIterable;
import com.lyndir.masterpassword.gui.*;
import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.*;
import java.util.Arrays; import java.util.Arrays;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -43,7 +44,7 @@ public class ModelUser extends User {
} }
public void setAvatar(final int avatar) { public void setAvatar(final int avatar) {
model.setAvatar(avatar % Res.avatars()); model.setAvatar( avatar % Res.avatars());
MPUserFileManager.get().save(); MPUserFileManager.get().save();
} }

View File

@ -1,4 +1,4 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.model;
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf; import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;

View File

@ -1,4 +1,4 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.model;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;

View File

@ -2,8 +2,8 @@ package com.lyndir.masterpassword.gui.platform.mac;
import com.apple.eawt.*; import com.apple.eawt.*;
import com.lyndir.masterpassword.gui.GUI; import com.lyndir.masterpassword.gui.GUI;
import com.lyndir.masterpassword.gui.PasswordFrame; import com.lyndir.masterpassword.gui.view.PasswordFrame;
import com.lyndir.masterpassword.gui.User; import com.lyndir.masterpassword.gui.model.User;
import javax.swing.*; import javax.swing.*;

View File

@ -1,8 +1,10 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.view;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.lyndir.masterpassword.MPIdenticon; import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.model.User;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.gui.view.UnlockFrame;
import java.awt.*; import java.awt.*;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;

View File

@ -1,5 +1,8 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.view;
import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.model.IncognitoUser;
import com.lyndir.masterpassword.gui.model.User;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Components;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;

View File

@ -1,4 +1,4 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.view;
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf; import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
@ -6,6 +6,8 @@ import com.google.common.base.Function;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.*; import com.google.common.collect.*;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.model.ModelUser;
import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.MPUserFileManager; import com.lyndir.masterpassword.model.MPUserFileManager;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Components;

View File

@ -1,4 +1,4 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.view;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.ifNotNullElse; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.ifNotNullElse;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
@ -9,6 +9,8 @@ import com.google.common.collect.Iterables;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.google.common.util.concurrent.*; import com.google.common.util.concurrent.*;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.model.*;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.gui.util.UnsignedIntegerModel; import com.lyndir.masterpassword.gui.util.UnsignedIntegerModel;
import java.awt.*; import java.awt.*;

View File

@ -1,12 +1,16 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.view;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import com.lyndir.masterpassword.MPIdenticon; import com.lyndir.masterpassword.MPIdenticon;
import com.lyndir.masterpassword.gui.*;
import com.lyndir.masterpassword.gui.model.User;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.model.IncorrectMasterPasswordException; import com.lyndir.masterpassword.model.IncorrectMasterPasswordException;
import java.awt.*; import java.awt.*;
import java.awt.event.*; import java.awt.event.*;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
@ -22,6 +26,7 @@ public class UnlockFrame extends JFrame {
private final JButton signInButton; private final JButton signInButton;
private final JPanel authenticationContainer; private final JPanel authenticationContainer;
private AuthenticationPanel authenticationPanel; private AuthenticationPanel authenticationPanel;
private Future<?> identiconFuture;
private boolean incognito; private boolean incognito;
public User user; public User user;
@ -139,18 +144,34 @@ public class UnlockFrame extends JFrame {
} }
boolean checkSignIn() { boolean checkSignIn() {
if (identiconFuture != null)
identiconFuture.cancel( false );
identiconFuture = Res.schedule( this, new Runnable() {
@Override
public void run() {
SwingUtilities.invokeLater( new Runnable() {
@Override
public void run() {
String fullName = user == null? "": user.getFullName();
char[] masterPassword = authenticationPanel.getMasterPassword();
if (fullName.isEmpty() || masterPassword.length == 0) {
identiconLabel.setText( " " );
return;
}
MPIdenticon identicon = new MPIdenticon( fullName, masterPassword );
identiconLabel.setText( identicon.getText() );
identiconLabel.setForeground(
Res.colors().fromIdenticonColor( identicon.getColor(), Res.Colors.BackgroundMode.DARK ) );
}
} );
}
}, 300, TimeUnit.MILLISECONDS );
String fullName = user == null? "": user.getFullName(); String fullName = user == null? "": user.getFullName();
char[] masterPassword = authenticationPanel.getMasterPassword(); char[] masterPassword = authenticationPanel.getMasterPassword();
boolean enabled = !fullName.isEmpty() && masterPassword.length > 0; boolean enabled = !fullName.isEmpty() && masterPassword.length > 0;
if (fullName.isEmpty() || masterPassword.length == 0)
identiconLabel.setText( " " );
else {
MPIdenticon identicon = new MPIdenticon( fullName, masterPassword );
identiconLabel.setText( identicon.getText() );
identiconLabel.setForeground( Res.colors().fromIdenticonColor( identicon.getColor(), Res.Colors.BackgroundMode.DARK ) );
}
signInButton.setEnabled( enabled ); signInButton.setEnabled( enabled );
return enabled; return enabled;
@ -197,7 +218,7 @@ public class UnlockFrame extends JFrame {
} ); } );
} }
interface SignInCallback { public interface SignInCallback {
void signedIn(User user); void signedIn(User user);
} }