fix: corrections de l'audit de sécurité et de robustesse
- VPN retiré : le contrôle reste activé (alerte parent, tunnel remonté après 30 s, autorisation redemandée à l'ouverture) - Horloge de référence : changer date/heure/fuseau ne contourne plus les plages horaires ni le temps d'écran (signalé au parent) - Mot de passe : PBKDF2 + attente croissante après 3 échecs ; conversion automatique des anciens hachages - Écrans protégés : aucun affichage avant chargement des réglages ; effacement du journal protégé - Signature release via keystore.properties (même clé que les versions publiées) - Sauvegarde Google : réglages, appairage et journal exclus - Alerte admin fiable (goAsync), relance refusée signalée, liste d'applis renvoyée à chaque installation, code d'appairage consommé - Journal thread-safe et sans recopie à chaque ligne ; correctifs lint - Tests unitaires (mot de passe, règles, versions, config Firebase) ; README aligné sur le comportement réel Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R9CBCVP5oDu2sfoYPisNCQ
This commit is contained in:
1 parent
f33776bbb5
commit
e269e9932a
37 files changed
+809
-147
No files matched your search
@@ -0,0 +1,38 @@
|
||||
package com.jttools.datacontrol.data
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.security.MessageDigest
|
||||
|
||||
class PasswordHasherTest {
|
||||
|
||||
@Test fun `bon et mauvais mot de passe`() {
|
||||
val salt = PasswordHasher.newSalt()
|
||||
val hash = PasswordHasher.hash("1234", salt)
|
||||
assertTrue(hash.startsWith("pbkdf2:"))
|
||||
assertTrue(PasswordHasher.verify("1234", salt, hash))
|
||||
assertFalse(PasswordHasher.verify("1235", salt, hash))
|
||||
assertFalse(PasswordHasher.needsUpgrade(hash))
|
||||
}
|
||||
|
||||
@Test fun `le sel change le hachage`() {
|
||||
val a = PasswordHasher.hash("1234", PasswordHasher.newSalt())
|
||||
val b = PasswordHasher.hash("1234", PasswordHasher.newSalt())
|
||||
assertTrue(a != b)
|
||||
}
|
||||
|
||||
@Test fun `ancien format SHA-256 toujours accepte et a convertir`() {
|
||||
val salt = "00112233445566778899aabbccddeeff"
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
md.update(salt.toByteArray())
|
||||
val legacy = md.digest("secret".toByteArray()).joinToString("") { "%02x".format(it) }
|
||||
assertTrue(PasswordHasher.verify("secret", salt, legacy))
|
||||
assertFalse(PasswordHasher.verify("autre", salt, legacy))
|
||||
assertTrue(PasswordHasher.needsUpgrade(legacy))
|
||||
}
|
||||
|
||||
@Test fun `hachage illisible refuse`() {
|
||||
assertFalse(PasswordHasher.verify("1234", PasswordHasher.newSalt(), "pbkdf2:abc:00"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.jttools.datacontrol.data
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class UpdateManagerTest {
|
||||
@Test fun `comparaison de versions`() {
|
||||
assertTrue(UpdateManager.isNewer("0.1.9", "0.1.8"))
|
||||
assertTrue(UpdateManager.isNewer("0.2.0", "0.1.10"))
|
||||
assertTrue(UpdateManager.isNewer("0.1.10", "0.1.9")) // numérique, pas alphabétique
|
||||
assertTrue(UpdateManager.isNewer("1.0", "0.9.9"))
|
||||
assertFalse(UpdateManager.isNewer("0.1.8", "0.1.8"))
|
||||
assertFalse(UpdateManager.isNewer("0.1.8", "0.1.8.0"))
|
||||
assertFalse(UpdateManager.isNewer("0.1.7", "0.1.8"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.jttools.datacontrol.guard
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class RulesTest {
|
||||
|
||||
// 2026-10-26 est un lundi.
|
||||
private fun at(day: Int, h: Int, m: Int = 0) = LocalDateTime.of(2026, 10, 25, h, m).plusDays(day.toLong())
|
||||
|
||||
private val json = """
|
||||
{"schedule":{"enabled":true,"cutsMobile":false,
|
||||
"windows":[{"days":[1,2,3,4,5],"start":480,"end":1200},{"days":[5],"start":1320,"end":120}]},
|
||||
"dailyLimitMin":90,
|
||||
"appLimits":[{"pkg":"com.game","min":30}],
|
||||
"blocked":["com.bad"],
|
||||
"bonus":{"date":"2026-10-26","globalMin":15,"apps":[{"pkg":"com.game","min":10}]}}
|
||||
""".trimIndent()
|
||||
|
||||
@Test fun `lecture complete`() {
|
||||
val r = Rules.parse(json)
|
||||
assertTrue(r.scheduleEnabled)
|
||||
assertFalse(r.scheduleCutsMobile)
|
||||
assertEquals(2, r.windows.size)
|
||||
assertEquals(90, r.dailyLimitMin)
|
||||
assertEquals(listOf(AppLimit("com.game", 30)), r.appLimits)
|
||||
assertEquals(setOf("com.bad"), r.blocked)
|
||||
assertEquals("2026-10-26", r.bonusDate)
|
||||
assertEquals(15, r.bonusGlobalMin)
|
||||
assertFalse(r.isEmpty)
|
||||
}
|
||||
|
||||
@Test fun `json vide ou invalide donne aucune regle`() {
|
||||
assertTrue(Rules.parse(null).isEmpty)
|
||||
assertTrue(Rules.parse("").isEmpty)
|
||||
assertTrue(Rules.parse("{pas du json").isEmpty)
|
||||
}
|
||||
|
||||
@Test fun `plage de journee`() {
|
||||
val r = Rules.parse(json)
|
||||
assertTrue(r.isOpenAt(at(1, 8))) // lundi 08:00 : début inclus
|
||||
assertTrue(r.isOpenAt(at(1, 19, 59)))
|
||||
assertFalse(r.isOpenAt(at(1, 20))) // fin exclue
|
||||
assertFalse(r.isOpenAt(at(1, 7, 59)))
|
||||
assertFalse(r.isOpenAt(at(6, 12))) // samedi : pas de plage de journée
|
||||
}
|
||||
|
||||
@Test fun `plage qui passe minuit deborde sur le lendemain`() {
|
||||
val r = Rules.parse(json)
|
||||
assertTrue(r.isOpenAt(at(5, 23))) // vendredi 23:00
|
||||
assertTrue(r.isOpenAt(at(6, 1, 30))) // samedi 01:30, suite de la plage du vendredi
|
||||
assertFalse(r.isOpenAt(at(6, 2)))
|
||||
assertFalse(r.isOpenAt(at(7, 1))) // dimanche 01:00 : pas de plage samedi soir
|
||||
}
|
||||
|
||||
@Test fun `prochaine ouverture`() {
|
||||
val r = Rules.parse(json)
|
||||
assertEquals("Reprise à 08:00.", r.nextOpeningText(at(1, 6)))
|
||||
assertEquals("Reprise à 08:00 (dans 2 j).", r.nextOpeningText(at(6, 12)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.jttools.datacontrol.remote
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import java.util.Base64
|
||||
|
||||
class FirebaseConfigTest {
|
||||
private val expected = FirebaseConfig("AIzaTest", "1:123:android:abc", "mon-projet")
|
||||
|
||||
@Test fun `bloc firebaseConfig de la console`() {
|
||||
val js = """
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaTest",
|
||||
authDomain: "mon-projet.firebaseapp.com",
|
||||
projectId: "mon-projet",
|
||||
appId: "1:123:android:abc"
|
||||
};
|
||||
""".trimIndent()
|
||||
assertEquals(expected, FirebaseConfig.parse(js))
|
||||
}
|
||||
|
||||
@Test fun `json`() {
|
||||
assertEquals(
|
||||
expected,
|
||||
FirebaseConfig.parse("""{"apiKey":"AIzaTest","appId":"1:123:android:abc","projectId":"mon-projet"}""")
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun `chaine fc1 de FamilyControl`() {
|
||||
val b64 = Base64.getEncoder().encodeToString(
|
||||
"""{"apiKey":"AIzaTest","appId":"1:123:android:abc","projectId":"mon-projet"}""".toByteArray()
|
||||
)
|
||||
assertEquals(expected, FirebaseConfig.parse("fc1:$b64"))
|
||||
assertEquals(expected, FirebaseConfig.parse(" fc1:${b64.chunked(20).joinToString("\n")} "))
|
||||
}
|
||||
|
||||
@Test fun `champ manquant ou illisible`() {
|
||||
assertNull(FirebaseConfig.parse("""{"apiKey":"AIzaTest","projectId":"mon-projet"}"""))
|
||||
assertNull(FirebaseConfig.parse("fc1:!!!"))
|
||||
assertNull(FirebaseConfig.parse(""))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user