Skip to content
This repository has been archived by the owner on Feb 24, 2018. It is now read-only.

Commit

Permalink
Merge branch 'feature/password-strength' into develop
Browse files Browse the repository at this point in the history
Adds password strength meter. The view is fine,
however the algorithm to compute strength based
on NIST isn't very good. However works fine
for now.
  • Loading branch information
vishesh committed Mar 1, 2015
2 parents 219558d + 3a34869 commit 2d175b8
Show file tree
Hide file tree
Showing 13 changed files with 10,484 additions and 13 deletions.
10,000 changes: 10,000 additions & 0 deletions Sealnote/src/main/assets/xato_passlist.txt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import android.widget.*;
import com.twistedplane.sealnote.data.DatabaseHandler;
import com.twistedplane.sealnote.utils.TimeoutHandler;
import com.twistedplane.sealnote.view.PasswordInput;
import net.sqlcipher.database.SQLiteException;

import java.io.File;
Expand All @@ -21,7 +22,7 @@
*/
public class PasswordActivity extends Activity {
public final static String TAG = "PasswordActivity";
private EditText mPasswordInput;
private PasswordInput mPasswordInput;

@Override
public void onCreate(Bundle savedInstanceState) {
Expand All @@ -30,7 +31,7 @@ public void onCreate(Bundle savedInstanceState) {

TimeoutHandler.instance().init();

mPasswordInput = (EditText) findViewById(R.id.password_input);
mPasswordInput = (PasswordInput) findViewById(R.id.password_input);

if (checkExistingDatabase()) {
createLoginScreen();
Expand Down Expand Up @@ -87,10 +88,12 @@ private void createLoginScreen() {
@Override
public void onClick(View view) {
toggleProgress();
new LoginTask().execute(mPasswordInput.getText().toString());
new LoginTask().execute(mPasswordInput.getText());
}
});

mPasswordInput.setMeterEnabled(false);

mPasswordInput.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package com.twistedplane.sealnote.crypto;

import android.util.Log;

import java.io.*;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Quick and dirty class to measure password quality
*/
public class PasswordQuality {
public final static String TAG = "PasswordQuality";
private Set<String> mDictionary;
final private Pattern mDictPattern = Pattern.compile("\\d+|[A-Za-z]+|[^\\d^A-Z^a-z]+");

private class CharsetCount {
int upper = 0;
int lower = 0;
int digits = 0;
int symbol = 0;
}

private enum DictionaryAttackResultType {
DICT_EXACT_MATCH,
DICT_SUBSET_MATCH,
DICT_NO_MATCH
}

private class DictionaryAttackResult {
DictionaryAttackResultType type;
CharSequence matched;

DictionaryAttackResult(DictionaryAttackResultType type, CharSequence matched) {
this.type = type;
this.matched = matched;
}
}

public PasswordQuality() {
mDictionary = new HashSet<String>();
}

public void initDictionary(InputStream dict) {
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(dict));

String line;
while ((line = bufferedReader.readLine()) != null) {
mDictionary.add(line);
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Dictionary not found in asset directory!");
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

public int score(CharSequence password) {
if (password.length() == 0) {
return 0;
}

double entropy = nistEntropy(password);

if (entropy >= 40) {
return 4;
} else if (entropy > 30) {
return 3;
} else if (entropy > 25) {
return 2;
}

return 1;
}

/**
* Calculate entropy roughly based on NIST standard
*/
private double nistEntropy(CharSequence password) {
double score = 0;
int length = password.length();

// First character has 4 bit entropy
if (length > 0) {
score += 4;
}

// Next seven characters has 2 bit entropy/char
if (length > 1) {
score += Math.min(length - 1, 7) * 2;
}

// Characters from index 9 to 20 have entropy 1.5bit/char
if (length > 8) {
score += Math.min(length - 8, 12) * 1.5;
}

// Character from index 21 and above have entropy 1bit/char
if (length > 20) {
score += length - 20;
}

final CharsetCount counts = computeCharsetCount(password);
if (counts.upper > 0 && counts.lower > 0 && (counts.digits > 0 || counts.symbol > 0)) {
score += 6;
}

if (length < 20) {
DictionaryAttackResult result = dictionaryAttack(password);
switch (result.type) {
case DICT_EXACT_MATCH:
score = 0;
break;
case DICT_SUBSET_MATCH:
// Penalize for match
score -= Math.log(result.matched.length())/Math.log(2);
break;
case DICT_NO_MATCH:
score += 6;
break;
}
}

return score;
}

private CharsetCount computeCharsetCount(CharSequence password) {
CharsetCount result = new CharsetCount();

for (int i = 0; i < password.length(); i++) {
char ch = password.charAt(i);
if (Character.isUpperCase(ch)) {
++result.upper;
} else if (Character.isLowerCase(ch)) {
++result.lower;
} else if (Character.isDigit(ch)) {
++result.digits;
} else if (Character.isDigit(ch)) {
++result.digits;
} else {
++result.symbol;
}
}

return result;
}

private DictionaryAttackResult dictionaryAttack(CharSequence _password) {
String password = _password.toString().toLowerCase();

if (mDictionary.contains(password)) {
return new DictionaryAttackResult(DictionaryAttackResultType.DICT_EXACT_MATCH, password);
}

Matcher matcher = mDictPattern.matcher(password);
while (matcher.find()) {
String group = matcher.group();
if (mDictionary.contains(group)) {
return new DictionaryAttackResult(DictionaryAttackResultType.DICT_SUBSET_MATCH, group);
}
}

return new DictionaryAttackResult(DictionaryAttackResultType.DICT_NO_MATCH, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.twistedplane.sealnote.R;
import com.twistedplane.sealnote.SealnoteApplication;
import com.twistedplane.sealnote.data.DatabaseHandler;
import com.twistedplane.sealnote.view.PasswordInput;
import net.sqlcipher.database.SQLiteException;

/**
Expand All @@ -29,7 +30,7 @@ public class PasswordPreference extends DialogPreference implements TextWatcher
public final static String TAG = "PasswordPreference";

private EditText mOldView;
private EditText mNewView;
private PasswordInput mNewView;
private EditText mNewConfirmView;
private Button mChangeButton;
private Button mCancelButton;
Expand Down Expand Up @@ -66,11 +67,14 @@ protected void onBindDialogView(@NonNull View view) {
super.onBindDialogView(view);

mOldView = (EditText) view.findViewById(R.id.diag_password_pref_oldPassword);
mNewView = (EditText) view.findViewById(R.id.diag_password_pref_newPassword);
mNewView = (PasswordInput) view.findViewById(R.id.diag_password_pref_newPassword);
mNewConfirmView = (EditText) view.findViewById(R.id.diag_password_pref_newPasswordConfirm);
mChangeButton = (Button) view.findViewById(R.id.button_positive);
mCancelButton = (Button) view.findViewById(R.id.button_negative);

//FIXME: Set in XML
mNewView.setHint(getContext().getResources().getString(R.string.new_password));

// initially everything is empty hence the button is disabled
mChangeButton.setEnabled(false);

Expand Down Expand Up @@ -129,7 +133,7 @@ public void afterTextChanged(Editable editable) {
*/
private void updateChangeButtonState() {
String oldPassword = mOldView.getText().toString();
String newPassword = mNewView.getText().toString();
String newPassword = mNewView.getText();
String newConfirmPassword = mNewConfirmView.getText().toString();

// any of three EditText is empty, we won't accept
Expand Down Expand Up @@ -208,7 +212,7 @@ protected void onPreExecute() {
DatabaseHandler db = SealnoteApplication.getDatabase();
oldDbPassword = db.getPassword();
oldPassword = mOldView.getText().toString();
newPassword = mNewView.getText().toString();
newPassword = mNewView.getText();
}

@Override
Expand Down
Loading

0 comments on commit 2d175b8

Please sign in to comment.