New CA certificates manager

This commit is contained in:
BinTianqi
2025-02-16 13:28:56 +08:00
parent d8044ee8ab
commit fd0073f907
8 changed files with 182 additions and 49 deletions

View File

@@ -14,6 +14,7 @@ Use Android Device owner privilege to manage your device.
- System - System
- Options: disable camera, disable screenshot, master volume mute, disable USB signal... - Options: disable camera, disable screenshot, master volume mute, disable USB signal...
- Permission policy - Permission policy
- Manage CA certificates
- _Wipe data_ - _Wipe data_
- ... - ...
- Network - Network

View File

@@ -14,7 +14,8 @@
- 系统 - 系统
- 选项禁用摄像头、禁止截屏、全局静音、禁用USB信号... - 选项禁用摄像头、禁止截屏、全局静音、禁用USB信号...
- 权限策略 - 权限策略
- 清除数据 - 管理CA证书
- _清除数据_
- ... - ...
- 网络 - 网络
- 添加/修改/删除 Wi-Fi - 添加/修改/删除 Wi-Fi
@@ -45,7 +46,7 @@
- 创建用户 - 创建用户
- ... - ...
- 密码与锁屏 - 密码与锁屏
- 重置密码 - _重置密码_
- 要求密码复杂度 - 要求密码复杂度
- 设置屏幕超时 - 设置屏幕超时
- ... - ...

View File

@@ -87,6 +87,9 @@ fun parseTimestamp(timestamp: Long): String {
return formatter.format(instant) return formatter.format(instant)
} }
fun parseDate(date: Date)
= SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(date)
val Long.humanReadableDate: String val Long.humanReadableDate: String
get() = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(Date(this)) get() = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(Date(this))

View File

