先看结论与判断条件

  • 先冻结原始包与加固包、应用签名身份、测试数据和密钥配置;候选或配置变化后,旧回执不能直接继承。
  • 密钥存在不等于调用契约正确。alias、算法、purpose、block mode、padding、digest 和用户认证条件都要进入对照记录。
  • 加密回归比较解密后的业务断言和元数据,不比较随机化加密产生的密文是否完全相同,也不能为测试复用固定 IV。
  • 签名回归既要验证成功路径,也要验证错误 key、错误消息、认证缺失和输入被改动时确实失败,避免只记录一次绿色结果。
  • 设备锁定、认证状态或密钥失效后的恢复由业务策略决定;捕获异常后无条件删 key 会把可诊断故障变成数据永久丢失。
  • CTS、Play Integrity 与 OWASP 控制提供平台和安全上下文,但不能替代目标 App 在真实候选、设备与账号上的 Keystore 业务验收。

先冻结候选、签名身份和密钥配置

Keystore 回归的第一个对象不是某段测试代码,而是唯一可识别的候选。记录 APK 或设备派生包摘要、applicationId、versionCode、签名证书摘要、构建变体和加固配置标识;原始包与加固包必须来自同一源码和依赖基线。候选身份不一致时,测试差异无法归因于加固。

密钥配置同样需要版本化。至少登记 alias 命名规则、算法、用途、密钥长度或曲线、block mode、padding、digest、是否要求用户认证、认证条件和创建触发点。不要把配置散落在代码、远程开关和测试说明中;回执要能回答本次调用实际使用了哪一套配置。

Android Keystore 允许应用限制密钥用途,并可能在设备支持时使用硬件保护,但页面定义不证明某台设备的具体实现,也不证明加固候选可以正常调用。事实层只引用平台契约;是否创建成功、是否硬件支持、异常如何映射和业务能否恢复,必须由目标候选的设备回执确认。

回归基线必须固定的身份字段
对象最小标识比较方式缺失后果
应用候选文件摘要与版本原始包和加固包一一对应无法归因差异
签名身份证书摘要确认安装与升级身份连续混入重签变量
密钥配置profile 版本和字段逐字段比较同 alias 不同语义
测试数据公开样本摘要两候选使用相同输入业务断言不可比
设备型号、系统与安全能力每次执行保留回执厂商差异被忽略
测试代码runner 与用例版本绑定候选和结果测试变更冒充回归

把 alias 和 KeyGenParameterSpec 当成接口契约

许多问题表现为找不到密钥,根因却是调用方使用了不同 alias、不同用户范围或不同创建条件。首次创建前先查询目标 alias,创建后读取可公开检查的属性并记录结果;后续签名或解密路径只能消费已经登记的 alias,不应在捕获任意异常后悄悄创建同名新 key。

KeyGenParameterSpec 一类配置决定 key 可以执行什么操作。只要 purpose、padding、block mode、digest 或认证要求不匹配,调用就应失败,而不是尝试更宽松的配置继续。加固前后分别记录配置投影和实际操作,能区分调用代码变换导致的错误与本来就不允许的使用方式。

工程上应把密钥 profile 与业务数据格式共同版本化。例如密文元数据记录 key profile、算法版本和必要参数,签名请求记录 profile 与消息格式。文章不规定通用 alias 或密钥长度;这些值应来自项目威胁模型、平台支持和兼容要求,并由安全负责人批准。

密钥调用契约的字段与负向用例
字段正向断言负向断言记录边界
alias找到预期条目错误 alias 不得回退不记录密钥材料
purpose操作属于允许集合解密 key 不得用于签名记录枚举值
algorithm与数据格式一致算法错配明确失败不宣称强度结论
padding 或 mode初始化参数一致不兼容组合被拒绝绑定 profile 版本
digest签名验签契约一致错误 digest 不得静默接受记录名称而非输入明文
认证条件满足条件后可调用未满足时返回可识别错误不记录生物特征数据

首次安装先验证生成与持久化边界

首次安装场景要从应用无本地状态开始,确认初始化代码只创建预期 alias,并且重复进入初始化不会覆盖现有 key。测试同时验证应用数据库或首选项中的 profile 标识、密文元数据和 Keystore alias 能够互相对应。只看到 containsAlias 返回成功,不足以证明后续业务操作可用。

生成路径要覆盖正常创建、已经存在、配置不支持和认证前置条件未满足等分支。错误必须映射为业务可处理状态,例如提示重新认证、限制功能或进入人工恢复;不能把所有异常都转换成创建成功,也不能用 catch 后删除重建来让测试变绿。

