2

One of our partner has a PHP website where we can connect using an API. This API requires some encrypted string to be sent in order to validate the client. They are using OpenSSL

This is the PHP code that allow them to generate their "secret"

$username = "MY_USERNAME";
$token = "V+Ylfg2XSTB4P0OD5LpuoyAzJAhApK9K+bDlLUtUlgdUzBu1ldfeBTavL7EC3v4UL4W52F0BPC9mL2mJxfURgILh0q/xpebPj2Ek3VdTQ9o5SO4WLjNx1Stg5uMYgcyd";
$key = md5($username);

// openssl
$cryptAlgo = 'AES-256-CFB';
$iv = mb_substr(base64_decode($token), 0, 16, '8bit');
$secret = base64_encode($iv.openssl_encrypt($token, $cryptAlgo, $key, 0, $iv));

echo $secret;

On our side, we are a Windows ASP.NET C# web app. How can we encrypt the same way they are doing it.

Any help would be appreciated.

EDIT: Removed irrelevant code sample.

  • 1
    Wait so... The C# code you posted isn't actually relevant to what you are doing? You should post the C# code that you tried, not the irrelevant, unchanged version. – Luke Joshua Park Mar 24 '17 at 03:53
  • @luke-park you are right, I should not have posted that code. – C. Belanger Mar 24 '17 at 19:07
  • If the web service is accessible over HTTPS, then this whole scheme is simply useless and bloated. Why not use an API key? If HTTPS is not used at all, then this is not secure at all, because the `$username` might be easily guessable which is then used as a key which any attacker can generate when they correctly guessed the username. – Artjom B. Mar 24 '17 at 19:18
  • @ArtjomB. yes they are using HTTPS. You may be right, but I do not have control on that... the token is the API key (linked to the username). – C. Belanger Mar 24 '17 at 19:42
  • Also see [C# version of OpenSSL EVP_BytesToKey method?](http://stackoverflow.com/q/8008253/608639), [AES encrypt with OpenSSL, decrypt with C# .Net](http://stackoverflow.com/q/15006717/608639) and [C version of OpenSSL EVP_BytesToKey method, not C# version](http://stackoverflow.com/q/29534656/608639) – jww Mar 25 '17 at 02:54

1 Answers1

1

I think that the OpenSSL CFB mode isn't compatible with .NET: there is a difference in the last block. OpenSSL CFB simply truncates it if it isn't full, while .NET will pad it. This code WILL work if you set the Mode = CipherMode.CBC, but will produce different result if you use the Mode = CipherMode.CFB (you can see that the last characters generated by GenerateSecret() are different). I'm giving a GenerateSecret() and a ExtractSecret() written in C#.

If someone with more experience than me can explain this difference (I've seen the explanation here, but it isn't clear how I should "solve" it).

public static string GenerateSecret(string username, string token)
{
    byte[] key;

    using (var md5 = MD5.Create())
    {
        key = md5.ComputeHash(Encoding.UTF8.GetBytes(username));
    }

    key = BytesToLowerHexBytes(key);

    var iv = Convert.FromBase64String(token);

    if (iv.Length != 16)
    {
        Array.Resize(ref iv, 16);
    }

    byte[] encrypted;

    using (var rijndael = new RijndaelManaged())
    {
        rijndael.Mode = CipherMode.CFB;
        rijndael.Padding = PaddingMode.PKCS7;
        rijndael.KeySize = 256;

        using (var msEncrypt = new MemoryStream())
        {
            using (ICryptoTransform encryptor = rijndael.CreateEncryptor(key, iv))
            using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
            {
                var buffer = Encoding.UTF8.GetBytes(token);
                csEncrypt.Write(buffer, 0, buffer.Length);
            }

            encrypted = msEncrypt.ToArray();
        }
    }

    var buffer2 = Encoding.UTF8.GetBytes(Convert.ToBase64String(encrypted));

    using (var ms = new MemoryStream(iv.Length + buffer2.Length))
    {
        ms.Write(iv, 0, iv.Length);
        ms.Write(buffer2, 0, buffer2.Length);

        return Convert.ToBase64String(ms.GetBuffer(), 0, (int)ms.Length);
    }
}

public static string ExtractSecret(string username, string encrypted)
{
    byte[] key;

    using (var md5 = MD5.Create())
    {
        key = md5.ComputeHash(Encoding.UTF8.GetBytes(username));
    }

    key = BytesToLowerHexBytes(key);

    byte[] bytes = Convert.FromBase64String(encrypted);

    byte[] iv = bytes;

    Array.Resize(ref iv, 16);

    byte[] decrypted;

    using (var rijndael = new RijndaelManaged())
    {
        rijndael.Mode = CipherMode.CFB;
        rijndael.Padding = PaddingMode.PKCS7;
        rijndael.KeySize = 256;

        using (var msDecrypt = new MemoryStream())
        {
            using (ICryptoTransform decryptor = rijndael.CreateDecryptor(key, iv))
            using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Write))
            {
                var buffer = Convert.FromBase64String(Encoding.UTF8.GetString(bytes, 16, bytes.Length - 16));
                csDecrypt.Write(buffer, 0, buffer.Length);
            }

            decrypted = msDecrypt.ToArray();
        }
    }

    return Encoding.UTF8.GetString(decrypted);
}

