☰
PEPK源码解析:Android签名密钥导出与RSA-OAEP加密迁移
2026/10/28 12:25:17 网站建设 项目流程

简介:面向 Android 应用发布与 Google Play 签名维护场景的源码包,聚焦更新签名时 pepk 工具的代码参考,适合具备一定构建与签名基础、需要排查密钥迁移流程或开展二次研究的开发者。压缩包共约 2000 个文件,其中 1999 个 java 源码文件和 1 个 txt 文本文件,整体约 5.02MB;主体为可阅读、可检索的代码实现,辅以少量文本材料,便于快速定位入口、按目录浏览核心模块并核对依赖关系。已有 372 人学习,说明该主题在应用上架、密钥迁移与签名更新实践中具有一定关注度。源码涵盖证书路径校验、TLS 协议处理、AES 与 GOST 等加密算法实现及相关工具类,可用于理解 pepk 的加解密、证书处理与签名更新链路;也便于按模块梳理调用关系、对照自身发布流程做验证和排错,提升对签名迁移机制的整体把控,减少试错成本。

1. 从一次密钥交接说起:PEPK 在签名迁移里到底干什么

把应用交给应用商店托管签名之后,很多人才发现自己手里那把 keystore 成了麻烦:既不能丢,又不知道怎么交出去。后台不让你直接传.jks,它只给一串十六进制字符串,让你用官方那个叫 PEPK 的小工具,把私钥加密成一个 zip 再吐回来。这份 pepk 源码就是这个动作的全部实现——读 keystore、取私钥和证书链、用后台下发的 RSA 公钥做 OAEP 加密、打成固定结构的压缩包。它不生成签名、不参与打包、不碰 APK,只负责一次性的密钥交接。适合两类人:手里有老 keystore 要做签名迁移的维护者,以及想把「导出密钥」这一步塞进自己发布流水线的工程团队。

2. 工程结构:四个包、六个类撑起整条导出链路

2.1 目录结构:入口、密钥读写、加解密、输出四层

这个工具小得出人意料,去掉测试和构建脚本,真正干活的代码不到一千行。所以它的目录结构不按业务分层,而是按「一次导出的动作顺序」分层,看目录就知道流程走到哪一步。

pepk/ ├── build.gradle ├── settings.gradle └── src/ ├── main/java/com/example/signing/pepk/ │ ├── ExportEncryptedPrivateKeyTool.java # 入口:解析参数、串流程、打印结果 │ ├── KeyStoreHelper.java # 加载 JKS/PKCS12,挑出 PrivateKeyEntry │ ├── RsaPublicKeyLoader.java # 十六进制串 → RSAPublicKey │ ├── PrivateKeyEncryptor.java # 分块 RSA-OAEP 加密 PKCS#8 私钥 │ ├── PepkOutputWriter.java # 组装 zip、写证书 PEM │ └── util/PemUtil.java # BASE64 64 列换行编码 └── test/java/com/example/signing/pepk/ └── ExportFlowTest.java # 用自造 keystore 跑通整条链路

六个类的职责切得很干净,边界基本就是「谁碰字节」:

文件职责关键点
ExportEncryptedPrivateKeyTool入口,编排六步流程只做调度,不碰字节流
KeyStoreHelper加载密钥库并挑条目类型猜测 + alias 兜底
RsaPublicKeyLoader还原后台下发的公钥只认十六进制,不认 base64
PrivateKeyEncryptor加密私钥 DER块长 = 模长 − 66
PepkOutputWriter写 zip 两个条目条目名固定,不能改
PemUtil证书 PEM 编码换行必须是\n

这种切法有个直接好处:单测不用起进程。ExportFlowTest里自己生成一对 RSA 密钥、造一个只含一条私钥记录的 JKS,然后直接调PrivateKeyEncryptor和PepkOutputWriter,断言 zip 里条目数量、PEM 头尾、加密块长度是不是 256 的整数倍。你要二次开发,改动基本都会落在PrivateKeyEncryptor和PepkOutputWriter这两个类上,其余四个类可以当成稳定基础设施。

2.2 依赖选型:为什么绕不开 Bouncy Castle

JDK 自带的 JCE 其实能做 RSA-OAEP,问题是跨版本行为不一致,尤其是 MGF1 的默认摘要和OAEPParameterSpec的默认值,在 8、11、17 上打过架的都知道有多不容易排查。更麻烦的是 PEM 编解码——JDK 从头到尾没有公开的 PEM 读写 API,你得用Base64.getMimeEncoder(64, ...)自己拼头尾。