设备能力会影响实际结果。instrumented test 适合验证真实 Android 运行时和系统 API,但单台设备通过不能代表全部 API、ABI 或厂商实现。矩阵至少记录设备型号、系统版本、锁屏配置、Keystore provider 可观察信息和实际结果,覆盖策略由用户设备分布与风险决定。

首次安装生成路径的可观察状态
步骤预期输出失败分类禁止补偿
查询 alias明确存在或不存在查询异常默认视为不存在
创建 key产生指定 profile不支持或参数错误放宽用途重试
写入 profile版本与 alias 对应本地写入失败只保留半套状态
再次初始化复用原 key配置漂移冲突覆盖同名 key
执行探针签名或解密成功操作级错误只凭 key 存在通过
保存回执绑定候选和设备证据不完整手工补写结论

加密解密回归比较明文断言而不是密文字节

随机化加密的密文通常不应在两次执行中保持完全相同,因此原始包与加固包不能以密文字节相等作为通过条件。更合理的断言是:每个候选使用自己的受控 key 对同一公开样本加密,再用相应 key 解密,得到与输入完全一致的业务数据,同时保存算法 profile、必要参数和失败类型。

测试不得为了可比较而固定或复用本应唯一的 nonce 或 IV。回归记录可以保存公开测试样本摘要、密文长度、参数存在性和解密断言,不保存生产明文、真实令牌或用户数据。对篡改后的密文、错误参数和错误 alias 还要执行拒绝测试,确认系统没有返回部分明文或静默空值。

进程重建后再解密能检查持久化边界:密钥条目仍由 Keystore 管理,应用只恢复必要元数据和密文。若恢复失败,先分辨 alias/profile 不一致、本地元数据丢失、认证状态改变与密文损坏;这些原因需要不同处置,不能统称为加固后解密失败。

加密解密路径的正向与拒绝断言
用例输入预期证据
同 key 往返公开样本解密后完全一致样本摘要和结果
进程重建已保存密文和元数据恢复后可正确解密重建时间线
错误 alias另一个测试 key明确拒绝错误类型
密文改动修改后的公开密文认证失败拒绝回执
参数缺失不完整元数据不尝试猜测字段错误
认证未满足受限 key要求认证或拒绝业务映射

签名验签要覆盖消息绑定与错误 key

签名回归使用公开、固定且带场景标识的消息,签名后由对应公钥验证。通过条件不是签名结果两次相同,而是每个候选都能按同一 profile 完成签名,并且验证器能识别正确消息。签名实现可能具有随机性,因此不能把签名字节完全相等当作跨候选语义一致。

拒绝路径至少包含消息被改动、使用错误公钥、alias 不存在、purpose 不允许和认证未满足。业务层要保留原始异常类别与映射结果,便于判断是需要重新认证、配置错误还是不可恢复状态。把所有失败统一成签名失败会丢失恢复决策所需信息。

如果签名用于服务端挑战或高风险指令,还要验证挑战、主体、用途和有效会话的绑定;Keystore 只负责受限 key 操作,不自动提供完整授权。Play Integrity verdict 可以作为服务端额外信号,但任何单一 verdict 都不应被写成设备绝对可信或本地签名必然有效。

签名验签矩阵的最低覆盖
场景签名调用验签结果业务决策
正确消息与 key允许通过继续受控流程
消息被修改使用原签名失败拒绝请求
错误公钥签名不变失败标记身份错配
错误 alias初始化失败不执行报告配置错误
purpose 不允许操作被拒绝不执行阻止放宽配置
认证未满足暂不可用不执行请求重新认证

升级和进程重建不能靠一次启动成功验收

覆盖升级要从已使用旧候选创建 key 和业务数据开始,再安装目标加固候选。升级后先核对应用签名与数据连续性,再读取 profile、执行解密或签名探针,并进入真实业务路径。新装通过只能证明创建路径,不能证明旧版本留下的 alias、元数据和数据格式仍能被新版本消费。

进程重建场景要在密钥操作前后分别终止进程,恢复 Activity、Service 或持久任务后继续操作。调用方不能依赖只存在于内存的 cipher、signature 或认证状态;恢复逻辑应从持久化的业务状态重新初始化原语,并对已经消耗的请求保持幂等。

这篇文章不重复 Room 数据库迁移的 schema 与逐级迁移问题。若 Keystore profile 与数据库字段共同升级,只在此核对二者的引用一致性和操作结果;数据库结构与迁移不变量仍由独立迁移验收负责,避免一个测试报告同时给两个搜索问题第二套答案。

升级与进程重建的阶段门禁
阶段准备状态核心断言失败归属
旧版准备旧候选创建数据profile 和样本可识别基线失败
覆盖安装签名与版本符合预期安装成功且数据未被重置发布身份或安装
升级后读取旧 alias 和元数据引用一致配置或数据边界
升级后操作旧数据与目标候选解密或签名契约成立Keystore 调用
进程终止保存操作状态无半完成副作用生命周期处理
恢复执行重建调用对象结果幂等且错误可解释恢复策略