private static byte[] BytesToLowerHexBytes(byte[] bytes)
{
    // The hash is a hex string
    var bytes2 = new byte[bytes.Length * 2];

    for (int i = 0, j = 0; i < bytes.Length; i++)
    {
        byte b1 = (byte)(bytes[i] >> 4);
        bytes2[j] = (byte)(b1 <= 9 ? '0' + b1 : 'a' + b1 - 10);
        j++;
        byte b2 = (byte)(bytes[i] & 15);
        bytes2[j] = (byte)(b2 <= 9 ? '0' + b2 : 'a' + b2 - 10);
        j++;
    }

    return bytes2;
}

Potentially working

I'm stripping the extra bytes generated by .NET for CFB mode. The BytesToLowerHexBytes is from the previous sample. Note that I'm not expert enough in cryptography to guarantee that what I'm doing is correct.

    public static string GenerateSecret(string username, string token)
    {
        byte[] key;

        using (var md5 = MD5.Create())
        {
            key = md5.ComputeHash(Encoding.UTF8.GetBytes(username));
        }

        key = BytesToLowerHexBytes(key);

        var iv = Convert.FromBase64String(token);

        if (iv.Length != 16)
        {
            Array.Resize(ref iv, 16);
        }

        byte[] encrypted;
        int encryptedLength;

        using (var rijndael = new RijndaelManaged())
        {
            rijndael.Mode = CipherMode.CFB;
            rijndael.Padding = PaddingMode.Zeros;
            rijndael.KeySize = 256;

            using (var msEncrypt = new MemoryStream())
            {
                var buffer = Encoding.UTF8.GetBytes(token);

                using (ICryptoTransform encryptor = rijndael.CreateEncryptor(key, iv))
                using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                {
                    csEncrypt.Write(buffer, 0, buffer.Length);
                }

                // CFB is a stream cipher, where the length of the encrypted text should be
                // equal to the length of the original text... So we strip the last bytes
                encrypted = msEncrypt.GetBuffer();
                encryptedLength = buffer.Length;
            }
        }

        var buffer2 = Encoding.UTF8.GetBytes(Convert.ToBase64String(encrypted, 0, encryptedLength));

        using (var ms = new MemoryStream(iv.Length + buffer2.Length))
        {
            ms.Write(iv, 0, iv.Length);
            ms.Write(buffer2, 0, buffer2.Length);

            return Convert.ToBase64String(ms.GetBuffer(), 0, (int)ms.Length);
        }
    }

    public static string ExtractSecret(string username, string encrypted)
    {
        byte[] key;

        using (var md5 = MD5.Create())
        {
            key = md5.ComputeHash(Encoding.UTF8.GetBytes(username));
        }

        key = BytesToLowerHexBytes(key);

        byte[] bytes = Convert.FromBase64String(encrypted);

        byte[] iv = bytes;

        Array.Resize(ref iv, 16);

        byte[] decrypted;
        int decryptedLength;

        using (var rijndael = new RijndaelManaged())
        {
            rijndael.Mode = CipherMode.CFB;
            rijndael.Padding = PaddingMode.Zeros;
            rijndael.KeySize = 256;

            using (var msDecrypt = new MemoryStream())
            {
                var buffer = Convert.FromBase64String(Encoding.UTF8.GetString(bytes, 16, bytes.Length - 16));

                using (ICryptoTransform decryptor = rijndael.CreateDecryptor(key, iv))
                using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Write))
                {
                    csDecrypt.Write(buffer, 0, buffer.Length);

                    // We have to add the remaining bytes of the block. They aren't
                    // important, so we add zeroes
                    int remaining = 16 - (buffer.Length % 16);

                    if (remaining != 0)
                    {
                        csDecrypt.Write(new byte[remaining], 0, remaining);
                    }
                }

                // CFB is a stream cipher, where the length of the encrypted text should be
                // equal to the length of the original text... So we strip the last bytes
                decrypted = msDecrypt.GetBuffer();
                decryptedLength = buffer.Length;
            }
        }

        return Encoding.UTF8.GetString(decrypted, 0, decryptedLength);
    }
Community
  • 1
  • 1
xanatos
  • 109,618
  • 12
  • 197
  • 280
  • Thank you so much! I will be honest and say I only tested the GenerateSecret function from your second code and it work. – C. Belanger Mar 24 '17 at 19:31