From 4799a15909d0a4c4fb4e8c2f7febaf5aa825eb50 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sun, 1 Sep 2024 09:45:39 +0800 Subject: [PATCH] Retrieve security logs --- .../java/com/bintianqi/owndroid/Receiver.kt | 10 +- .../main/java/com/bintianqi/owndroid/Utils.kt | 18 ++-- .../java/com/bintianqi/owndroid/dpm/DPM.kt | 55 ++++++++--- .../com/bintianqi/owndroid/dpm/Network.kt | 25 +++-- .../bintianqi/owndroid/dpm/SystemManager.kt | 91 ++++++++++++++----- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 8 files changed, 148 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt index fc22f7c..1192d1d 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt @@ -23,6 +23,7 @@ import android.widget.Toast import com.bintianqi.owndroid.dpm.getDPM import com.bintianqi.owndroid.dpm.getReceiver import com.bintianqi.owndroid.dpm.handleNetworkLogs +import com.bintianqi.owndroid.dpm.handleSecurityLogs import com.bintianqi.owndroid.dpm.isDeviceAdmin import com.bintianqi.owndroid.dpm.isDeviceOwner import com.bintianqi.owndroid.dpm.isProfileOwner @@ -54,12 +55,19 @@ class Receiver : DeviceAdminReceiver() { override fun onNetworkLogsAvailable(context: Context, intent: Intent, batchToken: Long, networkLogsCount: Int) { super.onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount) - if(VERSION.SDK_INT >= 28) { + if(VERSION.SDK_INT >= 26) { CoroutineScope(Dispatchers.IO).launch { handleNetworkLogs(context, batchToken) } } } + + override fun onSecurityLogsAvailable(context: Context, intent: Intent) { + super.onSecurityLogsAvailable(context, intent) + if(VERSION.SDK_INT >= 24) { + handleSecurityLogs(context) + } + } } val installAppDone = MutableStateFlow(false) diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 952a9ee..384ce79 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -2,7 +2,11 @@ package com.bintianqi.owndroid import android.Manifest import android.app.admin.DevicePolicyManager -import android.content.* +import android.content.ClipData +import android.content.ClipboardManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build.VERSION @@ -17,7 +21,6 @@ import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream -import java.nio.file.Files import java.util.Locale lateinit var getFile: ActivityResultLauncher @@ -76,12 +79,13 @@ fun writeClipBoard(context: Context, string: String):Boolean{ } lateinit var requestPermission: ActivityResultLauncher -lateinit var saveNetworkLogs: ActivityResultLauncher +lateinit var exportFile: ActivityResultLauncher +val exportFilePath = MutableStateFlow(null) fun registerActivityResult(context: ComponentActivity){ getFile = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> activityResult.data.let { - if(it==null){ + if(it == null){ Toast.makeText(context.applicationContext, R.string.file_not_exist, Toast.LENGTH_SHORT).show() }else{ fileUriFlow.value = it.data @@ -96,13 +100,13 @@ fun registerActivityResult(context: ComponentActivity){ } } requestPermission = context.registerForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted.value = it } - saveNetworkLogs = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + exportFile = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val intentData = result.data ?: return@registerForActivityResult val uriData = intentData.data ?: return@registerForActivityResult + val path = exportFilePath.value ?: return@registerForActivityResult context.contentResolver.openOutputStream(uriData).use { outStream -> if(outStream != null) { - val logFile = context.filesDir.resolve("NetworkLogs.json") - logFile.inputStream().use { inStream -> + File(path).inputStream().use { inStream -> inStream.copyTo(outStream) } Toast.makeText(context.applicationContext, R.string.success, Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt index 50a52a3..edd5d0f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt @@ -15,7 +15,6 @@ import android.content.Intent import android.content.pm.IPackageInstaller import android.content.pm.PackageInstaller import android.content.pm.PackageManager -import android.os.Build import android.os.Build.VERSION import androidx.activity.result.ActivityResultLauncher import androidx.annotation.DrawableRes @@ -320,41 +319,39 @@ fun permissionList(): List{ return list } -@RequiresApi(Build.VERSION_CODES.O) +@RequiresApi(26) @OptIn(ExperimentalSerializationApi::class) fun handleNetworkLogs(context: Context, batchToken: Long) { - val events = context.getDPM().retrieveNetworkLogs(context.getReceiver(), batchToken) ?: return - val eventsList = mutableListOf() + val networkEvents = context.getDPM().retrieveNetworkLogs(context.getReceiver(), batchToken) ?: return val file = context.filesDir.toPath().resolve("NetworkLogs.json") if(file.notExists()) file.writeText("[]") val json = Json { ignoreUnknownKeys = true; explicitNulls = false } - var jsonObj: MutableList + var events: MutableList file.inputStream().use { - jsonObj = json.decodeFromStream(it) + events = json.decodeFromStream(it) } - events.forEach { event -> + networkEvents.forEach { event -> try { val dnsEvent = event as DnsEvent val addresses = mutableListOf() dnsEvent.inetAddresses.forEach { inetAddresses -> addresses += inetAddresses.hostAddress } - eventsList += NetworkEventItem( + events += NetworkEventItem( id = if(VERSION.SDK_INT >= 28) event.id else null, packageName = event.packageName , timestamp = event.timestamp, type = "dns", hostName = dnsEvent.hostname, hostAddresses = addresses, totalResolvedAddressCount = dnsEvent.totalResolvedAddressCount ) } catch(e: Exception) { val connectEvent = event as ConnectEvent - eventsList += NetworkEventItem( + events += NetworkEventItem( id = if(VERSION.SDK_INT >= 28) event.id else null, packageName = event.packageName, timestamp = event.timestamp, type = "connect", hostAddress = connectEvent.inetAddress.hostAddress, port = connectEvent.port ) } } - jsonObj.addAll(eventsList) file.outputStream().use { - json.encodeToStream(jsonObj, it) + json.encodeToStream(events, it) } } @@ -364,9 +361,43 @@ data class NetworkEventItem( @SerialName("package_name") val packageName: String, val timestamp: Long, val type: String, - val port: Int? = null, @SerialName("address") val hostAddress: String? = null, + val port: Int? = null, @SerialName("host_name") val hostName: String? = null, @SerialName("count") val totalResolvedAddressCount: Int? = null, @SerialName("addresses") val hostAddresses: List? = null ) + +@OptIn(ExperimentalSerializationApi::class) +@RequiresApi(24) +fun handleSecurityLogs(context: Context) { + val file = context.filesDir.resolve("SecurityLogs.json") + val json = Json { ignoreUnknownKeys = true; explicitNulls = false } + if(!file.exists()) file.writeText("[]") + val securityEvents = context.getDPM().retrieveSecurityLogs(context.getReceiver()) + securityEvents ?: return + val logs: MutableList + file.inputStream().use { + logs = json.decodeFromStream(it) + } + securityEvents.forEach { + logs += SecurityEventItem( + id = if(VERSION.SDK_INT >= 28) it.id else null, + tag = it.tag, timeNanos = it.timeNanos, + logLevel = if(VERSION.SDK_INT >= 28) it.logLevel else null, + data = it.data.toString() + ) + } + file.outputStream().use { + json.encodeToStream(logs, it) + } +} + +@Serializable +data class SecurityEventItem( + val id: Long?, + val tag: Int, + @SerialName("time_nanos") val timeNanos: Long, + @SerialName("log_level") val logLevel: Int?, + val data: String +) diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index 2716988..c4b6972 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -99,8 +99,9 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R +import com.bintianqi.owndroid.exportFile +import com.bintianqi.owndroid.exportFilePath import com.bintianqi.owndroid.formatFileSize -import com.bintianqi.owndroid.saveNetworkLogs import com.bintianqi.owndroid.selectedPackage import com.bintianqi.owndroid.toText import com.bintianqi.owndroid.ui.Animations @@ -169,6 +170,8 @@ private fun Home(navCtrl:NavHostController, scrollState: ScrollState, wifiMacDia val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner + val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) + val dhizuku = sharedPref.getBoolean("dhizuku", false) Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) { Text( text = stringResource(R.string.network), @@ -196,7 +199,7 @@ private fun Home(navCtrl:NavHostController, scrollState: ScrollState, wifiMacDia if(deviceOwner) { SubPageItem(R.string.recommended_global_proxy, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("RecommendedGlobalProxy") } } - if(VERSION.SDK_INT >= 26&&(deviceOwner || (profileOwner && dpm.isManagedProfile(receiver)))) { + if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || (profileOwner && dpm.isManagedProfile(receiver)))) { SubPageItem(R.string.retrieve_net_logs, "", R.drawable.description_fill0) { navCtrl.navigate("NetworkLog") } } if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) { @@ -629,7 +632,6 @@ private fun NetworkLog() { val receiver = context.getReceiver() val logFile = context.filesDir.resolve("NetworkLogs.json") var fileSize by remember { mutableLongStateOf(0) } - var fileExists by remember { mutableStateOf(logFile.exists()) } LaunchedEffect(Unit) { fileSize = logFile.length() } @@ -638,26 +640,29 @@ private fun NetworkLog() { Text(text = stringResource(R.string.retrieve_net_logs), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 5.dp)) SwitchItem(R.string.enable, "", null, { dpm.isNetworkLoggingEnabled(receiver) }, { dpm.setNetworkLoggingEnabled(receiver,it) }, padding = false) - if(fileExists) { - Text(stringResource(R.string.retrieved_logs_are, formatFileSize(fileSize))) + Text(stringResource(R.string.log_file_size_is, formatFileSize(fileSize))) + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { Button( onClick = { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setType("application/json") intent.putExtra(Intent.EXTRA_TITLE, "NetworkLogs.json") - saveNetworkLogs.launch(intent) + exportFilePath.value = logFile.path + exportFile.launch(intent) }, - modifier = Modifier.fillMaxWidth() + enabled = fileSize > 0, + modifier = Modifier.fillMaxWidth(0.49F) ) { Text(stringResource(R.string.export_logs)) } Button( onClick = { - Toast.makeText(context, if(logFile.delete()) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() - fileExists = logFile.exists() + logFile.delete() + fileSize = logFile.length() }, - modifier = Modifier.fillMaxWidth() + enabled = fileSize > 0, + modifier = Modifier.fillMaxWidth(0.96F) ) { Text(stringResource(R.string.delete_logs)) } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/SystemManager.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/SystemManager.kt index 66fe68d..d009373 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/SystemManager.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/SystemManager.kt @@ -41,7 +41,6 @@ import android.content.Intent import android.net.Uri import android.os.Build.VERSION import android.os.UserManager -import android.util.Log import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize @@ -78,6 +77,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -103,7 +103,10 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.StopLockTaskModeReceiver +import com.bintianqi.owndroid.exportFile +import com.bintianqi.owndroid.exportFilePath import com.bintianqi.owndroid.fileUriFlow +import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.getFile import com.bintianqi.owndroid.prepareForNotification import com.bintianqi.owndroid.selectedPackage @@ -118,6 +121,9 @@ import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.ui.TopBar import com.bintianqi.owndroid.uriToStream import kotlinx.coroutines.launch +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToStream import java.util.Date import java.util.TimeZone import java.util.concurrent.Executors @@ -181,6 +187,7 @@ private fun Home(navCtrl: NavHostController, scrollState: ScrollState, rebootDia val dpm = context.getDPM() val receiver = context.getReceiver() val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) + val dhizuku = sharedPref.getBoolean("dhizuku", false) val dangerousFeatures = sharedPref.getBoolean("dangerous_features", false) val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner @@ -219,7 +226,7 @@ private fun Home(navCtrl: NavHostController, scrollState: ScrollState, rebootDia if(deviceOwner || profileOwner) { SubPageItem(R.string.ca_cert, "", R.drawable.license_fill0) { navCtrl.navigate("CaCert") } } - if(VERSION.SDK_INT >= 26 && (deviceOwner || dpm.isOrgProfile(receiver))) { + if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || dpm.isOrgProfile(receiver))) { SubPageItem(R.string.security_logs, "", R.drawable.description_fill0) { navCtrl.navigate("SecurityLogs") } } if(VERSION.SDK_INT >= 23 && (deviceOwner || dpm.isOrgProfile(receiver))) { @@ -981,42 +988,78 @@ private fun CaCert() { } } +@OptIn(ExperimentalSerializationApi::class) @SuppressLint("NewApi") @Composable private fun SecurityLogs() { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() + val logFile = context.filesDir.resolve("SecurityLogs.json") + var fileSize by remember { mutableLongStateOf(0) } + LaunchedEffect(Unit) { + fileSize = logFile.length() + } Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.security_logs), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 5.dp)) - Text(text = stringResource(R.string.developing)) - SwitchItem(R.string.enable, "", null, { dpm.isSecurityLoggingEnabled(receiver) }, { dpm.setSecurityLoggingEnabled(receiver,it) }, padding = false) - Button( - onClick = { - val log = dpm.retrieveSecurityLogs(receiver) - if(log!=null) { - for(i in log) { Log.d("SecureLog",i.toString()) } - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() - }else{ - Log.d("SecureLog", context.getString(R.string.none)) - Toast.makeText(context, R.string.no_logs, Toast.LENGTH_SHORT).show() - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.security_logs)) + SwitchItem(R.string.enable, "", null, { dpm.isSecurityLoggingEnabled(receiver) }, { dpm.setSecurityLoggingEnabled(receiver, it) }, padding = false) + Text(stringResource(R.string.log_file_size_is, formatFileSize(fileSize))) + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Button( + onClick = { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.setType("application/json") + intent.putExtra(Intent.EXTRA_TITLE, "SecurityLogs.json") + exportFilePath.value = logFile.path + exportFile.launch(intent) + }, + enabled = fileSize > 0, + modifier = Modifier.fillMaxWidth(0.49F) + ) { + Text(stringResource(R.string.export_logs)) + } + Button( + onClick = { + logFile.delete() + fileSize = logFile.length() + }, + enabled = fileSize > 0, + modifier = Modifier.fillMaxWidth(0.96F) + ) { + Text(stringResource(R.string.delete_logs)) + } } + Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { - val log = dpm.retrievePreRebootSecurityLogs(receiver) - if(log!=null) { - for(i in log) { Log.d("SecureLog",i.toString()) } - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() - }else{ - Log.d("SecureLog", context.getString(R.string.none)) + val logs = dpm.retrievePreRebootSecurityLogs(receiver) + if(logs == null) { Toast.makeText(context, R.string.no_logs, Toast.LENGTH_SHORT).show() + return@Button + } else { + val logsList = mutableListOf() + logs.forEach { + logsList += SecurityEventItem( + id = if(VERSION.SDK_INT >= 28) it.id else null, + tag = it.tag, timeNanos = it.timeNanos, + logLevel = if(VERSION.SDK_INT >= 28) it.logLevel else null, + data = it.data.toString() + ) + } + val preRebootSecurityLogs = context.filesDir.resolve("PreRebootSecurityLogs") + preRebootSecurityLogs.outputStream().use { + val json = Json { ignoreUnknownKeys = true; explicitNulls = false } + json.encodeToStream(logsList, it) + } + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.setType("application/json") + intent.putExtra(Intent.EXTRA_TITLE, "PreRebootSecurityLogs.json") + exportFilePath.value = preRebootSecurityLogs.path + exportFile.launch(intent) } }, modifier = Modifier.fillMaxWidth() diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 8f263c2..74b5324 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -241,7 +241,7 @@ Invalid config Exclude hosts Ağ kayıtları - Retrieved logs: %1$s + Log file size: %1$s Delete logs Export logs Geri al diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index be9f59c..a85e0e4 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -236,7 +236,7 @@ 无效配置 排除列表 收集网络日志 - 已收集的日志:%1$s + 日志文件大小:%1$s 删除日志 导出日志 收集 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2d7b46..5db16d3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -245,7 +245,7 @@ Invalid config Exclude hosts Network logs - Retrieved logs: %1$s + Log file size: %1$s Delete logs Export logs Retrieve