Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import dev.pranav.applock.data.repository.PreferencesRepository
import dev.pranav.applock.features.antiuninstall.ui.AntiUninstallScreen
import dev.pranav.applock.features.appintro.ui.AppIntroScreen
import dev.pranav.applock.features.applist.ui.MainScreen
import dev.pranav.applock.features.lockscreen.ui.PasswordOverlayScreen
import dev.pranav.applock.features.lockscreen.ui.AlphanumericPasswordOverlayScreen
import dev.pranav.applock.features.lockscreen.ui.PinPasswordOverlayScreen
import dev.pranav.applock.features.lockscreen.ui.PatternLockScreen
import dev.pranav.applock.features.setpassword.ui.AlphanumericSetPasswordScreen
import dev.pranav.applock.features.setpassword.ui.PatternSetPasswordScreen
import dev.pranav.applock.features.setpassword.ui.SetPasswordScreen
import dev.pranav.applock.features.settings.ui.SettingsScreen
Expand Down Expand Up @@ -52,17 +54,27 @@ fun AppNavHost(navController: NavHostController, startDestination: String) {
}

composable(Screen.ChangePassword.route) {
if (application.appLockRepository.getLockType() == PreferencesRepository.LOCK_TYPE_PATTERN) {
PatternSetPasswordScreen(navController, false)
} else {
SetPasswordScreen(navController, isFirstTimeSetup = false)
when (application.appLockRepository.getLockType()) {
PreferencesRepository.LOCK_TYPE_PATTERN -> {
PatternSetPasswordScreen(navController, false)
}
PreferencesRepository.LOCK_TYPE_PASSWORD -> {
AlphanumericSetPasswordScreen(navController, false)
}
else -> {
SetPasswordScreen(navController, isFirstTimeSetup = false)
}
}
}

composable(Screen.SetPasswordPattern.route) {
PatternSetPasswordScreen(navController, isFirstTimeSetup = true)
}

composable(Screen.SetPasswordAlphanumeric.route) {
AlphanumericSetPasswordScreen(navController, isFirstTimeSetup = true)
}

composable(Screen.Main.route) {
MainScreen(navController)
}
Expand All @@ -88,8 +100,21 @@ fun AppNavHost(navController: NavHostController, startDestination: String) {
)
}

PreferencesRepository.LOCK_TYPE_PASSWORD -> {
AlphanumericPasswordOverlayScreen(
showBiometricButton = application.appLockRepository.isBiometricAuthEnabled(),
fromMainActivity = true,
onBiometricAuth = {
handleBiometricAuthentication(context, navController)
},
onAuthSuccess = {
handleAuthenticationSuccess(navController)
}
)
}

else -> {
PasswordOverlayScreen(
PinPasswordOverlayScreen(
showBiometricButton = application.appLockRepository.isBiometricAuthEnabled(),
fromMainActivity = true,
onBiometricAuth = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ sealed class Screen(val route: String) {
object AppIntro : Screen("app_intro")
object SetPassword : Screen("set_password")
object SetPasswordPattern : Screen("set_password_pattern")
object SetPasswordAlphanumeric : Screen("set_password_alphanumeric")
object ChangePassword : Screen("change_password")
object Main : Screen("main")
object PasswordOverlay : Screen("password_overlay")
Expand Down
69 changes: 69 additions & 0 deletions app/src/main/java/dev/pranav/applock/core/utils/SecurityUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package dev.pranav.applock.core.utils

import android.util.Base64
import java.security.MessageDigest
import java.security.SecureRandom

object SecurityUtils {

private const val HASH_ALGORITHM = "SHA-256"
private const val SALT_LENGTH = 16
private const val MAX_PASSWORD_LENGTH = 64

/**
* Sanitizes the input string by filtering out control characters and null bytes.
* Also limits the length to [MAX_PASSWORD_LENGTH].
*/
fun sanitizePassword(input: String): String {
return input.filter { it.code >= 32 && it.code != 127 }
.take(MAX_PASSWORD_LENGTH)
}

/**
* Generates a random cryptographic salt.
*/
fun generateSalt(): ByteArray {
val random = SecureRandom()
val salt = ByteArray(SALT_LENGTH)
random.nextBytes(salt)
return salt
}

/**
* Hashes the password using SHA-256 with the provided salt.
* Returns a Base64 encoded string containing the salt and the hash.
* Format: salt:hash
*/
fun hashPassword(password: String, salt: ByteArray): String {
val md = MessageDigest.getInstance(HASH_ALGORITHM)
md.update(salt)
val hashedPassword = md.digest(password.toByteArray(Charsets.UTF_8))

val saltBase64 = Base64.encodeToString(salt, Base64.NO_WRAP)
val hashBase64 = Base64.encodeToString(hashedPassword, Base64.NO_WRAP)

return "$saltBase64:$hashBase64"
}

/**
* Verifies an input password against a stored salted hash string.
*/
fun verifyPassword(inputPassword: String, storedSaltedHash: String): Boolean {
return try {
val parts = storedSaltedHash.split(":")
if (parts.size != 2) return false

val salt = Base64.decode(parts[0], Base64.DEFAULT)
val storedHash = parts[1]

val md = MessageDigest.getInstance(HASH_ALGORITHM)
md.update(salt)
val inputHashed = md.digest(inputPassword.toByteArray(Charsets.UTF_8))
val inputHashBase64 = Base64.encodeToString(inputHashed, Base64.NO_WRAP)

inputHashBase64 == storedHash
} catch (e: Exception) {
false
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.pranav.applock.data.repository

import dev.pranav.applock.core.utils.SecurityUtils
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
Expand All @@ -17,20 +18,27 @@ class PreferencesRepository(context: Context) {
context.getSharedPreferences(PREFS_NAME_SETTINGS, Context.MODE_PRIVATE)

fun setPassword(password: String) {
appLockPrefs.edit { putString(KEY_PASSWORD, password) }
val salt = SecurityUtils.generateSalt()
val saltedHash = SecurityUtils.hashPassword(password, salt)
appLockPrefs.edit(commit = true) { putString(KEY_PASSWORD, saltedHash) }
}

fun getPassword(): String? {
return appLockPrefs.getString(KEY_PASSWORD, null)
}

fun validatePassword(inputPassword: String): Boolean {
val storedPassword = getPassword()
return storedPassword != null && inputPassword == storedPassword
val storedSaltedHash = getPassword() ?: return false

// If it's a legacy plain text password (doesn't contain ':'), migrate it or validate as is
// For simplicity and since this is a new feature, we assume all passwords should be hashed.
// If there's an existing plain text password, this will fail validation and require reset.

return SecurityUtils.verifyPassword(inputPassword, storedSaltedHash)
}

fun setPattern(pattern: String) {
appLockPrefs.edit { putString(KEY_PATTERN, pattern) }
appLockPrefs.edit(commit = true) { putString(KEY_PATTERN, pattern) }
}

fun getPattern(): String? {
Expand All @@ -43,7 +51,7 @@ class PreferencesRepository(context: Context) {
}

fun setLockType(lockType: String) {
settingsPrefs.edit { putString(KEY_LOCK_TYPE, lockType) }
settingsPrefs.edit(commit = true) { putString(KEY_LOCK_TYPE, lockType) }
}

fun getLockType(): String {
Expand Down Expand Up @@ -180,5 +188,6 @@ class PreferencesRepository(context: Context) {

const val LOCK_TYPE_PIN = "pin"
const val LOCK_TYPE_PATTERN = "pattern"
const val LOCK_TYPE_PASSWORD = "password"
}
}
Loading
Loading