Files
DataControl/app/src/main/java/com/jttools/datacontrol/admin/OwnerPolicy.kt
T
tuxgyverandClaude Opus 5.5 33ea4ff947 feat: tunnel WireGuard vers le serveur familyd et mode propriétaire de l'appareil
- 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
2026-10-25 14:44:18 +01:00

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
}
}
}