RISK CONTROL SDK

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

引入 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:直接调用

检测功能已包含在同一个 SDK 中,已接入的项目无需再添加依赖。首次接入请看主接入文档的依赖配置;这里仅说明检测 API 的调用。

SDK 发布包已开启混淆,不再附带源码 JAR。检测 API 和默认参数保持不变,宿主可继续开启 R8,无需额外添加 SDK keep 规则。内容与签名插件仍为可选,需用当前构建重新生成基准。

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 不要重叠扫描,独立函数请放后台线程。

SDK 的版本与仓库配置统一维护在主接入文档,无需重复配置。

03

两项可选接入

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

插件与 SDK 分开发包,目前工程仅配置本地 Maven 发布。以下插件依赖仅在需要自动生成签名/内容基准时添加,普通检测无需接入。

仅影响签名和内容两项。插件 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

用到时再展开

完整 API 参考:参数、返回字段、异常与示例

适用于 SDK 0.1.7;以下均为公开 API。常规检测无需额外初始化、危险权限或插件。签名 / 内容仅在需要基准时配置;Integrity 独立异步取凭证。使用 import com.remax.riskcontrol.sdk.*,Integrity 类型位于子包 .integrity。

① 检测调用、并发与规则参数

下面 12 个函数均同步返回 CheckResult,必须在后台调用。除特殊参数外,只需 session: DetectionSession;默认规则已经内置。

CheckId函数签名(均返回 CheckResult)结果 id / scope
EMULATORcheckEmulator(session: DetectionSession)emulator
known_environment_features
DEVICE_CAPABILITIEScheckDeviceCapabilities(session: DetectionSession)device_capabilities
app_visible_devices
ROOTcheckRoot(session: DetectionSession)root
visible_root_related_artifacts
HOOKcheckHook(session: DetectionSession, classLoader: ClassLoader = session.context.classLoader)hook
current_process_known_traces
DEBUGGERcheckDebugger(session: DetectionSession)debugger
current_process
ACCESSIBILITYcheckAccessibility(session: DetectionSession)accessibility
enabled_services
INSTALLED_TOOLScheckInstalledTools(session: DetectionSession, targets: List<ToolTarget> = defaultToolTargets())installed_tools
caller_visible_packages
VPN_PROXYcheckVpnProxy(session: DetectionSession)vpn_proxy
app_default_network
SIM_NETWORKcheckSimNetwork(session: DetectionSession)sim_network
observed_logical_slots_and_default_network
INSTALL_SOURCEcheckInstallSource(session: DetectionSession)install_source
host_install_or_update_record
SIGNATUREcheckSignature(session: DetectionSession, baseline: SignatureBaseline? = 生成基准)signature
host_package_signers
APK_CONTENTcheckApkContent(session: DetectionSession, baseline: ContentBaseline? = 生成基准)apk_content
baseline_named_entries_and_dex_inventory

“生成基准”为默认行为说明,不是 Kotlin 表达式:省略 baseline 时读取本轮已启用的对应生成基准;显式传 null 则本次无基准,返回 UNKNOWN / no_baseline。checkHook 的 classLoader 可指定宿主用于加载类的 ClassLoader;默认使用应用的。CheckId.title 是中文显示名称,不能把枚举名当作 CheckResult.id。

会话、扫描与手动分发

DetectionSession(context: Context, rules: DetectionRules = DetectionRules())
RiskScanner(context: Context)

// RiskScanner 实例方法
fun scan(
    config: ScanConfig = ScanConfig(),
    selected: Set<CheckId> = CheckId.entries.toSet(),
    onResult: (CheckId, CheckResult) -> Unit,
    onComplete: (Map<CheckId, CheckResult>) -> Unit = {}
): Unit
fun close(): Unit

// 顶层函数
fun executeCheck(id: CheckId, session: DetectionSession,
    config: ScanConfig = ScanConfig()): CheckResult
