安卓代码加固
安卓代码加固(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_KEY | null / "" / 空白 | 否 |
INVALID_KEY | 不是 64 hex,或解不开 / 不是 dex | 否 |
LOAD_FAILED | payload 缺失、native 失败、ABI 不匹配 | 否 |
SUCCESS | 本次解密并加载 | 是 |
ALREADY_INITIALIZED | 进程里已经成功过 | 不再重解 |
已经成功后再
init,不会用新密钥重新解密。更换密钥只能重启进程。