mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-09-04 08:15:11 +00:00
Rewrite pwgen package in Kotlin (#463)
* Rename classes to not be ugly as all hell Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com> * PgpActivity: Cleanup Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com> * Rewrite pwgen package in Kotlin Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com> * PRNGFixes: Remove constant conditions Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com>
This commit is contained in:
committed by
Mohamed Zenadi
parent
a82fb391d3
commit
b267321d54
@@ -16,7 +16,7 @@ import android.widget.EditText;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import com.zeapo.pwdstore.pwgen.pwgen;
|
import com.zeapo.pwdstore.pwgen.PasswordGenerator;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -25,9 +25,9 @@ import java.util.ArrayList;
|
|||||||
/**
|
/**
|
||||||
* A placeholder fragment containing a simple view.
|
* A placeholder fragment containing a simple view.
|
||||||
*/
|
*/
|
||||||
public class pwgenDialogFragment extends DialogFragment {
|
public class PasswordGeneratorDialogFragment extends DialogFragment {
|
||||||
|
|
||||||
public pwgenDialogFragment() {
|
public PasswordGeneratorDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ public class pwgenDialogFragment extends DialogFragment {
|
|||||||
builder.setView(view);
|
builder.setView(view);
|
||||||
|
|
||||||
SharedPreferences prefs
|
SharedPreferences prefs
|
||||||
= getActivity().getApplicationContext().getSharedPreferences("pwgen", Context.MODE_PRIVATE);
|
= getActivity().getApplicationContext().getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE);
|
||||||
|
|
||||||
CheckBox checkBox = view.findViewById(R.id.numerals);
|
CheckBox checkBox = view.findViewById(R.id.numerals);
|
||||||
checkBox.setChecked(!prefs.getBoolean("0", false));
|
checkBox.setChecked(!prefs.getBoolean("0", false));
|
||||||
@@ -90,7 +90,7 @@ public class pwgenDialogFragment extends DialogFragment {
|
|||||||
public void onShow(DialogInterface dialog) {
|
public void onShow(DialogInterface dialog) {
|
||||||
setPreferences();
|
setPreferences();
|
||||||
TextView textView = view.findViewById(R.id.passwordText);
|
TextView textView = view.findViewById(R.id.passwordText);
|
||||||
textView.setText(pwgen.generate(getActivity().getApplicationContext()).get(0));
|
textView.setText(PasswordGenerator.INSTANCE.generate(getActivity().getApplicationContext()).get(0));
|
||||||
|
|
||||||
Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL);
|
Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||||
b.setOnClickListener(new View.OnClickListener() {
|
b.setOnClickListener(new View.OnClickListener() {
|
||||||
@@ -98,7 +98,7 @@ public class pwgenDialogFragment extends DialogFragment {
|
|||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
setPreferences();
|
setPreferences();
|
||||||
TextView textView = view.findViewById(R.id.passwordText);
|
TextView textView = view.findViewById(R.id.passwordText);
|
||||||
textView.setText(pwgen.generate(callingActivity.getApplicationContext()).get(0));
|
textView.setText(PasswordGenerator.INSTANCE.generate(callingActivity.getApplicationContext()).get(0));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -126,9 +126,9 @@ public class pwgenDialogFragment extends DialogFragment {
|
|||||||
EditText editText = getDialog().findViewById(R.id.lengthNumber);
|
EditText editText = getDialog().findViewById(R.id.lengthNumber);
|
||||||
try {
|
try {
|
||||||
int length = Integer.valueOf(editText.getText().toString());
|
int length = Integer.valueOf(editText.getText().toString());
|
||||||
pwgen.setPrefs(getActivity().getApplicationContext(), preferences, length);
|
PasswordGenerator.INSTANCE.setPrefs(getActivity().getApplicationContext(), preferences, length);
|
||||||
} catch(NumberFormatException e) {
|
} catch(NumberFormatException e) {
|
||||||
pwgen.setPrefs(getActivity().getApplicationContext(), preferences);
|
PasswordGenerator.INSTANCE.setPrefs(getActivity().getApplicationContext(), preferences);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -116,7 +116,7 @@ public class PasswordStore extends AppCompatActivity {
|
|||||||
shortcutManager = getSystemService(ShortcutManager.class);
|
shortcutManager = getSystemService(ShortcutManager.class);
|
||||||
}
|
}
|
||||||
activity = this;
|
activity = this;
|
||||||
PRNGFixes.apply();
|
PRNGFixes.INSTANCE.apply();
|
||||||
|
|
||||||
// If user opens app with permission granted then revokes and returns,
|
// If user opens app with permission granted then revokes and returns,
|
||||||
// prevent attempt to create password list fragment
|
// prevent attempt to create password list fragment
|
||||||
|
@@ -1,8 +1,5 @@
|
|||||||
package com.zeapo.pwdstore.crypto
|
package com.zeapo.pwdstore.crypto
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
@@ -32,11 +29,12 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.zeapo.pwdstore.PasswordEntry
|
import com.zeapo.pwdstore.PasswordEntry
|
||||||
import com.zeapo.pwdstore.R
|
import com.zeapo.pwdstore.R
|
||||||
import com.zeapo.pwdstore.UserPreference
|
import com.zeapo.pwdstore.UserPreference
|
||||||
import com.zeapo.pwdstore.pwgenDialogFragment
|
import com.zeapo.pwdstore.PasswordGeneratorDialogFragment
|
||||||
import com.zeapo.pwdstore.utils.Otp
|
import com.zeapo.pwdstore.utils.Otp
|
||||||
import kotlinx.android.synthetic.main.decrypt_layout.*
|
import kotlinx.android.synthetic.main.decrypt_layout.*
|
||||||
import kotlinx.android.synthetic.main.encrypt_layout.*
|
import kotlinx.android.synthetic.main.encrypt_layout.*
|
||||||
@@ -117,7 +115,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
setContentView(R.layout.encrypt_layout)
|
setContentView(R.layout.encrypt_layout)
|
||||||
|
|
||||||
generate_password?.setOnClickListener {
|
generate_password?.setOnClickListener {
|
||||||
pwgenDialogFragment().show(supportFragmentManager, "generator")
|
PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator")
|
||||||
}
|
}
|
||||||
|
|
||||||
title = getString(R.string.new_password_title)
|
title = getString(R.string.new_password_title)
|
||||||
@@ -324,7 +322,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
val editor = settings.edit()
|
val editor = settings.edit()
|
||||||
editor.putBoolean("hotp_remember_check", true)
|
editor.putBoolean("hotp_remember_check", true)
|
||||||
editor.putBoolean("hotp_remember_choice", true)
|
editor.putBoolean("hotp_remember_choice", true)
|
||||||
editor.commit()
|
editor.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,7 +332,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
val editor = settings.edit()
|
val editor = settings.edit()
|
||||||
editor.putBoolean("hotp_remember_check", true)
|
editor.putBoolean("hotp_remember_check", true)
|
||||||
editor.putBoolean("hotp_remember_choice", false)
|
editor.putBoolean("hotp_remember_choice", false)
|
||||||
editor.commit()
|
editor.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val updateDialog = dialogBuilder.create()
|
val updateDialog = dialogBuilder.create()
|
||||||
@@ -439,7 +437,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
private fun editPassword() {
|
private fun editPassword() {
|
||||||
setContentView(R.layout.encrypt_layout)
|
setContentView(R.layout.encrypt_layout)
|
||||||
generate_password?.setOnClickListener {
|
generate_password?.setOnClickListener {
|
||||||
pwgenDialogFragment().show(supportFragmentManager, "generator")
|
PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator")
|
||||||
}
|
}
|
||||||
|
|
||||||
title = getString(R.string.edit_password_title)
|
title = getString(R.string.edit_password_title)
|
||||||
@@ -545,26 +543,26 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
Log.d(TAG, "onActivityResult resultCode: " + resultCode)
|
Log.d(TAG, "onActivityResult resultCode: $resultCode")
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
setResult(Activity.RESULT_CANCELED, null)
|
setResult(AppCompatActivity.RESULT_CANCELED, null)
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// try again after user interaction
|
// try again after user interaction
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == AppCompatActivity.RESULT_OK) {
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
REQUEST_DECRYPT -> decryptAndVerify(data)
|
REQUEST_DECRYPT -> decryptAndVerify(data)
|
||||||
REQUEST_KEY_ID -> getKeyIds(data)
|
REQUEST_KEY_ID -> getKeyIds(data)
|
||||||
else -> {
|
else -> {
|
||||||
setResult(Activity.RESULT_OK)
|
setResult(AppCompatActivity.RESULT_OK)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (resultCode == Activity.RESULT_CANCELED) {
|
} else if (resultCode == AppCompatActivity.RESULT_CANCELED) {
|
||||||
setResult(Activity.RESULT_CANCELED, data)
|
setResult(AppCompatActivity.RESULT_CANCELED, data)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -581,7 +579,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
return if (shown) charSequence else super.getTransformation("12345", view)
|
return if (shown) charSequence else super.getTransformation("12345", view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@Suppress("ClickableViewAccessibility")
|
||||||
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
|
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
|
||||||
when (motionEvent.action) {
|
when (motionEvent.action) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
@@ -608,7 +606,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
|
|
||||||
var clearAfter = 45
|
var clearAfter = 45
|
||||||
try {
|
try {
|
||||||
clearAfter = Integer.parseInt(settings.getString("general_show_time", "45"))
|
clearAfter = Integer.parseInt(settings.getString("general_show_time", "45") as String)
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
// ignore and keep default
|
// ignore and keep default
|
||||||
}
|
}
|
||||||
@@ -662,7 +660,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
return DateUtils.getRelativeTimeSpanString(this, timeStamp.toLong() * 1000, true)
|
return DateUtils.getRelativeTimeSpanString(this, timeStamp.toLong() * 1000, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@Suppress("StaticFieldLeak")
|
||||||
inner class DelayShow(val activity: PgpActivity) : AsyncTask<Void, Int, Boolean>() {
|
inner class DelayShow(val activity: PgpActivity) : AsyncTask<Void, Int, Boolean>() {
|
||||||
private val pb: ProgressBar by lazy { pbLoading }
|
private val pb: ProgressBar by lazy { pbLoading }
|
||||||
private var skip = false
|
private var skip = false
|
||||||
@@ -686,7 +684,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
|
|
||||||
override fun onPreExecute() {
|
override fun onPreExecute() {
|
||||||
showTime = try {
|
showTime = try {
|
||||||
Integer.parseInt(settings.getString("general_show_time", "45"))
|
Integer.parseInt(settings.getString("general_show_time", "45") as String)
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
45
|
45
|
||||||
}
|
}
|
||||||
@@ -744,7 +742,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
if (crypto_password_show != null) {
|
if (crypto_password_show != null) {
|
||||||
// clear password; if decrypt changed to encrypt layout via edit button, no need
|
// clear password; if decrypt changed to encrypt layout via edit button, no need
|
||||||
if(passwordEntry?.hotpIsIncremented() == false) {
|
if(passwordEntry?.hotpIsIncremented() == false) {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(AppCompatActivity.RESULT_CANCELED)
|
||||||
}
|
}
|
||||||
passwordEntry = null
|
passwordEntry = null
|
||||||
crypto_password_show.text = ""
|
crypto_password_show.text = ""
|
||||||
@@ -788,7 +786,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
* Gets the name of the password (excluding .gpg)
|
* Gets the name of the password (excluding .gpg)
|
||||||
*/
|
*/
|
||||||
fun getName(fullPath: String): String {
|
fun getName(fullPath: String): String {
|
||||||
return FilenameUtils.getBaseName(fullPath);
|
return FilenameUtils.getBaseName(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -797,16 +795,16 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getLongName(fullPath: String, repositoryPath: String, basename: String): String {
|
fun getLongName(fullPath: String, repositoryPath: String, basename: String): String {
|
||||||
var relativePath = getRelativePath(fullPath, repositoryPath)
|
var relativePath = getRelativePath(fullPath, repositoryPath)
|
||||||
if (relativePath.isNotEmpty() && relativePath != "/") {
|
return if (relativePath.isNotEmpty() && relativePath != "/") {
|
||||||
// remove preceding '/'
|
// remove preceding '/'
|
||||||
relativePath = relativePath.substring(1);
|
relativePath = relativePath.substring(1)
|
||||||
if (relativePath.endsWith('/')) {
|
if (relativePath.endsWith('/')) {
|
||||||
return relativePath + basename
|
relativePath + basename
|
||||||
} else {
|
} else {
|
||||||
return "$relativePath/$basename"
|
"$relativePath/$basename"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return basename
|
basename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,333 +0,0 @@
|
|||||||
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.
|
|
||||||
*/
|
|
||||||
private static DataInputStream sUrandomIn;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Output stream for writing to Linux PRNG or {@code null} if not yet
|
|
||||||
* opened.
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
|
|
||||||
@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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
310
app/src/main/java/com/zeapo/pwdstore/pwgen/PRNGFixes.kt
Normal file
310
app/src/main/java/com/zeapo/pwdstore/pwgen/PRNGFixes.kt
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
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 [.apply] before any use of Java
|
||||||
|
* Cryptography Architecture primitives. A good place to invoke them is in the
|
||||||
|
* application's `onCreate`.
|
||||||
|
*/
|
||||||
|
object PRNGFixes {
|
||||||
|
|
||||||
|
private const val VERSION_CODE_JELLY_BEAN_MR2 = 18
|
||||||
|
private val BUILD_FINGERPRINT_AND_DEVICE_SERIAL = buildFingerprintAndDeviceSerial
|
||||||
|
|
||||||
|
private val buildFingerprintAndDeviceSerial: ByteArray
|
||||||
|
get() {
|
||||||
|
val result = StringBuilder()
|
||||||
|
val fingerprint = Build.FINGERPRINT
|
||||||
|
if (fingerprint != null) {
|
||||||
|
result.append(fingerprint)
|
||||||
|
}
|
||||||
|
// TODO: Build#SERIAL is deprecated and should be removed or replaced
|
||||||
|
val serial = Build.SERIAL
|
||||||
|
if (serial != null) {
|
||||||
|
result.append(serial)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return result.toString().toByteArray(charset("UTF-8"))
|
||||||
|
} catch (e: UnsupportedEncodingException) {
|
||||||
|
throw RuntimeException("UTF-8 encoding not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies all fixes.
|
||||||
|
*
|
||||||
|
* @throws SecurityException if a fix is needed but could not be applied.
|
||||||
|
*/
|
||||||
|
fun 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.
|
||||||
|
*/
|
||||||
|
@Throws(SecurityException::class)
|
||||||
|
private fun applyOpenSSLFix() {
|
||||||
|
if (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", ByteArray::class.java)
|
||||||
|
.invoke(null, generateSeed())
|
||||||
|
|
||||||
|
// Mix output of Linux PRNG into OpenSSL's PRNG
|
||||||
|
val bytesRead = Class.forName(
|
||||||
|
"org.apache.harmony.xnet.provider.jsse.NativeCrypto"
|
||||||
|
)
|
||||||
|
.getMethod("RAND_load_file", String::class.java, Long::class.javaPrimitiveType)
|
||||||
|
.invoke(null, "/dev/urandom", 1024) as Int
|
||||||
|
if (bytesRead != 1024) {
|
||||||
|
throw IOException(
|
||||||
|
"Unexpected number of bytes read from Linux PRNG: $bytesRead"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw SecurityException("Failed to seed OpenSSL PRNG", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a Linux PRNG-backed `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.
|
||||||
|
*/
|
||||||
|
@Throws(SecurityException::class)
|
||||||
|
private fun installLinuxPRNGSecureRandom() {
|
||||||
|
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.
|
||||||
|
val secureRandomProviders = Security.getProviders("SecureRandom.SHA1PRNG")
|
||||||
|
if (secureRandomProviders == null
|
||||||
|
|| secureRandomProviders.isEmpty()
|
||||||
|
|| LinuxPRNGSecureRandomProvider::class.java != secureRandomProviders[0].javaClass
|
||||||
|
) {
|
||||||
|
Security.insertProviderAt(LinuxPRNGSecureRandomProvider(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that new SecureRandom() and
|
||||||
|
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
|
||||||
|
// by the Linux PRNG-based SecureRandom implementation.
|
||||||
|
val rng1 = SecureRandom()
|
||||||
|
if (LinuxPRNGSecureRandomProvider::class.java != rng1.provider.javaClass) {
|
||||||
|
throw SecurityException(
|
||||||
|
"new SecureRandom() backed by wrong Provider: " + rng1.provider.javaClass
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val rng2: SecureRandom
|
||||||
|
try {
|
||||||
|
rng2 = SecureRandom.getInstance("SHA1PRNG")
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw SecurityException("SHA1PRNG not available", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LinuxPRNGSecureRandomProvider::class.java != rng2.provider.javaClass) {
|
||||||
|
throw SecurityException(
|
||||||
|
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
|
||||||
|
+ " Provider: " + rng2.provider.javaClass
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `Provider` of `SecureRandom` engines which pass through
|
||||||
|
* all requests to the Linux PRNG.
|
||||||
|
*/
|
||||||
|
private class LinuxPRNGSecureRandomProvider :
|
||||||
|
Provider("LinuxPRNG", 1.0, "A Linux-specific random number provider that uses" + " /dev/urandom") {
|
||||||
|
init {
|
||||||
|
// 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.java.name)
|
||||||
|
put("SecureRandom.SHA1PRNG ImplementedIn", "Software")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [SecureRandomSpi] which passes all requests to the Linux PRNG
|
||||||
|
* (`/dev/urandom`).
|
||||||
|
*/
|
||||||
|
class LinuxPRNGSecureRandom : SecureRandomSpi() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 var mSeeded: Boolean = false
|
||||||
|
|
||||||
|
private// 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.
|
||||||
|
val urandomInputStream: DataInputStream
|
||||||
|
get() = synchronized(sLock) {
|
||||||
|
if (sUrandomIn == null) {
|
||||||
|
try {
|
||||||
|
sUrandomIn = DataInputStream(
|
||||||
|
FileInputStream(URANDOM_FILE)
|
||||||
|
)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw SecurityException(
|
||||||
|
"Failed to open "
|
||||||
|
+ URANDOM_FILE + " for reading", e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sUrandomIn as DataInputStream
|
||||||
|
}
|
||||||
|
|
||||||
|
private val urandomOutputStream: OutputStream
|
||||||
|
@Throws(IOException::class)
|
||||||
|
get() = synchronized(sLock) {
|
||||||
|
if (sUrandomOut == null) {
|
||||||
|
sUrandomOut = FileOutputStream(URANDOM_FILE)
|
||||||
|
}
|
||||||
|
return sUrandomOut as OutputStream
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun engineSetSeed(bytes: ByteArray) {
|
||||||
|
try {
|
||||||
|
val out: OutputStream = urandomOutputStream
|
||||||
|
out.write(bytes)
|
||||||
|
out.flush()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// On a small fraction of devices /dev/urandom is not writable.
|
||||||
|
// Log and ignore.
|
||||||
|
Log.w(
|
||||||
|
PRNGFixes::class.java.simpleName,
|
||||||
|
"Failed to mix seed into $URANDOM_FILE"
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
mSeeded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun engineNextBytes(bytes: ByteArray) {
|
||||||
|
if (!mSeeded) {
|
||||||
|
// Mix in the device- and invocation-specific seed.
|
||||||
|
engineSetSeed(generateSeed())
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val `in`: DataInputStream = urandomInputStream
|
||||||
|
synchronized(`in`) {
|
||||||
|
`in`.readFully(bytes)
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw SecurityException(
|
||||||
|
"Failed to read from $URANDOM_FILE", e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun engineGenerateSeed(size: Int): ByteArray {
|
||||||
|
val seed = ByteArray(size)
|
||||||
|
engineNextBytes(seed)
|
||||||
|
return seed
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 val URANDOM_FILE = File("/dev/urandom")
|
||||||
|
|
||||||
|
private val sLock = Any()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input stream for reading from Linux PRNG or `null` if not yet
|
||||||
|
* opened.
|
||||||
|
*/
|
||||||
|
private var sUrandomIn: DataInputStream? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output stream for writing to Linux PRNG or `null` if not yet
|
||||||
|
* opened.
|
||||||
|
*/
|
||||||
|
private var sUrandomOut: OutputStream? = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a device- and invocation-specific seed to be mixed into the
|
||||||
|
* Linux PRNG.
|
||||||
|
*/
|
||||||
|
private fun generateSeed(): ByteArray {
|
||||||
|
try {
|
||||||
|
val seedBuffer = ByteArrayOutputStream()
|
||||||
|
val seedBufferOut = 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 (e: IOException) {
|
||||||
|
throw SecurityException("Failed to generate seed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
125
app/src/main/java/com/zeapo/pwdstore/pwgen/PasswordGenerator.kt
Normal file
125
app/src/main/java/com/zeapo/pwdstore/pwgen/PasswordGenerator.kt
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package com.zeapo.pwdstore.pwgen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
object PasswordGenerator {
|
||||||
|
internal val DIGITS = 0x0001
|
||||||
|
internal val UPPERS = 0x0002
|
||||||
|
internal val SYMBOLS = 0x0004
|
||||||
|
internal val AMBIGUOUS = 0x0008
|
||||||
|
internal val NO_VOWELS = 0x0010
|
||||||
|
|
||||||
|
internal val DIGITS_STR = "0123456789"
|
||||||
|
internal val UPPERS_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
internal val LOWERS_STR = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
internal val SYMBOLS_STR = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
|
||||||
|
internal val AMBIGUOUS_STR = "B8G6I1l0OQDS5Z2"
|
||||||
|
internal val VOWELS_STR = "01aeiouyAEIOUY"
|
||||||
|
|
||||||
|
// No a, c, n, h, H, C, 1, N
|
||||||
|
private val pwOptions = "0ABsvy"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets password generation preferences.
|
||||||
|
*
|
||||||
|
* @param ctx context from which to retrieve SharedPreferences from
|
||||||
|
* preferences file 'PasswordGenerator'
|
||||||
|
* @param argv options for password generation
|
||||||
|
* <table summary="options for password generation">
|
||||||
|
* <tr><td>Option</td><td>Description</td></tr>
|
||||||
|
* <tr><td>0</td><td>don't include numbers</td></tr>
|
||||||
|
* <tr><td>A</td><td>don't include uppercase letters</td></tr>
|
||||||
|
* <tr><td>B</td><td>don't include ambiguous charactersl</td></tr>
|
||||||
|
* <tr><td>s</td><td>generate completely random passwords</td></tr>
|
||||||
|
* <tr><td>v</td><td>don't include vowels</td></tr>
|
||||||
|
* <tr><td>y</td><td>include at least one symbol</td></tr>
|
||||||
|
</table> *
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
fun setPrefs(ctx: Context, argv: ArrayList<String>, vararg numArgv: Int): Boolean {
|
||||||
|
val prefs = ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
|
||||||
|
val editor = prefs.edit()
|
||||||
|
|
||||||
|
for (option in pwOptions.toCharArray()) {
|
||||||
|
if (argv.contains(option.toString())) {
|
||||||
|
editor.putBoolean(option.toString(), true)
|
||||||
|
argv.remove(option.toString())
|
||||||
|
} else {
|
||||||
|
editor.putBoolean(option.toString(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var i = 0
|
||||||
|
while (i < numArgv.size && i < 2) {
|
||||||
|
if (numArgv[i] <= 0) {
|
||||||
|
// Invalid password length or number of passwords
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val name = if (i == 0) "length" else "num"
|
||||||
|
editor.putInt(name, numArgv[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
editor.apply()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates passwords using the preferences set by
|
||||||
|
* [.setPrefs].
|
||||||
|
*
|
||||||
|
* @param ctx context from which to retrieve SharedPreferences from
|
||||||
|
* preferences file 'PasswordGenerator'
|
||||||
|
* @return list of generated passwords
|
||||||
|
*/
|
||||||
|
fun generate(ctx: Context): ArrayList<String> {
|
||||||
|
val prefs = ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
var phonemes = true
|
||||||
|
var pwgenFlags = DIGITS or UPPERS
|
||||||
|
|
||||||
|
for (option in pwOptions.toCharArray()) {
|
||||||
|
if (prefs.getBoolean(option.toString(), false)) {
|
||||||
|
when (option) {
|
||||||
|
'0' -> pwgenFlags = pwgenFlags and DIGITS.inv()
|
||||||
|
'A' -> pwgenFlags = pwgenFlags and UPPERS.inv()
|
||||||
|
'B' -> pwgenFlags = pwgenFlags or AMBIGUOUS
|
||||||
|
's' -> phonemes = false
|
||||||
|
'y' -> pwgenFlags = pwgenFlags or SYMBOLS
|
||||||
|
'v' -> {
|
||||||
|
phonemes = false
|
||||||
|
pwgenFlags = pwgenFlags or NO_VOWELS // | DIGITS | UPPERS;
|
||||||
|
}
|
||||||
|
}// pwgenFlags = DIGITS | UPPERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val length = prefs.getInt("length", 8)
|
||||||
|
if (length < 5) {
|
||||||
|
phonemes = false
|
||||||
|
}
|
||||||
|
if (length <= 2) {
|
||||||
|
pwgenFlags = pwgenFlags and UPPERS.inv()
|
||||||
|
}
|
||||||
|
if (length <= 1) {
|
||||||
|
pwgenFlags = pwgenFlags and DIGITS.inv()
|
||||||
|
}
|
||||||
|
|
||||||
|
val passwords = ArrayList<String>()
|
||||||
|
val num = prefs.getInt("num", 1)
|
||||||
|
for (i in 0 until num) {
|
||||||
|
if (phonemes) {
|
||||||
|
passwords.add(Phonemes.phonemes(length, pwgenFlags))
|
||||||
|
} else {
|
||||||
|
passwords.add(RandomPasswordGenerator.rand(length, pwgenFlags))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return passwords
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
208
app/src/main/java/com/zeapo/pwdstore/pwgen/Phonemes.kt
Normal file
208
app/src/main/java/com/zeapo/pwdstore/pwgen/Phonemes.kt
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
package com.zeapo.pwdstore.pwgen
|
||||||
|
|
||||||
|
internal object Phonemes {
|
||||||
|
private const val CONSONANT = 0x0001
|
||||||
|
private const val VOWEL = 0x0002
|
||||||
|
private const val DIPTHONG = 0x0004
|
||||||
|
private const val NOT_FIRST = 0x0008
|
||||||
|
|
||||||
|
private val elements = arrayOf(
|
||||||
|
Element("a", VOWEL),
|
||||||
|
Element("ae", VOWEL or DIPTHONG),
|
||||||
|
Element("ah", VOWEL or DIPTHONG),
|
||||||
|
Element("ai", VOWEL or DIPTHONG),
|
||||||
|
Element("b", CONSONANT),
|
||||||
|
Element("c", CONSONANT),
|
||||||
|
Element("ch", CONSONANT or DIPTHONG),
|
||||||
|
Element("d", CONSONANT),
|
||||||
|
Element("e", VOWEL),
|
||||||
|
Element("ee", VOWEL or DIPTHONG),
|
||||||
|
Element("ei", VOWEL or DIPTHONG),
|
||||||
|
Element("f", CONSONANT),
|
||||||
|
Element("g", CONSONANT),
|
||||||
|
Element("gh", CONSONANT or DIPTHONG or NOT_FIRST),
|
||||||
|
Element("h", CONSONANT),
|
||||||
|
Element("i", VOWEL),
|
||||||
|
Element("ie", VOWEL or DIPTHONG),
|
||||||
|
Element("j", CONSONANT),
|
||||||
|
Element("k", CONSONANT),
|
||||||
|
Element("l", CONSONANT),
|
||||||
|
Element("m", CONSONANT),
|
||||||
|
Element("n", CONSONANT),
|
||||||
|
Element("ng", CONSONANT or DIPTHONG or NOT_FIRST),
|
||||||
|
Element("o", VOWEL),
|
||||||
|
Element("oh", VOWEL or DIPTHONG),
|
||||||
|
Element("oo", VOWEL or DIPTHONG),
|
||||||
|
Element("p", CONSONANT),
|
||||||
|
Element("ph", CONSONANT or DIPTHONG),
|
||||||
|
Element("qu", CONSONANT or DIPTHONG),
|
||||||
|
Element("r", CONSONANT),
|
||||||
|
Element("s", CONSONANT),
|
||||||
|
Element("sh", CONSONANT or DIPTHONG),
|
||||||
|
Element("t", CONSONANT),
|
||||||
|
Element("th", CONSONANT or DIPTHONG),
|
||||||
|
Element("u", VOWEL),
|
||||||
|
Element("v", CONSONANT),
|
||||||
|
Element("w", CONSONANT),
|
||||||
|
Element("x", CONSONANT),
|
||||||
|
Element("y", CONSONANT),
|
||||||
|
Element("z", CONSONANT)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val NUM_ELEMENTS = elements.size
|
||||||
|
|
||||||
|
private class Element internal constructor(internal var str: String, internal var flags: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* <table summary="bits of flag field">
|
||||||
|
* <tr><td>Bit</td><td>Condition</td></tr>
|
||||||
|
* <tr><td>0</td><td>include at least one number</td></tr>
|
||||||
|
* <tr><td>1</td><td>include at least one uppercase letter</td></tr>
|
||||||
|
* <tr><td>2</td><td>include at least one symbol</td></tr>
|
||||||
|
* <tr><td>3</td><td>don't include ambiguous characters</td></tr>
|
||||||
|
</table> *
|
||||||
|
* @return the generated password
|
||||||
|
*/
|
||||||
|
fun phonemes(size: Int, pwFlags: Int): String {
|
||||||
|
var password: String
|
||||||
|
var curSize: Int
|
||||||
|
var i: Int
|
||||||
|
var length: Int
|
||||||
|
var flags: Int
|
||||||
|
var featureFlags: Int
|
||||||
|
var prev: Int
|
||||||
|
var shouldBe: Int
|
||||||
|
var first: Boolean
|
||||||
|
var str: String
|
||||||
|
var cha: Char
|
||||||
|
|
||||||
|
do {
|
||||||
|
password = ""
|
||||||
|
featureFlags = pwFlags
|
||||||
|
curSize = 0
|
||||||
|
prev = 0
|
||||||
|
first = true
|
||||||
|
|
||||||
|
shouldBe = if (RandomNumberGenerator.number(2) == 1) VOWEL else CONSONANT
|
||||||
|
|
||||||
|
while (curSize < size) {
|
||||||
|
i = RandomNumberGenerator.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 and shouldBe == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Handle the NOT_FIRST flag
|
||||||
|
if (first && flags and NOT_FIRST > 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Don't allow VOWEL followed a Vowel/Dipthong pair
|
||||||
|
if (prev and VOWEL > 0 && flags and VOWEL > 0
|
||||||
|
&& flags and 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 and PasswordGenerator.UPPERS > 0) {
|
||||||
|
if ((first || flags and CONSONANT > 0) && RandomNumberGenerator.number(10) < 2) {
|
||||||
|
val index = password.length - length
|
||||||
|
password = password.substring(0, index) + str.toUpperCase()
|
||||||
|
featureFlags = featureFlags and PasswordGenerator.UPPERS.inv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the AMBIGUOUS flag
|
||||||
|
if (pwFlags and PasswordGenerator.AMBIGUOUS > 0) {
|
||||||
|
for (ambiguous in PasswordGenerator.AMBIGUOUS_STR.toCharArray()) {
|
||||||
|
if (password.contains(ambiguous.toString())) {
|
||||||
|
password = password.substring(0, curSize)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (password.length == curSize)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
curSize += length
|
||||||
|
|
||||||
|
// Time to stop?
|
||||||
|
if (curSize >= size)
|
||||||
|
break
|
||||||
|
|
||||||
|
// Handle DIGITS
|
||||||
|
if (pwFlags and PasswordGenerator.DIGITS > 0) {
|
||||||
|
if (!first && RandomNumberGenerator.number(10) < 3) {
|
||||||
|
var `val`: String
|
||||||
|
do {
|
||||||
|
cha = Character.forDigit(RandomNumberGenerator.number(10), 10)
|
||||||
|
`val` = cha.toString()
|
||||||
|
} while (pwFlags and PasswordGenerator.AMBIGUOUS > 0 && PasswordGenerator.AMBIGUOUS_STR.contains(
|
||||||
|
`val`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
password += `val`
|
||||||
|
curSize++
|
||||||
|
|
||||||
|
featureFlags = featureFlags and PasswordGenerator.DIGITS.inv()
|
||||||
|
|
||||||
|
first = true
|
||||||
|
prev = 0
|
||||||
|
shouldBe = if (RandomNumberGenerator.number(2) == 1) VOWEL else CONSONANT
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle SYMBOLS
|
||||||
|
if (pwFlags and PasswordGenerator.SYMBOLS > 0) {
|
||||||
|
if (!first && RandomNumberGenerator.number(10) < 2) {
|
||||||
|
var `val`: String
|
||||||
|
var num: Int
|
||||||
|
do {
|
||||||
|
num = RandomNumberGenerator.number(PasswordGenerator.SYMBOLS_STR.length)
|
||||||
|
cha = PasswordGenerator.SYMBOLS_STR.toCharArray()[num]
|
||||||
|
`val` = cha.toString()
|
||||||
|
} while (pwFlags and PasswordGenerator.AMBIGUOUS > 0 && PasswordGenerator.AMBIGUOUS_STR.contains(
|
||||||
|
`val`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
password += `val`
|
||||||
|
curSize++
|
||||||
|
|
||||||
|
featureFlags = featureFlags and PasswordGenerator.SYMBOLS.inv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK, figure out what the next Element should be
|
||||||
|
shouldBe = if (shouldBe == CONSONANT) {
|
||||||
|
VOWEL
|
||||||
|
} else {
|
||||||
|
if (prev and VOWEL > 0 || flags and DIPTHONG > 0
|
||||||
|
|| RandomNumberGenerator.number(10) > 3
|
||||||
|
) {
|
||||||
|
CONSONANT
|
||||||
|
} else {
|
||||||
|
VOWEL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev = flags
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
} while (featureFlags and (PasswordGenerator.UPPERS or PasswordGenerator.DIGITS or PasswordGenerator.SYMBOLS) > 0)
|
||||||
|
return password
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,26 @@
|
|||||||
|
package com.zeapo.pwdstore.pwgen
|
||||||
|
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
internal object RandomNumberGenerator {
|
||||||
|
private var random: SecureRandom
|
||||||
|
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
random = SecureRandom.getInstance("SHA1PRNG")
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw 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
|
||||||
|
*/
|
||||||
|
fun number(maxNum: Int): Int {
|
||||||
|
return random.nextInt(maxNum)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,72 @@
|
|||||||
|
package com.zeapo.pwdstore.pwgen
|
||||||
|
|
||||||
|
internal object RandomPasswordGenerator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* <table summary ="bits of flag field">
|
||||||
|
* <tr><td>Bit</td><td>Condition</td></tr>
|
||||||
|
* <tr><td>0</td><td>include at least one number</td></tr>
|
||||||
|
* <tr><td>1</td><td>include at least one uppercase letter</td></tr>
|
||||||
|
* <tr><td>2</td><td>include at least one symbol</td></tr>
|
||||||
|
* <tr><td>3</td><td>don't include ambiguous characters</td></tr>
|
||||||
|
* <tr><td>4</td><td>don't include vowels</td></tr>
|
||||||
|
</table> *
|
||||||
|
* @return the generated password
|
||||||
|
*/
|
||||||
|
fun rand(size: Int, pwFlags: Int): String {
|
||||||
|
var password: String
|
||||||
|
var cha: Char
|
||||||
|
var i: Int
|
||||||
|
var featureFlags: Int
|
||||||
|
var num: Int
|
||||||
|
var `val`: String
|
||||||
|
|
||||||
|
var bank = ""
|
||||||
|
if (pwFlags and PasswordGenerator.DIGITS > 0) {
|
||||||
|
bank += PasswordGenerator.DIGITS_STR
|
||||||
|
}
|
||||||
|
if (pwFlags and PasswordGenerator.UPPERS > 0) {
|
||||||
|
bank += PasswordGenerator.UPPERS_STR
|
||||||
|
}
|
||||||
|
bank += PasswordGenerator.LOWERS_STR
|
||||||
|
if (pwFlags and PasswordGenerator.SYMBOLS > 0) {
|
||||||
|
bank += PasswordGenerator.SYMBOLS_STR
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
password = ""
|
||||||
|
featureFlags = pwFlags
|
||||||
|
i = 0
|
||||||
|
while (i < size) {
|
||||||
|
num = RandomNumberGenerator.number(bank.length)
|
||||||
|
cha = bank.toCharArray()[num]
|
||||||
|
`val` = cha.toString()
|
||||||
|
if (pwFlags and PasswordGenerator.AMBIGUOUS > 0 && PasswordGenerator.AMBIGUOUS_STR.contains(
|
||||||
|
`val`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (pwFlags and PasswordGenerator.NO_VOWELS > 0 && PasswordGenerator.VOWELS_STR.contains(`val`)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
password += `val`
|
||||||
|
i++
|
||||||
|
if (PasswordGenerator.DIGITS_STR.contains(`val`)) {
|
||||||
|
featureFlags = featureFlags and PasswordGenerator.DIGITS.inv()
|
||||||
|
}
|
||||||
|
if (PasswordGenerator.UPPERS_STR.contains(`val`)) {
|
||||||
|
featureFlags = featureFlags and PasswordGenerator.UPPERS.inv()
|
||||||
|
}
|
||||||
|
if (PasswordGenerator.SYMBOLS_STR.contains(`val`)) {
|
||||||
|
featureFlags = featureFlags and PasswordGenerator.SYMBOLS.inv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (featureFlags and (PasswordGenerator.UPPERS or PasswordGenerator.DIGITS or PasswordGenerator.SYMBOLS) > 0)
|
||||||
|
return password
|
||||||
|
}
|
||||||
|
}
|
@@ -1,206 +0,0 @@
|
|||||||
package com.zeapo.pwdstore.pwgen;
|
|
||||||
|
|
||||||
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
|
|
||||||
* <table summary="bits of flag field">
|
|
||||||
* <tr><td>Bit</td><td>Condition</td></tr>
|
|
||||||
* <tr><td>0</td><td>include at least one number</td></tr>
|
|
||||||
* <tr><td>1</td><td>include at least one uppercase letter</td></tr>
|
|
||||||
* <tr><td>2</td><td>include at least one symbol</td></tr>
|
|
||||||
* <tr><td>3</td><td>don't include ambiguous characters</td></tr>
|
|
||||||
* </table>
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,70 +0,0 @@
|
|||||||
package com.zeapo.pwdstore.pwgen;
|
|
||||||
|
|
||||||
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
|
|
||||||
* <table summary ="bits of flag field">
|
|
||||||
* <tr><td>Bit</td><td>Condition</td></tr>
|
|
||||||
* <tr><td>0</td><td>include at least one number</td></tr>
|
|
||||||
* <tr><td>1</td><td>include at least one uppercase letter</td></tr>
|
|
||||||
* <tr><td>2</td><td>include at least one symbol</td></tr>
|
|
||||||
* <tr><td>3</td><td>don't include ambiguous characters</td></tr>
|
|
||||||
* <tr><td>4</td><td>don't include vowels</td></tr>
|
|
||||||
* </table>
|
|
||||||
* @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 {
|
|
||||||
password = "";
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,138 +0,0 @@
|
|||||||
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
|
|
||||||
* <table summary="options for password generation">
|
|
||||||
* <tr><td>Option</td><td>Description</td></tr>
|
|
||||||
* <tr><td>0</td><td>don't include numbers</td></tr>
|
|
||||||
* <tr><td>A</td><td>don't include uppercase letters</td></tr>
|
|
||||||
* <tr><td>B</td><td>don't include ambiguous charactersl</td></tr>
|
|
||||||
* <tr><td>s</td><td>generate completely random passwords</td></tr>
|
|
||||||
* <tr><td>v</td><td>don't include vowels</td></tr>
|
|
||||||
* <tr><td>y</td><td>include at least one symbol</td></tr>
|
|
||||||
* </table>
|
|
||||||
* @param numArgv numerical options for password generation: length of
|
|
||||||
* generated passwords followed by number of passwords to
|
|
||||||
* generate
|
|
||||||
* @return <code>false</code> if a numerical options is invalid,
|
|
||||||
* <code>true</code> otherwise
|
|
||||||
*/
|
|
||||||
public static boolean setPrefs(Context ctx, ArrayList<String> 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<String> 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<String> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@@ -1,26 +0,0 @@
|
|||||||
package com.zeapo.pwdstore.pwgen;
|
|
||||||
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -104,7 +104,7 @@
|
|||||||
<string name="prefs_export_passwords_title">تصدير كلمات السر</string>
|
<string name="prefs_export_passwords_title">تصدير كلمات السر</string>
|
||||||
<string name="prefs_version">النسخة</string>
|
<string name="prefs_version">النسخة</string>
|
||||||
|
|
||||||
<!-- pwgen fragment -->
|
<!-- PasswordGenerator fragment -->
|
||||||
<string name="pwgen_generate">توليد</string>
|
<string name="pwgen_generate">توليد</string>
|
||||||
<string name="pwgen_include">تحتوي على</string>
|
<string name="pwgen_include">تحتوي على</string>
|
||||||
<string name="pwgen_length">الطول</string>
|
<string name="pwgen_length">الطول</string>
|
||||||
|
@@ -106,7 +106,7 @@
|
|||||||
<string name="pref_external_repository_summary">Použít externí repozitář hesel</string>
|
<string name="pref_external_repository_summary">Použít externí repozitář hesel</string>
|
||||||
<string name="pref_select_external_repository">Vybrat externí repozitář</string>
|
<string name="pref_select_external_repository">Vybrat externí repozitář</string>
|
||||||
|
|
||||||
<!-- pwgen fragment -->
|
<!-- PasswordGenerator fragment -->
|
||||||
<string name="pwgen_generate">Generovat</string>
|
<string name="pwgen_generate">Generovat</string>
|
||||||
<string name="pwgen_include">Vložit</string>
|
<string name="pwgen_include">Vložit</string>
|
||||||
<string name="pwgen_numerals">Čísla</string>
|
<string name="pwgen_numerals">Čísla</string>
|
||||||
|
@@ -131,7 +131,7 @@
|
|||||||
<string name="prefs_export_passwords_summary">Exportiert die verschlüsselten Passwörter in ein externes Verzeichnis</string>
|
<string name="prefs_export_passwords_summary">Exportiert die verschlüsselten Passwörter in ein externes Verzeichnis</string>
|
||||||
<string name="prefs_version">Version</string>
|
<string name="prefs_version">Version</string>
|
||||||
|
|
||||||
<!-- pwgen fragment -->
|
<!-- PasswordGenerator fragment -->
|
||||||
<string name="pwgen_generate">Generieren</string>
|
<string name="pwgen_generate">Generieren</string>
|
||||||
<string name="pwgen_include">Include</string>
|
<string name="pwgen_include">Include</string>
|
||||||
<string name="pwgen_numerals">Nummern</string>
|
<string name="pwgen_numerals">Nummern</string>
|
||||||
|
@@ -110,7 +110,7 @@
|
|||||||
<string name="pref_external_repository_summary">Utilise un dépôt externe pour les mots de passe</string>
|
<string name="pref_external_repository_summary">Utilise un dépôt externe pour les mots de passe</string>
|
||||||
<string name="pref_select_external_repository">Choisissez un dépôt externe</string>
|
<string name="pref_select_external_repository">Choisissez un dépôt externe</string>
|
||||||
|
|
||||||
<!-- pwgen fragment -->
|
<!-- PasswordGenerator fragment -->
|
||||||
<string name="pwgen_generate">Générer</string>
|
<string name="pwgen_generate">Générer</string>
|
||||||
<string name="pwgen_include">Inclure</string>
|
<string name="pwgen_include">Inclure</string>
|
||||||
<string name="pwgen_length">Longueur</string>
|
<string name="pwgen_length">Longueur</string>
|
||||||
|
@@ -110,7 +110,7 @@
|
|||||||
<string name="pref_external_repository_summary">外部パスワードリポジトリを使用する</string>
|
<string name="pref_external_repository_summary">外部パスワードリポジトリを使用する</string>
|
||||||
<string name="pref_select_external_repository">外部リポジトリを選択</string>
|
<string name="pref_select_external_repository">外部リポジトリを選択</string>
|
||||||
|
|
||||||
<!-- pwgen fragment -->
|
<!-- PasswordGenerator fragment -->
|
||||||
<string name="pwgen_generate">生成</string>
|
<string name="pwgen_generate">生成</string>
|
||||||
<string name="pwgen_include">含める</string>
|
<string name="pwgen_include">含める</string>
|
||||||
<string name="pwgen_length">長さ</string>
|
<string name="pwgen_length">長さ</string>
|
||||||
|
@@ -133,7 +133,7 @@
|
|||||||
<string name="prefs_export_passwords_summary">Экспортировать пароли в открытом виде во внешнее хранилище</string>
|
<string name="prefs_export_passwords_summary">Экспортировать пароли в открытом виде во внешнее хранилище</string>
|
||||||
<string name="prefs_version">Версия</string>
|
<string name="prefs_version">Версия</string>
|
||||||
|
|
||||||
<!-- pwgen fragment -->
|
<!-- PasswordGenerator fragment -->
|
||||||
<string name="pwgen_generate">Сгенерировать</string>
|
<string name="pwgen_generate">Сгенерировать</string>
|
||||||
<string name="pwgen_include">Включая</string>
|
<string name="pwgen_include">Включая</string>
|
||||||
<string name="pwgen_length">Длина</string>
|
<string name="pwgen_length">Длина</string>
|
||||||
|
@@ -172,7 +172,7 @@
|
|||||||
<string name="prefs_export_passwords_summary">Exports the encrypted passwords to an external directory</string>
|
<string name="prefs_export_passwords_summary">Exports the encrypted passwords to an external directory</string>
|
||||||
<string name="prefs_version">Version</string>
|
<string name="prefs_version">Version</string>
|
||||||
|
|
||||||
<!-- pwgen fragment -->
|
<!-- PasswordGenerator fragment -->
|
||||||
<string name="pwgen_title">Generate Password</string>
|
<string name="pwgen_title">Generate Password</string>
|
||||||
<string name="pwgen_generate">Generate</string>
|
<string name="pwgen_generate">Generate</string>
|
||||||
<string name="pwgen_include">Include</string>
|
<string name="pwgen_include">Include</string>
|
||||||
|
Reference in New Issue
Block a user