RISK CONTROL SDK · 0.1.0

哪些直接用,哪些需要额外接入

引入 SDK 后,普通本地检测直接调用。额外接入只有两项:内容与签名检测插件和 Play Integrity 后端验证。

01

接入总表

功能当前可用方式业务需要做什么不做额外接入的影响
普通本地检测
模拟器、设备能力、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 Integrity
requestPlayIntegrityToken
可选 · 需后端验证提供 Cloud 项目编号,完成 Google 配置,适配后端验证凭证。不调用就不取凭证;不影响本地检测。

内容与签名检测插件的两个开关默认 false。双关闭时不注册相关构建任务;自动扫描跳过这两项,不读取基准、不调度对应检测。本地安装来源 checkInstallSource 不依赖 Integrity。检测未知或未检出不等于设备安全。

02

只接 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 时,无需引入风控插件。

03

两项可选接入

可选 ① 内容与签名检测插件

仅影响签名和内容两项。插件 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 为验证功能显式开启两项,不代表默认值。

contentEnabledsignatureEnabled构建行为自动扫描行为
falsefalse不注册基准生成、APK/AAB 转换和导出任务;不检查证书或 APK 输出数量。跳过这两项,不读取基准、不调度对应检测。
truefalse生成 assets / DEX 基准;按现有签名配置处理最终产物,不需要可信签名公钥证书。只执行内容校验。
falsetrue生成签名基准;保留 AGP 原有打包签名流程,不运行 DEX 转换。只执行签名校验。
truetrue生成两类基准,并处理最终产物。执行两项校验。

表中自动扫描行为以业务未传显式基准为前提。插件只在构建机运行,不会在 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;不额外申请用户权限。

04

用到时再展开

12 个函数与返回值
检测项函数顶层 status含义
模拟器checkEmulatorDETECTED / NOT_DETECTED / UNKNOWN检出已知模拟器特征
设备能力checkDeviceCapabilitiesDETECTED / NOT_DETECTED / UNKNOWN检出指定能力缺失
RootcheckRootDETECTED / NOT_DETECTED / UNKNOWN检出可见 su 痕迹
HookcheckHookDETECTED / NOT_DETECTED / UNKNOWN检出框架类、模块或调用栈痕迹
调试器checkDebuggerDETECTED / NOT_DETECTED / UNKNOWN检出调试连接、等待或 native tracer
无障碍checkAccessibilityDETECTED / NOT_DETECTED / UNKNOWN存在已启用服务
工具安装checkInstalledToolsDETECTED / NOT_DETECTED / UNKNOWN至少一个目标工具已安装;默认内置名单,可传 targets 追加
VPN / 代理checkVpnProxyDETECTED / NOT_DETECTED / UNKNOWN存在默认网络 VPN 或代理
SIM / eSIMcheckSimNetworkPRESENT / ABSENT / UNKNOWN / NOT_APPLICABLE有卡 / 确认无卡 / 未知 / 无电话能力;不再汇总网络组合条件。
安装来源checkInstallSourceGOOGLE_PLAY / OTHER / UNKNOWN系统记录:Play / 其他安装方 / 来源未知;不是 Google 官方验证。
签名checkSignatureCONSISTENT / INCONSISTENT / UNKNOWN包名与签名符合基准 / 不符合 / 无法判断。
内容checkApkContentCONSISTENT / INCONSISTENT / UNKNOWN覆盖范围内一致 / 已发现不一致 / 无法判断。
检测类型返回值
特征 / 条件检测DETECTED / NOT_DETECTED / UNKNOWN
签名 / 内容CONSISTENT / INCONSISTENT / UNKNOWN
SIM / eSIMPRESENT / 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.affectsSummarytrue:参与顶层汇总;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-Fino_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_COMBINATION
known_build_combination
是已知模拟器 Build 组合特征;命中规则在 evidence["rules"]。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。
FindingIds.qemuFile(path)
qemu_file:{path}
是每个配置的 QEMU 路径是否存在;path 是完整路径。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。
FindingIds.TRANSLATION_LAYER_HINT
translation_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_CAMERA
no_visible_camera
是没有 API 可见的摄像头;不等于物理镜头总数。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。
FindingIds.NO_VISIBLE_AUDIO_INPUT
no_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_HINT
writable_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_MODULE
framework_module
是当前进程中有已知 Hook 模块;evidence["modules"] 给出模块名。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。
FindingIds.FRAMEWORK_STACK
framework_stack
是当前调用栈有已知框架类;evidence["classes"] 给出类名。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。

