optimize OTP seed (check password =) remove legacy code
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 4s
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 4s
This commit is contained in:
@@ -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) }
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user