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.