2
0

First working Master Password for Android, yay!

This commit is contained in:
Maarten Billemont 2014-08-26 19:54:33 -04:00
parent 89145d6e13
commit 65cef6d8ed
15 changed files with 400 additions and 8 deletions

View File

@ -1,5 +1,6 @@
package com.lyndir.masterpassword; package com.lyndir.masterpassword;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import java.util.Set; import java.util.Set;
@ -93,14 +94,12 @@ public enum MPElementType {
* *
* @return All types that support the given class. * @return All types that support the given class.
*/ */
public static ImmutableSet<MPElementType> forClass(final MPElementTypeClass typeClass) { public static ImmutableList<MPElementType> forClass(final MPElementTypeClass typeClass) {
ImmutableSet.Builder<MPElementType> types = ImmutableSet.builder(); ImmutableList.Builder<MPElementType> types = ImmutableList.builder();
for (final MPElementType type : values()) { for (final MPElementType type : values())
if (type.getTypeClass() == typeClass) { if (type.getTypeClass() == typeClass)
types.add( type ); types.add( type );
}
}
return types.build(); return types.build();
} }

View File

@ -5,14 +5,14 @@
android:versionName="GIT-SNAPSHOT"> android:versionName="GIT-SNAPSHOT">
<uses-sdk <uses-sdk
android:minSdkVersion="11" android:minSdkVersion="14"
android:targetSdkVersion="19" /> android:targetSdkVersion="19" />
<application <application
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=".UsersActivity"> <activity android:name=".EmergencyActivity">
<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" />

View File

@ -46,6 +46,12 @@
<artifactId>butterknife</artifactId> <artifactId>butterknife</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-android</artifactId>
<version>1.7.7</version>
</dependency>
<!-- clone https://github.com/mosabua/maven-android-sdk-deployer.git <!-- clone https://github.com/mosabua/maven-android-sdk-deployer.git
run mvn install -P 4.4 --> run mvn install -P 4.4 -->
<dependency> <dependency>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,88 @@
<?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:orientation="vertical"
android:gravity="center">
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ProgressBar
android:id="@+id/progressView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<EditText
android:id="@+id/userNameField"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="text|textCapWords|textPersonName"
android:hint="@string/userName.hint"
android:gravity="center"
android:textColor="#FFFFFF"
android:textSize="26sp" />
<EditText
android:id="@+id/masterPasswordField"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="text|textPassword"
android:hint="@string/masterPassword.hint"
android:password="true"
android:gravity="center"
android:textColor="#FFFFFF"
android:textSize="18sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/double_"
android:contentDescription="@string/empty" />
<EditText
android:id="@+id/siteNameField"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="text|textNoSuggestions|textUri"
android:hint="@string/siteName.hint"
android:gravity="center"
android:textColor="#FFFFFF"
android:textSize="26sp" />
<TextView
android:id="@+id/sitePasswordField"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="#FFFFFF"
android:textSize="32sp"
android:text="LuxdZozvDuma4[" />
<Spinner
android:id="@+id/typeField"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<NumberPicker
android:id="@+id/counterField"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

View File

@ -2,4 +2,8 @@
<resources> <resources>
<string name="app_name">Master Password</string> <string name="app_name">Master Password</string>
<string name="avatar">User Avatar</string> <string name="avatar">User Avatar</string>
<string name="siteName.hint">Site Name</string>
<string name="userName.hint">Your Name</string>
<string name="masterPassword.hint">Your Master Password</string>
<string name="empty" />
</resources> </resources>

View File

@ -0,0 +1,255 @@
package com.lyndir.masterpassword;
import android.app.Activity;
import android.content.SharedPreferences;
import android.graphics.Paint;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.*;
import butterknife.ButterKnife;
import butterknife.InjectView;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.*;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.util.concurrent.*;
import java.util.prefs.Preferences;
public class EmergencyActivity extends Activity {
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( EmergencyActivity.class );
private final ListeningExecutorService executor = MoreExecutors.listeningDecorator( Executors.newSingleThreadExecutor() );
private final ValueChangedListener updateMasterKey = new ValueChangedListener() {
@Override
void update() {
updateMasterKey();
}
};
private final ValueChangedListener updateSitePassword = new ValueChangedListener() {
@Override
void update() {
updateSitePassword();
}
};
private ListenableFuture<byte[]> masterKeyFuture;
@InjectView(R.id.progressView)
ProgressBar progressView;
@InjectView(R.id.userNameField)
EditText userNameField;
@InjectView(R.id.masterPasswordField)
EditText masterPasswordField;
@InjectView(R.id.siteNameField)
EditText siteNameField;
@InjectView(R.id.typeField)
Spinner typeField;
@InjectView(R.id.counterField)
NumberPicker counterField;
@InjectView(R.id.sitePasswordField)
TextView sitePasswordField;
private int hc_userName;
private int hc_masterPassword;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate( savedInstanceState );
Res.init( getResources() );
setContentView( R.layout.activity_emergency );
ButterKnife.inject( this );
userNameField.setOnFocusChangeListener( updateMasterKey );
masterPasswordField.setOnFocusChangeListener( updateMasterKey );
siteNameField.addTextChangedListener( updateSitePassword );
typeField.setOnItemSelectedListener( updateSitePassword );
counterField.setOnValueChangedListener( updateSitePassword );
userNameField.setTypeface( Res.exo_Thin );
userNameField.setPaintFlags( userNameField.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG );
masterPasswordField.setTypeface( Res.sourceCodePro_ExtraLight );
masterPasswordField.setPaintFlags( userNameField.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG );
siteNameField.setTypeface( Res.exo_Regular );
siteNameField.setPaintFlags( userNameField.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG );
sitePasswordField.setTypeface( Res.sourceCodePro_Black );
sitePasswordField.setPaintFlags( userNameField.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG );
typeField.setAdapter( new ArrayAdapter<>( this, R.layout.type_item, MPElementType.forClass( MPElementTypeClass.Generated ) ) );
typeField.setSelection( MPElementType.GeneratedLong.ordinal() );
counterField.setMinValue( 1 );
counterField.setMaxValue( Integer.MAX_VALUE );
counterField.setWrapSelectorWheel( false );
}
@Override
protected void onResume() {
super.onResume();
userNameField.setText( getPreferences( MODE_PRIVATE ).getString( "userName", "" ) );
masterPasswordField.requestFocus();
}
@Override
protected void onPause() {
synchronized (this) {
hc_userName = hc_masterPassword = 0;
if (masterKeyFuture != null) {
masterKeyFuture.cancel( true );
masterKeyFuture = null;
}
}
sitePasswordField.setText( "" );
progressView.setVisibility( View.INVISIBLE );
super.onPause();
}
private synchronized void updateMasterKey() {
final String userName = userNameField.getText().toString();
final String masterPassword = masterPasswordField.getText().toString();
if (userName.hashCode() == hc_userName && masterPassword.hashCode() == hc_masterPassword)
return;
hc_userName = userName.hashCode();
hc_masterPassword = masterPassword.hashCode();
SharedPreferences.Editor pref = getPreferences( MODE_PRIVATE ).edit();
pref.putString( "userName", userName );
pref.commit();
if (masterKeyFuture != null)
masterKeyFuture.cancel( true );
if (userName.isEmpty() || masterPassword.isEmpty()) {
sitePasswordField.setText( "" );
progressView.setVisibility( View.INVISIBLE );
return;
}
progressView.setVisibility( View.VISIBLE );
(masterKeyFuture = executor.submit( new Callable<byte[]>() {
@Override
public byte[] call()
throws Exception {
try {
return MasterPassword.keyForPassword( masterPassword, userName );
}
catch (RuntimeException e) {
sitePasswordField.setText( "" );
progressView.setVisibility( View.INVISIBLE );
logger.err( e, "While generating master key." );
throw e;
}
}
} )).addListener( new Runnable() {
@Override
public void run() {
runOnUiThread( new Runnable() {
@Override
public void run() {
updateSitePassword();
}
} );
}
}, executor );
}
private void updateSitePassword() {
final String siteName = siteNameField.getText().toString();
final MPElementType type = (MPElementType) typeField.getSelectedItem();
final int counter = counterField.getValue();
if (masterKeyFuture == null || siteName.isEmpty() || type == null) {
sitePasswordField.setText( "" );
progressView.setVisibility( View.INVISIBLE );
return;
}
progressView.setVisibility( View.VISIBLE );
executor.submit( new Runnable() {
@Override
public void run() {
try {
final String sitePassword = MasterPassword.generateContent( type, siteName, masterKeyFuture.get(), counter );
runOnUiThread( new Runnable() {
@Override
public void run() {
sitePasswordField.setText( sitePassword );
progressView.setVisibility( View.INVISIBLE );
}
} );
}
catch (InterruptedException ignored) {
sitePasswordField.setText( "" );
progressView.setVisibility( View.INVISIBLE );
}
catch (ExecutionException e) {
sitePasswordField.setText( "" );
progressView.setVisibility( View.INVISIBLE );
logger.err( e, "While generating site password." );
throw Throwables.propagate( e );
}
catch (RuntimeException e) {
sitePasswordField.setText( "" );
progressView.setVisibility( View.INVISIBLE );
logger.err( e, "While generating site password." );
throw e;
}
}
} );
}
private abstract class ValueChangedListener
implements TextWatcher, NumberPicker.OnValueChangeListener, AdapterView.OnItemSelectedListener, View.OnFocusChangeListener {
abstract void update();
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
}
@Override
public void afterTextChanged(final Editable s) {
update();
}
@Override
public void onValueChange(final NumberPicker picker, final int oldVal, final int newVal) {
update();
}
@Override
public void onItemSelected(final AdapterView<?> parent, final View view, final int position, final long id) {
update();
}
@Override
public void onNothingSelected(final AdapterView<?> parent) {
update();
}
@Override
public void onFocusChange(final View v, final boolean hasFocus) {
if (!hasFocus)
update();
}
}
}

