karsur
karsur

Reputation: 65

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

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

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;
};

And here is the encrypted password from node js.

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

And already tried go decrpyt it using golang as follow

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "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)
}

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, ":")
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.

Upvotes: 2

Views: 3116

Answers (1)

Topaco
Topaco

Reputation: 49121

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 (
    "crypto/aes"
    "crypto/cipher"
    "encoding/hex"
    "fmt"
    "strings"

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

func main() {

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

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

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

Upvotes: 1

Related Questions