1

I am trying to write client code that requests for information from an external API over the web.

The API is simple enough (to me) except for the stipulations of how to generate the authorisation key. First some context: There are 6 string values required to start with:

  • token
  • password
  • devId
  • salt
  • orgId
  • givenKey

Now for the encryption stuff. First up SHA2.

hashedString = SHA2(token + password + devId)

Followed by AES.

authKey = AES(salt + orgId + "=" + hashedString)

The AES parameters are specified as follows:

  • Mode = ECB
  • Padding = PKCS5Padding
  • Secret Key = givenKey

My problem is that I know next to nothing about cryptography.

Below is the code I have attempting to accomplish the above.

// Generate Authorisation key
            byte[] fieldsBytes = Encoding.ASCII.GetBytes(token + password + devId);
            byte[] keyBytes = Encoding.ASCII.GetBytes(secretKey);
            SHA512 shaM = new SHA512Managed();
            string hashedFields = Encoding.ASCII.GetString(shaM.ComputeHash(fieldsBytes));
            byte[] encryptedBytes = EncryptStringToBytes_Aes(salt + orgId + "=" + hashedfields,
                keyBytes, keyBytes);
            string encryptedString = Encoding.ASCII.GetString(encryptedBytes);


private byte[] EncryptStringToBytes_Aes(string plainText, byte[] Key, byte[] IV)
        {
            // Check arguments.
            if (plainText == null || plainText.Length <= 0)
                throw new ArgumentNullException("plainText");
            if (Key == null || Key.Length <= 0)
                throw new ArgumentNullException("Key");
            if (IV == null || IV.Length <= 0)
                throw new ArgumentNullException("IV");
            byte[] encrypted;

            // Create an Aes object
            // with the specified key and IV.
            using (Aes aesAlg = Aes.Create())
            {
                aesAlg.Key = Key;
                aesAlg.IV = IV;
                aesAlg.Padding = PaddingMode.PKCS7;
                aesAlg.Mode = CipherMode.ECB;

                // Create an encryptor to perform the stream transform.
                ICryptoTransform encryptor = aesAlg.CreateEncryptor();

                // Create the streams used for encryption.
                using (MemoryStream msEncrypt = new MemoryStream())
                {
                    using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                        {
                            //Write all data to the stream.
                            swEncrypt.Write(plainText);
                        }
                        encrypted = msEncrypt.ToArray();
                    }
                }
            }

            // Return the encrypted bytes from the memory stream.
            return encrypted;
        }

This code gets a "401" from the external service.

My first issue is that there does not seem to be a NET method named SHA2. The closest I could find is SHA512 and I am not sure if SHA512 is a .NET implementation of SHA2.

Secondly, padding for AES has been specified as PKCS5Padding but again the closest (naming-wise) I could find is PKCS7 which I am not sure about how similar it is to PKCS5.

There is also the matter of an Initialisation Vector (IV), which the AES parameters don't specify but I see C# AES examples including. In the code, I have set it to have the same value as the Key (which I believe is what the API calls "secret key") out of sheer desperation but I have tried making the request without setting IV to any value and still get back a 401.

I should probably also mention that I am using ASCII encoding to convert to-and-from bytes because I first tried using UTF8 but when it came to actually making the HTTP request, I was getting an exception saying that header values (remember we are generating an authorisation key that will be tucked in a HTTP request header) should only be encoded in ASCII.

Any help pointing me in the right direction will be immensely appreciated as I am woefully out of my depth with this cryptography stuff.

Steve S
  • 509
  • 1
  • 11
  • 24

2 Answers2

1

Don't worry, crypto can feel overwhelmingly complicated. I think you're close.

  1. SHA2 is a family of hash functions. In practice, "SHA2" usually means SHA2-256 or occasionally SHA2-512. My guess is that your external API is probably using the more common SHA2-256.

  2. This answer on crypto.stackexchange explains that PKCS#5 is essentially a subset of PKCS#7. I'd be willing to bet that the API you're calling made the same mistake described in that answer and should really be calling it PKCS7Padding. Your code is fine!

  3. The IV isn't the same thing as the secret key (or just the "key" for AES). The IV should be random for every encryption run. You aren't supposed to derive it from the input plaintext or the input key. Fortunately, AesCryptoServiceProvider.GenerateIV() will generate one for you. It's up to you to prepend it to your output stream, though.

  4. Using Encoding.ASCII.GetBytes() to get the plaintext and secret key bytes makes sense to me. I don't think that's causing a problem.

Stealing from this excellent answer to a similar question (go give them a vote!), I'd try code like this:

