2
$\begingroup$

I'm relatively new to encrypting, and I have some questions. I intend to write a standalone C# program that will write and read an encrypted file with sensitive data using a password entered by the user when the program is started. Both the program and the file will be stored together on a private place, but one never knows that they could fall in the wrong hands. As the program is written in C# it can be decompiled, but I always heard that is not a problem, as the encryption should be safe even if the algorithm is known.

Here is the test code I've written, some questions will follow:

private string TestEncryption(string password, string input, string filename)
{
    // Encryption
    byte[] inputBytes = AddHash(Encoding.UTF8.GetBytes(input));
    int iterations = 10000;
    int saltSize = 64;
    using (Rfc2898DeriveBytes rbg = new Rfc2898DeriveBytes(password, saltSize, iterations))
    {
        if (rbg.Salt.Length != saltSize)
            throw new Exception("Invalid salt size");

        using (AesManaged aes = new AesManaged())
        {
            byte[] key = rbg.GetBytes(aes.KeySize >> 3);
            byte[] iv = rbg.GetBytes(aes.BlockSize >> 3);
            using (ICryptoTransform cryptoTransform = aes.CreateEncryptor(key, iv))
            using (MemoryStream memoryStream = new MemoryStream())
            {
                using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, CryptoStreamMode.Write))
                    cryptoStream.Write(inputBytes, 0, inputBytes.Length);

                byte[] encryptedBytes = AddHash(ConcatBytes(memoryStream.ToArray(), rbg.Salt));
                using (FileStream file = new FileStream(filename, FileMode.Create, FileAccess.Write))
                    file.Write(encryptedBytes, 0, encryptedBytes.Length);
            }
        }
    }

    // Decryption
    using (FileStream file = new FileStream(filename, FileMode.Open, FileAccess.Read))
    {
        inputBytes = new byte[file.Length];
        file.Read(inputBytes, 0, inputBytes.Length);
    }
    int length;
    if (!VerifyHash(inputBytes, out length))
        throw new Exception("File corrupted");

    byte[] salt = new byte[saltSize];
    length -= saltSize;
    Buffer.BlockCopy(inputBytes, length, salt, 0, saltSize);
    using (Rfc2898DeriveBytes rbg = new Rfc2898DeriveBytes(password, salt, iterations))
    using (AesManaged aes = new AesManaged())
    {
        byte[] key = rbg.GetBytes(aes.KeySize >> 3);
        byte[] iv = rbg.GetBytes(aes.BlockSize >> 3);
        try
        {
            using (ICryptoTransform cryptoTransform = aes.CreateDecryptor(key, iv))
            using (MemoryStream sourceStream = new MemoryStream(inputBytes, 0, length))
            using (CryptoStream cryptoStream = new CryptoStream(sourceStream, cryptoTransform, CryptoStreamMode.Read))
            using (MemoryStream decryptedStream = new MemoryStream())
            {
                cryptoStream.CopyTo(decryptedStream);
                byte[] decryptedBytes = decryptedStream.ToArray();
                if (!VerifyHash(decryptedBytes, out length))
                    throw new Exception("Invalid password");
                return Encoding.UTF8.GetString(decryptedBytes, 0, length);
            }
        }
        catch (CryptographicException exception)
        { throw new Exception("Probable invalid password", exception); }
    }

    return null;
}

private byte[] AddHash(byte[] bytes)
{
    using (MD5 md5 = MD5.Create())
    {
        byte[] hash = md5.ComputeHash(bytes);
        if (hash.Length != (md5.HashSize >> 3))
            throw new Exception("Invalid hash size");
        return ConcatBytes(bytes, hash);
    }
}

private bool VerifyHash(byte[] bytes, out int length)
{
    using (MD5 md5 = MD5.Create())
    {
        int hashSize = md5.HashSize >> 3;
        length = bytes.Length - hashSize;
        if (bytes.Length > hashSize)
        {
            byte[] hash = new byte[hashSize];
            Buffer.BlockCopy(bytes, length, hash, 0, hashSize);
            if (hash.SequenceEqual(md5.ComputeHash(bytes, 0, length)))
                return true;
        }
    }
    return false;
}

