Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,7 @@
# PdfBox-Android - ignore optional JPEG2000 codec dependencies
-dontwarn com.gemalto.jp2.JP2Decoder
-dontwarn com.gemalto.jp2.JP2Encoder

# Keep RNBridge models to ensure Gson serialization works in release builds
-keep class com.flowfoundation.wallet.reactnative.bridge.RNBridge { *; }
-keep class com.flowfoundation.wallet.reactnative.bridge.RNBridge$* { *; }
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ object ActivityManager {
}
}

@JvmStatic
fun getReactContext(): ReactApplicationContext? {
return reactContextRef?.get()
}

/**
* Clear all references
* Should be called when the application is being destroyed
Expand All @@ -91,4 +96,4 @@ object ActivityManager {
fun hasActivity(): Boolean {
return getCurrentActivity() != null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ object AppConfig {

fun bridgeFeePayer() = if (isTestnet()) config().getBridgeFeePayer().testnet else config().getBridgeFeePayer().mainnet

fun checkBloctoKeyRotation() = isDev() || isTesting() || (config().getFeatures().bloctoKeyRotation ?: false)

fun addressRegistry(network: Int): Map<String, String> {
return when (network) {
NETWORK_TESTNET -> flowAddressRegistry().testnet
Expand Down Expand Up @@ -196,6 +198,10 @@ private data class Features(
val txWarning: Boolean?,
@SerializedName("cover_bridge_fee")
val coverBridgeFee: Boolean?,
@SerializedName("blocto_key_rotation")
val bloctoKeyRotation: Boolean?,
@SerializedName("coa_migration")
val coaMigration: Boolean?
)

private data class Payer(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.flowfoundation.wallet.manager.rotation

import com.flowfoundation.wallet.manager.flow.FlowCadenceApi
import com.flowfoundation.wallet.utils.Env
import com.flowfoundation.wallet.utils.logd
import org.onflow.flow.models.HashingAlgorithm
import org.onflow.flow.models.SigningAlgorithm
import androidx.core.content.edit

class BloctoDetectorService {
data class Result(
val isBlocto: Boolean,
val needRevoke: Boolean,
val revokeKeyIndexes: List<Int>
)

companion object {
private const val PREF_NAME = "blocto_detector_cache"
private const val TAG = "BloctoDetectorService"

private fun cacheKey(address: String): String {
return "blocto.detector.false.${address.lowercase()}"
}

suspend fun detectBloctoKey(address: String): Result {
logd(TAG, "detectBloctoKey() called with address: $address")
val normalized = address.lowercase()
val prefs = Env.getApp().getSharedPreferences(PREF_NAME, 0)
if (prefs.getBoolean(cacheKey(normalized), false)) {
logd(TAG, "detectBloctoKey() - cached result found (not Blocto)")
return Result(isBlocto = false, needRevoke = false, revokeKeyIndexes = emptyList())
}

val account = FlowCadenceApi.getAccount(normalized)
val keys = account.keys ?: emptyList()
logd(TAG, "detectBloctoKey() - fetched ${keys.size} keys for account")

val candidateKeys = keys.filter { key ->
key.signingAlgorithm == SigningAlgorithm.ECDSA_secp256k1 &&
key.hashingAlgorithm == HashingAlgorithm.SHA3_256
}

val hasWeight999 = candidateKeys.any { it.weight.toInt() == 999 }
val hasWeight1 = candidateKeys.any { it.weight.toInt() == 1 }
val isBlocto = hasWeight999 && hasWeight1
val revokeKeyIndexes = candidateKeys
.filter { !it.revoked }
.map { it.index.toInt() }
val needRevoke = isBlocto && revokeKeyIndexes.isNotEmpty()

logd(TAG, "detectBloctoKey() - isBlocto: $isBlocto, needRevoke: $needRevoke, revokeKeyIndexes: $revokeKeyIndexes")

if (!isBlocto) {
prefs.edit { putBoolean(cacheKey(normalized), true) }
}

return Result(
isBlocto = isBlocto,
needRevoke = needRevoke,
revokeKeyIndexes = revokeKeyIndexes
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ import java.util.concurrent.atomic.AtomicReference
import com.flowfoundation.wallet.firebase.auth.firebaseUid
import com.flowfoundation.wallet.manager.account.AccountWalletManager
import com.flowfoundation.wallet.manager.account.KeyStoreMigrationManager
import com.flowfoundation.wallet.manager.config.AppConfig
import com.flowfoundation.wallet.manager.rotation.BloctoDetectorService
import com.flowfoundation.wallet.reactnative.ReactNativeActivity
import com.flowfoundation.wallet.reactnative.bridge.RNBridge

object WalletManager {
private val TAG = WalletManager::class.java.simpleName
Expand All @@ -48,6 +52,9 @@ object WalletManager {
private val initializationLock = Object()
private var isInitialized = false

private var lastRotationCheckTime = 0L
private const val ROTATION_CHECK_COOLDOWN = 2000L

// Add a job reference and callback mechanism
private var initializationJob: kotlinx.coroutines.Job? = null
private val walletReadyCallbacks = mutableListOf<() -> Unit>()
Expand Down Expand Up @@ -468,6 +475,43 @@ object WalletManager {
return networkStr ?: chainNetWorkString()
}

fun checkKeyRotation(activity: android.app.Activity) {
if (AppConfig.checkBloctoKeyRotation().not()) {
return
}
val address = wallet()?.walletAddress()
logd(TAG, "checkKeyRotation() called with address: $address")

if (address.isNullOrBlank()) {
logd(TAG, "checkKeyRotation() - address is blank, returning")
return
}

val currentTime = System.currentTimeMillis()
if (currentTime - lastRotationCheckTime < ROTATION_CHECK_COOLDOWN) {
logd(TAG, "checkKeyRotation() - skipped due to cooldown")
return
}
lastRotationCheckTime = currentTime

if (isChildAccount(address) || EVMWalletManager.isEVMWalletAddress(address)) {
logd(TAG, "checkKeyRotation() - address is child account or EVM address, returning")
return
}

ioScope {
logd(TAG, "checkKeyRotation() - detecting Blocto key...")
val result = BloctoDetectorService.detectBloctoKey(address)
logd(TAG, "checkKeyRotation() - detection result: $result")
if (result.needRevoke) {
logd(TAG, "checkKeyRotation() - need revoke, launching BACKUP_TIP")
uiScope {
ReactNativeActivity.launch(activity, RNBridge.ScreenType.BACKUP_TIP)
}
}
}
}

fun selectedWalletAddress(): String {
val currentTime = System.currentTimeMillis()
if (currentTime - lastAddressCheck < ADDRESS_CACHE_DURATION) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import com.flowfoundation.wallet.page.main.model.MainContentModel
import com.flowfoundation.wallet.page.main.model.MainDrawerLayoutModel
import com.flowfoundation.wallet.page.main.presenter.DrawerLayoutPresenter
import com.flowfoundation.wallet.page.main.presenter.MainContentPresenter
import com.flowfoundation.wallet.BuildConfig
import com.flowfoundation.wallet.page.others.NotificationPermissionActivity
import com.flowfoundation.wallet.page.window.WindowFrame
import com.flowfoundation.wallet.utils.debug.fragments.debugViewer.DebugViewerDataSource
Expand All @@ -33,6 +32,7 @@ import com.flowfoundation.wallet.utils.isRegistered
import com.flowfoundation.wallet.utils.uiScope
import com.instabug.bug.BugReporting
import com.instabug.library.Instabug
import com.flowfoundation.wallet.manager.wallet.WalletManager

class MainActivity : BaseActivity() {

Expand Down Expand Up @@ -72,7 +72,7 @@ class MainActivity : BaseActivity() {
firebaseInformationCheck()
}
contentPresenter.checkAndShowContent()

// Navigate to target tab if specified
if (targetTabIndex >= 0) {
val targetTab = HomeTab.values().find { it.index == targetTabIndex }
Expand All @@ -85,6 +85,7 @@ class MainActivity : BaseActivity() {
NotificationPermissionActivity.launch(this)
}
configurationInstabugBugReport()
WalletManager.checkKeyRotation(this)
}

private fun configurationInstabugBugReport() {
Expand Down Expand Up @@ -170,4 +171,4 @@ class MainActivity : BaseActivity() {

fun getInstance() = INSTANCE
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class PrivateKeyStoreUsernamePresenter(
binding.editText.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
hideStateView()
binding.progressBar.setVisible(true)
binding.progressBar.setVisible(s.isNotEmpty())
binding.nextButton.isEnabled = false
viewModel.verifyUsername(s.toString())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import com.flowfoundation.wallet.page.walletcreate.fragments.mnemonic.MnemonicMo
import com.flowfoundation.wallet.utils.extensions.res2String
import com.flowfoundation.wallet.utils.extensions.setVisible
import com.flowfoundation.wallet.utils.ioScope
import com.flowfoundation.wallet.utils.isNightMode
import com.flowfoundation.wallet.utils.textToClipboard
import com.flowfoundation.wallet.utils.toast
import com.flowfoundation.wallet.wallet.Wallet
import com.flowfoundation.wallet.widgets.itemdecoration.GridSpaceItemDecoration
import com.zackratos.ultimatebarx.ultimatebarx.UltimateBarX
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

Expand All @@ -31,6 +33,8 @@ class SecurityRecoveryActivity : BaseActivity() {
super.onCreate(savedInstanceState)
binding = ActivitySecurityRecoveryBinding.inflate(layoutInflater)
setContentView(binding.root)
UltimateBarX.with(this).fitWindow(true).light(!isNightMode(this)).applyStatusBar()
UltimateBarX.with(this).fitWindow(true).light(!isNightMode(this)).applyNavigationBar()
setupToolbar()
initPhrases()
}
Expand Down Expand Up @@ -90,4 +94,4 @@ class SecurityRecoveryActivity : BaseActivity() {

fun launchIntent(context: Context): Intent = Intent(context, SecurityRecoveryActivity::class.java)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ class WalletCreateUsernameViewModel : ViewModel() {

fun verifyUsername(username: String) {
this.username = username
handler.removeCallbacks(usernameCheckTask)
val verifyMsg = usernameVerify(username)
if (!verifyMsg.isNullOrEmpty()) {
usernameStateLiveData.postValue(Pair(false, verifyMsg))
return
}

handler.removeCallbacks(usernameCheckTask)
handler.postDelayed(usernameCheckTask, 500)
}

Expand Down
Loading