绝顶之上

会当凌绝顶,一览众山小。

浅析 CryptoJS.AES 加解密过程

发布日期:

AES 算法(Rijndael 算法)在进行加解密时需要用到密钥(Key)和初始向量(IV)。然而,用 CryptoJS 提供的 AES 算法只需要用到一个密码(Secret Passphrase)。CryptoJS 提供的 AES 加解密方法非常简单:

var encrypted = CryptoJS.AES.encrypt("Message", "Secret Passphrase");​
var decrypted = CryptoJS.AES.decrypt(encrypted, "Secret Passphrase");

起先我认为,所谓的密码就是密钥,初始向量取 0 向量,经过程序编写,可以确定,用于 CryptoJS.AES 算法的密码(Secret Passphrase)并不等同于密钥(Key)或者初始向量(IV)。这说明上述方法与传统的 AES 加密存在差别。如果想要在其他编程环境下,实现一致的加解密方法,就需要了解其加解密过程。

查阅资料得知,CryptoJS.AES 算法中的密钥和初始向量由密码推导得到。这个推导过程用到了 BytesToKey 函数。关于该函数,可以参见 EVP_BytesToKey。简单来说,通过 BytesToKey 函数,选定种子(Salt)和密码,就可以推导出密钥和初始向量。

CryptoJS.AES 用 Base64 编码来表示最终得到的加密文本。为了确保解密过程可以推导出相同的密钥和初始向量,CryptoJS.AES 将加密时用到的随机种子保存在加密文本中。加密文本所对应的字节数组由三部分组成:

  1. Salted__ 这八个字符对应的 ASCII 编码,占 8 个字节
  2. 加密时用到的随机种子,占 8 个字节
  3. 用 AES 算法(Rijndael 算法,CBC 模式,256 位密钥,128 位块)加密得到的字节数组

知道了加密逻辑,解密过程就容易实现了。将 Base64 编码的加密文本转换成字符数组,取出随机种子(8~16),加上密码,推导得到密钥和初始向量,用 AES 算法(Rijndael 算法,CBC 模式,256 位密钥,128位块)解密(16~)即得到原始文本。

需要指出的是,上述算法逻辑分析,并非直接来源于 CryptoJS.AES 的源码解读,而是来源于 Stack Overflow 问答区里面的同功能 C# 实现,在此表示衷心感谢。鉴于 Stack Overflow 的访问速度,将这部分代码搬运过来,如下:

public class Protection
{
    public string OpenSSLEncrypt(string plainText, string passphrase)
    {
        // generate salt
        byte[] key, iv;
        byte[] salt = new byte[8];
        RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
        rng.GetNonZeroBytes(salt);
        DeriveKeyAndIV(passphrase, salt, out key, out iv);
        // encrypt bytes
        byte[] encryptedBytes = EncryptStringToBytesAes(plainText, key, iv);
        // add salt as first 8 bytes
        byte[] encryptedBytesWithSalt = new byte[salt.Length + encryptedBytes.Length + 8];
        Buffer.BlockCopy(Encoding.ASCII.GetBytes("Salted__"), 0, encryptedBytesWithSalt, 0, 8);
        Buffer.BlockCopy(salt, 0, encryptedBytesWithSalt, 8, salt.Length);
        Buffer.BlockCopy(encryptedBytes, 0, encryptedBytesWithSalt, salt.Length + 8, encryptedBytes.Length);
        // base64 encode
        return Convert.ToBase64String(encryptedBytesWithSalt);
    }

    public string OpenSSLDecrypt(string encrypted, string passphrase)
    {
        // base 64 decode
        byte[] encryptedBytesWithSalt = Convert.FromBase64String(encrypted);
        // extract salt (first 8 bytes of encrypted)
        byte[] salt = new byte[8];
        byte[] encryptedBytes = new byte[encryptedBytesWithSalt.Length - salt.Length - 8];
        Buffer.BlockCopy(encryptedBytesWithSalt, 8, salt, 0, salt.Length);
        Buffer.BlockCopy(encryptedBytesWithSalt, salt.Length + 8, encryptedBytes, 0, encryptedBytes.Length);
        // get key and iv
        byte[] key, iv;
        DeriveKeyAndIV(passphrase, salt, out key, out iv);
        return DecryptStringFromBytesAes(encryptedBytes, key, iv);
    }

