哪些直接用,哪些需要额外接入
引入 SDK 后,普通本地检测直接调用。额外接入只有两项:内容与签名检测插件和 Play Integrity 后端验证。
接入总表
| 功能 | 当前可用方式 | 业务需要做什么 | 不做额外接入的影响 |
|---|---|---|---|
| 普通本地检测 模拟器、设备能力、Root、Hook、调试器、无障碍、VPN/代理、SIM/eSIM、本地安装来源 | 直接可用 · 9 项 | 引入 SDK 并调用。无需插件、后端或运行时权限授权。 | 按 SDK 默认规则检测;无需填 su 路径或传感器类型。 |
工具安装checkInstalledTools | 直接可用 · 1 项 | 默认查内置 5 个包名,无需传参或配置 queries。 | 自定义名单追加到默认名单,按包名去重;自定义包名另配可见性。 |
签名校验checkSignature | 函数可用 · 需要基准 | 可选插件开启 signatureEnabled;Play 模式提供 .der 证书。也可自行传可信基准。 | 未开启且未传显式基准 → 自动扫描跳过;直接调用无基准时返回 UNKNOWN/no_baseline。 |
内容校验checkApkContent | 函数可用 · 需要基准 | 可选插件开启 contentEnabled,自动生成 assets / DEX 基准,无需 .der。也可自行传可信基准。 | 未开启且未传显式基准 → 自动扫描跳过;直接调用无基准时返回 UNKNOWN/no_baseline。 |
Play IntegrityrequestPlayIntegrityToken | 可选 · 需后端验证 | 提供 Cloud 项目编号,完成 Google 配置,适配后端验证凭证。 | 不调用就不取凭证;不影响本地检测。 |
内容与签名检测插件的两个开关默认 false。双关闭时不注册相关构建任务;自动扫描跳过这两项,不读取基准、不调度对应检测。本地安装来源 checkInstallSource 不依赖 Integrity。检测未知或未检出不等于设备安全。
只接 SDK:依赖 + 调用
宿主 app/build.gradle.kts 添加依赖:
dependencies {
implementation("com.remax.riskcontrol:risk-control-sdk:0.1.0")
}import com.remax.riskcontrol.sdk.*
// 由 Activity / ViewModel 持有;生命周期结束时 scanner.close()。
val scanner = RiskScanner(applicationContext)
scanner.scan(
onResult = { id, result ->
// 结果在后台回调,更新 UI 请切回主线程。
val status = result.status
},
)默认候选集合包含 12 项。只接 SDK、未传显式基准时,实际执行 10 项,签名和内容自动跳过;开启或显式提供对应基准后再执行对应项。无需初始化。同一 scanner 不要重叠扫描,独立函数请放后台线程。
首次接入:本地 Maven 仓库怎么配
当前使用本地 Maven。我们执行 ./gradlew bundleLocalMavenRepository,将 build/distributions/risk-control-0.1.0-maven.zip 解压后的 risk-control-maven/ 给业务;本机也可直接引用 build/repository/。
// 业务 settings.gradle.kts:加入现有依赖仓库
dependencyResolutionManagement {
repositories {
maven { url = uri("/实际路径/risk-control-maven") }
google()
mavenCentral()
}
}首次构建仍需访问 Google / Maven 的第三方依赖仓库。SDK 是 AAR;插件是独立 Maven 产物,两者可放同一仓库。业务只使用 SDK 时,无需引入风控插件。
两项可选接入
可选 ① 内容与签名检测插件
仅影响签名和内容两项。插件 ID 为 com.remax.riskcontrol.content-signature,配置块为 riskContentSignature;SDK 与插件需配套更新,并重新构建应用。在 settings.gradle.kts 文件开头的现有 pluginManagement.repositories 加入插件仓库:
pluginManagement {
repositories {
maven { url = uri("/实际路径/risk-control-maven") }
google()
mavenCentral()
gradlePluginPortal()
}
}在宿主 App 模块的 app/build.gradle.kts 中:
plugins {
// 保留原有 Android / Kotlin 插件。
id("com.remax.riskcontrol.content-signature") version "0.1.0"
}
riskContentSignature {
contentEnabled.set(true) // 只开启内容基准,签名默认 false。
}两个开关默认关闭,可独立开启
上方示例显式开启内容校验;只应用插件、不写配置块时,两项都关闭。Demo 为验证功能显式开启两项,不代表默认值。
| contentEnabled | signatureEnabled | 构建行为 | 自动扫描行为 |
|---|---|---|---|
| false | false | 不注册基准生成、APK/AAB 转换和导出任务;不检查证书或 APK 输出数量。 | 跳过这两项,不读取基准、不调度对应检测。 |
| true | false | 生成 assets / DEX 基准;按现有签名配置处理最终产物,不需要可信签名公钥证书。 | 只执行内容校验。 |
| false | true | 生成签名基准;保留 AGP 原有打包签名流程,不运行 DEX 转换。 | 只执行签名校验。 |
| true | true | 生成两类基准,并处理最终产物。 | 执行两项校验。 |
表中自动扫描行为以业务未传显式基准为前提。插件只在构建机运行,不会在 App 内常驻运行;其他本地检测仍按所选项目执行。
扫描回调与直接调用的区别
selected是候选集合。关闭且没有显式基准的签名 / 内容项目不触发onResult,也不出现在onComplete的结果 Map 中;不要固定等待 12 个结果。- 所选项目全被跳过时,仍在工作线程调用一次
onComplete(emptyMap())。 - 通过
ScanConfig(signature = ..., content = ...)提供显式基准,即使插件未开启也会执行对应检测。useGeneratedBaselines = false时,这两项仅使用显式基准。 - 已开启但基准缺失、损坏或不可读时仍执行并返回
UNKNOWN,不会静默跳过。 - 直接调用
checkSignature/checkApkContent仍返回结果:关闭且未传显式基准时返回UNKNOWN/no_baseline,不打开基准文件。主动调用loadGeneratedBaselines仍会读取文件。
如需签名基准,另开 signatureEnabled,选择一种来源:
| 模式 | 配置 |
|---|---|
buildSigning | 使用当前变体已有签名配置,无需 .der。 |
playAppSigning(默认) | 指定 Play 应用签名公钥证书路径;缺失或无效会构建失败。 |
riskContentSignature {
contentEnabled.set(true)
signatureEnabled.set(true)
certificateFiles.setFrom(rootProject.file("certificates/play-app-signing.der"))
}证书在 Play Console 的 Play 应用签名页面下载“应用签名密钥证书”,不要选上传证书;放到 CI 可读的项目路径。下载指引。自行签名分发改用 signingMode.set("buildSigning"),不要配置外部证书。
CI 按当前变体的开关选择输出目录
至少开启一项:使用插件导出的最终产物。
app/build/outputs/risk-control/<变体名>/apk/*.apk
app/build/outputs/risk-control/<变体名>/bundle/*.aab
两项都关闭:使用 AGP 原生输出目录。APK 的子目录取决于 flavor / buildType 配置。
app/build/outputs/apk/
app/build/outputs/bundle/<变体名>/*.aab
// 无 flavor 的 debug APK 示例:
app/build/outputs/apk/debug/*.apk
// dev flavor 的 release APK 示例:
app/build/outputs/apk/dev/release/*.apk
从开启切换为关闭无需 clean,新原生产物不会携带旧生成基准。双关闭后不会刷新旧的 outputs/risk-control 目录,CI 不要继续从旧导出目录取包。
内容开启时复用现有签名配置重新签署;.der 不替代上传密钥。只开签名时不执行 DEX 转换或额外重签。需要加固时先查看下方兼容说明。
旧插件 ID com.remax.riskcontrol.baseline 和配置块 riskBaseline 已被新名称替换,没有保留旧 ID 别名。插件实现坐标为 com.remax.riskcontrol:risk-control-content-signature-plugin:0.1.0。
按变体配置(开发签名 / Play 签名)
riskContentSignature {
onVariants {
when (name) {
"devDebug", "devRelease" -> {
contentEnabled.set(true)
signatureEnabled.set(true)
signingMode.set("buildSigning")
certificateFiles.setFrom(emptyList<Any>())
}
"prodRelease" -> {
contentEnabled.set(true)
signatureEnabled.set(true)
signingMode.set("playAppSigning")
certificateFiles.setFrom(rootProject.file("certificates/play-app-signing.der"))
}
}
}
}示例假设已有 dev/prod flavor,其他变体保持默认关闭。name 是完整变体名,buildType 仅为 debug / release。setFrom 替换继承的证书;非空证书集合优先于构建签名,所以 buildSigning 分支清空集合。
可选 ② Play Integrity → 独立取凭证,需要后端验证
独立于内容与签名检测插件和本地扫描。Google 依赖已随 SDK 自动传递;业务提供项目编号、完成 Google 配置,并让后端解密验证凭证与请求绑定。
import com.remax.riskcontrol.sdk.integrity.PlayIntegrityClient
val client = PlayIntegrityClient(applicationContext, cloudProjectNumber)
client.requestPlayIntegrityToken()
.addOnSuccessListener { result ->
// 将 token 与 requestContext 等请求绑定信息交给业务后端验证。
// 取得 token 不等于校验通过,SDK 不会自动请求业务后端。
}
.addOnFailureListener { error -> /* 本次未取得凭证 */ }复用 client,可在主线程发起,默认 listener 在主线程回调。requestHash 由 SDK 自动生成,无需 Firebase JSON;不额外申请用户权限。
用到时再展开
12 个函数与返回值
| 检测项 | 函数 | 顶层 status | 含义 |
|---|---|---|---|
| 模拟器 | checkEmulator | DETECTED / NOT_DETECTED / UNKNOWN | 检出已知模拟器特征 |
| 设备能力 | checkDeviceCapabilities | DETECTED / NOT_DETECTED / UNKNOWN | 检出指定能力缺失 |
| Root | checkRoot | DETECTED / NOT_DETECTED / UNKNOWN | 检出可见 su 痕迹 |
| Hook | checkHook | DETECTED / NOT_DETECTED / UNKNOWN | 检出框架类、模块或调用栈痕迹 |
| 调试器 | checkDebugger | DETECTED / NOT_DETECTED / UNKNOWN | 检出调试连接、等待或 native tracer |
| 无障碍 | checkAccessibility | DETECTED / NOT_DETECTED / UNKNOWN | 存在已启用服务 |
| 工具安装 | checkInstalledTools | DETECTED / NOT_DETECTED / UNKNOWN | 至少一个目标工具已安装;默认内置名单,可传 targets 追加 |
| VPN / 代理 | checkVpnProxy | DETECTED / NOT_DETECTED / UNKNOWN | 存在默认网络 VPN 或代理 |
| SIM / eSIM | checkSimNetwork | PRESENT / ABSENT / UNKNOWN / NOT_APPLICABLE | 有卡 / 确认无卡 / 未知 / 无电话能力;不再汇总网络组合条件。 |
| 安装来源 | checkInstallSource | GOOGLE_PLAY / OTHER / UNKNOWN | 系统记录:Play / 其他安装方 / 来源未知;不是 Google 官方验证。 |
| 签名 | checkSignature | CONSISTENT / INCONSISTENT / UNKNOWN | 包名与签名符合基准 / 不符合 / 无法判断。 |
| 内容 | checkApkContent | CONSISTENT / INCONSISTENT / UNKNOWN | 覆盖范围内一致 / 已发现不一致 / 无法判断。 |
| 检测类型 | 返回值 |
|---|---|
| 特征 / 条件检测 | DETECTED / NOT_DETECTED / UNKNOWN |
| 签名 / 内容 | CONSISTENT / INCONSISTENT / UNKNOWN |
| SIM / eSIM | PRESENT / ABSENT / UNKNOWN / NOT_APPLICABLE |
| 本地安装来源 | GOOGLE_PLAY / OTHER / UNKNOWN |
所有本地函数返回 CheckResult;子项在 findings,原因在 finding.reason,事实在 facts。CONSISTENT 是与基准一致,INCONSISTENT 是不一致。UNKNOWN 不能当作通过;特征检出也不直接认定作弊。
独立子项:常量、取法、返回含义(完整参考)
每个检测函数返回一个 CheckResult,独立子项放在 result.findings: List<Finding> 中。子项不是另外一个需要执行的函数;先执行所属检测,再读取结果即可。
| 字段 | 业务如何使用 |
|---|---|
result.status | 本检测项的顶层结论,只汇总 affectsSummary=true 的子项。 |
finding.id | 用 FindingIds 常量或生成方法查询;批量查询使用 *_PREFIX 常量,无需手写 ID。 |
finding.status | 按所属子项的语义判断,见下方逐项表格。 |
finding.reason | 未知、不可用等原因;空字符串不等于“成功”。 |
finding.evidence | 子项证据,Map<String, String>;键可能不存在。文件缺失等不一致详情可能在 evidence["reason"]。 |
finding.affectsSummary | true:参与顶层汇总;false:仅辅助信息,业务仍可单独读取。 |
result.facts | 检测事实信息,Map<String, String>;不是子项状态,缺失或 UNKNOWN 不能当作否定值。 |
检测参数与结果查询参数不是一回事
正常检测无需业务填写 su 路径、传感器类型或 DEX 文件名。su 路径、传感器类型来自 SDK 默认规则;DEX 文件名来自插件生成或业务手动提供的可信基准。没有内容基准时返回未知,不会拿当前安装包自行生成可信基准。动态 ID 方法的参数只用于精确取结果,不能增加检查对象。
import com.remax.riskcontrol.sdk.*
// 后台线程调用。使用 SDK 默认规则,不传路径、传感器类型或 DEX 文件名。
val session = DetectionSession(applicationContext)
val root = checkRoot(session)
val suItems = root.findingsByPrefix(FindingIds.SU_ARTIFACT_PREFIX)
val hardware = checkDeviceCapabilities(session)
val sensors = hardware.findingsByPrefix(FindingIds.SENSOR_MISSING_PREFIX)
// 内容依赖可信基准:可选插件生成,或业务手动传入。
val content = checkApkContent(session)
val dexItems = content.findingsByPrefix(FindingIds.DEX_INVENTORY_PREFIX)
// 无基准时 content.status=UNKNOWN,dexItems 可能为空;不能当作校验通过。统一入口:FindingIds + 结果查询方法
导入 com.remax.riskcontrol.sdk.* 后即可使用。固定 ID 提供常量,动态 ID 提供生成方法和前缀常量;SDK 产生结果时也使用同一套定义。底层 Finding.id 仍为字符串,方便 JSON / 日志输出,其值和检测行为不变。
| 取法 | 调用示例 | 返回 |
|---|---|---|
| 单个固定子项 | result.finding(FindingIds.NO_SIM_AND_WIFI) | Finding?;未输出时为 null。 |
| 单个动态子项 | result.finding(FindingIds.sensorMissing(1)) | Finding?;参数为传感器类型数字。 |
| 相同 ID 的全部结果 | result.findingsById(FindingIds.entryMismatch("base", "classes.dex")) | List<Finding>;保留所有阶段的重复 ID。 |
| 动态家族的全部结果 | result.findingsByPrefix(FindingIds.SU_ARTIFACT_PREFIX) | List<Finding>;前缀已包含分隔符,避免相似 ID 混入。 |
| 检测级异常 | result.finding(result.id) | Finding?;只在检测级异常分支出现。 |
finding 只取首条;没有结果返回 null,列表查询没有结果返回空列表,均不伪造 NOT_DETECTED。需要处理重复内容校验结果时用 findingsById。此封装只针对子项 ID;包名、路径等仍是业务入参,facts / evidence 的键仍按文档读取。
可选:精确查询用的动态 ID 参数,以及无需参数的家族前缀
| 精确 ID 生成 | 批量查询前缀 |
|---|---|
FindingIds.qemuFile(path) | FindingIds.QEMU_FILE_PREFIX |
FindingIds.sensorMissing(type) | FindingIds.SENSOR_MISSING_PREFIX |
FindingIds.suArtifact(path) | FindingIds.SU_ARTIFACT_PREFIX |
FindingIds.managerInstalled(packageName) | FindingIds.MANAGER_INSTALLED_PREFIX |
FindingIds.frameworkClass(className) | FindingIds.FRAMEWORK_CLASS_PREFIX |
FindingIds.eventScopeIncludesHost(index) | FindingIds.EVENT_SCOPE_INCLUDES_HOST_PREFIX |
FindingIds.simSlot(slot) | FindingIds.SIM_SLOT_PREFIX |
FindingIds.entryMismatch(apk, entry) | FindingIds.ENTRY_MISMATCH_PREFIX |
FindingIds.archive(apk) | FindingIds.ARCHIVE_PREFIX |
FindingIds.dexModuleMissing(apk) | FindingIds.DEX_MODULE_MISSING_PREFIX |
FindingIds.dexModule(apk) | FindingIds.DEX_MODULE_PREFIX |
FindingIds.dexInventory(apk) | FindingIds.DEX_INVENTORY_PREFIX |
工具安装结果使用 FindingIds.installedTool(packageName),ID 本身就是包名,没有统一家族前缀;查全部指定工具可直接遍历该结果的 findings,注意 targets 诊断项。
固定 ID:以“无卡且 Wi-Fi”为例
import com.remax.riskcontrol.sdk.*
// 在业务后台线程执行;若使用 scanner,直接在回调中读取 result。
val result = checkSimNetwork(DetectionSession(applicationContext))
val item = result.finding(FindingIds.NO_SIM_AND_WIFI)
// 缺少子项时按未知处理,不能当作 NOT_DETECTED。
val status = item?.status ?: Status.UNKNOWN
val reason = item?.reason ?: "finding_unavailable"
when (status) {
Status.DETECTED -> { /* 已确认无卡,且当前默认网络是 Wi-Fi */ }
Status.NOT_DETECTED -> { /* 必要信息已知,组合条件不成立 */ }
Status.NOT_APPLICABLE -> { /* 设备无电话能力 */ }
Status.UNKNOWN -> { /* 信息不足,查看 reason */ }
else -> { /* 非该子项的预期状态,按未知处理 */ }
}
// result.status 是 PRESENT / ABSENT / UNKNOWN / NOT_APPLICABLE,
// 不要用它代替 no_sim_and_wifi 的状态。| SIM 状态 | 默认网络是否 Wi-Fi | no_sim_and_wifi |
|---|---|---|
ABSENT | 是 | DETECTED |
ABSENT | 否 | NOT_DETECTED |
PRESENT | 是或否 | NOT_DETECTED |
PRESENT / ABSENT | 读不到 | UNKNOWN |
UNKNOWN | 任意 | UNKNOWN |
NOT_APPLICABLE | 任意 | NOT_APPLICABLE |
例如:result.status=ABSENT,no_sim_and_wifi=UNKNOWN,表示已确认无卡,但网络信息不足。即便顶层有明确结论,也要看自己关心的子项,不能用顶层状态代替所有子项。
动态 ID:取全部相关子项
import com.remax.riskcontrol.sdk.*
// 在后台线程调用。rootPaths 可通过 DetectionRules 自定义。
val result = checkRoot(DetectionSession(applicationContext))
val suItems = result.findingsByPrefix(FindingIds.SU_ARTIFACT_PREFIX)
val suStatus = when {
suItems.any { it.status == Status.DETECTED } -> Status.DETECTED
suItems.isEmpty() || suItems.any { it.status != Status.NOT_DETECTED } -> Status.UNKNOWN
else -> Status.NOT_DETECTED
}
// 固定包名可以精确读取;它是辅助子项,不决定 Root 顶层状态。
val manager = result.finding(FindingIds.managerInstalled("com.topjohnwu.magisk"))
val managerStatus = manager?.status ?: Status.UNKNOWN表中 SDK 方法用于精确查询时才需要传具体参数;通常直接用对应 *_PREFIX 取整组即可。实际 ID 栏的 {path}、{type}、{className}、{index}、{packageName}、{apk}、{entry} 是 JSON / 日志占位说明,不要手动拼接。例如 sensor_missing:1、qemu_file:/dev/qemu_pipe、entry_mismatch:base:classes.dex。动态子项可能有多条;内容校验同一文件也可能出现在多个检查阶段,不要默认所有 ID 都唯一,重复 ID 用 findingsById,动态家族用 findingsByPrefix,并检查每条结果。
在并发扫描回调中直接读取
import com.remax.riskcontrol.sdk.*
// 由 Activity / ViewModel 持有,生命周期结束时 scanner.close()。
val scanner = RiskScanner(applicationContext)
scanner.scan(
selected = setOf(CheckId.SIM_NETWORK, CheckId.SIGNATURE),
onResult = { id, result ->
when (id) {
CheckId.SIM_NETWORK -> {
val item = result.finding(FindingIds.NO_SIM_AND_WIFI)
val status = item?.status ?: Status.UNKNOWN
// 使用 status;更新 UI 时切回主线程。
}
CheckId.SIGNATURE -> {
val item = result.finding(FindingIds.SIGNATURE_MISMATCH)
when (item?.status ?: Status.UNKNOWN) {
Status.CONSISTENT -> { /* 签名符合基准;包名另看 package_name_mismatch */ }
Status.INCONSISTENT -> { /* 签名不符合基准 */ }
else -> { /* 无法判断;查看 item?.reason */ }
}
}
else -> Unit
}
},
)12 项检测的子项清单
“参与汇总”只说明是否影响该函数的顶层 status,不代表业务必须拦截。特征 DETECTED 也不自动等于作弊。子项因配置、执行分支或读取异常可能缺失,缺失不等于否定结果。
模拟器 · checkEmulator
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.KNOWN_BUILD_COMBINATIONknown_build_combination | 是 | 已知模拟器 Build 组合特征;命中规则在 evidence["rules"]。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.qemuFile(path)qemu_file:{path} | 是 | 每个配置的 QEMU 路径是否存在;path 是完整路径。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.TRANSLATION_LAYER_HINTtranslation_layer_hint | 否 | 当前进程出现已知 ABI 翻译层模块,仅为辅助线索。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
设备能力 · checkDeviceCapabilities
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.sensorMissing(type)sensor_missing:{type} | 是 | 指定类型传感器不可见;DETECTED 表示缺少,不是存在。type 是 Android sensor type 数字。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.NO_VISIBLE_CAMERAno_visible_camera | 是 | 没有 API 可见的摄像头;不等于物理镜头总数。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.NO_VISIBLE_AUDIO_INPUTno_visible_audio_input | 是 | 没有 API 可见的音频输入设备。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
Root · checkRoot
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.suArtifact(path)su_artifact:{path} | 是 | 每个配置的 su 路径是否存在。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.managerInstalled(packageName)manager_installed:{packageName} | 否 | 指定 Root 管理工具是否可见且已安装;安装不证明获得 Root。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.WRITABLE_SYSTEM_MOUNT_HINTwritable_system_mount_hint | 否 | 观察到受保护系统目录的可写挂载;evidence["mounts"] 给出目录。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
Hook · checkHook
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.frameworkClass(className)framework_class:{className} | 是 | 当前 ClassLoader 能否找到指定框架类;不执行类初始化。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.FRAMEWORK_MODULEframework_module | 是 | 当前进程中有已知 Hook 模块;evidence["modules"] 给出模块名。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.FRAMEWORK_STACKframework_stack | 是 | 当前调用栈有已知框架类;evidence["classes"] 给出类名。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
调试器 · checkDebugger
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.JAVA_CONNECTEDjava_connected | 是 | Java 调试器已连接。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.JAVA_WAITINGjava_waiting | 是 | 当前进程正在等待 Java 调试器。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.NATIVE_TRACERnative_tracer | 是 | TracerPid > 0;evidence["tracerPid"] 给出读取值。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.DEBUGGABLE_BUILDdebuggable_build | 否 | 宿主声明 debuggable;不表示当前有调试器,也不表示重打包。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
无障碍 · checkAccessibility
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.SERVICE_ENABLEDservice_enabled | 是 | 至少一个无障碍服务已启用,不代表正在自动点击。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.eventScopeIncludesHost(index)event_scope_includes_host:{index} | 否 | 该服务订阅的事件包范围包含宿主,或为所有包;index 对应 facts["service.{index}"]。仅在有服务时出现。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
工具安装 · checkInstalledTools
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.installedTool(packageName){packageName} | 是 | ID 就是传入的包名,没有额外前缀;可见范围内判断是否安装。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 包名不合法或可见性未确认且查不到时为 UNKNOWN。 |
VPN / 代理 · checkVpnProxy
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.DEFAULT_NETWORK_VPNdefault_network_vpn | 是 | 当前默认网络声明 VPN transport。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.DEFAULT_HTTP_PROXYdefault_http_proxy | 是 | 默认 HTTP / PAC 代理配置存在。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
FindingIds.VIRTUAL_INTERFACE_HINTvirtual_interface_hint | 否 | 可见的活动网卡名符合 tun / tap / ppp / wg 规则,仅为辅助线索;evidence["interfaces"] 给出网卡名。 | DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。 |
SIM / eSIM · checkSimNetwork
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.SIM_PRESENCEsim_presence | 是 | 决定该函数顶层卡状态,兼容实体卡与已启用 eSIM。 | PRESENT:有卡;ABSENT:完整覆盖下确认无卡;UNKNOWN:证据不足;NOT_APPLICABLE:无电话能力。 |
FindingIds.NO_SIM_AND_WIFIno_sim_and_wifi | 否 | “确认无卡”与“默认网络是 Wi-Fi”的组合条件。 | DETECTED:无卡且 Wi-Fi;NOT_DETECTED:必要输入已知且组合不成立;UNKNOWN:输入不足;NOT_APPLICABLE:无电话能力。 |
安装来源 · checkInstallSource
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.INSTALLER_SOURCEinstaller_source | 是 | 系统记录的安装方;包名在 facts["installerPackage"],并非 Google 官方验证。 | GOOGLE_PLAY:记录为 com.android.vending;OTHER:记录为其他非空包名;UNKNOWN:来源不可用。 |
签名 · checkSignature
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.PACKAGE_NAME_MISMATCHpackage_name_mismatch | 是 | 宿主包名与可信基准是否一致;仅在基准有效后检查。 | CONSISTENT:与基准一致;INCONSISTENT:不一致;UNKNOWN:无法判断。 |
FindingIds.SIGNATURE_MISMATCHsignature_mismatch | 是 | 签名集合与可信基准是否一致。虽然 ID 带 mismatch,状态仍直接表达一致性。 | CONSISTENT:与基准一致;INCONSISTENT:不一致;UNKNOWN:无法判断。 没有基准时 UNKNOWN/no_baseline。 |
内容 · checkApkContent
| SDK 写法 / 实际 ID | 参与汇总 | 判断内容 / 证据 | 返回值含义 |
|---|---|---|---|
FindingIds.entryMismatch(apk, entry)entry_mismatch:{apk}:{entry} | 是 | 指定容器内文件是否存在且摘要一致。apk 为 base 或 split 名;entry 为包内完整路径。 | CONSISTENT:与基准一致;INCONSISTENT:不一致;UNKNOWN:无法判断。 文件缺失是 INCONSISTENT,详情可能在 evidence["reason"]。 |
FindingIds.dexInventory(apk)dex_inventory:{apk} | 是 | DEX 文件清单与摘要检查;安装时合并模块可能按摘要多重集合比较。 | CONSISTENT:与基准一致;INCONSISTENT:不一致;UNKNOWN:无法判断。 evidence 可含 added / missing / duplicate 或 comparison。 |
FindingIds.dexModuleMissing(apk)dex_module_missing:{apk} | 是 | 基准要求必需的 DEX 容器,但安装包中不存在;仅缺失时出现。 | 仅 INCONSISTENT。没有此子项不等于整个内容校验通过。 |
FindingIds.dexModule(apk)dex_module:{apk} | 否 | 可选功能模块未安装;只在这种情况下出现。 | 仅 UNKNOWN,reason=module_not_installed,不影响内容汇总。 |
FindingIds.CONTENT_MISMATCHcontent_mismatch | 是 | 内容检查前置条件不满足;没有基准、基准无效或包名 / 版本不匹配时出现。 | 仅 UNKNOWN;reason 为 no_baseline、invalid_baseline 或 baseline_for_different_package_or_version。 |
仅在异常时出现的诊断子项
| 所属检测 | SDK 写法 / 实际 ID | 参与汇总 | 返回含义 |
|---|---|---|---|
| 设备能力 | FindingIds.SENSORSFindingIds.CAMERASFindingIds.AUDIO_INPUTS | 是 | 仅 UNKNOWN:对应系统服务或枚举调用失败。成功时输出具体能力子项,不输出同名“成功”项。 |
| SIM / eSIM | FindingIds.SIM_READFindingIds.SIM_DEFAULTFindingIds.simSlot(slot)FindingIds.SIM_OPERATOR | 否 | 仅 UNKNOWN:总体读取、默认状态、指定卡槽或运营商信息失败;主结论仍由 sim_presence 决定。 |
| 内容 | FindingIds.archive(apk)archive:{apk} | 是 | 仅 UNKNOWN:对应 ZIP 容器读取失败。 |
| 所有检测 | result.finding(result.id) | 是 | 未被内部探针捕获的常规异常使用检测 ID 记录 UNKNOWN。例如 root、sim_network、signature、apk_content。查看 reason 获取具体原因。 |
不要假设每次都有全部子项。使用 finding / findingsById / findingsByPrefix,缺失时保留未知;必要时遍历该结果的所有 UNKNOWN 子项排查。不要把 UNKNOWN、NOT_APPLICABLE 或空列表当作“正常”。线程中断等仍可能向调用方传播;SDK 无法运行时也无法保证返回结果。
排查原因与证据
// result 是任意检测的返回值;以下只读取结果,不会再次检测。
val diagnostics = result.findings
.filter { it.status == Status.UNKNOWN }
.map { "${it.id}: ${it.reason}" }
val changedFiles = result.findingsByPrefix(FindingIds.ENTRY_MISMATCH_PREFIX)
.filter { it.status == Status.INCONSISTENT }
changedFiles.forEach { item ->
val detail = item.evidence["reason"] // 如 entry_missing;也可能为空。
val actualDigest = item.evidence["actualSha256"]
// 按业务需要消费证据,不要根据 reason 是否为空判断一致性。
}宿主加固:对签名与内容的影响
当前插件没有通用的加固后基准生成适配。加固不必然让两项检测同时失效。以下结果以 SDK 仍能正常运行、相关数据可读取为前提;SDK 不会自动区分“合法加固”与“恶意修改”。
| 加固后的实际情况 | 签名检测 | 内容检测 |
|---|---|---|
| 最终签名符合基准,受检内容也未改变 | CONSISTENT | CONSISTENT(仅覆盖基准范围) |
| 最终签名符合基准,但加固改写 / 加密 / 替换了原 DEX 或受检 assets;仍保留旧基准 | CONSISTENT | INCONSISTENT:摘要或 DEX 清单不一致。合法加固也会命中,不能直接当作弊。 |
| 重新签名,最终安装包签名不在允许基准中 | INCONSISTENT:signature_mismatch | 独立比较受检内容;仅改变签名不会必然命中内容检测。 |
| 已开启对应检测,但基准资产被删除、不可读取或生成基准格式损坏 | 对应基准不可用的项:UNKNOWN / no_baseline | 对应基准不可用的项:UNKNOWN / no_baseline |
| 基准可用,但系统签名信息或包文件读取失败 | UNKNOWN,原因如 signing_info_unavailable、read_error:* | 受影响子项 UNKNOWN,原因如 read_error:*;已发现其他不一致时汇总仍为 INCONSISTENT。 |
| SDK 被破坏,无法加载 / 调用,或宿主无法启动 | 无法保证有 CheckResult 返回 | 无法保证有 CheckResult 返回,不承诺自动返回 UNKNOWN。 |
CONSISTENT 仅表示本次可检查范围内未发现不一致,不等于全包安全。内容子项包括 entry_mismatch:...、dex_inventory:...;未知原因在 findings[].reason。生成基准加载错误可用后台调用 loadGeneratedBaselines(context) 查看。汇总优先级:INCONSISTENT > UNKNOWN > CONSISTENT。
加固项目如何接入
| 检测项 | 当前可采用的接入方式 |
|---|---|
| 签名 | 以用户最终安装包的签名为准。同证书重签可继续使用原基准;换证书则配置新的可信证书。Play 分发用 Play 最终应用签名证书,不能拿加固商或上传证书代替。前提是加固保留基准且 SDK 可正常运行。 |
| 内容 | 加固改写受检文件后,原基准会过期。需要在可信 CI 中针对加固后最终受检内容生成新基准,并确保写入基准后不再改写这些内容、之后完成最终签署。APK / AAB、壳格式和分包流程须按加固方案适配验证;当前插件不能直接保证此流程。 |
| 尚未适配加固内容 | 可以先保持 contentEnabled=false,签名和其他项按需使用。不手动传内容基准时,自动扫描跳过内容项;直接调用内容检查仍返回 UNKNOWN / no_baseline。已开启但基准损坏的检查不会被跳过;跳过或未知都不等于校验通过。 |
不能在用户手机上读取当前加固包,再把它自身的摘要当可信基准。仅把插件任务移动到加固任务之后也不保证兼容;需确认产物输入、基准写入、最终签名及 AAB 分包均匹配。即使完成适配,落盘的壳 / 加密文件校验也不等于运行时解密代码或内存校验。
工具包名与内容检查范围
// 后台线程直接调用:默认查 SDK 内置工具。
val tools = checkInstalledTools(DetectionSession(applicationContext))
// 自定义名单:自动追加到内置名单(先在宿主 Manifest 配好该包名的 queries)。
val config = ScanConfig(tools = listOf(ToolTarget("com.example.tool", visibilityDeclared = true)))
// 不传或传空列表都保留内置名单;相同包名只查一次。内容校验开启后,默认覆盖 base assets 和最终 DEX,不覆盖所有编译资源、native library 或运行时内存。可用 assetIncludes / assetExcludes 缩小 assets 范围。当前 assets + DEX 最多 64 个文件,单文件 64 MiB、合计 128 MiB。插件启用时,多 APK 输出暂不支持;内容开启时,部分动态功能交付和特殊签名重签暂不支持,构建会明确报错。两项都关闭时不施加插件的多 APK 输出限制。
默认工具查询包名:com.topjohnwu.magisk、de.robv.android.xposed.installer、org.lsposed.manager、org.autojs.autojs、org.autojs.autojs6。这些具体包名是实现时补充的初始名单;原需求文档列举了相关工具名称,并未提供这份完整包名清单,也尚未全部覆盖原文提到的工具。自定义包名需宿主声明精确 queries,再传 ToolTarget(packageName, visibilityDeclared = true);不可见且查不到返回未知。传入名单自动与内置名单合并,按包名去重;空列表仍查内置名单。同名包采用业务传入的 visibilityDeclared,便于宿主移除 queries 时显式设为 false。Root 默认管理工具名单与 tools 参数独立。