0

I've been breaking my head over this for a few days. To make an API call I have to encrypt some data. The receiving people gave me this code snippet to use for the encryption.

<?php
# Config
$algo = 'AES-256-GCM';
$hmac = 'SHA3-256';
$key = 'PUBLIC-KEY-HERE';
$encoded_key = hash_pbkdf2($hmac, substr($key, 0, 16), substr($key, 16), 4096, 32, TRUE);
$tagLength = 16;
$checksumlength = 64;
# Decrypt
$ciphertext = base64_decode("4T9Rm2FSRUrfkVH58Vd+oLCvwqOtVRKyHz8pDGMzM2IyZTNkZWE1NmFlNjQxMDgyNzZhYjIyOWNhZWI1YmU5OTc4N2U0OWVkYjY3ZjQ1NjZmYTI3NDQ3MzY3NzYAEHU4a3+czd8YSQ==");
$ivLength = openssl_cipher_iv_length('$AES-256-GCM');
$iv = substr($ciphertext, 0, $ivLength);
$tag = substr($ciphertext, $ivLength, $tagLength);
$checksum = substr($ciphertext, $ivLength + $tagLength, $checksumlength);
$payload = substr($ciphertext, $ivLength + $tagLength + $checksumlength);
try {
    $result = openssl_decrypt($payload, $algo, $encoded_key, OPENSSL_RAW_DATA, $iv, $tag);
}
catch(Exception $e) {
    echo $e->getMessage();
}
var_dump($result);
if(hash_hmac($hmac, $result, $key) == $checksum) {
    echo "<p>Valid decryption</p>";
}
# Encrypt
$payload = 'Lorum Ipsum';
$iv = openssl_random_pseudo_bytes($ivLength);
$checksum = hash_hmac($hmac, $payload, $key);
$tag= null;
$encrypted_text = openssl_encrypt($payload, $algo, $encoded_key, OPENSSL_RAW_DATA, $iv, $tag, '', $tagLength);
$result = base64_encode($iv . $tag . $checksum . $encrypted_text);
var_dump($result);

This is what I've made

        public static string EncryptUsingAes(string plainText, byte[] Key, byte[] IV)
        {
            // Check arguments.
            if (plainText == null || plainText.Length <= 0)
                throw new ArgumentNullException(nameof(plainText));
            if (Key == null || Key.Length <= 0)
                throw new ArgumentNullException(nameof(Key));
            if (IV == null || IV.Length <= 0)
                throw new ArgumentNullException(nameof(IV));
            

            // with the specified key and IV.
            var keyParam = new KeyParameter(Key);
            var parameters = new ParametersWithIV(keyParam, IV);

            var cipher = new GcmBlockCipher(new AesEngine());
            cipher.Init(true, parameters);

            var plainTextBytes = Encoding.UTF8.GetBytes(plainText);

            var ciphertext = new byte[cipher.GetOutputSize(plainTextBytes.Length)];
            var len = cipher.ProcessBytes(plainTextBytes, 0, plainTextBytes.Length, ciphertext, 0);
            cipher.DoFinal(ciphertext, len);

            var tag = cipher.GetMac();
            var checksum = GeneratePBKDF2Key(Encoding.UTF8.GetString(Key), plaintext);
            var combinedBytes = CombineByteArrays(IV, tag, checksum, ciphertext);
            var result = Convert.ToBase64String(combinedBytes);
            
            return result;
        }

        public static string DecryptUsingAes(string encrypted, byte[] Key)
        {
            var decoded = Convert.FromBase64String(encrypted);
            const int ivLength = 12;
            const int tagLength = 16;
            const int checksumLength = 32; //64 bytes
            var iv = new byte[ivLength];
            var tag = new byte[tagLength];
            var checksum = new byte[checksumLength];
            var encryptedText = new byte[decoded.Length - ivLength - tagLength - checksumLength];

            Buffer.BlockCopy(decoded, 0, iv, 0, ivLength);
            Buffer.BlockCopy(decoded, ivLength, tag, 0, tagLength);
            Buffer.BlockCopy(decoded, ivLength + tagLength, checksum, 0, checksumLength);
            Buffer.BlockCopy(decoded, ivLength + tagLength + checksumLength, encryptedText, 0, encryptedText.Length);


            var keyParam = new KeyParameter(Key);
            var parameters = new ParametersWithIV(keyParam, iv);

            var cipher = new GcmBlockCipher(new AesEngine());
            cipher.Init(false, parameters);
            
            var plaintextBytes = new byte[cipher.GetOutputSize(encryptedText.Length)];
            var len = cipher.ProcessBytes(encryptedText, 0, encryptedText.Length, plaintextBytes, 0);
            cipher.DoFinal(plaintextBytes, len);

            var checksum2 = GeneratePBKDF2Key(Encoding.UTF8.GetString(Key), Encoding.UTF8.GetString(plaintextBytes));

            Console.WriteLine(checksum2.SequenceEqual(checksum)
                ? "The encrypted data is valid."
                : "The encrypted data is invalid.");

            return Encoding.UTF8.GetString(plaintextBytes);
        }

        public static byte[] CombineByteArrays(params byte[][] arrays)
        {
            var totalLength = 0;
            foreach (var array in arrays)
            {
                totalLength += array.Length;
            }

            var combinedArray = new byte[totalLength];
            var offset = 0;
            foreach (var array in arrays)
            {
                Buffer.BlockCopy(array, 0, combinedArray, offset, array.Length);
                offset += array.Length;
            }

            return combinedArray;
        }

        public static byte[] getIv()
        {
            var random = new SecureRandom();
            var iv = new byte[12];
            random.NextBytes(iv);
            return iv;
        }

        public static byte[] GeneratePBKDF2Key(string key, string plaintext)
        {
            var keyBytes = Encoding.UTF8.GetBytes(key);
            var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
            IDigest digest = new Sha3Digest(256);
            var keyParameter1 = new KeyParameter(keyBytes);
            var hmac = new HMac(digest);
            hmac.Init(keyParameter1);

            var checksum = new byte[hmac.GetMacSize()];
            hmac.BlockUpdate(plaintextBytes, 0, plaintextBytes.Length);
            hmac.DoFinal(checksum, 0);
            return checksum;
        }

I have to use bouncycastle given the .NET version we are using.

I am able te encrypt and decrypt the message. But I get a "Cannot decrypt message" error when I do the API call.

I've searched the documentation of boucycastle, checked Stack Overflow, I've even asked ChatGPT to help with the code, but I can't seem to get it to work.