fun defaultToolTargets(): List<ToolTarget>
接口 / 字段类型与默认值含义与约束
DetectionSession.context / rulesContext / DetectionRules保存 applicationContext 和规则快照;本轮懒加载并复用进程、网络、包信息与生成基准。每轮新建,可供同轮多项并发共享;不要长期复用后期待网络等信息实时刷新。
scan.config / selected默认配置 / 全部 12 项自动跳过未启用且未传显式基准的签名 / 内容项;已启用但基准损坏时仍执行并返回 UNKNOWN。selected 空集抛 IllegalArgumentException。
scan.onResult / onComplete逐项结果 / 完成结果 Map后台回调;逐项可能并发、顺序不保证,完成 Map 按枚举排序且只包含实际执行项。全部跳过时完成回调为空 Map。UI 需切主线程,回调不要抛异常。
scan 返回 / 并发限制Unit立即返回;3 个普通工作线程 + 1 个内容检查线程。同 scanner 扫描中再次扫描,或关闭后调用,抛 IllegalStateException;不会自动排队后续轮次。
close()Unit幂等;取消任务并关闭线程池。取消时不保证继续回调,已在执行的回调可能尚未结束。生命周期结束时调用。
executeCheck(...)CheckResult同步分发,不执行 scanner 的跳过逻辑。规则来自 session;config.tools / signature / content / useGeneratedBaselines 控制对应检查。config.rules 不会替换已有 session 的规则。
defaultToolTargets()List<ToolTarget>返回新列表,包含下方内置 5 个包名。自定义名单始终与内置合并,同名采用调用方的 visibilityDeclared;空列表也保留内置项。
ToolTargetpackageName: String;visibilityDeclared: Boolean内置精确查询包名默认 true,其他默认 false。true 是调用方确认 Manifest 精确 queries 已覆盖,不能靠设 true 获得权限;宿主删掉内置 queries 后应传 false。不可见且未查到返回 UNKNOWN。

可恢复的读取失败一般转为子项 UNKNOWN;线程中断、生命周期取消或宿主回调异常不保证转换成完整 CheckResult。

ScanConfig:全部字段

接口 / 字段类型与默认值含义与约束
rulesDetectionRules = DetectionRules()用于本轮新建 DetectionSession。
toolsList<ToolTarget> = defaultToolTargets()工具安装检测名单,自动合并内置;不改变 rootTools。
signatureSignatureBaseline? = null显式签名基准优先;null 时再按开关决定是否使用生成基准。
contentContentBaseline? = null显式内容基准优先;null 时再按开关决定是否使用生成基准。
useGeneratedBaselinesBoolean = true是否使用插件生成基准;false 仍可用显式 signature / content。与直接 checkSignature(..., null) 的含义不同。

DetectionRules:全部字段与默认规则

接口 / 字段类型与默认值含义与约束
emulatorPathsList<String>默认 /dev/qemu_pipe、/dev/socket/qemud、/system/lib/libc_malloc_debug_qemu.so。
rootPathsList<String>默认 /system/bin/su、/system/xbin/su、/sbin/su、/su/bin/su、/system/bin/.ext/.su。
rootToolsList<ToolTarget>默认 ToolTarget("com.topjohnwu.magisk");与 tools 独立。
hookClassesList<String>默认 de.robv.android.xposed.XposedBridge、de.robv.android.xposed.XC_MethodHook。
hookModulesSet<String>默认 libfrida-gadget.so、libfrida-agent.so、libxposed_art.so、liblsposed.so、libedxposed.so。
sensorTypesList<Int>默认 [1, 4, 5, 6]:加速度计、陀螺仪、光照、气压。使用 Android Sensor 类型常量。

规则字段传入后替换该字段默认值;要追加请基于默认集合 copy。清空规则并不证明环境安全。工具安装名单合并是单独规则,不代表其他集合也会自动合并。

import android.content.Context
import com.remax.riskcontrol.sdk.*

// 在后台线程执行;每轮新建 session。
fun customRootCheck(context: Context, extraSuPath: String): CheckResult {
    val defaults = DetectionRules()
    val config = ScanConfig(rules = defaults.copy(
        rootPaths = defaults.rootPaths + extraSuPath
    ))
    val session = DetectionSession(context, config.rules)
    return executeCheck(CheckId.ROOT, session, config)
}
② 手动基准与生成基准返回值

优先使用可选插件自动生成。手动基准用于已有可信 CI / 发布系统的宿主;不能把用户设备当前包的摘要直接作为可信基准。SHA-256 为 64 位十六进制,大小写和证书摘要中的冒号会被规范化。

