说明文档及下载链接
主要对代码缺陷进行修复
问题 1:加密密钥硬编码在源码中
- 文件:
SecurityUtils.kt:14-21 - 严重性: 高
- 修改前:
private val KEY = SecretKeySpec("PTD_SecureKey123".toByteArray(), "AES") - 修改后:
private fun getSecretKey(): SecretKeySpec { val p1 = "***" val p2 = "***" val p3 = "***" val keyBytes = (p1 + p2 + p3).toByteArray(Charsets.UTF_8).sliceArray(0 until 16) return SecretKeySpec(keyBytes, "AES") } - 说明: 分段拼接 + 截断 16 字节,增加逆向难度。仍可被反编译提取,后续迁移至 Android KeyStore。
问题 2:解密失败时静默返回原文
- 文件:
SecurityUtils.kt:41-60 - 严重性: 高
- 修改前: 解密失败 catch 块返回
encryptedData(加密乱码),调用方将其当作有效数据解析,导致数据损坏且无提示。 - 修改后:
} catch (_: Exception) { throw Exception("解密失败:文件可能损坏或密钥不匹配") } - 说明: 解密失败直接抛出异常,调用方捕获后向用户显示错误提示。
问题 3:CSV 解析使用 split(","),缺少转义处理
- 文件:
MainActivity.kt:1537-1556 - 严重性: 中
- 修改前:
val p = line?.split(",") ?: continue - 修改后: 新增
parseCsvLine()方法:private fun parseCsvLine(line: String): List<String> { val result = mutableListOf<String>() var current = StringBuilder() var inQuotes = false var i = 0 while (i < line.length) { val c = line[i] when { c == '\"' -> inQuotes = !inQuotes c == ',' && !inQuotes -> { result.add(current.toString().trim()) current = StringBuilder() } else -> current.append(c) } i++ } result.add(current.toString().trim()) return result } - 说明: 支持双引号包裹字段内的逗号(如
"涵洞,限高4.5m"),导入行改为parseCsvLine(line ?: "")。
问题 4:"覆盖"模式只删除第一条线路的数据
- 文件:
InspectorDatabase.kt:228-233 - 严重性: 高
- 修改前:
database?.detectionDao()?.deletePointsByLine(refs.first().lineName, refs.first().lineDirection) - 修改后: 新增
overwriteMileageLibrary()事务方法:@Transaction suspend fun overwriteMileageLibrary(refs: List<MileageReference>) { val pairs = refs.map { it.lineName to it.lineDirection }.distinct() pairs.forEach { (name, dir) -> deletePointsByLine(name, dir) } insertMileageRefs(refs) } - 说明: 收集所有唯一的
(lineName, lineDirection)组合,全部删除。
问题 5:覆盖模式的删除和插入未包装在数据库事务中
- 文件:
InspectorDatabase.kt:228 - 严重性: 中
- 修改前: 删除和插入是两次独立操作,App 在中间崩溃会导致数据永久丢失。
- 修改后:
overwriteMileageLibrary()使用@Transaction注解。 - 说明: 删除 + 插入在同一数据库事务中,原子性保证。
问题 6:Base64.DEFAULT 会产生换行符
- 文件:
SecurityUtils.kt:32 - 严重性: 低
- 修改前:
MAGIC_PREFIX + Base64.encodeToString(combined, Base64.DEFAULT) - 修改后:
MAGIC_PREFIX + Base64.encodeToString(combined, Base64.NO_WRAP) - 说明:
Base64.DEFAULT每 76 字符插入\n,改为NO_WRAP生成连续字符串。
问题 7:isAdmin 仅依赖 SharedPreferences,可被篡改
- 文件:
MainActivity.kt:596-599 - 严重性: 中
- 修改前:
isAdmin = sp.getBoolean("is_admin", false) - 修改后:
isAdmin = if (BuildConfig.IS_ADMIN_BUILD) true else sp.getBoolean("is_admin", false), isAuthorized = if (BuildConfig.IS_ADMIN_BUILD) true else isStillValid, isPermanent = if (BuildConfig.IS_ADMIN_BUILD) true else isPermanent, - 说明: admin flavor 编译即自动拥有完整超级用户权限,user flavor 仍依赖 SP 记录。
build.gradle.kts:42,47分别定义IS_ADMIN_BUILD = true/false。
问题 8:导入文件类型过滤器过于宽松
- 文件:
MainActivity.kt:2165(原 2141 行) - 严重性: 低
- 修改前:
filePicker.launch("text/*") - 修改后:
filePicker.launch("*/*") - 说明: 放宽为
*/*,支持选择.ptd加密文件和.csv明文文件。
问题 9:加密文件导出 MIME 类型可优化
- 文件:
MainActivity.kt:1387 - 严重性: 低
- 修改前:
type = if (encrypted) "text/plain" else "text/csv" - 修改后:
type = if (encrypted) "application/x-ptd" else "text/csv" - 说明: 使用自定义 MIME 类型
application/x-ptd利于文件关联识别。
问题 10:CalibratedMileage 空字符串处理
- 文件:
MainActivity.kt:1509 - 严重性: 无
- 说明:
toDoubleOrNull()对空字符串返回null,导出时对null写"",导入时能正确解析为null。逻辑自洽,无需修改。
问题 11:decrypt() 语义不清
- 文件:
SecurityUtils.kt:66-73 - 严重性: 低
- 修改前:
decrypt()对非加密数据直接返回原文,职责不单一,调用方权限检查与解密逻辑耦合。 - 修改后: 新增
processImportContent()统一处理:fun processImportContent(rawContent: String, isAdmin: Boolean): String { val clean = rawContent.trim().removePrefix("\uFEFF") return if (isEncrypted(clean)) { decrypt(clean) } else { if (isAdmin) clean else throw Exception("权限不足:普通用户禁止导入明文库") } } - 说明:
decrypt()只做纯粹解密(非加密数据直接抛异常),processImportContent()封装了格式识别 + 权限校验 + 解密。
问题 12:授权码硬编码
文件:
MainActivity.kt:636-683严重性: 高
修改前:
if (code == "********") { updateSettings(context, _state.value.copy(isAuthorized = true, isAdmin = true, isPermanent = true)) return true }修改后: SHA-256 签名验证体系:
说明: 授权码绑定设备 ID 和包名,一个码只在一台设备有效。同时提供
generateCode()方法供管理员本地生成。
问题 13:encrypt() 异常处理
- 文件:
SecurityUtils.kt:33-35 - 修改前:
} catch (e: Exception) { "ENCRYPT_ERROR: ${e.message}" } - 修改后:
} catch (e: Exception) { throw Exception("加密失败: ${e.message}") } - 说明: 之前返回错误字符串作为"加密结果",调用方无法区分正常加密和加密失败。现改为抛出异常。
升级注意事项
⚠️ 密钥变更导致旧加密文件无法解密
新旧密钥不同。在此修改之前导出的 .ptd 加密文件,用当前代码无法解密,会抛 "解密失败:文件可能损坏或密钥不匹配"。请重新导出所有加密坐标库。
暂无评论