EDIT after some searching and trying I have come to this, it uses the PBKDF2 correctly now and it seems the tag is appended on the ciphertext, so now i pop it off en set it elsewhere.

        public static string EncryptUsingAes(string plainText, byte[] Key, byte[] IV)
        {
            // Check arguments.
            if (plainText == null || plainText.Length <= 0)
                throw new ArgumentNullException(nameof(plainText));
            if (Key == null || Key.Length <= 0)
                throw new ArgumentNullException(nameof(Key));
            if (IV == null || IV.Length <= 0)
                throw new ArgumentNullException(nameof(IV));
            

            // with the specified key and IV.
            var unused = Convert.ToBase64String(Key);
            var hashedKey = GeneratePBKDF2Hash(Key);
            var keyParam = new KeyParameter(hashedKey);
            var parameters = new AeadParameters(keyParam, 128, IV);
            var hasedKeyString = Convert.ToBase64String(hashedKey);

            var cipher = new GcmBlockCipher(new AesEngine());
            cipher.Init(true, parameters);

            var IVstring = Convert.ToBase64String(IV);

            var plainTextBytes = Encoding.UTF8.GetBytes(plainText);

            var ciphertextTag = new byte[cipher.GetOutputSize(plainTextBytes.Length)];
            var len = cipher.ProcessBytes(plainTextBytes, 0, plainTextBytes.Length, ciphertextTag, 0);
            cipher.DoFinal(ciphertextTag, len);

            var tag = ciphertextTag.Skip(ciphertextTag.Length - 16).Take(16).ToArray();
            var ciphertext = ciphertextTag.Take(ciphertextTag.Length - 16).ToArray();
            var TESTTAG = cipher.GetMac();
            var checksum = GenerateHmacSha3_256(Key, ciphertext);
            var combinedBytes = CombineByteArrays(IV, tag, checksum, ciphertext);
            
            var result = Convert.ToBase64String(combinedBytes);
            
            return result;
        }

        public static string DecryptUsingAes(string encrypted, byte[] Key)
        {
            var decoded = Convert.FromBase64String(encrypted);
            const int ivLength = 12;
            const int tagLength = 16;
            const int checksumLength = 64; 
            var iv = new byte[ivLength];
            var tag = new byte[tagLength];
            var checksum = new byte[checksumLength];
            var encryptedText = new byte[decoded.Length - ivLength - tagLength - checksumLength];

            Buffer.BlockCopy(decoded, 0, iv, 0, ivLength);
            Buffer.BlockCopy(decoded, ivLength, tag, 0, tagLength);
            Buffer.BlockCopy(decoded, ivLength + tagLength, checksum, 0, checksumLength);
            Buffer.BlockCopy(decoded, ivLength + tagLength + checksumLength, encryptedText, 0, encryptedText.Length);

            var ciphertext = Convert.ToBase64String(encryptedText);
            var ivtext = Convert.ToBase64String(iv);

            //var generator = GeneratorUtilities.GetKeyGenerator("AES");
            //generator.Init(new KeyGenerationParameters(new SecureRandom(), 256));
            var hashedkey = GeneratePBKDF2Hash(Key);
            var keyParam = new KeyParameter(hashedkey);
            var parameters = new AeadParameters(keyParam, 128, iv);
            var hashedKeyString = Convert.ToBase64String(hashedkey);

                var cipher = new GcmBlockCipher(new AesEngine());
            cipher.Init(false, parameters);

            var encryptedTextTag = CombineByteArrays(encryptedText, tag);
            var plaintextBytes = new byte[cipher.GetOutputSize(encryptedTextTag.Length)];
            var len = cipher.ProcessBytes(encryptedTextTag, 0, encryptedTextTag.Length, plaintextBytes, 0);
            cipher.DoFinal(plaintextBytes, len);

            var checksum2 = GenerateHmacSha3_256(Key, encryptedText);

            Console.WriteLine(checksum2.SequenceEqual(checksum)
                ? "The encrypted data is valid."
                : "The encrypted data is invalid.");

            return Encoding.UTF8.GetString(plaintextBytes);
        }

        public static byte[] CombineByteArrays(params byte[][] arrays)
        {
            var totalLength = 0;
            foreach (var array in arrays)
            {
                totalLength += array.Length;
            }

            var combinedArray = new byte[totalLength];
            var offset = 0;
            foreach (var array in arrays)
            {
                Buffer.BlockCopy(array, 0, combinedArray, offset, array.Length);
                offset += array.Length;
            }

            return combinedArray;
        }

        public static byte[] getIv()
        {
            var random = new SecureRandom();
            var iv = new byte[12];
            random.NextBytes(iv);
            return iv;
        }


    

        public static byte[] GeneratePBKDF2Hash(byte[] key)
        {
            int iterationCount = 4096;
            int keySize = 32;
            string hmacAlgorithm = "SHA3-256";

            // Split the key into two parts: first 16 bytes and the remaining bytes
            var keyPart1 = new byte[16];
            var keyPart2 = new byte[key.Length - 16];
            Buffer.BlockCopy(key, 0, keyPart1, 0, 16);
            Buffer.BlockCopy(key, 16, keyPart2, 0, key.Length - 16);

            // Create a digest based on the specified HMAC algorithm
            IDigest digest;
            if (hmacAlgorithm.Equals("SHA3-256", StringComparison.OrdinalIgnoreCase))
                digest = new Sha3Digest(256);
            else
                throw new NotSupportedException("Unsupported HMAC algorithm.");

            // Generate the PBKDF2 parameters
            var generator = new Pkcs5S2ParametersGenerator(digest);
            generator.Init(keyPart1, keyPart2, iterationCount);

            // Generate the derived key
            var keyParameter = (KeyParameter)generator.GenerateDerivedParameters(keySize * 8);
             
            var unused = Convert.ToBase64String(keyParameter.GetKey());

            return keyParameter.GetKey();
        }

        public static byte[] GenerateHmacSha3_256(byte[] key, byte[] payload)
        {
            // Create a SHA3-256 digest
            IDigest digest = new Sha3Digest(256);

            // Create an HMAC generator with the digest and key
            var hmac = new HMac(digest);
            hmac.Init(new KeyParameter(key));

            // Compute the HMAC
            hmac.BlockUpdate(payload, 0, payload.Length);
            var hmacBytes = new byte[hmac.GetMacSize()];
            hmac.DoFinal(hmacBytes, 0);

            var unused = Convert.ToBase64String(hmacBytes);

            return hmacBytes;
        }
