diff --git a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java index 5a113d63..ffa24408 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java +++ b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java @@ -168,6 +168,10 @@ public class Preferences { return _prefs.getString("pref_backups_error", null); } + public boolean isPinKeyboardEnabled() { + return _prefs.getBoolean("pref_pin_keyboard", false); + } + public boolean isTimeSyncWarningEnabled() { return _prefs.getBoolean("pref_warn_time_sync", true); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java index 9d189fad..9a0102a0 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java @@ -5,6 +5,7 @@ import android.content.Intent; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; +import android.text.InputType; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; @@ -59,6 +60,8 @@ public class AuthActivity extends AegisActivity { private BiometricSlot _bioSlot; private BiometricPrompt _bioPrompt; + private int _failedUnlockAttempts; + // the first time this activity is resumed after creation, it's possible to inhibit showing the // biometric prompt by setting 'inhibitBioPrompt' to false through the intent private boolean _inhibitBioPrompt; @@ -83,6 +86,10 @@ public class AuthActivity extends AegisActivity { return false; }); + if (_prefs.isPinKeyboardEnabled()) { + _textPassword.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + } + Intent intent = getIntent(); _inhibitBioPrompt = savedInstanceState == null ? !intent.getBooleanExtra("_inhibitBioPrompt", true) : savedInstanceState.getBoolean("_inhibitBioPrompt"); _cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction"); @@ -253,6 +260,9 @@ public class AuthActivity extends AegisActivity { } public BiometricPrompt showBiometricPrompt() { + InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(_textPassword.getWindowToken(), 0); + Cipher cipher; try { cipher = _bioSlot.createDecryptCipher(_bioKey); @@ -301,6 +311,21 @@ public class AuthActivity extends AegisActivity { finish(); } + private void onInvalidPassword() { + Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this) + .setTitle(getString(R.string.unlock_vault_error)) + .setMessage(getString(R.string.unlock_vault_error_description)) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword()) + .create()); + + _failedUnlockAttempts ++; + + if (_failedUnlockAttempts >= 3) { + _textPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + } + } + private class PasswordDerivationListener implements PasswordSlotDecryptTask.Callback { @Override public void onTaskFinished(PasswordSlotDecryptTask.Result result) { @@ -316,12 +341,7 @@ public class AuthActivity extends AegisActivity { finish(result.getKey(), result.isSlotRepaired()); } else { - Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this) - .setTitle(getString(R.string.unlock_vault_error)) - .setMessage(getString(R.string.unlock_vault_error_description)) - .setCancelable(false) - .setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword()) - .create()); + onInvalidPassword(); } } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java index 0fc0e827..dccd52e4 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java @@ -183,7 +183,7 @@ public class Dialogs { showSecureDialog(dialog); } - private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, boolean isSecret) { + private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener, boolean isSecret) { View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null); EditText input = view.findViewById(R.id.text_input); if (isSecret) { @@ -198,6 +198,11 @@ public class Dialogs { char[] text = EditTextHelper.getEditTextChars(input); listener.onTextInputResult(text); }); + + if (dismissListener != null) { + builder.setOnDismissListener(dismissListener); + } + if (messageId != 0) { builder.setMessage(messageId); } @@ -207,7 +212,7 @@ public class Dialogs { } private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) { - showTextInputDialog(context, titleId, 0, hintId, listener, isSecret); + showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret); } public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) { @@ -219,7 +224,11 @@ public class Dialogs { } public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) { - showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, true); + showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, null, true); + } + + public static void showPasswordInputDialog(Context context, @StringRes int setPasswordMessageId, @StringRes int messageId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener) { + showTextInputDialog(context, setPasswordMessageId, messageId, R.string.password, listener, dismissListener, true); } public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java index 7506dc0d..776aaca6 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java @@ -36,6 +36,7 @@ import com.beemdevelopment.aegis.importers.DatabaseImporterException; import com.beemdevelopment.aegis.services.NotificationService; import com.beemdevelopment.aegis.ui.models.ImportEntry; import com.beemdevelopment.aegis.ui.preferences.SwitchPreference; +import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask; import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.vault.VaultBackupManager; import com.beemdevelopment.aegis.vault.VaultEntry; @@ -63,6 +64,8 @@ import java.util.concurrent.atomic.AtomicReference; import javax.crypto.Cipher; +import static android.text.TextUtils.isDigitsOnly; + public class PreferencesFragment extends PreferenceFragmentCompat { // activity request codes private static final int CODE_IMPORT = 0; @@ -90,6 +93,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat { private Preference _slotsPreference; private Preference _groupsPreference; private Preference _passwordReminderPreference; + private SwitchPreferenceCompat _pinKeyboardPreference; private SwitchPreferenceCompat _backupsPreference; private Preference _backupsLocationPreference; private Preference _backupsTriggerPreference; @@ -370,6 +374,32 @@ public class PreferencesFragment extends PreferenceFragmentCompat { } }); + _pinKeyboardPreference = findPreference("pref_pin_keyboard"); + _pinKeyboardPreference.setOnPreferenceChangeListener((preference, o) -> { + if ((boolean)o) { + return true; + } + + Dialogs.showPasswordInputDialog(getActivity(), R.string.set_password_confirm, R.string.pin_keyboard_description, password -> { + if (isDigitsOnly(new String(password))) { + List slots = _vault.getCredentials().getSlots().findAll(PasswordSlot.class); + PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password); + new PasswordSlotDecryptTask(getActivity(), new PasswordConfirmationListener()).execute(params); + } else { + setPinKeyboardPreference(false); + Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) + .setTitle(R.string.pin_keyboard_error) + .setMessage(R.string.pin_keyboard_error_description) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, null) + .create()); + } + }, dialog -> { + setPinKeyboardPreference(false); + }); + return false; + }); + _groupsPreference = findPreference("pref_groups"); _groupsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override @@ -767,6 +797,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat { _biometricsPreference.setVisible(encrypted); _slotsPreference.setEnabled(encrypted); _autoLockPreference.setVisible(encrypted); + _pinKeyboardPreference.setVisible(encrypted); if (encrypted) { SlotList slots = _vault.getCredentials().getSlots(); @@ -810,6 +841,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat { startActivityForResult(intent, CODE_BACKUPS); } + private void setPinKeyboardPreference(boolean enable) { + _pinKeyboardPreference.setChecked(enable); + } + private class SetPasswordListener implements Dialogs.SlotListener { @Override public void onSlotResult(Slot slot, Cipher cipher) { @@ -895,4 +930,21 @@ public class PreferencesFragment extends PreferenceFragmentCompat { Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e); } } + + private class PasswordConfirmationListener implements PasswordSlotDecryptTask.Callback { + @Override + public void onTaskFinished(PasswordSlotDecryptTask.Result result) { + if (result != null) { + setPinKeyboardPreference(true); + } else { + Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) + .setTitle(R.string.pin_keyboard_error) + .setMessage(R.string.invalid_password) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, null) + .create()); + setPinKeyboardPreference(false); + } + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2012766..7d0155e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,6 +82,7 @@ Please enter a group name Please enter a number Please confirm the password + The password is incorrect A change in your device\'s security settings has been detected. Please go to \"Aegis -> Settings -> Biometrics\" and re-enable biometric unlock. It\'s been a while since you\'ve entered your password. Do you still remember it? It looks like your Authy tokens are encrypted. Please close Aegis, open Authy and unlock the tokens with your password. Instead, Aegis can also attempt to decrypt your Authy tokens for you, if you enter your password below. @@ -203,6 +204,9 @@ Make tokens easier to distinguish from each other by temporarily highlighting them when tapped Copy tokens when tapped Copy tokens to the clipboard by tapping them + Enter your password to enable the PIN keyboard. Note that this only works if your password only consists of numbers + Error enabling PIN keyboard + It\'s not possible to set PIN keyboard. Your password must only consists of numbers. Selected Dark theme Light theme @@ -281,4 +285,6 @@ Fair Good Strong + Use PIN keyboard on lockscreen + Enable this if you want to enable the PIN keyboard on the lockscreen. This only works for numeric passwords diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 470f5889..e8375d04 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -130,6 +130,13 @@ android:dependency="pref_biometrics" app:iconSpaceReserved="false"/> + +