security: encrypt credentials, caches, and harden lock screen
Auto Tag on Version Change / check-version (push) Successful in 6s

This commit is contained in:
2026-05-15 18:35:14 +05:00
parent 106004421e
commit fc031f1f2a
20 changed files with 506 additions and 149 deletions
@@ -56,6 +56,7 @@ class HomeActivity : AppCompatActivity() {
private val autolockHandler = Handler(Looper.getMainLooper())
private var warningDialog: AlertDialog? = null
private var countdownTimer: CountDownTimer? = null
private var pauseTime = 0L
private val warningRunnable = Runnable { showAutolockWarning() }
@@ -194,11 +195,24 @@ class HomeActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
// If we were away long enough to have hit the autolock timeout (e.g. while
// QrScannerActivity was in the foreground), lock immediately.
if (pauseTime > 0L) {
val elapsed = System.currentTimeMillis() - pauseTime
val timeout = getSharedPreferences("prefs", MODE_PRIVATE).getLong("autolock_timeout", 60_000L)
val securitySet = getSharedPreferences("prefs", MODE_PRIVATE).getString("security_method", null) != null
if (timeout > 0L && elapsed >= timeout && securitySet) {
startActivity(Intent(this, sh.sar.basedbank.LockActivity::class.java))
finish()
return
}
}
resetAutolockTimer()
}
override fun onPause() {
super.onPause()
pauseTime = System.currentTimeMillis()
autolockHandler.removeCallbacks(autolockRunnable)
autolockHandler.removeCallbacks(warningRunnable)
countdownTimer?.cancel(); countdownTimer = null
@@ -300,8 +314,7 @@ class HomeActivity : AppCompatActivity() {
val mibJob = mibCreds?.let {
async(Dispatchers.IO) {
try {
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
val flow = MibLoginFlow(prefs)
val flow = MibLoginFlow(CredentialStore(this@HomeActivity))
val accounts = flow.login(it.username, it.passwordHash, it.otpSeed)
val app = application as BasedBankApp
app.accounts = accounts
@@ -411,8 +424,7 @@ class HomeActivity : AppCompatActivity() {
private fun refreshContacts(session: MibSession?, profiles: List<MibProfile>) {
if (session == null || profiles.isEmpty()) return
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
val flow = MibLoginFlow(prefs)
val flow = MibLoginFlow(CredentialStore(this))
val contactsClient = MibContactsClient()
lifecycleScope.launch {
try {
@@ -465,8 +477,7 @@ class HomeActivity : AppCompatActivity() {
val fresh = withContext(Dispatchers.IO) {
val sess = app.mibSession ?: return@withContext null
val profile = app.mibProfiles.firstOrNull { it.profileId == src.profileId } ?: return@withContext null
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
try { MibLoginFlow(prefs).fetchAllProfiles(sess, listOf(profile), src.loginTag) }
try { MibLoginFlow(CredentialStore(this@HomeActivity)).fetchAllProfiles(sess, listOf(profile), src.loginTag) }
catch (_: Exception) { null }
} ?: return@launch
// Replace accounts from this profile only, keep everything else
@@ -480,8 +491,7 @@ class HomeActivity : AppCompatActivity() {
private fun refreshFinancing(session: MibSession?, profiles: List<MibProfile>) {
if (session == null || profiles.isEmpty()) return
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
val flow = MibLoginFlow(prefs)
val flow = MibLoginFlow(CredentialStore(this))
val client = MibFinancingClient()
lifecycleScope.launch {
try {
@@ -111,8 +111,7 @@ class CredentialsFragment : Fragment() {
binding.btnLogin.isEnabled = false
val passwordHash = MibLoginFlow.hashPassword(password)
val prefs = requireContext().getSharedPreferences("mib_prefs", android.content.Context.MODE_PRIVATE)
val flow = MibLoginFlow(prefs)
val flow = MibLoginFlow(CredentialStore(requireContext()))
viewLifecycleOwner.lifecycleScope.launch {
try {
@@ -12,8 +12,10 @@ import androidx.fragment.app.Fragment
import com.google.android.material.button.MaterialButton
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.FragmentSecuritySetupBinding
import java.security.MessageDigest
import sh.sar.basedbank.util.CredentialStore
import java.security.SecureRandom
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
class SecuritySetupFragment : Fragment() {
@@ -231,16 +233,25 @@ class SecuritySetupFragment : Fragment() {
private fun saveCredential(method: String, input: String) {
val salt = ByteArray(16).also { SecureRandom().nextBytes(it) }
val saltB64 = Base64.encodeToString(salt, Base64.NO_WRAP)
val hash = sha256(saltB64 + input)
val hash = pbkdf2(input, salt)
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).edit()
.putString("security_method", method)
.putString("security_salt", saltB64)
.putString("security_hash", hash)
// Remove legacy plaintext fields if they exist from an old install
.remove("security_salt")
.remove("security_hash")
.apply()
CredentialStore(requireContext()).saveSecurityHash(saltB64, hash)
}
private fun sha256(input: String) = MessageDigest.getInstance("SHA-256")
.digest(input.toByteArray()).joinToString("") { "%02x".format(it) }
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()
}
}
private fun finishSetup() {
val cb = activity as? Callback