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

huangapple go评论117阅读模式
英文:

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

问题

我必须使用Golang将数据发送到现有(遗留)服务,并使用Node.js加密进行解密,该服务使用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)

    // 创建新的AES密码块
    block, err := aes.NewCipher(key)
    if err != nil {
        return err.Error()
    }

    // IV(初始化向量)需要是唯一的,但不需要是安全的。
    // 因此,将其包含在密文开头是常见的做法。
    cipherText := make([]byte, aes.BlockSize+len(plainText))

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

    // 加密。
    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 Playground: https://play.golang.com/p/2I-BTyvUBKJ

运行结果如下:

encrypted data:
c7fa927db8d5e95d7a56eaa74ccdbd6c:5c739daa892ca101c0d0f9b122721a1ccda0de473ce1a1d81b12fafa2e63965022c10036fc991d1650e900

成功使用以下代码在Node.js中使用CryptoJS库进行解密:

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 decryptedData = decrypt(rawKey, encryptedData);
document.write("decrypted data: ", decryptedData);

JS Fiddle: https://jsfiddle.net/cnq7g0vp/

运行结果如下:

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

但是,现有的Node.js服务解密代码直接使用字符串密钥作为密钥参数(而不是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 decryptedData = decrypt(rawKey, encryptedData);
document.write("decrypted data: ", decryptedData);

JS Fiddle: https://jsfiddle.net/pyntruLj/

运行结果为空字符串,解密失败:

decrypted data:

为了使Golang加密代码能够使用字符串密钥直接解密,请根据我之前问题中的提示,使用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") // 目前硬编码

    // 从原始密钥获取密钥和IV。
    key, iv := evp.BytesToKeyAES256CBCMD5([]byte(salt), []byte(rawKey))

    plainText := []byte(data)

    // 创建新的AES密码块
    block, err := aes.NewCipher(key)
    if err != nil {
        return err.Error()
    }

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

    // 加密。
    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

有人可以帮助我找出我的Golang代码有什么问题,以便能够使用字符串密钥直接解密CryptoJS解密代码(由于这是遗留代码,我无法更改Node.js实现)吗?

英文:

I have to send data by using golang to existing (legacy) service with nodejs encryption that will decrypt data using AES CTR mode with Crypto JS libray. I have made some code as follow (the key encryption is a random key in this question).

Golang Encryption:

func main() {
	rawKey := &quot;46ca2a49c8074dadb99843f6b86c5975&quot;
	data := &quot;the quick brown fox jumps over the lazy dog&quot;
	
	encryptedData := encrypt(rawKey, data);
	fmt.Println(&quot;encrypted data: &quot;, 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&#39;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)] + &quot;:&quot; + encryptedDataHex[len(ivHex):]
}

GO Playground: https://play.golang.com/p/2I-BTyvUBKJ
and the result is as follows

> encrypted data:
> c7fa927db8d5e95d7a56eaa74ccdbd6c:5c739daa892ca101c0d0f9b122721a1ccda0de473ce1a1d81b12fafa2e63965022c10036fc991d1650e900

and successfully decode with nodejs with CryptoJS library with following code

const decrypt = function (rawKey, encryptedData) {
    const split = encryptedData.split(&#39;:&#39;);
    if (split.length &lt; 2) return &#39;&#39;;

    const reb64 = CryptoJS.enc.Hex.parse(split[1]);
    const bytes = reb64.toString(CryptoJS.enc.Base64);
    
    const hexKey = rawKey.split(&quot;&quot;)
     .map(c =&gt; c.charCodeAt(0).toString(16).padStart(2, &quot;0&quot;))
     .join(&quot;&quot;);
    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 = &#39;46ca2a49c8074dadb99843f6b86c5975&#39;;
const encryptedData = &#39;c7fa927db8d5e95d7a56eaa74ccdbd6c:5c739daa892ca101c0d0f9b122721a1ccda0de473ce1a1d81b12fafa2e63965022c10036fc991d1650e900&#39;;

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

JS Fiddle: https://jsfiddle.net/cnq7g0vp/ and the result is as follows

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

But the existing (legacy) service in nodejs is decryption code is using the string key directly as key parameter (not WordArray) and without NoPadding parameter as follows:

const decrypt = function (rawKey, encryptedData) {
    const split = encryptedData.split(&#39;:&#39;);
    if (split.length &lt; 2) return &#39;&#39;;

    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 = &#39;46ca2a49c8074dadb99843f6b86c5975&#39;;
const encryptedData = &#39;3a010df5e7985f2d8b0c00e3a096347f:6036327f61cf3050fddd6ea76325148c81e170a63b514b8818afbbb894c874c87cc4c865300c7b2d0e0fd8&#39;;

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

JS Fiddle: https://jsfiddle.net/pyntruLj/ and it failed with no result (empty string) as follows:

> decrypted data:

and here is what I write in golang encryption code to match the nodejs decryption code by using BytesToKeyAES256CBCMD5 to get the key and the iv based on the hint on my previous question

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

func encrypt(rawKey string, data string) string {
	salt := []byte(&quot;ABCDEFGH&quot;) // 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 + &quot;:&quot; + encryptedDataHex
}

GO Playground: https://play.golang.com/p/luyTVhvtyOn and the output is as follows:

> encrypted data:
> 3a010df5e7985f2d8b0c00e3a096347f:6036327f61cf3050fddd6ea76325148c81e170a63b514b8818afbbb894c874c87cc4c865300c7b2d0e0fd8

Can anyone help me what's wrong with my golang code to be able to be decrypted by using string key directly on CryptoJS decrypt code (also I cant change the nodejs implemention since it's a legacy code)?

答案1

得分: 1

代码大部分都没问题,只有一些小问题:

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

以下代码是根据你的代码进行修改,添加了PKCS#7填充以及结果的格式化/编码(请考虑代码中的注释)。请注意,为了简单起见,代码中使用了硬编码的盐,但在实际应用中,出于安全原因,必须使用随机生成的盐:

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. 使用PKCS#7填充明文
	fmt.Println("填充后的数据: ", hex.EncodeToString(data))

	encryptedData := encrypt(rawKey, data)
	fmt.Println("加密后的数据: ", encryptedData)
}

func encrypt(rawKey string, plainText []byte) string {
	salt := []byte("ABCDEFGH") // 目前是硬编码的

	// 从原始密钥获取密钥和IV
	key, iv := evp.BytesToKeyAES256CBCMD5([]byte(salt), []byte(rawKey))

	// 创建新的AES密码块
	block, err := aes.NewCipher(key)
	if err != nil {
		return err.Error()
	}

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

	// 加密
	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. 应用OpenSSL格式,对结果进行十六进制编码
	return ivHex + ":" + encryptedDataHex // 3. 这里可以使用任何值作为ivHex,例如"00000000000000000000000000000000"
}

输出结果为:

填充后的数据:  74686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f670505050505
加密后的数据:  3a010df5e7985f2d8b0c00e3a096347f:53616c7465645f5f41424344454647486036327f61cf3050fddd6ea76325148c81e170a63b514b8818afbbb894c874c87cc4c865300c7b2d0e0fd82826f3d3c5

可以使用以下旧代码解密此密文:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

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], // 如果使用内部PBKDF,则此项将被忽略
        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 = "解密后的数据: " + decyptedData;

<!-- language: lang-html -->

&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"&gt;&lt;/script&gt;
&lt;p style="font-family:'Courier New', monospace;" id="pt"&gt;&lt;/p&gt;

<!-- end snippet -->

英文:

The code is mostly OK, there are just a few minor issues:

  1. CryptoJS does not automatically disable the default PKCS7 padding for stream cipher modes like CTR. Therefore, PKCS7 padding must be applied in the Go code.
  2. Since the CryptoJS code uses the internal PBKDF, the OpenSSL format is required for decryption (i.e. the ASCII encoding of Salted__ followed by the 8 bytes salt and the actual ciphertext), hex encoded. So the Go code must format and encode the data accordingly.
  3. CryptoJS derives key and IV when using the internal PBKDF. Therefore, in the CryptoJS code, the specified IV is ignored during decryption. Hence, in the Go code, any IV can be specified.

The following code corresponds to your code, extended by the PKCS#7 padding and the formatting/encoding of the result (consider the comments in the code). Note that, as in your code, a hard-coded salt is used for simplicity, but in practice a randomly generated salt must be applied for security reasons:

package main

import (
	&quot;crypto/aes&quot;
	&quot;crypto/cipher&quot;
	&quot;encoding/hex&quot;
	&quot;fmt&quot;

	evp &quot;github.com/walkert/go-evp&quot;
	&quot;github.com/zenazn/pkcs7pad&quot;
)

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

	encryptedData := encrypt(rawKey, data)
	fmt.Println(&quot;encrypted data: &quot;, encryptedData)
}

func encrypt(rawKey string, plainText []byte) string {
	salt := []byte(&quot;ABCDEFGH&quot;) // 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(&quot;Salted__&quot;)) + hex.EncodeToString(salt) + hex.EncodeToString(cipherText) // 2. Apply the OpenSSL format, hex encode the result
	return ivHex + &quot;:&quot; + encryptedDataHex // 3. Any value for ivHex can be used here, e.g. &quot;00000000000000000000000000000000&quot;
}

The output is:

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

This ciphertext can be decrypted with the legacy code:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

const decrypt = function (rawKey, encryptedData) {
const split = encryptedData.split(&#39;:&#39;);
if (split.length &lt; 2) return &#39;&#39;;
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 = &#39;46ca2a49c8074dadb99843f6b86c5975&#39;;
const encryptedData = &#39;3a010df5e7985f2d8b0c00e3a096347f:53616c7465645f5f41424344454647486036327f61cf3050fddd6ea76325148c81e170a63b514b8818afbbb894c874c87cc4c865300c7b2d0e0fd82826f3d3c5&#39;;
const decyptedData = decrypt(rawKey, encryptedData);
document.getElementById(&quot;pt&quot;).innerHTML = &quot;decrypted data: &quot; + decyptedData;

<!-- language: lang-html -->

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js&quot;&gt;&lt;/script&gt;
&lt;p style=&quot;font-family:&#39;Courier New&#39;, monospace;&quot; id=&quot;pt&quot;&gt;&lt;/p&gt;

<!-- end snippet -->

huangapple
  • 本文由 发表于 2022年3月25日 16:30:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/71614033.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定