manager: Refinement of module signatures again

This commit is contained in:
ShirkNeko
2025-08-03 18:50:20 +08:00
parent e3f1e49fe1
commit cd4edf97bd
8 changed files with 408 additions and 22 deletions

View File

@@ -70,11 +70,15 @@ data class ModuleInstallStatus(
val totalModules: Int = 0,
val currentModule: Int = 0,
val currentModuleName: String = "",
val failedModules: MutableList<String> = mutableListOf()
val failedModules: MutableList<String> = mutableListOf(),
val verifiedModules: MutableList<String> = mutableListOf() // 添加已验证模块列表
)
private var moduleInstallStatus = mutableStateOf(ModuleInstallStatus())
// 存储模块URI和验证状态的映射
private var moduleVerificationMap = mutableMapOf<Uri, Boolean>()
fun setFlashingStatus(status: FlashingStatus) {
currentFlashingStatus.value = status
}
@@ -83,7 +87,8 @@ fun updateModuleInstallStatus(
totalModules: Int? = null,
currentModule: Int? = null,
currentModuleName: String? = null,
failedModule: String? = null
failedModule: String? = null,
verifiedModule: String? = null
) {
val current = moduleInstallStatus.value
moduleInstallStatus.value = current.copy(
@@ -99,6 +104,18 @@ fun updateModuleInstallStatus(
failedModules = updatedFailedModules
)
}
if (verifiedModule != null) {
val updatedVerifiedModules = current.verifiedModules.toMutableList()
updatedVerifiedModules.add(verifiedModule)
moduleInstallStatus.value = moduleInstallStatus.value.copy(
verifiedModules = updatedVerifiedModules
)
}
}
fun setModuleVerificationStatus(uri: Uri, isVerified: Boolean) {
moduleVerificationMap[uri] = isVerified
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -142,6 +159,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
)
hasFlashCompleted = false
hasExecuted = false
moduleVerificationMap.clear()
}
}
is FlashIt.FlashModuleUpdate -> {
@@ -179,6 +197,11 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
setFlashingStatus(FlashingStatus.FAILED)
} else {
setFlashingStatus(FlashingStatus.SUCCESS)
// 处理模块更新成功后的验证标志
val isVerified = moduleVerificationMap[flashIt.uri] ?: false
ModuleOperationUtils.handleModuleUpdate(context, flashIt.uri, isVerified)
viewModel.markNeedRefresh()
}
if (showReboot) {
@@ -239,6 +262,28 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
}
} else {
setFlashingStatus(FlashingStatus.SUCCESS)
// 处理模块安装成功后的验证标志
when (flashIt) {
is FlashIt.FlashModule -> {
val isVerified = moduleVerificationMap[flashIt.uri] ?: false
ModuleOperationUtils.handleModuleInstallSuccess(context, flashIt.uri, isVerified)
if (isVerified) {
updateModuleInstallStatus(verifiedModule = moduleInstallStatus.value.currentModuleName)
}
}
is FlashIt.FlashModules -> {
val currentUri = flashIt.uris[flashIt.currentIndex]
val isVerified = moduleVerificationMap[currentUri] ?: false
ModuleOperationUtils.handleModuleInstallSuccess(context, currentUri, isVerified)
if (isVerified) {
updateModuleInstallStatus(verifiedModule = moduleInstallStatus.value.currentModuleName)
}
}
else -> {}
}
viewModel.markNeedRefresh()
}
if (showReboot) {

View File

@@ -189,6 +189,8 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
for (uri in selectedModules) {
val isVerified = verifyModuleSignature(context, uri)
verificationResults[uri] = isVerified
// 存储验证状态
setModuleVerificationStatus(uri, isVerified)
if (forceVerification && !isVerified) {
withContext(Dispatchers.Main) {
@@ -253,6 +255,8 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
// 验证模块签名
val forceVerification = prefs.getBoolean("force_signature_verification", false)
val isVerified = verifyModuleSignature(context, uri)
// 存储验证状态
setModuleVerificationStatus(uri, isVerified)
if (forceVerification && !isVerified) {
signatureDialogMessage = context.getString(R.string.module_signature_invalid_message)
@@ -835,7 +839,12 @@ private fun ModuleList(
downloadUrl,
fileName,
downloading,
onDownloaded = onUpdateModule,
onDownloaded = { uri ->
// 验证更新模块的签名
val isVerified = verifyModuleSignature(context, uri)
setModuleVerificationStatus(uri, isVerified)
onUpdateModule(uri)
},
onDownloading = {
launch(Dispatchers.Main) {
Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show()
@@ -867,6 +876,8 @@ private fun ModuleList(
val success = loadingDialog.withLoading {
withContext(Dispatchers.IO) {
if (isUninstall) {
// 卸载时移除验证标志
ModuleOperationUtils.handleModuleUninstall(module.dirId)
uninstallModule(module.dirId)
} else {
restoreModule(module.dirId)
@@ -1075,14 +1086,48 @@ fun ModuleItem(
Column(
modifier = Modifier.fillMaxWidth(0.8f)
) {
Text(
text = module.name,
fontSize = MaterialTheme.typography.titleMedium.fontSize,
fontWeight = FontWeight.SemiBold,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
textDecoration = textDecoration,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = module.name,
fontSize = MaterialTheme.typography.titleMedium.fontSize,
fontWeight = FontWeight.SemiBold,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
textDecoration = textDecoration,
modifier = Modifier.weight(1f, false)
)
// 显示验证标签
if (module.isVerified) {
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
) {
Icon(
imageVector = Icons.Default.Verified,
contentDescription = stringResource(R.string.module_signature_verified),
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(12.dp)
)
Spacer(modifier = Modifier.width(2.dp))
Text(
text = stringResource(R.string.module_verified),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimary,
fontWeight = FontWeight.Medium
)
}
}
}
}
Text(
text = "$moduleVersion: ${module.version}",
@@ -1309,6 +1354,8 @@ fun ModuleItemPreview() {
hasActionScript = false,
dirId = "dirId",
config = ModuleConfig(),
isVerified = true,
verificationTimestamp = System.currentTimeMillis()
)
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
}

View File

@@ -36,7 +36,7 @@ object ModuleUtils {
}
}?.removeSuffix(".zip") ?: context.getString(R.string.unknown_module)
var formattedFileName = fileName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim()
val formattedFileName = fileName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim()
var moduleName = formattedFileName
try {
@@ -55,12 +55,10 @@ object ModuleUtils {
if (entry.name == "module.prop") {
val reader = BufferedReader(InputStreamReader(zipInputStream, StandardCharsets.UTF_8))
var line: String?
var nameFound = false
while (reader.readLine().also { line = it } != null) {
if (line?.startsWith("name=") == true) {
moduleName = line.substringAfter("=")
moduleName = moduleName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim()
nameFound = true
break
}
}
@@ -105,6 +103,45 @@ object ModuleUtils {
Log.e(TAG, "Unable to get persistent permissions on URIs: $uri, Error: ${e.message}")
}
}
fun extractModuleId(context: Context, uri: Uri): String? {
if (uri == Uri.EMPTY) {
return null
}
return try {
val inputStream = context.contentResolver.openInputStream(uri)
if (inputStream == null) {
return null
}
val zipInputStream = ZipInputStream(inputStream)
var entry = zipInputStream.nextEntry
var moduleId: String? = null
// 遍历ZIP文件中的条目查找module.prop文件
while (entry != null) {
if (entry.name == "module.prop") {
val reader = BufferedReader(InputStreamReader(zipInputStream, StandardCharsets.UTF_8))
var line: String?
while (reader.readLine().also { line = it } != null) {
if (line?.startsWith("id=") == true) {
moduleId = line.substringAfter("=").trim()
break
}
}
break
}
entry = zipInputStream.nextEntry
}
zipInputStream.close()
moduleId
} catch (e: Exception) {
Log.e(TAG, "提取模块ID时发生异常: ${e.message}", e)
null
}
}
}
// 模块签名验证工具类
@@ -143,3 +180,71 @@ object ModuleSignatureUtils {
fun verifyModuleSignature(context: Context, moduleUri: Uri): Boolean {
return ModuleSignatureUtils.verifyModuleSignature(context, moduleUri)
}
object ModuleOperationUtils {
private const val TAG = "ModuleOperationUtils"
fun handleModuleInstallSuccess(context: Context, moduleUri: Uri, isSignatureVerified: Boolean) {
if (!isSignatureVerified) {
Log.d(TAG, "模块签名未验证,跳过创建验证标志")
return
}
try {
// 从ZIP文件提取模块ID
val moduleId = ModuleUtils.extractModuleId(context, moduleUri)
if (moduleId == null) {
Log.e(TAG, "无法提取模块ID无法创建验证标志")
return
}
// 创建验证标志文件
val success = ModuleVerificationManager.createVerificationFlag(moduleId)
if (success) {
Log.d(TAG, "模块 $moduleId 验证标志创建成功")
} else {
Log.e(TAG, "模块 $moduleId 验证标志创建失败")
}
} catch (e: Exception) {
Log.e(TAG, "处理模块安装成功时发生异常", e)
}
}
fun handleModuleUninstall(moduleId: String) {
try {
val success = ModuleVerificationManager.removeVerificationFlag(moduleId)
if (success) {
Log.d(TAG, "模块 $moduleId 验证标志移除成功")
} else {
Log.d(TAG, "模块 $moduleId 验证标志移除失败或不存在")
}
} catch (e: Exception) {
Log.e(TAG, "处理模块卸载时发生异常: $moduleId", e)
}
}
fun handleModuleUpdate(context: Context, moduleUri: Uri, isSignatureVerified: Boolean) {
try {
val moduleId = ModuleUtils.extractModuleId(context, moduleUri)
if (moduleId == null) {
Log.e(TAG, "无法提取模块ID无法处理验证标志")
return
}
if (isSignatureVerified) {
// 签名验证通过,创建或更新验证标志
val success = ModuleVerificationManager.createVerificationFlag(moduleId)
if (success) {
Log.d(TAG, "模块 $moduleId 更新后验证标志已更新")
} else {
Log.e(TAG, "模块 $moduleId 更新后验证标志更新失败")
}
} else {
// 签名验证失败,移除验证标志
ModuleVerificationManager.removeVerificationFlag(moduleId)
Log.d(TAG, "模块 $moduleId 更新后签名未验证,验证标志已移除")
}
} catch (e: Exception) {
Log.e(TAG, "处理模块更新时发生异常", e)
}
}
}

View File

@@ -0,0 +1,127 @@
package com.sukisu.ultra.ui.util
import android.util.Log
/**
* @author ShirkNeko
* @date 2025/8/3
*/
object ModuleVerificationManager {
private const val TAG = "ModuleVerificationManager"
private const val VERIFICATION_FLAGS_DIR = "/data/adb/ksu/verified_modules"
/**
* 为指定模块创建验证标志文件
*
* @param moduleId 模块文件夹名称
* @return 是否成功创建标志文件
*/
fun createVerificationFlag(moduleId: String): Boolean {
return try {
val shell = getRootShell()
val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId"
// 确保目录存在
val createDirCommand = "mkdir -p '$VERIFICATION_FLAGS_DIR'"
shell.newJob().add(createDirCommand).exec()
// 创建验证标志文件,写入验证时间戳
val timestamp = System.currentTimeMillis()
val command = "echo '$timestamp' > '$flagFilePath'"
val result = shell.newJob().add(command).exec()
if (result.isSuccess) {
Log.d(TAG, "验证标志文件创建成功: $flagFilePath")
true
} else {
Log.e(TAG, "验证标志文件创建失败: $moduleId")
false
}
} catch (e: Exception) {
Log.e(TAG, "创建验证标志文件时发生异常: $moduleId", e)
false
}
}
fun removeVerificationFlag(moduleId: String): Boolean {
return try {
val shell = getRootShell()
val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId"
val command = "rm -f '$flagFilePath'"
val result = shell.newJob().add(command).exec()
if (result.isSuccess) {
Log.d(TAG, "验证标志文件移除成功: $flagFilePath")
true
} else {
Log.e(TAG, "验证标志文件移除失败: $moduleId")
false
}
} catch (e: Exception) {
Log.e(TAG, "移除验证标志文件时发生异常: $moduleId", e)
false
}
}
fun getVerificationTimestamp(moduleId: String): Long {
return try {
val shell = getRootShell()
val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId"
val command = "cat '$flagFilePath' 2>/dev/null || echo '0'"
val result = shell.newJob().add(command).to(ArrayList(), null).exec()
if (result.isSuccess && result.out.isNotEmpty()) {
val timestampStr = result.out.firstOrNull()?.trim() ?: "0"
timestampStr.toLongOrNull() ?: 0L
} else {
0L
}
} catch (e: Exception) {
Log.e(TAG, "获取验证时间戳时发生异常: $moduleId", e)
0L
}
}
fun batchCheckVerificationStatus(moduleIds: List<String>): Map<String, Boolean> {
if (moduleIds.isEmpty()) return emptyMap()
return try {
val shell = getRootShell()
val result = mutableMapOf<String, Boolean>()
// 确保目录存在
val createDirCommand = "mkdir -p '$VERIFICATION_FLAGS_DIR'"
shell.newJob().add(createDirCommand).exec()
// 批量检查所有模块的验证标志文件
val commands = moduleIds.map { moduleId ->
"test -f '$VERIFICATION_FLAGS_DIR/$moduleId' && echo '$moduleId:true' || echo '$moduleId:false'"
}
val command = commands.joinToString(" && ")
val shellResult = shell.newJob().add(command).to(ArrayList(), null).exec()
if (shellResult.isSuccess) {
shellResult.out.forEach { line ->
val parts = line.split(":")
if (parts.size == 2) {
val moduleId = parts[0]
val isVerified = parts[1] == "true"
result[moduleId] = isVerified
}
}
}
Log.d(TAG, "批量验证检查完成,共检查 ${moduleIds.size} 个模块")
result
} catch (e: Exception) {
Log.e(TAG, "批量检查验证状态时发生异常", e)
// 返回默认值,所有模块都标记为未验证
moduleIds.associateWith { false }
}
}
}

View File

@@ -16,6 +16,7 @@ import kotlinx.coroutines.launch
import com.sukisu.ultra.ui.util.HanziToPinyin
import com.sukisu.ultra.ui.util.listModules
import com.sukisu.ultra.ui.util.getRootShell
import com.sukisu.ultra.ui.util.ModuleVerificationManager
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
@@ -86,6 +87,8 @@ class ModuleViewModel : ViewModel() {
val hasActionScript: Boolean,
val dirId: String, // real module id (dir name)
var config: ModuleConfig? = null,
var isVerified: Boolean = false, // 添加验证状态字段
var verificationTimestamp: Long = 0L, // 添加验证时间戳
)
var isRefreshing by mutableStateOf(false)
@@ -131,7 +134,7 @@ class ModuleViewModel : ViewModel() {
Log.i(TAG, "result: $result")
val array = JSONArray(result)
modules = (0 until array.length())
val moduleInfos = (0 until array.length())
.asSequence()
.map { array.getJSONObject(it) }
.map { obj ->
@@ -151,6 +154,26 @@ class ModuleViewModel : ViewModel() {
obj.getString("dir_id")
)
}.toList()
// 批量检查所有模块的验证状态
val moduleIds = moduleInfos.map { it.dirId }
val verificationStatus = ModuleVerificationManager.batchCheckVerificationStatus(moduleIds)
// 更新模块验证状态
modules = moduleInfos.map { moduleInfo ->
val isVerified = verificationStatus[moduleInfo.dirId] ?: false
val verificationTimestamp = if (isVerified) {
ModuleVerificationManager.getVerificationTimestamp(moduleInfo.dirId)
} else {
0L
}
moduleInfo.copy(
isVerified = isVerified,
verificationTimestamp = verificationTimestamp
)
}
launch {
modules.forEach { module ->
withContext(Dispatchers.IO) {
@@ -207,6 +230,14 @@ class ModuleViewModel : ViewModel() {
}
}
fun createModuleVerificationFlag(moduleId: String): Boolean {
return ModuleVerificationManager.createVerificationFlag(moduleId)
}
fun removeModuleVerificationFlag(moduleId: String): Boolean {
return ModuleVerificationManager.removeVerificationFlag(moduleId)
}
private fun sanitizeVersionString(version: String): String {
return version.replace(Regex("[^a-zA-Z0-9.\\-_]"), "_")
}
@@ -269,6 +300,31 @@ class ModuleViewModel : ViewModel() {
}
}
fun ModuleViewModel.ModuleInfo.copy(
id: String = this.id,
name: String = this.name,
author: String = this.author,
version: String = this.version,
versionCode: Int = this.versionCode,
description: String = this.description,
enabled: Boolean = this.enabled,
update: Boolean = this.update,
remove: Boolean = this.remove,
updateJson: String = this.updateJson,
hasWebUi: Boolean = this.hasWebUi,
hasActionScript: Boolean = this.hasActionScript,
dirId: String = this.dirId,
config: ModuleConfig? = this.config,
isVerified: Boolean = this.isVerified,
verificationTimestamp: Long = this.verificationTimestamp
): ModuleViewModel.ModuleInfo {
return ModuleViewModel.ModuleInfo(
id, name, author, version, versionCode, description,
enabled, update, remove, updateJson, hasWebUi, hasActionScript,
dirId, config, isVerified, verificationTimestamp
)
}
/**
* 模块大小缓存管理器
*/