Decrypt AES with Secret Key and IV From Node to Golang Panic

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

Decrypt AES with Secret Key and IV From Node to Golang Panic

问题

以下是使用Node.js和crypto-js库对密码进行AES加密的代码:

const crypto = require('crypto-js');

const cryptKey = 'b676eac8cf70442385dfd4bcfaa61b52';

const createRandomIv = function () {
    const keySize = 192 / 32;
    const ivSize = 128 / 32;
    const evp = crypto.algo.EvpKDF.create({ keySize: keySize + ivSize, hasher: crypto.algo.SHA1 }).compute(cryptKey);
    const iv = crypto.lib.WordArray.create(evp.words.slice(keySize), ivSize * 4);
    return iv.toString();
};

const encryptPassword = function (password) {
    const iv = createRandomIv();
    const hash = crypto.AES.encrypt(
        password,
        cryptKey, {
            iv,
            mode: crypto.mode.CTR
        }
    );
    const base64 = crypto.enc.Base64.parse(hash.toString());
    const eHex = base64.toString(crypto.enc.Hex);

    return `${iv}:${eHex}`;
};

const decryptPassword = function (encryptedPwd) {
    const split = encryptedPwd.split(':');
    if (split.length < 2) return '';
    const reb64 = crypto.enc.Hex.parse(split[1]);
    const bytes = reb64.toString(crypto.enc.Base64);
    const hash = crypto.AES.decrypt(bytes, cryptKey, {
        iv: split[0],
        mode: crypto.mode.CTR
    });
    const plain = hash.toString(crypto.enc.Utf8);
    return plain;
};

const encryptedPassword = encryptPassword("Stack Overflow");
console.log(encryptedPassword);
// 2db5c01b4825b6d4dd7a7b96f04f3bb5:53616c7465645f5f691671363cda1b9d05ee6bdd637e1e99bc3b29ef2ad7ec53

以下是使用Golang对加密密码进行解密的代码:

package main

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

func main() {
	secretKey := "b676eac8cf70442385dfd4bcfaa61b52"
	encryptedPwd := "2db5c01b4825b6d4dd7a7b96f04f3bb5:53616c7465645f5f691671363cda1b9d05ee6bdd637e1e99bc3b29ef2ad7ec53"
	split := strings.Split(encryptedPwd, ":")

	c, _ := aes.NewCipher([]byte(secretKey))
	cfbdec := cipher.NewCBCDecrypter(c, []byte(split[0]))
	plaintext := make([]byte, len(split[1]))
	cfbdec.CryptBlocks(plaintext, []byte(split[1]))
	fmt.Println(plaintext)
}

在你的Golang代码中,出现了错误。错误信息是"cipher.NewCBCDecrypter: IV length must equal block size"。这是因为你使用的IV长度不正确。

为了解决这个问题,你需要将IV转换为字节数组,并确保其长度与加密算法的块大小相等。在你的代码中,IV的长度是32个字符,但是AES算法的块大小是16字节(128位)。因此,你需要将IV从十六进制字符串解码为字节数组,并截取前16个字节作为IV。

以下是修复后的Golang代码:

package main

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

func main() {
	secretKey := "b676eac8cf70442385dfd4bcfaa61b52"
	encryptedPwd := "2db5c01b4825b6d4dd7a7b96f04f3bb5:53616c7465645f5f691671363cda1b9d05ee6bdd637e1e99bc3b29ef2ad7ec53"
	split := strings.Split(encryptedPwd, ":")

	c, _ := aes.NewCipher([]byte(secretKey))
	iv, _ := hex.DecodeString(split[0])
	iv = iv[:aes.BlockSize]
	cfbdec := cipher.NewCBCDecrypter(c, iv)
	ciphertext, _ := hex.DecodeString(split[1])
	plaintext := make([]byte, len(ciphertext))
	cfbdec.CryptBlocks(plaintext, ciphertext)
	fmt.Println(string(plaintext))
}

修复后的代码将正确解密密码,并输出可读的明文密码。

英文:

I have following code in node.js using crypto-js to encrypt password using AES with Secret Key and IV.