private byte[] ConcatBytes(params byte[][] buffers)
{
    int length = buffers.Sum(b => b.Length);
    byte[] result = new byte[length];
    int offset = 0;
    foreach (byte[] buffer in buffers)
    {
        Buffer.BlockCopy(buffer, 0, result, offset, buffer.Length);
        offset += buffer.Length;
    }
    return result;
}

Now for some questions:

  1. As you may have noticed, I save the salt I used to generate the Key and IV in the file. Does this compromise the encryption or not ? I supposed it is not, as when we store hashed passwords, they also include their salt.
  2. I'm using a salt size of 64 and 10000 iterations, are these a correct values ?
  3. I'm also saving some hash values (MD5) to detect errors. I'm saving one at the end of the file, calculated on the encrypted bytes (including the salt), to test if the file is not corrupted. I don't think this should be any problem as it doesn't compromise the encryption. The other hash code is added to the unencrypted data before encryption to test if the data is correctly decrypted. I'm wondering if this is a good idea, as I think it could help brute force decryption attacks ? If not, what are better alternatives ?
  4. Is AES (Rijndael) still the best symmetric encryption algorithm around, or are there better ?
  5. Should i enforce a minimum size for the password or is that not needed as the Key and IV are randomly generated based on the password and random salt ?
  6. Can the CryptographicException be avoided when using a wrong password ?

(I posted this question on stackoverflow, but someone suggested i move it to a more appropriate site)

Marc

EDIT: Added a sixth question.

$\endgroup$

2 Answers 2

2
$\begingroup$
  1. Salt and IV do not need to be secret, but should be unique. If I understand correctly, the Rfc2898DeriveBytes class generates a random salt? In that case you are fine.

  2. 64 bytes is (more than) enough for a random salt. The iteration count depends on your needs. You should basically pick the largest number that isn't too slow. Rather than a constant number, you could scale it to available processing power somehow.

  3. See Should we MAC-then-encrypt or encrypt-then-MAC? You should probably pick something more secure than MD5.

  4. Subjective, but it's not broken.

  5. The password can still be brute forced: PBKDF2 only makes it slower and the salt only prevents parallel attacks on multiple passwords. Poor passwords are still poor.

$\endgroup$
9
  • $\begingroup$ Thanks for the answer, I would upvote it but I don't have enough reputation yet on this site. Indeed, the Rfc2898DeriveBytes class generates a random salt when only the saltsize is given. I'll read up on MAC and see how I can incorporate it in my test program. $\endgroup$
    – Marc
    Commented May 27, 2014 at 18:12
  • $\begingroup$ After a quick glance, I noticed that these MAC algorithms require a Key. As I started with the supposition than the program is decompilable, I can't hardcode a Key into the program. Can I use the same password to generate a Key for the MAC algorithm ? I also added a sixth question to my original question. $\endgroup$
    – Marc
    Commented May 27, 2014 at 18:42
  • $\begingroup$ @Marc, it's a good idea to use separate keys if you easily can. If you choose a strong MAC that is independent of AES (like HMAC-SHA-256), reusing a key may not break your security, but it may be impossible to prove that. Your sixth question is a coding questing that's a bit off topic for the site, and I don't know enough about those APIs to help you there. $\endgroup$
    – otus
    Commented May 27, 2014 at 18:58
  • $\begingroup$ I did not intend to use the same key, but generate a new key using the Rfc2898DeriveBytes class and the same password. Or is that basically the same result as using the same key ? $\endgroup$
    – Marc
    Commented May 27, 2014 at 19:01
  • $\begingroup$ Oh, that should be fine. $\endgroup$
    – otus
    Commented May 27, 2014 at 19:01
0
$\begingroup$

This Example Is Using The Built-In PasswordDeriveBytes Function

    Aes Alg = Aes.Create();
    byte[] IV = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
    byte[] Salt = Encoding.ASCII.getBytes("Example Salt);
    string Password = "Example";
    PasswordDeriveBytes pdb = new PasswordDeriveBytes(Password,salt);
    Aes.Key = pdb.CryptDeriveKey("Aes","SHA256",256,IV);

This Code Has Been Not Been Tested, Please Ensure You Change The IV, Salt And Password.

The Following Should Answer Question 6, First You Will Need To Get The HResult Code When The Error Is Thrown.

    catch(Exception e)
    {
      if(e.HResult.ToString() == "Code")
      {
        //Do Nothing
      }
      else
      {
        throw e;
      }
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.