设备锁定变化和密钥失效必须分开处理

用户认证绑定 key 的可用性会受配置和设备安全状态影响。测试应在项目允许的锁屏、认证和设备变化场景中记录操作前置条件、实际异常与业务提示。不要在文章中给所有设备套用同一失效结论;平台、系统版本、厂商实现和 key 配置都可能改变结果。

业务错误至少区分暂时缺少认证、需要用户重新认证、key 已不可用、alias 不存在、参数不匹配和未知 provider 错误。只有产品明确允许且数据可以恢复时,才能进入新 key 流程;若旧 key 保护的是不可再生数据,无条件重建会永久切断解密能力。

恢复方案要在故障前设计。可再生的会话 key、服务端可恢复的数据 key 和仅用于本地签名的身份具有不同处置。工程判断必须写明谁批准重建、哪些数据会失效、如何通知用户、旧密文怎样标记,以及新 key 创建后哪些服务端绑定需要更新。

用结构化脚本检查回归矩阵是否真的覆盖关键路径

下面的 Python 代码不访问 Android Keystore,也不生成任何真实 key;它读取设备端 instrumented test 导出的公开安全 JSON,只检查矩阵完整性。每条结果必须绑定候选摘要、设备、profile、场景、操作、预期和实际状态,缺少关键路径或同一 profile 配置漂移时直接失败。

示例要求每个候选覆盖生成、加密解密、签名验签、认证拒绝、进程恢复和受控恢复六类场景。真实项目可以增加升级、锁屏变化或厂商设备,但不能删除与业务实际使用相关的路径。脚本仅给出结构门禁,不证明 Android API 调用正确,也不替代人工分析异常。

代码特意拒绝在结果中出现 keyMaterial、plaintext、token 等敏感字段。业务排障只需要摘要、profile 和错误类型;若必须保存更详细日志,应按数据分类、最小访问和保留策略处理。测试通过也只适用于记录中的候选、设备与场景,不能外推为御盾产品或所有 Android 设备结论。

校验 Android Keystore 回归矩阵覆盖与配置一致性
import json
import re
import sys
from pathlib import Path

REQUIRED_SCENARIOS = {
    'generate',
    'encrypt_decrypt',
    'sign_verify',
    'auth_rejected',
    'process_restore',
    'controlled_recovery',
}
SENSITIVE_FIELDS = {'keyMaterial', 'plaintext', 'token', 'privateKey'}

if len(sys.argv) != 2:
    raise SystemExit('usage: validate_keystore_matrix.py results.json')
input_path = Path(sys.argv[1])
if not input_path.is_file():
    raise SystemExit('results file is missing')
data = json.loads(input_path.read_text(encoding='utf-8'))
results = data.get('results')
if not isinstance(results, list) or not results:
    raise SystemExit('results array is missing')
profiles = {}
coverage = {}
for index, item in enumerate(results):
    if not isinstance(item, dict):
        raise SystemExit(f'result {index} is not an object')
    leaked = SENSITIVE_FIELDS.intersection(item)
    if leaked:
        raise SystemExit(f'result {index} contains sensitive fields: {sorted(leaked)}')
    required = {'candidateSha256', 'device', 'profile', 'scenario', 'operation', 'expected', 'actual'}
    if not required.issubset(item):
        raise SystemExit(f'result {index} is missing required fields')
    candidate = str(item['candidateSha256']).lower()
    if not re.fullmatch(r'[0-9a-f]{64}', candidate):
        raise SystemExit(f'result {index} has an invalid candidate digest')
    scenario = str(item['scenario'])
    if scenario not in REQUIRED_SCENARIOS:
        raise SystemExit(f'result {index} has an unknown scenario')
    profile = item['profile']
    if not isinstance(profile, dict):
        raise SystemExit(f'result {index} profile is not an object')
    profile_fields = {'id', 'alias', 'algorithm', 'purposes', 'authRequired'}
    if not profile_fields.issubset(profile):
        raise SystemExit(f'result {index} has an incomplete profile')
    profile_id = str(profile['id'])
    stable_profile = json.dumps(profile, ensure_ascii=False, sort_keys=True, separators=(',', ':'))
    if profile_id in profiles and profiles[profile_id] != stable_profile:
        raise SystemExit(f'profile {profile_id} changes between results')
    profiles[profile_id] = stable_profile
    coverage.setdefault(candidate, set()).add(scenario)
    if item['actual'] != item['expected']:
        error_type = str(item.get('errorType', '')).strip()
        if not error_type:
            raise SystemExit(f'result {index} failed without an error type')
