Updated on 2026-08-14
This commit is contained in:
parent
54cd8b7cd7
commit
1563b48e8a
14 changed files with 485 additions and 34 deletions
|
|
@ -5,6 +5,7 @@ import com.tangem.data.wallets.DefaultWalletsRepository
|
|||
import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository
|
||||
import com.tangem.data.wallets.derivations.DefaultDerivationsRepository
|
||||
import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository
|
||||
import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
|
|
@ -13,6 +14,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore
|
|||
import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -68,4 +70,10 @@ internal interface WalletsDataBindsModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindColdMapDerivationsRepository(impl: DefaultColdMapDerivationsRepository): ColdMapDerivationsRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindHotWalletAccessCodeAttemptsRepository(
|
||||
impl: DefaultHotWalletAccessCodeAttemptsRepository,
|
||||
): HotWalletAccessCodeAttemptsRepository
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package com.tangem.data.wallets.hot
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.provider.Settings
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.ATTEMPTS_BEFORE_DELETION
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.COOLDOWN_SECONDS
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_ATTEMPTS_BEFORE_DELETION
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : HotWalletAccessCodeAttemptsRepository {
|
||||
|
||||
override suspend fun incrementAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId) {
|
||||
val attemptsKey = PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())
|
||||
|
||||
appPreferencesStore.editData { preferences ->
|
||||
val currentAttempts = preferences[attemptsKey] ?: 0
|
||||
val newAttempts = currentAttempts + 1
|
||||
|
||||
preferences[attemptsKey] = newAttempts
|
||||
val currentBootCount = currentBootCount()
|
||||
preferences[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] = currentBootCount
|
||||
|
||||
if (newAttempts >= MAX_FAST_FORWARD_ATTEMPTS) {
|
||||
val currentDeadline = SystemClock.elapsedRealtime() + COOLDOWN_SECONDS * 1000
|
||||
preferences[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] = currentDeadline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resetAttempts(hotWalletId: HotWalletId) {
|
||||
val authAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId(
|
||||
hotWalletId = hotWalletId,
|
||||
auth = true,
|
||||
)
|
||||
val noAuthAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId(
|
||||
hotWalletId = hotWalletId,
|
||||
auth = false,
|
||||
)
|
||||
|
||||
appPreferencesStore.editData {
|
||||
it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey()))
|
||||
it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(noAuthAttemptId.attemptIdKey()))
|
||||
it.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey()))
|
||||
it.remove(PreferencesKeys.getHotWalletUnlockBootKey(noAuthAttemptId.attemptIdKey()))
|
||||
it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey()))
|
||||
it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(noAuthAttemptId.attemptIdKey()))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun getAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Flow<Attempts> {
|
||||
val flow = appPreferencesStore.data.map {
|
||||
AttemptsPersistentData(
|
||||
attempts = it[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0,
|
||||
bootCount = it[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0,
|
||||
deadline = it[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L,
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
return flow.transformLatest {
|
||||
while (true) {
|
||||
emit(toState(id, it.attempts, it.deadline, it.bootCount))
|
||||
val remaining = remainingSeconds(it.deadline, it.bootCount)
|
||||
if (remaining <= 0) break
|
||||
delay(timeMillis = 1000)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
override suspend fun getAttemptsSync(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Attempts {
|
||||
val prefs = appPreferencesStore.data.first()
|
||||
val count = prefs[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0
|
||||
val boot = prefs[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0
|
||||
val deadline = prefs[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L
|
||||
return toState(id, count, deadline, boot)
|
||||
}
|
||||
|
||||
private fun remainingSeconds(deadline: Long, bootStored: Int): Int {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val bootNow = currentBootCount()
|
||||
if (bootNow != bootStored) {
|
||||
// If the boot happened after the last attempt, we consider timer to start from the beginning
|
||||
return maxOf(0, COOLDOWN_SECONDS - (now / 1000).toInt())
|
||||
}
|
||||
return maxOf(0, ((deadline - now) / 1000).toInt())
|
||||
}
|
||||
|
||||
private fun toState(
|
||||
id: HotWalletAccessCodeAttemptsRepository.AttemptId,
|
||||
count: Int,
|
||||
deadlineElapsed: Long,
|
||||
bootStored: Int,
|
||||
): Attempts {
|
||||
val fast = MAX_FAST_FORWARD_ATTEMPTS
|
||||
val attention = ATTEMPTS_BEFORE_DELETION
|
||||
val deletion = MAX_ATTEMPTS_BEFORE_DELETION
|
||||
|
||||
return when {
|
||||
count < fast -> Attempts.FastForward(count)
|
||||
id.auth && count >= deletion -> Attempts.Deletion
|
||||
id.auth && count >= attention -> {
|
||||
val remaining = remainingSeconds(deadlineElapsed, bootStored)
|
||||
Attempts.BeforeDeletion(count, remaining, deletion - count)
|
||||
}
|
||||
else -> {
|
||||
val remaining = remainingSeconds(deadlineElapsed, bootStored)
|
||||
Attempts.WithDelay(count, remaining)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun HotWalletAccessCodeAttemptsRepository.AttemptId.attemptIdKey(): String {
|
||||
return "${hotWalletId.value}_$auth"
|
||||
}
|
||||
|
||||
private fun currentBootCount(): Int = Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT, 0)
|
||||
|
||||
private data class AttemptsPersistentData(
|
||||
val attempts: Int,
|
||||
val bootCount: Int,
|
||||
val deadline: Long,
|
||||
)
|
||||
}
|
||||
|
|
@ -33,10 +33,16 @@ class HotWalletAccessor @Inject constructor(
|
|||
|
||||
val auth = when (hotWalletId.authType) {
|
||||
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
|
||||
HotWalletId.AuthType.Password -> requestPassword(false)
|
||||
HotWalletId.AuthType.Password -> requestPassword(
|
||||
hotWalletId = hotWalletId,
|
||||
hasBiometry = false,
|
||||
)
|
||||
HotWalletId.AuthType.Biometry -> {
|
||||
if (isAccessCodeRequired) {
|
||||
requestPassword(false)
|
||||
requestPassword(
|
||||
hotWalletId = hotWalletId,
|
||||
hasBiometry = false,
|
||||
)
|
||||
} else {
|
||||
HotAuth.Biometry
|
||||
}
|
||||
|
|
@ -56,6 +62,7 @@ class HotWalletAccessor @Inject constructor(
|
|||
block: suspend (auth: HotAuth) -> T,
|
||||
): T {
|
||||
return runCatchingWrongPassInternal(
|
||||
hotWalletId = hotWalletId,
|
||||
originalAuth = auth,
|
||||
auth = auth,
|
||||
block = { blockAuth ->
|
||||
|
|
@ -97,6 +104,7 @@ class HotWalletAccessor @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun <T> runCatchingWrongPassInternal(
|
||||
hotWalletId: HotWalletId,
|
||||
originalAuth: HotAuth,
|
||||
auth: HotAuth,
|
||||
block: suspend (auth: HotAuth) -> T,
|
||||
|
|
@ -105,9 +113,13 @@ class HotWalletAccessor @Inject constructor(
|
|||
}.getOrElse { exception ->
|
||||
if (auth is HotAuth.Biometry && exception.isBiometryError()) {
|
||||
// fallback to password if biometry fails
|
||||
val passAuth = requestPassword(true)
|
||||
val passAuth = requestPassword(
|
||||
hotWalletId = hotWalletId,
|
||||
hasBiometry = true,
|
||||
)
|
||||
|
||||
return@getOrElse runCatchingWrongPassInternal(
|
||||
hotWalletId = hotWalletId,
|
||||
originalAuth = originalAuth,
|
||||
auth = passAuth,
|
||||
block = block,
|
||||
|
|
@ -121,17 +133,28 @@ class HotWalletAccessor @Inject constructor(
|
|||
// If the exception is a wrong password, we need to request the password again
|
||||
|
||||
hotWalletPasswordRequester.wrongPassword()
|
||||
val passResult = requestPassword(originalAuth is HotAuth.Biometry)
|
||||
val passResult = requestPassword(
|
||||
hotWalletId = hotWalletId,
|
||||
hasBiometry = originalAuth is HotAuth.Biometry,
|
||||
)
|
||||
|
||||
runCatchingWrongPassInternal(
|
||||
hotWalletId = hotWalletId,
|
||||
originalAuth = originalAuth,
|
||||
auth = passResult,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun requestPassword(hasBiometry: Boolean): HotAuth {
|
||||
return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled()
|
||||
private suspend fun requestPassword(hotWalletId: HotWalletId, hasBiometry: Boolean): HotAuth {
|
||||
val attemptRequest = HotWalletPasswordRequester.AttemptRequest(
|
||||
hotWalletId = hotWalletId,
|
||||
authMode = false,
|
||||
hasBiometry = hasBiometry,
|
||||
)
|
||||
|
||||
return hotWalletPasswordRequester.requestPassword(attemptRequest).toAuth()
|
||||
?: throw TangemSdkError.UserCancelled()
|
||||
}
|
||||
|
||||
private fun Throwable.isBiometryError(): Boolean {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue