Allow the application developer to specify the iteration count for membership passwords.

This commit is contained in:
Levi B 2014-11-05 12:08:20 -08:00
parent 8672bd7797
commit bf8728bec9
5 changed files with 89 additions and 22 deletions

View File

@ -30,6 +30,7 @@ namespace Microsoft.AspNet.Identity
*/ */
private readonly PasswordHasherCompatibilityMode _compatibilityMode; private readonly PasswordHasherCompatibilityMode _compatibilityMode;
private readonly int _iterCount;
private readonly RandomNumberGenerator _rng; private readonly RandomNumberGenerator _rng;
/// <summary> /// <summary>
@ -44,9 +45,22 @@ namespace Microsoft.AspNet.Identity
} }
_compatibilityMode = options.Options.CompatibilityMode; _compatibilityMode = options.Options.CompatibilityMode;
if (!IsValidCompatibilityMode(_compatibilityMode)) switch (_compatibilityMode)
{ {
throw new InvalidOperationException(Resources.InvalidPasswordHasherCompatibilityMode); case PasswordHasherCompatibilityMode.IdentityV2:
// nothing else to do
break;
case PasswordHasherCompatibilityMode.IdentityV3:
_iterCount = options.Options.IterationCount;
if (_iterCount < 1)
{
throw new InvalidOperationException(Resources.InvalidPasswordHasherIterationCount);
}
break;
default:
throw new InvalidOperationException(Resources.InvalidPasswordHasherCompatibilityMode);
} }
_rng = options.Options.Rng; _rng = options.Options.Rng;
@ -114,11 +128,11 @@ namespace Microsoft.AspNet.Identity
return outputBytes; return outputBytes;
} }
private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng) private byte[] HashPasswordV3(string password, RandomNumberGenerator rng)
{ {
return HashPasswordV3(password, rng, return HashPasswordV3(password, rng,
prf: KeyDerivationPrf.Sha256, prf: KeyDerivationPrf.Sha256,
iterCount: 10000, iterCount: _iterCount,
saltSize: 128 / 8, saltSize: 128 / 8,
numBytesRequested: 256 / 8); numBytesRequested: 256 / 8);
} }
@ -140,11 +154,6 @@ namespace Microsoft.AspNet.Identity
return outputBytes; return outputBytes;
} }
private static bool IsValidCompatibilityMode(PasswordHasherCompatibilityMode compatibilityMode)
{
return (compatibilityMode == PasswordHasherCompatibilityMode.IdentityV2 || compatibilityMode == PasswordHasherCompatibilityMode.IdentityV3);
}
private static uint ReadNetworkByteOrder(byte[] buffer, int offset) private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
{ {
return ((uint)(buffer[offset + 0]) << 24) return ((uint)(buffer[offset + 0]) << 24)
@ -194,9 +203,18 @@ namespace Microsoft.AspNet.Identity
} }
case 0x01: case 0x01:
return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword) int embeddedIterCount;
? PasswordVerificationResult.Success if (VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, out embeddedIterCount))
: PasswordVerificationResult.Failed; {
// If this hasher was configured with a higher iteration count, change the entry now.
return (embeddedIterCount < _iterCount)
? PasswordVerificationResult.SuccessRehashNeeded
: PasswordVerificationResult.Success;
}
else
{
return PasswordVerificationResult.Failed;
}
default: default:
return PasswordVerificationResult.Failed; // unknown format marker return PasswordVerificationResult.Failed; // unknown format marker
@ -227,13 +245,15 @@ namespace Microsoft.AspNet.Identity
return ByteArraysEqual(actualSubkey, expectedSubkey); return ByteArraysEqual(actualSubkey, expectedSubkey);
} }
private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password) private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, out int iterCount)
{ {
iterCount = default(int);
try try
{ {
// Read header information // Read header information
KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1); KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
int iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5); iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9); int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);
// Read the salt: must be >= 128 bits // Read the salt: must be >= 128 bits

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Security.Cryptography; using System.Security.Cryptography;
using Microsoft.AspNet.Security.DataProtection;
namespace Microsoft.AspNet.Identity namespace Microsoft.AspNet.Identity
{ {
@ -20,6 +21,15 @@ namespace Microsoft.AspNet.Identity
/// </remarks> /// </remarks>
public PasswordHasherCompatibilityMode CompatibilityMode { get; set; } = PasswordHasherCompatibilityMode.IdentityV3; public PasswordHasherCompatibilityMode CompatibilityMode { get; set; } = PasswordHasherCompatibilityMode.IdentityV3;
/// <summary>
/// Specifies the number of iterations to use when hashing passwords using PBKDF2.
/// </summary>
/// <remarks>
/// This value is only used when the compatibiliy mode is set to 'V3'.
/// The value must be a positive integer. The default value is 10,000.
/// </remarks>
public int IterationCount { get; set; } = 10000;
// for unit testing // for unit testing
internal RandomNumberGenerator Rng { get; set; } = _defaultRng; internal RandomNumberGenerator Rng { get; set; } = _defaultRng;
} }

View File

@ -202,6 +202,22 @@ namespace Microsoft.AspNet.Identity
return GetString("InvalidPasswordHasherCompatibilityMode"); return GetString("InvalidPasswordHasherCompatibilityMode");
} }
/// <summary>
/// The iteration count must be a positive integer.
/// </summary>
internal static string InvalidPasswordHasherIterationCount
{
get { return GetString("InvalidPasswordHasherIterationCount"); }
}
/// <summary>
/// The iteration count must be a positive integer.
/// </summary>
internal static string FormatInvalidPasswordHasherIterationCount()
{
return GetString("InvalidPasswordHasherIterationCount");
}
/// <summary> /// <summary>
/// Invalid token. /// Invalid token.
/// </summary> /// </summary>

