diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt index ee1c4ed44..c4c028f7b 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt @@ -110,7 +110,6 @@ class API( val configFlow: Flow get() = configRepository.stream - suspend fun tonapiFetch( url: String, options: String diff --git a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt index 8161f748b..1c00bad95 100644 --- a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt +++ b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt @@ -76,7 +76,9 @@ class AccountRepository( private val migrationHelper = RNMigrationHelper(rnLegacy) private val _selectedStateFlow = MutableStateFlow(SelectedState.Initialization) - val selectedStateFlow = _selectedStateFlow.stateIn(scope, SharingStarted.Eagerly, + val selectedStateFlow = _selectedStateFlow.stateIn( + scope, + SharingStarted.Eagerly, SelectedState.Initialization ) val selectedWalletFlow = selectedStateFlow.filterNotNull().filterIsInstance().map { @@ -112,15 +114,13 @@ class AccountRepository( private suspend fun migrationFromRN() = withContext(Dispatchers.IO) { val (selectedId, wallets) = migrationHelper.loadLegacy() - if (wallets.isEmpty()) { - _selectedStateFlow.value = SelectedState.Empty - } else { + if (wallets.isNotEmpty()) { database.insertAccounts(wallets) for (wallet in wallets) { val token = rnLegacy.getTonProof(wallet.id) ?: continue storageSource.setTonProofToken(wallet.publicKey, token) } - setSelectedWallet(selectedId) + storageSource.setSelectedId(selectedId) } } diff --git a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/source/VaultSource.kt b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/source/VaultSource.kt index 79316cdd4..c58a71a4a 100644 --- a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/source/VaultSource.kt +++ b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/source/VaultSource.kt @@ -3,6 +3,7 @@ package com.tonapps.wallet.data.account.source import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit +import com.tonapps.blockchain.ton.extensions.getPrivateKey import com.tonapps.blockchain.ton.extensions.hex import com.tonapps.extensions.getByteArray import com.tonapps.extensions.putByteArray @@ -47,10 +48,7 @@ internal class VaultSource(context: Context) { } fun getPrivateKey(publicKey: PublicKeyEd25519): PrivateKeyEd25519? { - val seed = prefs.getByteArray(privateKey(publicKey)) ?: return null - val privateKey = PrivateKeyEd25519(seed) - seed.clear() - return privateKey + return prefs.getPrivateKey(privateKey(publicKey)) } private fun privateKey(publicKey: PublicKeyEd25519) = key(PRIVATE_KEY_PREFIX, publicKey) diff --git a/apps/wallet/data/backup/src/main/java/com/tonapps/wallet/data/backup/BackupRepository.kt b/apps/wallet/data/backup/src/main/java/com/tonapps/wallet/data/backup/BackupRepository.kt index c07d410eb..f65545abc 100644 --- a/apps/wallet/data/backup/src/main/java/com/tonapps/wallet/data/backup/BackupRepository.kt +++ b/apps/wallet/data/backup/src/main/java/com/tonapps/wallet/data/backup/BackupRepository.kt @@ -30,7 +30,7 @@ class BackupRepository( init { scope.launch(Dispatchers.IO) { if (rnLegacy.isRequestMigration()) { - localDataSource.clear() + // localDataSource.clear() migrationFromRN() } _stream.value = localDataSource.getAllBackups() diff --git a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/BlobDataSource.kt b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/BlobDataSource.kt index 7d7bc75a4..6f0818466 100644 --- a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/BlobDataSource.kt +++ b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/BlobDataSource.kt @@ -3,6 +3,7 @@ package com.tonapps.wallet.data.core import android.content.Context import android.os.Parcelable import android.util.Log +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tonapps.extensions.cacheFolder import com.tonapps.extensions.file import com.tonapps.extensions.toByteArray @@ -47,6 +48,7 @@ abstract class BlobDataSource( val string = String(bytes) fromJSON(string) } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) null } } @@ -73,9 +75,13 @@ abstract class BlobDataSource( } private fun setDiskCache(key: String, value: D) { - val file = diskFile(key) - val bytes = onMarshall(value) - file.writeBytes(bytes) + try { + val file = diskFile(key) + val bytes = onMarshall(value) + file.writeBytes(bytes) + } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) + } } private fun diskFile(key: String): File { @@ -98,6 +104,7 @@ abstract class BlobDataSource( val bytes = file.readBytes() if (bytes.isEmpty()) null else bytes } catch (e: IOException) { + FirebaseCrashlytics.getInstance().recordException(e) null } } diff --git a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/WalletCurrency.kt b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/WalletCurrency.kt index 5a65950ed..041e0db5b 100644 --- a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/WalletCurrency.kt +++ b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/WalletCurrency.kt @@ -22,6 +22,7 @@ data class WalletCurrency( "GBP", // Great Britain Pound "CHF", // Swiss Franc "CNY", // China Yuan + "GEL", // Georgian Lari "KRW", // South Korean Won "IDR", // Indonesian Rupiah "INR", // Indian Rupee diff --git a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/LockScreen.kt b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/LockScreen.kt index 98f6390bc..994fd2ff0 100644 --- a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/LockScreen.kt +++ b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/LockScreen.kt @@ -51,7 +51,7 @@ class LockScreen( } } - fun biometric(result: BiometricPrompt.AuthenticationResult) { + fun biometric() { hide() } diff --git a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeBiometric.kt b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeBiometric.kt index 277f3815a..ea4366fa5 100644 --- a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeBiometric.kt +++ b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeBiometric.kt @@ -5,6 +5,7 @@ import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tonapps.extensions.activity import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume @@ -28,6 +29,7 @@ object PasscodeBiometric { } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + // Yes. Biometric right now is only UI feature. continuation.resume(true) } }) @@ -54,6 +56,7 @@ object PasscodeBiometric { biometricPrompt.authenticate(builder.build()) } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) callback.onAuthenticationError(BiometricPrompt.ERROR_HW_NOT_PRESENT, "Unknown error") } } diff --git a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeHelper.kt b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeHelper.kt index c0bd873eb..e6ebd80bd 100644 --- a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeHelper.kt +++ b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeHelper.kt @@ -1,6 +1,7 @@ package com.tonapps.wallet.data.passcode import android.content.Context +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tonapps.extensions.logError import com.tonapps.wallet.data.account.AccountRepository import com.tonapps.wallet.data.passcode.source.PasscodeStore @@ -47,6 +48,7 @@ class PasscodeHelper( store.setPinCode(code) true } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) context.logError(e) false } diff --git a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeManager.kt b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeManager.kt index cd465691a..ccd45c92a 100644 --- a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeManager.kt +++ b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeManager.kt @@ -1,7 +1,9 @@ package com.tonapps.wallet.data.passcode import android.content.Context +import android.util.Log import androidx.biometric.BiometricPrompt +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tonapps.extensions.logError import com.tonapps.wallet.data.account.AccountRepository import com.tonapps.wallet.data.passcode.dialog.PasscodeDialog @@ -10,11 +12,13 @@ import com.tonapps.wallet.data.settings.SettingsRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uikit.navigation.Navigation -import java.util.concurrent.atomic.AtomicBoolean class PasscodeManager( private val accountRepository: AccountRepository, @@ -30,16 +34,17 @@ class PasscodeManager( get() = lockscreen.stateFlow init { - scope.launch(Dispatchers.IO) { + settingsRepository.isMigratedFlow.onEach { lockscreen.init() - } + }.launchIn(scope) } - fun lockscreenBiometric(result: BiometricPrompt.AuthenticationResult) { - lockscreen.biometric(result) + fun lockscreenBiometric() { + lockscreen.biometric() } fun deleteAll() { + settingsRepository.biometric = false scope.launch { reset() } @@ -77,6 +82,7 @@ class PasscodeManager( migration(context, code) true } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) context.logError(e) false } @@ -97,6 +103,7 @@ class PasscodeManager( } true } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) context.logError(e) false } @@ -108,10 +115,33 @@ class PasscodeManager( } suspend fun reset() = withContext(Dispatchers.IO) { + settingsRepository.lockScreen = false + settingsRepository.biometric = false helper.reset() rnLegacy.clearMnemonic() } + suspend fun confirmationByBiometric( + context: Context, + title: String + ): Boolean = withContext(Dispatchers.Main) { + try { + if (isRequestMigration()) { + val passcode = rnLegacy.exportPasscodeWithBiometry() + if (passcode.isBlank()) { + throw Exception("failed to request passcode") + } + migration(context, passcode) + true + } else { + PasscodeBiometric.showPrompt(context, title) + } + } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) + false + } + } + suspend fun confirmation( context: Context, title: String @@ -146,6 +176,7 @@ class PasscodeManager( } throw Exception("biometry is disabled") } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) return null } } @@ -165,6 +196,7 @@ class PasscodeManager( migration(context, passcode) true } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) context.logError(e) false } @@ -175,10 +207,10 @@ class PasscodeManager( code: String ) = withContext(Dispatchers.Main) { val navigation = Navigation.from(context) - navigation?.toast("...", true, 0) + navigation?.migrationLoader(true) accountRepository.importPrivateKeysFromRNLegacy(code) save(code) - navigation?.toast("...", false, 0) + navigation?.migrationLoader(false) } fun confirmationFlow(context: Context, title: String) = flow { diff --git a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSeedStorage.kt b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSeedStorage.kt index 38999895f..f5c29a6ed 100644 --- a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSeedStorage.kt +++ b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSeedStorage.kt @@ -2,6 +2,7 @@ package com.tonapps.wallet.data.rn import android.content.Context import androidx.fragment.app.FragmentActivity +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tonapps.wallet.data.rn.data.RNVaultState import com.tonapps.wallet.data.rn.expo.SecureStoreModule import com.tonapps.wallet.data.rn.expo.SecureStoreOptions @@ -66,7 +67,12 @@ internal class RNSeedStorage(context: Context) { } suspend fun hasPinCode(): Boolean { - return readState() != null + return try { + readState() != null + } catch (e: Exception) { + FirebaseCrashlytics.getInstance().recordException(e) + false + } } suspend fun removeAll() { @@ -79,7 +85,7 @@ internal class RNSeedStorage(context: Context) { } suspend fun get(passcode: String): RNVaultState = withContext(Dispatchers.IO) { - val state = readState() ?: throw Exception("Seed state is null") + val state = readState() ?: return@withContext RNVaultState() val json = JSONObject(ScryptBox.decrypt(passcode, state)) RNVaultState.of(json) } diff --git a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSql.kt b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSql.kt index ffd66dd49..47e805fd0 100644 --- a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSql.kt +++ b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSql.kt @@ -3,12 +3,13 @@ package com.tonapps.wallet.data.rn import android.content.ContentValues import android.content.Context import android.database.sqlite.SQLiteDatabase +import android.os.Build import android.os.SystemClock import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tonapps.sqlite.SQLiteHelper import org.json.JSONArray import org.json.JSONObject -import java.util.concurrent.ConcurrentHashMap +import android.database.CursorWindow internal class RNSql(context: Context): SQLiteHelper(context, DATABASE_NAME, DATABASE_VERSION) { @@ -27,6 +28,10 @@ internal class RNSql(context: Context): SQLiteHelper(context, DATABASE_NAME, DAT override fun onConfigure(db: SQLiteDatabase) { super.onConfigure(db) db.execSQL("PRAGMA foreign_keys=OFF;") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + db.setMaxSqlCacheSize(SQLiteDatabase.MAX_SQL_CACHE_SIZE) + } + initCursorWindowSize() } fun getValue(key: String): String? { @@ -96,4 +101,14 @@ internal class RNSql(context: Context): SQLiteHelper(context, DATABASE_NAME, DAT setValue(key, value.toString()) } + private fun initCursorWindowSize() { + try { + val field = CursorWindow::class.java.getDeclaredField("sCursorWindowSize") + field.isAccessible = true + field.set(null, 100 * 1024 * 1024) //the 100MB is the new size + } catch (e: Exception) { + FirebaseCrashlytics.getInstance().recordException(e) + } + } + } \ No newline at end of file diff --git a/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt index 8d6f0bf7a..244dc8783 100644 --- a/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt +++ b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map @@ -79,6 +80,9 @@ class SettingsRepository( private val _walletPush = MutableEffectFlow() val walletPush = _walletPush.shareIn(scope, SharingStarted.Eagerly) + private val _isMigratedFlow = MutableStateFlow(null) + val isMigratedFlow = _isMigratedFlow.asStateFlow().filterNotNull() + fun notifyWalletPush() { _walletPush.tryEmit(Unit) } @@ -387,6 +391,7 @@ class SettingsRepository( searchEngine = legacyValues.searchEngine } + _isMigratedFlow.value = true _currencyFlow.tryEmit(currency) _languageFlow.tryEmit(language) _hiddenBalancesFlow.tryEmit(hiddenBalances) diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/workerModule.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/workerModule.kt index de465e2e8..ee6befbeb 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/workerModule.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/workerModule.kt @@ -1,18 +1,13 @@ package com.tonapps.tonkeeper.koin -import android.content.Context -import androidx.work.WorkerParameters -import com.tonapps.tonkeeper.manager.push.PushManager import com.tonapps.tonkeeper.worker.DAppPushToggleWorker import com.tonapps.tonkeeper.worker.PushToggleWorker import com.tonapps.tonkeeper.worker.WidgetUpdaterWorker -import com.tonapps.wallet.data.account.AccountRepository -import org.koin.androidx.workmanager.dsl.worker import org.koin.androidx.workmanager.dsl.workerOf import org.koin.dsl.module val workerModule = module { - worker { DAppPushToggleWorker(get(), get(), get(), get(), get()) } - worker { PushToggleWorker(get(), get(), get(), get()) } - worker { WidgetUpdaterWorker(get(), get(), get(), get(), get(), get(), get()) } + workerOf(::DAppPushToggleWorker) + workerOf(::PushToggleWorker) + workerOf(::WidgetUpdaterWorker) } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt index 65b794156..7ae2141ad 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt @@ -14,12 +14,9 @@ import androidx.core.app.ActivityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding -import com.google.android.play.core.appupdate.AppUpdateManager -import com.google.android.play.core.appupdate.AppUpdateManagerFactory import com.tonapps.blockchain.ton.extensions.base64 import com.tonapps.extensions.currentTimeSeconds import com.tonapps.extensions.toUriOrNull -import com.tonapps.icu.Coins import com.tonapps.tonkeeper.App import com.tonapps.tonkeeper.deeplink.DeepLink import com.tonapps.tonkeeper.extensions.isDarkMode @@ -37,6 +34,7 @@ import com.tonapps.tonkeeper.ui.screen.send.transaction.SendTransactionScreen import com.tonapps.tonkeeper.ui.screen.start.StartScreen import com.tonapps.tonkeeper.ui.screen.tonconnect.TonConnectScreen import com.tonapps.tonkeeperx.R +import com.tonapps.uikit.color.backgroundPageColor import com.tonapps.wallet.api.entity.TokenEntity import com.tonapps.wallet.data.account.entities.WalletEntity import com.tonapps.wallet.data.core.Theme @@ -56,6 +54,8 @@ import uikit.base.BaseFragment import uikit.dialog.alert.AlertDialog import uikit.extensions.collectFlow import uikit.extensions.findFragment +import uikit.extensions.runAnimation +import uikit.extensions.withAlpha class RootActivity: BaseWalletActivity() { @@ -73,6 +73,8 @@ class RootActivity: BaseWalletActivity() { private lateinit var lockView: View private lateinit var lockPasscodeView: PasscodeView private lateinit var lockSignOut: View + private lateinit var migrationLoaderContainer: View + private lateinit var migrationLoaderIcon: View override fun onCreate(savedInstanceState: Bundle?) { val theme = settingsRepository.theme @@ -99,6 +101,10 @@ class RootActivity: BaseWalletActivity() { lockSignOut = findViewById(R.id.lock_sign_out) lockSignOut.setOnClickListener { signOutAll() } + migrationLoaderContainer = findViewById(R.id.migration_loader_container) + migrationLoaderContainer.setOnClickListener { } + migrationLoaderIcon = findViewById(R.id.migration_loader_icon) + ViewCompat.setOnApplyWindowInsetsListener(lockView) { _, insets -> val statusInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars()) val navInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) @@ -108,7 +114,7 @@ class RootActivity: BaseWalletActivity() { collectFlow(viewModel.hasWalletFlow) { init(it) } collectFlow(viewModel.eventFlow) { event(it) } - collectFlow(passcodeManager.lockscreenFlow, ::pinState) + collectFlow(viewModel.lockscreenFlow, ::pinState) App.applyConfiguration(resources.configuration) } @@ -130,7 +136,7 @@ class RootActivity: BaseWalletActivity() { viewModel.disconnectTonConnectBridge() } - private fun pinState(state: LockScreen.State) { + private suspend fun pinState(state: LockScreen.State) { if (state == LockScreen.State.None) { lockView.visibility = View.GONE lockPasscodeView.setSuccess() @@ -138,19 +144,10 @@ class RootActivity: BaseWalletActivity() { lockPasscodeView.setError() } else { lockView.visibility = View.VISIBLE - if (state is LockScreen.State.Biometric) { - PasscodeBiometric.showPrompt(this, getString(Localization.app_name), object : BiometricPrompt.AuthenticationCallback() { - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - passcodeManager.lockscreenBiometric(result) - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - toast(Localization.authorization_required) - } - }) + if (passcodeManager.confirmationByBiometric(this, getString(Localization.app_name))) { + passcodeManager.lockscreenBiometric() + } else { + toast(Localization.authorization_required) } } } @@ -165,6 +162,18 @@ class RootActivity: BaseWalletActivity() { } } + override fun migrationLoader(show: Boolean) { + super.migrationLoader(show) + if (show) { + migrationLoaderContainer.visibility = View.VISIBLE + migrationLoaderContainer.setBackgroundColor(backgroundPageColor.withAlpha(.64f)) + migrationLoaderIcon.runAnimation(R.anim.gear_loading) + } else { + migrationLoaderContainer.visibility = View.GONE + migrationLoaderIcon.clearAnimation() + } + } + override fun isNeedRemoveModals(fragment: BaseFragment): Boolean { if (fragment is QRCameraScreen || fragment is LedgerSignScreen) { return false @@ -301,8 +310,8 @@ class RootActivity: BaseWalletActivity() { builder.setTitle(Localization.sign_out_all_title) builder.setMessage(Localization.sign_out_all_description) builder.setNegativeButton(Localization.sign_out) { - viewModel.signOut() passcodeManager.deleteAll() + viewModel.signOut() setIntroFragment() } builder.setPositiveButton(Localization.cancel) @@ -318,7 +327,9 @@ class RootActivity: BaseWalletActivity() { } private fun setIntroFragment() { - setPrimaryFragment(StartScreen.newInstance()) + setPrimaryFragment(StartScreen.newInstance(), runnable = { + lockView.visibility = View.GONE + }) } private fun setMainFragment() { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt index f211e9830..aa1d035bd 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt @@ -78,6 +78,8 @@ import com.tonapps.wallet.data.browser.BrowserRepository import com.tonapps.wallet.data.core.ScreenCacheSource import com.tonapps.wallet.data.core.entity.SignRequestEntity import com.tonapps.wallet.data.dapps.entities.AppConnectEntity +import com.tonapps.wallet.data.passcode.LockScreen +import com.tonapps.wallet.data.passcode.PasscodeManager import com.tonapps.wallet.data.purchase.PurchaseRepository import com.tonapps.wallet.data.settings.SettingsRepository import com.tonapps.wallet.data.token.TokenRepository @@ -116,6 +118,7 @@ class RootViewModel( private val pushManager: PushManager, private val tokenRepository: TokenRepository, private val environment: Environment, + private val passcodeManager: PasscodeManager, savedStateHandle: SavedStateHandle, ): BaseWalletVM(app) { @@ -135,6 +138,18 @@ class RootViewModel( private val ignoreTonConnectTransaction = mutableListOf() + val lockscreenFlow = combine( + passcodeManager.lockscreenFlow, + accountRepository.selectedStateFlow.filter { it !is AccountRepository.SelectedState.Initialization }.take(1) + ) { lockscreen, state -> + if ((lockscreen is LockScreen.State.Input || lockscreen is LockScreen.State.Biometric) && state !is AccountRepository.SelectedState.Wallet) { + passcodeManager.reset() + LockScreen.State.None + } else { + lockscreen + } + } + init { pushManager.clearNotifications() diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/send/main/SendViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/send/main/SendViewModel.kt index fa2a1c178..b1638a839 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/send/main/SendViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/send/main/SendViewModel.kt @@ -881,6 +881,7 @@ class SendViewModel( } fun sign() = transferFlow.take(1).map { transfer -> + _uiEventFlow.tryEmit(SendEvent.Loading) lastTransferEntity = transfer val excessesAddress = if (sendTransferType is SendTransferType.WithExcessesAddress) { (sendTransferType as SendTransferType.WithExcessesAddress).excessesAddress diff --git a/apps/wallet/instance/app/src/main/res/layout/activity_root.xml b/apps/wallet/instance/app/src/main/res/layout/activity_root.xml index c5da3a755..152019fb4 100644 --- a/apps/wallet/instance/app/src/main/res/layout/activity_root.xml +++ b/apps/wallet/instance/app/src/main/res/layout/activity_root.xml @@ -52,4 +52,20 @@ android:layout_marginHorizontal="@dimen/offsetLarge" android:visibility="gone"/> + + + + + + \ No newline at end of file diff --git a/apps/wallet/instance/main/build.gradle.kts b/apps/wallet/instance/main/build.gradle.kts index a33c0186b..0660ba414 100644 --- a/apps/wallet/instance/main/build.gradle.kts +++ b/apps/wallet/instance/main/build.gradle.kts @@ -24,7 +24,7 @@ android { targetSdk = 34 versionCode = 600 - versionName = "5.0.6" + versionName = "5.0.7" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/apps/wallet/instance/main/proguard-rules.pro b/apps/wallet/instance/main/proguard-rules.pro index 103779326..95ee5d864 100644 --- a/apps/wallet/instance/main/proguard-rules.pro +++ b/apps/wallet/instance/main/proguard-rules.pro @@ -36,5 +36,9 @@ -keep class com.tonapps.tonkeeper.manager.** { *; } +-keep class android.graphics.ColorSpace { *; } +-keep class org.koin.** { *; } +-keep class com.tonapps.tonkeeper.App { *; } + diff --git a/build.gradle.kts b/build.gradle.kts index ed3111883..b133dbdd1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,13 +1,13 @@ import com.android.build.gradle.AppExtension plugins { - id("com.android.application") version "8.7.1" apply false + id("com.android.application") version "8.7.2" apply false id("org.jetbrains.kotlin.android") version "2.0.0" apply false - id("com.android.library") version "8.7.1" apply false + id("com.android.library") version "8.7.2" apply false id("com.google.gms.google-services") version "4.4.2" apply false id("com.google.firebase.crashlytics") version "3.0.2" apply false id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20" - id("com.android.test") version "8.7.1" apply false + id("com.android.test") version "8.7.2" apply false id("androidx.baselineprofile") version "1.3.0" id("org.jetbrains.kotlin.jvm") version "1.9.0" apply false id("com.google.firebase.firebase-perf") version "1.4.2" apply false diff --git a/lib/blockchain/src/main/java/com/tonapps/blockchain/ton/extensions/SharedPreferences.kt b/lib/blockchain/src/main/java/com/tonapps/blockchain/ton/extensions/SharedPreferences.kt new file mode 100644 index 000000000..eba0ce89a --- /dev/null +++ b/lib/blockchain/src/main/java/com/tonapps/blockchain/ton/extensions/SharedPreferences.kt @@ -0,0 +1,34 @@ +package com.tonapps.blockchain.ton.extensions + +import android.content.SharedPreferences +import android.util.Base64 +import com.tonapps.base64.decodeBase64 +import org.ton.api.pk.PrivateKeyEd25519 + +fun SharedPreferences.getPrivateKey(key: String): PrivateKeyEd25519? { + return getPrivateKey2(key) ?: getPrivateKey1(key) +} + +private fun SharedPreferences.getPrivateKey1(key: String): PrivateKeyEd25519? { + try { + val base64 = getString(key, null) + if (base64.isNullOrEmpty()) { + return null + } + return PrivateKeyEd25519(base64.decodeBase64()) + } catch (e: Throwable) { + return null + } +} + +private fun SharedPreferences.getPrivateKey2(key: String): PrivateKeyEd25519? { + try { + val base64 = getString(key, null) + if (base64.isNullOrEmpty()) { + return null + } + return PrivateKeyEd25519(Base64.decode(base64, Base64.DEFAULT)) + } catch (e: Throwable) { + return null + } +} \ No newline at end of file diff --git a/lib/security/src/main/cpp/sodium_lib.cpp b/lib/security/src/main/cpp/sodium_lib.cpp index 1edaa9808..eba61763b 100644 --- a/lib/security/src/main/cpp/sodium_lib.cpp +++ b/lib/security/src/main/cpp/sodium_lib.cpp @@ -25,12 +25,12 @@ extern "C" JNIEXPORT jint JNICALL Java_com_tonapps_security_Sodium_cryptoBoxEasy jbyteArray remote_public_key, jbyteArray local_private_key ) { - jbyte *native_src_plain = env->GetByteArrayElements(src_plain, NULL); - jbyte *native_nonce = env->GetByteArrayElements(nonce, NULL); - jbyte *native_remote_public_key = env->GetByteArrayElements(remote_public_key, NULL); - jbyte *native_local_private_key = env->GetByteArrayElements(local_private_key, NULL); + jbyte *native_src_plain = env->GetByteArrayElements(src_plain, nullptr); + jbyte *native_nonce = env->GetByteArrayElements(nonce, nullptr); + jbyte *native_remote_public_key = env->GetByteArrayElements(remote_public_key, nullptr); + jbyte *native_local_private_key = env->GetByteArrayElements(local_private_key, nullptr); - jbyte *native_dst_cipher = env->GetByteArrayElements(dst_cipher, NULL); + jbyte *native_dst_cipher = env->GetByteArrayElements(dst_cipher, nullptr); int result = crypto_box_easy( reinterpret_cast(native_dst_cipher), @@ -60,12 +60,12 @@ extern "C" JNIEXPORT jint JNICALL Java_com_tonapps_security_Sodium_cryptoBoxOpen jbyteArray remote_public_key, jbyteArray local_private_key ) { - jbyte *native_src_cipher = env->GetByteArrayElements(src_cipher, NULL); - jbyte *native_nonce = env->GetByteArrayElements(nonce, NULL); - jbyte *native_remote_public_key = env->GetByteArrayElements(remote_public_key, NULL); - jbyte *native_local_private_key = env->GetByteArrayElements(local_private_key, NULL); + jbyte *native_src_cipher = env->GetByteArrayElements(src_cipher, nullptr); + jbyte *native_nonce = env->GetByteArrayElements(nonce, nullptr); + jbyte *native_remote_public_key = env->GetByteArrayElements(remote_public_key, nullptr); + jbyte *native_local_private_key = env->GetByteArrayElements(local_private_key, nullptr); - jbyte *native_dst_plain = env->GetByteArrayElements(dst_plain, NULL); + jbyte *native_dst_plain = env->GetByteArrayElements(dst_plain, nullptr); int result = crypto_box_open_easy( reinterpret_cast(native_dst_plain), @@ -91,8 +91,8 @@ extern "C" JNIEXPORT jint JNICALL Java_com_tonapps_security_Sodium_cryptoBoxKeyP jbyteArray remote_public_key, jbyteArray local_private_key ) { - jbyte *native_remote_public_key = env->GetByteArrayElements(remote_public_key, NULL); - jbyte *native_local_private_key = env->GetByteArrayElements(local_private_key, NULL); + jbyte *native_remote_public_key = env->GetByteArrayElements(remote_public_key, nullptr); + jbyte *native_local_private_key = env->GetByteArrayElements(local_private_key, nullptr); int result = crypto_box_keypair( reinterpret_cast(native_remote_public_key), @@ -113,24 +113,24 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tonapps_security_Sodium_argon2I jbyteArray salt, jint outlen ) { - jchar *native_passwd = env->GetCharArrayElements(passwd, NULL); + jchar *native_passwd = env->GetCharArrayElements(passwd, nullptr); jsize passwdlen = env->GetArrayLength(passwd) * 2; if (sodium_mlock(native_passwd, passwdlen) != 0) { env->ReleaseCharArrayElements(passwd, native_passwd, JNI_ABORT); - return NULL; + return nullptr; } - jbyte *native_salt = env->GetByteArrayElements(salt, NULL); + jbyte *native_salt = env->GetByteArrayElements(salt, nullptr); char *out = (char *) malloc(outlen); - if (out == NULL || sodium_mlock(out, outlen) != 0) { + if (out == nullptr || sodium_mlock(out, outlen) != 0) { sodium_memzero(native_passwd, passwdlen); sodium_munlock(native_passwd, passwdlen); env->ReleaseCharArrayElements(passwd, native_passwd, JNI_ABORT); - if (out != NULL) free(out); - return NULL; + if (out != nullptr) free(out); + return nullptr; } int result = crypto_pwhash_argon2id( @@ -151,7 +151,7 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tonapps_security_Sodium_argon2I env->ReleaseCharArrayElements(passwd, native_passwd, JNI_ABORT); env->ReleaseByteArrayElements(salt, native_salt, JNI_ABORT); - return NULL; + return nullptr; } jbyteArray jhash = env->NewByteArray(outlen); @@ -179,35 +179,35 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tonapps_security_Sodium_scryptH jint p, jint dkLen ) { - jbyte *native_password = env->GetByteArrayElements(password, NULL); + jbyte *native_password = env->GetByteArrayElements(password, nullptr); jsize passwordlen = env->GetArrayLength(password); if (sodium_mlock(native_password, passwordlen) != 0) { env->ReleaseByteArrayElements(password, native_password, JNI_ABORT); - return NULL; + return nullptr; } - jbyte *native_salt = env->GetByteArrayElements(salt, NULL); + jbyte *native_salt = env->GetByteArrayElements(salt, nullptr); jsize saltlen = env->GetArrayLength(salt); if (sodium_mlock(native_salt, saltlen) != 0) { sodium_munlock(native_password, passwordlen); env->ReleaseByteArrayElements(password, native_password, JNI_ABORT); env->ReleaseByteArrayElements(salt, native_salt, JNI_ABORT); - return NULL; + return nullptr; } char *out = (char *) malloc(dkLen); - if (out == NULL || sodium_mlock(out, dkLen) != 0) { + if (out == nullptr || sodium_mlock(out, dkLen) != 0) { sodium_memzero(native_password, passwordlen); sodium_munlock(native_password, passwordlen); env->ReleaseByteArrayElements(password, native_password, JNI_ABORT); sodium_memzero(native_salt, saltlen); sodium_munlock(native_salt, saltlen); env->ReleaseByteArrayElements(salt, native_salt, JNI_ABORT); - if (out != NULL) free(out); - return NULL; + if (out != nullptr) free(out); + return nullptr; } int result = crypto_pwhash_scryptsalsa208sha256_ll( @@ -227,7 +227,7 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tonapps_security_Sodium_scryptH sodium_memzero(native_salt, saltlen); sodium_munlock(native_salt, saltlen); env->ReleaseByteArrayElements(salt, native_salt, JNI_ABORT); - return NULL; + return nullptr; } jbyteArray jhash = env->NewByteArray(dkLen); @@ -253,25 +253,25 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tonapps_security_Sodium_cryptoS jbyteArray none, jbyteArray key ) { - jbyte *native_box = env->GetByteArrayElements(box, NULL); + jbyte *native_box = env->GetByteArrayElements(box, nullptr); jsize boxlen = env->GetArrayLength(box); if (sodium_mlock(native_box, boxlen) != 0) { env->ReleaseByteArrayElements(box, native_box, JNI_ABORT); - return NULL; + return nullptr; } - jbyte *native_none = env->GetByteArrayElements(none, NULL); + jbyte *native_none = env->GetByteArrayElements(none, nullptr); jsize nonelen = env->GetArrayLength(none); if (sodium_mlock(native_none, nonelen) != 0) { sodium_munlock(native_box, boxlen); env->ReleaseByteArrayElements(box, native_box, JNI_ABORT); env->ReleaseByteArrayElements(none, native_none, JNI_ABORT); - return NULL; + return nullptr; } - jbyte *native_key = env->GetByteArrayElements(key, NULL); + jbyte *native_key = env->GetByteArrayElements(key, nullptr); jsize keylen = env->GetArrayLength(key); if (sodium_mlock(native_key, keylen) != 0) { @@ -280,12 +280,12 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tonapps_security_Sodium_cryptoS sodium_munlock(native_none, nonelen); env->ReleaseByteArrayElements(none, native_none, JNI_ABORT); env->ReleaseByteArrayElements(key, native_key, JNI_ABORT); - return NULL; + return nullptr; } char *out = (char *) malloc(boxlen); - if (out == NULL || sodium_mlock(out, boxlen) != 0) { + if (out == nullptr || sodium_mlock(out, boxlen) != 0) { sodium_memzero(native_box, boxlen); sodium_munlock(native_box, boxlen); env->ReleaseByteArrayElements(box, native_box, JNI_ABORT); @@ -295,8 +295,8 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tonapps_security_Sodium_cryptoS sodium_memzero(native_key, keylen); sodium_munlock(native_key, keylen); env->ReleaseByteArrayElements(key, native_key, JNI_ABORT); - if (out != NULL) free(out); - return NULL; + if (out != nullptr) free(out); + return nullptr; } int result = crypto_secretbox_open_easy( @@ -320,7 +320,7 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tonapps_security_Sodium_cryptoS sodium_memzero(native_key, keylen); sodium_munlock(native_key, keylen); env->ReleaseByteArrayElements(key, native_key, JNI_ABORT); - return NULL; + return nullptr; } jbyteArray jplain = env->NewByteArray(boxlen); @@ -345,58 +345,88 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tonapps_security_Sodium_cryptoS extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tonapps_security_Sodium_cryptoSecretbox( JNIEnv *env, - jobject, + jobject /* this */, jbyteArray message, jbyteArray nonce, jbyteArray key ) { - jbyte *native_message = env->GetByteArrayElements(message, NULL); - jsize messagelen = env->GetArrayLength(message); - - jbyte *native_nonce = env->GetByteArrayElements(nonce, NULL); - jsize noncelen = env->GetArrayLength(nonce); + if (!message || !nonce || !key) { + return nullptr; + } - jbyte *native_key = env->GetByteArrayElements(key, NULL); - jsize keylen = env->GetArrayLength(key); + const jsize messagelen = env->GetArrayLength(message); + const jsize noncelen = env->GetArrayLength(nonce); + const jsize keylen = env->GetArrayLength(key); if (noncelen != crypto_secretbox_NONCEBYTES || keylen != crypto_secretbox_KEYBYTES) { - env->ReleaseByteArrayElements(message, native_message, JNI_ABORT); - env->ReleaseByteArrayElements(nonce, native_nonce, JNI_ABORT); - env->ReleaseByteArrayElements(key, native_key, JNI_ABORT); - return NULL; + return nullptr; + } + + class ByteArrayGuard { + public: + ByteArrayGuard(JNIEnv* env, jbyteArray array) + : env_(env), array_(array), data_(nullptr) { + if (array) { + data_ = env->GetByteArrayElements(array, nullptr); + } + } + ~ByteArrayGuard() { + if (data_) { + env_->ReleaseByteArrayElements(array_, data_, JNI_ABORT); + } + } + jbyte* get() { return data_; } + private: + JNIEnv* env_; + jbyteArray array_; + jbyte* data_; + }; + + ByteArrayGuard messageGuard(env, message); + ByteArrayGuard nonceGuard(env, nonce); + ByteArrayGuard keyGuard(env, key); + + if (!messageGuard.get() || !nonceGuard.get() || !keyGuard.get()) { + return nullptr; } - jsize cipherlen = crypto_secretbox_MACBYTES + messagelen; - unsigned char *cipher = (unsigned char *) sodium_malloc(cipherlen); + const jsize cipherlen = crypto_secretbox_MACBYTES + messagelen; + auto* cipher = static_cast(sodium_malloc(cipherlen)); - if (cipher == NULL) { - env->ReleaseByteArrayElements(message, native_message, JNI_ABORT); - env->ReleaseByteArrayElements(nonce, native_nonce, JNI_ABORT); - env->ReleaseByteArrayElements(key, native_key, JNI_ABORT); - return NULL; + if (!cipher) { + return nullptr; } - int result = crypto_secretbox_easy( + struct CipherGuard { + explicit CipherGuard(unsigned char* ptr) : ptr_(ptr) {} + ~CipherGuard() { + if (ptr_) { + sodium_free(ptr_); + } + } + unsigned char* get() { return ptr_; } + private: + unsigned char* ptr_; + } cipherGuard(cipher); + + const int result = crypto_secretbox_easy( cipher, - reinterpret_cast(native_message), + reinterpret_cast(messageGuard.get()), messagelen, - reinterpret_cast(native_nonce), - reinterpret_cast(native_key) + reinterpret_cast(nonceGuard.get()), + reinterpret_cast(keyGuard.get()) ); - env->ReleaseByteArrayElements(message, native_message, JNI_ABORT); - env->ReleaseByteArrayElements(nonce, native_nonce, JNI_ABORT); - env->ReleaseByteArrayElements(key, native_key, JNI_ABORT); - if (result != 0) { - sodium_free(cipher); - return NULL; + return nullptr; } jbyteArray jcipher = env->NewByteArray(cipherlen); - env->SetByteArrayRegion(jcipher, 0, cipherlen, reinterpret_cast(cipher)); + if (!jcipher) { + return nullptr; + } - sodium_free(cipher); + env->SetByteArrayRegion(jcipher, 0, cipherlen, reinterpret_cast(cipher)); return jcipher; } \ No newline at end of file diff --git a/lib/security/src/main/java/com/tonapps/security/Security.kt b/lib/security/src/main/java/com/tonapps/security/Security.kt index 8b6e4935b..e3ead155d 100644 --- a/lib/security/src/main/java/com/tonapps/security/Security.kt +++ b/lib/security/src/main/java/com/tonapps/security/Security.kt @@ -10,6 +10,7 @@ import android.os.Build import android.provider.Settings import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties +import android.util.Log import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKeys import kotlinx.coroutines.withTimeout @@ -41,6 +42,7 @@ object Security { @Synchronized fun pref(context: Context, keyAlias: String, name: String): SharedPreferences { + Log.d("SecurityPrefLog", "pref: $keyAlias") KeyHelper.createIfNotExists(keyAlias) return EncryptedSharedPreferences.create( @@ -50,6 +52,8 @@ object Security { EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) + + // UserNotAuthenticatedException } fun generatePrivateKey(keySize: Int): SecretKey { diff --git a/ui/uikit/core/src/main/java/uikit/navigation/Navigation.kt b/ui/uikit/core/src/main/java/uikit/navigation/Navigation.kt index 8973b8afa..cbbb199b9 100644 --- a/ui/uikit/core/src/main/java/uikit/navigation/Navigation.kt +++ b/ui/uikit/core/src/main/java/uikit/navigation/Navigation.kt @@ -66,4 +66,8 @@ interface Navigation { fun run(fragment: BaseFragment) { } + + fun migrationLoader(show: Boolean) { + + } }