前后端对接,多语言实现 CryptoJS 的 AES 简单加密解密

前后端对接,多语言实现 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

image-20240506105756897

翻译:CryptoJS支持AES-128、AES-192和AES-256。它会根据你传入的密钥的大小来选择变体。如果你使用密码短语,它会生成一个256位的密钥。

也就是说:

  • CryptoJS 会根据 passphrase 生成一个256位的密钥,这个秘钥才是 AES 加密的真正秘钥(256bit/8 = 32byte)
  • CryptoJS 的 AES 简单加密默认使用 aes-256

继续往下找,可以得知:加密模式默认为 CBC 填充方式默认为 Pkcs7

image-20240506105655072

得知加密模式后出现了两个问题:

  1. 秘钥 Key 是如何生成的?
  2. CBC 模式需要的偏移量 IV 是如何生成的?
用于加密的六种常见分组密码操作模式

(图片来自Wikipedia)

继续从官方文档往下翻,发现 OpenSSL 和 CryptoJS 可以互通,也就是说,CryptoJS 生成 Key 和 iv 的方式与 OpenSSL 一致

image-20240506112733824

经过多次尝试与搜索,找到这篇文章 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
}

其他参考

分组密码操作模式 - 维基百科,自由的百科全书 (wikipedia.org)

理解AES加密解密的使用方法_aes iv任意长度-CSDN博客

  • 10
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在PHP和Java之间进行AES加密和解密时出现乱码问题,可能是因为两个语言之间使用了不同的编码方式。为了解决这个问题,我们需要在两个语言之间选择一种通用的编码方式,以确保加密和解密时的数据一致。 一种通用的编码方式是Base64编码。在PHP中,可以使用base64_encode()函数将加密后的数据转换为Base64编码。在Java中,可以使用java.util.Base64类进行编码和解码。在进行解密操作之前,需要先将Base64编码的数据解码成原始二进制数据。 下面是一个PHP和Java之间进行AES加密和解密的示例代码: PHP代码: ``` $key = 'your_key'; $data = 'your_data'; // 加密 $encrypted = openssl_encrypt($data, 'AES-128-ECB', $key, OPENSSL_RAW_DATA); $base64_encrypted = base64_encode($encrypted); // 将加密后的数据传递给Java ``` Java代码: ``` import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; public class AESUtils { private static final String ALGO = "AES"; private static final String MODE = "ECB"; private static final String PADDING = "PKCS5Padding"; public static byte[] decrypt(byte[] key, byte[] encryptedData) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec(key, ALGO); Cipher cipher = Cipher.getInstance(ALGO + "/" + MODE + "/" + PADDING); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); return cipher.doFinal(encryptedData); } public static void main(String[] args) throws Exception { String keyStr = "your_key"; String encryptedStr = "your_base64_encrypted_data"; // 解码Base64编码的数据 byte[] encrypted = Base64.getDecoder().decode(encryptedStr); byte[] key = keyStr.getBytes(); // 解密 byte[] decrypted = AESUtils.decrypt(key, encrypted); String data = new String(decrypted); System.out.println(data); } } ``` 在上面的示例中,PHP代码将数据加密并转换为Base64编码,然后将其传递给Java代码。Java代码解码Base64编码的数据,使用AES算法进行解密,并将解密后的数据转换为字符串输出。注意,在实际应用中,需要确保加密和解密使用相同的密钥、算法、模式和填充方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值