Search code examples
c#stringencryptionstreamaes

How to recode functions to use Strings instad of FileStreams


I'm building a game which needs encrypted saves etc. So I'm making a few functions to help me encrypt them. However, the only function I currently have uses FileStreams for input and output, but I would like to use strings instead. The functions work fine on Files, but I'm having trouble swapping from FileStreams to MemoryStreams to strings. NOTE: I HAVE REMOVED IRRELEVANT CODE. OurCodeWorld.GenerateRandomSalt() 100% works, tested with FileStream encryption

FULL CODE:

Program.cs:

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;


namespace Encryption_test
{

    class Program
    {
        static public string encryptedExtension = ".aes";
        static public string decryptedExtension = ".decrypted";

        static void Main(string[] args)
        {
            string path = Environment.CurrentDirectory;
            Log($"Current path: {path}");
            string filePath = $"{path}/unencrypted.txt";
            string message =
@"Hello world!
This is my message. 1234";

            RunStackOverFlowString();

            //Sleep forever...zzz
            Thread.Sleep(10000);
            Console.WriteLine();

            float TicksToMs(long ticks)
            {
                return ticks / (float)Stopwatch.Frequency * 1000F;
            }

            void RunStackOverFlowString()
            {
                byte[] salt = OurCodeWorld.GenerateRandomSalt();
                int iterations = 1024;
                string password = "";
                string enc = StackOverflow.EncryptString(message, password, salt, iterations);
                Log($"Enc: {enc}");
                string dec = StackOverflow.DecryptString(enc, password, salt, iterations);
                Log($"Dec: {dec}");
            }
        private static void WriteFile(string path, string value)
        {
            FileStream _file = File.OpenWrite(path);
            byte[] info = new UTF8Encoding(true).GetBytes(value);
            _file.Write(info, 0, info.Length);
            _file.Close();
        }

        private static string ReadFile(string filePath, long length = long.MaxValue)
        {
            FileStream _file = File.OpenRead(filePath);
            if (length == long.MaxValue)
                length = _file.Length;

            byte[] b = new byte[length];
            UTF8Encoding temp = new UTF8Encoding(true);

            _file.Read(b, 0, b.Length);
            _file.Close();

            return temp.GetString(b);
        }

        private static void DeleteFile(string path)
        {
            File.Delete(path);
        }
        private static void CreateFile(string path)
        {
            if (File.Exists(path))
                DeleteFile(path);
            FileStream a = File.Open(path, FileMode.Create);
            a.Close();
        }
        static void Log(string message)
        {
            Console.WriteLine(message);
        }
    }
}

StackOverFlow.cs:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

class StackOverflow
{

    // Rfc2898DeriveBytes constants:

    /// <summary>Decrypt a file.</summary>
    /// <remarks>NB: "Padding is invalid and cannot be removed." is the Universal CryptoServices error.  Make sure the password, salt and iterations are correct before getting nervous.</remarks>
    /// <param name="sourceFilename">The full path and name of the file to be decrypted.</param>
    /// <param name="destinationFilename">The full path and name of the file to be output.</param>
    /// <param name="password">The password for the decryption.</param>
    /// <param name="salt">The salt to be applied to the password.</param>
    /// <param name="iterations">The number of iterations Rfc2898DeriveBytes should use before generating the key and initialization vector for the decryption.</param>
    public static void DecryptFile(string sourceFilename, string destinationFilename, string password, byte[] salt, int iterations)
    {
        AesManaged aes = new AesManaged();
        aes.BlockSize = aes.LegalBlockSizes[0].MaxSize;
        aes.KeySize = aes.LegalKeySizes[0].MaxSize;
        // NB: Rfc2898DeriveBytes initialization and subsequent calls to   GetBytes   must be eactly the same, including order, on both the encryption and decryption sides.
        Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password, salt, iterations);
        aes.Key = key.GetBytes(aes.KeySize / 8);
        aes.IV = key.GetBytes(aes.BlockSize / 8);
        aes.Mode = CipherMode.CBC;
        ICryptoTransform transform = aes.CreateDecryptor(aes.Key, aes.IV);

        using (FileStream destination = new FileStream(destinationFilename, FileMode.Create, FileAccess.Write, FileShare.None))
        {
            using (CryptoStream cryptoStream = new CryptoStream(destination, transform, CryptoStreamMode.Write))
            {
                try
                {
                    using (FileStream source = new FileStream(sourceFilename, FileMode.Open, FileAccess.Read, FileShare.Read))
                    {
                        source.CopyTo(cryptoStream);
                    }
                }
                catch (CryptographicException exception)
                {
                    if (exception.Message == "Padding is invalid and cannot be removed.")
                        throw new ApplicationException("Universal Microsoft Cryptographic Exception (Not to be believed!)", exception);
                    else
                        throw;
                }
            }
        }
    }

