3

Jump to Update below < not sure how to make a link :(

I've made some improvments on the code from : Csharp-AES-bits-Encryption-Library-with-Salt

  • saltBytes is now the SHA512 of the password.
  • Random IV for each encryption call. ( IV length 16 is added to the encrypted file , removed from file before decryption)

Do you see any flaws, something that needs optimization? In particular, my questions are:

  • Is my generateIV() method below in the "Encryption Code" safe and secure ? Note that its only dependency is on the .NET RNGCryptoServiceProvider Class.

  • Is it safe to use password hash as a salt? or it should be random like the IV and stored along with the ciphertext?


Just for reference, here is my code.

The Code bellow is working, I've made a test to encrypt two text files with exact same text inside each. The result is both have different data, and decrypt succeed.

Also, I checked before the random IV, both files had same encrypted text, results in same data.

Encryption code :

    private static int IV_LENGTH = 16;

    public static byte[] AES_Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
    {

        byte[] encryptedBytes = null;
        byte[] encryptedBytesAndIV = null;
        byte[] saltBytes = SHA512.Create().ComputeHash(passwordBytes);
        using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
        {
            using (AesCryptoServiceProvider AES = new AesCryptoServiceProvider())
            {
                AES.KeySize = 256;
                //AES.BlockSize = 128;

                var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 100);
                AES.Key = key.GetBytes(AES.KeySize / 8);
                AES.IV = generateIV();

                AES.Mode = CipherMode.CBC;

                using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
                    cs.Close();
                }
                encryptedBytes = ms.ToArray();
                encryptedBytesAndIV = new byte[encryptedBytes.Length + AES.IV.Length];
                AES.IV.CopyTo(encryptedBytesAndIV, 0);
                encryptedBytes.CopyTo(encryptedBytesAndIV, IV_LENGTH);
            }
        }

        return encryptedBytesAndIV;
    }

    private static byte[] generateIV()
    {
        using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
        {
            byte[] nonce = new byte[IV_LENGTH];
            rng.GetBytes(nonce);
            return nonce;
        }
    }

Decryption code :

    public static byte[] AES_Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes)
    {
        byte[] decryptedBytes = null;


        byte[] saltBytes = SHA512.Create().ComputeHash(passwordBytes);

        using (MemoryStream ms = new MemoryStream())
        {
            using (AesCryptoServiceProvider AES = new AesCryptoServiceProvider())
            {
                AES.KeySize = 256;
                //AES.BlockSize = 128;

                var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 100);
                AES.Key = key.GetBytes(AES.KeySize / 8);
                AES.IV = getIV(bytesToBeDecrypted);
                bytesToBeDecrypted = removeTagAndIV(bytesToBeDecrypted);
                AES.Mode = CipherMode.CBC;

                using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
                    cs.Close();
                }
                decryptedBytes = ms.ToArray();


            }
        }

        return decryptedBytes;
    }

    private static byte[] removeTagAndIV(byte[] arr)
    {
        byte[] enc = new byte[arr.Length - IV_LENGTH];
        Array.Copy(arr, IV_LENGTH, enc, 0, arr.Length - IV_LENGTH);
        return enc;
    }

    private static byte[] getIV(byte[] arr)
    {
        byte[] IV = new byte[IV_LENGTH];
        Array.Copy(arr, 0, IV, 0, IV_LENGTH);
        return IV;
    }

Update:

Here is updated code based on comments/recommendations/advice

  • Random Salt True
  • iterations 100000

