首页  编辑  

实现CryptoJS等效的AES加解密

Tags: /Java/   Date Created:
NodeJS中,有个 crypto-js 库,提供了AES加解密的功能,示例代码如下:
const CryptoJS = require('crypto-js');
const plainText = 'Hello world!';
const passphrase = '1234';
const encrypted = CryptoJS.AES.encrypt(plainText, passphrase).toString();
console.log(encrypted);
CryptoJS.AES.decrypt(encrypted, passphrase).toString(CryptoJS.enc.Utf8);
运行效果:
其原理是:
我们知道AES加密必须要求密钥长度是16字节或者32字节,而我们调用的passphrase长度可以为任意值,CryptoJS是如何实现的呢?
原来CryptoJS实现如下:
  1. 在加密前,先根据密码派生一个加密用的密钥,然后用派生的密钥再进行AES加密。派生的时候,会首先随机生成8字节长度的salt,然后利用EVP KDF算法,结合密码,Salt,MD5算法进行一轮迭代生成派生的密钥用于AES加密。
  2. 调用AES加密算法,结合派生密钥和明文,进行加密,得到加密后的密文数据
  3. 按 常量"Salted__" + 8字节Salt + 密文数据 格式组装数据
  4. 对组装数据进行Base64编码,得到输出结果。
其对应的Java实现方法如下:
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;

public class AESUtil {

  /**
   * Conforming with CryptoJS AES method
   * **** YOU NEED TO ADD "JCE policy" to have ability to DEC/ENC 256 key-length with AES Cipher ****
   */

  // Prevent facing error when using "PKCS7Padding" in Cipher
  // add Bouncycastle provider [link=http://www.bouncycastle.org/latest_releases.html]
//  static {
//    java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
//  }

  static int KEY_SIZE = 256;
  static int IV_SIZE = 128;
  static String HASH_CIPHER = "AES/CBC/PKCS5Padding";
  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__";

  /**
   * Encrypt
   *
   * @param password  passphrase
   * @param plainText plain string
   */
  public static String encrypt(String password, String plainText) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, IllegalBlockSizeException, BadPaddingException {
    byte[] saltBytes = generateSalt(8);
    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, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {
    byte[] ctBytes = Base64.getDecoder().decode(cipherText);
    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);

    return derivedBytes; // key + iv
  }

  private static byte[] generateSalt(int length) {
    Random r = new SecureRandom();
    byte[] salt = new byte[length];
    r.nextBytes(salt);
    return salt;
  }
}


参考资料: AES encryption/decryption in crypto-js way, use KDF for generating IV and Key, use CBC with PKCS7Padding for Cipher · GitHub