Allow the application developer to specify the iteration count for membership passwords.
This commit is contained in:
parent
8672bd7797
commit
bf8728bec9
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 < 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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue