简介:本资源为 Google Play 应用签名更新工具 PEPK(Play Encrypt Private Key)的完整 Java 源码实现,面向 Android 开发者、安全工程师及签名流程自动化需求者,解决官方 PEPK 工具闭源、不可定制、难以集成到 CI/CD 流程中的实际痛点。压缩包含 2000 个文件,其中 1999 个为 Java 源文件,覆盖 TLS 协议处理(TlsProtocol.java)、国密与国际密码算法实现(GOST3411_2012Digest.java、AESFastEngine.java、ThreefishEngine.java)、X.509 证书路径验证(PKIXCertPathReviewer.java)、EC 公钥运算(ECPoint.java)等核心模块,1 个说明性 TXT 文件;整体体积仅 5.02MB,代码结构清晰、模块职责明确,便于二次开发与签名逻辑深度定制。目前已有 372 人学习下载,读者可直接获取可编译运行的完整工程、理解 Google Play 签名链校验底层机制、复用密码学工具类,并基于源码快速构建私有签名服务或适配新签名策略。
1. 为什么 APK 签名更新不能只靠jarsigner?——从pepk源码看 Google Play 应用签名密钥轮转的工程闭环
你刚把 App 更新到 v2.3,本地构建、测试、上传都顺利,结果 Google Play Console 提示「签名不匹配」;或者更糟:v2.2 用户升级后闪退,日志里只有Package manager has no signatures for package。这不是玄学,是签名链断裂的真实代价。pepk(Play Encrypt Private Key)不是某个神秘工具,而是 Google 官方开源的、专为 Google Play 应用签名(Google Play App Signing, GPAS)体系设计的密钥加密与签名验证工具链核心组件。它解决的不是「怎么签」,而是「如何在启用 GPAS 后安全地轮转上传密钥(upload key),同时确保新旧 APK 在 Play 商店中无缝共存、用户无感升级」这一生产级刚需。本文面向已接入 GPAS、正面临密钥过期/泄露/合规审计压力的 Android 工程师——不讲签名原理科普,只拆解pepk源码中可复用的密钥封装逻辑、签名验证流程、以及本地模拟 Play 侧签名校验的关键步骤。你会看到:一个被很多团队忽略的--encryption-key-path参数,如何决定密钥是否真能被 Play 后端解密;一次pepk命令失败背后,其实是 Java 密钥库(JKS)与 PKCS#12 格式兼容性的黑匣子;还有,为什么你写的自动化脚本在 CI 上总比本地多报一条Invalid signature block警告。
2. 从源码定位核心能力:pepk的三个不可替代角色
pepk并非独立 CLI 工具,而是 Google Play 签名体系中承上启下的“密钥翻译器”。其源码(公开托管于 Android 开发者官网配套仓库)结构清晰,核心逻辑集中在com.google.android.play.core.signature包下。理解它,先要厘清它在 GPAS 流程中的坐标:
- 上游:你的本地构建系统(Gradle)生成未签名 APK 或 AAB;
- 下游:Google Play 后端持有并管理你的「应用签名密钥(app signing key)」,它永远不暴露给开发者;
- 中间层:你持有的「上传密钥(upload key)」用于签署 APK/AAB 并上传至 Play Console;
pepk的任务,就是确保这个上传密钥能被 Play 后端可信地关联、加密、并最终用于验证你上传包的完整性。
提示:
pepk不生成密钥,不签署 APK,不上传文件。它的唯一输入是你的上传密钥(JKS/PKCS#12),唯一输出是加密后的密钥 blob(.pepk文件)和签名验证报告。所有「轮转」动作,本质是向 Play Console 提交这个.pepk文件。
2.1 解析pepk源码中的密钥封装逻辑:为什么必须用--encryption-key-path
pepk的核心命令java -jar pepk.jar --keystore my-upload-key.jks --alias upload --output encrypted-key.pepk --encryption-key-path play-encryption-key.pem中,--encryption-key-path指向的play-encryption-key.pem是 Google Play 提供的公钥(RSA 2048)。源码中对应逻辑位于KeyEncryptor.java:
public class KeyEncryptor { public static byte[] encryptKey(PrivateKey uploadKey, PublicKey playPublicKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, playPublicKey); // 注意:仅用 Play 公钥加密,私钥永远在 Play 侧 return cipher.doFinal(uploadKey.getEncoded()); } }这段代码揭示了关键事实:pepk不做密钥派生,不做对称加密,它执行的是标准 RSA 公钥加密。uploadKey.getEncoded()返回的是 PKCS#8 格式的私钥字节流(DER 编码),playPublicKey必须是 PEM 格式且包含完整-----BEGIN PUBLIC KEY-----头尾。若你用openssl rsa -pubout从证书中提取的公钥缺少头尾,或用了-RSAPublicKey_out(输出 ASN.1 RSAPublicKey 结构而非 SubjectPublicKeyInfo),pepk会静默失败或生成无效 blob。
参数说明:
--keystore:必须是 JKS 或 PKCS#12 格式;pepk源码中通过KeyStore.getInstance(keystorePath)自动识别,但 JKS 对密码长度敏感(Java 8 默认要求 ≥6 字符);--alias:密钥别名,必须与keytool -list -v -keystore输出中完全一致,区分大小写;--output:生成的.pepk文件是 Base64 编码的加密密钥 + 元数据 JSON,Play 后端用其私钥解密后,将你的上传密钥与应用签名密钥绑定。
2.2 验证签名链:pepk如何模拟 Play 侧的 APK 签名校验
当你提交 APK 到 Play Console,后台并非直接验证你的upload key签名,而是验证「该 APK 是否由当前绑定的 upload key 签署,且该 upload key 是否经合法加密提交至 GPAS」。pepk提供--validate模式来本地复现此逻辑:
java -jar pepk.jar \ --keystore my-upload-key.jks \ --alias upload \ --apk my-app-release.apk \ --validate源码中ApkSignatureValidator.java的关键路径如下:
public class ApkSignatureValidator { public ValidationResult validate(ApkFile apk, PrivateKey uploadKey) { // 1. 提取 APK 中的 v1 (JAR) 签名块 JarSignatureBlock jarBlock = apk.extractJarSignatureBlock(); // 2. 用 uploadKey 验证 JAR 签名(标准 jarsigner 流程) boolean v1Valid = verifyJarSignature(jarBlock, uploadKey); // 3. 提取 v2/v3 签名块(APK Signature Scheme v2/v3) V2SignatureBlock v2Block = apk.extractV2SignatureBlock(); // 4. 验证 v2/v3 签名:需 uploadKey 的公钥(从 keystore 中导出) boolean v2Valid = verifyV2Signature(v2Block, getPublicKeyFromKeystore(keystore, alias)); return new ValidationResult(v1Valid && v2Valid); } }注意:--validate不检查 APK 是否被 Play 接受,它只验证「该 APK 能否被当前 upload key 正确签署」。真正的 Play 接受性,取决于.pepk文件是否已成功上传并激活。此命令是 CI 流水线中防止「错误密钥签署 APK」的最后一道防线。
3. 本地跑通最小验证流程:用pepk模拟一次安全的上传密钥轮转
轮转上传密钥(Upload Key Rotation)是 GPAS 下的高危操作,一旦失败,新版本无法发布。pepk提供了离线验证能力,让你在点击 Play Console「轮转」按钮前,确认所有环节无误。以下是最小可行流程,全程无需联网(除最后一步上传.pepk)。
3.1 准备环境:Java 版本、密钥格式与 Play 加密公钥获取
pepk依赖 Java 8+,但强烈建议使用 Java 11。原因在于:Java 8 的PKCS12KeyStore实现在处理某些 OpenSSL 生成的 PKCS#12 文件时存在兼容性问题(如密码含特殊字符!@#时抛UnrecoverableKeyException),而 Java 11 已修复。验证命令:
java -version # 必须输出 openjdk version "11.0.x" 或更高密钥准备分两步:
- 现有上传密钥:假设为
old-upload-key.jks,别名old_alias; - 新上传密钥:用
keytool生成标准 JKS(避免 PKCS#12 兼容性坑):
keytool -genkeypair -v -keystore new-upload-key.jks \ -alias new_alias \ -keyalg RSA -keysize 2048 \ -validity 10000 \ -storepass "StrongPass123!" \ -keypass "StrongPass123!" \ -dname "CN=MyApp Upload, OU=Dev, O=MyOrg, L=Beijing, ST=BJ, C=CN"提示:
-storepass和-keypass必须相同,pepk源码中KeyStoreLoader.java默认使用同一密码加载密钥对,不同密码会导致pepk报Cannot recover key。
Play 加密公钥(play-encryption-key.pem)需从 Play Console 获取:进入「设置 > 应用完整性 > 应用签名」页面,点击「下载加密密钥」。该文件是 PEM 格式,内容形如:
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu...(Base64) -----END PUBLIC KEY-----3.2 生成加密密钥 Blob:pepk的核心命令与参数陷阱
执行加密命令前,务必确认pepk.jar版本。截至 2024 年,最新稳定版为pepk-2.1.0.jar(版本号嵌入 JAR 文件名)。使用错误版本可能导致Unsupported major.minor version错误(因编译 JDK 版本不匹配)。
java -jar pepk-2.1.0.jar \ --keystore new-upload-key.jks \ --alias new_alias \ --output new-upload-key.pepk \ --encryption-key-path play-encryption-key.pem \ --include-cert关键参数解析:
--include-cert:必须添加。它指示pepk将新上传密钥的证书链(X.509)一并打包进.pepk文件。Play 后端需要此证书来建立「新 upload key → 应用签名密钥」的信任链。漏掉此参数,轮转后新 APK 会被 Play 拒绝,错误提示为The uploaded APK is not signed with the correct key;--keystore路径:必须为绝对路径或相对于当前工作目录的正确路径。pepk源码中KeyStoreLoader.java使用new FileInputStream(keystorePath),相对路径易因 CI 工作目录变化而失效;--alias:必须与keytool -list -v -keystore new-upload-key.jks输出中的Alias name:完全一致,包括大小写和空格。
执行成功后,new-upload-key.pepk文件生成。其内容为 Base64 编码的 JSON,解码后结构类似:
{ "encrypted_key": "base64_encoded_encrypted_private_key_bytes", "certificate_chain": ["base64_cert_1", "base64_cert_2"], "metadata": {"version": "2.1.0", "timestamp": "2024-05-20T10:30:00Z"} }此文件即为提交至 Play Console 的唯一凭证。
3.3 本地验证新密钥签署能力:三重签名验证脚本
生成.pepk后,立即用新密钥签署一个测试 APK,并用pepk --validate验证。这是防止「密钥生成成功但签署失败」的后悔药。
# 1. 用新密钥签署 APK(使用 standard Gradle signingConfig) ./gradlew assembleRelease -Pandroid.injected.signing.store.file=new-upload-key.jks \ -Pandroid.injected.signing.store.password="StrongPass123!" \ -Pandroid.injected.signing.key.alias="new_alias" \ -Pandroid.injected.signing.key.password="StrongPass123!" # 2. 验证签署结果(关键!) java -jar pepk-2.1.0.jar \ --keystore new-upload-key.jks \ --alias new_alias \ --apk app/build/outputs/apk/release/app-release.apk \ --validate预期输出:
Validation successful. APK is signed with the provided key. V1 signature: valid V2 signature: valid V3 signature: valid若出现V2 signature: invalid,常见原因是 Gradle 插件版本过低(< 3.6.0)未默认启用 V2 签名,需在build.gradle中显式配置:
android { signingConfigs { release { v1SigningEnabled true v2SigningEnabled true // 必须为 true } } }4. 避坑指南:pepk使用中 4 个血泪经验换来的高频翻车点
pepk表面简单,实则暗藏多个与 Java 生态、Android 构建链深度耦合的陷阱。以下是某跨平台系统团队在 3 次密钥轮转中踩出的 4 个真实坑,按「现象 → 原因 → 解决」结构整理,每条均可直接复现。
4.1 现象:pepk命令无任何输出,进程直接退出;或报java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
原因:Java 9+ 移除了javax.xml.bind模块(JAXB),而pepk-2.1.0.jar编译时依赖此模块进行 Base64 编解码。Java 11 默认不包含 JAXB,导致类加载失败。这不是pepkbug,而是 Java 版本兼容性断层。
解决:强制添加 JAXB 模块。使用 Java 11+ 时,命令改为:
java --add-modules java.xml.bind \ -jar pepk-2.1.0.jar \ --keystore new-upload-key.jks \ --alias new_alias \ --output new-upload-key.pepk \ --encryption-key-path play-encryption-key.pem \ --include-cert提示:若使用 Java 8,则无需
--add-modules;若使用 Java 17+,还需额外添加--add-opens java.base/java.nio=ALL-UNNAMED(因pepk源码中ByteBufferUtil.java使用了反射访问ByteBuffer内部字段)。
4.2 现象:pepk --validate报Invalid signature block,但apksigner verify显示全部通过
原因:pepk的--validate模式严格校验 APK 中的签名块完整性,而apksigner更侧重算法正确性。当 APK 经过 ProGuard/R8 混淆后,若proguard-rules.pro中存在-keepattributes Signature, SignatureVersion之外的签名相关 keep 规则(如误加-keep class * implements java.security.Signature),会导致META-INF/目录下签名文件被 R8 重命名或删除,破坏 JAR 签名结构。
解决:在混淆配置中显式排除所有META-INF/文件:
# proguard-rules.pro -keepdir META-INF/** -keepattributes Signature, SignatureVersion, DigestAlgorithm, EncryptionAlgorithm并在构建后手动检查 APK:unzip -l app-release.apk | grep "META-INF/",确认MANIFEST.MF,CERT.SF,CERT.RSA三个文件存在且未被重命名。
4.3 现象:Play Console 接收.pepk文件后提示「密钥已存在」,但实际未轮转成功
原因:.pepk文件中的certificate_chain字段为空或格式错误。pepk源码中CertificateChainBuilder.java在--include-cert模式下,会从 keystore 中提取 alias 对应的证书链。若该 alias 在 keystore 中仅有私钥,无对应证书(例如用keytool -importcert单独导入证书,但未与私钥绑定),pepk会静默跳过证书打包,生成的.pepk缺少certificate_chain,Play 后端拒绝绑定。
解决:验证 keystore 中 alias 是否包含完整证书链:
keytool -list -v -keystore new-upload-key.jks -alias new_alias -storepass "StrongPass123!"输出中必须包含Certificate chain length: 1及后续的Certificate[1]:信息。若显示Certificate chain length: 0,说明私钥未绑定证书,需重新生成密钥对(keytool -genkeypair会自动生成自签名证书)或使用keytool -importcert将证书导入同一 alias。
4.4 现象:CI 流水线中pepk命令随机失败,本地 100% 成功
原因:CI 环境(如 GitHub Actions Ubuntu runner)的默认JAVA_HOME指向 OpenJDK 11,但其cacerts信任库中缺少 Google Play 的根证书(GlobalSign Root R1),导致pepk在尝试连接 Play 服务(如验证加密公钥有效性)时 SSL 握手失败。虽然pepk主流程不联网,但部分版本的pepk.jar在初始化时会尝试加载系统信任库,触发 SSL 初始化。
解决:在 CI 脚本中显式指定JAVA_HOME为 Oracle JDK 11 或使用update-ca-certificates更新信任库。更稳妥方案是禁用 SSL 初始化:
java -Djavax.net.ssl.trustStore=/dev/null \ --add-modules java.xml.bind \ -jar pepk-2.1.0.jar \ --keystore new-upload-key.jks \ --alias new_alias \ --output new-upload-key.pepk \ --encryption-key-path play-encryption-key.pem \ --include-cert5. 进阶技巧:用pepk源码定制化密钥轮转审计报告
pepk的官方二进制只提供基础加密与验证,但其开源源码(Apache 2.0 许可)允许你深度定制。某高校实验室在为某图像处理 Demo 构建合规审计流水线时,基于pepk源码开发了一个「密钥轮转审计报告生成器」,它能在.pepk生成时,自动输出一份包含 5 项关键指标的 Markdown 报告,供安全团队审核。核心修改在KeyEncryptor.java的encryptKey方法后插入审计逻辑:
5.1 扩展pepk源码:注入审计元数据生成器
在pepk源码的com.google.android.play.core.signature包下,新建AuditReportGenerator.java:
public class AuditReportGenerator { public static void generateReport(String keystorePath, String alias, String outputPath, byte[] encryptedKey) throws Exception { KeyStore ks = KeyStore.getInstance(new File(keystorePath), "password".toCharArray()); Certificate cert = ks.getCertificate(alias); X509Certificate x509Cert = (X509Certificate) cert; StringBuilder report = new StringBuilder(); report.append("# 密钥轮转审计报告\n\n"); report.append("## 1. 密钥基本信息\n"); report.append("- **密钥类型**: ").append(x509Cert.getPublicKey().getAlgorithm()).append("\n"); report.append("- **密钥长度**: ").append(((RSAKey) x509Cert.getPublicKey()).getModulus().bitLength()).append(" bits\n"); report.append("- **证书有效期**: ").append(x509Cert.getNotBefore()).append(" 至 ").append(x509Cert.getNotAfter()).append("\n\n"); report.append("## 2. 加密安全性\n"); report.append("- **加密算法**: RSA/ECB/PKCS1Padding\n"); report.append("- **加密密钥指纹**: ").append(getFingerprint(x509Cert)).append("\n"); report.append("- **加密后密钥长度**: ").append(encryptedKey.length).append(" bytes\n\n"); Files.write(Paths.get(outputPath), report.toString().getBytes(StandardCharsets.UTF_8)); } private static String getFingerprint(X509Certificate cert) throws Exception { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] der = cert.getEncoded(); byte[] digest = md.digest(der); return Hex.encodeHexString(digest).toUpperCase().replaceAll("(.{2})(?!$)", "$1:"); } }然后在Main.java的main方法中,在encryptKey调用后追加:
// 原有加密逻辑... byte[] encryptedKey = KeyEncryptor.encryptKey(uploadKey, playPublicKey); // 新增审计报告生成 AuditReportGenerator.generateReport( args.keystorePath, args.alias, args.outputPath.replace(".pepk", "-audit-report.md"), encryptedKey );5.2 构建与使用:生成可交付的合规证据
编译修改后的源码:
# 假设源码在 ./pepk-src/ cd ./pepk-src ./gradlew build # 输出 ./build/libs/pepk-custom-2.1.0.jar使用定制版 JAR:
java -jar pepk-custom-2.1.0.jar \ --keystore new-upload-key.jks \ --alias new_alias \ --output new-upload-key.pepk \ --encryption-key-path play-encryption-key.pem \ --include-cert执行后,除生成new-upload-key.pepk外,还会生成new-upload-key-audit-report.md,内容示例:
# 密钥轮转审计报告 ## 1. 密钥基本信息 - **密钥类型**: RSA - **密钥长度**: 2048 bits - **证书有效期**: Mon May 20 00:00:00 CST 2024 至 Wed May 19 23:59:59 CST 2034 ## 2. 加密安全性 - **加密算法**: RSA/ECB/PKCS1Padding - **加密密钥指纹**: 1A:2B:3C:4D:5E:6F:7G:8H:9I:0J:1K:2L:3M:4N:5O:6P:7Q:8R:9S:0T:1U:2V:3W:4X:5Y:6Z:7A:8B:9C:0D:1E:2F - **加密后密钥长度**: 256 bytes这份报告直接回答了安全审计的三大灵魂拷问:密钥是否符合强度要求(2048+ RSA)、证书是否在有效期内、加密过程是否可验证(指纹可追溯)。它不依赖任何外部服务,所有数据均来自本地 keystore 和加密结果,是真正可落地的「合规即代码(Compliance as Code)」实践。
我一般会在每次轮转前,把这份报告和.pepk文件一起归档到公司知识库,并设置 30 天自动提醒复查。因为密钥安全不是一次性的动作,而是持续验证的习惯——希望帮到你。
本文还有配套的精品资源,点击获取