From 2ea32af377180f743a6df8e2efdd435da510050d Mon Sep 17 00:00:00 2001 From: Severiano Jaramillo Date: Wed, 20 Feb 2019 11:03:39 -0600 Subject: [PATCH] Created the methods required to block the security lock PIN option when the user has entered incorrectly the current PIN more times than the max allowed. When that happens the text field to type the PIN gets disabled and a propper message is shown, explaining the issue and also showing the time that needs to pass until he can try again. --- .../fragments/BaseSecurityLockDialog.kt | 84 +++++++++++++++++++ .../fragments/PINSecurityLockDialog.kt | 36 +++++++- .../fragments/PatternSecurityLockDialog.kt | 8 ++ .../bitsybitshareswallet/utils/Constants.kt | 14 ++++ app/src/main/res/values/strings.xml | 2 + 5 files changed, 142 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/BaseSecurityLockDialog.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/BaseSecurityLockDialog.kt index 7659bf5..64dcee6 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/BaseSecurityLockDialog.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/BaseSecurityLockDialog.kt @@ -1,13 +1,16 @@ package cy.agorise.bitsybitshareswallet.fragments import android.os.Bundle +import android.os.CountDownTimer import android.preference.PreferenceManager import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment +import cy.agorise.bitsybitshareswallet.R import cy.agorise.bitsybitshareswallet.utils.Constants import io.reactivex.disposables.CompositeDisposable +import kotlin.math.roundToInt /** * Encapsulates the shared logic required for the PIN and Pattern Security Lock Fragments. @@ -60,6 +63,16 @@ abstract class BaseSecurityLockDialog : DialogFragment() { /** Salt used to hash the current PIN/Pattern */ protected var currentPINPatternSalt: String? = null + /** Current count of incorrect attempts to verify the current security lock */ + protected var incorrectSecurityLockAttempts = 0 + + /** Time of the last lock/disable due to too many incorrect attempts to verify the security lock */ + protected var incorrectSecurityLockTime = 0L + + /** Timer used to update the error message when the user has tried too many incorrect attempts tu enter the + * current security lock option */ + private var mCountDownTimer: CountDownTimer? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -71,6 +84,12 @@ abstract class BaseSecurityLockDialog : DialogFragment() { currentPINPatternSalt = PreferenceManager.getDefaultSharedPreferences(context) .getString(Constants.KEY_PIN_PATTERN_SALT, "") + incorrectSecurityLockAttempts = PreferenceManager.getDefaultSharedPreferences(context) + .getInt(Constants.KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS, 0) + + incorrectSecurityLockTime = PreferenceManager.getDefaultSharedPreferences(context) + .getLong(Constants.KEY_INCORRECT_SECURITY_LOCK_TIME, 0L) + currentStep = arguments?.getInt(KEY_STEP_SECURITY_LOCK) ?: -1 actionIdentifier = arguments?.getInt(KEY_ACTION_IDENTIFIER) ?: -1 @@ -88,6 +107,68 @@ abstract class BaseSecurityLockDialog : DialogFragment() { } } + /** + * Increases the incorrectSecurityLockAttempts counter by one and saves that value into the shared preferences + * to account for cases when the user could try to trick the app by just closing and reopening the dialog. Also, + * stores the current time, so that when the number of attempts is bigger than the maximum allowed, the security + * lock gets locked for a certain amount of time. + */ + protected fun increaseIncorrectSecurityLockAttemptsAndTime() { + val now = System.currentTimeMillis() + + incorrectSecurityLockTime = now + + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putInt(Constants.KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS, ++incorrectSecurityLockAttempts) + .putLong(Constants.KEY_INCORRECT_SECURITY_LOCK_TIME, now) + .apply() + } + + /** + * Resets the values of the incorrectSecurityLockAttempts and Time, both in the local variable as well as the one + * stored in the shared preferences. + */ + protected fun resetIncorrectSecurityLockAttemptsAndTime() { + incorrectSecurityLockTime = 0 + incorrectSecurityLockAttempts = 0 + + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putInt(Constants.KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS, incorrectSecurityLockAttempts) + .putLong(Constants.KEY_INCORRECT_SECURITY_LOCK_TIME, incorrectSecurityLockTime) + .apply() + } + + protected fun startContDownTimer() { + var millis = incorrectSecurityLockTime + Constants.INCORRECT_SECURITY_LOCK_COOLDOWN - System.currentTimeMillis() + millis = millis / 1000 * 1000 + 1000 // Make sure millis account for a whole second multiple + + mCountDownTimer = object : CountDownTimer(millis, 1000) { + override fun onTick(millisUntilFinished: Long) { + val secondsUntilFinished = millisUntilFinished / 1000 + if (secondsUntilFinished < 60) { + // Less than a minute remains + val errorMessage = getString(R.string.error__security_lock_too_many_attempts_seconds, + secondsUntilFinished.toInt()) + onTimerSecondPassed(errorMessage) + } else { + // At least a minute remains + val minutesUntilFinished = (secondsUntilFinished / 60.0).roundToInt() + val errorMessage = getString(R.string.error__security_lock_too_many_attempts_minutes, + minutesUntilFinished) + onTimerSecondPassed(errorMessage) + } + } + + override fun onFinish() { + onTimerFinished() + } + }.start() + } + + abstract fun onTimerSecondPassed(errorMessage: String) + + abstract fun onTimerFinished() + override fun onResume() { super.onResume() @@ -100,5 +181,8 @@ abstract class BaseSecurityLockDialog : DialogFragment() { super.onDestroy() if (!mDisposables.isDisposed) mDisposables.dispose() + + mCountDownTimer?.cancel() + mCountDownTimer = null } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/PINSecurityLockDialog.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/PINSecurityLockDialog.kt index 180c6e5..6ad5442 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/PINSecurityLockDialog.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/PINSecurityLockDialog.kt @@ -46,12 +46,19 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() { if (hashedPIN == currentHashedPINPattern) { // PIN is correct, proceed + resetIncorrectSecurityLockAttemptsAndTime() tietPIN.hideKeyboard() rootView.requestFocus() dismiss() mCallback?.onPINPatternEntered(actionIdentifier) } else { - tilPIN.error = getString(R.string.error__wrong_pin) + increaseIncorrectSecurityLockAttemptsAndTime() + if (incorrectSecurityLockAttempts < Constants.MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS) { + // Show the error only when the user has not reached the max attempts limit, because if that + // is the case another error is gonna be shown in the setupScreen() method + tilPIN.error = getString(R.string.error__wrong_pin) + } + setupScreen() } } else if (currentStep == STEP_SECURITY_LOCK_CREATE) { @@ -88,9 +95,11 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() { mDisposables.add( tietPIN.textChanges() + .skipInitialValue() .observeOn(AndroidSchedulers.mainThread()) .subscribe { - if (currentStep == STEP_SECURITY_LOCK_VERIFY) { + if (currentStep == STEP_SECURITY_LOCK_VERIFY && + incorrectSecurityLockAttempts < Constants.MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS) { // Make sure the error is removed when the user types again tilPIN.isErrorEnabled = false } else if (currentStep == STEP_SECURITY_LOCK_CREATE) { @@ -110,6 +119,21 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() { STEP_SECURITY_LOCK_VERIFY -> { tvTitle.text = getString(R.string.title__re_enter_your_pin) tvSubTitle.text = getString(R.string.msg__enter_your_pin) + if (incorrectSecurityLockAttempts >= Constants.MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS) { + // User has entered the PIN incorrectly too many times + val now = System.currentTimeMillis() + if (now <= incorrectSecurityLockTime + Constants.INCORRECT_SECURITY_LOCK_COOLDOWN) { + tietPIN.setText("") + tietPIN.isEnabled = false + startContDownTimer() + return + } else { + resetIncorrectSecurityLockAttemptsAndTime() + } + } + // This is not in an else statement because we also want to enable the EditText and remove the error + // when the cooldown time has been reached + tietPIN.isEnabled = true tilPIN.helperText = "" tilPIN.isErrorEnabled = false } @@ -129,4 +153,12 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() { } } } + + override fun onTimerSecondPassed(errorMessage: String) { + tilPIN.error = errorMessage + } + + override fun onTimerFinished() { + setupScreen() + } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/PatternSecurityLockDialog.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/PatternSecurityLockDialog.kt index ea41769..f8d7667 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/PatternSecurityLockDialog.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/PatternSecurityLockDialog.kt @@ -167,4 +167,12 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() { } } } + + override fun onTimerSecondPassed(errorMessage: String) { + tvMessage.error = errorMessage + } + + override fun onTimerFinished() { + setupScreen() + } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt index c96345d..4fa421e 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt @@ -25,6 +25,20 @@ object Constants { /** Key used to store the user's selected Security Lock option */ const val KEY_SECURITY_LOCK_SELECTED = "key_security_lock_selected" + /** Maximum allowed number of incorrect attempts to input the current security lock */ + const val MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS = 1 // TODO 5 + + /** Minimum time that the security lock options will be disabled when the user has incorrectly tried to enter + * the current security lock option more than MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS times */ + const val INCORRECT_SECURITY_LOCK_COOLDOWN = 2 * 60L * 1000 // 5 seconds TODO 5L * 60 * 1000 // 5 minutes + + /** Key used to store the consecutive number of times the user has incorrectly tried to enter the + * current security lock option */ + const val KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS = "key_incorrect_security_lock_attempts" + + /** Key used to store the time in millis when the security lock options got locked due to many incorrect attempts */ + const val KEY_INCORRECT_SECURITY_LOCK_TIME = "key_incorrect_security_lock_time" + /** Name of the account passed to the faucet as the referrer */ const val FAUCET_REFERRER = "agorise" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ffbe8f6..990affa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -165,5 +165,7 @@ Wrong pattern Pattern recorded Connect at least 4 dots. Try again. + Too many incorrect attempts. Try again in %1$d minutes. + Too many incorrect attempts. Try again in %1$d seconds.