4. Keystore 密码和密钥密码管理

密码管理这件事,说大不大,说小不小。我见过不少项目,签名配置能写进版本控制里,密码明文躺在 git 历史中——嗯,我是说“见过”,而不是“做过”。密码泄露的风险往往不是攻防技术的问题,而是管理习惯的问题。

Keystore 里其实有两层密码:store 密码(Keystore 密码)和key 密码(密钥密码)。它们各自管什么,为什么需要分开,怎么安全地管理它们,下面挨个聊。

4.1 两层密码:Store 和 Key

很多新手以为 Keystore 只有一个密码。实际上,打开 .jks 文件用 store 密码,读取里面的某个密钥条目还需要 key 密码。你可以把它们想象成:

  • Store 密码——仓库大门的锁
  • Key 密码——保险柜里每个箱子的小锁

当用 keytool -list 查看 Keystore 内容时,只需要 store 密码。但如果你想导出证书或签名,那就要提供 key 密码。我个人习惯把这两个密码设置为不同的值,这样即使某个 key 密码意外泄露,攻击者也无法直接把整个 Keystore 掏走。

下面这张图能帮你快速理解两层密码的关系:

Keystore 密码与密钥密码的关系 Keystore 容器 (.jks / .p12) ⚠ 需要 Store 密码才能访问 密钥条目 1 别名: release 密钥条目 2 别名: debug 🔒 🔑 Key 密码 (每个条目独立) Store 密码 一个 Keystore 可以有多个密钥条目,每个条目都有自己的密码 两个密码可以相同,但为了安全建议分开设置

4.2 创建时就要区分好

用 keytool -genkeypair 时,默认会让 store 密码和 key 密码相同。命令行里有个 -keypass 参数可以单独指定。我曾经在一家创业公司接手项目,发现上一个同事把大量 debug 签名和 release 签名混在同一个 Keystore 里,所有密钥密码都是一样的。结果有一次误操作覆盖了 debug 密钥,导致整个团队无法安装测试包……

建议做法:
- 生产签名使用单独的 Keystore,不要和调试签名混在一起
- store 密码和 key 密码尽量不同
- 每个密钥条目的 key 密码也可以不同(尤其是多个应用共用一个 Keystore 时)

4.3 常见错误:密码写死在构建脚本里

很多 CI/CD 配置文件会把密码直接写进 signingConfigs,比如:

android {
    signingConfigs {
        release {
            storeFile file("release.jks")
            storePassword "123456"
            keyAlias "mykey"
            keyPassword "123456"
        }
    }
}

为什么我不建议这么干?
因为在 Git 仓库历史里,密码会被永久保留。即使你后来删掉并修改了密码,旧版本中的密码依然可以被任何人看到。说实话,这不是技术问题,是流程问题。

我个人的改进方案:把密码放到 local.properties 或者环境变量中读取:

android {
    signingConfigs {
        release {
            storeFile file("release.jks")
            storePassword System.getenv("STORE_PASSWORD") ?: project.findProperty("storePassword")
            keyAlias System.getenv("KEY_ALIAS") ?: "release"
            keyPassword System.getenv("KEY_PASSWORD") ?: project.findProperty("keyPassword")
        }
    }
}

这样密码就不会提交到版本控制系统,只有本地环境变量或者 gradle.properties(记得把它加到 .gitignore)里才会有。

4.4 密码丢失了怎么办?

这是个让人头疼的问题。Keystore 和密码是一对一绑定的,密码丢失后,没有任何后门可以找回。你想想看,如果 Google 能帮你找回,那黑客也能利用同样的漏洞。

警告: 密码丢失 = 密钥永久丢失。
应用商店里已经发布的旧版本将无法使用该签名更新。唯一办法是更换签名包名(或申请签名变更),但这会失去用户数据继承、推送 等。

我记得团队里有个同事,把 release 签名放在 U 盘里,结果 U 盘摔坏了。密码虽然还记得,但 Keystore 文件本身也损坏了——没有备份,只能重新申请签名。从那以后,我们制定了严格的密码和文件备份策略。

4.5 密码管理方案对比

我整理了几种常见的密码管理方式,供你参考:

方案 安全性 可维护性 适用场景
环境变量 中 高 CI/CD、本地开发
密码管理器(1Password/LastPass) 高 中 团队共享、生产环境
加密配置文件(如 encrypted-properties) 高 低(需额外工具解密) 需要脚本自动化部署的场景
硬件安全模块(HSM) 极高 低 大型企业、金融 App

4.6 密码复杂度要求

Android 官方其实没有强制规定 Keystore 密码的复杂度,但作为安全最佳实践,我建议你遵循下面的规则:

  • 长度至少 12 位——最好包含大小写字母、数字和特殊字符
  • 不要使用常见的词汇或日期——比如公司名、项目名、生日
  • store 密码和 key 密码不要相同——前面解释过原因
  • 定期更换密码——但记住:更换密码不等于更换密钥
关于更换密码:
你可以用 keytool -storepasswd 修改 store 密码,用 keytool -keypasswd 修改 key 密码。
这不会改变私钥本身,已经发布的 APK 签名依然有效。但如果密钥丢失了,改密码也没有用。

4.7 避坑指南:记住这几个教训

  • 我曾经把生产签名密码记在 Notion 里,结果 Notion 账号被恶意登录,虽然没造成实际损失,但想想都后怕。现在我用离线密码管理器 + 物理备份。
  • 还有一个团队把 Keystore 文件放在共享文件夹里,密码写在代码注释中。嗯,他们的应用后来被抄袭者轻松重新签名并上传到第三方商店……
  • 最离谱的一次——有人把 store 密码和 key 密码设成了同一个,还写在了 build.gradle 里提交到了公共仓库。我接手时发现密码是 "keystore"……

说句实在话,密码管理不是技术难题,而是习惯问题。你只需要在项目初始化时多花 5 分钟把流程建立好,就能避免后面很多麻烦。

最后送你一个实用的小技巧:
在创建 Keystore 时,顺手生成一个包含密码和别名的凭据文件,加密后存储在本地的 ~/.android/creds.enc 中。这样即使你忘了密码,也可以解密查看。
但千万!不要把解密密钥放在同一台机器上。