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.