Search code examples
c#rubycryptographyencryption-symmetric

AES encryption and decryption between C# and Ruby


I'm currently working on a project where I need to port AES encryption between C# to Ruby and also provide backward compatibility. While both of them work well independently, I am facing an issue while encrypting data in C# and decrypting the same in Ruby.

While I have a gut feeling that there might be an issue with the way data is converted to string in ruby, I'm not sure about this as I'm not an expert in this field (SECURITY).

Any guidance on what needs to be corrected in the ruby code to decrypt encrypted text in C# will be helpful.

Below is my C# Code.

public class Encryption
{
    private const string SECRET = "readasecret";
    static byte[] KEY = new byte[] { 222, 11, 149, 155, 122, 97, 170, 8, 40, 250, 67, 227, 129, 147, 159, 81, 108, 136, 221, 41, 247, 146, 114, 133, 232, 31, 33, 196, 130, 88, 136, 238 };
    private static readonly byte[] Salt = Encoding.ASCII.GetBytes("o6MKe324346722kbM7c5");

    public static string Encrypt(string nonCrypted)
    {
        return EncryptStringAES(nonCrypted ?? string.Empty, SECRET);
    }

    public static string Decrypt(string encrypted)
    {
        return DecryptStringAES(encrypted, SECRET);
    }

    private static string EncryptStringAES(string plainText, string sharedSecret)
    {
        //if (string.IsNullOrEmpty(plainText))
        //   throw new ArgumentNullException("plainText");
        if (string.IsNullOrEmpty(sharedSecret))
            throw new ArgumentNullException("sharedSecret");

        string outStr;                  // Encrypted string to return
        RijndaelManaged aesAlg = null;  // RijndaelManaged object used to encrypt the data.

        try
        {
            // generate the key from the shared SECRET and the salt
            var key = new Rfc2898DeriveBytes(sharedSecret, Salt);

            // Create a RijndaelManaged object
            aesAlg = new RijndaelManaged();
            aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);

            // Create a decryptor to perform the stream transform.
            ICryptoTransform encryptor = aesAlg.CreateEncryptor(KEY, aesAlg.IV);
            // Create the streams used for encryption.
            using (var msEncrypt = new MemoryStream())
            {
                // prepend the IV
                msEncrypt.Write(BitConverter.GetBytes(aesAlg.IV.Length), 0, sizeof(int));
                msEncrypt.Write(aesAlg.IV, 0, aesAlg.IV.Length);
                using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                {
                    using (var swEncrypt = new StreamWriter(csEncrypt))
                    {
                        //Write all data to the stream.
                        swEncrypt.Write(plainText);
                    }
                }
                outStr = Convert.ToBase64String(msEncrypt.ToArray());
            }
        }
        finally
        {
            // Clear the RijndaelManaged object.
            if (aesAlg != null)
                aesAlg.Clear();
        }

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

    private static string DecryptStringAES(string cipherText, string sharedSecret)
    {
        if (string.IsNullOrEmpty(cipherText))
            throw new ArgumentNullException("cipherText");
        if (string.IsNullOrEmpty(sharedSecret))
            throw new ArgumentNullException("sharedSecret");

        RijndaelManaged aesAlg = null;

        // Declare the string used to hold
        // the decrypted text.
        string plaintext;

        try
        {
            // generate the key from the shared SECRET and the salt
            var key = new Rfc2898DeriveBytes(sharedSecret, Salt);

            // Create the streams used for decryption.                
            byte[] bytes = Convert.FromBase64String(cipherText);
            using (var msDecrypt = new MemoryStream(bytes))
            {
                aesAlg = new RijndaelManaged();
                aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
                // Get the initialization vector from the encrypted stream
                aesAlg.IV = ReadByteArray(msDecrypt);
                var decryptor = aesAlg.CreateDecryptor(KEY, aesAlg.IV);
                using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                {
                    using (var srDecrypt = new StreamReader(csDecrypt))
                        plaintext = srDecrypt.ReadToEnd();
                }
            }
        }
        catch (Exception e)
        {
            return string.Empty;
        }
        finally
        {
            // Clear the RijndaelManaged object.
            if (aesAlg != null)
               aesAlg.Clear();
        }

        return plaintext;
    }

    private static byte[] ReadByteArray(Stream s)
    {
        var rawLength = new byte[sizeof(int)];
        if (s.Read(rawLength, 0, rawLength.Length) != rawLength.Length)
            throw new SystemException("Stream did not contain properly formatted byte array");

        var buffer = new byte[BitConverter.ToInt32(rawLength, 0)];
        if (s.Read(buffer, 0, buffer.Length) != buffer.Length)
            throw new SystemException("Did not read byte array properly");
        return buffer;
    }
}

and the equivalent Ruby code is below.

require 'pbkdf2'
require "openssl"
require "base64"
require "encrypted"
require "securerandom"

