From ea47420e992e14ced73b69e49c9c3b23b7550a0f Mon Sep 17 00:00:00 2001
From: akwizgran <akwizgran@users.sourceforge.net>
Date: Thu, 9 Jan 2014 01:29:00 +0000
Subject: [PATCH] Add a password strength meter to SetupActivity. Dev task #42.

---
 briar-android/res/values/strings.xml          |  6 ++-
 .../briarproject/android/SetupActivity.java   | 41 ++++++++++-----
 .../android/util/StrengthMeter.java           | 52 +++++++++++++++++++
 .../api/crypto/PasswordStrengthEstimator.java | 16 ++++++
 .../org/briarproject/crypto/CryptoModule.java |  3 ++
 .../crypto/PasswordStrengthEstimatorImpl.java | 34 ++++++++++++
 briar-tests/build.xml                         |  1 +
 .../crypto/PasswordStrengthEstimatorTest.java | 29 +++++++++++
 8 files changed, 168 insertions(+), 14 deletions(-)
 create mode 100644 briar-android/src/org/briarproject/android/util/StrengthMeter.java
 create mode 100644 briar-api/src/org/briarproject/api/crypto/PasswordStrengthEstimator.java
 create mode 100644 briar-core/src/org/briarproject/crypto/PasswordStrengthEstimatorImpl.java
 create mode 100644 briar-tests/src/org/briarproject/crypto/PasswordStrengthEstimatorTest.java

diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index ce19379981..a74ff9c721 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -7,7 +7,11 @@
 	<string name="choose_nickname">Choose your nickname:</string>
 	<string name="choose_password">Choose your password:</string>
 	<string name="confirm_password">Confirm your password:</string>
-	<string name="format_min_password">Password must be at least %1$d characters long</string>
+	<string name="password_too_weak">Password is too weak</string>
+	<string name="password_weak">Password is weak</string>
+	<string name="password_quite_weak">Password is quite weak</string>
+	<string name="password_quite_strong">Password is quite strong</string>
+	<string name="password_strong">Password is strong</string>
 	<string name="passwords_do_not_match">Passwords do not match</string>
 	<string name="enter_password">Enter your password:</string>
 	<string name="try_again">Wrong password, try again:</string>
diff --git a/briar-android/src/org/briarproject/android/SetupActivity.java b/briar-android/src/org/briarproject/android/SetupActivity.java
index f5b3c227f2..a9e5681904 100644
--- a/briar-android/src/org/briarproject/android/SetupActivity.java
+++ b/briar-android/src/org/briarproject/android/SetupActivity.java
@@ -11,6 +11,7 @@ import static android.view.View.VISIBLE;
 import static android.widget.LinearLayout.VERTICAL;
 import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
 import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.WEAK;
 
 import java.util.Arrays;
 import java.util.concurrent.Executor;
@@ -18,12 +19,14 @@ import java.util.concurrent.Executor;
 import javax.inject.Inject;
 
 import org.briarproject.R;
+import org.briarproject.android.util.StrengthMeter;
 import org.briarproject.api.AuthorFactory;
 import org.briarproject.api.LocalAuthor;
 import org.briarproject.api.android.ReferenceManager;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.CryptoExecutor;
 import org.briarproject.api.crypto.KeyPair;
+import org.briarproject.api.crypto.PasswordStrengthEstimator;
 import org.briarproject.api.db.DatabaseConfig;
 import org.briarproject.util.StringUtils;
 
@@ -31,7 +34,6 @@ import roboguice.activity.RoboActivity;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
-import android.content.res.Resources;
 import android.os.Bundle;
 import android.text.Editable;
 import android.view.View;
