diff --git a/src/Microsoft.AspNet.Identity/Crypto.cs b/src/Microsoft.AspNet.Identity/Crypto.cs
deleted file mode 100644
index 57913e08ae..0000000000
--- a/src/Microsoft.AspNet.Identity/Crypto.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Runtime.CompilerServices;
-using System.Security.Cryptography;
-
-namespace Microsoft.AspNet.Identity
-{
- internal static class Crypto
- {
- private const int Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes
- private const int Pbkdf2SubkeyLength = 256 / 8; // 256 bits
- private const int SaltSize = 128 / 8; // 128 bits
-
- /* =======================
- * HASHED PASSWORD FORMATS
- * =======================
- *
- * Version 0:
- * PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
- * (See also: SDL crypto guidelines v5.1, Part III)
- * Format: { 0x00, salt, subkey }
- */
-
- public static string HashPassword(string password)
- {
- if (password == null)
- {
- throw new ArgumentNullException("password");
- }
-
- // Produce a version 0 (see comment above) text hash.
- byte[] salt;
- byte[] subkey;
- using (var deriveBytes = new Rfc2898DeriveBytes(password, SaltSize, Pbkdf2IterCount))
- {
- salt = deriveBytes.Salt;
- subkey = deriveBytes.GetBytes(Pbkdf2SubkeyLength);
- }
-
- var outputBytes = new byte[1 + SaltSize + Pbkdf2SubkeyLength];
- Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
- Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, Pbkdf2SubkeyLength);
- return Convert.ToBase64String(outputBytes);
- }
-
- // hashedPassword must be of the format of HashWithPassword (salt + Hash(salt+input)
- public static bool VerifyHashedPassword(string hashedPassword, string password)
- {
- if (hashedPassword == null)
- {
- return false;
- }
- if (password == null)
- {
- throw new ArgumentNullException("password");
- }
-
- // Verify a version 0 (see comment above) text hash.
- var hashedPasswordBytes = Convert.FromBase64String(hashedPassword);
- if (hashedPasswordBytes.Length != (1 + SaltSize + Pbkdf2SubkeyLength) || hashedPasswordBytes[0] != 0x00)
- {
- // Wrong length or version header.
- return false;
- }
- var salt = new byte[SaltSize];
- Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, SaltSize);
- var storedSubkey = new byte[Pbkdf2SubkeyLength];
- Buffer.BlockCopy(hashedPasswordBytes, 1 + SaltSize, storedSubkey, 0, Pbkdf2SubkeyLength);
-
- byte[] generatedSubkey;
- using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, Pbkdf2IterCount))
- {
- generatedSubkey = deriveBytes.GetBytes(Pbkdf2SubkeyLength);
- }
- return ByteArraysEqual(storedSubkey, generatedSubkey);
- }
-
- // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized.
- [MethodImpl(MethodImplOptions.NoOptimization)]
- private static bool ByteArraysEqual(byte[] a, byte[] b)
- {
- if (ReferenceEquals(a, b))
- {
- return true;
- }
- if (a == null || b == null || a.Length != b.Length)
- {
- return false;
- }
- var areSame = true;
- for (var i = 0; i < a.Length; i++)
- {
- areSame &= (a[i] == b[i]);
- }
- return areSame;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Identity/PasswordHasher.cs b/src/Microsoft.AspNet.Identity/PasswordHasher.cs
index e5726ade4f..bebcc63614 100644
--- a/src/Microsoft.AspNet.Identity/PasswordHasher.cs
+++ b/src/Microsoft.AspNet.Identity/PasswordHasher.cs
@@ -1,6 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using Microsoft.AspNet.Security.DataProtection;
+using Microsoft.Framework.OptionsModel;
+
namespace Microsoft.AspNet.Identity
{
///
@@ -8,6 +14,64 @@ namespace Microsoft.AspNet.Identity
///
public class PasswordHasher : IPasswordHasher where TUser : class
{
+ /* =======================
+ * HASHED PASSWORD FORMATS
+ * =======================
+ *
+ * Version 2:
+ * PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
+ * (See also: SDL crypto guidelines v5.1, Part III)
+ * Format: { 0x00, salt, subkey }
+ *
+ * Version 3:
+ * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
+ * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
+ * (All UInt32s are stored big-endian.)
+ */
+
+ private readonly PasswordHasherCompatibilityMode _compatibilityMode;
+ private readonly RandomNumberGenerator _rng;
+
+ ///
+ /// Constructs a PasswordHasher using the specified options
+ ///
+ ///
+ public PasswordHasher(IOptions options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _compatibilityMode = options.Options.CompatibilityMode;
+ if (!IsValidCompatibilityMode(_compatibilityMode))
+ {
+ throw new InvalidOperationException(Resources.InvalidPasswordHasherCompatibilityMode);
+ }
+
+ _rng = options.Options.Rng;
+ }
+
+ // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized.
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ private static bool ByteArraysEqual(byte[] a, byte[] b)
+ {
+ if (a == null && b == null)
+ {
+ return true;
+ }
+ if (a == null || b == null || a.Length != b.Length)
+ {
+ return false;
+ }
+ var areSame = true;
+ for (var i = 0; i < a.Length; i++)
+ {
+ areSame &= (a[i] == b[i]);
+ }
+ return areSame;
+ }
+
///
/// Hash a password
///
@@ -16,7 +80,77 @@ namespace Microsoft.AspNet.Identity
///
public virtual string HashPassword(TUser user, string password)
{
- return Crypto.HashPassword(password);
+ if (password == null)
+ {
+ throw new ArgumentNullException(nameof(password));
+ }
+
+ if (_compatibilityMode == PasswordHasherCompatibilityMode.IdentityV2)
+ {
+ return Convert.ToBase64String(HashPasswordV2(password, _rng));
+ }
+ else
+ {
+ return Convert.ToBase64String(HashPasswordV3(password, _rng));
+ }
+ }
+
+ private static byte[] HashPasswordV2(string password, RandomNumberGenerator rng)
+ {
+ const KeyDerivationPrf Pbkdf2Prf = KeyDerivationPrf.Sha1; // default for Rfc2898DeriveBytes
+ const int Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes
+ const int Pbkdf2SubkeyLength = 256 / 8; // 256 bits
+ const int SaltSize = 128 / 8; // 128 bits
+
+ // Produce a version 2 (see comment above) text hash.
+ byte[] salt = new byte[SaltSize];
+ rng.GetBytes(salt);
+ byte[] subkey = KeyDerivation.Pbkdf2(password, salt, Pbkdf2Prf, Pbkdf2IterCount, Pbkdf2SubkeyLength);
+
+ var outputBytes = new byte[1 + SaltSize + Pbkdf2SubkeyLength];
+ outputBytes[0] = 0x00; // format marker
+ Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
+ Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, Pbkdf2SubkeyLength);
+ return outputBytes;
+ }
+
+ private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng)
+ {
+ return HashPasswordV3(password, rng,
+ prf: KeyDerivationPrf.Sha256,
+ iterCount: 10000,
+ saltSize: 128 / 8,
+ numBytesRequested: 256 / 8);
+ }
+
+ private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
+ {
+ // Produce a version 3 (see comment above) text hash.
+ byte[] salt = new byte[saltSize];
+ rng.GetBytes(salt);
+ byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);
+
+ var outputBytes = new byte[13 + salt.Length + subkey.Length];
+ outputBytes[0] = 0x01; // format marker
+ WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
+ WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount);
+ WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
+ Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
+ Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
+ return outputBytes;
+ }
+
+ private static bool IsValidCompatibilityMode(PasswordHasherCompatibilityMode compatibilityMode)
+ {
+ return (compatibilityMode == PasswordHasherCompatibilityMode.IdentityV2 || compatibilityMode == PasswordHasherCompatibilityMode.IdentityV3);
+ }
+
+ private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
+ {
+ return ((uint)(buffer[offset + 0]) << 24)
+ | ((uint)(buffer[offset + 1]) << 16)
+ | ((uint)(buffer[offset + 2]) << 8)
+ | ((uint)(buffer[offset + 3]));
}
///
@@ -28,9 +162,116 @@ namespace Microsoft.AspNet.Identity
///
public virtual PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
{
- return Crypto.VerifyHashedPassword(hashedPassword, providedPassword)
- ? PasswordVerificationResult.Success
- : PasswordVerificationResult.Failed;
+ if (hashedPassword == null)
+ {
+ throw new ArgumentNullException(nameof(hashedPassword));
+ }
+ if (providedPassword == null)
+ {
+ throw new ArgumentNullException(nameof(providedPassword));
+ }
+
+ byte[] decodedHashedPassword = Convert.FromBase64String(hashedPassword);
+
+ // read the format marker from the hashed password
+ if (decodedHashedPassword.Length == 0)
+ {
+ return PasswordVerificationResult.Failed;
+ }
+ switch (decodedHashedPassword[0])
+ {
+ case 0x00:
+ if (VerifyHashedPasswordV2(decodedHashedPassword, providedPassword))
+ {
+ // This is an old password hash format - the caller needs to rehash if we're not running in an older compat mode.
+ return (_compatibilityMode == PasswordHasherCompatibilityMode.IdentityV3)
+ ? PasswordVerificationResult.SuccessRehashNeeded
+ : PasswordVerificationResult.Success;
+ }
+ else
+ {
+ return PasswordVerificationResult.Failed;
+ }
+
+ case 0x01:
+ return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword)
+ ? PasswordVerificationResult.Success
+ : PasswordVerificationResult.Failed;
+
+ default:
+ return PasswordVerificationResult.Failed; // unknown format marker
+ }
+ }
+
+ private static bool VerifyHashedPasswordV2(byte[] hashedPassword, string password)
+ {
+ const KeyDerivationPrf Pbkdf2Prf = KeyDerivationPrf.Sha1; // default for Rfc2898DeriveBytes
+ const int Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes
+ const int Pbkdf2SubkeyLength = 256 / 8; // 256 bits
+ const int SaltSize = 128 / 8; // 128 bits
+
+ // We know ahead of time the exact length of a valid hashed password payload.
+ if (hashedPassword.Length != 1 + SaltSize + Pbkdf2SubkeyLength)
+ {
+ return false; // bad size
+ }
+
+ byte[] salt = new byte[SaltSize];
+ Buffer.BlockCopy(hashedPassword, 1, salt, 0, salt.Length);
+
+ byte[] expectedSubkey = new byte[Pbkdf2SubkeyLength];
+ Buffer.BlockCopy(hashedPassword, 1 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
+
+ // Hash the incoming password and verify it
+ byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, Pbkdf2Prf, Pbkdf2IterCount, Pbkdf2SubkeyLength);
+ return ByteArraysEqual(actualSubkey, expectedSubkey);
+ }
+
+ private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password)
+ {
+ try
+ {
+ // Read header information
+ KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
+ int iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
+ int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);
+
+ // Read the salt: must be >= 128 bits
+ if (saltLength < 128 / 8)
+ {
+ return false;
+ }
+ byte[] salt = new byte[saltLength];
+ Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length);
+
+ // Read the subkey (the rest of the payload): must be >= 128 bits
+ int subkeyLength = hashedPassword.Length - 13 - salt.Length;
+ if (subkeyLength < 128 / 8)
+ {
+ return false;
+ }
+ byte[] expectedSubkey = new byte[subkeyLength];
+ Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
+
+ // Hash the incoming password and verify it
+ byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength);
+ return ByteArraysEqual(actualSubkey, expectedSubkey);
+ }
+ catch
+ {
+ // This should never occur except in the case of a malformed payload, where
+ // we might go off the end of the array. Regardless, a malformed payload
+ // implies verification failed.
+ return false;
+ }
+ }
+
+ private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
+ {
+ buffer[offset + 0] = (byte)(value >> 24);
+ buffer[offset + 1] = (byte)(value >> 16);
+ buffer[offset + 2] = (byte)(value >> 8);
+ buffer[offset + 3] = (byte)(value >> 0);
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Identity/PasswordHasherCompatibilityMode.cs b/src/Microsoft.AspNet.Identity/PasswordHasherCompatibilityMode.cs
new file mode 100644
index 0000000000..4dade7f693
--- /dev/null
+++ b/src/Microsoft.AspNet.Identity/PasswordHasherCompatibilityMode.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNet.Identity
+{
+ ///
+ /// An enum that describes the format used for hashing passwords.
+ ///
+ public enum PasswordHasherCompatibilityMode
+ {
+ ///
+ /// Hashes passwords in a way that is compatible with ASP.NET Identity versions 1 and 2.
+ ///
+ IdentityV2,
+
+ ///
+ /// Hashes passwords in a way that is compatible with ASP.NET Identity version 3.
+ ///
+ IdentityV3
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs b/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs
new file mode 100644
index 0000000000..5dccb93196
--- /dev/null
+++ b/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Security.Cryptography;
+
+namespace Microsoft.AspNet.Identity
+{
+ ///
+ /// Allows configuring how passwords are hashed.
+ ///
+ public class PasswordHasherOptions
+ {
+ private static readonly RandomNumberGenerator _defaultRng = RandomNumberGenerator.Create(); // secure PRNG
+
+ ///
+ /// Specifies the compatibility mode to use when hashing passwords.
+ ///
+ ///
+ /// The default compatibility mode is 'ASP.NET Identity version 3'.
+ ///
+ public PasswordHasherCompatibilityMode CompatibilityMode { get; set; } = PasswordHasherCompatibilityMode.IdentityV3;
+
+ // for unit testing
+ internal RandomNumberGenerator Rng { get; set; } = _defaultRng;
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs
index 4bf5983229..26e7ca7330 100644
--- a/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs
@@ -186,6 +186,22 @@ namespace Microsoft.AspNet.Identity
return string.Format(CultureInfo.CurrentCulture, GetString("InvalidEmail"), p0);
}
+ ///
+ /// The provided PasswordHasherCompatibilityMode is invalid.
+ ///
+ internal static string InvalidPasswordHasherCompatibilityMode
+ {
+ get { return GetString("InvalidPasswordHasherCompatibilityMode"); }
+ }
+
+ ///
+ /// The provided PasswordHasherCompatibilityMode is invalid.
+ ///
+ internal static string FormatInvalidPasswordHasherCompatibilityMode()
+ {
+ return GetString("InvalidPasswordHasherCompatibilityMode");
+ }
+
///
/// Invalid token.
///
diff --git a/src/Microsoft.AspNet.Identity/Resources.resx b/src/Microsoft.AspNet.Identity/Resources.resx
index 56f3d202d2..aad5e2d1e8 100644
--- a/src/Microsoft.AspNet.Identity/Resources.resx
+++ b/src/Microsoft.AspNet.Identity/Resources.resx
@@ -161,6 +161,10 @@
Email '{0}' is invalid.
invalid email
+
+ The provided PasswordHasherCompatibilityMode is invalid.
+ Error when the password hasher doesn't understand the format it's being asked to produce.
+
Invalid token.
Error when a token is not recognized
diff --git a/src/Microsoft.AspNet.Identity/project.json b/src/Microsoft.AspNet.Identity/project.json
index 0e8a569a52..0ef1603874 100644
--- a/src/Microsoft.AspNet.Identity/project.json
+++ b/src/Microsoft.AspNet.Identity/project.json
@@ -29,9 +29,9 @@
"System.Runtime": "4.0.20-beta-*",
"System.Runtime.Extensions": "4.0.10-beta-*",
"System.Security.Claims": "4.0.0-beta-*",
- "System.Security.Cryptography.DeriveBytes": "4.0.0-beta-*",
"System.Security.Cryptography.Hashing": "4.0.0-beta-*",
"System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*",
+ "System.Security.Cryptography.RandomNumberGenerator": "4.0.0-beta-*",
"System.Security.Principal": "4.0.0-beta-*",
"System.Text.Encoding": "4.0.10-beta-*",
"System.Text.Encoding.Extensions": "4.0.10-beta-*",
diff --git a/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs b/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs
index 5ce7b7ce57..c04b73efd6 100644
--- a/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs
+++ b/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs
@@ -4,6 +4,7 @@
using System.Security.Claims;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
+using Microsoft.Framework.OptionsModel;
using Xunit;
namespace Microsoft.AspNet.Identity.Test
@@ -31,7 +32,7 @@ namespace Microsoft.AspNet.Identity.Test
[Fact]
public void CanSpecifyPasswordHasherInstance()
{
- CanOverride>(new PasswordHasher());
+ CanOverride>(new PasswordHasher(new PasswordHasherOptionsAccessor()));
}
[Fact]
@@ -39,6 +40,7 @@ namespace Microsoft.AspNet.Identity.Test
{
var services = new ServiceCollection();
services.AddIdentity();
+ services.Add(OptionsServices.GetDefaultServices());
var provider = services.BuildServiceProvider();
var userValidator = provider.GetRequiredService>() as UserValidator;
diff --git a/test/Microsoft.AspNet.Identity.Test/PasswordHasherTest.cs b/test/Microsoft.AspNet.Identity.Test/PasswordHasherTest.cs
new file mode 100644
index 0000000000..fa86312974
--- /dev/null
+++ b/test/Microsoft.AspNet.Identity.Test/PasswordHasherTest.cs
@@ -0,0 +1,175 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using Microsoft.Framework.OptionsModel;
+using Xunit;
+
+namespace Microsoft.AspNet.Identity.Test
+{
+ public class PasswordHasherTest
+ {
+ [Fact]
+ public void Ctor_InvalidCompatMode_Throws()
+ {
+ // Act & assert
+ var ex = Assert.Throws(() =>
+ {
+ new PasswordHasher(compatMode: (PasswordHasherCompatibilityMode)(-1));
+ });
+ Assert.Equal("The provided PasswordHasherCompatibilityMode is invalid.", ex.Message);
+ }
+
+ [Theory]
+ [InlineData(PasswordHasherCompatibilityMode.IdentityV2)]
+ [InlineData(PasswordHasherCompatibilityMode.IdentityV3)]
+ public void FullRoundTrip(PasswordHasherCompatibilityMode compatMode)
+ {
+ // Arrange
+ var hasher = new PasswordHasher(compatMode: compatMode);
+
+ // Act & assert - success case
+ var hashedPassword = hasher.HashPassword(null, "password 1");
+ var successResult = hasher.VerifyHashedPassword(null, hashedPassword, "password 1");
+ Assert.Equal(PasswordVerificationResult.Success, successResult);
+
+ // Act & assert - failure case
+ var failedResult = hasher.VerifyHashedPassword(null, hashedPassword, "password 2");
+ Assert.Equal(PasswordVerificationResult.Failed, failedResult);
+ }
+
+ [Fact]
+ public void HashPassword_DefaultsToVersion3()
+ {
+ // Arrange
+ var hasher = new PasswordHasher(compatMode: null);
+
+ // Act
+ string retVal = hasher.HashPassword(null, "my password");
+
+ // Assert
+ Assert.Equal("AQAAAAEAACcQAAAAEAABAgMEBQYHCAkKCwwNDg+yWU7rLgUwPZb1Itsmra7cbxw2EFpwpVFIEtP+JIuUEw==", retVal);
+ }
+
+ [Fact]
+ public void HashPassword_Version2()
+ {
+ // Arrange
+ var hasher = new PasswordHasher(compatMode: PasswordHasherCompatibilityMode.IdentityV2);
+
+ // Act
+ string retVal = hasher.HashPassword(null, "my password");
+
+ // Assert
+ Assert.Equal("AAABAgMEBQYHCAkKCwwNDg+ukCEMDf0yyQ29NYubggHIVY0sdEUfdyeM+E1LtH1uJg==", retVal);
+ }
+
+ [Fact]
+ public void HashPassword_Version3()
+ {
+ // Arrange
+ var hasher = new PasswordHasher(compatMode: PasswordHasherCompatibilityMode.IdentityV3);
+
+ // Act
+ string retVal = hasher.HashPassword(null, "my password");
+
+ // Assert
+ Assert.Equal("AQAAAAEAACcQAAAAEAABAgMEBQYHCAkKCwwNDg+yWU7rLgUwPZb1Itsmra7cbxw2EFpwpVFIEtP+JIuUEw==", retVal);
+ }
+
+ [Theory]
+ // Version 2 payloads
+ [InlineData("AAABAgMEBQYHCAkKCwwNDg+uAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALtH1uJg==")] // incorrect password
+ [InlineData("AAABAgMEBQYHCAkKCwwNDg+ukCEMDf0yyQ29NYubggE=")] // too short
+ [InlineData("AAABAgMEBQYHCAkKCwwNDg+ukCEMDf0yyQ29NYubggHIVY0sdEUfdyeM+E1LtH1uJgAAAAAAAAAAAAA=")] // extra data at end
+ // Version 3 payloads
+ [InlineData("AQAAAAAAAAD6AAAAEAhftMyfTJyAAAAAAAAAAAAAAAAAAAih5WsjXaR3PA9M")] // incorrect password
+ [InlineData("AQAAAAIAAAAyAAAAEOMwvh3+FZxqkdMBz2ekgGhwQ4A=")] // too short
+ [InlineData("AQAAAAIAAAAyAAAAEOMwvh3+FZxqkdMBz2ekgGhwQ4B6pZWND6zgESBuWiHwAAAAAAAAAAAA")] // extra data at end
+ public void VerifyHashedPassword_FailureCases(string hashedPassword)
+ {
+ // Arrange
+ var hasher = new PasswordHasher();
+
+ // Act
+ var result = hasher.VerifyHashedPassword(null, hashedPassword, "my password");
+
+ // Assert
+ Assert.Equal(PasswordVerificationResult.Failed, result);
+ }
+
+ [Theory]
+ // Version 2 payloads
+ [InlineData("ANXrDknc7fGPpigibZXXZFMX4aoqz44JveK6jQuwY3eH/UyPhvr5xTPeGYEckLxz9A==")] // SHA1, 1000 iterations, 128-bit salt, 256-bit subkey
+ // Version 3 payloads
+ [InlineData("AQAAAAIAAAAyAAAAEOMwvh3+FZxqkdMBz2ekgGhwQ4B6pZWND6zgESBuWiHw")] // SHA512, 50 iterations, 128-bit salt, 128-bit subkey
+ [InlineData("AQAAAAIAAAD6AAAAIJbVi5wbMR+htSfFp8fTw8N8GOS/Sje+S/4YZcgBfU7EQuqv4OkVYmc4VJl9AGZzmRTxSkP7LtVi9IWyUxX8IAAfZ8v+ZfhjCcudtC1YERSqE1OEdXLW9VukPuJWBBjLuw==")] // SHA512, 250 iterations, 256-bit salt, 512-bit subkey
+ [InlineData("AQAAAAAAAAD6AAAAEAhftMyfTJylOlZT+eEotFXd1elee8ih5WsjXaR3PA9M")] // SHA1, 250 iterations, 128-bit salt, 128-bit subkey
+ [InlineData("AQAAAAEAA9CQAAAAIESkQuj2Du8Y+kbc5lcN/W/3NiAZFEm11P27nrSN5/tId+bR1SwV8CO1Jd72r4C08OLvplNlCDc3oQZ8efcW+jQ=")] // SHA256, 250000 iterations, 256-bit salt, 256-bit subkey
+ public void VerifyHashedPassword_Version2CompatMode_SuccessCases(string hashedPassword)
+ {
+ // Arrange
+ var hasher = new PasswordHasher(compatMode: PasswordHasherCompatibilityMode.IdentityV2);
+
+ // Act
+ var result = hasher.VerifyHashedPassword(null, hashedPassword, "my password");
+
+ // Assert
+ Assert.Equal(PasswordVerificationResult.Success, result);
+ }
+
+ [Theory]
+ // Version 2 payloads
+ [InlineData("ANXrDknc7fGPpigibZXXZFMX4aoqz44JveK6jQuwY3eH/UyPhvr5xTPeGYEckLxz9A==", PasswordVerificationResult.SuccessRehashNeeded)] // SHA1, 1000 iterations, 128-bit salt, 256-bit subkey
+ // Version 3 payloads
+ [InlineData("AQAAAAIAAAAyAAAAEOMwvh3+FZxqkdMBz2ekgGhwQ4B6pZWND6zgESBuWiHw", PasswordVerificationResult.Success)] // SHA512, 50 iterations, 128-bit salt, 128-bit subkey
+ [InlineData("AQAAAAIAAAD6AAAAIJbVi5wbMR+htSfFp8fTw8N8GOS/Sje+S/4YZcgBfU7EQuqv4OkVYmc4VJl9AGZzmRTxSkP7LtVi9IWyUxX8IAAfZ8v+ZfhjCcudtC1YERSqE1OEdXLW9VukPuJWBBjLuw==", PasswordVerificationResult.Success)] // SHA512, 250 iterations, 256-bit salt, 512-bit subkey
+ [InlineData("AQAAAAAAAAD6AAAAEAhftMyfTJylOlZT+eEotFXd1elee8ih5WsjXaR3PA9M", PasswordVerificationResult.Success)] // SHA1, 250 iterations, 128-bit salt, 128-bit subkey
+ [InlineData("AQAAAAEAA9CQAAAAIESkQuj2Du8Y+kbc5lcN/W/3NiAZFEm11P27nrSN5/tId+bR1SwV8CO1Jd72r4C08OLvplNlCDc3oQZ8efcW+jQ=", PasswordVerificationResult.Success)] // SHA256, 250000 iterations, 256-bit salt, 256-bit subkey
+ public void VerifyHashedPassword_Version3CompatMode_SuccessCases(string hashedPassword, PasswordVerificationResult expectedResult)
+ {
+ // Arrange
+ var hasher = new PasswordHasher(compatMode: PasswordHasherCompatibilityMode.IdentityV3);
+
+ // Act
+ var actualResult = hasher.VerifyHashedPassword(null, hashedPassword, "my password");
+
+ // Assert
+ Assert.Equal(expectedResult, actualResult);
+ }
+
+ private sealed class PasswordHasher : PasswordHasher