    private static void DeriveKeyAndIV(string passphrase, byte[] salt, out byte[] key, out byte[] iv)
    {
        // generate key and iv
        List<byte> concatenatedHashes = new List<byte>(48);

        byte[] password = Encoding.UTF8.GetBytes(passphrase);
        byte[] currentHash = new byte[0];
        MD5 md5 = MD5.Create();
        bool enoughBytesForKey = false;
        // See http://www.openssl.org/docs/crypto/EVP_BytesToKey.html#KEY_DERIVATION_ALGORITHM
        while (!enoughBytesForKey)
        {
            int preHashLength = currentHash.Length + password.Length + salt.Length;
            byte[] preHash = new byte[preHashLength];

            Buffer.BlockCopy(currentHash, 0, preHash, 0, currentHash.Length);
            Buffer.BlockCopy(password, 0, preHash, currentHash.Length, password.Length);
            Buffer.BlockCopy(salt, 0, preHash, currentHash.Length + password.Length, salt.Length);

            currentHash = md5.ComputeHash(preHash);
            concatenatedHashes.AddRange(currentHash);

            if (concatenatedHashes.Count >= 48)
                enoughBytesForKey = true;
        }

        key = new byte[32];
        iv = new byte[16];
        concatenatedHashes.CopyTo(0, key, 0, 32);
        concatenatedHashes.CopyTo(32, iv, 0, 16);

        md5.Clear();
        md5 = null;
    }

    static byte[] EncryptStringToBytesAes(string plainText, byte[] key, byte[] iv)
    {
        // Check arguments.
        if (plainText == null || plainText.Length <= 0)
            throw new ArgumentNullException("plainText");
        if (key == null || key.Length <= 0)
            throw new ArgumentNullException("key");
        if (iv == null || iv.Length <= 0)
            throw new ArgumentNullException("iv");

        // Declare the stream used to encrypt to an in memory
        // array of bytes.
        MemoryStream msEncrypt;

        // Declare the RijndaelManaged object
        // used to encrypt the data.
        RijndaelManaged aesAlg = null;

        try
        {
            // Create a RijndaelManaged object
            // with the specified key and IV.
            aesAlg = new RijndaelManaged { Mode = CipherMode.CBC, KeySize = 256, BlockSize = 128, Key = key, IV = iv };

            // Create an encryptor to perform the stream transform.
            ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);

            // Create the streams used for encryption.
            msEncrypt = new MemoryStream();
            using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
            {
                using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                {

                    //Write all data to the stream.
                    swEncrypt.Write(plainText);
                    swEncrypt.Flush();
                    swEncrypt.Close();
                }
            }
        }
        finally
        {
            // Clear the RijndaelManaged object.
            if (aesAlg != null)
                aesAlg.Clear();
        }

        // Return the encrypted bytes from the memory stream.
        return msEncrypt.ToArray();
    }

    static string DecryptStringFromBytesAes(byte[] cipherText, byte[] key, byte[] iv)
    {
        // Check arguments.
        if (cipherText == null || cipherText.Length <= 0)
            throw new ArgumentNullException("cipherText");
        if (key == null || key.Length <= 0)
            throw new ArgumentNullException("key");
        if (iv == null || iv.Length <= 0)
            throw new ArgumentNullException("iv");

        // Declare the RijndaelManaged object
        // used to decrypt the data.
        RijndaelManaged aesAlg = null;

        // Declare the string used to hold
        // the decrypted text.
        string plaintext;

        try
        {
            // Create a RijndaelManaged object
            // with the specified key and IV.
            aesAlg = new RijndaelManaged {Mode = CipherMode.CBC, KeySize = 256, BlockSize = 128, Key = key, IV = iv};

            // Create a decrytor to perform the stream transform.
            ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
            // Create the streams used for decryption.
            using (MemoryStream msDecrypt = new MemoryStream(cipherText))
            {
                using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                {
                    using (StreamReader srDecrypt = new StreamReader(csDecrypt))
                    {
                        // Read the decrypted bytes from the decrypting stream
                        // and place them in a string.
                        plaintext = srDecrypt.ReadToEnd();
                        srDecrypt.Close();
                    }
                }
            }
        }
        finally
        {
            // Clear the RijndaelManaged object.
            if (aesAlg != null)
                aesAlg.Clear();
        }

        return plaintext;
    }
}

另外,在网上也找到了一个 Python 的例子:16: vue + crypto-js + python前后端加密解密,写的也很不错。