Search code examples
c#securityencryptionaes

AES decryption only producing part of the answer C#


I'm trying to learn cyber security and this is the very first thing I've done on it. I'm using this MSDN document ( https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.rfc2898derivebytes?redirectedfrom=MSDN&view=netcore-3.1 ) and it partly works. I assume it's encrypting it fine as when it comes to the decryption some of the original data is there but some is lost. The data that is being encrypted is a class that has been formatted into a JSON string( I don't think this is relevant as it's still a string being encrypted). original data

But once it's been encrypted and decrypted it turns out like this: Encrypted then decrypted data

I've ran this code and compared the results 5+ times and it's always: the start is wrong, username is partly right, password is always right and loginkey is partly right. So the error is recurring and always in the same spot.

Information you should know, the data get's encrypted and saved to a .txt file. The programme will run again and it will try and decrypted it. The Salt and password are saved on another file and those are read and used in the decryption.

There is a similar question on stackoverflow but the answer just says to use Rijndael(so not really an answer), this code is for me to learn and want an answer that isn't 4 lines long.

Code if curious(but it's basically the same as the MSDN document):

Encryption:

    static void EncryptFile()
    {
        string pwd1 = SteamID;//steamID is referring to account ID on Valve Steam           
        using (RNGCryptoServiceProvider rngCsp = new
        RNGCryptoServiceProvider())
        {
            rngCsp.GetBytes(salt1); //salt1 is a programme variable and will get saved to a file
        }
        SecureData File = new SecureData(_UserName,_PassWord,_LoginKey);
        string JsonFile = JsonConvert.SerializeObject(File); //puts the class into Json format
        int myIterations = 1000; //not needed
        try
        {
            Rfc2898DeriveBytes k1 = new Rfc2898DeriveBytes(pwd1, salt1,
            myIterations);
            Aes encAlg = Aes.Create(); // This might be the issue as AES will be different when you decrypt 
            encAlg.Key = k1.GetBytes(16);
            MemoryStream encryptionStream = new MemoryStream();
            CryptoStream encrypt = new CryptoStream(encryptionStream,
            encAlg.CreateEncryptor(), CryptoStreamMode.Write);
            byte[] utfD1 = new System.Text.UTF8Encoding(false).GetBytes(
            JsonFile); //encrypt Data 

            encrypt.Write(utfD1, 0, utfD1.Length);
            encrypt.FlushFinalBlock();
            encrypt.Close();
            byte[] edata1 = encryptionStream.ToArray();
            k1.Reset();

            System.IO.File.WriteAllBytes(SecureFile, edata1); //writes encrypted data to file
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: ", e);
        }
    }

Decryption:

    static void DecryptFile()
    {
        string pwd1 = SteamID;
        byte[] edata1;
        try
        {
            edata1 = System.IO.File.ReadAllBytes(SecureFile); //reads the file with encrypted data on it
            Aes encAlg = Aes.Create(); //I think this is the problem as the keyvalue changes when you create a new programme
            Rfc2898DeriveBytes k2 = new Rfc2898DeriveBytes(pwd1, salt1); //inputs from last time carry over
            Aes decAlg = Aes.Create();
            decAlg.Key = k2.GetBytes(16);
            decAlg.IV = encAlg.IV;
            MemoryStream decryptionStreamBacking = new MemoryStream();
            CryptoStream decrypt = new CryptoStream(
            decryptionStreamBacking, decAlg.CreateDecryptor(), CryptoStreamMode.Write);
            decrypt.Write(edata1, 0, edata1.Length);
            decrypt.Flush();
            decrypt.Close();
            k2.Reset();
            string data2 = new UTF8Encoding(false).GetString(
            decryptionStreamBacking.ToArray());//decrypted data  
            SecureData items = JsonConvert.DeserializeObject<SecureData>(data2); //reformat it out of JSon(Crashes as format isn't accepted)
            _UserName = items.S_UserName;
            _PassWord = items.S_Password;
            _LoginKey = items.S_LoginKey;
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: ", e);
            NewLogin();
        }
    }

Class Struct:

    class SecureData
    {
        public string S_UserName { get; set; } //These are variables that are apart of Valve steam Login process 
        public string S_Password { get; set; }
        public string S_LoginKey { get; set; }

        public SecureData(string z, string x, string y)
        {
            S_UserName = z;
            S_Password = x;
            S_LoginKey = y;
        }
    }

Solution

  • The problem is caused by different IVs for encryption and decryption. For a successful decryption the IV from the encryption must be used.

    Why are different IVs applied? When an AES instance is created, a random IV is implicitly generated. Two different AES instances therefore mean two different IVs. In the posted code, different AES instances are used for encryption and decryption. Although the reference encAlg used in the decryption has the same name as that of the encryption, the referenced instance is a different one (namely an instance newly created during decryption). This is different in the Microsoft example. There, the IV of the encryption is used in the decryption: decAlg.IV = encAlg.IV, where encAlg is the AES instance with which the encryption was performed.

    The solution is to store the IV from the encryption in the file so that it can be used in the decryption. The IV is not secret and is usually placed before the ciphertext:

    Necessary changes in EncryptFile:

    ...
    byte[] utfD1 = new System.Text.UTF8Encoding(false).GetBytes(JsonFile); 
    
    encryptionStream.Write(encAlg.IV, 0, encAlg.IV.Length);           // Write the IV
    encryptionStream.Flush();
                
    encrypt.Write(utfD1, 0, utfD1.Length);
    ...
                
    

    Necessary changes in DecryptFile:

    ...
    edata1 = System.IO.File.ReadAllBytes(SecureFile); 
                
    byte[] iv = new byte[16];                                         // Separate IV and ciphertext
    byte[] ciphertext = new byte[edata1.Length - iv.Length];
    Array.Copy(edata1, 0, iv, 0, iv.Length);
    Array.Copy(edata1, iv.Length, ciphertext, 0, ciphertext.Length);
    ...     
    Aes encAlg = Aes.Create();                                        // Remove this line
    ...
    decAlg.IV = iv;                                                   // Use the separated IV
    ...
    decrypt.Write(ciphertext, 0, ciphertext.Length);                  // Use the separated ciphertext
    

    A few remarks:

    • For each encryption a new, random salt should be generated and concatenated with the ciphertext analogous to the IV. During decryption, the salt can then be determined analogous to IV. Consider additionally RFC8018, sec 4.1.
    • The iteration count slows down the derivation of the key, which should make an attack by repeated attempts more difficult. Therefore the value should be as large as possible. Consider additionally RFC8018, sec 4.2.
    • Authentication data (i.e. passwords) are not encrypted, but hashed, here.