View File

@ -0,0 +1,35 @@
package com.lyndir.masterpassword;
import android.content.res.Resources;
import android.graphics.Paint;
import android.graphics.Typeface;
/**
* @author lhunath, 2014-08-25
*/
public class Res {
public static Typeface sourceCodePro_Black;
public static Typeface sourceCodePro_ExtraLight;
public static Typeface exo_Bold;
public static Typeface exo_ExtraBold;
public static Typeface exo_Regular;
public static Typeface exo_Thin;
private static boolean initialized;
public static void init(Resources resources) {
if (initialized)
return;
initialized = true;
sourceCodePro_Black = Typeface.createFromAsset( resources.getAssets(), "SourceCodePro-Black.otf" );
sourceCodePro_ExtraLight = Typeface.createFromAsset( resources.getAssets(), "SourceCodePro-ExtraLight.otf" );
exo_Bold = Typeface.createFromAsset( resources.getAssets(), "Exo2.0-Bold.otf" );
exo_ExtraBold = Typeface.createFromAsset( resources.getAssets(), "Exo2.0-ExtraBold.otf" );
exo_Regular = Typeface.createFromAsset( resources.getAssets(), "Exo2.0-Regular.otf" );
exo_Thin = Typeface.createFromAsset( resources.getAssets(), "Exo2.0-Thin.otf" );
}
}