SignatureBaseline(
    packageName: String,
    allowedSignerSets: List<Set<String>>,
    allowedRotationAncestors: Set<String> = emptySet()
)
ContentBaseline(
    packageName: String, versionCode: Long,
    files: List<ContentFile>, dexModules: List<DexModule> = emptyList()
)
ContentFile(apk: String = "base", entry: String, sha256: String)
DexModule(apk: String, files: Map<String, String>,
    required: Boolean = apk == "base", matchByDigest: Boolean = false)
接口 / 字段类型与默认值含义与约束
SignatureBaseline.packageNameString目标应用包名,必须与当前宿主一致;不一致为 INCONSISTENT。
allowedSignerSetsList<Set<String>>外层是多套允许的签名方案;每个内层集合是完整的当前签名者证书 SHA-256 集合,不是任选其中一个。
allowedRotationAncestorsSet<String> = emptySet()可接受的签名轮换祖先证书摘要;只接受 Android 9+ 系统验证的单签名者历史链,不放宽多签名者集合匹配。
ContentBaseline.packageName / versionCodeString / Long可信构建的包名与版本号;不匹配返回 UNKNOWN / baseline_for_different_package_or_version,不用旧版本基准判篡改。
ContentBaseline.filesList<ContentFile>需检查的具体 ZIP 条目;可以为空,但 files 与 dexModules 不能同时为空。
ContentFile.apk / entry / sha256String / String / Stringapk 为 base 或安装后的 splitName,不是磁盘 APK 文件名;entry 为 ZIP 内相对路径,例如 assets/config.json;sha256 是可信文件摘要。路径不得含绝对路径、反斜杠或 . / .. 段。
ContentBaseline.dexModulesList<DexModule> = emptyList()声明完整 DEX 清单;为空时仅检查 files,不能检查额外新增 DEX。非空时还会检查所有已安装 APK 中的意外 DEX;必须恰有一个 required=true 的 base。
DexModule.apk / filesString / Map<String, String>模块名和根目录 DEX 文件名 → SHA-256,例如 classes.dex;不支持嵌套 DEX 路径。
DexModule.requiredBoolean = (apk == "base")必须安装的模块缺失为 INCONSISTENT;可选模块未安装为 UNKNOWN 子项且 affectsSummary=false。
DexModule.matchByDigestBoolean = falsefalse 比较文件名及摘要;true 按摘要多重集合比较,允许融合后的 DEX 重命名,但数量必须一致。

基准无效为 UNKNOWN / invalid_baseline;没有基准为 UNKNOWN / no_baseline。内容基准最多 32 个模块,assets / 命名文件与 DEX 合计最多 64 个;模块和文件标识不得重复。单文件读取上限 64 MiB、总计 128 MiB,超限 / 无法读取返回 UNKNOWN。

import android.content.Context
import com.remax.riskcontrol.sdk.*

// 所有 expected / trusted 值由可信构建系统提供,不从当前安装包反向生成。
// 示例假设应用只有 base,且其完整 DEX 清单只有 classes.dex。
fun checkWithTrustedBaselines(
    context: Context, expectedPackage: String, expectedVersionCode: Long,
    trustedCertificateSha256: String, trustedDexSha256: String
): Pair<CheckResult, CheckResult> {
    val signature = SignatureBaseline(
        packageName = expectedPackage,
        allowedSignerSets = listOf(setOf(trustedCertificateSha256))
    )
    val content = ContentBaseline(
        packageName = expectedPackage,
        versionCode = expectedVersionCode,
        files = emptyList(),
        dexModules = listOf(DexModule(
            apk = "base", files = mapOf("classes.dex" to trustedDexSha256)
        ))
    )
    val session = DetectionSession(context)
    return checkSignature(session, signature) to checkApkContent(session, content)
}

生成基准的诊断返回