static byte[] AesEncrypt(byte[] data, byte[] key)
{
    if (data == null || data.Length <= 0)
    {
        throw new ArgumentNullException($"{nameof(data)} cannot be empty");
    }

    if (key == null || key.Length != AesKeySize)
    {
        throw new ArgumentException($"{nameof(key)} must be length of {AesKeySize}");
    }

    using (var aes = new AesCryptoServiceProvider
    {
        Key = key,
        Mode = CipherMode.CBC,
        Padding = PaddingMode.PKCS7
    })
    {
        aes.GenerateIV();
        var iv = aes.IV;
        using (var encrypter = aes.CreateEncryptor(aes.Key, iv))
        using (var cipherStream = new MemoryStream())
        {
            using (var tCryptoStream = new CryptoStream(cipherStream, encrypter, CryptoStreamMode.Write))
            using (var tBinaryWriter = new BinaryWriter(tCryptoStream))
            {
                // prepend IV to data
                cipherStream.Write(iv);
                tBinaryWriter.Write(data);
                tCryptoStream.FlushFinalBlock();
            }
            var cipherBytes = cipherStream.ToArray();

            return cipherBytes;
        }
    }
}

Unless there's something else weird going on with this API, I'd guess it's probably #3 above that is causing your request to fail.

Nate Barbettini
  • 51,256
  • 26
  • 134
  • 147
  • I have already left the office for the day. I will definitely try this first thing once I am back in the office but I must say you've already made it look a lot less intimidating. :) Thank you very much. – Steve S Sep 16 '19 at 23:00
  • I finally found a solution and posted it. Thanks for all your help in steering me towards the right direction. – Steve S Sep 20 '19 at 21:32
  • @SteveS Glad you got it sorted! – Nate Barbettini Sep 21 '19 at 02:18
1

I finally managed to get it to work.

A big part of the problem was that API documentation did not specify that hashedString has to be composed of hexadecimal characters. It also didn't say that authKey should be a base64 string. I don't know if this is so standard that it goes without saying but knowing this could have saved me hours of agony. I was converting the hashed/encrypted bytes back to ASCII and much of it was unprintable characters that were causing the server to send back a HTTP response with status 400 BAD_REQUEST.

It also required hashedString to be hashed using SHA256 but the documentation does not mention it. Thanks to @Nate Barbettini's answer for steering me in the right direction on this.

Also, it appears that AES ECB mode does not require an initialisation vector unlike other modes like CBC so I didn't specify an IV.

For padding I specified PKCS7 (again thanks to @Nate Barbettini for that).

With that here's the code that finally worked out for me.

 string hashedFields = ComputeSha256HashHex(authToken + password + devId);

 string encryptedString = AesEncryptToBase64String(saltString + orgId + "=" + hashedFields, secretKey);

        private string AesEncryptToBase64String(string plainText, string key)
        {
            // Convert string arguments into byte arrays
            byte[] keyBytes = Encoding.ASCII.GetBytes(key);
            byte[] plainTextBytes = Encoding.ASCII.GetBytes(plainText);

            // Check arguments.
            if (plainText == null || plainText.Length <= 0)
                throw new ArgumentNullException("plainText");
            if (key == null || key.Length <= 0)
                throw new ArgumentNullException("key");
            byte[] encrypted;

            // Create an Aes object
            // with the specified key and IV.
            using (Aes aesAlg = Aes.Create())
            {
                aesAlg.Key = keyBytes;
                aesAlg.Padding = PaddingMode.PKCS7;
                aesAlg.Mode = CipherMode.ECB;

                // Create an encryptor to perform the stream transform.
                ICryptoTransform encryptor = aesAlg.CreateEncryptor();

                // Create the streams used for encryption.
                using (MemoryStream msEncrypt = new MemoryStream())
                {
                    using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                        {
                            //Write all data to the stream.
                            swEncrypt.Write(plainText);
                        }
                        encrypted = msEncrypt.ToArray();
                    }
                }
            }

            // Return encrypted bytes as Base 64 string
            return Convert.ToBase64String(encrypted);
        }


        private string ComputeSha256HashHex(string plainText)
        {
            using (SHA256 sha256Hash = SHA256.Create())
            {
                // ComputeHash - returns byte array  
                byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(plainText));

                // Convert byte array to a string   
                return BytesToHexString(bytes);
            }
        }


        private string BytesToHexString(byte[] bytes)
        {
            // Convert byte array to a string   
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < bytes.Length; i++)
            {
                builder.Append(bytes[i].ToString("x2"));
            }
            return builder.ToString();
        }
Steve S
  • 509
  • 1
  • 11
  • 24