前后端对接,多语言实现 CryptoJS 的 AES 简单加密解密
前言
在逆向某个网站接口时,发现的其参数使用了 CryptoJS 的 AES 简单加密,只需要两个参数,而在使用其他语言解密时却无法复现
var encrypted = CryptoJS.AES.encrypt("Message", "Secret Passphrase");
var decrypted = CryptoJS.AES.decrypt(encrypted, "Secret Passphrase");
折腾了好久,查阅了多篇文章,终于解决
为什么要用多语言实现?因为参考的文章使用的是 Go ,自己测试的时候为了方便用 Python,最后的业务用的是 Java
解密思路
解密时会出现第一个问题:AES 的秘钥长度是固定的,而 CryptoJS.AES 的 passphrase
却是随意长度
首先,在 Crypto 官网 得知 CryptoJS 的 AES 简单加密默认使用 aes-256
翻译:CryptoJS支持AES-128、AES-192和AES-256。它会根据你传入的密钥的大小来选择变体。如果你使用密码短语,它会生成一个256位的密钥。
也就是说:
- CryptoJS 会根据
passphrase
生成一个256位的密钥,这个秘钥才是 AES 加密的真正秘钥(256bit/8 = 32byte) - CryptoJS 的 AES 简单加密默认使用
aes-256
继续往下找,可以得知:加密模式默认为 CBC
,填充方式默认为 Pkcs7
得知加密模式后出现了两个问题:
- 秘钥 Key 是如何生成的?
- CBC 模式需要的偏移量 IV 是如何生成的?
(图片来自Wikipedia)
继续从官方文档往下翻,发现 OpenSSL 和 CryptoJS 可以互通,也就是说,CryptoJS 生成 Key 和 iv 的方式与 OpenSSL 一致
经过多次尝试与搜索,找到这篇文章 AES解密中IV值的默认选择方法 (我的很多思路都从这里来,包括后面的 go 解密代码),之后又根据文章内容找到了 OpenSSL AES 算法中 Key 和 IV 是如何生成的? 这篇文章在博客园也有转载 https://www.cnblogs.com/findumars/p/12627336.html
从这两篇文章得到了解密的方式
hash1 = MD5(Passphrase + Salt)
hash2 = MD5(hash1 + Passphrase + Salt)
hash3 = MD5(hash2 + Passphrase + Salt)
Key = hash1 + hash2
IV = hash3
先使用 Base64 将加密字符串解码,会发现其开头为 "Salted__",在这个前缀后面的八个字节就是 salt,使用该 salt 与 Passphrase 根据上述计算方式可以计算出 Key 和 IV
加密思路
模拟这个“简单加密”:解密思路已经有了,加密与解密思路相反即可
先随机生成 8 字节的 salt,根据 salt 生成 Key 和 IV
用生成的 Key 和 IV 加密得到密文,密文拼接 "Salted__" 和 salt
最后使用 Base64 加密得到加密结果
代码过程
以下代码仅供参考
Go 实现
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"encoding/base64"
"errors"
)
// AES256Decode : 解密函数 参考:https://www.cnblogs.com/caiawo/p/17255857.html
func AES256Decode(encodeStr string, passphrase string) (string, error) {
// base64Decode
ciphertext, err := base64.StdEncoding.DecodeString(encodeStr)
if err != nil {
return "", err
}
salt := ciphertext[8:16]
// 这个方法里面是上面所说的伪代码部分
calKey, iv := getKeyAndIv(salt, passphrase)
block, err := aes.NewCipher(calKey)
if err != nil {
return "", err
}
mode := cipher.NewCBCDecrypter(block, iv)
// 去除前缀与salt
ciphertext = ciphertext[16:]
plaintext := make([]byte, len(ciphertext))
mode.CryptBlocks(plaintext, ciphertext)
// 去除填充
paddingLen := int(plaintext[len(plaintext)-1])
if paddingLen > len(plaintext) {
return "", errors.New("padding len error")
}
return string(plaintext[:len(plaintext)-paddingLen]), nil
}
// AES256Encode : 加密函数
func AES256Encode(plaintext string, passphrase string) (string, error) {
// 将明文编码为字节串
textBytes := []byte(plaintext)
// 进行填充
blockSize := aes.BlockSize
padding := blockSize - len(textBytes)%blockSize
paddedText := append(textBytes, bytes.Repeat([]byte{byte(padding)}, padding)...)
// 获取 salt
salt := make([]byte, 8) // 生成一个长度为8的随机字节串作为salt
if _, err := rand.Read(salt); err != nil {
return "", err
}
// 使用 salt 和密钥生成 key 和 iv
calKey, iv := getKeyAndIv(salt, passphrase)
// 创建 AES 加密器
block, err := aes.NewCipher(calKey)
if err != nil {
return "", err
}
// 创建加密模式
mode := cipher.NewCBCEncrypter(block, iv)
// 加密
cipherText := make([]byte, len(paddedText))
mode.CryptBlocks(cipherText, paddedText)
// 添加 Salted__ 和 salt 前缀
cipherText = append([]byte("Salted__"), append(salt, cipherText...)...)
// 返回 base64 编码的密文
return base64.StdEncoding.EncodeToString(cipherText), nil
}
// 获取 Key 和 IV
func getKeyAndIv(salt []byte, passphrase string) (calKey []byte, iv []byte) {
hash1 := md5.Sum([]byte(passphrase + string(salt)))
hash2 := md5.Sum(append(hash1[:], []byte(passphrase+string(salt))...))
hash3 := md5.Sum(append(hash2[:], []byte(passphrase+string(salt))...))
calKey = append(hash1[:], hash2[:]...)
iv = hash3[:]
return
}
func main() {
plaintext := "Hello, world"
passphrase := "dTUvPrClrXwho&%q]N+*ZDF*O]OZAo"
encode, _ := AES256Encode(plaintext, passphrase)
println(encode) // U2FsdGVkX1+vx7KHqsuhaFnv7ADSZEkIEZYdAdhzIso=
decode, _ := AES256Decode(encode, passphrase)
println(decode) // Hello, world
}
Python 实现
依赖
# AES 加密解密库
pip install pycryptodome
代码
"""
模拟 CryptoJS, 复现 AES 加密解密
"""
import base64
import hashlib
import os
from typing import Tuple
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.Padding import unpad
def get_key_and_iv(salt: bytes, passphrase: str) -> Tuple[bytes, bytes]:
"""
根据 salt 和秘钥生成 key 和 iv
:param salt: salt
:param passphrase: 密码短语
:return: key, iv
"""
passphrase_bytes = passphrase.encode('utf-8')
hash1 = hashlib.md5(passphrase_bytes + salt).digest()
hash2 = hashlib.md5(hash1 + passphrase_bytes + salt).digest()
hash3 = hashlib.md5(hash2 + passphrase_bytes + salt).digest()
key = hash1 + hash2
iv = hash3
return key, iv
def encrypt_AES_CBC(plaintext: str, passphrase: str) -> str:
"""
CBC 模式的 AES 加密
:param plaintext: 明文
:param passphrase: 密码短语
:return: 加密后的结果字符串
"""
# 将明文编码为字节串
text_bytes = plaintext.encode('utf-8')
# 进行填充
padded_text = pad(text_bytes, AES.block_size)
# 获取 salt
salt = os.urandom(8) # 生成一个长度为8的随机字节串作为salt
# 获取 key 和 iv
key, iv = get_key_and_iv(salt, passphrase)
# 创建 CBC 模式的 AES 加密器
aes = AES.new(key, AES.MODE_CBC, iv)
# AES 加密
cipher_text = aes.encrypt(padded_text)
# 添加 Salted__ 和 salt 前缀
cipher_text = b"Salted__" + salt + cipher_text
# 返回 base64 编码的密文
return base64.b64encode(cipher_text).decode('utf-8')
def decrypt_AES_CBC(ciphertext: str, passphrase: str) -> str:
"""
CBC 模式的 AES 解密
:param ciphertext: 加密文本
:param passphrase: 密码短语
:return: 解密后的结果字符串
"""
# 将 base64 编码的密文解码为字节串
ciphertext = base64.b64decode(ciphertext)
# 从密文中提取 salt
salt = ciphertext[8:16]
# 获取 iv 和 key
key, iv = get_key_and_iv(salt, passphrase)
# 去除前缀与 salt
ciphertext = ciphertext[16:]
# 创建 CBC 模式的 AES 解密器
aes = AES.new(key, AES.MODE_CBC, iv)
# AES 解密
blob_ciphertext = aes.decrypt(ciphertext)
# 去除填充
return unpad(blob_ciphertext, AES.block_size).decode('utf-8')
plaintext = 'Hello, world'
passphrase = "dTUvPrClrXwho&%q]N+*ZDF*O]OZAo"
encrypt = encrypt_AES_CBC(plaintext, passphrase)
print("Encrypted text: ", encrypt) # U2FsdGVkX19II99DvAA6quSaWbcMjE1vvg13hZyCpqw=
decrypt = decrypt_AES_CBC(encrypt, passphrase)
print("Decrypted text: ", decrypt) # Hello, world
Java 实现
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
/**
* 模拟 CryptoJS AES 加密解密
*
* @author soladuor
*/
public class AnalogCryptoJS {
private static final Charset utf8 = StandardCharsets.UTF_8;
/**
* 辅助方法:连接多个字节数组
*
* @param arrays byte 数组
* @return 连接后的 byte 数组
*/
public static byte[] connectByteArray(byte[]... arrays) {
int length = 0;
for (byte[] array : arrays) {
length += array.length;
}
byte[] result = new byte[length];
int offset = 0;
for (byte[] array : arrays) {
// 参数:原数组,复制起始点,结果数组,粘贴起始点,复制的长度
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
return result;
}
/**
* 辅助方法:生成长度为 8 的随机 salt
*/
public static byte[] generateSalt() {
// SecureRandom 是比 Random 更满足加密要求的强随机数生成器
SecureRandom random = new SecureRandom();
byte[] salt = new byte[8];
random.nextBytes(salt);
return salt;
}
/**
* 根据 salt 和秘钥生成 key 和 iv
*
* @param salt salt
* @param passphrase 密码短语
* @return 由 key 和 iv 组成的数组
*/
public static byte[][] getKeyAndIv(byte[] salt, String passphrase) throws NoSuchAlgorithmException {
byte[] passphraseBytes = passphrase.getBytes(utf8);
MessageDigest md5 = MessageDigest.getInstance("MD5");
// 计算 MD5 哈希值
byte[] hash1 = md5.digest(connectByteArray(passphraseBytes, salt));
byte[] hash2 = md5.digest(connectByteArray(hash1, passphraseBytes, salt));
byte[] hash3 = md5.digest(connectByteArray(hash2, passphraseBytes, salt));
// 生成 key 和 iv
// key = hash1 + hash2
byte[] key = connectByteArray(hash1, hash2);
// iv = hash3
byte[] iv = hash3;
return new byte[][]{key, iv};
}
/**
* CBC 模式的 AES 加密
*
* @param plaintext 明文
* @param passphrase 密码短语
* @return 加密后的结果字符串
*/
public static String encrypt_AES_CBC(String plaintext, String passphrase) {
try {
// 将明文编码为字节串
byte[] textBytes = plaintext.getBytes(utf8);
// 进行填充
// paddedText = pad(text_bytes, AES.block_size)
// 获取 salt
byte[] salt = generateSalt(); // 生成长度为 8 的随机 salt
// 获取 key 和 iv
byte[][] keyAndIV = getKeyAndIv(salt, passphrase);
byte[] key = keyAndIV[0];
byte[] iv = keyAndIV[1];
// 创建 CBC 模式的 AES 加密器
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
// AES 加密
byte[] cipherText = cipher.doFinal(textBytes);
// 添加 Salted__ 和 salt 前缀
byte[] saltedCipherText = connectByteArray("Salted__".getBytes(utf8), salt, cipherText);
// 返回 base64 编码的密文
return Base64.getEncoder().encodeToString(saltedCipherText);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* CBC 模式的 AES 解密
*
* @param ciphertext 加密文本
* @param passphrase 密码短语
* @return 解密后的结果字符串
*/
public static String decrypt_AES_CBC(String ciphertext, String passphrase) {
try {
// 将 base64 编码的密文解码为字节串
byte[] ciphertextBytes = Base64.getDecoder().decode(ciphertext);
// 从密文中提取 salt
byte[] salt = Arrays.copyOfRange(ciphertextBytes, 8, 16);
// 获取 iv 和 key
byte[][] keyAndIV = getKeyAndIv(salt, passphrase);
byte[] key = keyAndIV[0];
byte[] iv = keyAndIV[1];
// 去除前缀与 salt
byte[] ciphertextWithoutSalt = Arrays.copyOfRange(ciphertextBytes, 16, ciphertextBytes.length);
// 创建 AES 解密器
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// AES 解密(自动去除填充)
byte[] decryptedBytes = cipher.doFinal(ciphertextWithoutSalt);
// 去除填充,转为 utf-8 字符串
return new String(decryptedBytes, utf8);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
测试
public static void main(String[] args) {
String plaintext = "Hello, world";
String passphrase = "dTUvPrClrXwho&%q]N+*ZDF*O]OZAo";
String encrypt = AnalogCryptoJS.encrypt_AES_CBC(plaintext, passphrase);
System.out.println("hello-enc = " + encrypt); // U2FsdGVkX18zrOB/HZlV5DeZirUDqu5lTvRh7iXf6nM=
String decrypt = AnalogCryptoJS.decrypt_AES_CBC(encrypt, passphrase);
System.out.println("hello-dec = " + decrypt); // Hello, world
}