Encryption:

        public static byte[] AES_Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
    {

        byte[] encryptedBytes = null;
        byte[] encryptedBytesFinal = null;
        byte[] saltBytes = generateIVandSalt(16);
        using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
        {
            using (AesCryptoServiceProvider AES = new AesCryptoServiceProvider())
            {
                AES.KeySize = 256;

                var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 100000);

                AES.Key = key.GetBytes(AES.KeySize / 8);

                AES.IV = generateIVandSalt(16);

                AES.Mode = CipherMode.CBC;

                using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
                    cs.Close();
                }
                encryptedBytes = ms.ToArray();
                encryptedBytesFinal = new byte[encryptedBytes.Length + AES.IV.Length + saltBytes.Length];

                AES.IV.CopyTo(encryptedBytesFinal, 0);
                saltBytes.CopyTo(encryptedBytesFinal, 16);
                encryptedBytes.CopyTo(encryptedBytesFinal, 16 + 16);

            }
        }

        return encryptedBytesFinal;
    }
    private static byte[] generateIVandSalt(int len)
    {
        using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
        {
            byte[] nonce = new byte[len];
            rng.GetBytes(nonce);
            return nonce;
        }
    }

Decryption:

        public static byte[] AES_Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes)
    {
        byte[] decryptedBytes = null;


        byte[] saltBytes = getSalt(bytesToBeDecrypted);

        using (MemoryStream ms = new MemoryStream())
        {
            using (AesCryptoServiceProvider AES = new AesCryptoServiceProvider())
            {
                AES.KeySize = 256;


                var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 100000);
                AES.Key = key.GetBytes(AES.KeySize / 8);
                AES.IV = getIV(bytesToBeDecrypted);
                bytesToBeDecrypted = removeIVandSalt(bytesToBeDecrypted);
                AES.Mode = CipherMode.CBC;

                using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
                    cs.Close();
                }
                decryptedBytes = ms.ToArray();


            }
        }

        return decryptedBytes;
    }

    private static byte[] removeIVandSalt(byte[] arr)
    {
        byte[] enc = new byte[arr.Length - 16 - IV_LENGTH];
        Array.Copy(arr, IV_LENGTH + 16, enc, 0, arr.Length - IV_LENGTH - 16);
        return enc;
    }
    private static byte[] getIV(byte[] arr)
    {
        byte[] IV = new byte[IV_LENGTH];
        Array.Copy(arr, 0, IV, 0, IV_LENGTH);
        return IV;
    }

    private static byte[] getSalt(byte[] arr)
    {
        byte[] salt = new byte[16];
        Array.Copy(arr, IV_LENGTH, salt, 0, 16);
        return salt;
    }
