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)
@@ -1074,6 +1085,10 @@ fun ModuleItem(
Column(
modifier = Modifier.fillMaxWidth(0.8f)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = module.name,
@@ -1082,8 +1097,38 @@ fun ModuleItem(
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}",
fontSize = MaterialTheme.typography.bodySmall.fontSize,
@@ -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
)
}
/**
* 模块大小缓存管理器
*/

View File

@@ -539,8 +539,8 @@
<string name="susfs_backup_info_date">Ngày sao lưu: %s</string>
<string name="susfs_backup_info_device">Thiết bị: %s</string>
<string name="susfs_backup_info_version">Phiên bản: %s</string>
<string name="hide_bl_script">Trạng thái khóa</string>
<string name="hide_bl_script_description">Ghi đè thuộc tính trạng thái khóa khởi động ở chế độ dịch vụ late_start</string>
<string name="hide_bl_script">Trạng thái Lock BL</string>
<string name="hide_bl_script_description">Ghi đè thuộc tính trạng thái lock bootloader ở chế độ dịch vụ late_start</string>
<string name="cleanup_residue">Dọn rác</string>
<string name="cleanup_residue_description">Dọn dẹp các file và folder còn sót lại của các module và công cụ (Có thể bị xóa nhầm, dẫn đến mất dữ liệu và không khởi động được)</string>
<string name="susfs_edit_sus_path">Chỉnh sửa Đường dẫn SuS</string>
@@ -607,9 +607,9 @@
<string name="sus_loop_path_feature_label">Đường dẫn Vòng lặp SuS</string>
<string name="sus_loop_paths_description_title">Cấu hình Đường dẫn Vòng lặp</string>
<string name="sus_loop_paths_description_text">Đường dẫn Vòng lặp được đổi tên thành SUS_PATH mỗi khi một ứng dụng không phải root hoặc dịch vụ cô lập được khởi động. Điều này giúp giải quyết vấn đề đường dẫn đã thêm có thể trở nên không hợp lệ do trạng thái inode được đặt lại hoặc inode được tạo lại trong Kernel</string>
<string name="module_signature_verification">Xác minh chữ ký module</string>
<string name="module_signature_verification_summary">Buộc xác minh chữ ký khi cài đặt module</string>
<string name="module_signature_invalid">Xác minh chữ ký module thất bại</string>
<string name="module_signature_invalid_message">Cài đặt sẽ bị chặn do cài đặt bảo mật</string>
<string name="module_signature_verification_failed">Quá trình cài đặt sẽ tiếp tục, vui lòng chú ý xem có tập lệnh mã hóa không xác định nào trong module hay không</string>
<string name="module_signature_verification">Xác minh chữ ký</string>
<string name="module_signature_verification_summary">Buộc xác minh chữ ký khi cài đặt module (Chỉ khả dụng cho arm64-v8a)</string>
<string name="module_signature_invalid">Tác giả không xác định</string>
<string name="module_signature_invalid_message">Các module chưa được ký có thể chưa hoàn chỉnh. Để bảo vệ thiết bị của bạn, module này đã bị chặn cài đặt</string>
<string name="module_signature_verification_failed">Các module chưa được ký có thể chưa hoàn chỉnh. Bạn có muốn cài đặt module này từ một tác giả chưa xác định không?</string>
</resources>

View File

@@ -606,6 +606,9 @@
<string name="sus_loop_path_feature_label">SUS循环路径</string>
<string name="sus_loop_paths_description_title">循环路径配置</string>
<string name="sus_loop_paths_description_text">循环路径会在每次非root用户应用或隔离服务启动时重新标记为SUS_PATH。这有助于解决添加的路径可能因inode状态重置或内核中inode重新创建而失效的问题</string>
<!-- 模块签名功能描述 -->
<string name="module_verified">已验证</string>
<string name="module_signature_verified">模块签名已验证</string>
<string name="module_signature_verification">验证签名</string>
<string name="module_signature_verification_summary">模块安装时,强制验证签名。(仅 arm64-v8a 可用)</string>
<string name="module_signature_invalid">未知发布者</string>

View File

@@ -609,6 +609,9 @@
<string name="sus_loop_path_feature_label">SUS Loop Path</string>
<string name="sus_loop_paths_description_title">Loop Path Configuration</string>
<string name="sus_loop_paths_description_text">Loop paths are re-flagged as SUS_PATH on each non-root user app or isolated service startup. This helps address issues where added paths may have their inode status reset or inode re-created in the kernel.</string>
<!-- 模块签名功能描述 -->
<string name="module_verified">Validated</string>
<string name="module_signature_verified">Module signature verified</string>
<string name="module_signature_verification">Signature Verification</string>
<string name="module_signature_verification_summary">Force signature verification when installing modules. (Only available for arm64-v8a)</string>
<string name="module_signature_invalid">Unknown publisher</string>