AES、DES、RSA等加密出现解密失败原因

我们在用微信支付或者其他第三方接口用对称或者非对称加密时经常会遇到明明公钥私钥都正确,可是第三方服务端还是返回解密失败

解密错误原因

1.本地字符串编码和加密后字节编码不对应,java中String类的getBytes方法如下

public byte[] getBytes() {
      return StringCoding.encode(value, 0, value.length);
  }

该方法是调用StringCoding的静态encode方法,encode方法如下:

static byte[] encode(char[] ca, int off, int len) {
        String csn = Charset.defaultCharset().name();
        try {
            // 此处调用Charset默认字符编码
            return encode(csn, ca, off, len);
        } catch (UnsupportedEncodingException x) {
            warnUnsupportedCharset(csn);
        }
        try {
        	//如果不支持则采用ISO-8859-1编码
            return encode("ISO-8859-1", ca, off, len);
        } catch (UnsupportedEncodingException x) {
            // If this code is hit during VM initialization, MessageUtils is
            // the only way we will be able to get any kind of error message.
            MessageUtils.err("ISO-8859-1 charset not available: "
                             + x.toString());
            // If we can not find ISO-8859-1 (a required encoding) then things
            // are seriously wrong with the installation.
            System.exit(1);
            return null;
        }
    }

我们再看下Charset类默认编码是什么,代码如下所示:

public static Charset defaultCharset() {
	//defaultCharset 是Charset静态volatile变量
        if (defaultCharset == null) {
            synchronized (Charset.class) {
            //这里读取属性所设置的文件编码,如果没有则默认UTF-8
                String csn = AccessController.doPrivileged(
                    new GetPropertyAction("file.encoding"));
                Charset cs = lookup(csn);
                if (cs != null)
                    defaultCharset = cs;
                else
                    defaultCharset = forName("UTF-8");
            }
        }
        return defaultCharset;
    }

从System.getProperty(“file.encoding”)获取的编码跟我在IDEA设置了jvm变量file.endcoding一致为UTF-8,如果不设置file.endcoding变量,则默认系统编码,系统编码如果不支持则默认编码UTF-8,当然java几乎没有不能支持的编码,sun包里StandardCharsets提供了一千多种编码,那么至此我们明白字符串转字节数组时默认是系统编码或者UTF-8,windows下默认gbk,linux下默认ISO8859-1。
通常我们采用AES加密会采用如下方式,

public static byte[] encrypt(String content, String password) {
  try {
    Cipher cipher = Cipher.getInstance("AES");// 创建密码器

    byte[] byteContent = content.getBytes();

    cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(password));// 初始化为加密模式的密码器

    return  cipher.doFinal(byteContent);// 加密
  } catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
  } catch (NoSuchPaddingException e) {
    e.printStackTrace();
  } catch (InvalidKeyException e) {
    e.printStackTrace();
  } catch (IllegalBlockSizeException e) {
    e.printStackTrace();
  } catch (BadPaddingException e) {
    e.printStackTrace();
  }
  return null;
}
  /**
   * 生成加密秘钥
   *
   * @return
   */
  private static SecretKeySpec getSecretKey(String password) {
    //返回生成指定算法密钥生成器的 KeyGenerator 对象
    KeyGenerator kg = null;

    try {
      kg = KeyGenerator.getInstance("AES");

      //AES 要求密钥长度为 128
      kg.init(128, new SecureRandom(password.getBytes()));

      //生成一个密钥
      SecretKey secretKey = kg.generateKey();

      return new SecretKeySpec(secretKey.getEncoded(), "AES");// 转换为AES专用密钥
    } catch (NoSuchAlgorithmException ex) {
      ex.printStackTrace();
    }

    return null;
  }

我们采用如下测试:

public static void main(String[] args) throws UnsupportedEncodingException {
  //获取默认编码
  System.out.println(System.getProperty("file.encoding"));
  String str = "abc123阿达";
  byte[] bytes = encrypt(str, "123456");
  System.out.println(bytes.length == new String(bytes).getBytes().length);
  System.out.println(bytes.length == new String(bytes, "UTF-8").getBytes("UTF-8").length);
  System.out.println(bytes.length == new String(bytes, "GBK").getBytes("GBK").length);
  System.out.println(bytes.length == new String(bytes, "ISO8859-1").getBytes("ISO8859-1").length);
}

结果如下:

UTF-8
false
false
true
true

综上所知采用sun包下的Cipher加密返回的byte[]编码方式是ISO8859-1,至此我们明白罪魁祸首是这个加密方式的默认编码

如何避免

1.加密后返回byte数组转换成16进制字符串或者base64编码字符串,如下

String str = "abc123阿达";
    System.out.println(new String(str.getBytes(),"UTF-8"));
    byte[] bytes = encrypt(str, "123456");
    String result= Hex.bytes2Hex(bytes);
    System.out.println(new String(decrypt(Hex.hexToBytes(result),"123456")));

结果如下

abc123阿达
abc123阿达

同时注意解密时要把字符串进行解码,防止下面异常

javax.crypto.BadPaddingException: Given final block not properly padded
	at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:966)
	at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:824)
	at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:436)
	at javax.crypto.Cipher.doFinal(Cipher.java:2165)

2.当加密内容调用微信等第三方时,一般第三方不会解码,所以此时我们只能对加密后byte[]转string时设置iso8859-1编码。不然第三方一直返回解密失败。