const crypto = require(&#39;crypto-js&#39;);
const cryptKey = &#39;b676eac8cf70442385dfd4bcfaa61b52&#39;;
const createRandomIv = function () {
const keySize = 192 / 32;
const ivSize = 128 / 32;
const evp = crypto.algo.EvpKDF.create({ keySize: keySize + ivSize, hasher: crypto.algo.SHA1 }).compute(cryptKey);
const iv = crypto.lib.WordArray.create(evp.words.slice(keySize), ivSize * 4);
return iv.toString();
};
const encryptPassword = function (password) {
const iv = createRandomIv();
const hash = crypto.AES.encrypt(
password,
cryptKey, {
iv,
mode: crypto.mode.CTR
}
);
const base64 = crypto.enc.Base64.parse(hash.toString());
const eHex = base64.toString(crypto.enc.Hex);
return `${iv}:${eHex}`;
};
const decryptPassword = function (encryptedPwd) {
const split = encryptedPwd.split(&#39;:&#39;);
if (split.length &lt; 2) return &#39;&#39;;
const reb64 = crypto.enc.Hex.parse(split[1]);
const bytes = reb64.toString(crypto.enc.Base64);
const hash = crypto.AES.decrypt(bytes, cryptKey, {
iv: split[0],
mode: crypto.mode.CTR
});
const plain = hash.toString(crypto.enc.Utf8);
return plain;
};

And here is the encrypted password from node js.

const encryptedPassword = encryptPassword(&quot;Stack Overflow&quot;);
console.log(encryptedPassword);
// 2db5c01b4825b6d4dd7a7b96f04f3bb5:53616c7465645f5f691671363cda1b9d05ee6bdd637e1e99bc3b29ef2ad7ec53

And already tried go decrpyt it using golang as follow

package main
import (
&quot;crypto/aes&quot;
&quot;crypto/cipher&quot;
&quot;fmt&quot;
&quot;strings&quot;
)
func main() {
secretKey := &quot;b676eac8cf70442385dfd4bcfaa61b52&quot;
encryptedPwd := &quot;2db5c01b4825b6d4dd7a7b96f04f3bb5:53616c7465645f5f691671363cda1b9d05ee6bdd637e1e99bc3b29ef2ad7ec53&quot;
split := strings.Split(encryptedPwd, &quot;:&quot;)
c, _ := aes.NewCipher([]byte(secretKey))
cfbdec := cipher.NewCBCDecrypter(c, []byte(split[0]))
plaintext := make([]byte, len(split[1]))
cfbdec.CryptBlocks(plaintext, []byte(split[1]))
fmt.Println(plaintext)
}

But it panics as follow.

> panic: cipher.NewCBCDecrypter: IV length must equal block size
> goroutine 1 [running]:
> crypto/cipher.NewCBCDecrypter({0x10c4ee8, 0xc000066060}, {0xc00001e040, 0x1, 0x20})

Updated 1

I updated the code to decrypted without using iv, but the result is not human readable.

split := strings.Split(encryptedPwd, &quot;:&quot;)
ciphertext, _ := hex.DecodeString(split[1])
c, _ := aes.NewCipher([]byte(secretKey))
plaintext := make([]byte, len(ciphertext))
c.Decrypt(plaintext, []byte(ciphertext))
fmt.Println(string(plaintext))

> |A/��c�*Z�S/�x

What's wrong with the decryption in my golang code, can anybody please help me? I already use the same key and iv by splitting it from the encrypted password.

答案1

得分: 1

在CryptoJS代码中,crypto.AES.encrypt()中的第二个参数被传递为字符串,因此被解释为口令。

因此,在加密过程中,首先创建一个8字节的盐,并使用KDF EVP_BytesToKey()从盐和口令派生出密钥和IV。

使用createRandomIv()派生的IV以及在crypto.AES.encrypt()中显式传递的IV都会被忽略!

hash.ToString()以OpenSSL格式返回结果,包括前缀Salted__,后跟盐和实际密文,都是Base64编码的。eHex包含相同的数据,但是以十六进制而不是Base64编码。

CryptoJS不会自动禁用流密码模式(如CTR)的填充,因此数据会使用PKCS#7进行填充,尽管对于CTR来说这是不必要的。


在Go代码中,首先需要删除不需要的IV。从剩余的数据中确定盐和密文。

通过盐和口令,可以使用evp.BytesToKeyAES256CBCMD5()检索出密钥和IV。

使用密钥和IV进行AES-CTR解密。

最后,必须删除PKCS#7填充。

以下Go代码实现了这些步骤。输入数据是使用NodeJS代码生成的:

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

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

func main() {

	// 确定盐和实际密文
	encryptedPwd := "2db5c01b4825b6d4dd7a7b96f04f3bb5:53616c7465645f5f66cbd1d539b6e51d45efded11e2211fa5e02278855dc86145d4e4891b0e25df9df96fb97a10a9f444f4519f2da4c69c430c5cbf3e9803a1f"
	split := strings.Split(encryptedPwd, ":")
	saltCiphertext, _ := hex.DecodeString(split[1])
	salt := saltCiphertext[8:16]
	ciphertext := saltCiphertext[16:]

	// 获取密钥和IV
	key, iv := evp.BytesToKeyAES256CBCMD5([]byte(salt), []byte("b676eac8cf70442385dfd4bcfaa61b52"))

	// 解密
	block, _ := aes.NewCipher(key)
	plaintext := make([]byte, len(ciphertext))
	stream := cipher.NewCTR(block, iv)
	stream.XORKeyStream(plaintext, ciphertext)

	// 取消填充
	unpaddedPlaintext := PKCS7Unpad(plaintext)

	fmt.Println("解密数据:", string(unpaddedPlaintext)) // 解密数据:The quick brown fox jumps over the lazy dog
}

func PKCS7Unpad(src []byte) []byte {
	length := len(src)
	unpadding := int(src[length-1])
	return src[:(length - unpadding)]
}

关于安全性:
CryptoJS使用EVP_BytesToKey()进行密钥和IV的派生,在今天被认为是不安全的。
更安全的替代方法是将第二个参数作为WordArray传递,这样它将被解释为密钥并直接使用。
每次加密都必须生成一个随机的IV。
可选地,可以使用可靠的密钥派生函数(如PBKDF2)与每次加密都随机生成的盐一起使用。
IV和盐(都不是秘密)应与密文连接在一起。
最好使用GCM而不是CTR作为密码模式,以便可以验证密文的真实性。

英文:

In the CryptoJS code, the second parameter in crypto.AES.encrypt() is passed as a string, so it is interpreted as passphrase.

Therefore, during encryption, an eight bytes salt is first created and from this, along with the passphrase, key and IV are derived using the KDF EVP_BytesToKey().

The IV derived with createRandomIv() and explicitly passed in crypto.AES.encrypt() is ignored!

hash.ToString() returns the result in OpenSSL format consisting of the prefix Salted__ followed by the salt and by the actual ciphertext, all Base64 encoded. eHex contains the same data, but hex instead of Base64 encoded.

CryptoJS does not automatically disable padding for stream cipher modes like CTR, so the data is padded with PKCS#7, although this would not be necessary for CTR.


In the Go code, the IV that is not required must first be removed. From the remaining data, salt and ciphertext are determined.

From salt and passphrase, key and IV can be retrieved with evp.BytesToKeyAES256CBCMD5().

With key and IV the decryption with AES-CTR can be performed.

Finally, the PKCS#7 padding must be removed.

The following Go code implements these steps. The input data was generated with the NodeJS code:

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

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

func main() {

	// Determine salt and actual ciphertext
	encryptedPwd := &quot;2db5c01b4825b6d4dd7a7b96f04f3bb5:53616c7465645f5f66cbd1d539b6e51d45efded11e2211fa5e02278855dc86145d4e4891b0e25df9df96fb97a10a9f444f4519f2da4c69c430c5cbf3e9803a1f&quot;
	split := strings.Split(encryptedPwd, &quot;:&quot;)
	saltCiphertext, _ := hex.DecodeString(split[1])
	salt := saltCiphertext[8:16]
	ciphertext := saltCiphertext[16:]

	// Get key and IV
	key, iv := evp.BytesToKeyAES256CBCMD5([]byte(salt), []byte(&quot;b676eac8cf70442385dfd4bcfaa61b52&quot;))

	// Decrypt
	block, _ := aes.NewCipher(key)
	plaintext := make([]byte, len(ciphertext))
	stream := cipher.NewCTR(block, iv)
	stream.XORKeyStream(plaintext, ciphertext)

	// Unpad
	unpaddedPlaintext := PKCS7Unpad(plaintext)

	fmt.Println(&quot;Decrypted data: &quot;, string(unpaddedPlaintext)) // Decrypted data:  The quick brown fox jumps over the lazy dog
}

func PKCS7Unpad(src []byte) []byte {
	length := len(src)
	unpadding := int(src[length-1])
	return src[:(length - unpadding)]
}

Regarding security:<br>
The derivation of key and IV performed by CryptoJS with EVP_BytesToKey() is considered insecure today.<br>
The more secure alternative would be to pass the 2nd parameter as WordArray, so that it is interpreted as key and used directly.<br>
For each encryption a random IV has to be generated.<br>
Optionally, a reliable key derivation (e.g. PBKDF2) can be used in conjunction with a salt randomly generated for each encryption.<br>
IV and salt (both not secret) are to be concatenated with the ciphertext.<br>
It is better to use GCM instead of CTR as cipher, so that the authenticity of the ciphertext can be verified.

huangapple
  • 本文由 发表于 2022年1月7日 20:53:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/70621572.html
匿名

发表评论

匿名网友

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

确定