ArturoYi
安卓代码加固

安卓代码加固(6):密钥外置

SDK / AAR 不带 AES 密钥。CI 用环境变量,本地 debug 用 gitignore 文件。

用于解开 payload 的 32 字节密钥 不在 hidden-sdk 源码、AAR 和 so 里。谁打包,谁持有密钥;谁接入,谁在运行时传入。拿到 AAR 并不等于能够加载 dex。

Demo 为了能直接安装,把同一串密钥写进 BuildConfig.HIDDEN_PAYLOAD_KEY。正式接入不要沿用这一做法,应改成接入方自己的安全存储或下发通道。

密钥格式

必须是 正好 64 个 hex 字符(0-9a-fA-F),解码为 32 随机字节,作为 AES-256-GCM 密钥。不能把口令的 UTF-8 字节直接当作 key。

hidden-sdk/src/main/java/com/example/minidex/sdk/internal/HiddenCrypto.kt
const val KEY_HEX_LENGTH = 64

fun isUsableKey(hex: String): Boolean {
    if (hex.length != KEY_HEX_LENGTH) return false
    return hex.all { ch ->
        ch in '0'..'9' || ch in 'a'..'f' || ch in 'A'..'F'
    }
}

Java 只检查格式。把 hex 转为 32 字节发生在 so 的 parse_key_hex。nonce 写在密文头里,不随密钥一起分发。

解析打包密钥

tools/payload_key.py 的优先级:

环境变量

HIDDEN_PAYLOAD_KEY 有值就用它。CI / release 必须走这条路径。

本地文件

tools/.payload_key 存在则读取。该文件已在 .gitignore。

允许时生成

仅 debug 且带 --allow-generate 时,用 secrets.token_hex(32) 写文件。release 任务不带这个开关。

tools/payload_key.py
ENV_NAME = "HIDDEN_PAYLOAD_KEY"
KEY_FILE = Path(__file__).resolve().parent / ".payload_key"

def resolve(*, require_env: bool, allow_generate: bool) -> str:
    env = os.environ.get(ENV_NAME, "").strip()
    if env:
        return validate(env)
    if require_env:
        raise SystemExit(f"Release / CI 打包必须设置环境变量 {ENV_NAME}")
    if KEY_FILE.exists():
        return validate(KEY_FILE.read_text())
    if not allow_generate:
        raise SystemExit("没有密钥,也没有本地文件")
    key = secrets.token_hex(32)
    KEY_FILE.write_text(key + "\n")
    return key

encryptHiddenPayloadRelease 不传 --allow-generate。没有环境变量就无法完成编译。

Demo 如何注入密钥

:app 在配置期跑同一套脚本,把结果写进 BuildConfig:

app/build.gradle.kts
buildConfigField("String", "HIDDEN_PAYLOAD_KEY", "\"${resolveHiddenPayloadKey(allowGenerate = true)}\"")
app/src/main/java/com/example/minidex/DemoPayloadKeys.kt
object DemoPayloadKeys {
    val PAYLOAD_KEY: String = BuildConfig.HIDDEN_PAYLOAD_KEY
}
app/src/main/java/com/example/minidex/DemoApplication.kt
HiddenSdk.init(this, DemoPayloadKeys.PAYLOAD_KEY)

Application.onCreate 必须尽早调用 init。ConnectionService 可能在首页尚未打开时就被系统拉起。密钥为空时不读取 assets,也不创建 ClassLoader。

对照失败码

code含义会加载 dex 吗
EMPTY_KEYnull / "" / 空白否
INVALID_KEY不是 64 hex,或解不开 / 不是 dex否
LOAD_FAILEDpayload 缺失、native 失败、ABI 不匹配否
SUCCESS本次解密并加载是
ALREADY_INITIALIZED进程里已经成功过不再重解
已经成功后再 init,不会用新密钥重新解密。更换密钥只能重启进程。

下一步

  • 加密DEX:打包时密钥如何进入 PayloadEncryptor
  • 构建校验:AAR 自检不会、也不该读到密钥
Copyright © 2026