WebCrypto RSA-OAEP-SHA-512加密内容Java解密填充错误排查
RSA-OAEP-SHA-512跨WebCrypto与Java解密报Padding Error的解决方法
问题背景
客户端通过WebCrypto API使用RSA-OAEP-SHA-512加密字符串,服务端Java解密时抛出javax.crypto.BadPaddingException: Padding error in decryption。已知两端各自加解密正常,但跨语言操作失败,更换SHA-1、SHA-256等其他哈希算法也无法解决问题。
客户端代码
密钥生成逻辑
async function generateRSAKeyPair() { const rsaKeyPair = await window.crypto.subtle.generateKey( { name: 'RSA-OAEP', modulusLength: 4096, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 hash: { name: 'SHA-512' }, }, true, ['encrypt', 'decrypt'] ); return rsaKeyPair; }
加密逻辑
async function encryptWithRSA(jsonString, jsonRSAPublicKey) { // Parse JSON keys const rsaPublicKey = JSON.parse(jsonRSAPublicKey); // Import RSA public key const importedRSAPublicKey = await window.crypto.subtle.importKey( 'jwk', rsaPublicKey, { name: 'RSA-OAEP', hash: { name: 'SHA-512' } }, true, ['encrypt'] ); // Convert JSON String to ArrayBuffer const aesKeyBuffer = new TextEncoder().encode(jsonString); // Encrypt AES key using RSA public key const encryptedAesKeyBuffer = await window.crypto.subtle.encrypt( { name: 'RSA-OAEP' }, importedRSAPublicKey, aesKeyBuffer ); // Convert encrypted AES key to base64 const encryptedAesKeyBase64 = window.btoa(String.fromCharCode(...new Uint8Array(encryptedAesKeyBuffer))); return encryptedAesKeyBase64; }
服务端Java原报错代码
解密逻辑
import javax.crypto.Cipher; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.RSAPrivateCrtKeySpec; import java.math.BigInteger; import java.util.Base64; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; public void decrypt() { try { PrivateKey privateKey = loadPrivateKey(); Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-512AndMGF1Padding"); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] encryptedBytes = Base64.getDecoder().decode(cipherText); byte[] decryptedBytes = cipher.doFinal(encryptedBytes); this.clearText = new String(decryptedBytes); System.out.println("Decrypted clear text: " + clearText); } catch (Exception e) { e.printStackTrace(); System.err.println("Decryption failed: " + e.getMessage()); } } private PrivateKey loadPrivateKey() throws Exception { // Parse the JWK JSON string ObjectMapper mapper = new ObjectMapper(); Map<String, Object> jwk = mapper.readValue(rsaKey, Map.class); System.out.println("JWK parsed: " + jwk); // Extract the key components from the JWK String nBase64 = (String) jwk.get("n"); String dBase64 = (String) jwk.get("d"); String pBase64 = (String) jwk.get("p"); String qBase64 = (String) jwk.get("q"); String dpBase64 = (String) jwk.get("dp"); String dqBase64 = (String) jwk.get("dq"); String qiBase64 = (String) jwk.get("qi"); String eBase64 = (String) jwk.get("e"); // Decode the Base64 URL-encoded components BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(nBase64)); BigInteger privateExponent = new BigInteger(1, Base64.getUrlDecoder().decode(dBase64)); BigInteger publicExponent = new BigInteger(1, Base64.getUrlDecoder().decode(eBase64)); BigInteger primeP = new BigInteger(1, Base64.getUrlDecoder().decode(pBase64)); BigInteger primeQ = new BigInteger(1, Base64.getUrlDecoder().decode(qBase64)); BigInteger primeExponentP = new BigInteger(1, Base64.getUrlDecoder().decode(dpBase64)); BigInteger primeExponentQ = new BigInteger(1, Base64.getUrlDecoder().decode(dqBase64)); BigInteger crtCoefficient = new BigInteger(1, Base64.getUrlDecoder().decode(qiBase64)); // Create the RSA private key specification RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient); // Generate the private key KeyFactory keyFactory = KeyFactory.getInstance(AppConfig.CRYPTO_RSA); return keyFactory.generatePrivate(keySpec); }
报错信息
javax.crypto.BadPaddingException: Padding error in decryption at java.base/com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:383) at java.base/com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:419) at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2244) at zkgitclientcli.crypto.RsaEncryptionHandler2.decrypt(RsaEncryptionHandler2.java:48) at zkgitclientcli.commands.login.DecryptAesKey.execute(DecryptAesKey.java:23) at zkgitclientcli.commands.CommandManager.executeCommand(CommandManager.java:44) at zkgitclientcli.ZkGit.main(ZkGit.java:12) at org.codehaus.mojo.exec.ExecJavaMojo.doMain(ExecJavaMojo.java:385) at org.codehaus.mojo.exec.ExecJavaMojo.doExec(ExecJavaMojo.java:374) at org.codehaus.mojo.exec.ExecJavaMojo.lambda$execute$0(ExecJavaMojo.java:296) at java.base/java.lang.Thread.run(Thread.java:1583) Decryption failed: Padding error in decryption
问题原因
WebCrypto API中,RSA-OAEP默认使用与主哈希算法一致的哈希函数作为MGF1(掩码生成函数)的哈希——即主哈希为SHA-512时,MGF1也用SHA-512。但Java默认的OAEPWithSHA-512AndMGF1Padding实现中,MGF1的哈希函数默认是SHA-1,两者参数不匹配导致padding验证失败。
解决方案
修改Java解密代码,通过OAEPParameterSpec明确指定MGF1使用SHA-512哈希算法,与WebCrypto保持参数一致:
import javax.crypto.Cipher; import javax.crypto.spec.OAEPParameterSpec; import javax.crypto.spec.PSource; import javax.crypto.spec.MGF1ParameterSpec; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.RSAPrivateCrtKeySpec; import java.math.BigInteger; import java.util.Base64; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; public void decrypt() { try { PrivateKey privateKey = loadPrivateKey(); // 明确配置OAEP参数:主哈希SHA-512,MGF1哈希SHA-512,无额外参数 OAEPParameterSpec oaepParams = new OAEPParameterSpec( "SHA-512", "MGF1", new MGF1ParameterSpec("SHA-512"), PSource.PSpecified.DEFAULT ); Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-512AndMGF1Padding"); cipher.init(Cipher.DECRYPT_MODE, privateKey, oaepParams); byte[] encryptedBytes = Base64.getDecoder().decode(cipherText); byte[] decryptedBytes = cipher.doFinal(encryptedBytes); this.clearText = new String(decryptedBytes); System.out.println("Decrypted clear text: " + clearText); } catch (Exception e) { e.printStackTrace(); System.err.println("Decryption failed: " + e.getMessage()); } } // loadPrivateKey方法保持不变 private PrivateKey loadPrivateKey() throws Exception { ObjectMapper mapper = new ObjectMapper(); Map<String, Object> jwk = mapper.readValue(rsaKey, Map.class); System.out.println("JWK parsed: " + jwk); String nBase64 = (String) jwk.get("n"); String dBase64 = (String) jwk.get("d"); String pBase64 = (String) jwk.get("p"); String qBase64 = (String) jwk.get("q"); String dpBase64 = (String) jwk.get("dp"); String dqBase64 = (String) jwk.get("dq"); String qiBase64 = (String) jwk.get("qi"); String eBase64 = (String) jwk.get("e"); BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(nBase64)); BigInteger privateExponent = new BigInteger(1, Base64.getUrlDecoder().decode(dBase64)); BigInteger publicExponent = new BigInteger(1, Base64.getUrlDecoder().decode(eBase64)); BigInteger primeP = new BigInteger(1, Base64.getUrlDecoder().decode(pBase64)); BigInteger primeQ = new BigInteger(1, Base64.getUrlDecoder().decode(qBase64)); BigInteger primeExponentP = new BigInteger(1, Base64.getUrlDecoder().decode(dpBase64)); BigInteger primeExponentQ = new BigInteger(1, Base64.getUrlDecoder().decode(dqBase64)); BigInteger crtCoefficient = new BigInteger(1, Base64.getUrlDecoder().decode(qiBase64)); RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient); KeyFactory keyFactory = KeyFactory.getInstance(AppConfig.CRYPTO_RSA); return keyFactory.generatePrivate(keySpec); }
注意事项
- 确保代码中导入
javax.crypto.spec.MGF1ParameterSpec类 - 该适配针对OpenJDK/SunJCE默认加密 provider,若使用第三方provider需调整参数格式
- 客户端
btoa生成的是标准Base64,Java用Base64.getDecoder()解析正确,无需修改编码逻辑
内容的提问来源于stack exchange,提问作者Roggab32
相关产品推荐
相关产品推荐