View File

@ -165,6 +165,10 @@
<value>The provided PasswordHasherCompatibilityMode is invalid.</value> <value>The provided PasswordHasherCompatibilityMode is invalid.</value>
<comment>Error when the password hasher doesn't understand the format it's being asked to produce.</comment> <comment>Error when the password hasher doesn't understand the format it's being asked to produce.</comment>
</data> </data>
<data name="InvalidPasswordHasherIterationCount" xml:space="preserve">
<value>The iteration count must be a positive integer.</value>
<comment>Error when the iteration count is &lt; 1.</comment>
</data>
<data name="InvalidToken" xml:space="preserve"> <data name="InvalidToken" xml:space="preserve">
<value>Invalid token.</value> <value>Invalid token.</value>
<comment>Error when a token is not recognized</comment> <comment>Error when a token is not recognized</comment>

View File

@ -21,6 +21,19 @@ namespace Microsoft.AspNet.Identity.Test
Assert.Equal("The provided PasswordHasherCompatibilityMode is invalid.", ex.Message); Assert.Equal("The provided PasswordHasherCompatibilityMode is invalid.", ex.Message);
} }
[Theory]
[InlineData(-1)]
[InlineData(0)]
public void Ctor_InvalidIterCount_Throws(int iterCount)
{
// Act & assert
var ex = Assert.Throws<InvalidOperationException>(() =>
{
new PasswordHasher(iterCount: iterCount);
});
Assert.Equal("The iteration count must be a positive integer.", ex.Message);
}
[Theory] [Theory]
[InlineData(PasswordHasherCompatibilityMode.IdentityV2)] [InlineData(PasswordHasherCompatibilityMode.IdentityV2)]
[InlineData(PasswordHasherCompatibilityMode.IdentityV3)] [InlineData(PasswordHasherCompatibilityMode.IdentityV3)]
@ -123,9 +136,9 @@ namespace Microsoft.AspNet.Identity.Test
// Version 2 payloads // Version 2 payloads
[InlineData("ANXrDknc7fGPpigibZXXZFMX4aoqz44JveK6jQuwY3eH/UyPhvr5xTPeGYEckLxz9A==", PasswordVerificationResult.SuccessRehashNeeded)] // SHA1, 1000 iterations, 128-bit salt, 256-bit subkey [InlineData("ANXrDknc7fGPpigibZXXZFMX4aoqz44JveK6jQuwY3eH/UyPhvr5xTPeGYEckLxz9A==", PasswordVerificationResult.SuccessRehashNeeded)] // SHA1, 1000 iterations, 128-bit salt, 256-bit subkey
// Version 3 payloads // Version 3 payloads
[InlineData("AQAAAAIAAAAyAAAAEOMwvh3+FZxqkdMBz2ekgGhwQ4B6pZWND6zgESBuWiHw", PasswordVerificationResult.Success)] // SHA512, 50 iterations, 128-bit salt, 128-bit subkey [InlineData("AQAAAAIAAAAyAAAAEOMwvh3+FZxqkdMBz2ekgGhwQ4B6pZWND6zgESBuWiHw", PasswordVerificationResult.SuccessRehashNeeded)] // 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("AQAAAAIAAAD6AAAAIJbVi5wbMR+htSfFp8fTw8N8GOS/Sje+S/4YZcgBfU7EQuqv4OkVYmc4VJl9AGZzmRTxSkP7LtVi9IWyUxX8IAAfZ8v+ZfhjCcudtC1YERSqE1OEdXLW9VukPuJWBBjLuw==", PasswordVerificationResult.SuccessRehashNeeded)] // SHA512, 250 iterations, 256-bit salt, 512-bit subkey
[InlineData("AQAAAAAAAAD6AAAAEAhftMyfTJylOlZT+eEotFXd1elee8ih5WsjXaR3PA9M", PasswordVerificationResult.Success)] // SHA1, 250 iterations, 128-bit salt, 128-bit subkey [InlineData("AQAAAAAAAAD6AAAAEAhftMyfTJylOlZT+eEotFXd1elee8ih5WsjXaR3PA9M", PasswordVerificationResult.SuccessRehashNeeded)] // 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 [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) public void VerifyHashedPassword_Version3CompatMode_SuccessCases(string hashedPassword, PasswordVerificationResult expectedResult)
{ {
@ -141,18 +154,22 @@ namespace Microsoft.AspNet.Identity.Test
private sealed class PasswordHasher : PasswordHasher<object> private sealed class PasswordHasher : PasswordHasher<object>
{ {
public PasswordHasher(PasswordHasherCompatibilityMode? compatMode = null) public PasswordHasher(PasswordHasherCompatibilityMode? compatMode = null, int? iterCount = null)
: base(BuildOptions(compatMode)) : base(BuildOptions(compatMode, iterCount))
{ {
} }
private static IOptions<PasswordHasherOptions> BuildOptions(PasswordHasherCompatibilityMode? compatMode) private static IOptions<PasswordHasherOptions> BuildOptions(PasswordHasherCompatibilityMode? compatMode, int? iterCount)
{ {
var options = new PasswordHasherOptionsAccessor(); var options = new PasswordHasherOptionsAccessor();
if (compatMode != null) if (compatMode != null)
{ {
options.Options.CompatibilityMode = (PasswordHasherCompatibilityMode)compatMode; options.Options.CompatibilityMode = (PasswordHasherCompatibilityMode)compatMode;
} }
if (iterCount != null)
{
options.Options.IterationCount = (int)iterCount;
}
Assert.NotNull(options.Options.Rng); // should have a default value Assert.NotNull(options.Options.Rng); // should have a default value
options.Options.Rng = new SequentialRandomNumberGenerator(); options.Options.Rng = new SequentialRandomNumberGenerator();
return options; return options;