说明文档及下载链接

主要对代码缺陷进行修复

问题 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 加密文件,用当前代码无法解密,会抛 "解密失败:文件可能损坏或密钥不匹配"。请重新导出所有加密坐标库。