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 int _iterCount;
private readonly RandomNumberGenerator _rng;
/// <summary>
@ -44,11 +45,24 @@ namespace Microsoft.AspNet.Identity
}
_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;
}
@ -114,11 +128,11 @@ namespace Microsoft.AspNet.Identity
return outputBytes;
}
private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng)
private byte[] HashPasswordV3(string password, RandomNumberGenerator rng)
{
return HashPasswordV3(password, rng,
prf: KeyDerivationPrf.Sha256,
iterCount: 10000,
iterCount: _iterCount,
saltSize: 128 / 8,
numBytesRequested: 256 / 8);
}
@ -140,11 +154,6 @@ namespace Microsoft.AspNet.Identity
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)
@ -194,9 +203,18 @@ namespace Microsoft.AspNet.Identity
}
case 0x01:
return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword)
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
int embeddedIterCount;
if (VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, out embeddedIterCount))
{
// 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:
return PasswordVerificationResult.Failed; // unknown format marker
@ -227,13 +245,15 @@ namespace Microsoft.AspNet.Identity
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
{
// Read header information
KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
int iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);
// 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.
using System.Security.Cryptography;
using Microsoft.AspNet.Security.DataProtection;
namespace Microsoft.AspNet.Identity
{
@ -20,7 +21,16 @@ namespace Microsoft.AspNet.Identity
/// </remarks>
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
internal RandomNumberGenerator Rng { get; set; } = _defaultRng;
}
}
}

View File

@ -202,6 +202,22 @@ namespace Microsoft.AspNet.Identity
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>
/// Invalid token.
/// </summary>

View File

@ -165,6 +165,10 @@
<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>
</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">
<value>Invalid token.</value>
<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);
}
[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]
[InlineData(PasswordHasherCompatibilityMode.IdentityV2)]
[InlineData(PasswordHasherCompatibilityMode.IdentityV3)]
@ -123,9 +136,9 @@ namespace Microsoft.AspNet.Identity.Test
// 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("AQAAAAIAAAAyAAAAEOMwvh3+FZxqkdMBz2ekgGhwQ4B6pZWND6zgESBuWiHw", PasswordVerificationResult.SuccessRehashNeeded)] // SHA512, 50 iterations, 128-bit salt, 128-bit subkey
[InlineData("AQAAAAIAAAD6AAAAIJbVi5wbMR+htSfFp8fTw8N8GOS/Sje+S/4YZcgBfU7EQuqv4OkVYmc4VJl9AGZzmRTxSkP7LtVi9IWyUxX8IAAfZ8v+ZfhjCcudtC1YERSqE1OEdXLW9VukPuJWBBjLuw==", PasswordVerificationResult.SuccessRehashNeeded)] // SHA512, 250 iterations, 256-bit salt, 512-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
public void VerifyHashedPassword_Version3CompatMode_SuccessCases(string hashedPassword, PasswordVerificationResult expectedResult)
{
@ -141,18 +154,22 @@ namespace Microsoft.AspNet.Identity.Test
private sealed class PasswordHasher : PasswordHasher<object>
{
public PasswordHasher(PasswordHasherCompatibilityMode? compatMode = null)
: base(BuildOptions(compatMode))
public PasswordHasher(PasswordHasherCompatibilityMode? compatMode = null, int? iterCount = null)
: base(BuildOptions(compatMode, iterCount))
{
}
private static IOptions<PasswordHasherOptions> BuildOptions(PasswordHasherCompatibilityMode? compatMode)
private static IOptions<PasswordHasherOptions> BuildOptions(PasswordHasherCompatibilityMode? compatMode, int? iterCount)
{
var options = new PasswordHasherOptionsAccessor();
if (compatMode != null)
{
options.Options.CompatibilityMode = (PasswordHasherCompatibilityMode)compatMode;
}
if (iterCount != null)
{
options.Options.IterationCount = (int)iterCount;
}
Assert.NotNull(options.Options.Rng); // should have a default value
options.Options.Rng = new SequentialRandomNumberGenerator();
return options;