1

I'm wanting to encrypt a byte[] in a C# api endpoint and decrypt the byte array in an Angular 14 app to get the base64 version of the decrypted document so I can provide it to a pdf viewer so I can view the document in the browser.

I followed this [https://stackoverflow.com/questions/63598208/encryption-in-asp-net-core-and-decryption-in-angular/75637618#75637618]. I WAS ABLE to get this to work with a simple string value, but I cannot get the decryption to work with a byte array coming from the api.

SERVER SIDE ENCRYPTION:

{bytesToEncrypt} is the byte array of a binary file I am pulling out of Sharepoint

{key} is the secret key I'm using to encrypt with

public byte[] EncryptAESData(byte[] bytesToEncrypt, string key)
    {
        byte[] keyBytes = Encoding.UTF8.GetBytes(key);
        byte[] iv = new byte[16];
        
        byte[] encryptedBytes = new byte[0];

        using (Aes aes = Aes.Create())
        {
            aes.Key = keyBytes;
            aes.IV = iv;
            aes.Padding = PaddingMode.PKCS7;
            aes.Mode = CipherMode.CBC;

            ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

            using (MemoryStream ms = new MemoryStream())
            {
                using (CryptoStream cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(bytesToEncrypt, 0, bytesToEncrypt.Length);
                }

                encryptedBytes = ms.ToArray();
            }
        }
        //return encrypted string as base64
        return encryptedBytes;
    }

CLIENT-SIDE DECRYPTION:

{key} the same key used to encrypt the byte array in the API

{bytesToDecrypt} the encrypted byte array coming from the API

decryptData(key: string, bytesToDecrypt: number[]) {

    let ciphertextArray = new CryptoJS.lib.WordArray(bytesToDecrypt);

    // Base64 encoded ciphertext, 32 bytes string as key
    var keyBytes = CryptoJS.enc.Utf8.parse(key);                     // Convert into WordArray (using Utf8)
    var iv = new CryptoJS.lib.WordArray([0x00, 0x00, 0x00, 0x00]);   // Use zero vector as IV

    let cipherParams = new CryptoJS.lib.CipherParams({
      ciphertext: ciphertextArray,
      key: keyBytes,
      iv: iv
    });


    var decryptedBytes = CryptoJS.AES.decrypt(cipherParams, keyBytes, { iv: iv }); // By default: CBC, PKCS7 
    return decryptedBytes;
}

In the angular app, I call this do decrypt the binary. It returns a WordArray

let decryptedDoc = this.decryptData(key, this.document.file);

I assumed that if I tried converting the number array to a base64 string, I'd have the decrypted binary...

let decryptedBase64 = Buffer.from(decryptedDoc.words).toString('base64');

But the encoded string is corrupt.

Any help on this would be GREATLY appreciated!

Thanks in advance.

Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
John Scott
  • 85
  • 1
  • 9
  • Have you tried `var base64 = CryptoJS.enc.Base64.stringify(words);`? you are not getting a bad padding error it seems, so that means that the decryption likely succeeded. – Maarten Bodewes Mar 06 '23 at 01:02
  • If the issue persists, post test data. – Topaco Mar 06 '23 at 10:53
  • @MaartenBodewes ...I only have Hex and UTF8 as options when I reference the CryptoJS.enc ...this is the error I get "Property 'Base64' does not exist on type '{ Utf8: typeof Utf8; Hex: typeof Hex; }'" For reference this is in an angular app...I've added this package npm i crypto-ts In my package.json: "crypto-ts": "^1.0.2" Is this even the right library?? – John Scott Mar 06 '23 at 15:38
  • Not sure if that's the right lib, would have to investigate, no time for that. Pretty sure that it has something to do with the encoding though as decryption of CBC has a very high chance of generating an error if it fails due to the padding used. So much so that it can be used for a padding oracle attack if it is used for transport security, by the way (!). – Maarten Bodewes Mar 06 '23 at 15:56

1 Answers1

0

Differences between CryptoJS and CryptoTS:

As it is stated in your comment, you are using CryptoTS. CryptoJS and CryptoTS are not quite consistent. There are e.g. the following differences:

  • With CryptoJS a WordArray is created with CryptoJS.lib.WordArray.create(), while new CryptoJS.lib.WordArray() is not defined. In CryptoTS, it is the other way around.
  • With CryptoJS, a byte array can be converted to a WordArray with create() (via a typedArray), while this is not possible in CryptoTS using the WordArray constructor.

This is demonstrated in the last section with code samples.


Byte array to WordArray conversion:

The above differences are the reasons for the failed decryption. With CryptoTS the conversion of the byte array into a WordArray must be implemented explicitly. Here such a conversion for CryptoJS can be found, which can be ported to CryptoTS accordingly:

function byteArrayToWordArray(byteArray) {
    var wordArray = [], i;
    for (i = 0; i < byteArray.length; i++) {
        wordArray[(i / 4) | 0] |= byteArray[i] << (24 - 8 * i);
    }
    return new CryptoTS.lib.WordArray(wordArray, byteArray.length);
}

// Test:
var byteArray = [0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7];
var wordArray1 = new CryptoTS.lib.WordArray(byteArray);
var wordArray2 = byteArrayToWordArray(byteArray);
console.log(wordArray1.toString(CryptoTS.enc.Hex)); // 000000a1000000a2000000a3000000a4000000a5000000a6000000a7 wrong
console.log(wordArray2.toString(CryptoTS.enc.Hex)); // a1a2a3a4a5a6a7                                           correct
<script src=" https://cdn.jsdelivr.net/npm/crypto-ts@1.0.2/bundles/crypto-ts.umd.min.js "></script>

Decryption with CryptoTS:

With this, decryption with CryptoTS works as follows:

function byteArrayToWordArray(byteArray) {
    var wordArray = [], i;
    for (i = 0; i < byteArray.length; i++) {
        wordArray[(i / 4) | 0] |= byteArray[i] << (24 - 8 * i);
    }
    return new CryptoTS.lib.WordArray(wordArray, byteArray.length);
}

var ciphertextBA = [221,58,14,190,92,182,96,22,69,219,250,192,113,254,21,153,20,172,218,254,222,251,25,174,78,16,120,126,15,252,68,106,0,230,45,160,88,68,95,14,226,21,82,149,156,127,118,44];
var key = "0123456789012345";

var keyWA= CryptoTS.enc.Utf8.parse(key); 
var ivWA = byteArrayToWordArray([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);                            
var ciphertextWA = byteArrayToWordArray(ciphertextBA);
var ciphertextCP = new CryptoTS.lib.CipherParams({ciphertext: ciphertextWA});
var decryptedWA = CryptoTS.AES.decrypt(ciphertextCP, keyWA, {iv: ivWA}); 
var decrypted = decryptedWA.toString(CryptoTS.enc.Utf8);                       
console.log(decrypted); // The quick brown fox jumps over the lazy dog
<script src=" https://cdn.jsdelivr.net/npm/crypto-ts@1.0.2/bundles/crypto-ts.umd.min.js "></script>

The ciphertext in the above example was generated using the C# code.


Base64 encoding:

CryptoTS does not seem to provide a Base64 encoder (crypto-ts, #7). A workaround is possible using Buffer and Hex encoder:

var decryptedB64 = Buffer.from(decryptedWA.toString(CryptoTS.enc.Hex), 'hex').toString('base64')

Alternatively, the WordArray can be converted directly into a byte array (e.g. with convertWordArrayToUint8Array() from here) with subsequent Base64 decoding using Buffer:

var decryptedB64 = Buffer.from(convertWordArrayToUint8Array(decryptedWA)).toString('base64')

or a helper from here.


Efficiency and security:

Consider passing the ciphertext hex encoded, as this eliminates the need for byteArrayToWordArray().

Because of the limitations of CryptoTS compared to Crypto JS, it should be considered to use CryptoJS instead of CryptoTS (if possible). With CryptoJS the decryption including Base64 encoding is simply:

var ciphertextBA = [221,58,14,190,92,182,96,22,69,219,250,192,113,254,21,153,20,172,218,254,222,251,25,174,78,16,120,126,15,252,68,106,0,230,45,160,88,68,95,14,226,21,82,149,156,127,118,44];
var key = "0123456789012345";

var keyWA= CryptoJS.enc.Utf8.parse(key); 
var ivWA = CryptoJS.lib.WordArray.create([0x00,0x00,0x00,0x00]);             
var ciphertextWA = CryptoJS.lib.WordArray.create(new Uint8Array(ciphertextBA));
var decryptedWA = CryptoJS.AES.decrypt({ciphertext: ciphertextWA}, keyWA, {iv: ivWA}); 
var decrypted = decryptedWA.toString(CryptoJS.enc.Utf8);                       
var decryptedB64 = decryptedWA.toString(CryptoJS.enc.Base64);                       
console.log(decrypted); 
console.log(decryptedB64);
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>

Note that a string as key and a static IV are vulnerabilities (but for testing it is enough).


Code samples regarding differences between CryptoJS and CryptoTS:

The differences described above can be seen, if the following script is executed once with CryptoJS:

var byteArray = [0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7];

try {
      var wordArray1 = new CryptoJS.lib.WordArray(byteArray);                      // CryptoJS: failed
    console.log("wordArray1:", wordArray1.toString(CryptoJS.enc.Hex));
} catch(e){
      console.log("wordArray1:", e.message)
}

try {
      var wordArray2 = new CryptoJS.lib.WordArray(new Uint8Array(byteArray));      // CryptoJS: failed
    console.log("wordArray2:", wordArray2.toString(CryptoJS.enc.Hex));
} catch(e){
      console.log("wordArray2:", e.message)
}

try{
    var wordArray3 = CryptoJS.lib.WordArray.create(byteArray);                     // CryptoJS: wrong result
    console.log("wordArray3:", wordArray3.toString(CryptoJS.enc.Hex));
} catch(e){
      console.log("wordArray3:", e.message)
}

try{
      var wordArray4 = CryptoJS.lib.WordArray.create(new Uint8Array(byteArray));   // CryptoJS: correct result
      console.log("wordArray4:", wordArray4.toString(CryptoJS.enc.Hex));
} catch(e){
      console.log("wordArray4:", e.message)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>

and once with CryptoTS:

var byteArray = [0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7];

try {
      var wordArray1 = new CryptoTS.lib.WordArray(byteArray);                       // CryptoTS: wrong result
    console.log("wordArray1:", wordArray1.toString(CryptoTS.enc.Hex));
} catch(e){
      console.log("wordArray1:", e.message)
}

try {
      var wordArray2 = new CryptoTS.lib.WordArray(new Uint8Array(byteArray));       // CryptoTS: wrong result
    console.log("wordArray2:", wordArray2.toString(CryptoTS.enc.Hex));
} catch(e){
      console.log("wordArray2:", e.message)
}

try{
    var wordArray3 = CryptoTS.lib.WordArray.create(byteArray);                      // CryptoTS:  failed
    console.log("wordArray3:", wordArray3.toString(CryptoTS.enc.Hex));
} catch(e){
      console.log("wordArray3:", e.message)
}

try{
      var wordArray4 = CryptoTS.lib.WordArray.create(new Uint8Array(byteArray));    // CryptoTS: failed
      console.log("wordArray4:", wordArray4.toString(CryptoTS.enc.Hex));
} catch(e){
      console.log("wordArray4:", e.message)
}
<script src=" https://cdn.jsdelivr.net/npm/crypto-ts@1.0.2/bundles/crypto-ts.umd.min.js "></script>
Topaco
  • 40,594
  • 4
  • 35
  • 62