目录

AES Java 实践

AES 算法是一种迭代的对称密钥分组密码,支持 128、192 和 256 位的加密密钥以加密和解密 128 位块中的数据。如果要加密的数据不满足 128 位的块大小要求,则必须对其进行填充。填充是将最后一个块填充到 128 位的过程。

JCE

Java Cryptography Extension(JCE)是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现。它提供对对称、不对称、块和流密码的加密支持,它还支持安全流和密封的对象。它不对外出口,用它开发完成封装后将无法调用。

JCE 密钥长度限制

加密强度的含义。它由发现密钥的难度定义,这取决于使用的密码和密钥的长度。通常,较长的密钥提供更强的加密。

  • 有限的加密强度使用最大 128 位密钥。
  • 无限制使用最大长度为 2147483647 位的密钥。

众所周知,JRE 本身包含加密功能。JCE 使用管辖策略文件来控制加密强度。策略文件由两个 jar 组成:local_policy.jarUS_export_policy.jar 。多亏了这一点,Java 平台内置了对加密强度的控制。原因很简单,一些国家需要限制加密强度。 如果某个国家/地区的法律允许无限加密强度,则可以根据 Java 版本捆绑或启用它。

检查秘钥长度限制

// 2147483647 表示无限制
int maxKeySize = javax.crypto.Cipher.getMaxAllowedKeyLength("AES");
  • Java 版本 8u151 及更早版本包含 JAVA_HOME/jre/lib/security 目录中的策略文件。
  • 从版本 8u151 开始,JRE 提供了不同的策略文件集。 因此,在 JRE 目录 JAVA_HOME/jre/lib/security/policy 中有 2 个子目录:limitedunlimited。第一个包含有限强度策略文件。第二个包含无限的。

必须用 Oracle 网站上的无限版本替换它

在 Java 版本 8u151 及更高版本中,JCE 框架默认使用无限强度策略文件。此外,还有一个系统属性控制强度。

static{
  Security.setProperty("crypto.policy", "unlimited");
}

必须在 JCE 框架初始化之前设置属性。

Java AESUtil

/**
 * Cipher 类 getInstance 方法需传递一个加密算法的名称作为参数,用来创建对应的 Cipher,其格式为 algorithm/mode/padding,即
 * 算法名称/工作模式/填充方式,例如 AES/CBC/PKCS5Padding。具体有哪些可选的加密方式,可以参考文档:
 *
 * <p>https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher
 *
 * @author Ethan Wang
 */
public class AesUtil {

  /** 指定字符集 */
  private static final String UTF_8 = StandardCharsets.UTF_8.name();

  public static final String KEY_ALGORITHM_AES = "AES";

  public static final String CIPHER_OPERATION_MODE_ECB = "AES/ECB/PKCS5Padding";
  public static final String CIPHER_OPERATION_MODE_CBC = "AES/CBC/PKCS5Padding";
  public static final String CIPHER_OPERATION_MODE_GCM = "AES/GCM/NoPadding";

  private static final SecureRandom SECURE_RANDOM = new SecureRandom();

  private AesUtil() {
    //    SECURE_RANDOM = SecureRandom.getInstance("SHA1PRNG", "SUN");
  }

  public enum KeyBitLength {
    /** 基本长度 */
    SIXTEEN(128),
    TWENTY_FIVE(192),
    THIRTY_TWO(256);

    KeyBitLength(int length) {
      this.length = length;
    }

    private final int length;

    public int getLength() {
      return length;
    }
  }

