Skip to content

Commit c053566

Browse files
committed
Fix: fix older passwords breaking
1 parent 482f894 commit c053566

4 files changed

Lines changed: 62 additions & 31 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ android {
1414
applicationId = "dev.pranav.applock"
1515
minSdk = 26
1616
targetSdk = 37
17-
versionCode = 250
18-
versionName = "2.5.0"
17+
versionCode = 251
18+
versionName = "2.5.1"
1919

2020
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2121
}
Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
11
package dev.pranav.applock.core.utils
22

33
import android.util.Base64
4+
import dev.pranav.applock.core.utils.SecurityUtils.MAX_PASSWORD_LENGTH
45
import java.security.MessageDigest
56
import java.security.SecureRandom
67

78
object SecurityUtils {
89

910
private const val HASH_ALGORITHM = "SHA-256"
1011
private const val SALT_LENGTH = 16
11-
private const val MAX_PASSWORD_LENGTH = 64
12+
const val MAX_PASSWORD_LENGTH = 64
1213

1314
/**
1415
* Sanitizes the input string by filtering out control characters and null bytes.
1516
* Also limits the length to [MAX_PASSWORD_LENGTH].
1617
*/
1718
fun sanitizePassword(input: String): String {
18-
return input.filter { it.code >= 32 && it.code != 127 }
19+
return input
20+
.filter { it.code >= 32 && it.code != 127 }
1921
.take(MAX_PASSWORD_LENGTH)
2022
}
2123

2224
/**
2325
* Generates a random cryptographic salt.
2426
*/
2527
fun generateSalt(): ByteArray {
26-
val random = SecureRandom()
27-
val salt = ByteArray(SALT_LENGTH)
28-
random.nextBytes(salt)
29-
return salt
28+
return ByteArray(SALT_LENGTH).also { salt ->
29+
SecureRandom().nextBytes(salt)
30+
}
31+
}
32+
33+
fun hashPassword(password: String): String {
34+
return hashPassword(password, generateSalt())
3035
}
3136

3237
/**
@@ -35,35 +40,51 @@ object SecurityUtils {
3540
* Format: salt:hash
3641
*/
3742
fun hashPassword(password: String, salt: ByteArray): String {
43+
val sanitizedPassword = sanitizePassword(password)
44+
3845
val md = MessageDigest.getInstance(HASH_ALGORITHM)
3946
md.update(salt)
40-
val hashedPassword = md.digest(password.toByteArray(Charsets.UTF_8))
41-
47+
val hash = md.digest(sanitizedPassword.toByteArray(Charsets.UTF_8))
48+
4249
val saltBase64 = Base64.encodeToString(salt, Base64.NO_WRAP)
43-
val hashBase64 = Base64.encodeToString(hashedPassword, Base64.NO_WRAP)
44-
50+
val hashBase64 = Base64.encodeToString(hash, Base64.NO_WRAP)
51+
4552
return "$saltBase64:$hashBase64"
4653
}
4754

55+
fun isSaltedHash(value: String): Boolean {
56+
val parts = value.split(":")
57+
if (parts.size != 2) return false
58+
59+
return try {
60+
Base64.decode(parts[0], Base64.NO_WRAP)
61+
Base64.decode(parts[1], Base64.NO_WRAP)
62+
true
63+
} catch (_: Exception) {
64+
false
65+
}
66+
}
67+
4868
/**
4969
* Verifies an input password against a stored salted hash string.
5070
*/
5171
fun verifyPassword(inputPassword: String, storedSaltedHash: String): Boolean {
5272
return try {
5373
val parts = storedSaltedHash.split(":")
5474
if (parts.size != 2) return false
55-
56-
val salt = Base64.decode(parts[0], Base64.DEFAULT)
57-
val storedHash = parts[1]
58-
75+
76+
val salt = Base64.decode(parts[0], Base64.NO_WRAP)
77+
val expectedHash = Base64.decode(parts[1], Base64.NO_WRAP)
78+
79+
val sanitizedInput = sanitizePassword(inputPassword)
80+
5981
val md = MessageDigest.getInstance(HASH_ALGORITHM)
6082
md.update(salt)
61-
val inputHashed = md.digest(inputPassword.toByteArray(Charsets.UTF_8))
62-
val inputHashBase64 = Base64.encodeToString(inputHashed, Base64.NO_WRAP)
63-
64-
inputHashBase64 == storedHash
65-
} catch (e: Exception) {
83+
val actualHash = md.digest(sanitizedInput.toByteArray(Charsets.UTF_8))
84+
85+
MessageDigest.isEqual(actualHash, expectedHash)
86+
} catch (_: Exception) {
6687
false
6788
}
6889
}
69-
}
90+
}

app/src/main/java/dev/pranav/applock/data/repository/PreferencesRepository.kt

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package dev.pranav.applock.data.repository
22

3-
import dev.pranav.applock.core.utils.SecurityUtils
43
import android.content.Context
54
import android.content.SharedPreferences
65
import androidx.core.content.edit
6+
import dev.pranav.applock.core.utils.SecurityUtils
77

88
/**
99
* Repository for managing application preferences and settings.
@@ -27,14 +27,22 @@ class PreferencesRepository(context: Context) {
2727
return appLockPrefs.getString(KEY_PASSWORD, null)
2828
}
2929

30-
fun validatePassword(inputPassword: String): Boolean {
31-
val storedSaltedHash = getPassword() ?: return false
32-
33-
// If it's a legacy plain text password (doesn't contain ':'), migrate it or validate as is
34-
// For simplicity and since this is a new feature, we assume all passwords should be hashed.
35-
// If there's an existing plain text password, this will fail validation and require reset.
36-
37-
return SecurityUtils.verifyPassword(inputPassword, storedSaltedHash)
30+
fun validatePassword(input: String): Boolean {
31+
val stored = getPassword()
32+
if (stored.isNullOrBlank()) return false
33+
34+
val sanitizedInput = SecurityUtils.sanitizePassword(input)
35+
36+
if (SecurityUtils.isSaltedHash(stored)) {
37+
return SecurityUtils.verifyPassword(sanitizedInput, stored)
38+
}
39+
40+
if (stored == input || stored == sanitizedInput) {
41+
setPassword(sanitizedInput)
42+
return true
43+
}
44+
45+
return false
3846
}
3947

4048
fun setPattern(pattern: String) {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
* fix previous passwords breaking
2+
* fix password screen showing again on back press

0 commit comments

Comments
 (0)