@@ -67,16 +67,21 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.colorScheme
@@ -125,6 +130,7 @@ import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.SharedPrefs import com.bintianqi.owndroid.SharedPrefs
import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.humanReadableDate import com.bintianqi.owndroid.humanReadableDate
import com.bintianqi.owndroid.parseDate
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.CheckBoxItem
import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.FunctionItem
@@ -135,10 +141,15 @@ import com.bintianqi.owndroid.ui.NavIcon
import com.bintianqi.owndroid.ui.RadioButtonItem import com.bintianqi.owndroid.ui.RadioButtonItem
import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.ui.SwitchItem
import com.bintianqi.owndroid.uriToStream import com.bintianqi.owndroid.uriToStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.Date import java.util.Date
import java.util.TimeZone import java.util.TimeZone
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -1228,62 +1239,165 @@ private fun ColumnScope.LockTaskFeatures() {
} }
} }
data class CaCertInfo(
val hash: String,
val data: ByteArray
)
@Serializable object CaCert @Serializable object CaCert
@OptIn(ExperimentalMaterial3Api::class, ExperimentalStdlibApi::class)
@Composable @Composable
fun CaCertScreen(onNavigateUp: () -> Unit) { fun CaCertScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val dpm = context.getDPM() val dpm = context.getDPM()
val receiver = context.getReceiver() val receiver = context.getReceiver()
var dialog by remember { mutableStateOf(false) } /** 0:none, 1:install, 2:info, 3:uninstall all */
var caCertByteArray = remember { byteArrayOf() } var dialog by remember { mutableIntStateOf(0) }
val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> var caCertByteArray by remember { mutableStateOf(byteArrayOf()) }
uri ?: return@rememberLauncherForActivityResult val coroutine = rememberCoroutineScope()
val getCertLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {uri ->
if(uri != null) {
uriToStream(context, uri) { uriToStream(context, uri) {
caCertByteArray = it.readBytes() caCertByteArray = it.readBytes()
} }
dialog = true dialog = 1
} }
MyScaffold(R.string.ca_cert, 8.dp, onNavigateUp) {
Button(
onClick = {
getFileLauncher.launch("*/*")
},
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
) {
Text(stringResource(R.string.select_ca_cert))
} }
Button( val exportCertLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
onClick = { if(uri != null) {
dpm.uninstallAllUserCaCerts(receiver) context.contentResolver.openOutputStream(uri)?.use {
it.write(caCertByteArray)
}
context.showOperationResultToast(true) context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.uninstall_all_user_ca_cert))
} }
if(dialog) { }
val exist = dpm.hasCaCertInstalled(receiver, caCertByteArray) val caCerts = remember { mutableStateListOf<CaCertInfo>() }
AlertDialog( fun refresh() {
caCerts.clear()
coroutine.launch(Dispatchers.IO) {
val md = MessageDigest.getInstance("SHA-256")
dpm.getInstalledCaCerts(receiver).forEach { ba ->
val hash = md.digest(ba).toHexString()
withContext(Dispatchers.Main) { caCerts += CaCertInfo(hash, ba) }
}
}
}
LaunchedEffect(Unit) { refresh() }
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.ca_cert)) },
navigationIcon = { NavIcon(onNavigateUp) },
actions = {
IconButton({ dialog = 3 }) {
Icon(Icons.Outlined.Delete, stringResource(R.string.delete))
}
}
)
},
floatingActionButton = {
FloatingActionButton({
Toast.makeText(context, R.string.select_ca_cert, Toast.LENGTH_SHORT).show()
getCertLauncher.launch(arrayOf("*/*"))
}) {
Icon(Icons.Default.Add, stringResource(R.string.install))
}
}
) { paddingValues ->
LazyColumn(
Modifier.fillMaxSize().padding(paddingValues),
horizontalAlignment = Alignment.CenterHorizontally
) {
items(caCerts, { it.hash }) { cert ->
Column(
Modifier.fillMaxWidth().clickable{
caCertByteArray = cert.data
dialog = 2
}.animateItem().padding(vertical = 10.dp, horizontal = 8.dp)
) {
Text(cert.hash.substring(0..7))
}
HorizontalDivider()
}
item {
if(caCerts.isEmpty()) Text(stringResource(R.string.no_ca_cert), Modifier.padding(top = 8.dp), colorScheme.onSurfaceVariant)
else Spacer(Modifier.padding(vertical = 30.dp))
}
}
if(dialog != 0) AlertDialog(
text = {
if(dialog == 3) Text(stringResource(R.string.uninstall_all_user_ca_cert))
else {
var text = ""
val sha256 = MessageDigest.getInstance("SHA-256").digest(caCertByteArray).toHexString()
try {
val cf = CertificateFactory.getInstance("X.509")
val cert = cf.generateCertificate(caCertByteArray.inputStream()) as X509Certificate
text = "Serial number\n" + cert.serialNumber.toString(16) + "\n\n" +
"Subject\n" + cert.subjectX500Principal.name + "\n\n" +
"Issuer\n" + cert.issuerX500Principal.name + "\n\n" +
"Issued on: " + parseDate(cert.notBefore) + "\n" +
"Expires on: " + parseDate(cert.notAfter) + "\n\n" +
"SHA-256 fingerprint" + "\n$sha256"
} catch(e: Exception) {
e.printStackTrace()
text = stringResource(R.string.parse_cert_failed)
}
Column(Modifier.verticalScroll(rememberScrollState())) {
SelectionContainer {
Text(text)
}
if(dialog == 2) Row(Modifier.fillMaxWidth().padding(top = 4.dp), Arrangement.SpaceBetween) {
TextButton(
onClick = {
dpm.uninstallCaCert(receiver, caCertByteArray)
refresh()
dialog = 0
},
modifier = Modifier.fillMaxWidth(0.49F),
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error)
) {
Text(stringResource(R.string.uninstall))
}
FilledTonalButton(
onClick = {
exportCertLauncher.launch(sha256.substring(0..7) + ".0")
},
modifier = Modifier.fillMaxWidth(0.96F)
) {
Text(stringResource(R.string.export))
}
}
}
}
},
confirmButton = { confirmButton = {
TextButton({ TextButton({
if(exist) { try {
dpm.uninstallCaCert(receiver, caCertByteArray) if(dialog == 1) {
} else { context.showOperationResultToast(dpm.installCaCert(receiver, caCertByteArray))
val result = dpm.installCaCert(receiver, caCertByteArray) }
context.showOperationResultToast(result) if(dialog == 3) {
dpm.uninstallAllUserCaCerts(receiver)
}
refresh()
dialog = 0
} catch(e: Exception) {
e.printStackTrace()
context.showOperationResultToast(false)
} }
dialog = false
}) { }) {
Text(stringResource(if(exist) R.string.uninstall else R.string.install)) Text(stringResource(if(dialog == 1) R.string.install else R.string.confirm))
} }
}, },
dismissButton = { dismissButton = {
TextButton({ dialog = false }) { Text(stringResource(R.string.cancel)) } if(dialog != 2) TextButton({ dialog = 0 }) {
}, Text(stringResource(R.string.cancel))
onDismissRequest = { dialog = false }
)
} }
},
onDismissRequest = { dialog = 0 }
)
} }
} }

View File