secret = "readasecret"
salt = "o6MKe324346722kbM7c5"
encrypt_this = "fiskbullsmacka med extra sovs"

rfc_db = PBKDF2.new(password: secret, salt: salt, iterations: 1000, key_length: 32, hash_function: :sha1).bin_string
key = rfc_db.bytes[0, 32]
puts key.inspect
cipherkey = key.pack('c*')


# ----------------- ENCRYPTION -------------------------
cipher = Encrypted::Ciph.new("256-128")
cipher.key = cipherkey
cipher.iv = cipher.generate_iv


encrypted_text = cipher.encrypt(encrypt_this)
# Convert string to byte[]
unpackENCString = encrypted_text.unpack("c*")
# Combine IV and data
combEncrypt = cipher.iv.unpack("c*").concat(encrypted_text.unpack("c*"))

# Convert byte[] to string
passingString = combEncrypt.pack("c*")

enc = Base64.encode64(passingString)

puts "Encrypted text :"+enc


# ----------------- DECRYPTION -------------------------

plain = Base64.decode64(enc)

passingbyteArray = plain.unpack("c*")

rfc_db = PBKDF2.new(password: secret, salt: salt, iterations: 1000, key_length: 32, hash_function: :sha1).bin_string
key = rfc_db.bytes[0, 32]
decipherkey = key.pack('c*')

decrypt_this = passingbyteArray[16,passingbyteArray.length() - 16].pack("c*")               #from above
decipher = Encrypted::Ciph.new("256-128")
cipher.key = decipherkey     #key used above to encrypt
cipher.iv = passingbyteArray[0,16].pack("c*")      #initialization vector used above
decrypted_text = cipher.decrypt(decrypt_this)

puts "Decrypted text: "+decrypted_text

Solution

  • In the posted C# code, a key derivation via PBKDF2 is implemented, but it's not used. Instead, the hard-coded key KEY is applied. This may have been done for testing purposes.

    In the following, not the hard coded key, but the derived key is considered. For this in the C# code in aesAlg.CreateEncryptor() and in aesAlg.CreateDecryptor() instead of KEY aesAlg.Key must be passed, to which the derived key was assigned before.

    The C# code concatenates after encryption the size of the IV (to 4 bytes), the IV and the ciphertext in this order. On decryption the corresponding separation takes place.
    Note that storing the size of the IV is actually not necessary, since it's known: the sizeof the IV is equal to the block size, and is thus 16 bytes for AES.
    In the following, the concatenation of the IV size is kept for simplicity.

    In the Ruby code, various crypto ibraries are used, although openssl is actually sufficient. Therefore, the following implementation applies openssl only:

    The key derivation using PBKDF2 is:

    require "openssl"
    require "base64"
    
    # Key derivation (PBKDF2)
    secret = "readasecret"
    salt = "o6MKe324346722kbM7c5"
    key = OpenSSL::KDF.pbkdf2_hmac(secret, salt: salt, iterations: 1000, length: 32, hash: "sha1")
    

    The encryption is:

    # Encryption
    plaintext = "fiskbullsmacka med extra sovs"
    cipher = OpenSSL::Cipher.new('AES-256-CBC')
    cipher.encrypt
    cipher.key = key
    nonce = cipher.random_iv # random IV
    cipher.iv = nonce
    ciphertext = cipher.update(plaintext) + cipher.final
    # Concatenation
    sizeIvCiphertext = ['10000000'].pack('H*').concat(nonce.concat(ciphertext))
    sizeIvCiphertextB64 =  Base64.encode64(sizeIvCiphertext)
    puts sizeIvCiphertextB64 # e.g. EAAAAC40tnEeaRtwutravBiH8vpn4vtjk6s9CAq/XEbyGTGMPwxENInIoAqWlZvR413Aqg==
    

    and the decryption:

    # Separation
    sizeIvCiphertext = Base64.decode64(sizeIvCiphertextB64)
    size = sizeIvCiphertext[0, 4]
    iv = sizeIvCiphertext [4, 16]
    ciphertext = sizeIvCiphertext[4+16, sizeIvCiphertext.length-16]
    # Decryption
    decipher = OpenSSL::Cipher.new('AES-256-CBC')
    decipher.decrypt
    decipher.key = key
    decipher.iv = iv
    decrypted = decipher.update(ciphertext) + decipher.final
    puts decrypted # fiskbullsmacka med extra sovs
    

    The ciphertext generated with this can be decrypted with the C# code. Likewise, a ciphertext of the C# code can be decrypted with the above Ruby code.


    Keep in mind that both codes contain a vulnerability. The codes use a static salt for the key derivation, which is insecure. Instead, a random salt should be generated for each key derivation. Just like the IV, the salt is not secret and is usually passed along with the IV and the ciphertext, e.g. salt | IV | ciphertext.
    Also, for PBKDF2, an iteration count of 1000 is generally too small.