接口 / 字段类型与默认值含义与约束
loadGeneratedBaselines(context)GeneratedBaselines同步读取 assets/risk-control/baseline.properties,放后台执行。显式调用时会尝试读取文件,即使两项开关关闭。
GeneratedBaselines.signature / contentSignatureBaseline? / ContentBaseline?各自解析成功的基准;未生成该节或该节无效为 null。一节无效不必然丢弃另一节。构造默认均 null。
GeneratedBaselines.reasonString = ""成功或有效文件中未启用某节时可为空;读取失败 generated_baseline_unavailable:*;整体格式错误 invalid_generated_baseline;分节错误 invalid_generated_signature_baseline / invalid_generated_content_baseline,可用分号连接。异常类名后缀仅供诊断。
③ CheckResult、Finding 与 facts 字段字典
接口 / 字段类型与默认值含义与约束
CheckResult.idString检测标识,见函数表;不同于 CheckId 枚举名。
CheckResult.scopeString该检查覆盖范围,见函数表;不是登录身份 scope。
CheckResult.findingsList<Finding>所有独立子项,包含不参与汇总的诊断项。相同 ID 可有多条。
CheckResult.factsMap<String, String>本次可读取的事实,字段可缺失,所有值都是字符串;缺失不等于 false / 0。
CheckResult.observedAtMillis / durationMillisLong / Long检查开始的 Unix 毫秒 / 单调时钟测得的耗时毫秒。session 缓存事实可能早于该检查开始。
CheckResult.statusStatus;只读计算属性只汇总 affectsSummary=true 的子项;不等于取第一项。
Finding.id / statusString / Status子项 ID 和结果。固定 ID / 动态 ID 构造函数见现有子项表。
Finding.reasonString = ""未知 / 失败原因;空字符串表示未附说明,不必然是成功。
Finding.evidenceMap<String, String> = emptyMap()该子项的证据键值;按检查而异,字段可缺失;不能把字符串证据当统一安全结论。
Finding.affectsSummaryBoolean = truefalse 时只展示诊断,不改变顶层 status。
finding(id) / findingsById(id) / findingsByPrefix(prefix)Finding? / List<Finding> / List<Finding>精确首项(缺失 null)/ 全部精确项 / 全部前缀项(缺失空列表);动态前缀建议带冒号。

条件类:DETECTED > UNKNOWN > NOT_DETECTED;校验类:INCONSISTENT > UNKNOWN > CONSISTENT。分类类(GOOGLE_PLAY / OTHER、PRESENT / ABSENT / NOT_APPLICABLE)须一致;必需子项未知、没有有效汇总项或混合不同结果族均为 UNKNOWN。NOT_DETECTED 只是本次未检出,UNKNOWN 不能当作未安装 / 无卡 / 校验通过。

facts 字段字典

检查可返回的键值与解释
模拟器fingerprint, model, manufacturer, hardware, product, brand, device, abisAndroid Build 字段;abis 为逗号分隔的支持 ABI。
设备能力visibleSensorCount, visibleCameraCount, visibleAudioInputCount, audioInputTypes计数字符串;audioInputTypes 为 Android AudioDeviceInfo.type 数字的逗号列表。均为 API 可见设备,不等于实际物理数量。
Root / Hook / 调试器interpretation(Root)Root 可带解释文本;Hook / 调试器主要读取 findings / evidence,没有固定 facts 数值字段。
无障碍enabledServiceCount, service.{index}, service.{index}.eventPackages数量、服务组件 / ID、监听包名的逗号列表;all_packages 表示未限定。不能由此证明自动点击。
工具安装interpretation解释文本;每个包的判断在 findings 中。
VPN / 代理transports, networkObservedAt当前默认网络的 wifi / cellular / ethernet / vpn 逗号列表;网络采样 Unix 毫秒。不是所有连接的实际路由证明。
SIM 与网络telephonyCapable, activeModemCount, eSimSupported电话能力布尔、当前调制解调器数量;eSimSupported 仅表示 eSIM 硬件能力,不代表有已启用配置。
SIM 状态defaultSimState, slot.{slot}.stateAndroid TelephonyManager.SIM_STATE_* 整数的字符串,slot 为逻辑槽索引,不等于物理卡槽。
SIM 运营商defaultSimMccMnc, defaultSimCountry默认 SIM 的 MCC/MNC、国家 ISO 字符串;拿不到为 UNKNOWN,不能据此认定无卡。
SIM 汇总simPresence, absenceCoverageComplete, simReady, transportssimPresence 为 Status 名;absenceCoverageComplete 表示是否充分覆盖无卡证明;simReady 为 true / false / UNKNOWN,未就绪不等于没卡;transports 同网络项。实体卡 / 已启用 eSIM 都可作为有卡证据。
本地安装来源initiatingPackage, installerPackage, record发起方(Android 11+)、安装器包名,拿不到为 UNKNOWN;record 为 RECORDED_GOOGLE_PLAY / RECORDED_OTHER / UNKNOWN。不是 Google 验证结论。
签名currentSignerSha256, multipleSigners排序后的当前证书 SHA-256 逗号列表;是否多签名者 true / false。
内容bytesRead, coveredEntries, dexModules, installedApks累计读取字节、基准覆盖的 apk:entry 列表、基准模块列表、实际安装容器列表;后 3 项逗号分隔。coveredEntries 不表示所有文件已成功验证。
解释文本interpretation部分检查附带的可读范围说明,不作为机器协议字段判断。

