- Firebase et le pare-feu local (trou noir, liste blanche par application, coupure cyclique locale) sont retirés - Client WireGuard officiel (GoBackend) : tout le trafic passe par le serveur familyd, qui filtre - Appairage par code ou lien familyd:// ; clé privée WireGuard et jeton chiffrés par l'Android Keystore - Remontées par le tunnel : état, temps d'écran, applications installées, évènements (file persistante) - Mode propriétaire (Device Owner) : VPN toujours actif avec blocage hors VPN, réglages verrouillés, garde-fou si le serveur ne répond plus pendant 15 min - Plan de la refonte : docs/PLAN-wireguard.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R9CBCVP5oDu2sfoYPisNCQ
116 lines
5.1 KiB
Kotlin
116 lines
5.1 KiB
Kotlin
package com.jttools.datacontrol.admin
|
|
|
|
import android.app.admin.DevicePolicyManager
|
|
import android.content.Context
|
|
import android.os.Build
|
|
import android.os.UserManager
|
|
import com.jttools.datacontrol.log.AppLog
|
|
|
|
/**
|
|
* Mode « propriétaire de l'appareil » (Device Owner), provisionné une fois par adb :
|
|
* adb shell dpm set-device-owner com.jttools.datacontrol/.admin.AppDeviceAdminReceiver
|
|
*
|
|
* DataControl impose alors au système : VPN toujours actif (connexions bloquées hors tunnel), réglages
|
|
* VPN / date et heure / DNS privé verrouillés, pas de réinitialisation, pas de mode sans échec, pas
|
|
* d'autre utilisateur, désinstallation impossible. Sans ce mode, toutes ces fonctions sont inertes.
|
|
*/
|
|
object OwnerPolicy {
|
|
|
|
/** Restrictions posées tant que la protection est active (le débogage USB reste permis : maintenance). */
|
|
private val RESTRICTIONS = buildList {
|
|
add(UserManager.DISALLOW_CONFIG_VPN)
|
|
add(UserManager.DISALLOW_CONFIG_DATE_TIME)
|
|
add(UserManager.DISALLOW_FACTORY_RESET)
|
|
add(UserManager.DISALLOW_SAFE_BOOT)
|
|
add(UserManager.DISALLOW_ADD_USER)
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) add(UserManager.DISALLOW_CONFIG_PRIVATE_DNS)
|
|
}
|
|
|
|
/** État voulu, appliqué tel quel ; [lockdown] = connexions bloquées hors VPN. */
|
|
data class Target(val protect: Boolean, val lockdown: Boolean)
|
|
|
|
private var applied: Target? = null
|
|
|
|
fun isDeviceOwner(context: Context): Boolean = AdminController.isDeviceOwner(context)
|
|
|
|
private fun dpm(context: Context) = context.getSystemService(DevicePolicyManager::class.java)
|
|
|
|
/** Applique [target] si DataControl est propriétaire et que l'état a changé. Renvoie faux en cas d'échec. */
|
|
fun sync(context: Context, target: Target, force: Boolean = false): Boolean {
|
|
if (!isDeviceOwner(context)) {
|
|
applied = null
|
|
return true
|
|
}
|
|
if (!force && target == applied) return true
|
|
val dpm = dpm(context)
|
|
val admin = AdminController.component(context)
|
|
return try {
|
|
if (target.protect) {
|
|
// Le système accorde lui-même l'autorisation VPN au paquet désigné : aucune fenêtre de consentement.
|
|
dpm.setAlwaysOnVpnPackage(admin, context.packageName, target.lockdown)
|
|
RESTRICTIONS.forEach { dpm.addUserRestriction(admin, it) }
|
|
dpm.setUninstallBlocked(admin, context.packageName, true)
|
|
requireAutoTime(context)
|
|
} else {
|
|
dpm.setAlwaysOnVpnPackage(admin, null, false)
|
|
RESTRICTIONS.forEach { dpm.clearUserRestriction(admin, it) }
|
|
dpm.setUninstallBlocked(admin, context.packageName, false)
|
|
}
|
|
applied = target
|
|
AppLog.i("Propriétaire", when {
|
|
!target.protect -> "Protections levées (contrôle désactivé ou appareil non associé)"
|
|
target.lockdown -> "VPN toujours actif, connexions bloquées hors VPN, réglages verrouillés"
|
|
else -> "VPN toujours actif SANS blocage hors VPN (serveur injoignable), réglages verrouillés"
|
|
})
|
|
true
|
|
} catch (e: Exception) {
|
|
AppLog.e("Propriétaire", "Application des protections impossible", e)
|
|
false
|
|
}
|
|
}
|
|
|
|
@Suppress("DEPRECATION")
|
|
private fun requireAutoTime(context: Context) {
|
|
val dpm = dpm(context)
|
|
val admin = AdminController.component(context)
|
|
runCatching {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
dpm.setAutoTimeEnabled(admin, true)
|
|
dpm.setAutoTimeZoneEnabled(admin, true)
|
|
} else {
|
|
dpm.setAutoTimeRequired(admin, true)
|
|
}
|
|
}
|
|
}
|
|
|
|
/** VPN toujours actif imposé par DataControl, et avec quel blocage. */
|
|
fun alwaysOnState(context: Context): Pair<Boolean, Boolean> {
|
|
if (!isDeviceOwner(context)) return false to false
|
|
val dpm = dpm(context)
|
|
val admin = AdminController.component(context)
|
|
return runCatching {
|
|
val pkg = dpm.getAlwaysOnVpnPackage(admin)
|
|
val lockdown = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && dpm.isAlwaysOnVpnLockdownEnabled(admin)
|
|
(pkg == context.packageName) to lockdown
|
|
}.getOrDefault(false to false)
|
|
}
|
|
|
|
/**
|
|
* Sortie du mode propriétaire (après vérification du mot de passe) : protections levées, puis
|
|
* DataControl redevient une application ordinaire (l'administrateur d'appareil simple reste actif).
|
|
*/
|
|
fun release(context: Context): Boolean {
|
|
if (!isDeviceOwner(context)) return true
|
|
sync(context, Target(protect = false, lockdown = false), force = true)
|
|
return runCatching {
|
|
dpm(context).clearDeviceOwnerApp(context.packageName)
|
|
applied = null
|
|
AppLog.w("Propriétaire", "Mode propriétaire de l'appareil retiré")
|
|
true
|
|
}.getOrElse {
|
|
AppLog.e("Propriétaire", "Retrait du mode propriétaire impossible", it)
|
|
false
|
|
}
|
|
}
|
|
}
|