PEPK 上传应用签名密钥踩坑:RSA/NONE/OAEPWithSHA1AndMGF1Padding 报错排查全过程

在 Google Play 上传应用签名密钥时,PEPK 工具报 Cannot find any provider supporting RSA/NONE/OAEPWithSHA1AndMGF1Padding。本文记录从该错误起的完整排查与解决过程:注册 BouncyCastle、发现内置 BC 被裁剪、改用完整 bcprov 库,附每一步的错误输出与验证证据。

CompanyPEPKGoogle Play应用签名BouncyCastleJDKFlutter

背景

在 Google Play 后台「应用签名偏好设置」中选择 「从 Java 密钥库导出并上传密钥」,需要用 Google 提供的 PEPK(Play Encrypt Private Key)工具,把本地 keystore 里的私钥导出并用 Google 的公钥加密成 output.zip 再上传。

官方给的命令模板是:

java -jar pepk.jar \
  --keystore=foo.keystore \
  --alias=foo \
  --output=output.zip \
  --include-cert \
  --rsa-aes-encryption \
  --encryption-key-path=/path/to/encryption_public_key.pem

按项目实际 keystore 参数替换后(本机 keystore 为 signature.jks,alias 为 kkshort),命令能正常读取密码并进入加密流程,但随即抛出算法找不到的异常。本文从这个错误开始,记录完整的排查与解决过程。

环境

