optimize OTP seed (check password =) remove legacy code
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 4s

This commit is contained in:
2026-05-18 23:57:05 +05:00
parent b35f44f35b
commit d4f86bb738
4 changed files with 14 additions and 77 deletions

View File

@@ -18,8 +18,6 @@ import kotlinx.coroutines.withContext
import sh.sar.basedbank.databinding.ActivityLockBinding
import sh.sar.basedbank.ui.home.HomeActivity
import sh.sar.basedbank.util.CredentialStore
import java.security.MessageDigest
import java.security.SecureRandom
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
@@ -31,7 +29,6 @@ class LockActivity : AppCompatActivity() {
private lateinit var salt: String
private lateinit var storedHash: String
private var biometricsEnabled = false
private var isLegacyFormat = false
private var isVerifying = false
private val lockPrefs get() = getSharedPreferences("lock_attempts", MODE_PRIVATE)
@@ -51,17 +48,9 @@ class LockActivity : AppCompatActivity() {
method = prefs.getString("security_method", "pin") ?: "pin"
biometricsEnabled = prefs.getBoolean("biometrics_enabled", false)
// Try new encrypted format first; fall back to legacy SHA-256
val stored = CredentialStore(this).loadSecurityHash()
if (stored != null) {
salt = stored.first
storedHash = stored.second
isLegacyFormat = false
} else {
salt = prefs.getString("security_salt", "") ?: ""
storedHash = prefs.getString("security_hash", "") ?: ""
isLegacyFormat = true
}
val stored = CredentialStore(this).loadSecurityHash() ?: run { finish(); return }
salt = stored.first
storedHash = stored.second
if (method == "pin") {
binding.viewPin.visibility = View.VISIBLE
@@ -150,7 +139,6 @@ class LockActivity : AppCompatActivity() {
val ok = withContext(Dispatchers.Default) { verify(entered) }
isVerifying = false
if (ok) {
migrateIfNeeded(entered)
resetFailures()
proceed()
} else {
@@ -175,7 +163,6 @@ class LockActivity : AppCompatActivity() {
val ok = withContext(Dispatchers.Default) { verify(entered) }
isVerifying = false
if (ok) {
migrateIfNeeded(entered)
resetFailures()
proceed()
} else {
@@ -217,30 +204,8 @@ class LockActivity : AppCompatActivity() {
private fun verify(input: String): Boolean {
if (storedHash.isBlank()) return false
return if (isLegacyFormat) {
sha256Legacy(salt + input) == storedHash
} else {
val saltBytes = Base64.decode(salt, Base64.NO_WRAP)
pbkdf2(input, saltBytes) == storedHash
}
}
/**
* On the first successful unlock after legacy SHA-256 format is detected,
* transparently migrate to PBKDF2 + CredentialStore.
*/
private fun migrateIfNeeded(input: String) {
if (!isLegacyFormat) return
try {
val newSalt = ByteArray(16).also { SecureRandom().nextBytes(it) }
val newHash = pbkdf2(input, newSalt)
val saltB64 = Base64.encodeToString(newSalt, Base64.NO_WRAP)
CredentialStore(this).saveSecurityHash(saltB64, newHash)
// Remove legacy plaintext fields
getSharedPreferences("prefs", MODE_PRIVATE).edit()
.remove("security_salt").remove("security_hash").apply()
isLegacyFormat = false
} catch (_: Exception) { /* migration will retry next unlock */ }
val saltBytes = Base64.decode(salt, Base64.NO_WRAP)
return pbkdf2(input, saltBytes) == storedHash
}
private fun triggerBiometric() {
@@ -313,9 +278,4 @@ class LockActivity : AppCompatActivity() {
}
}
/** Legacy: raw SHA-256(salt + input) — only used for migration path. */
private fun sha256Legacy(input: String) = MessageDigest.getInstance("SHA-256")
.digest(input.toByteArray()).joinToString("") { "%02x".format(it) }
}

View File

@@ -115,21 +115,23 @@ class CredentialsFragment : Fragment() {
private fun updateLoginButtonState() {
val username = binding.etUsername.text.toString().trim()
val password = binding.etPassword.text.toString()
val otpSeed = resolveOtpSeed(binding.etOtpSeed.text.toString().trim())
val otpSeedRaw = binding.etOtpSeed.text.toString().trim()
val otpSeed = resolveOtpSeed(otpSeedRaw)
binding.btnLogin.isEnabled = when (bankType) {
"FAHIPAY" -> username.isNotEmpty() && password.isNotEmpty()
else -> username.isNotEmpty() && password.isNotEmpty() && otpSeed.isNotEmpty() && password != otpSeed
else -> username.isNotEmpty() && password.isNotEmpty() && otpSeed.isNotEmpty() && password != otpSeedRaw
}
}
private fun updateOtpDisplay() {
val seed = resolveOtpSeed(binding.etOtpSeed.text.toString().trim())
val otpSeedRaw = binding.etOtpSeed.text.toString().trim()
val seed = resolveOtpSeed(otpSeedRaw)
if (seed.isEmpty()) {
binding.cardOtp.visibility = View.INVISIBLE
return
}
val password = binding.etPassword.text.toString()
if (seed == password || seed.matches(Regex("\\d{6}"))) {
if (otpSeedRaw == password || seed.matches(Regex("\\d{6}"))) {
binding.cardOtp.visibility = View.INVISIBLE
return
}

View File

@@ -217,9 +217,6 @@ class SecuritySetupFragment : Fragment() {
val hash = pbkdf2(input, salt)
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).edit()
.putString("security_method", method)
// Remove legacy plaintext fields if they exist from an old install
.remove("security_salt")
.remove("security_hash")
.apply()
CredentialStore(requireContext()).saveSecurityHash(saltB64, hash)
}

View File

@@ -93,32 +93,10 @@ class CredentialStore(context: Context) {
// ── BML login credentials (multi-login, keyed by loginId = username) ────────
fun getBmlLoginIds(): List<String> {
val json = prefs.getString("bml_login_ids", null)
if (json != null) {
return try {
val arr = org.json.JSONArray(json)
(0 until arr.length()).map { arr.getString(it) }
} catch (_: Exception) { emptyList() }
}
// One-time migration from single-slot BML storage
val oldEncUsername = prefs.getString("bml_enc_username", null) ?: return emptyList()
val json = prefs.getString("bml_login_ids", null) ?: return emptyList()
return try {
val key = getOrCreateKey()
val loginId = decrypt(oldEncUsername, key)
val edit = prefs.edit()
prefs.getString("bml_enc_password", null)?.let { edit.putString("bml_${loginId}_enc_password", it) }
prefs.getString("bml_enc_otp_seed", null)?.let { edit.putString("bml_${loginId}_enc_otp_seed", it) }
prefs.getString("bml_enc_token", null)?.let { edit.putString("bml_${loginId}_enc_token", it) }
prefs.getString("bml_enc_device_id", null)?.let { edit.putString("bml_${loginId}_enc_device_id", it) }
prefs.getString("bml_enc_profile", null)?.let { edit.putString("bml_${loginId}_enc_profile", it) }
edit.putString("bml_${loginId}_enc_username", oldEncUsername)
edit.remove("bml_enc_username").remove("bml_enc_password").remove("bml_enc_otp_seed")
.remove("bml_enc_token").remove("bml_enc_device_id")
.remove("bml_enc_profile").remove("bml_enc_full_name")
val ids = org.json.JSONArray(listOf(loginId)).toString()
edit.putString("bml_login_ids", ids)
edit.apply()
listOf(loginId)
val arr = org.json.JSONArray(json)
(0 until arr.length()).map { arr.getString(it) }
} catch (_: Exception) { emptyList() }
}