逗号列表解析时去除空白;异常、提前返回、系统版本限制会导致部分键不存在。业务分支优先使用 Status、FindingIds 和明确的公开模型,不依赖英文解释或异常类名。FindingIds.TARGETS 是保留常量,当前工具检测不输出该子项。

④ Play Integrity:全部返回字段、请求绑定与错误
PlayIntegrityClient(context: Context, cloudProjectNumber: Long)
fun requestPlayIntegrityToken(): Task<IntegrityTokenResult>

复用同一应用 / 项目的 client 以共享预热;每次请求仍生成新 requestId 与凭证。cloudProjectNumber 是 Google Cloud 数字项目编号,不是 projectId 字符串;≤ 0 在请求时返回失败 Task。业务无需传 requestHash,也无需额外申请用户权限。仅调用成功表示取得凭证,不能当作完整性验证通过。

接口 / 字段类型与默认值含义与约束
IntegrityTokenResult.tokenStringGoogle 返回的加密凭证,供业务后端验证;SDK 不解密、不自动上传,也不提供最终风险结论。
cloudProjectNumberLong本次使用的项目编号;后端仍应使用自身可信配置,不盲信客户端提交值。
requestContextIntegrityRequestContext请求绑定上下文,须与 token 一同交给后端。
durationMillisLong成功请求总耗时(含需要的预热),单位毫秒。
requestHashString等于 requestContext.requestHash;不是服务端 nonce。
IntegrityRequestContext.packageNameString真实 Android 应用包名,不是登录接口 bundleId。
requestId / createdAtMillisString / LongSDK 新生成的 UUID / 客户端 Unix 毫秒。
requestHash / canonicalPayload()String / String规范文本的 UTF-8 SHA-256 小写十六进制 / 规范文本本身。

后端如何匹配本次请求

risk-control/environment/v1
{packageName}
{requestId}
{createdAtMillis}

上面 4 行以 LF 换行,末尾不加换行,时间为十进制整数。后端按传入上下文重建文本并计算 SHA-256,再与 Google 解密结果的 requestDetails.requestHash 比较;同时核验预期包名、时效和业务要求的应用 / 设备等结果。不要只比较客户端传来的两个 hash。上下文由客户端生成,不能代替服务端挑战、登录授权或业务防重放策略;后端接口由业务实现。

失败、超时与线程

接口 / 字段类型与默认值含义与约束
IntegrityTokenException.stageIntegrityStageCONFIGURATION:本地项目参数;PREPARE:预热;REQUEST:取凭证。
reasonString本地 cloud_project_number_missing_or_invalid、cancelled、client_error;Google 错误对应名称,未知码为 GOOGLE_ERROR_数字。
googleErrorCodeInt?Google 数字错误码;本地异常通常 null。可结合 stage 分析,异常 cause 可供诊断。
90 秒截止时间TimeoutException返回的 Task 最多等待 90 秒;超时不一定包装为 IntegrityTokenException。已提交 Google 的操作不能被强制停止,晚到的预热不会继续发起已过期请求。
重试 / 回调异步 Task仅 provider 失效(-19)时自动重新预热并请求一次;其他配置 / 网络失败不无限自动重试。后续显式调用可重新尝试。可主线程发起,默认 listener 在主线程执行,重活切后台。
Google 错误码reason
-1API_NOT_AVAILABLE
-2PLAY_STORE_NOT_FOUND
-3NETWORK_ERROR
-5APP_NOT_INSTALLED
-6PLAY_SERVICES_NOT_FOUND
-7APP_UID_MISMATCH
-8TOO_MANY_REQUESTS
-9CANNOT_BIND_TO_SERVICE
-12GOOGLE_SERVER_UNAVAILABLE
-14PLAY_STORE_VERSION_OUTDATED
-15PLAY_SERVICES_VERSION_OUTDATED
-16CLOUD_PROJECT_NUMBER_IS_INVALID
-17REQUEST_HASH_TOO_LONG
-18CLIENT_TRANSIENT_ERROR
-19INTEGRITY_TOKEN_PROVIDER_INVALID
-100INTERNAL_ERROR
import com.remax.riskcontrol.sdk.integrity.*
import java.util.concurrent.TimeoutException