  /**
   * 密码生成 AES要求密钥的长度可以是128位16个字节、192位(25字节)或者256位(32字节), 位数越高, 加密强度自然越大, 但是加密的效率自然会低一 些, 因此要做好衡量
   *
   * @param salt 加盐
   * @param keyBitLength 密钥长度 bits
   * @return 密钥 new String(x, UTF_8) 根据需要 Base64.getEncoder().encodeToString(x)
   */
  public static byte[] generateAesKey(final String salt, KeyBitLength keyBitLength) {
    Objects.requireNonNull(salt);
    try {
      KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM_AES);
      SECURE_RANDOM.setSeed(salt.getBytes());
      kg.init(keyBitLength.getLength(), SECURE_RANDOM);
      // 产生原始对称密钥
      SecretKey secretKey = kg.generateKey();
      // key转换,根据字节数组生成AES密钥
      return secretKey.getEncoded();
    } catch (NoSuchAlgorithmException e) {
      throw new UtilException(e);
    }
  }

  /**
   * 加密
   *
   * @param data 需要加密的明文
   * @param key 加密用密钥 base64 byte[]
   * @return 加密结果 base64
   */
  public static byte[] encryptECB(byte[] data, byte[] key) {
    byte[] result;
    try {
      Cipher cipher = Cipher.getInstance(CIPHER_OPERATION_MODE_ECB);
      cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, KEY_ALGORITHM_AES));
      result = cipher.doFinal(data);
    } catch (NoSuchPaddingException
        | NoSuchAlgorithmException
        | InvalidKeyException
        | BadPaddingException
        | IllegalBlockSizeException e) {
      throw new UtilException(e);
    }
    return result;
  }

  /**
   * 解密 ECB 模式
   *
   * @param data data.getBytes(UTF_8) 如果编码 Base64.getDecoder().decode(data)
   * @param key key.getBytes(UTF_8)
   * @return 解密 new String(x, UTF_8) 根据需要 Base64.getEncoder().encodeToString(x)
   */
  public static byte[] decryptECB(byte[] data, byte[] key) {
    byte[] result;
    try {
      Cipher cipher = Cipher.getInstance(CIPHER_OPERATION_MODE_ECB);
      cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"));
      result = cipher.doFinal(data);
    } catch (NoSuchPaddingException
        | NoSuchAlgorithmException
        | InvalidKeyException
        | BadPaddingException
        | IllegalBlockSizeException e) {
      throw new UtilException(e);
    }
    return result;
  }

  /**
   * 初始向量IV, 初始向量IV的长度规定为128位16个字节, 初始向量的来源为随机生成.
   *
   * @return 结果 new String(x, UTF_8) 根据需要 Base64.getEncoder().encodeToString(x)
   */
  public static byte[] iv() {
    byte[] bytes = new byte[16];
    SECURE_RANDOM.nextBytes(bytes);
    return bytes;
  }

  /**
   * 解密时用到的密钥, 初始向量IV, 加密模式, Padding模式必须和加密时的保持一致, 否则则会解密失败.
   *
   * @param data data.getBytes(UTF_8) 如果编码 Base64.getDecoder().decode(data)
   * @param key key.getBytes(UTF_8)
   * @return 解密 new String(x, UTF_8) 根据需要 Base64.getEncoder().encodeToString(x)
   */
  public static byte[] encryptCBC(byte[] data, byte[] key, byte[] iv) {
    byte[] result;
    try {
      IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
      Cipher cipher = Cipher.getInstance(CIPHER_OPERATION_MODE_CBC);
      cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), ivParameterSpec);
      result = cipher.doFinal(data);
    } catch (NoSuchPaddingException
        | NoSuchAlgorithmException
        | InvalidKeyException
        | BadPaddingException
        | IllegalBlockSizeException
        | InvalidAlgorithmParameterException e) {
      throw new UtilException(e);
    }
    return result;
  }

  /**
   * @param data data.getBytes(UTF_8) 如果编码 Base64.getDecoder().decode(data)
   * @param key key.getBytes(UTF_8)
   * @return 解密 new String(x, UTF_8) 根据需要 Base64.getEncoder().encodeToString(x)
   */
  public static byte[] decryptCBC(byte[] data, byte[] key, byte[] iv) {
    byte[] result;

    try {
      //  CBC 模式需要用到初始向量参数
      IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
      Cipher cipher = Cipher.getInstance(CIPHER_OPERATION_MODE_CBC);
      cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), ivParameterSpec);
      result = cipher.doFinal(data);
    } catch (NoSuchPaddingException
        | NoSuchAlgorithmException
        | InvalidKeyException
        | BadPaddingException
        | IllegalBlockSizeException
        | InvalidAlgorithmParameterException e) {
      throw new UtilException(e);
    }

    return result;
  }

  /**
   * @param data data.getBytes(UTF_8) 如果编码 Base64.getDecoder().decode(data)
   * @param key key.getBytes(UTF_8)
   * @return 结果 new String(x, UTF_8) 根据需要 Base64.getEncoder().encodeToString(x)
   */
  public static byte[] encryptGCM(byte[] data, byte[] key, byte[] iv, byte[] aad) {
    byte[] result;
    try {
      Cipher cipher = Cipher.getInstance(CIPHER_OPERATION_MODE_GCM);
      cipher.init(
          Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
      cipher.updateAAD(aad);
      result = cipher.doFinal(data);
    } catch (NoSuchPaddingException
        | NoSuchAlgorithmException
        | InvalidKeyException
        | BadPaddingException
        | IllegalBlockSizeException
        | InvalidAlgorithmParameterException e) {
      throw new UtilException(e);
    }
    return result;
  }

  /**
   * @param data data.getBytes(UTF_8) 如果编码 Base64.getDecoder().decode(data)
   * @param key key.getBytes(UTF_8)
   * @return 解密 new String(x, UTF_8) 根据需要 Base64.getEncoder().encodeToString(x)
   */
  public static byte[] decryptGCM(byte[] data, byte[] key, byte[] iv, byte[] aad) {
    byte[] result;
    try {
      Cipher cipher = Cipher.getInstance(CIPHER_OPERATION_MODE_GCM);
      cipher.init(
          Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
      cipher.updateAAD(aad);
      result = cipher.doFinal(data);
    } catch (NoSuchPaddingException
        | NoSuchAlgorithmException
        | InvalidKeyException
        | BadPaddingException
        | IllegalBlockSizeException
        | InvalidAlgorithmParameterException e) {
      throw new UtilException(e);
    }
    return result;
  }
}

Java 和 CryptoJS 互相加解密问题

java 和 js 互相加解密时,如果不指定工作模式和填充方式,会失败。在 js 一般使用crypto-js库来加解密。

  • 默认的工作模式是 CryptoJS.mode.CBC,初始向量 IV 是通过 key 计算(推荐 SecureRandom 随机生成并拼接到结果开始的128bit)
  • 默认填充方式 CryptoJS.pad.Pkcs7

JCE 中 AES 支持五中模式:CBC,CFB,ECB,OFB,PCBC;支持三种填充:NoPaddingPKCS5PaddingISO10126Padding

CryptoJS 的 CryptoJS.pad.Pkcs7 与 Java 的 PKCS5Padding 兼容, 另外 BouncyCastleProvider 支持 PKCS7Padding

static {
   Security.addProvider(new BouncyCastleProvider());
}

padding

某些加密算法要求明文需要按一定长度对齐,叫做块大小(BlockSize),比如16字节,那么对于一段任意的数据,加密前需要对最后一个块填充到16 字节,解密后需要删除掉填充的数据。

  • ZeroPadding,数据长度不对齐时使用0填充,否则不填充。
  • PKCS7Padding,假设数据长度需要填充n(n>0)个字节才对齐,那么填充n个字节,每个字节都是n;如果数据本身就已经对齐了,则填充一块长度为块大小的数据,每个字节都是块大小。
  • PKCS5Padding,PKCS7Padding的子集,块大小固定为8字节。

由于使用PKCS7Padding/PKCS5Padding填充时,最后一个字节肯定为填充数据的长度,所以在解密后可以准确删除填充的数据,而使用ZeroPadding填充时,没办法区分真实数据与填充数据,所以只适合以\0结尾的字符串加解密。

js 采用AES/CBC/ZeroPadding方式加密。由于java没有提供ZeroPadding填充方式,可以使用AES/CBC/NoPadding方式解密,解密后从后往前依次剔除0x00的字节,遇到非0x00的字节时停止。剩下的字节就是解密后的结果。不过,这种方式需要注意,原始文本转byte数组后,最后一个字节不能是0x00。如果是java加密,js解密,将过程反过来即可。

key 与 iv 计算

源码中 cfg.kdf 默认是 CryptoJS.kdf.OpenSSL(即 EvpKDF)

在密码学中,密钥派生函数(英语:Key derivation function,简称:KDF)使用伪随机函数从诸如主密钥或密码的秘密值中派生出一个或多个密钥。KDF可用于将密钥扩展为更长的密钥或获取所需格式的密钥,例如将作为迪菲-赫尔曼密钥交换结果的组元素转换为用于高级加密标准(AES)的对称密钥。用于密钥派生的伪随机函数最常见的示例是密码散列函数。

推荐前后端约定好算法模式补码 指定 iv🤝

不显示指定工作模式和填充方式,会造成前后端很难调通。强烈建议前后端约定加解密算法,指定 iv。

算法模式补码推荐 AES/CBC/PKCS5Padding 测试通过,如果不行试试 BouncyCastleProvider 的 PKCS7Padding

指定 iv 可以不用 Java 写 key 生成 iv 的逻辑。

下面代码中的 key 与 iv 是 byte[] 的十六进制表示,可以用 Base64 比较短。其中 iv 必须是 32 个字符 2个十六进制字符1个byte,一共 128个bit。

var CryptoJS = require("crypto-js");

key = CryptoJS.enc.Hex.parse("3132333435363738393041424344454631323334353637383930414243444566")
iv = CryptoJS.enc.Hex.parse("30313233343536373839414243444546")
var src = "hello world";
console.log('原字符串:', src);
var enc = CryptoJS.AES.encrypt(src, key, {
  iv: iv,
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
})

console.log('加密后 base64:',enc.toString());
var enced = enc.ciphertext.toString()
console.log("加密后 16进制:", enced);
// 023c3310b87608bf92763c7bdf4914c7
var dec = CryptoJS.AES.decrypt(CryptoJS.format.Hex.parse(enced), key, {
  iv: iv,
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
})
console.log('解密:', CryptoJS.enc.Utf8.stringify(dec));
byte[] bytes =
        AesUtil.decryptCBC(
            HexUtil.hex2Bytes("023c3310b87608bf92763c7bdf4914c7"),
            HexUtil.hex2Bytes("3132333435363738393041424344454631323334353637383930414243444566"),
            HexUtil.hex2Bytes("30313233343536373839414243444546"));

    System.out.println(new String(bytes));

js AES 默认参数示例

一般上面比较通用,如果前段已经固定了默认方式。这里提供下 Java 实现的关键。

CryptoJS

npm install crypto-js
var CryptoJS = require("crypto-js");
var AES = require("crypto-js/aes");
var enc_utf8 = require("crypto-js/enc-utf8");
var enc_hex = require("crypto-js/enc-hex");
var enc_base64 = require("crypto-js/enc-base64");


var ciphertext = "message";
var encrypted = AES.encrypt(ciphertext, "Secret Passphrase");
var cryptoJsEncryped4Java =  encrypted.toString();
console.info(cryptoJsEncryped4Java);

var decrypted = AES.decrypt(encrypted, "Secret Passphrase",{
  mode: CryptoJS.mode.CBC,    //可省略
  padding: CryptoJS.pad.Pkcs7 //可省略
});
console.info(enc_utf8.stringify(decrypted));

var javaEncrypt4CryptoJs = "U2FsdGVkX1+JpiQ85Byakq6FfZ/BkiWDwy7upoN6Ie0=";
var content = AES.decrypt(javaEncrypt4CryptoJs, "Secret Passphrase");
console.info(enc_utf8.stringify(content));

vs code 运行选 node.js, 通过调试可以查看 key 与 iv 的产生算法过程。

Java

package com.ynthm.demo.jdk17.util;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.util.Arrays;
import java.util.Base64;
import java.util.Random;

/**
 * Conforming with CryptoJS AES method
 */
public class AESHelper {

  //DK 不支持 PKCS7Padding 填充 add Bouncycastle provider
  static {
    Security.addProvider(new BouncyCastleProvider());
  }

  private static int KEY_SIZE = 256;
  static int IV_SIZE = 128;
  static String HASH_CIPHER = "AES/CBC/PKCS7Padding";
  static String AES = "AES";
  static String CHARSET_TYPE = "UTF-8";
  static String KDF_DIGEST = "MD5";

  /** Seriously crypto-js, what's wrong with you? */
  static String APPEND = "Salted__";

  private AESHelper(){}

  /**
   * Encrypt
   *
   * @param password passphrase
   * @param plainText plain string
   */
  public static String encrypt(String password, String plainText)
      throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException,
          InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException,
          IllegalBlockSizeException {
    byte[] saltBytes = generateSalt();
    byte[] key = new byte[KEY_SIZE / 8];
    byte[] iv = new byte[IV_SIZE / 8];

    EvpKDF(password.getBytes(CHARSET_TYPE), KEY_SIZE, IV_SIZE, saltBytes, key, iv);

    SecretKey keyS = new SecretKeySpec(key, AES);

    Cipher cipher = Cipher.getInstance(HASH_CIPHER);
    IvParameterSpec ivSpec = new IvParameterSpec(iv);
    cipher.init(Cipher.ENCRYPT_MODE, keyS, ivSpec);
    byte[] cipherText = cipher.doFinal(plainText.getBytes(CHARSET_TYPE));

    // Thanks kientux for this: https://gist.github.com/kientux/bb48259c6f2133e628ad
    // Create CryptoJS-like encrypted !
    byte[] sBytes = APPEND.getBytes(CHARSET_TYPE);
    byte[] b = new byte[sBytes.length + saltBytes.length + cipherText.length];
    System.arraycopy(sBytes, 0, b, 0, sBytes.length);
    System.arraycopy(saltBytes, 0, b, sBytes.length, saltBytes.length);
    System.arraycopy(cipherText, 0, b, sBytes.length + saltBytes.length, cipherText.length);

    return Base64.getEncoder().encodeToString(b);
  }

  /**
   * Decrypt Thanks Artjom B. for this: http://stackoverflow.com/a/29152379/4405051
   *
   * @param password passphrase
   * @param cipherText encrypted string
   */
  public static String decrypt(String password, String cipherText)
      throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException,
          InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException,
          IllegalBlockSizeException {
    byte[] ctBytes = Base64.getDecoder().decode(cipherText.getBytes(CHARSET_TYPE));
    byte[] saltBytes = Arrays.copyOfRange(ctBytes, 8, 16);
    byte[] ciphertextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.length);
    byte[] key = new byte[KEY_SIZE / 8];
    byte[] iv = new byte[IV_SIZE / 8];

    EvpKDF(password.getBytes(CHARSET_TYPE), KEY_SIZE, IV_SIZE, saltBytes, key, iv);

    Cipher cipher = Cipher.getInstance(HASH_CIPHER);
    SecretKey keyS = new SecretKeySpec(key, AES);

    cipher.init(Cipher.DECRYPT_MODE, keyS, new IvParameterSpec(iv));
    byte[] plainText = cipher.doFinal(ciphertextBytes);
    return new String(plainText);
  }

  private static byte[] EvpKDF(
      byte[] password, int keySize, int ivSize, byte[] salt, byte[] resultKey, byte[] resultIv)
      throws NoSuchAlgorithmException {
    return EvpKDF(password, keySize, ivSize, salt, 1, KDF_DIGEST, resultKey, resultIv);
  }

  private static byte[] EvpKDF(
      byte[] password,
      int keySize,
      int ivSize,
      byte[] salt,
      int iterations,
      String hashAlgorithm,
      byte[] resultKey,
      byte[] resultIv)
      throws NoSuchAlgorithmException {
    keySize = keySize / 32;
    ivSize = ivSize / 32;
    int targetKeySize = keySize + ivSize;
    byte[] derivedBytes = new byte[targetKeySize * 4];
    int numberOfDerivedWords = 0;
    byte[] block = null;
    MessageDigest hasher = MessageDigest.getInstance(hashAlgorithm);
    while (numberOfDerivedWords < targetKeySize) {
      if (block != null) {
        hasher.update(block);
      }
      hasher.update(password);
      block = hasher.digest(salt);
      hasher.reset();

      // Iterations
      for (int i = 1; i < iterations; i++) {
        block = hasher.digest(block);
        hasher.reset();
      }

      System.arraycopy(
          block,
          0,
          derivedBytes,
          numberOfDerivedWords * 4,
          Math.min(block.length, (targetKeySize - numberOfDerivedWords) * 4));

      numberOfDerivedWords += block.length / 4;
    }

    System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4);
    System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4);
    // key + iv
    return derivedBytes;
  }

  private static byte[] generateSalt() {
    Random r = new SecureRandom();
    byte[] salt = new byte[8];
    r.nextBytes(salt);
    return salt;
  }
}
  @Test
  void decrypt() {
   	String encrypt4CryptoJs = encrypt("Secret Passphrase", "message");
    System.out.println(encrypt4CryptoJs);

    String encodeByCryptoJs = "U2FsdGVkX19Wjsb/yMflbmlXI+1GT2s9MD5byvIZyb0=";
    String decryptByJava = decrypt("Secret Passphrase", encodeByCryptoJs);
    System.out.println(decryptByJava);
  }

CryptoJS DES

#加密,key: testtest iv:abcdefgh  HexUtil.byte2hex("testtest".getBytes(StandardCharsets.UTF_8)) = 7465737474657374
echo message | openssl enc -a -des-cbc -K 7465737474657374 -iv 6162636465666768
#输出:kEiztbNAuirF25k0FAcylA==
//解密
var key = CryptoJS.enc.Utf8.parse('testtest');
var iv = CryptoJS.enc.Utf8.parse('abcdefgh');
CryptoJS.DES.decrypt("kEiztbNAuirF25k0FAcylA==", key, {
    iv : iv,
    mode: CryptoJS.mode.CBC,    //可省略
    padding: CryptoJS.pad.Pkcs7 //可省略
}).toString(CryptoJS.enc.Utf8);
//输出:message

附录

echo Hello World | base64
# SGVsbG8gV29ybGQK

echo SGVsbG8gV29ybGQK | base64 -d
# Hello World