forked from thijooree/android
add app lock secruity and support for saving mib credentials
This commit is contained in:
@@ -18,6 +18,7 @@ import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.util.Totp
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import sh.sar.basedbank.databinding.FragmentCredentialsBinding
|
||||
import sh.sar.basedbank.ui.home.HomeActivity
|
||||
|
||||
@@ -98,6 +99,7 @@ class CredentialsFragment : Fragment() {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.btnLogin.isEnabled = false
|
||||
|
||||
val passwordHash = MibLoginFlow.hashPassword(password)
|
||||
val prefs = requireContext().getSharedPreferences("mib_prefs", android.content.Context.MODE_PRIVATE)
|
||||
val flow = MibLoginFlow(prefs)
|
||||
|
||||
@@ -105,9 +107,10 @@ class CredentialsFragment : Fragment() {
|
||||
try {
|
||||
Log.d(TAG, "Starting login flow on IO dispatcher")
|
||||
val accounts = withContext(Dispatchers.IO) {
|
||||
flow.login(username, password, otpSeed)
|
||||
flow.login(username, passwordHash, otpSeed)
|
||||
}
|
||||
Log.d(TAG, "Login succeeded, got ${accounts.size} accounts")
|
||||
CredentialStore(requireContext()).saveMibCredentials(username, passwordHash, otpSeed)
|
||||
(requireActivity().application as BasedBankApp).accounts = accounts
|
||||
startActivity(Intent(requireContext(), HomeActivity::class.java))
|
||||
requireActivity().finish()
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
package sh.sar.basedbank.ui.login
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||
import sh.sar.basedbank.databinding.ActivityLoginBinding
|
||||
import sh.sar.basedbank.ui.home.HomeActivity
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
|
||||
@@ -14,5 +23,31 @@ class LoginActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val creds = CredentialStore(this).loadMibCredentials()
|
||||
if (creds != null) {
|
||||
binding.navHostFragment.visibility = View.GONE
|
||||
binding.autoLoginGroup.visibility = View.VISIBLE
|
||||
autoLogin(creds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun autoLogin(creds: CredentialStore.MibCredentials) {
|
||||
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
||||
val flow = MibLoginFlow(prefs)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val accounts = withContext(Dispatchers.IO) {
|
||||
flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
||||
}
|
||||
(application as BasedBankApp).accounts = accounts
|
||||
startActivity(Intent(this@LoginActivity, HomeActivity::class.java))
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
// Auto-login failed — fall back to manual login form
|
||||
binding.autoLoginGroup.visibility = View.GONE
|
||||
binding.navHostFragment.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sh.sar.basedbank.ui.onboarding
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -12,22 +13,24 @@ import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.databinding.ActivityOnboardingBinding
|
||||
import sh.sar.basedbank.ui.login.LoginActivity
|
||||
|
||||
class OnboardingActivity : AppCompatActivity() {
|
||||
class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
||||
|
||||
private lateinit var binding: ActivityOnboardingBinding
|
||||
private lateinit var prefs: SharedPreferences
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityOnboardingBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||
|
||||
val adapter = OnboardingPagerAdapter(this)
|
||||
binding.viewPager.adapter = adapter
|
||||
|
||||
TabLayoutMediator(binding.dotsIndicator, binding.viewPager) { _, _ -> }.attach()
|
||||
|
||||
// Pre-select the chip for the saved language without triggering the listener
|
||||
val savedLang = getSharedPreferences("prefs", MODE_PRIVATE).getString("language", null)
|
||||
// Pre-select language chip without triggering the listener
|
||||
val savedLang = prefs.getString("language", null)
|
||||
binding.languageChipGroup.setOnCheckedStateChangeListener(null)
|
||||
when (savedLang) {
|
||||
"en" -> binding.chipEnglish.isChecked = true
|
||||
@@ -35,18 +38,24 @@ class OnboardingActivity : AppCompatActivity() {
|
||||
}
|
||||
binding.languageChipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
|
||||
if (checkedIds.isNotEmpty()) {
|
||||
val lang = if (checkedIds[0] == R.id.chipEnglish) "en" else "dv"
|
||||
selectLanguage(lang)
|
||||
selectLanguage(if (checkedIds[0] == R.id.chipEnglish) "en" else "dv")
|
||||
}
|
||||
}
|
||||
|
||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
binding.languageChipGroup.visibility = if (position == 0) View.VISIBLE else View.GONE
|
||||
// Block forward swipe on slide 1 until security is set up
|
||||
if (position == 1) {
|
||||
binding.viewPager.isUserInputEnabled =
|
||||
prefs.getString("security_method", null) != null
|
||||
} else {
|
||||
binding.viewPager.isUserInputEnabled = true
|
||||
}
|
||||
updateButtons(position, adapter.itemCount)
|
||||
}
|
||||
})
|
||||
// Show chips and set initial button state for page 0
|
||||
|
||||
binding.languageChipGroup.visibility = View.VISIBLE
|
||||
updateButtons(0, adapter.itemCount)
|
||||
|
||||
@@ -56,26 +65,41 @@ class OnboardingActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
binding.btnGetStarted.setOnClickListener {
|
||||
getSharedPreferences("prefs", MODE_PRIVATE)
|
||||
.edit().putBoolean("onboarding_done", true).apply()
|
||||
prefs.edit().putBoolean("onboarding_done", true).apply()
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
// Called by SecuritySetupFragment when setup is complete
|
||||
override fun onSecuritySetupComplete() {
|
||||
binding.viewPager.isUserInputEnabled = true
|
||||
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 3)
|
||||
}
|
||||
|
||||
private fun selectLanguage(lang: String) {
|
||||
getSharedPreferences("prefs", MODE_PRIVATE).edit().putString("language", lang).apply()
|
||||
val locales = LocaleListCompat.forLanguageTags(lang)
|
||||
AppCompatDelegate.setApplicationLocales(locales)
|
||||
// Update buttons immediately in case locale didn't change (no recreation)
|
||||
prefs.edit().putString("language", lang).apply()
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(lang))
|
||||
updateButtons(binding.viewPager.currentItem, binding.viewPager.adapter?.itemCount ?: 3)
|
||||
}
|
||||
|
||||
private fun updateButtons(position: Int, count: Int) {
|
||||
val langSelected = getSharedPreferences("prefs", MODE_PRIVATE).getString("language", null) != null
|
||||
val langSelected = prefs.getString("language", null) != null
|
||||
val securityDone = prefs.getString("security_method", null) != null
|
||||
val isLast = position == count - 1
|
||||
binding.btnNext.visibility = if (isLast) View.GONE else View.VISIBLE
|
||||
|
||||
binding.btnGetStarted.visibility = if (isLast) View.VISIBLE else View.GONE
|
||||
binding.btnNext.isEnabled = position > 0 || langSelected
|
||||
|
||||
// Hide Next on slide 1 until security is done (avoids a disabled-button-with-no-explanation)
|
||||
binding.btnNext.visibility = when {
|
||||
isLast -> View.GONE
|
||||
position == 1 && !securityDone -> View.GONE
|
||||
else -> View.VISIBLE
|
||||
}
|
||||
binding.btnNext.isEnabled = when (position) {
|
||||
0 -> langSelected
|
||||
1 -> securityDone
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,10 @@ class OnboardingPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(
|
||||
|
||||
override fun getItemCount() = slides.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment =
|
||||
OnboardingFragment.newInstance(slides[position])
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
1 -> SecuritySetupFragment()
|
||||
else -> OnboardingFragment.newInstance(slides[position])
|
||||
}
|
||||
}
|
||||
|
||||
data class OnboardingSlide(
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package sh.sar.basedbank.ui.onboarding
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class PatternView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : View(context, attrs) {
|
||||
|
||||
private data class Cell(val index: Int, var cx: Float = 0f, var cy: Float = 0f)
|
||||
|
||||
private val cells = List(9) { Cell(it) }
|
||||
private val selected = mutableListOf<Cell>()
|
||||
private var touchX = 0f
|
||||
private var touchY = 0f
|
||||
private var recording = false
|
||||
private var errorState = false
|
||||
|
||||
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
|
||||
private val ringPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE }
|
||||
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
}
|
||||
|
||||
var onPatternComplete: ((List<Int>) -> Unit)? = null
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
val sz = min(w, h).toFloat()
|
||||
val cell = sz / 3f
|
||||
cells.forEachIndexed { i, c ->
|
||||
c.cx = (i % 3) * cell + cell / 2f
|
||||
c.cy = (i / 3) * cell + cell / 2f
|
||||
}
|
||||
linePaint.strokeWidth = cell * 0.07f
|
||||
ringPaint.strokeWidth = cell * 0.05f
|
||||
}
|
||||
|
||||
override fun onMeasure(ws: Int, hs: Int) {
|
||||
val s = min(MeasureSpec.getSize(ws), MeasureSpec.getSize(hs))
|
||||
setMeasuredDimension(s, s)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
val sz = min(width, height).toFloat()
|
||||
val cell = sz / 3f
|
||||
val dotR = cell * 0.13f
|
||||
val ringR = cell * 0.26f
|
||||
val activeColor = if (errorState)
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorError, Color.RED)
|
||||
else
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimary, Color.BLUE)
|
||||
val normalColor =
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorOutlineVariant, Color.GRAY)
|
||||
|
||||
// Lines between selected cells
|
||||
linePaint.color = activeColor; linePaint.alpha = 160
|
||||
for (i in 0 until selected.size - 1)
|
||||
canvas.drawLine(selected[i].cx, selected[i].cy, selected[i + 1].cx, selected[i + 1].cy, linePaint)
|
||||
|
||||
// Trailing line to finger position
|
||||
if (recording && selected.isNotEmpty()) {
|
||||
linePaint.alpha = 80
|
||||
canvas.drawLine(selected.last().cx, selected.last().cy, touchX, touchY, linePaint)
|
||||
}
|
||||
|
||||
// Dots
|
||||
cells.forEach { c ->
|
||||
val isSelected = selected.contains(c)
|
||||
dotPaint.color = if (isSelected) activeColor else normalColor
|
||||
canvas.drawCircle(c.cx, c.cy, dotR, dotPaint)
|
||||
if (isSelected) {
|
||||
ringPaint.color = activeColor; ringPaint.alpha = 60
|
||||
canvas.drawCircle(c.cx, c.cy, ringR, ringPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (errorState) return false
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
recording = true; selected.clear()
|
||||
hit(event.x, event.y)
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
touchX = event.x; touchY = event.y
|
||||
hit(event.x, event.y)
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
recording = false
|
||||
invalidate()
|
||||
onPatternComplete?.invoke(selected.map { it.index })
|
||||
}
|
||||
}
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun hit(x: Float, y: Float) {
|
||||
val hitR = min(width, height) / 3f * 0.40f
|
||||
cells.forEach { c ->
|
||||
val dx = x - c.cx; val dy = y - c.cy
|
||||
if (sqrt(dx * dx + dy * dy) <= hitR && !selected.contains(c)) selected.add(c)
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() { selected.clear(); recording = false; errorState = false; invalidate() }
|
||||
|
||||
fun showError() {
|
||||
errorState = true; invalidate()
|
||||
postDelayed({ reset() }, 700)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package sh.sar.basedbank.ui.onboarding
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.biometric.BiometricManager
|
||||
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 java.security.SecureRandom
|
||||
|
||||
class SecuritySetupFragment : Fragment() {
|
||||
|
||||
interface Callback {
|
||||
fun onSecuritySetupComplete()
|
||||
}
|
||||
|
||||
private var _b: FragmentSecuritySetupBinding? = null
|
||||
private val b get() = _b!!
|
||||
|
||||
private enum class Step { CHOOSE, PIN_ENTER, PIN_CONFIRM, PATTERN_ENTER, PATTERN_CONFIRM, BIOMETRIC }
|
||||
|
||||
private var step = Step.CHOOSE
|
||||
private val pinDigits = mutableListOf<Int>()
|
||||
private var firstPin = ""
|
||||
private var firstPattern: List<Int> = emptyList()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_b = FragmentSecuritySetupBinding.inflate(inflater, container, false)
|
||||
return b.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||
if (prefs.getString("security_method", null) != null) {
|
||||
(activity as? Callback)?.onSecuritySetupComplete()
|
||||
}
|
||||
|
||||
b.cardPin.setOnClickListener { goTo(Step.PIN_ENTER) }
|
||||
b.cardPattern.setOnClickListener { goTo(Step.PATTERN_ENTER) }
|
||||
|
||||
b.btnPinBack.setOnClickListener {
|
||||
when (step) {
|
||||
Step.PIN_CONFIRM -> goTo(Step.PIN_ENTER)
|
||||
else -> goTo(Step.CHOOSE)
|
||||
}
|
||||
}
|
||||
b.btnPatternBack.setOnClickListener { goTo(Step.CHOOSE) }
|
||||
|
||||
b.patternView.onPatternComplete = { pattern -> handlePattern(pattern) }
|
||||
|
||||
b.btnEnableBiometrics.setOnClickListener {
|
||||
prefs.edit().putBoolean("biometrics_enabled", true).apply()
|
||||
finishSetup()
|
||||
}
|
||||
b.btnSkipBiometrics.setOnClickListener {
|
||||
prefs.edit().putBoolean("biometrics_enabled", false).apply()
|
||||
finishSetup()
|
||||
}
|
||||
|
||||
buildNumpad()
|
||||
goTo(Step.CHOOSE)
|
||||
}
|
||||
|
||||
private fun buildNumpad() {
|
||||
val rows = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf("⌫", "0", "✓")
|
||||
)
|
||||
rows.forEach { keys ->
|
||||
val row = LinearLayout(requireContext()).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f
|
||||
)
|
||||
}
|
||||
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(requireContext(), null, style).apply {
|
||||
text = key
|
||||
textSize = 20f
|
||||
insetTop = 0
|
||||
insetBottom = 0
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f)
|
||||
.also { it.setMargins(4, 4, 4, 4) }
|
||||
}
|
||||
btn.setOnClickListener { handleKey(key) }
|
||||
row.addView(btn)
|
||||
}
|
||||
b.numpadContainer.addView(row)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleKey(key: String) {
|
||||
when (key) {
|
||||
"⌫" -> if (pinDigits.isNotEmpty()) { pinDigits.removeLast(); updateDots() }
|
||||
"✓" -> if (pinDigits.size >= 4) submitPin()
|
||||
else -> if (pinDigits.size < 8) { pinDigits.add(key.toInt()); updateDots() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDots() {
|
||||
val n = pinDigits.size
|
||||
val total = maxOf(n, 4)
|
||||
b.tvPinDots.text = "●".repeat(n) + "○".repeat(total - n)
|
||||
}
|
||||
|
||||
private fun submitPin() {
|
||||
val entered = pinDigits.joinToString("")
|
||||
when (step) {
|
||||
Step.PIN_ENTER -> {
|
||||
firstPin = entered
|
||||
pinDigits.clear()
|
||||
goTo(Step.PIN_CONFIRM)
|
||||
}
|
||||
Step.PIN_CONFIRM -> {
|
||||
if (entered == firstPin) {
|
||||
saveCredential("pin", entered)
|
||||
goToBiometricOrFinish()
|
||||
} else {
|
||||
b.tvPinDots.text = getString(R.string.pin_no_match)
|
||||
pinDigits.clear()
|
||||
b.root.postDelayed({ updateDots() }, 1200)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePattern(pattern: List<Int>) {
|
||||
if (pattern.size < 4) {
|
||||
b.patternView.showError()
|
||||
b.tvPatternStatus.text = getString(R.string.pattern_min_dots)
|
||||
return
|
||||
}
|
||||
when (step) {
|
||||
Step.PATTERN_ENTER -> {
|
||||
firstPattern = pattern
|
||||
step = Step.PATTERN_CONFIRM
|
||||
b.tvPatternTitle.text = getString(R.string.confirm_pattern)
|
||||
b.tvPatternStatus.text = getString(R.string.pattern_draw_again)
|
||||
b.patternView.reset()
|
||||
}
|
||||
Step.PATTERN_CONFIRM -> {
|
||||
if (pattern == firstPattern) {
|
||||
saveCredential("pattern", pattern.joinToString(""))
|
||||
goToBiometricOrFinish()
|
||||
} else {
|
||||
b.patternView.showError()
|
||||
b.tvPatternStatus.text = getString(R.string.pattern_no_match)
|
||||
step = Step.PATTERN_ENTER
|
||||
b.tvPatternTitle.text = getString(R.string.draw_pattern)
|
||||
b.root.postDelayed({
|
||||
b.tvPatternStatus.text = getString(R.string.pattern_min_dots)
|
||||
}, 1200)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun goToBiometricOrFinish() {
|
||||
val canAuth = BiometricManager.from(requireContext())
|
||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
|
||||
if (canAuth == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
goTo(Step.BIOMETRIC)
|
||||
} else {
|
||||
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||
.edit().putBoolean("biometrics_enabled", false).apply()
|
||||
finishSetup()
|
||||
}
|
||||
}
|
||||
|
||||
private fun goTo(s: Step) {
|
||||
step = s
|
||||
b.viewChooseMethod.visibility = if (s == Step.CHOOSE) View.VISIBLE else View.GONE
|
||||
b.viewPinSetup.visibility = if (s == Step.PIN_ENTER || s == Step.PIN_CONFIRM) View.VISIBLE else View.GONE
|
||||
b.viewPatternSetup.visibility = if (s == Step.PATTERN_ENTER || s == Step.PATTERN_CONFIRM) View.VISIBLE else View.GONE
|
||||
b.viewBiometric.visibility = if (s == Step.BIOMETRIC) View.VISIBLE else View.GONE
|
||||
|
||||
when (s) {
|
||||
Step.PIN_ENTER -> {
|
||||
pinDigits.clear()
|
||||
b.tvPinTitle.text = getString(R.string.enter_pin)
|
||||
updateDots()
|
||||
}
|
||||
Step.PIN_CONFIRM -> {
|
||||
b.tvPinTitle.text = getString(R.string.confirm_pin)
|
||||
updateDots()
|
||||
}
|
||||
Step.PATTERN_ENTER -> {
|
||||
b.tvPatternTitle.text = getString(R.string.draw_pattern)
|
||||
b.tvPatternStatus.text = getString(R.string.pattern_min_dots)
|
||||
b.patternView.reset()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).edit()
|
||||
.putString("security_method", method)
|
||||
.putString("security_salt", saltB64)
|
||||
.putString("security_hash", hash)
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun sha256(input: String) = MessageDigest.getInstance("SHA-256")
|
||||
.digest(input.toByteArray()).joinToString("") { "%02x".format(it) }
|
||||
|
||||
private fun finishSetup() {
|
||||
(activity as? Callback)?.onSecuritySetupComplete()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_b = null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user