// client 由宿主持有并复用;onToken 交给业务自己的后端调用层。
fun obtainIntegrityToken(
    client: PlayIntegrityClient,
    onToken: (IntegrityTokenResult) -> Unit,
    onFailure: (String) -> Unit
) {
    client.requestPlayIntegrityToken()
        .addOnSuccessListener { result ->
            // 同时传 token 与 requestContext,不只传 cloudProjectNumber。
            onToken(result)
        }
        .addOnFailureListener { error ->
            val description = when (error) {
                is IntegrityTokenException ->
                    "${error.stage}: ${error.reason}, code=${error.googleErrorCode}"
                is TimeoutException -> "本次取凭证超时"
                else -> "取凭证失败:${error.message}"
            }
            onFailure(description)
        }
}
⑤ 插件全部配置、路径与默认值

插件为 app 模块级可选接入,仅影响签名与内容基准。插件版本 0.1.0 当前仅配置本地 Maven 发布,不能假定 GitHub Packages 已有插件;SDK 0.1.7 的远端发布不代表插件已远端发布。具体引入见插件接入。

接口 / 字段类型与默认值含义与约束
contentEnabledProperty<Boolean>;false启用 assets + 最终 DEX 内容基准;不开启不执行对应生成 / 改包流程。
signatureEnabledProperty<Boolean>;false独立启用签名基准。两项都 false 时不校验证书,不施加插件多 APK 输出限制。
signingModeProperty<String>;"playAppSigning"仅 buildSigning / playAppSigning;前者取当前变体签名,后者必须提供 Play 应用签名公钥证书。
certificateFilesConfigurableFileCollection;空X.509 DER / PEM 公钥证书文件,非 keystore、非私钥。最多 16 个不同证书摘要;每个证书作为一套单签名者方案,不表示多签名者组合。非空时优先于 buildSigning 的 keystore。
certificateFiles 路径rootProject.file(...) / file(...)分别相对工程根目录 / 当前 app 模块;证书需随项目或 CI 可信输入提供。setFrom 替换继承集合,from 追加。开启签名但缺失 / 无效时构建报错;Play 模式缺文件会提示到 Console 下载应用签名证书。
assetIncludesListProperty<String>;["**"]Ant 风格 glob,相对合并后的 assets 根目录;例如 config/**,不要加 assets/ 前缀。
assetExcludesListProperty<String>;[]从 includes 中排除匹配路径;两者仅控制 assets,不影响自动生成的完整 DEX 清单。
onVariants { variant -> ... }Action<in RiskVariantBaseline>先继承全局配置,再按注册顺序应用回调;变体间配置独立。variant.name 是完整名如 devRelease,variant.buildType 是 release;均为 String。variant 支持以上同名选项。
// app/build.gradle.kts;先按上方说明应用插件。
riskContentSignature {
    // 默认保持关闭;仅为指定变体开启。
    onVariants { variant ->
        if (variant.name == "prodRelease") {
            variant.contentEnabled.set(true)
            variant.assetIncludes.set(listOf("config/**", "rules/*.json"))
            variant.assetExcludes.set(listOf("config/debug/**"))
            variant.signatureEnabled.set(true)
            variant.signingMode.set("playAppSigning")
            variant.certificateFiles.setFrom(
                rootProject.file("certificates/play-app-signing.der")
            )
        }
    }
}

自动基准写入 assets/risk-control/baseline.properties,该路径保留给插件,冲突会报错。使用公开 AGP Artifacts API 接入 APK / AAB;已验证 AGP 8.13.2 / Gradle 8.13 / JDK 17,不承诺未来版本无需适配。构建 API 或产物约束不兼容时应构建失败,不会静默生成“通过”基准。最终导出目录为 app/build/outputs/risk-control/<variant>/apk 或 bundle。

运行时是否跳过取决于开关:未启用且未显式传基准的扫描项跳过;已启用但基准缺失 / 损坏仍执行并返回 UNKNOWN。插件不保证适配加固改写,详情见加固影响;多 APK、部分动态功能交付等限制见检查范围。

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,并按包名定位;FindingIds.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 参数独立。