@@ -195,7 +195,11 @@
<string name="ltf_keyguard">Разрешить блокировку экрана</string> <string name="ltf_keyguard">Разрешить блокировку экрана</string>
<string name="ltf_block_activity_start_in_task">Блокировать запуск активности в задаче</string> <string name="ltf_block_activity_start_in_task">Блокировать запуск активности в задаче</string>
<string name="ca_cert">CA-сертификат</string> <string name="ca_cert">CA-сертификат</string>
<string name="select_ca_cert" tools:ignore="TypographyEllipsis">Выберите сертификат...</string> <!--TODO: 4 strings-->
<string name="no_ca_cert">No user CA certificate installed</string>
<string name="parse_cert_failed">Failed to parse certificate</string>
<string name="export">Export</string>
<string name="select_ca_cert">Select a CA certificate</string>
<string name="uninstall_all_user_ca_cert">Удалить все пользовательские CA-сертификаты</string> <string name="uninstall_all_user_ca_cert">Удалить все пользовательские CA-сертификаты</string>
<string name="security_logging">Журнал безопасности</string> <string name="security_logging">Журнал безопасности</string>
<string name="pre_reboot_security_logs">Журналы безопасности перед перезагрузкой</string> <string name="pre_reboot_security_logs">Журналы безопасности перед перезагрузкой</string>

View File

@@ -201,8 +201,12 @@
<string name="ltf_global_actions">Küresel eylemlere izin ver</string> <string name="ltf_global_actions">Küresel eylemlere izin ver</string>
<string name="ltf_keyguard">Ekran kilidine izin ver</string> <string name="ltf_keyguard">Ekran kilidine izin ver</string>
<string name="ltf_block_activity_start_in_task">Görevde etkinlik başlatmayı engelle</string> <string name="ltf_block_activity_start_in_task">Görevde etkinlik başlatmayı engelle</string>
<string name="ca_cert">CA sertifikası</string> <!--TODO--> <string name="ca_cert">CA sertifikası</string>
<string name="select_ca_cert" tools:ignore="TypographyEllipsis">Sertifika seç...</string> <!--TODO--> <!--TODO: 4 strings-->
<string name="no_ca_cert">No user CA certificate installed</string>
<string name="parse_cert_failed">Failed to parse certificate</string>
<string name="export">Export</string>
<string name="select_ca_cert">Select a CA certificate</string>
<string name="uninstall_all_user_ca_cert">Tüm kullanıcı sertifikalarını kaldır</string> <!--TODO--> <string name="uninstall_all_user_ca_cert">Tüm kullanıcı sertifikalarını kaldır</string> <!--TODO-->
<string name="security_logging">Security logging</string> <!--TODO--> <string name="security_logging">Security logging</string> <!--TODO-->
<string name="pre_reboot_security_logs">Yeniden başlatmadan önce güvenlik kayıtları</string> <string name="pre_reboot_security_logs">Yeniden başlatmadan önce güvenlik kayıtları</string>

View File

@@ -193,7 +193,10 @@
<string name="package_name">包名</string> <string name="package_name">包名</string>
<string name="not_exist">不存在</string> <string name="not_exist">不存在</string>
<string name="ca_cert">CA证书</string> <string name="ca_cert">CA证书</string>
<string name="select_ca_cert" tools:ignore="TypographyEllipsis">选择证书...</string> <string name="no_ca_cert">没有已安装的CA证书</string>
<string name="parse_cert_failed">解析证书失败</string>
<string name="export">导出</string>
<string name="select_ca_cert">选择一个CA证书</string>
<string name="uninstall_all_user_ca_cert">卸载所有用户证书</string> <string name="uninstall_all_user_ca_cert">卸载所有用户证书</string>
<string name="security_logging">安全日志</string> <string name="security_logging">安全日志</string>
<string name="pre_reboot_security_logs">重启前安全日志</string> <string name="pre_reboot_security_logs">重启前安全日志</string>

View File

@@ -222,7 +222,10 @@
<string name="ltf_keyguard">Allow keyguard</string> <string name="ltf_keyguard">Allow keyguard</string>
<string name="ltf_block_activity_start_in_task">Block activity start in task</string> <string name="ltf_block_activity_start_in_task">Block activity start in task</string>
<string name="ca_cert">CA certificate</string> <string name="ca_cert">CA certificate</string>
<string name="select_ca_cert" tools:ignore="TypographyEllipsis">Select certificate...</string> <string name="no_ca_cert">No user CA certificate installed</string>
<string name="parse_cert_failed">Failed to parse certificate</string>
<string name="export">Export</string>
<string name="select_ca_cert">Select a CA certificate</string>
<string name="uninstall_all_user_ca_cert">Uninstall all user CA certificate</string> <string name="uninstall_all_user_ca_cert">Uninstall all user CA certificate</string>
<string name="security_logging">Security logging</string> <string name="security_logging">Security logging</string>
<string name="pre_reboot_security_logs">Pre-reboot security logs</string> <string name="pre_reboot_security_logs">Pre-reboot security logs</string>