xhxx
  • 33
  • 4
  • 1
    Maybe try http://crypto.stackexchange.com/ – CaffeineAddiction Nov 21 '16 at 17:31
  • Code review is off topic here - there is a codereview.se site though. – Matthew Nov 21 '16 at 17:31
  • 2
    @Matthew I've been told to ask here where more security guys, : [link](http://codereview.stackexchange.com/questions/147608/c-aes-encryption-random-iv-per-file) – xhxx Nov 21 '16 at 17:34
  • 3
    They suggested you ask about the design of your system, not to ask for code review. However, the general recommendation would be not to make modifications to crypto code unless you really know what you're doing – Matthew Nov 21 '16 at 17:39
  • 2
    This is a tough call, it's certainly asking for a code review, but code review of his usage of cryptographic APIs. I'd say it's on-topic. @xhxx could you provide a link to the documentation for the `RNGCryptoServiceProvider()` call you're talking about? It's hard to answer without knowing exactly which library / implementation you're talking about. – Mike Ounsworth Nov 21 '16 at 18:03
  • @MikeOunsworth here is the link [RNGCryptoServiceProvider](https://msdn.microsoft.com/en-us/library/system.security.cryptography.rngcryptoserviceprovider(v=vs.110).aspx) – xhxx Nov 21 '16 at 18:18

2 Answers2

6

From a security perspective, the generateIV() method is fine, if a bit redundant as .NET will generate an IV for you in exactly the same way if one is not provided.

The three bigger issues are that 100 iterations is way too few for PBKDF2, the lack of authentication, which is an issue if the ciphertext is ever out of your control (as an aside you have a method named removeTagAndIV() but no tag implemented in the first place) and the use of the password hash as a salt. The point of a salt is to be globally unique. The hash of a raw password is not. You've now significantly weakened your PBKFD, greatly limited the potential size of the practical keyspace, and created the potential for rainbow tables to be used to identify keys. The salt must be globally unique. The easiest way to achieve this would be to simply generate an salt using the generateIV() method you already have, and store it alongside the IV and ciphertext.

Xander
  • 35,616
  • 27
  • 114
  • 141
  • Thank you, the iterations 100 is just for testing final is 1000 or more, authentication tag implementation was removed also. – xhxx Nov 21 '16 at 18:34
  • 2
    @xhxx Unless you have a very specific reason not to, I would suggest something more along the lines of 50,000 to 100,000 iterations as a reasonable minimum, and the more the better. – Xander Nov 21 '16 at 18:36
  • Indeed 100 iterations is way too low. It is best to benchmark this. Should require about 100ms (with appropriate DoS protection) on your target hardware. (knowing that attackers will use faster hardware for Brute force) Also to be GPU resistant you could consider using BCrypt, SCrypt or Argon2. [details on each here](http://security.stackexchange.com/questions/138363/why-is-md5-considered-a-vulnerable-algorithm/138369#138369) – 700 Software Nov 21 '16 at 18:54
  • @Xander updated the code. – xhxx Nov 21 '16 at 20:30
3
  1. I'm not a .NET programmer, and do not fully understand some of the code here.

  2. How much entropy is in passwordBytes? If the entropy is below 72 bits, you'll need to use a (Slow) Password Hash, or more specifically a Key Derivation Function.

  3. generateIV should be fine as long as it is random. It doesn't even need to be a Cryptographically Secure random number, so long as it is globally unique.

  4. If you want to be sure that CBC is a good chaining mode, you can ask a separate question. We'll have answers on the pros and cons of each chaining mode.

  5. You should not ask us to vet the decryption code for security. You can vet it yourself by simply testing.

  6. Salt should be random, not deterministic. Also add Pepper.

  7. See #5, and then make your encryption code as short and concise as possible. Ask questions on any individual points you like, but having us review the whole thing is a bit off topic here.

  8. AES is a good choice.

700 Software
  • 13,897
  • 3
  • 53
  • 82
  • Those are a good points, thank you, the passwordBytes is the SHA256 of the input plain text password. – xhxx Nov 21 '16 at 18:22
  • 1
    The plain text is 32 : example "OCyzbXUlrxN5TBhCyBpwtSXf9SGcbN4I" using this website to check [Strength Test](http://rumkin.com/tools/password/passchk.php) the result is : Entropy: 174.3 bits, Strength: Very Strong - More often than not, this level of security is overkill. – xhxx Nov 21 '16 at 18:34
  • @xhxx the entropy george was talking about is not the entropy of the plaintext, but of the password. I guess what you have is not the password. (And don't try to measure the entropy of the SHA256 of your password, that would be way off.) – Paŭlo Ebermann Nov 21 '16 at 22:46
  • 1
    @Paŭlo Ebermann am i missing something ? password plaintext = OCyzbXUlrxN5TBhCyBpwtSXf9SGcbN4I , password SHA256 hash = 3e24078070c5bd731051c3f2d65771a33152e3d12af44463043d76b3a7dde987 – xhxx Nov 22 '16 at 01:28
  • @xhxx no, I guess I missed something. That just didn't look like a password (because a password is usually assumed something humans can remember). I guess this was not chosen by a human, but generated by some process? Then looking at this process can tell us its entropy better than any strength tester. – Paŭlo Ebermann Nov 23 '16 at 20:13
  • @Paŭlo Ebermann Yes, this is generated password, for human to remember it is called passphrase AFIK example : the quick brown fox jumps over the lazy dog – xhxx Nov 24 '16 at 04:11