Golang 中的 AES 256 CTR 加密 使用 CryptoJS 在 Node JS 中解密,密钥是字符串(不是 WordArray)

AES 256 CTR Encryption in Golang Decrypt in Node JS with CryptoJS and the Key is String (not WordArray)

我必须使用 golang 将数据发送到具有 nodejs 加密的现有(遗留)服务,该服务将使用带有 Crypto JS 库的 AES CTR 模式解密数据。我做了一些代码如下(这个问题的密钥加密是一个随机密钥)。

Golang 加密:

func main() {
    rawKey := "46ca2a49c8074dadb99843f6b86c5975"
    data := "the quick brown fox jumps over the lazy dog"
    
    encryptedData := encrypt(rawKey, data);
    fmt.Println("encrypted data: ", encryptedData)
}

func encrypt(rawKey string, data string) string {
    key := []byte(rawKey)
    plainText := []byte(data)

    // Create new AES cipher block
    block, err := aes.NewCipher(key)
    if err != nil {
        return err.Error()
    }

    // The IV (Initialization Vector) need to be unique, but not secure.
    // Therefore, it's common to include it at the beginning of the cipher text.
    cipherText := make([]byte, aes.BlockSize+len(plainText))

    // Creates IV.
    iv := cipherText[:aes.BlockSize]
    if _, err := io.ReadFull(rand.Reader, iv); err != nil {
        return err.Error()
    }

    // Encrypt.
    encryptStream := cipher.NewCTR(block, iv)
    encryptStream.XORKeyStream(cipherText[aes.BlockSize:], plainText)

    ivHex := hex.EncodeToString(iv)
    encryptedDataHex := hex.EncodeToString(cipherText)
    return encryptedDataHex[0:len(ivHex)] + ":" + encryptedDataHex[len(ivHex):]
}

GO 游乐场:https://play.golang.com/p/2I-BTyvUBKJ 结果如下

encrypted data: c7fa927db8d5e95d7a56eaa74ccdbd6c:5c739daa892ca101c0d0f9b122721a1ccda0de473ce1a1d81b12fafa2e63965022c10036fc991d1650e900

并使用带有 CryptoJS 库的 nodejs 成功解码,代码如下

const decrypt = function (rawKey, encryptedData) {
    const split = encryptedData.split(':');
    if (split.length < 2) return '';

    const reb64 = CryptoJS.enc.Hex.parse(split[1]);
    const bytes = reb64.toString(CryptoJS.enc.Base64);
    
    const hexKey = rawKey.split("")
     .map(c => c.charCodeAt(0).toString(16).padStart(2, "0"))
     .join("");
    const hash = CryptoJS.AES.decrypt(bytes, CryptoJS.enc.Hex.parse(hexKey), {
        iv: CryptoJS.enc.Hex.parse(split[0]),
        mode: CryptoJS.mode.CTR,
        padding: CryptoJS.pad.NoPadding
    });
    const plain = hash.toString(CryptoJS.enc.Utf8);
    return plain;
}

const rawKey = '46ca2a49c8074dadb99843f6b86c5975';
const encryptedData = 'c7fa927db8d5e95d7a56eaa74ccdbd6c:5c739daa892ca101c0d0f9b122721a1ccda0de473ce1a1d81b12fafa2e63965022c10036fc991d1650e900';

const decyptedData = decrypt(rawKey, encryptedData);
document.write("decrypted data: ", decyptedData)

JS Fiddle: https://jsfiddle.net/cnq7g0vp/ 结果如下

decrypted data: the quick brown fox jumps over the lazy dog

但是nodejs中现有的(遗留)服务是解密代码直接使用字符串密钥作为密钥参数(不是WordArray)并且没有NoPadding参数如下:

const decrypt = function (rawKey, encryptedData) {
    const split = encryptedData.split(':');
    if (split.length < 2) return '';

    const reb64 = CryptoJS.enc.Hex.parse(split[1]);
    const bytes = reb64.toString(CryptoJS.enc.Base64);
    
    const hash = CryptoJS.AES.decrypt(bytes, rawKey, {
        iv: split[0],
        mode: CryptoJS.mode.CTR
    });
    const plain = hash.toString(CryptoJS.enc.Utf8);
    return plain;
}

const rawKey = '46ca2a49c8074dadb99843f6b86c5975';
const encryptedData = '3a010df5e7985f2d8b0c00e3a096347f:6036327f61cf3050fddd6ea76325148c81e170a63b514b8818afbbb894c874c87cc4c865300c7b2d0e0fd8';

const decyptedData = decrypt(rawKey, encryptedData);
document.write("decrypted data: ", decyptedData);

JS Fiddle: https://jsfiddle.net/pyntruLj/ 失败,没有结果(空字符串)如下:

decrypted data:

这里是我写的golang加密代码匹配nodejs解密代码通过使用BytesToKeyAES256CBCMD5根据我上一个问题的提示得到密钥和iv

func main() {
    rawKey := "46ca2a49c8074dadb99843f6b86c5975"
    data := "the quick brown fox jumps over the lazy dog"
    
    encryptedData := encrypt(rawKey, data);
    fmt.Println("encrypted data: ", encryptedData)
}