操作系统Windows 10
JDKOracle JDK17.0.15D:\Softwares\JDK\jdk17.0.15
工具pepk.jar(Google Play Console 下载的可运行版)
工作目录D:\Code(放置 pepk.jarencryption_public_key.pem

问题:Cannot find any provider supporting RSA/NONE/OAEPWithSHA1AndMGF1Padding

运行命令后,两次密码输入正常,但加密阶段报错:

D:\Code>java -jar pepk.jar --keystore=AltaTV/android/signature.jks --alias=kkshort --output=output.zip --include-cert --rsa-aes-encryption --encryption-key-path=encryption_public_key.pem
Enter password for store 'signature.jks':
Enter password for key 'kkshort':
Error: Unable to export or encrypt the private key
java.security.NoSuchAlgorithmException: Cannot find any provider supporting RSA/NONE/OAEPWithSHA1AndMGF1Padding
        at java.base/javax.crypto.Cipher.getInstance(Cipher.java:571)
        at com.google.wireless.android.vending.developer.signing.tools.extern.export.ExportEncryptedPrivateKeyTool.encryptPrivateKeyWithCkmRsaAesKeyWrapEncryption(ExportEncryptedPrivateKeyTool.java:284)
        at com.google.wireless.android.vending.developer.signing.tools.extern.export.ExportEncryptedPrivateKeyTool.run(ExportEncryptedPrivateKeyTool.java:213)
        at com.google.wireless.android.vending.developer.signing.tools.extern.export.ExportEncryptedPrivateKeyTool.main(ExportEncryptedPrivateKeyTool.java:165)

初步分析

RSA/NONE/OAEPWithSHA1AndMGF1Padding 这个加密变换(transformation)不是 JDK 自带 SunJCE 提供的——SunJCE 只提供 RSA/ECB/OAEPWithSHA-1AndMGF1PaddingRSA/NONE/... 这种写法由 BouncyCastle 提供。报错说明当前 JDK 里没有注册 BouncyCastle 这个 JCE 安全提供者(Security Provider)。

先确认 JDK 版本和已注册的提供者列表:

# 查看 java 版本
java -version
# java version "17.0.15" 2025-04-15 LTS

# 查看 java.security 中已注册的 provider
Select-String -Path "$env:JAVA_HOME\conf\security\java.security" -Pattern '^security\.provider\.\d+='

输出(默认 13 个提供者,没有 BouncyCastle):

security.provider.1=SUN
security.provider.2=SunRsaSign
security.provider.3=SunEC
security.provider.4=SunJSSE
security.provider.5=SunJCE
security.provider.6=SunJGSS
security.provider.7=SunSASL
security.provider.8=XMLDSig
security.provider.9=SunPCSC
security.provider.10=JdkLDAP
security.provider.11=JdkSASL
security.provider.12=SunMSCAPI
security.provider.13=SunPKCS11

再确认 pepk.jar 内部是否自带 BouncyCastle 的类:

Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead("D:\Code\pepk.jar")
($zip.Entries | Where-Object { $_.FullName -like "org/bouncycastle/*" -and $_.FullName.EndsWith(".class") }).Count
$zip.Dispose()

输出:

pepk.jar 内 BouncyCastle .class 数量: 4337
含 BouncyCastleProvider.class: True

结论:BouncyCastle 的类就在 pepk.jar(含 BouncyCastleProvider.class),只是没有被注册为 JCE 提供者。看起来只要把它注册进去就能解决。


尝试一:注册 BouncyCastle 提供者(部分有效,但仍失败)

JDK 允许通过 -Djava.security.properties=<file>单次运行时叠加安全配置(不改动全局 JDK),前提是 java.security 中开关为 security.overridePropertiesFile=true(JDK 默认开启,已确认)。

创建叠加文件 D:\Code\bc.security,把 BouncyCastle 追加为第 14 个提供者:

security.provider.14=org.bouncycastle.jce.provider.BouncyCastleProvider

重新运行(在 -jar 前加 -Djava.security.properties=bc.security):

D:\Code>java -Djava.security.properties=bc.security -jar pepk.jar --keystore=AltaTV/android/signature.jks --alias=kkshort --output=output.zip --include-cert --rsa-aes-encryption --encryption-key-path=encryption_public_key.pem
Enter password for store 'signature.jks':
Enter password for key 'kkshort':
Error: Unable to export or encrypt the private key
java.security.NoSuchAlgorithmException: Cannot find any provider supporting RSA/NONE/OAEPWithSHA1AndMGF1Padding
        at java.base/javax.crypto.Cipher.getInstance(Cipher.java:571)
        ...

依然报同样的错。 为了避免反复输密码,写一个小测试程序精确定位:BC 到底注册成功了没有、这个算法能不能找到。

// CipherTest.java
import javax.crypto.Cipher;
import java.security.Security;
import java.security.Provider;

public class CipherTest {
    public static void main(String[] args) throws Exception {
        Provider bc = Security.getProvider("BC");
        System.out.println("BC provider = " + bc
            + " (version: " + (bc == null ? "NULL" : bc.getVersionStr()) + ")");
        if (bc != null) {
            System.out.println("==== BC services containing RSA/OAEP ====");
            for (Provider.Service s : bc.getServices()) {
                String algo = s.getAlgorithm();
                if (s.getType().equals("Cipher")
                        && (algo.contains("RSA") || algo.contains("OAEP"))) {
                    System.out.println("  Cipher." + algo);
                }
            }
        }
        String[] tries = {
            "RSA/NONE/OAEPWithSHA1AndMGF1Padding",
            "RSA/NONE/OAEPWithSHA-1AndMGF1Padding",
            "RSA/ECB/OAEPWithSHA1AndMGF1Padding",
            "RSA/NONE/OAEPPadding",
        };
        for (String t : tries) {
            try {
                Cipher c = Cipher.getInstance(t);
                System.out.println("  OK   " + t + " -> " + c.getProvider().getName());
            } catch (Exception e) {
                System.out.println("  FAIL " + t + " -> " + e.getClass().getSimpleName());
            }
        }
    }
}

编译并运行(把 pepk.jar 放到类路径,让它提供 BC 类):

javac CipherTest.java
java "-Djava.security.properties=D:\Code\bc.security" -cp "D:\Code\pepk.jar;." CipherTest

输出(关键证据):

BC provider = BC version 1.645 (version info: 1.645)
==== BC services containing RSA/OAEP ====
  Cipher.RSA
  Cipher.RSA/OAEP
  Cipher.RSA/PKCS1
  Cipher.RSA/ISO9796-1
  Cipher.RSA/1
  Cipher.RSA/2
  Cipher.RSA/RAW
==== try transformations ====
  FAIL RSA/NONE/OAEPWithSHA1AndMGF1Padding -> NoSuchAlgorithmException
  FAIL RSA/NONE/OAEPWithSHA-1AndMGF1Padding -> NoSuchAlgorithmException
  OK   RSA/ECB/OAEPWithSHA1AndMGF1Padding -> SunJCE
  FAIL RSA/NONE/OAEPPadding -> NoSuchAlgorithmException

真正的根因

BouncyCastle 确实注册成功了(provider 列表里出现了 BC),但它是 Google 用 singlejar 打包时裁剪过的 BC 1.645:只注册了笼统的 Cipher.RSA/OAEP缺少 RSA/NONE/OAEPWith... 这种带完整参数的变换实现。所以即便注册了 BC,RSA/NONE/OAEPWithSHA1AndMGF1Padding 仍然解析不出来。

所以问题不在「有没有注册 BC」,而在「注册进来的 BC 是残缺的」。解决方向:用一个完整的、独立的 BouncyCastle 库替换它。


尝试二:改用完整的 bcprov 库(成功)

从 Maven Central 下载完整的 BouncyCastle 提供者库:

Invoke-WebRequest `
  -Uri "https://repo1.maven.org/maven2/org/bouncycastle/bcprov-jdk18on/1.78.1/bcprov-jdk18on-1.78.1.jar" `
  -OutFile "D:\Code\bcprov.jar"
# 下载成功: 8324412 bytes

用测试程序验证——把 bcprov.jar 放在类路径最前面,让完整 BC 覆盖 pepk 内置的残缺 BC 1.645:

java "-Djava.security.properties=D:\Code\bc.security" -cp "D:\Code\bcprov.jar;D:\Code\pepk.jar;." CipherTest

输出(验证通过):

BC provider = BC version 1.7801 (version info: 1.7801)
==== BC services containing RSA/OAEP ====
  Cipher.RSA
  Cipher.RSA/ISO9796-1
  Cipher.RSA/RAW
  Cipher.RSA/OAEP
  Cipher.RSA/PKCS1
  Cipher.RSA/1
  Cipher.RSA/2
==== try transformations ====
  OK   RSA/NONE/OAEPWithSHA1AndMGF1Padding -> BC
  OK   RSA/NONE/OAEPWithSHA-1AndMGF1Padding -> BC
  OK   RSA/ECB/OAEPWithSHA1AndMGF1Padding -> SunJCE
  OK   RSA/NONE/OAEPPadding -> BC

BC version 1.7801,且 RSA/NONE/OAEPWithSHA1AndMGF1Padding -> BC 成功解析


最终解决方案

D:\Code 目录准备好三样东西:

文件说明
pepk.jarGoogle Play Console 下载的 PEPK 工具(可运行版,不是源码包)
bcprov.jar从 Maven 下载的完整bcprov-jdk18on-1.78.1.jar
bc.security一行内容:security.provider.14=org.bouncycastle.jce.provider.BouncyCastleProvider

运行最终命令(注意:用 -cp 而非 -jar,并显式指定主类):

java -Djava.security.properties=bc.security ^
  -cp "bcprov.jar;pepk.jar" ^
  com.google.wireless.android.vending.developer.signing.tools.extern.export.ExportEncryptedPrivateKeyTool ^
  --keystore=AltaTV/android/signature.jks ^
  --alias=kkshort ^
  --output=output.zip ^
  --include-cert ^
  --rsa-aes-encryption ^
  --encryption-key-path=encryption_public_key.pem

相比最初命令的三处关键改动:

  1. -cp "bcprov.jar;pepk.jar" 替代 -jar pepk.jar——把完整 BouncyCastle 放在类路径最前,覆盖 pepk 内置的残缺 BC 1.645;类路径分隔符 Windows 用 ;,Linux/macOS 用 :
  2. 因为不再用 -jar,需要显式写主类名 com.google.wireless.android.vending.developer.signing.tools.extern.export.ExportEncryptedPrivateKeyTool
  3. 保留 -Djava.security.properties=bc.security 把 BouncyCastle 注册为 JCE 提供者。

运行时按提示两次输入 keystore 密码与 key 密码,即可正常生成 output.zip,回到 Play Console 上传该文件即可完成上传密钥。


排查过程总结

阶段现象根因解决
问题NoSuchAlgorithmException: ... RSA/NONE/OAEPWithSHA1AndMGF1PaddingJDK 17 未注册 BouncyCastle需要注册 BC 提供者
尝试一注册 BC 后仍报同样的错pepk 内置的BC 1.645 被裁剪,缺 RSA/NONE/OAEP 完整变换需换完整 BC 库
尝试二算法成功解析 → BC完整bcprov 1.78.1 提供该变换✅ 用-cp 把完整 bcprov 放类路径最前 + 注册 BC

经验教训

  • NoSuchAlgorithmException 分两层:一层是「提供者压根没注册」,另一层是「提供者注册了但缺这个具体算法」。看到 provider 列表里已有 BC 却仍报错时,要往第二层想。
  • 不要盲目反复重跑需要交互输入密码的命令:写一个几行的 Cipher.getInstance 测试程序,把「provider 是否注册」「算法是否可用」一次性打印出来,比一遍遍输密码试错高效得多。
  • java -jar 会忽略 -cp-jar 模式下类路径只有那个 jar,要加额外依赖必须改用 -cp 并显式写主类。
  • -Djava.security.properties=(单个等号)是叠加,==(双等号)是整体替换:单等号方式只对本次运行生效、不污染全局 JDK 配置,更安全可回滚。
  • Google 打包的工具内可能带裁剪过的第三方库,遇到「类在、功能缺」的诡异现象时,用一个独立的完整依赖覆盖它往往是最快的路。