Another implementation - from before I found others like RoadWarrior, Zer and thasiznets had already done it.
This, like Rfc2898DeriveBytes
derives from .NET's System.Cryptography.DeriveBytes
. In other words, usage is the same - although I only implemented the one constructor I use.
Other than that lineage, it's not based on Microsoft's implementation at all. Which also calls for a disclaimer - see bottom of this answer.
It allows an arbitrary Pseudo Random Function, which means we can plug in HMAC SHA256 or HMAC SHA512 - or someone with more cryptographic insight and courage than me could plug in whatever they want - like the RFC allows. It also uses long
rather than int
for the iteration count - just for the crazy ones.
/// <summary>
/// More generic version of the built-in Rfc2898DeriveBytes class. This one
/// allows an arbitrary Pseudo Random Function, meaning we can use e.g.
/// HMAC SHA256 or HMAC SHA512 rather than the hardcoded HMAC SHA-1 of the
/// built-in version.
/// </summary>
public class PBKDF2DeriveBytes : DeriveBytes
{
// Initialization:
private readonly IPseudoRandomFunction prf;
private readonly byte[] salt;
private readonly long iterationCount;
private readonly byte[] saltAndBlockNumber;
// State:
// Last result of prf.Transform - also used as buffer
// between GetBytes() calls:
private byte[] buffer;
private int bufferIndex;
private int nextBlock;
/// <param name="prf">
/// The Pseudo Random Function to use for calculating the derived key
/// </param>
/// <param name="salt">
/// The initial salt to use in calculating the derived key
/// </param>
/// <param name="iterationCount">
/// Number of iterations. RFC 2898 recommends a minimum of 1000
/// iterations (in the year 2000) ideally with number of iterations
/// adjusted on a regular basis (e.g. each year).
/// </param>
public PBKDF2DeriveBytes(
IPseudoRandomFunction prf, byte[] salt, long iterationCount)
{
if (prf == null)
{
throw new ArgumentNullException("prf");
}
if (salt == null)
{
throw new ArgumentNullException("salt");
}
this.prf = prf;
this.salt = salt;
this.iterationCount = iterationCount;
// Prepare combined salt = concat(original salt, block number)
saltAndBlockNumber = new byte[salt.Length + 4];
Buffer.BlockCopy(salt, 0, saltAndBlockNumber, 0, salt.Length);
Reset();
}
/// <summary>
/// Retrieves a derived key of the length specified.
/// Successive calls to GetBytes will return different results -
/// calling GetBytes(20) twice is equivalent to calling
/// GetBytes(40) once. Use Reset method to clear state.
/// </summary>
/// <param name="keyLength">
/// The number of bytes required. Note that for password hashing, a
/// key length greater than the output length of the underlying Pseudo
/// Random Function is redundant and does not increase security.
/// </param>
/// <returns>The derived key</returns>
public override byte[] GetBytes(int keyLength)
{
var result = new byte[keyLength];
int resultIndex = 0;
// If we have bytes in buffer from previous run, use those first:
if (buffer != null && bufferIndex > 0)
{
int bufferRemaining = prf.HashSize - bufferIndex;
// Take at most keyLength bytes from the buffer:
int bytesFromBuffer = Math.Min(bufferRemaining, keyLength);
if (bytesFromBuffer > 0)
{
Buffer.BlockCopy(buffer, bufferIndex, result, 0,
bytesFromBuffer);
bufferIndex += bytesFromBuffer;
resultIndex += bytesFromBuffer;
}
}
// If, after filling from buffer, we need more bytes to fill
// the result, they need to be computed:
if (resultIndex < keyLength)
{
ComputeBlocks(result, resultIndex);
// If we used the entire buffer, reset index:
if (bufferIndex == prf.HashSize)
{
bufferIndex = 0;
}
}
return result;
}
/// <summary>
/// Resets state. The next call to GetBytes will return the same
/// result as an initial call to GetBytes.
/// Sealed since it's called from constructor.
/// </summary>
public sealed override void Reset()
{
buffer = null;
bufferIndex = 0;
nextBlock = 1;
}
private void ComputeBlocks(byte[] result, int resultIndex)
{
int currentBlock = nextBlock;
// Keep computing blocks until we've filled the result array:
while (resultIndex < result.Length)
{
// Run iterations for block:
F(currentBlock);
// Populate result array with the block, but only as many bytes
// as are needed - keep the rest in buffer:
int bytesFromBuffer = Math.Min(
prf.HashSize,
result.Length - resultIndex
);
Buffer.BlockCopy(buffer, 0, result, resultIndex, bytesFromBuffer);
bufferIndex = bytesFromBuffer;
resultIndex += bytesFromBuffer;
currentBlock++;
}
nextBlock = currentBlock;
}
private void F(int currentBlock)
{
// First iteration:
// Populate initial salt with the current block index:
Buffer.BlockCopy(
BlockNumberToBytes(currentBlock), 0,
saltAndBlockNumber, salt.Length, 4
);
buffer = prf.Transform(saltAndBlockNumber);
// Remaining iterations:
byte[] result = buffer;
for (long iteration = 2; iteration <= iterationCount; iteration++)
{
// Note that the PRF transform takes the immediate result of the
// last iteration, not the combined result (in buffer):
result = prf.Transform(result);
for (int byteIndex = 0; byteIndex < buffer.Length; byteIndex++)
{
buffer[byteIndex] ^= result[byteIndex];
}
}
}
private static byte[] BlockNumberToBytes(int blockNumber)
{
byte[] result = BitConverter.GetBytes(blockNumber);
// Make sure the result is big endian:
if (BitConverter.IsLittleEndian)
{
Array.Reverse(result);
}
return result;
}
}
IPseudoRandomFunction
is declared as:
public interface IPseudoRandomFunction : IDisposable
{
int HashSize { get; }
byte[] Transform(byte[] input);
}
An example HMAC-SHA512 IPseudoRandomFunction (for brevity - I use a generic class allowing any of .NET's HMAC classes):
public class HMACSHA512PseudoRandomFunction : IPseudoRandomFunction
{
private HMAC hmac;
private bool disposed;
public HmacPseudoRandomFunction(byte[] input)
{
hmac = new HMACSHA512(input);
}
public int HashSize
{
// Might as well return a constant 64
get { return hmac.HashSize / 8; }
}
public byte[] Transform(byte[] input)
{
return hmac.ComputeHash(input);
}
public void Dispose()
{
if (!disposed)
{
hmac.Dispose();
hmac = null;
disposed = true;
}
}
}
Result... This:
using (var prf = new HMACSHA512PseudoRandomFunction(input))
{
using (var hash = new PBKDF2DeriveBytes(prf, salt, 1000))
{
hash.GetBytes(32);
}
}
... is the HMAC-SHA512 equivalent of this:
using (var hash = new Rfc2898DeriveBytes(input, salt, 1000))
{
hash.GetBytes(32);
}
Testing
The PBKDF2DeriveBytes class has been tested for
It has also been run through simple tests of Reset()
and multiple calls to GetBytes()
.
A few preliminary performance tests reveal it to be on par with the .NET implementation for SHA-1 for 1000 runs of 1000 iterations on "pass"/"saltSALT" converted to bytes in ASCII encoding with GetBytes(200)
. Sometimes a little faster than the built-in implementation, sometimes a little slower - we're talking something like 84 vs. 83 seconds on my ancient computer. All of those were done with a debug build of PBKDF2DeriveBytes
, though (since the bulk of the work is obviously done in the HMAC, we'd need many more iterations or runs to measure an actual difference anyway).
Disclaimer:
I'm not a cryptography genius. As the above indicates, this has not been heavily tested. I make no guarantees. But maybe, along with the other answers and implementations, it can help in understanding the methodology.