for candidate, scenarios in coverage.items():
    missing = sorted(REQUIRED_SCENARIOS - scenarios)
    if missing:
        raise SystemExit(f'candidate {candidate} lacks scenarios: {missing}')
print(json.dumps({'candidates': len(coverage), 'profiles': len(profiles), 'status': 'pass'}))

把允许、拒绝和恢复回执绑定到发布判断

发布门禁需要同时看到正向、拒绝和恢复证据。正向证明生成、解密和签名契约可执行;拒绝证明错误 key、错误输入和认证缺失不会被静默放行;恢复证明进程重建、升级或受控 key 失效后,业务给出明确状态而不是崩溃、空数据或无限重试。

CTS 验证设备实现与 Android 兼容性定义的一致性,但设备通过 CTS 不等于第三方加固 App 的业务路径通过。OWASP MASVS 将抗篡改与抗逆向放在纵深防御中,也不提供某个候选的保护强度结论。NIST SSDF 支持保留来源、构建、验证与变更证据,具体通过线仍由项目定义。

需要继续定位 Keystore 失败时,可先阅读本站的加固后闪退分层排查指南,确定最早异常与候选身份,再携带脱敏矩阵、错误类型和设备回执申请御盾技术评估。所有申请、登录和控制台动作仍由御盾中央平台承接;文章不声称尚未取得的兼容结果或产品能力。

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
Android Keystore 可以限制 key 的允许用途,并可能在设备支持时使用硬件保护。Android Keystore 说明密钥存储、用途限制与硬件支持边界。文档不证明某台设备一定硬件保护,也不保护解密后进入业务内存的明文。
依赖 Android 运行时、组件和系统 API 的语义应在设备端执行测试。Android instrumented tests 说明 instrumented test 在 Android 设备或模拟器环境运行。单一设备通过不能代表完整系统、ABI、厂商和业务账号矩阵。
CTS 用于验证设备实现与 Android 兼容性定义的一致性。Android CTS overview 说明 CTS 的兼容性测试定位。CTS 通过不代表第三方 App 加固候选的 Keystore 业务回归通过。
完整性响应包含请求详情与多类平台信号,服务端应按场景组合使用。Play Integrity verdicts 说明 verdict 字段和服务端解释方式。单一 verdict 不是绝对信任、Root 判定、Keystore 证明或 VMP 配置证明。
抗篡改与抗逆向控制属于移动安全的纵深防御。OWASP MASVS-RESILIENCE 描述移动应用韧性控制域。控制目录不证明某个候选已经达到防护强度,也不替代服务端授权。
安全发布应保留来源、构建、验证和变更证据。NIST SP 800-218 SSDF 提供组织级安全软件开发与供应链风险实践。SSDF 不定义御盾或其他加固产品功能,也不给出本项目的通过结论。
Keystore 回归应把 alias、profile、候选、设备、预期与实际状态共同记录。工程判断:缺少任一标识会使配置错配、环境差异和候选差异无法归因。记录完整只证明证据可追溯,不证明 key 强度、实现安全或业务已经通过。
捕获任意异常后删除并重建 key 不是通用恢复方案。工程判断:重建可能永久切断旧密文或服务端身份绑定,恢复必须依据错误类型与数据可再生性。实际处置取决于 key 用途、数据恢复能力、产品流程和项目批准,文章不提供统一删除策略。

工程常见问题

加固后还能查到 alias,是否说明 Keystore 没有兼容问题?

不能。还要按实际 profile 执行生成、加密解密、签名验签、认证拒绝和恢复路径,并核对业务错误映射;key 存在只是一个前置状态。

为什么原始包和加固包生成的密文不应该直接比较相等?

随机化加密通常会产生不同密文。回归应比较各自解密后的公开样本断言、profile 和拒绝路径,不应为了相等而固定或复用 nonce、IV。

遇到 Keystore 异常后直接删除 key 再创建可以吗?

不能作为通用处理。先区分认证缺失、配置错误、alias 丢失和 key 不可用;若旧 key 保护不可再生数据,删除会造成永久数据损失。

设备通过 CTS 是否可以省略 App 的 Keystore 回归?

不可以。CTS 验证设备兼容性实现,不验证第三方 App 的加固候选、密钥 profile、业务数据和错误恢复路径。

Play Integrity verdict 能否证明本地 Keystore key 安全?

不能。verdict 是服务端风险决策的一类平台信号,不是某个 key 的硬件保护证明,也不能替代 Keystore 操作和业务授权验收。

申请御盾协助定位 Keystore 回归需要准备什么?

准备候选摘要、签名身份、脱敏 key profile、设备和系统、场景矩阵、最早异常、错误类型及恢复结果,再通过御盾中央平台提交。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: App 加固 PoC 如何形成发布结论