    /// <summary>Encrypt a file.</summary>
    /// <param name="sourceFilename">The full path and name of the file to be encrypted.</param>
    /// <param name="destinationFilename">The full path and name of the file to be output.</param>
    /// <param name="password">The password for the encryption.</param>
    /// <param name="salt">The salt to be applied to the password.</param>
    /// <param name="iterations">The number of iterations Rfc2898DeriveBytes should use before generating the key and initialization vector for the decryption.</param>
    public static void EncryptFile(string sourceFilename, string destinationFilename, string password, byte[] salt, int iterations)
    {
        AesManaged aes = new AesManaged();
        aes.BlockSize = aes.LegalBlockSizes[0].MaxSize;
        aes.KeySize = aes.LegalKeySizes[0].MaxSize;
        // NB: Rfc2898DeriveBytes initialization and subsequent calls to   GetBytes   must be eactly the same, including order, on both the encryption and decryption sides.
        Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password, salt, iterations);
        aes.Key = key.GetBytes(aes.KeySize / 8);
        aes.IV = key.GetBytes(aes.BlockSize / 8);
        aes.Mode = CipherMode.CBC;
        ICryptoTransform transform = aes.CreateEncryptor(aes.Key, aes.IV);

        using (FileStream destination = new FileStream(destinationFilename, FileMode.Create, FileAccess.Write, FileShare.None))
        {
            using (CryptoStream cryptoStream = new CryptoStream(destination, transform, CryptoStreamMode.Write))
            {
                using (FileStream source = new FileStream(sourceFilename, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    source.CopyTo(cryptoStream);
                }
            }
        }
    }

    //THIS IS MY CODE
    public static string EncryptString(string inputString, string password, byte[] salt, int iterations)
    {
        AesManaged aes = new AesManaged();
        aes.BlockSize = aes.LegalBlockSizes[0].MaxSize;
        aes.KeySize = aes.LegalKeySizes[0].MaxSize;
        // NB: Rfc2898DeriveBytes initialization and subsequent calls to   GetBytes   must be eactly the same, including order, on both the encryption and decryption sides.
        Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password, salt, iterations);
        aes.Key = key.GetBytes(aes.KeySize / 8);
        aes.IV = key.GetBytes(aes.BlockSize / 8);
        aes.Mode = CipherMode.CBC;
        ICryptoTransform transform = aes.CreateEncryptor(aes.Key, aes.IV);

        var source = StringToStream(inputString);
        var output = new MemoryStream();

        CryptoStream cryptoStream = new CryptoStream(output, transform, CryptoStreamMode.Write);
        source.CopyTo(cryptoStream);
        return StreamToString(output);
    }

    public static string DecryptString(string inputString, string password, byte[] salt, int iterations)
    {
        AesManaged aes = new AesManaged();
        aes.BlockSize = aes.LegalBlockSizes[0].MaxSize;
        aes.KeySize = aes.LegalKeySizes[0].MaxSize;
        // NB: Rfc2898DeriveBytes initialization and subsequent calls to   GetBytes   must be eactly the same, including order, on both the encryption and decryption sides.
        Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password, salt, iterations);
        aes.Key = key.GetBytes(aes.KeySize / 8);
        aes.IV = key.GetBytes(aes.BlockSize / 8);
        aes.Mode = CipherMode.CBC;
        ICryptoTransform transform = aes.CreateDecryptor(aes.Key, aes.IV);

        var source = StringToStream(inputString);
        var output = new MemoryStream();

        CryptoStream cryptoStream = new CryptoStream(output, transform, CryptoStreamMode.Write);
        source.CopyTo(cryptoStream);
        return StreamToString(output);
    }


    public static Stream StringToStream(string s)
    {
        var stream = new MemoryStream();
        var writer = new StreamWriter(stream);
        writer.Write(s);
        writer.Flush();
        stream.Position = 0;
        return stream;
    }
    public static string StreamToString(Stream s)
    {
        s.Position = 0;
        byte[] buffer = new byte[s.Length];
        s.Read(buffer, 0, (int)s.Length);
        return Encoding.Default.GetString(buffer);
    }
}

I have already tested the StreamToString and StringToStream methods, and they work fine. When I run DecryptString, there is no output, and the function returns garbled strings, often looking similar to this Dec: ?K??? ?@?????n?l?r????T?


Solution

  • I tested the code. There are two issues:

    1. You take encrypted bytes and convert them to a string using an encoding. This process will fail in general because you can't take arbitrary bytes and convert them to a string. The mapping between bytes and strings is not 1:1. Likely, you should not work with strings at all here. Rather, keep the data in byte format and write those bytes to a file. The method signature should be like byte[] Encrypt(byte[] input, ...). The encryption code has no business converting from and to strings.
    2. EncryptString must flush the crypto stream using FlushFinalBlock. If this is not done data at the end will be missing.