Basje313
  • 29
  • 5
  • 2
    Note how in your PHP you're using Base64 to get the ciphertext to decrypt... you're not doing that in C#. I suspect you want `var ciphertext = Convert.FromBase64String(plainText);` – Jon Skeet Jul 04 '23 at 07:22
  • Please note that, in general, translating code between languages isn't something you can ask of volunteers . See this [meta post](https://meta.stackoverflow.com/questions/296119/is-how-do-i-convert-code-from-this-language-to-this-language-too-broad) – Ken Lee Jul 04 '23 at 07:39
  • 1
    `GeneratePBKDF2Key()` seems to implement the checksum determination and not (as the name suggests) the key derivation via PBKDF2. The latter seems to be missing, or did you forget to post it? – Topaco Jul 04 '23 at 07:44
  • 1
    I suggest you debug step by step to ensure the input and output of each step are the same. – shingo Jul 04 '23 at 07:48
  • 1
    Besides the missing PBKDF2 key derivation, another bug is that `ciphertext` in the C# code is the concatenation of actual ciphertext and tag, unlike the PHP code where `$encrypted_text` is merely the actual ciphertext, resulting in different concatenations. – Topaco Jul 04 '23 at 11:38
  • @Topaco I added/edited the changes you suggested, but to no avail. I am closer to the solution now though. – Basje313 Jul 04 '23 at 14:23
  • 1
    Key derivation and separation of the ciphertext/tag look OK at first glance after the last changes. However, there are still problems when determining the MAC (`GenerateHmacSha3_256()`): The PHP code uses the plaintext (and not the ciphertext). Also, in the PHP code, when concatenating, (the ASCII encoding of) the hex encoded MAC is used (and not the raw MAC). – Topaco Jul 04 '23 at 14:29
  • When I generate the checksum, it always comes back as 32 bytes. The checksum they send is 64 bytes. I can't see why that is, as we seem to be using the same hashing method. – Basje313 Jul 04 '23 at 15:34
  • As already mentioned in my last comment in the last sentence: Change in `EncryptUsingAes()`: `var checksum = Encoding.ASCII.GetBytes(Convert.ToHexString(GenerateHmacSha3_256(Key, plainTextBytes)))`. In case of further problems you can find a C# implementation [here](https://dotnetfiddle.net/xxc3nM), which is equivalent to the PHP code [here](https://paiza.io/projects/Z--vJy0eyuoOHtwQMe8Lnw). – Topaco Jul 04 '23 at 17:20

0 Answers0