@@ -45,11 +47,11 @@ import android.widget.TextView;
 
 public class SetupActivity extends RoboActivity implements OnClickListener {
 
-	private static final int MIN_PASSWORD_LENGTH = 8;
-
 	@Inject @CryptoExecutor private Executor cryptoExecutor;
+	@Inject private PasswordStrengthEstimator strengthEstimator;
 	private EditText nicknameEntry = null;
 	private EditText passwordEntry = null, passwordConfirmation = null;
+	private StrengthMeter strengthMeter = null;
 	private TextView feedback = null;
 	private Button continueButton = null;
 	private ProgressBar progress = null;
@@ -128,12 +130,15 @@ public class SetupActivity extends RoboActivity implements OnClickListener {
 		passwordConfirmation.setInputType(inputType);
 		layout.addView(passwordConfirmation);
 
+		strengthMeter = new StrengthMeter(this);
+		strengthMeter.setPadding(30, 10, 30, 0);
+		layout.addView(strengthMeter);
+
 		feedback = new TextView(this);
 		feedback.setGravity(CENTER);
 		feedback.setTextSize(14);
 		feedback.setPadding(10, 10, 10, 10);
-		String format = getResources().getString(R.string.format_min_password);
-		feedback.setText(String.format(format, MIN_PASSWORD_LENGTH));
+		feedback.setText("");
 		layout.addView(feedback);
 
 		continueButton = new Button(this);
@@ -157,25 +162,35 @@ public class SetupActivity extends RoboActivity implements OnClickListener {
 	}
 
 	private void enableOrDisableContinueButton() {
-		if(nicknameEntry == null || passwordEntry == null ||
-				passwordConfirmation == null || continueButton == null) return;
+		if(continueButton == null) return; // Not created yet
 		boolean nicknameNotEmpty = nicknameEntry.getText().length() > 0;
 		char[] firstPassword = getChars(passwordEntry.getText());
 		char[] secondPassword = getChars(passwordConfirmation.getText());
-		boolean passwordLength = firstPassword.length >= MIN_PASSWORD_LENGTH;
 		boolean passwordsMatch = Arrays.equals(firstPassword, secondPassword);
+		float strength = strengthEstimator.estimateStrength(firstPassword);
 		for(int i = 0; i < firstPassword.length; i++) firstPassword[i] = 0;
 		for(int i = 0; i < secondPassword.length; i++) secondPassword[i] = 0;
-		if(!passwordLength) {
-			Resources res = getResources();
-			String format = res.getString(R.string.format_min_password);
-			feedback.setText(String.format(format, MIN_PASSWORD_LENGTH));
+		strengthMeter.setStrength(strength);
+		if(firstPassword.length == 0) {
+			feedback.setText("");
+		} else if(secondPassword.length == 0 || passwordsMatch) {
+			if(strength < PasswordStrengthEstimator.WEAK) {
+				feedback.setText(R.string.password_too_weak);
+			} else if(strength < PasswordStrengthEstimator.QUITE_WEAK) {
+				feedback.setText(R.string.password_weak);
+			} else if(strength < PasswordStrengthEstimator.QUITE_STRONG) {
+				feedback.setText(R.string.password_quite_weak);
+			} else if(strength < PasswordStrengthEstimator.STRONG) {
+				feedback.setText(R.string.password_quite_strong);
+			} else {
+				feedback.setText(R.string.password_strong);
+			}
 		} else if(!passwordsMatch) {
 			feedback.setText(R.string.passwords_do_not_match);
 		} else {
 			feedback.setText("");
 		}
-		boolean valid = nicknameNotEmpty && passwordLength && passwordsMatch;
+		boolean valid = nicknameNotEmpty && passwordsMatch && strength >= WEAK;
 		continueButton.setEnabled(valid);
 	}
 
diff --git a/briar-android/src/org/briarproject/android/util/StrengthMeter.java b/briar-android/src/org/briarproject/android/util/StrengthMeter.java
new file mode 100644
index 0000000000..6bc75fb078
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/util/StrengthMeter.java
@@ -0,0 +1,52 @@
+package org.briarproject.android.util;
+
+import static android.graphics.drawable.ClipDrawable.HORIZONTAL;
+import static android.view.Gravity.LEFT;
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.QUITE_STRONG;
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.STRONG;
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.WEAK;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ClipDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RectShape;
+import android.widget.ProgressBar;
+
+public class StrengthMeter extends ProgressBar {
+
+	private static final int MAX = 100;
+	private static final int RED = Color.rgb(255, 0, 0);
+	private static final int ORANGE = Color.rgb(255, 160, 0);
+	private static final int YELLOW = Color.rgb(250, 255, 15);
+	private static final int LIME = Color.rgb(190, 255, 0);
+	private static final int GREEN = Color.rgb(7, 255, 0);
+
+	private final ShapeDrawable bar;
+
+	public StrengthMeter(Context context) {
+		super(context, null, android.R.attr.progressBarStyleHorizontal);
+		bar = new ShapeDrawable(new RectShape());
+		bar.getPaint().setColor(Color.RED);
+		ClipDrawable progress = new ClipDrawable(bar, LEFT, HORIZONTAL);
+		setProgressDrawable(progress);
+		setIndeterminate(false);
+	}
+
+	@Override
+	public int getMax() {
+		return MAX;
+	}
+
+	public void setStrength(float strength) {
+		if(strength < 0 || strength > 1) throw new IllegalArgumentException();
+		int colour;
+		if(strength < WEAK) colour = RED;
+		else if(strength < QUITE_WEAK) colour = ORANGE;
+		else if(strength < QUITE_STRONG) colour = YELLOW;
+		else if(strength < STRONG) colour = LIME;
+		else colour = GREEN;
+		bar.getPaint().setColor(colour);
+		setProgress((int) (strength * MAX));
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/crypto/PasswordStrengthEstimator.java b/briar-api/src/org/briarproject/api/crypto/PasswordStrengthEstimator.java
new file mode 100644
index 0000000000..9ffa2dc244
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/crypto/PasswordStrengthEstimator.java
@@ -0,0 +1,16 @@
+package org.briarproject.api.crypto;
+
+public interface PasswordStrengthEstimator {
+
+	float NONE = 0;
+	float WEAK = 0.4f;
+	float QUITE_WEAK = 0.6f;
+	float QUITE_STRONG = 0.8f;
+	float STRONG = 1;
+
+	/**
+	 * Returns an estimate between 0 (weakest) and 1 (strongest), inclusive,
+	 * of the strength of the given password.
+	 */
+	float estimateStrength(char[] password);
+}
diff --git a/briar-core/src/org/briarproject/crypto/CryptoModule.java b/briar-core/src/org/briarproject/crypto/CryptoModule.java
index 39a18c6ac0..f0eb56f05d 100644
--- a/briar-core/src/org/briarproject/crypto/CryptoModule.java
+++ b/briar-core/src/org/briarproject/crypto/CryptoModule.java
@@ -13,6 +13,7 @@ import javax.inject.Singleton;
 
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.CryptoExecutor;
+import org.briarproject.api.crypto.PasswordStrengthEstimator;
 import org.briarproject.api.lifecycle.LifecycleManager;
 
 import com.google.inject.AbstractModule;
@@ -40,6 +41,8 @@ public class CryptoModule extends AbstractModule {
 	protected void configure() {
 		bind(CryptoComponent.class).to(
 				CryptoComponentImpl.class).in(Singleton.class);
+		bind(PasswordStrengthEstimator.class).to(
+				PasswordStrengthEstimatorImpl.class);
 	}
 
 	@Provides @Singleton @CryptoExecutor
diff --git a/briar-core/src/org/briarproject/crypto/PasswordStrengthEstimatorImpl.java b/briar-core/src/org/briarproject/crypto/PasswordStrengthEstimatorImpl.java
new file mode 100644
index 0000000000..aeec2ddc3b
--- /dev/null
+++ b/briar-core/src/org/briarproject/crypto/PasswordStrengthEstimatorImpl.java
@@ -0,0 +1,34 @@
+package org.briarproject.crypto;
+
+import java.util.HashSet;
+
+import org.briarproject.api.crypto.PasswordStrengthEstimator;
+
+class PasswordStrengthEstimatorImpl implements PasswordStrengthEstimator {
+
+	private static final int LOWER = 26;
+	private static final int UPPER = 26;
+	private static final int DIGIT = 10;
+	private static final int OTHER = 10;
+	private static final double STRONG = Math.log(Math.pow(LOWER + UPPER +
+			DIGIT + OTHER, 10));
+
+	public float estimateStrength(char[] password) {
+		HashSet<Character> unique = new HashSet<Character>();
+		for(char c : password) unique.add(c);
+		boolean lower = false, upper = false, digit = false, other = false;
+		for(char c : unique) {
+			if(Character.isLowerCase(c)) lower = true;
+			else if(Character.isUpperCase(c)) upper = true;
+			else if(Character.isDigit(c)) digit = true;
+			else other = true;
+		}
+		int alphabetSize = 0;
+		if(lower) alphabetSize += LOWER;
+		if(upper) alphabetSize += UPPER;
+		if(digit) alphabetSize += DIGIT;
+		if(other) alphabetSize += OTHER;
+		double score = Math.log(Math.pow(alphabetSize, unique.size()));
+		return Math.min(1, (float) (score / STRONG));
+	}
+}
diff --git a/briar-tests/build.xml b/briar-tests/build.xml
index 33043bff61..d6fb7a8745 100644
--- a/briar-tests/build.xml
+++ b/briar-tests/build.xml
@@ -97,6 +97,7 @@
 			<test name='org.briarproject.crypto.KeyDerivationTest'/>
 			<test name='org.briarproject.crypto.KeyEncodingAndParsingTest'/>
 			<test name="org.briarproject.crypto.PasswordBasedKdfTest"/>
+			<test name="org.briarproject.crypto.PasswordStrengthEstimatorTest"/>
 			<test name='org.briarproject.crypto.SecretKeyImplTest'/>
 			<test name='org.briarproject.db.BasicH2Test'/>
 			<test name='org.briarproject.db.DatabaseCleanerImplTest'/>
diff --git a/briar-tests/src/org/briarproject/crypto/PasswordStrengthEstimatorTest.java b/briar-tests/src/org/briarproject/crypto/PasswordStrengthEstimatorTest.java
new file mode 100644
index 0000000000..9f703b378f
--- /dev/null
+++ b/briar-tests/src/org/briarproject/crypto/PasswordStrengthEstimatorTest.java
@@ -0,0 +1,29 @@
+package org.briarproject.crypto;
+
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.QUITE_STRONG;
+
+import org.briarproject.BriarTestCase;
+import org.briarproject.api.crypto.PasswordStrengthEstimator;
+import org.junit.Test;
+
+public class PasswordStrengthEstimatorTest extends BriarTestCase {
+
+	@Test
+	public void testWeakPasswords() {
+		PasswordStrengthEstimator e = new PasswordStrengthEstimatorImpl();
+		assertTrue(e.estimateStrength("".toCharArray()) < QUITE_STRONG);
+		assertTrue(e.estimateStrength("password".toCharArray()) < QUITE_STRONG);
+		assertTrue(e.estimateStrength("letmein".toCharArray()) < QUITE_STRONG);
+		assertTrue(e.estimateStrength("123456".toCharArray()) < QUITE_STRONG);
+	}
+
+	@Test
+	public void testStrongPasswords() {
+		PasswordStrengthEstimator e = new PasswordStrengthEstimatorImpl();
+		// Industry standard
+		assertTrue(e.estimateStrength("Tr0ub4dor&3".toCharArray())
+				> QUITE_STRONG);
+		assertTrue(e.estimateStrength("correcthorsebatterystaple".toCharArray())
+				> QUITE_STRONG);
+	}
+}
-- 
GitLab