Created
November 2, 2018 10:30
-
-
Save C0BR4cH/f154c7901c8910fb6b3a5e687f43b38c to your computer and use it in GitHub Desktop.
Simple C# password hasher running from .NET 2.0 onward
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Security.Cryptography; | |
| namespace SaltyCrypter | |
| { | |
| /// <summary> | |
| /// Creates the salt and hash for a password | |
| /// </summary> | |
| public class CryptoMagic | |
| { | |
| private const int hashBlockSize = 5; | |
| private const int algorithmIndex = 0; | |
| private const int pbkdf2IterationsIndex = 1; | |
| private const int bytesSizeIndex = 2; | |
| private const int saltIndex = 3; | |
| private const int hashIndex = 4; | |
| /// <summary> | |
| /// SHA-1 algorithm name | |
| /// </summary> | |
| public const string Sha1Name = "sha1"; | |
| /// <summary> | |
| /// Amount of output bytes for SHA-1 | |
| /// </summary> | |
| public const int Sha1Bytes = 20; | |
| /// <summary> | |
| /// Minimum of iterations for PBKDF2 | |
| /// </summary> | |
| public const int MinPbkdf2Iterations = 64000; | |
| /// <summary> | |
| /// Size of the bytes array for salt and hash | |
| /// </summary> | |
| public int BytesSize { get; private set; } = Sha1Bytes; | |
| /// <summary> | |
| /// Generates the password hash. | |
| /// The output is formatted as followed: | |
| /// <code>algorithm:iterations:hashSize:salt:hash</code> | |
| /// </summary> | |
| /// <param name="password">Password to hash</param> | |
| /// <param name="iterations">(Optional) Amount of iterations for PBKDF2 algorithm. Default and minimum is 64000</param> | |
| /// <returns>Hash formatted in blocks</returns> | |
| public string GeneratePasswordHash(string password, int iterations = -1) | |
| { | |
| // Get configures iterations | |
| int pbkdf2Iterations = iterations > MinPbkdf2Iterations ? iterations : MinPbkdf2Iterations; | |
| byte[] salt = GenerateSalt(); | |
| byte[] hash = GetPbkdf2Bytes(password, salt, pbkdf2Iterations, BytesSize); | |
| return $"{Sha1Name}:{pbkdf2Iterations}:{BytesSize}:{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}"; | |
| } | |
| /// <summary> | |
| /// Verifies if the password matches to the hash. | |
| /// The hash needs to be formatted in blocks like algorithm:iterations:hashSize:salt:hash | |
| /// </summary> | |
| /// <param name="password">Password to verify</param> | |
| /// <param name="hash">Hash formatted in blocks</param> | |
| /// <returns><code>true</code> if the password matches with the hash, <code>false</code> otherwise.</returns> | |
| public bool VerifyPassowrd(string password, string hash) | |
| { | |
| // Parameter validation | |
| if (string.IsNullOrEmpty(password) || password.Trim().Length == 0) | |
| { | |
| throw new ArgumentException("Password can't be empty / null.", nameof(password)); | |
| } | |
| if (string.IsNullOrEmpty(hash) || hash.Trim().Length == 0) | |
| { | |
| throw new ArgumentException("Hash can't be empty / null.", nameof(hash)); | |
| } | |
| // Hash validation | |
| string[] hashBlocks = hash.Split(':'); | |
| if (hashBlocks.Length != hashBlockSize) | |
| { | |
| throw new InvalidHashException("Hash needs to be in format 'algorithm:pbkdf2Iterations:hashSize:salt:hash'."); | |
| } | |
| // Algorithm validation | |
| if (hashBlocks[algorithmIndex] != Sha1Name) | |
| { | |
| throw new InvalidHashException($"Hash algorithm of {hashBlocks[algorithmIndex]} is invalid. Needs to be {Sha1Name}"); | |
| } | |
| // Iterations validation | |
| int iterations = -1; | |
| try | |
| { | |
| iterations = int.Parse(hashBlocks[pbkdf2IterationsIndex]); | |
| } | |
| catch (OverflowException e) | |
| { | |
| throw new InvalidHashException($"Iterations of {hashBlocks[pbkdf2IterationsIndex]} are to large.", e); | |
| } | |
| catch (Exception e) | |
| { | |
| if (e is FormatException || e is ArgumentException) | |
| { | |
| throw new InvalidHashException($"Could not parse {hashBlocks[pbkdf2IterationsIndex]} into an int.", e); | |
| } | |
| throw new InvalidHashException("Invalid argument for iterations.", e); | |
| } | |
| // Salt validation | |
| byte[] saltBytes = null; | |
| try | |
| { | |
| saltBytes = Convert.FromBase64String(hashBlocks[saltIndex]); | |
| } | |
| catch (Exception e) | |
| { | |
| if (e is ArgumentException || e is FormatException) | |
| { | |
| throw new InvalidHashException($"Could not parse {hashBlocks[saltIndex]} into a byte array.", e); | |
| } | |
| throw new InvalidHashException("$Invalid argument for salt.", e); | |
| } | |
| // Hash validation | |
| byte[] hashBytes = null; | |
| try | |
| { | |
| hashBytes = Convert.FromBase64String(hashBlocks[hashIndex]); | |
| } | |
| catch (Exception e) | |
| { | |
| if (e is ArgumentException || e is FormatException) | |
| { | |
| throw new InvalidHashException($"Could not parse {hashBlocks[hashIndex]} into a byte array.", e); | |
| } | |
| throw new InvalidHashException("$Invalid argument for hash.", e); | |
| } | |
| // Hash size validation | |
| int hashSize = -1; | |
| try | |
| { | |
| hashSize = int.Parse(hashBlocks[bytesSizeIndex]); | |
| } | |
| catch (OverflowException e) | |
| { | |
| throw new InvalidHashException($"Hashsize of {hashBlocks[bytesSizeIndex]} are to large.", e); | |
| } | |
| catch (Exception e) | |
| { | |
| if (e is FormatException || e is ArgumentException) | |
| { | |
| throw new InvalidHashException($"Could not parse {hashBlocks[bytesSizeIndex]} into an int.", e); | |
| } | |
| throw new InvalidHashException("Invalid argument for iterations.", e); | |
| } | |
| // Hash size validation | |
| if (hashSize != hashBytes.Length) | |
| { | |
| throw new InvalidHashException("Hash length doesn't match the stored hash size."); | |
| } | |
| // Hash provided password | |
| byte[] verifyingHash = GetPbkdf2Bytes(password, saltBytes, iterations, hashSize); | |
| // Check if both hashes are the same | |
| return SlowEquals(hashBytes, verifyingHash); | |
| } | |
| private static bool SlowEquals(byte[] a, byte[] b) | |
| { | |
| uint diff = (uint)a.Length ^ (uint)b.Length; | |
| for (int i = 0; i < a.Length && i < b.Length; i++) | |
| { | |
| diff |= (uint)(a[i] ^ b[i]); | |
| } | |
| return diff == 0; | |
| } | |
| private byte[] GenerateSalt() | |
| { | |
| byte[] salt = new byte[BytesSize]; | |
| RNGCryptoServiceProvider cryptoRandom = new RNGCryptoServiceProvider(); | |
| try | |
| { | |
| cryptoRandom.GetBytes(salt); | |
| } | |
| finally | |
| { | |
| cryptoRandom = null; | |
| } | |
| return salt; | |
| } | |
| private byte[] GetPbkdf2Bytes(string password, byte[] salt, int iterations, int outputBytesSize) | |
| { | |
| Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations); | |
| byte[] output = null; | |
| try | |
| { | |
| output = pbkdf2.GetBytes(outputBytesSize); | |
| } | |
| finally | |
| { | |
| pbkdf2.Reset(); | |
| pbkdf2 = null; | |
| } | |
| return output; | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| namespace SaltyCrypter | |
| { | |
| /// <summary> | |
| /// Exception when the hash is invalid in any way | |
| /// </summary> | |
| public class InvalidHashException : Exception | |
| { | |
| public InvalidHashException(string message) : base(message) { } | |
| public InvalidHashException(string message, Exception e) : base(message, e) { } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment