26

I want to use Rfc2898 in c# to derive a key. I also need to use SHA256 as Digest for Rfc2898. I found the class Rfc2898DeriveBytes, but it uses SHA-1 and I don't see a way to make it use a different digest.

Is there a way to use Rfc2898 in c# with SHA256 as digest (short of implementing it from scratch)?

Carsten
  • 4,204
  • 4
  • 32
  • 49
  • There is no security need to use a different digest, SHA-1 is not "broken" in this use. – zaph May 12 '17 at 09:32
  • 5
    @zaph Ridiculous! He wants to use SHA-2. SHA1 may not be broken for you, but it is broken for Carsten. – Douglas Held Nov 15 '17 at 11:36
  • @DouglasHeld Ridiculous! Rfc2898 or PBKDF2 using SHA1 is not broken, there is no collision vulnerability. SHA1 is broken for signing. See [Is PBKDF2-HMAC-SHA1 really broken?](https://crypto.stackexchange.com/a/15221/4747) and [Is PBKDF2 (RFC 2898) broken because SHA1 is broken?](https://crypto.stackexchange.com/a/47523/4747). – zaph Nov 15 '17 at 13:06
  • I see what you mean. But that is no justification for Carsten not to seek to use an implementation with SHA2. – Douglas Held Nov 15 '17 at 17:33
  • @DouglasHeld Surely you are joking! There is no security issue at all with Rfc2898 or PBKDF2 using SHA1. If SHA2 is available as an option that would be a good choice but would not improve security. With this type of password verification the security is in the time to brute force attack based in iterations with a list of well known passwords, not SHA1 vs SHA2. A real improvement would be a move to a GPU resistant hash such as Argon2. – zaph Nov 15 '17 at 22:12
  • 2
    Did anyone say ANYTHING about improving security? – Douglas Held Nov 15 '17 at 22:54
  • 2
    To solve the mystery ;-) : The reason why I needed to use SHA256 is that my application needed to decrypt data that so happens to have been encrypted by a 3rd party software using PBKDF2 with SHA256. Since I can't change the way the 3rd party software encrypts the data I didn't really have any choice what digest to use. – Carsten Nov 17 '17 at 05:13
  • `RFC2898DeriveBytes` implements PBKDF2, which might not be the best practice anymore for password hashing; other key derivation functions like `scrypt` and `argon2` require an investment in memory as well as time, and I am aware of a function called `ProgPoW` which is designed to be not much slower on general-purpose hardware than on specialized hardware running an optimized implementation. – Peter O. Sep 20 '18 at 11:34

8 Answers8

18

.NET Core has a new implementation of Rfc2898DeriveBytes.

The CoreFX version no longer has the the hashing algorithm hard-coded

The code is available on Github. It was merged to master on March 2017 and has been shipped with .NET Core 2.0.

Bruno Garcia
  • 6,029
  • 3
  • 25
  • 38
  • 5
    This is the best answer here at the moment. If Microsoft has shipped the updated library, that is going to be better in principle than any of the other implementations, simply because of its signoff by Microsoft QA. Matthew's answer and Peter O.'s answer may be great code but from what I can see, subsequent comments have already found potential bugs in the implementations. This supports the general principle not to roll your own... – Douglas Held Nov 15 '17 at 12:19
18

For those who need it, .NET Framework 4.7.2 includes an overload of Rfc2898DeriveBytes that allows the hashing algorithm to be specified:

byte[] bytes;
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256))
{
    bytes = deriveBytes.GetBytes(PBKDF2SubkeyLength);
}

The HashAlgorithmName options at the moment are:

  • MD5
  • SHA1
  • SHA256
  • SHA384
  • SHA512
Tieson T.
  • 20,774
  • 6
  • 77
  • 92
15

See Bruno Garcia's answer.


At the time I started this answer, Rfc2898DeriveBytes was not configurable to use a different hash function. In the meantime, though, it has been improved; see Bruno Garcia's answer. The following function can be used to generate a hashed version of a user-provided password to store in a database for authentication purposes.

For users of older .NET frameworks, this is still useful:

// NOTE: The iteration count should
// be as high as possible without causing
// unreasonable delay.  Note also that the password
// and salt are byte arrays, not strings.  After use,
// the password and salt should be cleared (with Array.Clear)

