update + Fix
This commit is contained in:
1 parent
d168c69c66
commit
202ee1cc58
13 files changed
+475
-4
No files matched your search
@@ -15,10 +15,15 @@ data class AppSettings(
|
||||
val cutoffIntervalMinutes: Int = 15,
|
||||
val baselineMobileBytesAtReset: Long = 0L,
|
||||
val whitelistedPackages: Set<String> = emptySet(),
|
||||
val showSystemApps: Boolean = false
|
||||
val showSystemApps: Boolean = false,
|
||||
val passwordHash: String? = null,
|
||||
val passwordSalt: String? = null
|
||||
) {
|
||||
val planStartDate: LocalDate get() = LocalDate.ofEpochDay(planStartDateEpochDay)
|
||||
|
||||
/** Un mot de passe de verrouillage protège la désactivation du contrôle et l'accès aux réglages. */
|
||||
val hasPassword: Boolean get() = !passwordHash.isNullOrEmpty()
|
||||
|
||||
/** Prochaine date de réinitialisation automatique (anniversaire mensuel du début de forfait). */
|
||||
fun nextAutoResetDate(): LocalDate {
|
||||
var next = planStartDate.plusMonths(1)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.jttools.datacontrol.data
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
|
||||
/** Hachage salé (SHA-256) du mot de passe de verrouillage — jamais stocké en clair. */
|
||||
object PasswordHasher {
|
||||
private const val SALT_BYTES = 16
|
||||
|
||||
fun newSalt(): String {
|
||||
val bytes = ByteArray(SALT_BYTES)
|
||||
SecureRandom().nextBytes(bytes)
|
||||
return bytes.toHex()
|
||||
}
|
||||
|
||||
fun hash(password: String, saltHex: String): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
digest.update(saltHex.toByteArray(Charsets.UTF_8))
|
||||
return digest.digest(password.toByteArray(Charsets.UTF_8)).toHex()
|
||||
}
|
||||
|
||||
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import androidx.datastore.preferences.core.doublePreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
@@ -32,6 +34,8 @@ class SettingsRepository @Inject constructor(
|
||||
val BASELINE_MOBILE_BYTES = longPreferencesKey("baseline_mobile_bytes")
|
||||
val WHITELISTED_PACKAGES = stringSetPreferencesKey("whitelisted_packages")
|
||||
val SHOW_SYSTEM_APPS = booleanPreferencesKey("show_system_apps")
|
||||
val PASSWORD_HASH = stringPreferencesKey("password_hash")
|
||||
val PASSWORD_SALT = stringPreferencesKey("password_salt")
|
||||
}
|
||||
|
||||
val settingsFlow: Flow<AppSettings> = context.dataStore.data.map { prefs ->
|
||||
@@ -46,7 +50,9 @@ class SettingsRepository @Inject constructor(
|
||||
cutoffIntervalMinutes = prefs[Keys.CUTOFF_INTERVAL_MINUTES] ?: defaults.cutoffIntervalMinutes,
|
||||
baselineMobileBytesAtReset = prefs[Keys.BASELINE_MOBILE_BYTES] ?: defaults.baselineMobileBytesAtReset,
|
||||
whitelistedPackages = prefs[Keys.WHITELISTED_PACKAGES] ?: defaults.whitelistedPackages,
|
||||
showSystemApps = prefs[Keys.SHOW_SYSTEM_APPS] ?: defaults.showSystemApps
|
||||
showSystemApps = prefs[Keys.SHOW_SYSTEM_APPS] ?: defaults.showSystemApps,
|
||||
passwordHash = prefs[Keys.PASSWORD_HASH],
|
||||
passwordSalt = prefs[Keys.PASSWORD_SALT]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,4 +101,29 @@ class SettingsRepository @Inject constructor(
|
||||
suspend fun rebaseline(currentMobileBytes: Long) {
|
||||
context.dataStore.edit { it[Keys.BASELINE_MOBILE_BYTES] = currentMobileBytes }
|
||||
}
|
||||
|
||||
/** Définit (ou supprime, si [password] est vide/null) le mot de passe de verrouillage. */
|
||||
suspend fun setPassword(password: String?) {
|
||||
if (password.isNullOrBlank()) {
|
||||
context.dataStore.edit {
|
||||
it.remove(Keys.PASSWORD_HASH)
|
||||
it.remove(Keys.PASSWORD_SALT)
|
||||
}
|
||||
} else {
|
||||
val salt = PasswordHasher.newSalt()
|
||||
val hash = PasswordHasher.hash(password, salt)
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[Keys.PASSWORD_HASH] = hash
|
||||
prefs[Keys.PASSWORD_SALT] = salt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Vrai si aucun mot de passe n'est défini, ou si [password] correspond au mot de passe stocké. */
|
||||
suspend fun verifyPassword(password: String): Boolean {
|
||||
val prefs = context.dataStore.data.first()
|
||||
val hash = prefs[Keys.PASSWORD_HASH] ?: return true
|
||||
val salt = prefs[Keys.PASSWORD_SALT] ?: return true
|
||||
return PasswordHasher.hash(password, salt) == hash
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user