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.

This commit is contained in:
Severiano Jaramillo 2019-02-20 11:03:39 -06:00
parent 97d9e8bcfb
commit 2ea32af377
5 changed files with 142 additions and 2 deletions

View file

@ -1,13 +1,16 @@
package cy.agorise.bitsybitshareswallet.fragments package cy.agorise.bitsybitshareswallet.fragments
import android.os.Bundle import android.os.Bundle
import android.os.CountDownTimer
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Constants
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import kotlin.math.roundToInt
/** /**
* Encapsulates the shared logic required for the PIN and Pattern Security Lock Fragments. * 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 */ /** Salt used to hash the current PIN/Pattern */
protected var currentPINPatternSalt: String? = null 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -71,6 +84,12 @@ abstract class BaseSecurityLockDialog : DialogFragment() {
currentPINPatternSalt = PreferenceManager.getDefaultSharedPreferences(context) currentPINPatternSalt = PreferenceManager.getDefaultSharedPreferences(context)
.getString(Constants.KEY_PIN_PATTERN_SALT, "") .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 currentStep = arguments?.getInt(KEY_STEP_SECURITY_LOCK) ?: -1
actionIdentifier = arguments?.getInt(KEY_ACTION_IDENTIFIER) ?: -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() { override fun onResume() {
super.onResume() super.onResume()
@ -100,5 +181,8 @@ abstract class BaseSecurityLockDialog : DialogFragment() {
super.onDestroy() super.onDestroy()
if (!mDisposables.isDisposed) mDisposables.dispose() if (!mDisposables.isDisposed) mDisposables.dispose()
mCountDownTimer?.cancel()
mCountDownTimer = null
} }
} }

View file

@ -46,13 +46,20 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
if (hashedPIN == currentHashedPINPattern) { if (hashedPIN == currentHashedPINPattern) {
// PIN is correct, proceed // PIN is correct, proceed
resetIncorrectSecurityLockAttemptsAndTime()
tietPIN.hideKeyboard() tietPIN.hideKeyboard()
rootView.requestFocus() rootView.requestFocus()
dismiss() dismiss()
mCallback?.onPINPatternEntered(actionIdentifier) mCallback?.onPINPatternEntered(actionIdentifier)
} else { } else {
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) tilPIN.error = getString(R.string.error__wrong_pin)
} }
setupScreen()
}
} else if (currentStep == STEP_SECURITY_LOCK_CREATE) { } else if (currentStep == STEP_SECURITY_LOCK_CREATE) {
// The user is trying to create a new PIN // The user is trying to create a new PIN
@ -88,9 +95,11 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
mDisposables.add( mDisposables.add(
tietPIN.textChanges() tietPIN.textChanges()
.skipInitialValue()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .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 // Make sure the error is removed when the user types again
tilPIN.isErrorEnabled = false tilPIN.isErrorEnabled = false
} else if (currentStep == STEP_SECURITY_LOCK_CREATE) { } else if (currentStep == STEP_SECURITY_LOCK_CREATE) {
@ -110,6 +119,21 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
STEP_SECURITY_LOCK_VERIFY -> { STEP_SECURITY_LOCK_VERIFY -> {
tvTitle.text = getString(R.string.title__re_enter_your_pin) tvTitle.text = getString(R.string.title__re_enter_your_pin)
tvSubTitle.text = getString(R.string.msg__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.helperText = ""
tilPIN.isErrorEnabled = false tilPIN.isErrorEnabled = false
} }
@ -129,4 +153,12 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
} }
} }
} }
override fun onTimerSecondPassed(errorMessage: String) {
tilPIN.error = errorMessage
}
override fun onTimerFinished() {
setupScreen()
}
} }

View file

@ -167,4 +167,12 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
} }
} }
} }
override fun onTimerSecondPassed(errorMessage: String) {
tvMessage.error = errorMessage
}
override fun onTimerFinished() {
setupScreen()
}
} }

View file

@ -25,6 +25,20 @@ object Constants {
/** Key used to store the user's selected Security Lock option */ /** Key used to store the user's selected Security Lock option */
const val KEY_SECURITY_LOCK_SELECTED = "key_security_lock_selected" 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 */ /** Name of the account passed to the faucet as the referrer */
const val FAUCET_REFERRER = "agorise" const val FAUCET_REFERRER = "agorise"

View file

@ -165,5 +165,7 @@
<string name="error__wront_pattern">Wrong pattern</string> <string name="error__wront_pattern">Wrong pattern</string>
<string name="text__pattern_recorded">Pattern recorded</string> <string name="text__pattern_recorded">Pattern recorded</string>
<string name="error__connect_at_least_4_dots">Connect at least 4 dots. Try again.</string> <string name="error__connect_at_least_4_dots">Connect at least 4 dots. Try again.</string>
<string name="error__security_lock_too_many_attempts_minutes">Too many incorrect attempts. Try again in %1$d minutes.</string>
<string name="error__security_lock_too_many_attempts_seconds">Too many incorrect attempts. Try again in %1$d seconds.</string>
</resources> </resources>