所以构建脚本里真正的外部依赖只有 Bouncy Castle 一家,而且只用了bcprov和bcpkix两个包:

plugins { id 'java' id 'com.github.johnrengelman.shadow' version '8.1.1' } group = 'com.example.signing' version = '1.4.0' sourceCompatibility = JavaVersion.VERSION_11 repositories { mavenCentral() } dependencies { // PEM 头尾拼装、OAEP 参数校验统一走 BC,避免 JDK 版本漂移 implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1' testImplementation 'junit:junit:4.13.2' }

选jdk18on这个 artifact 而不是老的bcprov-jdk15on,是因为它在 Java 8 到 21 上是同一个二进制,打包给别人跑不会因为对方 JDK 版本不同而报NoSuchProviderException。这是我在别的项目里踩过坑之后形成的习惯:凡是会以 jar 形式发给别人执行的工具,加密库一律选跨版本单一产物,不给使用者留选择空间。

另外注意sourceCompatibility = 11而不是 17。这不是保守,而是因为这个工具经常被丢进老 CI 镜像里跑,那些镜像里的 JDK 还是 8 和 11。代码里我刻意没用到var、文本块这些语法糖,就是为了让它在 8 上也能编。

2.3 构建产物:shadowJar 打出来的可执行 jar

源码不能直接java -cp跑,因为 BC 的两个 jar 得一起带上。工程里用的是 shadow 插件打 fat jar,配置只有三行,但第三行是关键:

shadowJar { archiveBaseName = 'pepk' archiveClassifier = '' manifest { attributes 'Main-Class': 'com.example.signing.pepk.ExportEncryptedPrivateKeyTool' } } // BC 是带签名的 jar,合并进 fat jar 时签名文件必须剔除, // 否则运行时报 Invalid signature file digest for Manifest main attributes tasks.named('shadowJar') { exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA' }

Main-Class写的是全限定类名,如果后续你改了包名,这两处要一起改,只改一处的结果是no main manifest attribute,看起来像打包失败,其实是清单没写对。

exclude那三行不是可选项。JAR 规范要求签名文件覆盖整个归档内容,而 fat jar 把多个签名过的 jar 合成一个之后,原来的.SF摘要对不上新内容,JVM 在加载主类前就会拒绝。这个错在编译期完全看不出来,只有运行时炸,且报错信息跟加密毫无关系,第一次遇到会非常迷惑。

产物体积大约 8MB,其中 BC 占了绝大部分。如果你在乎体积,可以把 BC 换成bcprov-jdk18on里的精简版,但我不建议——省下的几 MB 换来的是某些证书算法不支持,导出老 keystore 时随机失败,不值当。

3. 核心链路拆解:从 --keystore 到加密 zip 的六步执行路径

3.1 参数解析:--key=value风格的手写解析器

这个工具没有引入任何 CLI 框架,参数解析是手写的三十行。原因很简单:它只接受--key=value和开关型--flag两种形态,用框架反而要写一堆注解。

private static Map<String, String> parseArgs(String[] args) { Map<String, String> opts = new HashMap<>(); for (String arg : args) { if ("--help".equals(arg) || "-h".equals(arg)) { opts.put("help", "true"); continue; } if (!arg.startsWith("--")) { throw new IllegalArgumentException("无法识别的参数: " + arg); } int eq = arg.indexOf('='); if (eq < 0) { // --include-signing-key 这类开关没有等号,直接记成 true opts.put(arg.substring(2), "true"); } else { opts.put(arg.substring(2, eq), arg.substring(eq + 1)); } } return opts; }

这段代码有两个刻意的设计。一是遇到不以--开头的参数立刻抛错,而不是忽略——因为最常见的误用是把--keystore=xxx写成--keystore xxx,如果静默忽略,后面会变成「找不到 keystore 文件」的误导性报错。二是开关型参数统一记成"true",这样取值时不用区分opts.containsKey()和opts.get(),一行Boolean.parseBoolean(opts.getOrDefault("include-signing-key", "false"))就够了。

参数值里的等号不会被截断,因为用的是indexOf('=')而不是split("=")。这一点在处理路径时很实际——Windows 下带盘符和某些编码的路径里出现等号的情况虽少,但split会把值切断,而且不报错,属于典型的静默错误。

3.2 加载 KeyStore:JKS 与 PKCS12 的分叉口

密钥库格式是整个流程里最容易出错的一环。老项目多数是 JKS,新工具链默认产出 PKCS12,两者的密码语义还不一样。源码里的处理是先猜再回退:

public static KeyStore loadKeyStore(File file, char[] storePass) throws Exception { // 先按扩展名猜类型:.p12/.pfx 当 PKCS12,其余当 JKS String guess = (file.getName().endsWith(".p12") || file.getName().endsWith(".pfx")) ? "PKCS12" : "JKS"; try { return load(file, storePass, guess); } catch (IOException e) { // JKS 魔数是 FEEDFEED,PKCS12 是 3082,猜错了换另一种再试一次 String fallback = "PKCS12".equals(guess) ? "JKS" : "PKCS12"; return load(file, storePass, fallback); } } private static KeyStore load(File file, char[] pass, String type) throws Exception { KeyStore ks = KeyStore.getInstance(type); try (FileInputStream in = new FileInputStream(file)) { ks.load(in, pass); } return ks; }

load方法本身没什么好说的,重点是那个catch (IOException)。KeyStore.load在格式不匹配时抛的是IOException: Invalid keystore format,在密码错误时抛的是IOException: keystore password was incorrect——两种都算 IO 异常,所以回退重试之后,如果第二次还是失败,必须把原始异常带上,否则用户只知道「加载失败」,不知道到底是格式错还是密码错。

我一般会在回退时把两次异常串起来:

catch (IOException first) { try { return load(file, pass, fallback); } catch (Exception second) { second.addSuppressed(first); // 保留第一次的真实原因 throw second; } }

addSuppressed是这类「试错式加载」的标准写法。打印堆栈时两段原因都会出来,看一眼就知道是「格式猜错了」还是「密码本来就不对」。

3.3 取私钥与证书链:为什么必须校验 alias 类型

拿到KeyStore之后要找一个PrivateKeyEntry。这里有两个容易忽略的判断,源码里都做了。

KeyStore.PrivateKeyEntry entry; if (alias == null) { // 没给 alias:全库扫描,找到唯一一条私钥条目;多于一条直接报错 entry = null; Enumeration<String> aliases = ks.aliases(); while (aliases.hasMoreElements()) { String a = aliases.nextElement(); if (!ks.isKeyEntry(a)) continue; if (entry != null) { throw new IllegalStateException("keystore 中存在多条私钥记录,请用 --alias 指定"); } entry = (KeyStore.PrivateKeyEntry) ks.getEntry(a, new KeyStore.PasswordProtection(keyPass)); } } else { if (!ks.isKeyEntry(alias)) { throw new IllegalStateException("alias " + alias + " 不是私钥条目,无法导出"); } entry = (KeyStore.PrivateKeyEntry) ks.getEntry(alias, new KeyStore.PasswordProtection(keyPass)); } if (entry == null) throw new IllegalStateException("keystore 中没有可导出的私钥条目");

第一处判断是isKeyEntry。很多人以为containsAlias就够了,但containsAlias对信任证书条目也返回 true,强转PrivateKeyEntry会直接ClassCastException。这个异常信息不会告诉你「你选的是一条证书」,只会告诉你类型不匹配,误导性很强。

第二处是「多条私钥记录时直接报错」。工程里常见一个 keystore 同时存了 release 和 debug 两把钥匙,如果代码默默取第一条,导出成功、上传成功、商店也接受了,但签出来的包跟历史版本对不上,问题要到用户升级失败才暴露。宁可在这里报错让人指定--alias,也不要给这种「成功但错了」的结果。

entry.getPrivateKey().getEncoded()返回的是 PKCS#8 DER。这个返回值可能为null——某些 HSM 或 PKCS#11 提供者的密钥对象不可导出,源码里没有做兜底,因为这种场景本来就没法导出密钥。你在二次开发时如果接入了自定义Provider,记得先判空再往下走。

3.4 还原公钥并加密:十六进制串与分块 OAEP

后台给的那一长串字符是十六进制编码的 X.509 SubjectPublicKeyInfo,不是 base64。这一点搞错的话会得到InvalidKeySpecException,报错里完全没有提示你编码方式错了。

public static RSAPublicKey fromHex(String hex) throws Exception { // 去掉可能存在的空白和换行,商店页面复制时常带尾随空格 byte[] der = Hex.decode(hex.replaceAll("\\s", "")); return (RSAPublicKey) KeyFactory.getInstance("RSA") .generatePublic(new X509EncodedKeySpec(der)); }

真正麻烦的是加密环节。RSA-OAEP 单块能加密的明文长度是模长字节数 − 2*hLen − 2,2048 位密钥配 SHA-256 时只有 190 字节,而一把 RSA 2048 的 PKCS#8 私钥 DER 有 1200 多字节。必须分块:

public byte[] encrypt(byte[] pkcs8, RSAPublicKey pub) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding", "BC"); // MGF1 的摘要必须显式指定,BC 对 "OAEPWithSHA-256AndMGF1Padding" 的默认 MGF1 摘要是 SHA-1 OAEPParameterSpec spec = new OAEPParameterSpec( "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT); cipher.init(Cipher.ENCRYPT_MODE, pub, spec); int blockSize = (pub.getModulus().bitLength() + 7) / 8 - 2 * 32 - 2; // 190 for 2048/SHA-256 ByteArrayOutputStream out = new ByteArrayOutputStream(); for (int off = 0; off < pkcs8.length; off += blockSize) { int len = Math.min(blockSize, pkcs8.length - off); byte[] part = cipher.doFinal(pkcs8, off, len); // 每块独立做一次完整 OAEP out.write(part, 0, part.length); } return out.toByteArray(); }

这里最容易出错的地方在第三行:Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")这个名字里只有一个 SHA-256,但 OAEP 实际上有两个摘要参数——主摘要和 MGF1 摘要。BC 的默认实现里主摘要是 SHA-256、MGF1 摘要是 SHA-1,跟大多数人「两个都是 SHA-256」的直觉不符。单独看这个配置本身没问题,但如果自己改代码,用MGF1ParameterSpec.SHA256手工构造OAEPParameterSpec,就会和默认行为不一致。只要哪一端改了、另一端没改,上传后台必然解析失败。

所以我在二次开发章节会强调:加密参数是一份契约,不是配置项。

3.5 组装输出:zip 里的两个条目与 base64 收尾

最后一步是把密文和证书装进 zip。条目名是固定的两个,后台按名字读,改了就解析不了。

public void write(File outFile, byte[] encryptedKey, X509Certificate cert) throws IOException { try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outFile))) { zos.putNextEntry(new ZipEntry("encryptedPrivateKey")); // 密文是二进制,zip 里先做 base64,方便排查时直接 cat zos.write(Base64.getEncoder().encode(encryptedKey)); zos.closeEntry(); zos.putNextEntry(new ZipEntry("certificate.pem")); zos.write(PemUtil.encode(cert.getEncoded(), "CERTIFICATE")); zos.closeEntry(); } }

PemUtil.encode做的是 BASE64 加头尾,只有十几行,但有个细节必须保证:

public static byte[] encode(byte[] der, String type) { // 64 列换行,行分隔符固定 \n;用 CRLF 会让部分后台解析器把 \r 当成字符 String body = Base64.getMimeEncoder(64, new byte[]{'\n'}).encodeToString(der); String pem = "-----BEGIN " + type + "-----\n" + body + "\n-----END " + type + "-----\n"; return pem.getBytes(StandardCharsets.US_ASCII); }

getMimeEncoder而不是getEncoder,是因为 PEM 规范要求 64 列折行;byte[]{'\n'}而不是System.lineSeparator(),是因为 PEM 的行分隔符跟操作系统无关。这两处在 Linux 上跑不出问题,在 Windows 上跑出来的文件才会出问题,属于典型的「本地好好的,CI 上失败」。

整个流程到这儿就结束了:输出一个 zip,里面一条加密私钥、一张证书,工具打印成功信息,你把它传回后台。

4. 跑起来:编译、命令行调用与产物验证

4.1 环境与编译

最低要求是 JDK 8,但我建议 11 或 17,因为 BC 的新版本在 8 上有额外的兼容层,启动会慢一点点。整个编译过程就两条命令:

# 1) 打包(第一次会拉 BC 依赖,大概几十秒) ./gradlew clean shadowJar # 2) 确认产物存在 ls -lh build/libs/pepk.jar # 预期 8MB 上下

如果没有 Gradle 环境,也可以用javac直接编,前提是你自己把 BC 的 jar 下下来:

javac -encoding UTF-8 -cp "bcprov-jdk18on-1.78.1.jar:bcpkix-jdk18on-1.78.1.jar" \ -d out $(find src/main/java -name "*.java") jar cfe pepk.jar com.example.signing.pepk.ExportEncryptedPrivateKeyTool -C out .

注意jar cfe的第一个参数是入口类,写错了后面运行会报no main manifest attribute。另外-cp里的分隔符在 Windows 上是;,跨平台脚本里最好用 Gradle,别手拼。

4.2 参数表:每个开关管什么

参数是否必填说明
--keystore必填密钥库路径,.jks/.keystore/.p12都行
--alias建议填私钥条目别名;不填则全库扫描,多条时报错
--output必填输出 zip 路径,文件不存在会自动创建
--encryptionkey必填后台页面给的十六进制公钥串,一整段粘进去
--storepass可选密钥库口令,不填则交互式询问
--keypass可选私钥口令,不填则复用 storepass
--include-signing-key可选开关型;是否带签名证书随后台导出页面给的命令走,别自己加
--help可选打印用法

--storepass/--keypass这两个参数存在,但我不建议在命令行里用。shell 历史、CI 日志、ps输出都可能把口令漏出来。源码里readPassword用的是System.console().readPassword(),输入时不回显:

private static char[] readPassword(Map<String, String> opts, String key, String prompt) { if (opts.containsKey(key)) return opts.get(key).toCharArray(); if (opts.containsKey("storepass") && "keypass".equals(key)) { return opts.get("storepass").toCharArray(); // 未单独给 keypass 时复用之 } Console console = System.console(); if (console == null) { throw new IllegalStateException("当前环境无终端,请用 --" + key + " 显式传入"); } return console.readPassword(prompt); }

System.console()在 IDE 的 Run 窗口和某些 CI 环境下会返回null,这时readPassword直接抛错而不是卡住等输入。这个设计是有意为之——卡住比报错难查得多。

4.3 三种典型跑法

最常规的一种,口令交互输入:

java -jar build/libs/pepk.jar \ --keystore=release.jks \ --alias=release \ --output=export.zip \ --encryptionkey=7c9a3f...(后台页面复制的那一整串) # 执行后依次提示 Keystore password / Key password

第二种,从 PKCS12 库导出,别名和口令都写全。注意 PKCS12 里私钥口令和库口令必须一致,所以只传--storepass就够了:

java -jar build/libs/pepk.jar \ --keystore=release.p12 \ --alias=upload \ --output=export.zip \ --encryptionkey=7c9a3f... \ --storepass="$PEPK_STOREPASS" # 从 CI 环境变量注入,不要硬编码

第三种,先看帮助确认参数名,再决定怎么传。不同后台页面的导出命令写法略有差别,照抄页面给的命令是最稳的:

java -jar build/libs/pepk.jar --help

跑完正常会看到类似Success!的输出,同时export.zip出现在当前目录。如果只看到Usage: ...而没有报错,说明必填参数缺了,去看 4.2 的表。

4.4 产物验证:不用解密也能确认对不对

zip 里的私钥是加密的,本地解不开,但你可以验证两件更重要的事:结构对不对、证书是不是同一张。

# 1) 条目应该正好两条,顺序不重要,名字必须完全一致 unzip -l export.zip # 2) 从 zip 里取出证书,打印 SHA-256 指纹 unzip -p export.zip certificate.pem | openssl x509 -noout -fingerprint -sha256 # 3) 跟原密钥库里的证书指纹对比,两个值必须一模一样 keytool -list -v -keystore release.jks -alias release | grep -i "SHA256:"

指纹对上了,说明你导出的确实是要交接的那把钥匙;条目名和数量对上,说明打包没写歪。这两项都过了,加密那一步出问题的概率就极低——加密要么抛异常,要么产出长度是模长整数倍的密文,不会「静默生成一个错的」。

还有一条快速检查,看密文长度:

unzip -p export.zip encryptedPrivateKey | base64 -d | wc -c # 结果必须是 256 的整数倍(2048 位密钥),否则分块逻辑有问题

5. 避坑与排查:五个让密钥导出返工的现场

5.1 编译打包阶段的两个失败

现象一:java -jar pepk.jar直接抛Invalid signature file digest for Manifest main attributes,连帮助信息都打不出来。

原因:shadow 插件把 Bouncy Castle 的META-INF/*.SF、*.RSA一起合并进了 fat jar,JVM 校验签名时发现摘要对不上,拒绝加载主类。

解决:在shadowJar任务里 exclude 掉这三类文件(见 2.3 的示例)。加完记得clean一次,增量构建会留着旧的 jar 骗你。

现象二:./gradlew shadowJar报Unsupported class file major version 61。

原因:Gradle 版本太老,跑在 JDK 17 上不认新的字节码版本。

解决:升级 Gradle wrapper 到 7.6 以上,或者把JAVA_HOME指到 JDK 11 再构建。别去改sourceCompatibility硬扛,那是治标。

5.2 运行阶段的三个高发错误

现象三:keystore password was incorrect,但密码明明是对的(在别的工具里能用)。

原因:三种可能——把 PKCS12 当 JKS 加载了;PKCS12 里私钥口令不等于库口令而你给错了;密码串末尾带了空格或换行(从密码管理器复制时最常见)。

解决:先用keytool -list -keystore xxx -storetype PKCS12确认格式和口令;再在代码里打印口令长度(不要打印内容)核对有没有多余字符;PKCS12 只传--storepass,让它自动复用。

现象四:InvalidKeySpecException: Only RSAPublicKeySpec and X509EncodedKeySpec supported或IOException: invalid key format。

原因:--encryptionkey传的不是十六进制串。有人会把它当 base64 解码,或者从页面复制时把中间的换行、引号一起带进去了。

解决:Hex.decode之前先replaceAll("\\s", "")去掉所有空白,并校验长度是否为偶数。另外确认没有把页面上的说明文字一起复制进来——那一串通常很长,凭肉眼很难发现尾巴上多了个字符。

现象五:导出成功,zip 也生成了,但上传后台提示解析失败。

原因:加密参数被改过。最常见的是把 MGF1 摘要换成 SHA-256 却没同步另一端,或者 base64 用了 CRLF 换行,或者 zip 条目名写成了encrypted_private_key这种变体。

解决:别动加密参数和条目名。真要改,先在一个能回滚的测试应用上验证,别拿线上的签名钥匙做实验。

5.3 收尾阶段的隐性风险

现象六:导出成功后,有人在仓库里 commit 了export.zip。

原因:.gitignore没覆盖*.zip和*.jks。

解决:把导出目录从工程目录移出去,比如固定输出到~/pepk-out/,并在.gitignore里显式写死导出文件名。加密归加密,这个 zip 和加密公钥一起泄露并不是零风险,没必要留。

6. 二次开发:把导出动作接进发布流水线的三种改法

改造的第一原则是别碰加密格式。我在PrivateKeyEncryptor上面加了一层不可变配置类,把所有「契约型参数」钉死成常量:条目名、摘要算法、MGF1 摘要、分块公式。想改就改这个类,改完必须在能回滚的测试环境里验证一遍上传,别拿线上签名钥匙试。

第一种改法最省事,在 Gradle 里挂个任务,从环境变量取口令,跑完顺手做指纹比对:

tasks.register('exportPepk', Exec) { def out = file("$buildDir/pepk/export.zip") doFirst { out.parentFile.mkdirs() } // 口令走环境变量,绝不写进 build.gradle environment 'PEPK_STOREPASS', System.getenv('PEPK_STOREPASS') commandLine 'java', '-jar', 'tools/pepk.jar', "--keystore=${findProperty('pepk.keystore')}", "--alias=${findProperty('pepk.alias')}", "--output=${out}", "--encryptionkey=${findProperty('pepk.encryptionkey')}" }

第二种改法是把工具当库用,去掉main直接调ExportEncryptedPrivateKeyTool.export(config),适合要塞进自己的发布平台、需要拿到结构化结果(而不是解析控制台输出)的场景。改的时候把main里的流程抽成一个返回ExportResult的方法,成功与否用异常表达,别用System.exit——一exit整个服务进程就没了。

第三种改法是批量导出多个 flavor 的钥匙。这种情况我一般会先写一个 dry-run 校验:只做加载和取条目、不加密不写文件,把每个 keystore 的证书指纹打出来,确认一一对应之后再真跑。这一步多花十秒,能挡住「导错钥匙」这种返工一整天的事故。

验证方法有三条,我每次都走一遍:unzip -l看条目名,openssl x509 -fingerprint -sha256跟keytool的输出对比,再base64 -d | wc -c确认长度是模长整数倍。三条全过再上传,比上传失败后回头猜要快得多。

从那以后,我每次把密钥导出接进流水线,都强制先跑一遍 dry-run 和指纹比对,宁可多一步,也不让「成功但导错了」的东西流出去。源码拿到手,先--help,再照着第 4 章的验证清单走一遍,基本不会翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询