func encrypt(rawKey string, data string) string {
    salt := []byte("ABCDEFGH") // hardcoded at the moment

    // Gets key and IV from raw key.
    key, iv := evp.BytesToKeyAES256CBCMD5([]byte(salt), []byte(rawKey))

    plainText := []byte(data)

    // Create new AES cipher block
    block, err := aes.NewCipher(key)
    if err != nil {
        return err.Error()
    }

    cipherText := make([]byte, len(plainText))

    // Encrypt.
    encryptStream := cipher.NewCTR(block, iv)
    encryptStream.XORKeyStream(cipherText, plainText)

    ivHex := hex.EncodeToString(iv)
    encryptedDataHex := hex.EncodeToString(cipherText)
    return ivHex + ":" + encryptedDataHex
}

GO Playground:https://play.golang.com/p/luyTVhvtyOn 输出如下:

encrypted data: 3a010df5e7985f2d8b0c00e3a096347f:6036327f61cf3050fddd6ea76325148c81e170a63b514b8818afbbb894c874c87cc4c865300c7b2d0e0fd8

谁能帮我直接在 CryptoJS 解密代码上使用字符串密钥解密我的 golang 代码有什么问题(我也不能更改 nodejs 实现,因为它是遗留代码)?

代码基本没问题,只有几个小问题:

  1. CryptoJS 不会自动禁用流密码模式(如 CTR)的默认 PKCS7 填充。因此,Go代码中必须应用PKCS7 padding。
  2. 由于CryptoJS代码使用内部PBKDF,解密需要OpenSSL格式(即Salted__的ASCII编码后跟8字节salt和实际密文),十六进制编码。因此 Go 代码必须相应地格式化和编码数据。
  3. CryptoJS 在使用内部 PBKDF 时导出密钥 和 IV。因此,在 CryptoJS 代码中,指定的 IV 在解密期间被 忽略 。因此,在 Go 代码中,可以指定 any IV。

以下代码对应于您的代码,由 PKCS#7 填充和结果的 formatting/encoding 扩展(考虑代码中的注释)。请注意,与您的代码一样,为简单起见,使用了 hard-coded 盐,但实际上出于安全原因,必须应用随机生成的盐:

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "encoding/hex"
    "fmt"

    evp "github.com/walkert/go-evp"
    "github.com/zenazn/pkcs7pad"
)

func main() {
    rawKey := "46ca2a49c8074dadb99843f6b86c5975"
    data := pkcs7pad.Pad([]byte("the quick brown fox jumps over the lazy dog"), 16) // 1. Pad the plaintext with PKCS#7
    fmt.Println("padded data: ", hex.EncodeToString(data))

    encryptedData := encrypt(rawKey, data)
    fmt.Println("encrypted data: ", encryptedData)
}

func encrypt(rawKey string, plainText []byte) string {
    salt := []byte("ABCDEFGH") // hardcoded at the moment

    // Gets key and IV from raw key.
    key, iv := evp.BytesToKeyAES256CBCMD5([]byte(salt), []byte(rawKey))

    // Create new AES cipher block
    block, err := aes.NewCipher(key)
    if err != nil {
        return err.Error()
    }

    cipherText := make([]byte, len(plainText))

    // Encrypt.
    encryptStream := cipher.NewCTR(block, iv)
    encryptStream.XORKeyStream(cipherText, plainText)

    ivHex := hex.EncodeToString(iv)
    encryptedDataHex := hex.EncodeToString([]byte("Salted__")) + hex.EncodeToString(salt) + hex.EncodeToString(cipherText) // 2. Apply the OpenSSL format, hex encode the result
    return ivHex + ":" + encryptedDataHex // 3. Any value for ivHex can be used here, e.g. "00000000000000000000000000000000"
}

输出为:

padded data:  74686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f670505050505
encrypted data:  3a010df5e7985f2d8b0c00e3a096347f:53616c7465645f5f41424344454647486036327f61cf3050fddd6ea76325148c81e170a63b514b8818afbbb894c874c87cc4c865300c7b2d0e0fd82826f3d3c5

这段密文可以用遗留密码解密:

const decrypt = function (rawKey, encryptedData) {
    const split = encryptedData.split(':');
    if (split.length < 2) return '';

    const reb64 = CryptoJS.enc.Hex.parse(split[1]);
    const bytes = reb64.toString(CryptoJS.enc.Base64);
    
    const hash = CryptoJS.AES.decrypt(bytes, rawKey, {
        iv: split[0], // This is ignored if the internal PBKDF is used
        mode: CryptoJS.mode.CTR
    });
    const plain = hash.toString(CryptoJS.enc.Utf8);
    return plain;
}

const rawKey = '46ca2a49c8074dadb99843f6b86c5975';
const encryptedData = '3a010df5e7985f2d8b0c00e3a096347f:53616c7465645f5f41424344454647486036327f61cf3050fddd6ea76325148c81e170a63b514b8818afbbb894c874c87cc4c865300c7b2d0e0fd82826f3d3c5';

const decyptedData = decrypt(rawKey, encryptedData);
document.getElementById("pt").innerHTML = "decrypted data: " + decyptedData;
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<p style="font-family:'Courier New', monospace;" id="pt"></p>