public static byte[] PBKDF2Sha256GetBytes(int dklen, byte[] password, byte[] salt, int iterationCount){
    using(var hmac=new System.Security.Cryptography.HMACSHA256(password)){
        int hashLength=hmac.HashSize/8;
        if((hmac.HashSize&7)!=0)
            hashLength++;
        int keyLength=dklen/hashLength;
        if((long)dklen>(0xFFFFFFFFL*hashLength) || dklen<0)
            throw new ArgumentOutOfRangeException("dklen");
        if(dklen%hashLength!=0)
            keyLength++;
        byte[] extendedkey=new byte[salt.Length+4];
        Buffer.BlockCopy(salt,0,extendedkey,0,salt.Length);
        using(var ms=new System.IO.MemoryStream()){
            for(int i=0;i<keyLength;i++){
                extendedkey[salt.Length]=(byte)(((i+1)>>24)&0xFF);
                extendedkey[salt.Length+1]=(byte)(((i+1)>>16)&0xFF);
                extendedkey[salt.Length+2]=(byte)(((i+1)>>8)&0xFF);
                extendedkey[salt.Length+3]=(byte)(((i+1))&0xFF);
                byte[] u=hmac.ComputeHash(extendedkey);
                Array.Clear(extendedkey,salt.Length,4);
                byte[] f=u;
                for(int j=1;j<iterationCount;j++){
                    u=hmac.ComputeHash(u);
                    for(int k=0;k<f.Length;k++){
                        f[k]^=u[k];
                    }
                }
                ms.Write(f,0,f.Length);
                Array.Clear(u,0,u.Length);
                Array.Clear(f,0,f.Length);
            }
            byte[] dk=new byte[dklen];
            ms.Position=0;
            ms.Read(dk,0,dklen);
            ms.Position=0;
            for(long i=0;i<ms.Length;i++){
                ms.WriteByte(0);
            }
            Array.Clear(extendedkey,0,extendedkey.Length);
            return dk;
        }
    }
General Grievance
  • 4,555
  • 31
  • 31
  • 45
Peter O.
  • 32,158
  • 14
  • 82
  • 96
  • 1
    Nice. You may want to zero out the internal state (including the stream) of the algorithm once the values are not required anymore. Otherwise you key material may be exposed. – Maarten Bodewes Sep 06 '13 at 23:45
  • @owlstead: That's mostly done already; calling Dispose (or exiting the "using" scope) will zero out the password and salt. Is there anything else that needs to be done here? – Peter O. Sep 07 '13 at 20:54
  • You have cleared out the input but not the stream (as already mentioned) nor the other internal variables declared in the loop. Those will be deallocated or removed from stack of course, but that does not mean that the value is removed from memory. The class design is slightly strange. There is no need to put password and salt in a field - there is little use to store them for later. Currently you can only change the iteration count and regenerate the key (possibly with a different length). – Maarten Bodewes Sep 07 '13 at 23:05
  • The interface used in this class is merely to mimic the interface used in Rfc2898DeriveBytes. Your concerns are valid, however; for one thing, Rfc2898DeriveBytes stores the password as a string, which is immutable and can't be cleared when done. A better choice would have been a char array, as in Java's PBEKeySpec. – Peter O. Sep 08 '13 at 03:12
  • 1
    Compatibility is certainly a good reason to keep to a certain design. Note that using strings and characters have the drawback that PBKDF2 does not have a default character encoding; UTF-8 is mentioned but the algorithm itself works on bytes. Oracle specifies that the 8 lower bits are used (yuk), Microsoft *silently* uses UTF-8, at least in tests. Using bytes as you do is probably the best way to go; it makes the user think about the encoding, and the bytes can be cleared (by the user, when required). **Really nice update!!** – Maarten Bodewes Sep 08 '13 at 13:57
  • While your class is really cool, however this gives me different results than: `val spec = new PBEKeySpec(password.toCharArray, salt.getBytes, iterations, digestSize)` `val skf: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")` `val hash: Array[Byte] = skf.generateSecret(spec).getEncoded` ``` in Scala/Java, any ideas? – Christian Schmitt Jul 13 '16 at 13:51
  • Also I found https://www.medo64.com/2012/04/pbkdf2-with-sha-256-and-others/ which is the only C# method that yields the same values as the Python 2.7.8+ / Java Values. – Christian Schmitt Jul 13 '16 at 14:39
  • To the asker: Can you please un-accept this answer and accept Bruno Garcia's answer instead? – Peter O. Sep 20 '18 at 11:19
  • @PeterO I restored the original code, as this is still useful for older .NET versions, hope that's OK. Still works perfect :) – Wiebe Tijsma Oct 30 '18 at 15:01
9

The BCL Rfc2898DeriveBytes is hardcoded to use sha-1.

KeyDerivation.Pbkdf2 allows for exactly the same output, but it also allows HMAC SHA-256 and HMAC SHA-512. It's faster too; on my machine by around 5 times - and that's good for security, because it allows for more rounds, which makes life for crackers harder (incidentally sha-512 is a lot less gpu-friendly than sha-256 or sha1). And the api is simpler, to boot:

byte[] salt = ...
string password = ...
var rounds = 50000;                       // pick something bearable
var num_bytes_requested = 16;             // 128 bits is fine
var prf = KeyDerivationPrf.HMACSHA512;    // or sha256, or sha1
byte[] hashed = KeyDerivation.Pbkdf2(password, salt, prf, rounds, num_bytes_requested);

It's from the nuget package Microsoft.AspNetCore.Cryptography.KeyDerivation which does not depend on asp.net core; it runs on .net 4.5.1 or .net standard 1.3 or higher.

Eamon Nerbonne
  • 47,023
  • 20
  • 101
  • 166
2

You could use Bouncy Castle. The C# specification lists the algorithm "PBEwithHmacSHA-256", which can only be PBKDF2 with SHA-256.

Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
2

I know this is an old question, but for anyone that comes across it, you can now use KeyDerivation.Pbkdf2 from the Microsoft.AspNetCore.Cryptography.KeyDerivation nuget package. It is what is used in asp.net core.

Unfortunately it will add a ton of references that aren't really needed. You could just copy the code and paste it into your own project (although you will now have to maintain cryto code which is a PITA)

F.A.
  • 602
  • 1
  • 7
  • 15
1

For what it's worth, here's a copy of Microsoft's implementation but with SHA-1 replaced with SHA512:

namespace System.Security.Cryptography
{
using System.Globalization;
using System.IO;
using System.Text;

[System.Runtime.InteropServices.ComVisible(true)]
public class Rfc2898DeriveBytes_HMACSHA512 : DeriveBytes
{
    private byte[] m_buffer;
    private byte[] m_salt;
    private HMACSHA512 m_HMACSHA512;  // The pseudo-random generator function used in PBKDF2

    private uint m_iterations;
    private uint m_block;
    private int m_startIndex;
    private int m_endIndex;
    private static RNGCryptoServiceProvider _rng;
    private static RNGCryptoServiceProvider StaticRandomNumberGenerator
    {
        get
        {
            if (_rng == null)
            {
                _rng = new RNGCryptoServiceProvider();
            }
            return _rng;
        }
    }

    private const int BlockSize = 20;

    //
    // public constructors 
    // 

    public Rfc2898DeriveBytes_HMACSHA512(string password, int saltSize) : this(password, saltSize, 1000) { }

    public Rfc2898DeriveBytes_HMACSHA512(string password, int saltSize, int iterations)
    {
        if (saltSize < 0)
            throw new ArgumentOutOfRangeException("saltSize", Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));

        byte[] salt = new byte[saltSize];
        StaticRandomNumberGenerator.GetBytes(salt);

        Salt = salt;
        IterationCount = iterations;
        m_HMACSHA512 = new HMACSHA512(new UTF8Encoding(false).GetBytes(password));
        Initialize();
    }

    public Rfc2898DeriveBytes_HMACSHA512(string password, byte[] salt) : this(password, salt, 1000) { }

    public Rfc2898DeriveBytes_HMACSHA512(string password, byte[] salt, int iterations) : this(new UTF8Encoding(false).GetBytes(password), salt, iterations) { }

    public Rfc2898DeriveBytes_HMACSHA512(byte[] password, byte[] salt, int iterations)
    {
        Salt = salt;
        IterationCount = iterations;
        m_HMACSHA512 = new HMACSHA512(password);
        Initialize();
    }

    //
    // public properties 
    //

    public int IterationCount
    {
        get { return (int)m_iterations; }
        set
        {
            if (value <= 0)
                throw new ArgumentOutOfRangeException("value", Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
            m_iterations = (uint)value;
            Initialize();
        }
    }

    public byte[] Salt
    {
        get { return (byte[])m_salt.Clone(); }
        set
        {
            if (value == null)
                throw new ArgumentNullException("value");
            if (value.Length < 8)
                throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, Environment.GetResourceString("Cryptography_PasswordDerivedBytes_FewBytesSalt")));
            m_salt = (byte[])value.Clone();
            Initialize();
        }
    }

    // 
    // public methods
    // 

    public override byte[] GetBytes(int cb)
    {
        if (cb <= 0)
            throw new ArgumentOutOfRangeException("cb", Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
        byte[] password = new byte[cb];

        int offset = 0;
        int size = m_endIndex - m_startIndex;
        if (size > 0)
        {
            if (cb >= size)
            {
                Buffer.InternalBlockCopy(m_buffer, m_startIndex, password, 0, size);
                m_startIndex = m_endIndex = 0;
                offset += size;
            }
            else
            {
                Buffer.InternalBlockCopy(m_buffer, m_startIndex, password, 0, cb);
                m_startIndex += cb;
                return password;
            }
        }

        //BCLDebug.Assert(m_startIndex == 0 && m_endIndex == 0, "Invalid start or end index in the internal buffer.");

        while (offset < cb)
        {
            byte[] T_block = Func();
            int remainder = cb - offset;
            if (remainder > BlockSize)
            {
                Buffer.InternalBlockCopy(T_block, 0, password, offset, BlockSize);
                offset += BlockSize;
            }
            else
            {
                Buffer.InternalBlockCopy(T_block, 0, password, offset, remainder);
                offset += remainder;
                Buffer.InternalBlockCopy(T_block, remainder, m_buffer, m_startIndex, BlockSize - remainder);
                m_endIndex += (BlockSize - remainder);
                return password;
            }
        }
        return password;
    }

    public override void Reset()
    {
        Initialize();
    }

    private void Initialize()
    {
        if (m_buffer != null)
            Array.Clear(m_buffer, 0, m_buffer.Length);
        m_buffer = new byte[BlockSize];
        m_block = 1;
        m_startIndex = m_endIndex = 0;
    }
    internal static byte[] Int(uint i)
    {
        byte[] b = BitConverter.GetBytes(i);
        byte[] littleEndianBytes = { b[3], b[2], b[1], b[0] };
        return BitConverter.IsLittleEndian ? littleEndianBytes : b;
    }
    // This function is defined as follow : 
    // Func (S, i) = HMAC(S || i) | HMAC2(S || i) | ... | HMAC(iterations) (S || i)
    // where i is the block number. 
    private byte[] Func()
    {
        byte[] INT_block = Int(m_block);

        m_HMACSHA512.TransformBlock(m_salt, 0, m_salt.Length, m_salt, 0);
        m_HMACSHA512.TransformFinalBlock(INT_block, 0, INT_block.Length);
        byte[] temp = m_HMACSHA512.Hash;
        m_HMACSHA512.Initialize();

        byte[] ret = temp;
        for (int i = 2; i <= m_iterations; i++)
        {
            temp = m_HMACSHA512.ComputeHash(temp);
            for (int j = 0; j < BlockSize; j++)
            {
                ret[j] ^= temp[j];
            }
        }

        // increment the block count.
        m_block++;
        return ret;
    }
}
}

In addition to replacing HMACSHA1 with HMACSHA512, you need to add a StaticRandomNumberGenerator property because Utils.StaticRandomNumberGenerator is internal in the microsoft assembly, and you need to add the static byte[] Int(uint i) method because microsoft's Utils.Int is also internal. Other than that, the code works.

Matthew
  • 4,149
  • 2
  • 26
  • 53
  • -1: I initially used this, and while I don't know why, it doesn't produce valid derived keys. I found this by writing and comparing with my own implementation, but this can be confirmed by testing this class against BouncyCastle or even the accepted answer by substituting `HMACSHA256` for `HMACSHA512` – caesay Feb 07 '16 at 23:44
0

Although this is an old question, since I added reference to this question in my Question Configurable Rfc2898DeriveBytes where I asked whether a generic implementation of the Rfc2898DeriveBytes algorithm was correct.

I have now tested and validated that it generates the exact same hash values if HMACSHA1 is provided for TAlgorithm as the .NET implementation of Rfc2898DeriveBytes

In order to use the class, one must provide the constructor for the HMAC algorithm requiring a byte array as the first argument.

e.g.:

var rfcGenSha1 = new Rfc2898DeriveBytes<HMACSHA1>(b => new HMACSHA1(b), key, ...)
var rfcGenSha256 = new Rfc2898DeriveBytes<HMACSHA256>(b => new HMACSHA256(b), key, ...)

This requires the algorithm to inherit HMAC at this point, I'm believe one might be able to Reduce the restriction to require inheritance from KeyedHashAlgorithm instead of HMAC, as long as the constructor of the algorithm accepts an array of bytes to the constructor.

Community
  • 1
  • 1