From d2a252a06b589edee912e1193ab2804ff9238dd3 Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Fri, 10 Jul 2015 19:44:22 -0400 Subject: [PATCH 1/8] Import pwgen classes --- .../com/zeapo/pwdstore/pwgen/pw_phonemes.java | 206 ++++++++++++++++++ .../com/zeapo/pwdstore/pwgen/pw_rand.java | 69 ++++++ .../java/com/zeapo/pwdstore/pwgen/pwgen.java | 138 ++++++++++++ .../com/zeapo/pwdstore/pwgen/randnum.java | 26 +++ 4 files changed, 439 insertions(+) create mode 100644 app/src/main/java/com/zeapo/pwdstore/pwgen/pw_phonemes.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/pwgen/pw_rand.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/pwgen/pwgen.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/pwgen/randnum.java diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgen/pw_phonemes.java b/app/src/main/java/com/zeapo/pwdstore/pwgen/pw_phonemes.java new file mode 100644 index 00000000..1b312232 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/pwgen/pw_phonemes.java @@ -0,0 +1,206 @@ +package com.zeapo.pwdstore.pwgen; + +public class pw_phonemes { + private static final int CONSONANT = 0x0001; + private static final int VOWEL = 0x0002; + private static final int DIPTHONG = 0x0004; + private static final int NOT_FIRST = 0x0008; + + private static final element elements[] = { + new element("a", VOWEL), + new element("ae", VOWEL | DIPTHONG), + new element("ah", VOWEL | DIPTHONG), + new element("ai", VOWEL | DIPTHONG), + new element("b", CONSONANT), + new element("c", CONSONANT), + new element("ch", CONSONANT | DIPTHONG), + new element("d", CONSONANT), + new element("e", VOWEL), + new element("ee", VOWEL | DIPTHONG), + new element("ei", VOWEL | DIPTHONG), + new element("f", CONSONANT), + new element("g", CONSONANT), + new element("gh", CONSONANT | DIPTHONG | NOT_FIRST), + new element("h", CONSONANT), + new element("i", VOWEL), + new element("ie", VOWEL | DIPTHONG), + new element("j", CONSONANT), + new element("k", CONSONANT), + new element("l", CONSONANT), + new element("m", CONSONANT), + new element("n", CONSONANT), + new element("ng", CONSONANT | DIPTHONG | NOT_FIRST), + new element("o", VOWEL), + new element("oh", VOWEL | DIPTHONG), + new element("oo", VOWEL | DIPTHONG), + new element("p", CONSONANT), + new element("ph", CONSONANT | DIPTHONG), + new element("qu", CONSONANT | DIPTHONG), + new element("r", CONSONANT), + new element("s", CONSONANT), + new element("sh", CONSONANT | DIPTHONG), + new element("t", CONSONANT), + new element("th", CONSONANT | DIPTHONG), + new element("u", VOWEL), + new element("v", CONSONANT), + new element("w", CONSONANT), + new element("x", CONSONANT), + new element("y", CONSONANT), + new element("z", CONSONANT) + }; + + private static class element { + String str; + int flags; + element(String str, int flags) { + this.str = str; + this.flags = flags; + } + } + + private static final int NUM_ELEMENTS = elements.length; + + /** + * Generates a human-readable password. + * + * @param size length of password to generate + * @param pwFlags flag field where set bits indicate conditions the + * generated password must meet + * + * + * + * + * + * + *
BitCondition
0include at least one number
1include at least one uppercase letter
2include at least one symbol
3don't include ambiguous characters
+ * @return the generated password + */ + public static String phonemes(int size, int pwFlags) { + String password; + int curSize, i, length, flags, featureFlags, prev, shouldBe; + boolean first; + String str; + char cha; + + do { + password = ""; + featureFlags = pwFlags; + curSize = 0; + prev = 0; + first = true; + + shouldBe = randnum.number(2) == 1 ? VOWEL : CONSONANT; + + while (curSize < size) { + i = randnum.number(NUM_ELEMENTS); + str = elements[i].str; + length = str.length(); + flags = elements[i].flags; + // Filter on the basic type of the next element + if ((flags & shouldBe) == 0) { + continue; + } + // Handle the NOT_FIRST flag + if (first && (flags & NOT_FIRST) > 0) { + continue; + } + // Don't allow VOWEL followed a Vowel/Dipthong pair + if ((prev & VOWEL) > 0 && (flags & VOWEL) > 0 + && (flags & DIPTHONG) > 0) { + continue; + } + // Don't allow us to overflow the buffer + if (length > size - curSize) { + continue; + } + // OK, we found an element which matches our criteria, let's do + // it + password += str; + + // Handle UPPERS + if ((pwFlags & pwgen.UPPERS) > 0) { + if ((first || (flags & CONSONANT) > 0) + && (randnum.number(10) < 2)) { + int index = password.length() - length; + password = password.substring(0, index) + + str.toUpperCase(); + featureFlags &= ~pwgen.UPPERS; + } + } + + // Handle the AMBIGUOUS flag + if ((pwFlags & pwgen.AMBIGUOUS) > 0) { + for (char ambiguous : pwgen.AMBIGUOUS_STR.toCharArray()) { + if (password.contains(String.valueOf(ambiguous))) { + password = password.substring(0, curSize); + break; + } + } + if (password.length() == curSize) + continue; + } + + curSize += length; + + // Time to stop? + if (curSize >= size) + break; + + // Handle DIGITS + if ((pwFlags & pwgen.DIGITS) > 0) { + if (!first && (randnum.number(10) < 3)) { + String val; + do { + cha = Character.forDigit(randnum.number(10), 10); + val = String.valueOf(cha); + } while ((pwFlags & pwgen.AMBIGUOUS) > 0 + && pwgen.AMBIGUOUS_STR.contains(val)); + password += val; + curSize++; + + featureFlags &= ~pwgen.DIGITS; + + first = true; + prev = 0; + shouldBe = randnum.number(2) == 1 ? VOWEL : CONSONANT; + continue; + } + } + + // Handle SYMBOLS + if ((pwFlags & pwgen.SYMBOLS) > 0) { + if (!first && (randnum.number(10) < 2)) { + String val; + int num; + do { + num = randnum.number(pwgen.SYMBOLS_STR.length()); + cha = pwgen.SYMBOLS_STR.toCharArray()[num]; + val = String.valueOf(cha); + } while ((pwFlags & pwgen.AMBIGUOUS) > 0 + && pwgen.AMBIGUOUS_STR.contains(val)); + password += val; + curSize++; + + featureFlags &= ~pwgen.SYMBOLS; + } + } + + // OK, figure out what the next element should be + if (shouldBe == CONSONANT) { + shouldBe = VOWEL; + } else { + if ((prev & VOWEL) > 0 || (flags & DIPTHONG) > 0 + || (randnum.number(10) > 3)) { + shouldBe = CONSONANT; + } else { + shouldBe = VOWEL; + } + } + prev = flags; + first = false; + } + } while ((featureFlags & (pwgen.UPPERS | pwgen.DIGITS | pwgen.SYMBOLS)) + > 0); + return password; + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgen/pw_rand.java b/app/src/main/java/com/zeapo/pwdstore/pwgen/pw_rand.java new file mode 100644 index 00000000..1f4acdec --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/pwgen/pw_rand.java @@ -0,0 +1,69 @@ +package com.zeapo.pwdstore.pwgen; + +public class pw_rand { + + /** + * Generates a completely random password. + * + * @param size length of password to generate + * @param pwFlags flag field where set bits indicate conditions the + * generated password must meet + * + * + * + * + * + * + * + *
BitCondition
0include at least one number
1include at least one uppercase letter
2include at least one symbol
3don't include ambiguous characters
4don't include vowels
+ * @return the generated password + */ + public static String rand(int size, int pwFlags) { + String password = ""; + char cha; + int i, featureFlags, num; + String val; + + String bank = ""; + if ((pwFlags & pwgen.DIGITS) > 0) { + bank += pwgen.DIGITS_STR; + } + if ((pwFlags & pwgen.UPPERS) > 0) { + bank += pwgen.UPPERS_STR; + } + bank += pwgen.LOWERS_STR; + if ((pwFlags & pwgen.SYMBOLS) > 0) { + bank += pwgen.SYMBOLS_STR; + } + do { + featureFlags = pwFlags; + i = 0; + while (i < size) { + num = randnum.number(bank.length()); + cha = bank.toCharArray()[num]; + val = String.valueOf(cha); + if ((pwFlags & pwgen.AMBIGUOUS) > 0 + && pwgen.AMBIGUOUS_STR.contains(val)) { + continue; + } + if ((pwFlags & pwgen.NO_VOWELS) > 0 + && pwgen.VOWELS_STR.contains(val)) { + continue; + } + password += val; + i++; + if (pwgen.DIGITS_STR.contains(val)) { + featureFlags &= ~pwgen.DIGITS; + } + if (pwgen.UPPERS_STR.contains(val)) { + featureFlags &= ~pwgen.UPPERS; + } + if (pwgen.SYMBOLS_STR.contains(val)) { + featureFlags &= ~pwgen.SYMBOLS; + } + } + } while ((featureFlags & (pwgen.UPPERS | pwgen.DIGITS | pwgen.SYMBOLS)) + > 0); + return password; + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgen/pwgen.java b/app/src/main/java/com/zeapo/pwdstore/pwgen/pwgen.java new file mode 100644 index 00000000..e08573f0 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/pwgen/pwgen.java @@ -0,0 +1,138 @@ +package com.zeapo.pwdstore.pwgen; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.util.ArrayList; + +public class pwgen { + static final int DIGITS = 0x0001; + static final int UPPERS = 0x0002; + static final int SYMBOLS = 0x0004; + static final int AMBIGUOUS = 0x0008; + static final int NO_VOWELS = 0x0010; + + static final String DIGITS_STR = "0123456789"; + static final String UPPERS_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + static final String LOWERS_STR = "abcdefghijklmnopqrstuvwxyz"; + static final String SYMBOLS_STR = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; + static final String AMBIGUOUS_STR = "B8G6I1l0OQDS5Z2"; + static final String VOWELS_STR = "01aeiouyAEIOUY"; + + // No a, c, n, h, H, C, 1, N + private static final String pwOptions = "0ABsvy"; + + /** + * Sets password generation preferences. + * + * @param ctx context from which to retrieve SharedPreferences from + * preferences file 'pwgen' + * @param argv options for password generation + * + * + * + * + * + * + * + * + *
OptionDescription
0don't include numbers
Adon't include uppercase letters
Bdon't include ambiguous charactersl
sgenerate completely random passwords
vdon't include vowels
yinclude at least one symbol
+ * @param numArgv numerical options for password generation: length of + * generated passwords followed by number of passwords to + * generate + * @return false if a numerical options is invalid, + * true otherwise + */ + public static boolean setPrefs(Context ctx, ArrayList argv + , int... numArgv) { + SharedPreferences prefs + = ctx.getSharedPreferences("pwgen", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + for (char option : pwOptions.toCharArray()) { + if (argv.contains(String.valueOf(option))) { + editor.putBoolean(String.valueOf(option), true); + argv.remove(String.valueOf(option)); + } else { + editor.putBoolean(String.valueOf(option), false); + } + } + for (int i = 0; i < numArgv.length && i < 2; i++) { + if (numArgv[i] <= 0) { + // Invalid password length or number of passwords + return false; + } + String name = i == 0 ? "length" : "num"; + editor.putInt(name, numArgv[i]); + } + editor.apply(); + return true; + } + + /** + * Generates passwords using the preferences set by + * {@link #setPrefs(Context, ArrayList, int...)}. + * + * @param ctx context from which to retrieve SharedPreferences from + * preferences file 'pwgen' + * @return list of generated passwords + */ + public static ArrayList generate(Context ctx) { + SharedPreferences prefs + = ctx.getSharedPreferences("pwgen", Context.MODE_PRIVATE); + + boolean phonemes = true; + int pwgenFlags = DIGITS | UPPERS; + + for (char option : pwOptions.toCharArray()) { + if (prefs.getBoolean(String.valueOf(option), false)) { + switch(option) { + case '0': + pwgenFlags &= ~DIGITS; + break; + case 'A': + pwgenFlags &= ~UPPERS; + break; + case 'B': + pwgenFlags |= AMBIGUOUS; + break; + case 's': + phonemes = false; + // pwgenFlags = DIGITS | UPPERS; + break; + case 'y': + pwgenFlags |= SYMBOLS; + break; + case 'v': + phonemes = false; + pwgenFlags |= NO_VOWELS; // | DIGITS | UPPERS; + break; + } + } + } + + int length = prefs.getInt("length", 8); + if (length < 5) { + phonemes = false; + } + if (length <= 2) { + pwgenFlags &= ~UPPERS; + } + if (length <= 1) { + pwgenFlags &= ~DIGITS; + } + + ArrayList passwords = new ArrayList<>(); + int num = prefs.getInt("num", 1); + for (int i = 0; i < num; i++) { + if (phonemes) { + passwords.add(pw_phonemes.phonemes(length, pwgenFlags)); + } else { + passwords.add(pw_rand.rand(length, pwgenFlags)); + } + } + return passwords; + } + +} + diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgen/randnum.java b/app/src/main/java/com/zeapo/pwdstore/pwgen/randnum.java new file mode 100644 index 00000000..83cf4b03 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/pwgen/randnum.java @@ -0,0 +1,26 @@ +package com.zeapo.pwdstore.pwgen; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public class randnum { + private static SecureRandom random; + + static { + try { + random = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + } + + /** + * Generate a random number n, where 0 <= n < maxNum. + * + * @param maxNum the bound on the random number to be returned + * @return the generated random number + */ + public static int number(int maxNum) { + return random.nextInt(maxNum); + } +} From fccefadd3205204cd766af66a391e2b18f52bd67 Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Fri, 10 Jul 2015 19:45:32 -0400 Subject: [PATCH 2/8] Create password generation dialog --- .../zeapo/pwdstore/pwgenDialogFragment.java | 123 ++++++++++++++++++ app/src/main/res/layout/fragment_pwgen.xml | 108 +++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java create mode 100644 app/src/main/res/layout/fragment_pwgen.xml diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java b/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java new file mode 100644 index 00000000..fced4c1c --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java @@ -0,0 +1,123 @@ +package com.zeapo.pwdstore; + +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.TextView; + +import com.zeapo.pwdstore.pwgen.pwgen; + +import java.util.ArrayList; + + +/** + * A placeholder fragment containing a simple view. + */ +public class pwgenDialogFragment extends DialogFragment { + + public pwgenDialogFragment() { + } + + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); + final View view = inflater.inflate(R.layout.fragment_pwgen, null); + builder.setView(view); + + SharedPreferences prefs + = getActivity().getApplicationContext().getSharedPreferences("pwgen", Context.MODE_PRIVATE); + + CheckBox checkBox = (CheckBox) view.findViewById(R.id.numerals); + checkBox.setChecked(!prefs.getBoolean("0", false)); + + checkBox = (CheckBox) view.findViewById(R.id.symbols); + checkBox.setChecked(prefs.getBoolean("y", false)); + + checkBox = (CheckBox) view.findViewById(R.id.uppercase); + checkBox.setChecked(!prefs.getBoolean("A", false)); + + checkBox = (CheckBox) view.findViewById(R.id.ambiguous); + checkBox.setChecked(!prefs.getBoolean("B", false)); + + checkBox = (CheckBox) view.findViewById(R.id.pronounceable); + checkBox.setChecked(!prefs.getBoolean("s", false)); + + TextView textView = (TextView) view.findViewById(R.id.lengthNumber); + textView.setText(Integer.toString(prefs.getInt("length", 8))); + + textView = (TextView) view.findViewById(R.id.passwordText); + textView.setText(pwgen.generate(getActivity().getApplicationContext()).get(0)); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + setPreferences(); + TextView textView = (TextView) getActivity().findViewById(R.id.crypto_password_edit); + textView.append(pwgen.generate(getActivity().getApplicationContext()).get(0)); + } + }); + + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }); + + builder.setNeutralButton("Generate", null); + + final AlertDialog ad = builder.setTitle("Generate Password").create(); + ad.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL); + b.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setPreferences(); + TextView textView = (TextView) getDialog().findViewById(R.id.passwordText); + textView.setText(pwgen.generate(getActivity().getApplicationContext()).get(0)); + } + }); + } + }); + return ad; + } + + private boolean setPreferences () { + ArrayList preferences = new ArrayList<>(); + if (!((CheckBox)getDialog().findViewById(R.id.numerals)).isChecked()) { + preferences.add("0"); + } + if (((CheckBox) getDialog().findViewById(R.id.symbols)).isChecked()) { + preferences.add("y"); + } + if (!((CheckBox) getDialog().findViewById(R.id.uppercase)).isChecked()) { + preferences.add("A"); + } + if (!((CheckBox) getDialog().findViewById(R.id.ambiguous)).isChecked()) { + preferences.add("B"); + } + if (!((CheckBox) getDialog().findViewById(R.id.pronounceable)).isChecked()) { + preferences.add("s"); + } + TextView textView = (TextView) getDialog().findViewById(R.id.lengthNumber); + try { + int length = Integer.valueOf(textView.getText().toString()); + return pwgen.setPrefs(getActivity().getApplicationContext(), preferences, length); + } catch(NumberFormatException e) { + return pwgen.setPrefs(getActivity().getApplicationContext(), preferences); + } + } +} + diff --git a/app/src/main/res/layout/fragment_pwgen.xml b/app/src/main/res/layout/fragment_pwgen.xml new file mode 100644 index 00000000..3fd8bd51 --- /dev/null +++ b/app/src/main/res/layout/fragment_pwgen.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From d63b16eb2671608cd9acc7e935f1ec3db1d0b396 Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Fri, 10 Jul 2015 19:46:18 -0400 Subject: [PATCH 3/8] Open password generation dialog when button pressed --- .../java/com/zeapo/pwdstore/crypto/PgpHandler.java | 5 +++++ app/src/main/res/layout/encrypt_layout.xml | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java index 28aea81e..9c0bc877 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java @@ -1,6 +1,7 @@ package com.zeapo.pwdstore.crypto; import android.app.Activity; +import android.app.DialogFragment; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipboardManager; @@ -28,6 +29,7 @@ import android.widget.Toast; import com.google.common.primitives.Longs; import com.zeapo.pwdstore.R; import com.zeapo.pwdstore.UserPreference; +import com.zeapo.pwdstore.pwgenDialogFragment; import com.zeapo.pwdstore.utils.PasswordRepository; import org.apache.commons.io.FileUtils; @@ -174,6 +176,9 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne case R.id.crypto_get_key_ids: getKeyIds(new Intent()); break; + case R.id.generate_password: + DialogFragment df = new pwgenDialogFragment(); + df.show(getFragmentManager(), "generator"); default: // should not happen diff --git a/app/src/main/res/layout/encrypt_layout.xml b/app/src/main/res/layout/encrypt_layout.xml index 5fc93b3c..dcb6bf88 100644 --- a/app/src/main/res/layout/encrypt_layout.xml +++ b/app/src/main/res/layout/encrypt_layout.xml @@ -64,7 +64,15 @@ android:id="@+id/crypto_password_edit" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:typeface="monospace"/> + android:typeface="monospace" + android:layout_weight="1"/> + Date: Fri, 10 Jul 2015 20:04:46 -0400 Subject: [PATCH 4/8] Apply PRNGFixes from http://android-developers.blogspot.ca/2013/08/some-securerandom-thoughts.html --- .../com/zeapo/pwdstore/PasswordStore.java | 2 + .../com/zeapo/pwdstore/pwgen/PRNGFixes.java | 336 ++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 app/src/main/java/com/zeapo/pwdstore/pwgen/PRNGFixes.java diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java index a99ec34a..5562f0b9 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java @@ -20,6 +20,7 @@ import android.view.View; import com.zeapo.pwdstore.crypto.PgpHandler; import com.zeapo.pwdstore.git.GitActivity; import com.zeapo.pwdstore.git.GitAsyncTask; +import com.zeapo.pwdstore.pwgen.PRNGFixes; import com.zeapo.pwdstore.utils.PasswordItem; import com.zeapo.pwdstore.utils.PasswordRecyclerAdapter; import com.zeapo.pwdstore.utils.PasswordRepository; @@ -49,6 +50,7 @@ public class PasswordStore extends AppCompatActivity { setContentView(R.layout.activity_pwdstore); settings = PreferenceManager.getDefaultSharedPreferences(this.getApplicationContext()); activity = this; + PRNGFixes.apply(); } @Override diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgen/PRNGFixes.java b/app/src/main/java/com/zeapo/pwdstore/pwgen/PRNGFixes.java new file mode 100644 index 00000000..201e821d --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/pwgen/PRNGFixes.java @@ -0,0 +1,336 @@ +package com.zeapo.pwdstore.pwgen; + +/* + * This software is provided 'as-is', without any express or implied + * warranty. In no event will Google be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, as long as the origin is not misrepresented. + */ + +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; +import java.security.Security; + +/** + * Fixes for the output of the default PRNG having low entropy. + * + * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = + getBuildFingerprintAndDeviceSerial(); + + /** Hidden constructor to prevent instantiation. */ + private PRNGFixes() {} + + /** + * Applies all fixes. + * + * @throws SecurityException if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class.forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() + throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = + Security.getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class.equals( + secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng1.getProvider().getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng2.getProvider().getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through + * all requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", + 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG + * ({@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException( + "Failed to read from " + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream( + new FileInputStream(URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = + new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +} \ No newline at end of file From 9b61c6991347e10606051b6d790b8d9e52dc3ba7 Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Fri, 10 Jul 2015 20:13:54 -0400 Subject: [PATCH 5/8] Append password from dialog, not a newly generated one --- .../main/java/com/zeapo/pwdstore/pwgenDialogFragment.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java b/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java index fced4c1c..12822b3f 100644 --- a/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java @@ -61,9 +61,9 @@ public class pwgenDialogFragment extends DialogFragment { builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - setPreferences(); - TextView textView = (TextView) getActivity().findViewById(R.id.crypto_password_edit); - textView.append(pwgen.generate(getActivity().getApplicationContext()).get(0)); + TextView edit = (TextView) getActivity().findViewById(R.id.crypto_password_edit); + TextView generate = (TextView) getDialog().findViewById(R.id.passwordText); + edit.append(generate.getText()); } }); From 5c46fea58d36b28e7ac4a1b744f3973c0e12ad50 Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Sat, 11 Jul 2015 12:22:56 -0400 Subject: [PATCH 6/8] Use string resources --- .../zeapo/pwdstore/pwgenDialogFragment.java | 43 +++++++------------ app/src/main/res/layout/fragment_pwgen.xml | 10 ++--- app/src/main/res/values/strings.xml | 8 ++++ 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java b/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java index 12822b3f..38e7a7b9 100644 --- a/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java @@ -50,46 +50,35 @@ public class pwgenDialogFragment extends DialogFragment { checkBox.setChecked(!prefs.getBoolean("B", false)); checkBox = (CheckBox) view.findViewById(R.id.pronounceable); - checkBox.setChecked(!prefs.getBoolean("s", false)); + checkBox.setChecked(!prefs.getBoolean("s", true)); TextView textView = (TextView) view.findViewById(R.id.lengthNumber); - textView.setText(Integer.toString(prefs.getInt("length", 8))); + textView.setText(Integer.toString(prefs.getInt("length", 20))); textView = (TextView) view.findViewById(R.id.passwordText); textView.setText(pwgen.generate(getActivity().getApplicationContext()).get(0)); - builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - TextView edit = (TextView) getActivity().findViewById(R.id.crypto_password_edit); - TextView generate = (TextView) getDialog().findViewById(R.id.passwordText); - edit.append(generate.getText()); - } + builder.setPositiveButton(getResources().getString(R.string.dialog_ok), (dialog, which) -> { + setPreferences(); + TextView edit = (TextView) getActivity().findViewById(R.id.crypto_password_edit); + TextView generate = (TextView) getDialog().findViewById(R.id.passwordText); + edit.append(generate.getText()); }); - builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { + builder.setNegativeButton(getResources().getString(R.string.dialog_cancel), (dialog, which) -> { - } }); - builder.setNeutralButton("Generate", null); + builder.setNeutralButton(getResources().getString(R.string.pwgen_generate), null); final AlertDialog ad = builder.setTitle("Generate Password").create(); - ad.setOnShowListener(new DialogInterface.OnShowListener() { - @Override - public void onShow(DialogInterface dialog) { - Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL); - b.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - setPreferences(); - TextView textView = (TextView) getDialog().findViewById(R.id.passwordText); - textView.setText(pwgen.generate(getActivity().getApplicationContext()).get(0)); - } - }); - } + ad.setOnShowListener(dialog -> { + Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL); + b.setOnClickListener(v -> { + setPreferences(); + TextView textView1 = (TextView) getDialog().findViewById(R.id.passwordText); + textView1.setText(pwgen.generate(getActivity().getApplicationContext()).get(0)); + }); }); return ad; } diff --git a/app/src/main/res/layout/fragment_pwgen.xml b/app/src/main/res/layout/fragment_pwgen.xml index 3fd8bd51..072a78a9 100644 --- a/app/src/main/res/layout/fragment_pwgen.xml +++ b/app/src/main/res/layout/fragment_pwgen.xml @@ -39,7 +39,7 @@ android:id="@+id/include" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Include" + android:text="@string/pwgen_include" android:textAppearance="?android:attr/textAppearanceSmall" android:layout_marginBottom="8dp"/> @@ -47,26 +47,26 @@ android:id="@+id/numerals" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Numerals"/> + android:text="@string/pwgen_numerals"/> + android:text="@string/pwgen_symbols"/> + android:text="@string/pwgen_uppercase"/> + android:text="@string/pwgen_ambiguous"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8db99ff..a9b03151 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -114,6 +114,14 @@ Recursive filtering Recursively find passwords of the current directory. + + Generate + Include + Numerals + Symbols + Uppercase + Ambiguous + OK Yes From 20da17c2d835233e7a6f30d5c2f8b23364d95ade Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Sat, 11 Jul 2015 12:31:09 -0400 Subject: [PATCH 7/8] Undo lambda-ing --- .../zeapo/pwdstore/pwgenDialogFragment.java | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java b/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java index 38e7a7b9..6ebf6f91 100644 --- a/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java @@ -58,27 +58,39 @@ public class pwgenDialogFragment extends DialogFragment { textView = (TextView) view.findViewById(R.id.passwordText); textView.setText(pwgen.generate(getActivity().getApplicationContext()).get(0)); - builder.setPositiveButton(getResources().getString(R.string.dialog_ok), (dialog, which) -> { - setPreferences(); - TextView edit = (TextView) getActivity().findViewById(R.id.crypto_password_edit); - TextView generate = (TextView) getDialog().findViewById(R.id.passwordText); - edit.append(generate.getText()); + builder.setPositiveButton(getResources().getString(R.string.dialog_ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + pwgenDialogFragment.this.setPreferences(); + TextView edit = (TextView) pwgenDialogFragment.this.getActivity().findViewById(R.id.crypto_password_edit); + TextView generate = (TextView) pwgenDialogFragment.this.getDialog().findViewById(R.id.passwordText); + edit.append(generate.getText()); + } }); - builder.setNegativeButton(getResources().getString(R.string.dialog_cancel), (dialog, which) -> { + builder.setNegativeButton(getResources().getString(R.string.dialog_cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } }); builder.setNeutralButton(getResources().getString(R.string.pwgen_generate), null); final AlertDialog ad = builder.setTitle("Generate Password").create(); - ad.setOnShowListener(dialog -> { - Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL); - b.setOnClickListener(v -> { - setPreferences(); - TextView textView1 = (TextView) getDialog().findViewById(R.id.passwordText); - textView1.setText(pwgen.generate(getActivity().getApplicationContext()).get(0)); - }); + ad.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL); + b.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + pwgenDialogFragment.this.setPreferences(); + TextView textView1 = (TextView) pwgenDialogFragment.this.getDialog().findViewById(R.id.passwordText); + textView1.setText(pwgen.generate(pwgenDialogFragment.this.getActivity().getApplicationContext()).get(0)); + } + }); + } }); return ad; } From 32cf7f7813c89f7eff43712705b1d1453637a180 Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Sat, 11 Jul 2015 12:51:00 -0400 Subject: [PATCH 8/8] Set default preferences (length 20, .etc) on first use of generator, only save preferences when they've been used to generate i.e. Generate pressed --- .../com/zeapo/pwdstore/pwgenDialogFragment.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java b/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java index 6ebf6f91..af905b5d 100644 --- a/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/pwgenDialogFragment.java @@ -55,13 +55,9 @@ public class pwgenDialogFragment extends DialogFragment { TextView textView = (TextView) view.findViewById(R.id.lengthNumber); textView.setText(Integer.toString(prefs.getInt("length", 20))); - textView = (TextView) view.findViewById(R.id.passwordText); - textView.setText(pwgen.generate(getActivity().getApplicationContext()).get(0)); - builder.setPositiveButton(getResources().getString(R.string.dialog_ok), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - pwgenDialogFragment.this.setPreferences(); TextView edit = (TextView) pwgenDialogFragment.this.getActivity().findViewById(R.id.crypto_password_edit); TextView generate = (TextView) pwgenDialogFragment.this.getDialog().findViewById(R.id.passwordText); edit.append(generate.getText()); @@ -81,13 +77,17 @@ public class pwgenDialogFragment extends DialogFragment { ad.setOnShowListener(new DialogInterface.OnShowListener() { @Override public void onShow(DialogInterface dialog) { + setPreferences(); + TextView textView = (TextView) view.findViewById(R.id.passwordText); + textView.setText(pwgen.generate(getActivity().getApplicationContext()).get(0)); + Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL); b.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - pwgenDialogFragment.this.setPreferences(); - TextView textView1 = (TextView) pwgenDialogFragment.this.getDialog().findViewById(R.id.passwordText); - textView1.setText(pwgen.generate(pwgenDialogFragment.this.getActivity().getApplicationContext()).get(0)); + setPreferences(); + TextView textView = (TextView) getDialog().findViewById(R.id.passwordText); + textView.setText(pwgen.generate(getActivity().getApplicationContext()).get(0)); } }); } @@ -97,7 +97,7 @@ public class pwgenDialogFragment extends DialogFragment { private boolean setPreferences () { ArrayList preferences = new ArrayList<>(); - if (!((CheckBox)getDialog().findViewById(R.id.numerals)).isChecked()) { + if (!((CheckBox) getDialog().findViewById(R.id.numerals)).isChecked()) { preferences.add("0"); } if (((CheckBox) getDialog().findViewById(R.id.symbols)).isChecked()) {