diff --git a/src/Microsoft.AspNet.Identity/PasswordHasher.cs b/src/Microsoft.AspNet.Identity/PasswordHasher.cs index bebcc63614..09557ae983 100644 --- a/src/Microsoft.AspNet.Identity/PasswordHasher.cs +++ b/src/Microsoft.AspNet.Identity/PasswordHasher.cs @@ -30,6 +30,7 @@ namespace Microsoft.AspNet.Identity */ private readonly PasswordHasherCompatibilityMode _compatibilityMode; + private readonly int _iterCount; private readonly RandomNumberGenerator _rng; /// @@ -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 diff --git a/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs b/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs index 5dccb93196..e441a7bee0 100644 --- a/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs +++ b/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs @@ -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 /// public PasswordHasherCompatibilityMode CompatibilityMode { get; set; } = PasswordHasherCompatibilityMode.IdentityV3; + /// + /// Specifies the number of iterations to use when hashing passwords using PBKDF2. + /// + /// + /// 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. + /// + public int IterationCount { get; set; } = 10000; + // 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 26e7ca7330..240e80680c 100644 --- a/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs @@ -202,6 +202,22 @@ namespace Microsoft.AspNet.Identity return GetString("InvalidPasswordHasherCompatibilityMode"); } + /// + /// The iteration count must be a positive integer. + /// + internal static string InvalidPasswordHasherIterationCount + { + get { return GetString("InvalidPasswordHasherIterationCount"); } + } + + /// + /// The iteration count must be a positive integer. + /// + internal static string FormatInvalidPasswordHasherIterationCount() + { + return GetString("InvalidPasswordHasherIterationCount"); + } + /// /// Invalid token. /// diff --git a/src/Microsoft.AspNet.Identity/Resources.resx b/src/Microsoft.AspNet.Identity/Resources.resx index aad5e2d1e8..b51f0157d8 100644 --- a/src/Microsoft.AspNet.Identity/Resources.resx +++ b/src/Microsoft.AspNet.Identity/Resources.resx @@ -165,6 +165,10 @@ The provided PasswordHasherCompatibilityMode is invalid. Error when the password hasher doesn't understand the format it's being asked to produce. + + The iteration count must be a positive integer. + Error when the iteration count is < 1. + Invalid token. Error when a token is not recognized diff --git a/test/Microsoft.AspNet.Identity.Test/PasswordHasherTest.cs b/test/Microsoft.AspNet.Identity.Test/PasswordHasherTest.cs index fa86312974..dcc8e5efa4 100644 --- a/test/Microsoft.AspNet.Identity.Test/PasswordHasherTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/PasswordHasherTest.cs @@ -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(() => + { + 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 { - public PasswordHasher(PasswordHasherCompatibilityMode? compatMode = null) - : base(BuildOptions(compatMode)) + public PasswordHasher(PasswordHasherCompatibilityMode? compatMode = null, int? iterCount = null) + : base(BuildOptions(compatMode, iterCount)) { } - private static IOptions BuildOptions(PasswordHasherCompatibilityMode? compatMode) + private static IOptions 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;