package sh.sar.basedbank import android.content.Intent import android.os.Bundle import android.util.Base64 import android.view.View import android.widget.LinearLayout import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.lifecycleScope import com.google.android.material.button.MaterialButton import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import sh.sar.basedbank.databinding.ActivityLockBinding import sh.sar.basedbank.ui.home.HomeActivity import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.ThemeHelper import sh.sar.basedbank.BasedBankApp import javax.crypto.SecretKeyFactory import javax.crypto.spec.PBEKeySpec class LockActivity : AppCompatActivity() { private lateinit var binding: ActivityLockBinding private val pinDigits = mutableListOf() private lateinit var method: String private lateinit var salt: String private lateinit var storedHash: String private var biometricsEnabled = false private var autoUnlockPin = false private var pinLength = 4 private var isVerifying = false private val lockPrefs get() = getSharedPreferences("lock_attempts", MODE_PRIVATE) companion object { private const val MAX_ATTEMPTS = 5 private const val LOCKOUT_MS = 30_000L const val EXTRA_RESUME = "resume" } override fun onCreate(savedInstanceState: Bundle?) { ThemeHelper.applyAccent(this) super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) binding = ActivityLockBinding.inflate(layoutInflater) setContentView(binding.root) val isLight = (resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) == android.content.res.Configuration.UI_MODE_NIGHT_NO WindowCompat.getInsetsController(window, window.decorView).apply { isAppearanceLightStatusBars = isLight isAppearanceLightNavigationBars = isLight } val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground)) window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK) ta.recycle() ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets -> val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) view.setPadding(bars.left, bars.top, bars.right, bars.bottom) insets } val prefs = getSharedPreferences("prefs", MODE_PRIVATE) method = prefs.getString("security_method", "pin") ?: "pin" biometricsEnabled = prefs.getBoolean("biometrics_enabled", false) autoUnlockPin = prefs.getBoolean("auto_unlock_pin", false) pinLength = prefs.getInt("pin_length", 4) val stored = CredentialStore(this).loadSecurityHash() ?: run { finish(); return } salt = stored.first storedHash = stored.second if (method == "pin") { binding.viewPin.visibility = View.VISIBLE buildNumpad() updateDots() if (biometricsEnabled) binding.btnLockBiometricFromPin.visibility = View.VISIBLE binding.btnLockBiometricFromPin.setOnClickListener { triggerBiometric() } } else { binding.viewPattern.visibility = View.VISIBLE if (biometricsEnabled) binding.btnLockBiometricFromPattern.visibility = View.VISIBLE binding.btnLockBiometricFromPattern.setOnClickListener { triggerBiometric() } binding.lockPatternView.onPatternComplete = { pattern -> verifyPattern(pattern) } } if (biometricsEnabled) triggerBiometric() onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { moveTaskToBack(true) } }) } private fun buildNumpad() { val dp = resources.displayMetrics.density val btnSize = (68 * dp).toInt() val btnMarginH = (10 * dp).toInt() val rowMarginV = (6 * dp).toInt() val rows = listOf( listOf("1", "2", "3"), listOf("4", "5", "6"), listOf("7", "8", "9"), listOf("⌫", "0", "✓") ) rows.forEach { keys -> val row = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL gravity = android.view.Gravity.CENTER layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT ).also { it.setMargins(0, rowMarginV, 0, rowMarginV) } } keys.forEach { key -> val style = if (key == "✓") com.google.android.material.R.attr.materialButtonStyle else com.google.android.material.R.attr.materialButtonOutlinedStyle val btn = MaterialButton(this, null, style).apply { text = key textSize = 24f insetTop = 0; insetBottom = 0 minimumWidth = 0; minimumHeight = 0 cornerRadius = btnSize / 2 layoutParams = LinearLayout.LayoutParams(btnSize, btnSize) .also { it.setMargins(btnMarginH, 0, btnMarginH, 0) } } btn.setOnClickListener { handleKey(key) } row.addView(btn) } binding.lockNumpadContainer.addView(row) } } private fun handleKey(key: String) { if (isVerifying) return when (key) { "⌫" -> if (pinDigits.isNotEmpty()) { pinDigits.removeLast(); updateDots() } "✓" -> if (pinDigits.size >= 4) verifyPin() else -> if (pinDigits.size < 8) { pinDigits.add(key.toInt()) updateDots() if (autoUnlockPin && pinDigits.size == pinLength) verifyPin() } } } private fun updateDots() { val n = pinDigits.size val total = if (autoUnlockPin) pinLength else maxOf(n, 4) binding.tvLockPinDots.text = "●".repeat(n) + "○".repeat(maxOf(total - n, 0)) } private fun verifyPin() { if (checkAndShowLockout()) return val entered = pinDigits.joinToString("") pinDigits.clear() updateDots() isVerifying = true lifecycleScope.launch { val ok = withContext(Dispatchers.Default) { verify(entered) } isVerifying = false if (ok) { resetFailures() proceed() } else { showFailure() } } } private fun verifyPattern(pattern: List) { if (pattern.size < 4) { binding.lockPatternView.showError() binding.tvPatternHint.text = getString(R.string.pattern_min_dots) return } if (checkAndShowLockout()) { binding.lockPatternView.showError() return } val entered = pattern.joinToString("") isVerifying = true lifecycleScope.launch { val ok = withContext(Dispatchers.Default) { verify(entered) } isVerifying = false if (ok) { resetFailures() proceed() } else { binding.lockPatternView.showError() val msg = failureMessage() binding.tvPatternHint.text = msg binding.root.postDelayed({ binding.tvPatternHint.text = "" }, 1500) } } } /** Returns true and shows the lockout message if currently locked out. */ private fun checkAndShowLockout(): Boolean { val remaining = lockoutRemainingMs() if (remaining <= 0) return false val secs = ((remaining + 999L) / 1000L).toInt() val msg = getString(R.string.unlock_locked_out, secs) binding.tvPinHint.text = msg binding.root.postDelayed({ binding.tvPinHint.text = ""; updateDots() }, remaining) return true } private fun showFailure() { val msg = failureMessage() binding.tvPinHint.text = msg binding.root.postDelayed({ binding.tvPinHint.text = ""; updateDots() }, 1200) } private fun failureMessage(): String { val fails = incrementFailures() val left = MAX_ATTEMPTS - fails return if (left <= 0) { val secs = (LOCKOUT_MS / 1000L).toInt() getString(R.string.unlock_locked_out, secs) } else { getString(R.string.unlock_attempts_remaining, left) } } private fun verify(input: String): Boolean { if (storedHash.isBlank()) return false val saltBytes = Base64.decode(salt, Base64.NO_WRAP) return pbkdf2(input, saltBytes) == storedHash } private fun triggerBiometric() { val canAuth = BiometricManager.from(this) .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) if (canAuth != BiometricManager.BIOMETRIC_SUCCESS) return val prompt = BiometricPrompt( this, ContextCompat.getMainExecutor(this), object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { resetFailures() proceed() } override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { // User cancelled or error — fall back to PIN/pattern (already visible) } } ) val info = BiometricPrompt.PromptInfo.Builder() .setTitle(getString(R.string.unlock_app)) .setSubtitle(getString(R.string.biometric_prompt_subtitle)) .setNegativeButtonText(getString(R.string.biometric_negative_btn)) .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK) .build() prompt.authenticate(info) } private fun proceed() { (application as BasedBankApp).isUnlocked = true if (intent.getBooleanExtra(EXTRA_RESUME, false)) { finish() } else { val store = CredentialStore(this) val hasCredentials = store.hasMibCredentials() || store.hasBmlCredentials() || store.hasFahipayCredentials() if (!hasCredentials) { startActivity(Intent(this, sh.sar.basedbank.ui.login.LoginActivity::class.java)) finish() return } val navDest = intent.getIntExtra("nav_destination", -1) val autoScan = intent.getBooleanExtra("auto_scan", false) startActivity(Intent(this, HomeActivity::class.java).apply { if (navDest != -1) putExtra("nav_destination", navDest) if (autoScan) putExtra("auto_scan", true) }) finish() } } // ── Brute-force tracking ────────────────────────────────────────────────── private fun incrementFailures(): Int { val fails = lockPrefs.getInt("fail_count", 0) + 1 lockPrefs.edit() .putInt("fail_count", fails) .putLong("last_fail_time", System.currentTimeMillis()) .apply() return fails } private fun resetFailures() { lockPrefs.edit().remove("fail_count").remove("last_fail_time").apply() } private fun lockoutRemainingMs(): Long { val fails = lockPrefs.getInt("fail_count", 0) if (fails < MAX_ATTEMPTS) return 0L val lastFail = lockPrefs.getLong("last_fail_time", 0L) return maxOf(0L, LOCKOUT_MS - (System.currentTimeMillis() - lastFail)) } // ── Crypto ──────────────────────────────────────────────────────────────── private fun pbkdf2(input: String, salt: ByteArray): String { val spec = PBEKeySpec(input.toCharArray(), salt, 100_000, 256) return try { val hash = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).encoded Base64.encodeToString(hash, Base64.NO_WRAP) } finally { spec.clearPassword() } } }