/// <summary> /// Hash the password with different algorithms depending on the HashVersion, and optionally append the version string. /// Defaults to using Bcrypt, and appends the version string by default. /// This method should only be used for testing, and generation of the initial intermediate hashes. /// If version is HashVersion.SHA256, then input MUST be an SHA256 hash of the original password. /// </summary> /// <param name="input">String to hash</param> /// <param name="version">HashVersion to use for hashing</param> /// <param name="addVersion">Append the version string to the hash. Defaults true</param> /// <returns>Hashed string with appended version</returns> public static string CreateHashWithVersion(string input, HashVersionEnum version = HashVersionEnum.Bcrypt, bool addVersion = true) { string hash; switch (version) { case HashVersionEnum.SHA256: // Use original SHA256 hashing. hash = CreateSHA256Hash(input); break; case HashVersionEnum.Intermediate_SHA256_Bcrypt: // Use intermediate hashing algorithm. // The input MUST be an SHA256 hash of the original password. hash = CreateBcryptHash(input); break; case HashVersionEnum.Bcrypt: default: // Otherwise we always want to hash with Bcrypt. hash = CreateBcryptHash(input); version = HashVersionEnum.Bcrypt; break; } // Optionally append Hash Version to hashed Password. if (addVersion) { hash += CreateHashVersionString(version); } return(hash); }
/// <summary> /// Parse a version string into a HashVersion Enum. The version string should be an integer as string. /// If parsing fails, the referenced HashVersion will stay unchanged. /// </summary> /// <param name="versionString">Version of password hash.</param> /// <param name="hashVersion">Current HashVersion</param> /// <returns>HashVersion enum</returns> public static void TryParseHashVersion(string versionString, ref HashVersionEnum hashVersion) { // 0 is both the default int value and a HashVersion with value "Unknown", so use 0 as default. int intVersion = 0; // We're using int.Parse inside a Try/Catch instead of int.TryParse because the // build tool is failing to build code with int.TryParse. try { intVersion = int.Parse(versionString); } // If we catch an error, then the version is obviously invalid, so return false. catch { return; } // Check that the version is a valid HashVersion, and return false if not. // We use Enum.IsDefined because Enum.TryParse returns true for any numeric value. // https://stackoverflow.com/questions/6741649/enum-tryparse-returns-true-for-any-numeric-values bool isDefined = Enum.IsDefined(typeof(HashVersionEnum), intVersion); if (!isDefined) { return; } // intVersion is valid, so cast to HashVersion and return true. hashVersion = (HashVersionEnum)intVersion; }
/// <summary> /// Verify that a Password matches the hashed Password. /// </summary> /// <param name="hashedPassword">Hashed Password</param> /// <param name="providedPassword">Password to verify against hashed Password</param> /// <returns></returns> public PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword) { // Verification should fail if the provided Password is null, empty, or whitespace. if (string.IsNullOrEmpty(providedPassword)) { return(PasswordVerificationResult.Failed); } // Verification should fail if the hashed Password is null, empty, whitespace, or the string "$deleted$". if (string.IsNullOrEmpty(hashedPassword) || hashedPassword.Trim() == "$deleted$") { return(PasswordVerificationResult.Failed); } // Get the current Hash Version and the Hashed Password without the version string. Tuple <string, HashVersionEnum> result = HashHelpers.GetHashVersion(hashedPassword); string currentHash = result.Item1; HashVersionEnum hashVersion = result.Item2; // Verify the provided Password against the current Hash. try { switch (hashVersion) { case HashVersionEnum.Unknown: // We have an invalid or Unknown HashVersion, so we cannot verify the password. // This case should not be hit if we hash our passwords correctly, but should be included as a precaution. return(PasswordVerificationResult.Failed); case HashVersionEnum.SHA256: // Use original SHA256 hashing, return SuccessRehashNeeded if valid. return(VerifySHA256Hash(providedPassword, currentHash) ? PasswordVerificationResult.SuccessRehashNeeded : PasswordVerificationResult.Failed); case HashVersionEnum.Intermediate_SHA256_Bcrypt: // Use intermediate hashing algorithm, pass SHA256 hash of password as input to Bcrypt. // Return SuccessRehashNeeded if valid. return(VerifyBcryptHash(HashHelpers.CreateSHA256Hash(providedPassword), currentHash) ? PasswordVerificationResult.SuccessRehashNeeded : PasswordVerificationResult.Failed); case HashVersionEnum.Bcrypt: default: // Otherwise we always want to verify with Bcrypt by default. Return Success if valid. return(VerifyBcryptHash(providedPassword, currentHash) ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed); } } catch (SaltParseException) { return(PasswordVerificationResult.Failed); } }
/// <summary> /// Parses a Hashed Password string for a Hash Version and the original Password Hash. /// Returns the split original Password Hash and the Hash Version as a Tuple. /// A Hash Version is a $ symbol followed by a number, appended to the Password Hash. e.g. "$1". /// SHA256 is returned as the default Hash Version, unless the Hash has a valid Bcrypt prefix. /// </summary> /// <param name="hashedPassword">A hashed password string, possibly including appended version</param> /// <returns>Tuple of original hashed password with appended version string removed, and HashVersion enum</returns> public static Tuple <string, HashVersionEnum> GetHashVersion(string hashedPassword) { // Use Unknown as the default HashVersion. HashVersionEnum version = HashVersionEnum.Unknown; string passwordHash = ""; // If the Hashed Password starts with "$2" then the Password has already been encrypted with Bcrypt. // Therefore we will set the version to Bcrypt in case the Hashed Password doesn't have a version string, or an invalid string. if (hashedPassword.StartsWith("$2")) { version = HashVersionEnum.Bcrypt; } // Else, we check if the Hashed Password matches the SHA256 regex. // Again, we set the version to SHA256 in case the Hashed Password doesn't have a version string, or an invalid string. else if (MatchesSHA256(hashedPassword)) { version = HashVersionEnum.SHA256; } // Our custom hash version will be a single character after the last $ symbol. // The $ symbol is the designated delimiter between sections in a hash string. int lastDollarSignIndex = hashedPassword.LastIndexOf('$'); string versionString = hashedPassword.Substring(lastDollarSignIndex + 1); // If the versionString has a length of 1, then it should be our custom hash version. if (versionString.Length == 1) { // Safely parse the version number into a HashVersion enum. TryParseHashVersion(versionString, ref version); } // Get a count of all $ symbols in the hashed password. int dollarSignCount = hashedPassword.Count(f => f == '$'); // If there are 4 or more $ symbols, then the password is using Bcrypt, and we have a version string appended. // Likewise, if there is only one $ symbol, the password is not hashed with Bcrypt, but we still have a version string. // In both cases, we need to return the hashed string without the appended version string. if (dollarSignCount >= 4 || dollarSignCount == 1) { passwordHash = hashedPassword.Substring(0, lastDollarSignIndex); } else { passwordHash = hashedPassword; } return(new Tuple <string, HashVersionEnum>(passwordHash, version)); }
/// <summary> /// Creates a HashVersion string with $ sign. e.g. "$1", "$2" /// </summary> /// <param name="version">HashVersion enum</param> /// <returns>HashVersion string</returns> public static string CreateHashVersionString(HashVersionEnum version) { return("$" + (int)version); }