浅析 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 将加密时用到的随机种子保存在加密文本中。加密文本所对应的字节数组由三部分组成:
Salted__
这八个字符对应的 ASCII 编码,占 8 个字节- 加密时用到的随机种子,占 8 个字节
- 用 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前后端加密解密,写的也很不错。