调试器 · checkDebugger

SDK 写法 / 实际 ID参与汇总判断内容 / 证据返回值含义
FindingIds.JAVA_CONNECTED
java_connected
是Java 调试器已连接。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。
FindingIds.JAVA_WAITING
java_waiting
是当前进程正在等待 Java 调试器。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。
FindingIds.NATIVE_TRACER
native_tracer
是TracerPid > 0;evidence["tracerPid"] 给出读取值。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。
FindingIds.DEBUGGABLE_BUILD
debuggable_build
否宿主声明 debuggable;不表示当前有调试器,也不表示重打包。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。

无障碍 · checkAccessibility

SDK 写法 / 实际 ID参与汇总判断内容 / 证据返回值含义
FindingIds.SERVICE_ENABLED
service_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_VPN
default_network_vpn
是当前默认网络声明 VPN transport。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。
FindingIds.DEFAULT_HTTP_PROXY
default_http_proxy
是默认 HTTP / PAC 代理配置存在。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。
FindingIds.VIRTUAL_INTERFACE_HINT
virtual_interface_hint
否可见的活动网卡名符合 tun / tap / ppp / wg 规则,仅为辅助线索;evidence["interfaces"] 给出网卡名。DETECTED:条件成立;NOT_DETECTED:未检出;UNKNOWN:无法判断。

SIM / eSIM · checkSimNetwork

SDK 写法 / 实际 ID参与汇总判断内容 / 证据返回值含义
FindingIds.SIM_PRESENCE
sim_presence
是决定该函数顶层卡状态,兼容实体卡与已启用 eSIM。PRESENT:有卡;ABSENT:完整覆盖下确认无卡;UNKNOWN:证据不足;NOT_APPLICABLE:无电话能力。
FindingIds.NO_SIM_AND_WIFI
no_sim_and_wifi
否“确认无卡”与“默认网络是 Wi-Fi”的组合条件。DETECTED:无卡且 Wi-Fi;NOT_DETECTED:必要输入已知且组合不成立;UNKNOWN:输入不足;NOT_APPLICABLE:无电话能力。

安装来源 · checkInstallSource

SDK 写法 / 实际 ID参与汇总判断内容 / 证据返回值含义
FindingIds.INSTALLER_SOURCE
installer_source
是系统记录的安装方;包名在 facts["installerPackage"],并非 Google 官方验证。GOOGLE_PLAY:记录为 com.android.vending;OTHER:记录为其他非空包名;UNKNOWN:来源不可用。

签名 · checkSignature

SDK 写法 / 实际 ID参与汇总判断内容 / 证据返回值含义
FindingIds.PACKAGE_NAME_MISMATCH
package_name_mismatch
是宿主包名与可信基准是否一致;仅在基准有效后检查。CONSISTENT:与基准一致;INCONSISTENT:不一致;UNKNOWN:无法判断。
FindingIds.SIGNATURE_MISMATCH
signature_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_MISMATCH
content_mismatch
是内容检查前置条件不满足;没有基准、基准无效或包名 / 版本不匹配时出现。仅 UNKNOWN;reason 为 no_baseline、invalid_baseline 或 baseline_for_different_package_or_version。

仅在异常时出现的诊断子项

所属检测SDK 写法 / 实际 ID参与汇总返回含义
设备能力FindingIds.SENSORS
FindingIds.CAMERAS
FindingIds.AUDIO_INPUTS
是仅 UNKNOWN:对应系统服务或枚举调用失败。成功时输出具体能力子项,不输出同名“成功”项。
SIM / eSIMFindingIds.SIM_READ
FindingIds.SIM_DEFAULT
FindingIds.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 不会自动区分“合法加固”与“恶意修改”。

加固后的实际情况签名检测内容检测
最终签名符合基准,受检内容也未改变CONSISTENTCONSISTENT(仅覆盖基准范围)
最终签名符合基准,但加固改写 / 加密 / 替换了原 DEX 或受检 assets;仍保留旧基准CONSISTENTINCONSISTENT:摘要或 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 参数独立。