Home > Software design >  AES 256 CTR Encryption in Golang Decrypt in Node JS with CryptoJS and the Key is String (not WordArr
AES 256 CTR Encryption in Golang Decrypt in Node JS with CryptoJS and the Key is String (not WordArr

Time:03-28

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 := "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 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(':');
    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/ 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(':');
    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/ 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 := "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 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)?

CodePudding user response:

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 (
    "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"
}

The output is:

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

This ciphertext can be decrypted with the legacy code:

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>

  • Related