diff --git a/src/Microsoft.AspNet.Identity/Crypto.cs b/src/Microsoft.AspNet.Identity/Crypto.cs index 86e26060..520f2abb 100644 --- a/src/Microsoft.AspNet.Identity/Crypto.cs +++ b/src/Microsoft.AspNet.Identity/Crypto.cs @@ -1,10 +1,105 @@ -#if K10 +#if NET45 using System; using System.Runtime.CompilerServices; -//using System.Security.Cryptography; +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"); + } + + var hashedPasswordBytes = Convert.FromBase64String(hashedPassword); + + // Verify a version 0 (see comment above) text hash. + + 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; + } + } } #endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/PasswordHasher.cs b/src/Microsoft.AspNet.Identity/PasswordHasher.cs new file mode 100644 index 00000000..252e8150 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/PasswordHasher.cs @@ -0,0 +1,37 @@ +namespace Microsoft.AspNet.Identity +{ + /// + /// Implements password hashing methods + /// + public class PasswordHasher : IPasswordHasher + { + /// + /// Hash a password + /// + /// + /// + public virtual string HashPassword(string password) + { +#if NET45 + return Crypto.HashPassword(password); +#else + return password; +#endif + } + + /// + /// Verify that a password matches the hashedPassword + /// + /// + /// + /// + public virtual PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) + { +#if NET45 + return Crypto.VerifyHashedPassword(hashedPassword, providedPassword) ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed; +#else + return hashedPassword == providedPassword ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed; +#endif + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/UserManager.cs b/src/Microsoft.AspNet.Identity/UserManager.cs index 7535483f..b8768dca 100644 --- a/src/Microsoft.AspNet.Identity/UserManager.cs +++ b/src/Microsoft.AspNet.Identity/UserManager.cs @@ -56,7 +56,7 @@ namespace Microsoft.AspNet.Identity } Store = store; UserValidator = new UserValidator(this); - //PasswordHasher = new PasswordHasher(); + PasswordHasher = new PasswordHasher(); //ClaimsIdentityFactory = new ClaimsIdentityFactory(); } diff --git a/src/Microsoft.AspNet.Identity/project.json b/src/Microsoft.AspNet.Identity/project.json index 1c1f0c09..de269b57 100644 --- a/src/Microsoft.AspNet.Identity/project.json +++ b/src/Microsoft.AspNet.Identity/project.json @@ -1,6 +1,7 @@ { "version" : "0.1-alpha-*", "dependencies": { + "System.Collections.Immutable" : "1.1.15.0", "Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*", "System.Security.Claims" : "0.1-alpha-*" }, diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStoreTest.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStoreTest.cs index 1e889bbc..d23ebd5e 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStoreTest.cs +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStoreTest.cs @@ -1,10 +1,6 @@ using System; using System.Linq; -#if NET45 using System.Security.Claims; -#else -using System.Security.ClaimsK; -#endif using System.Threading.Tasks; using Xunit; @@ -53,21 +49,21 @@ namespace Microsoft.AspNet.Identity.InMemory.Test Assert.Equal(providerKey, logins.First().ProviderKey); } - //[Fact] - //public async Task CreateUserLoginAndAddPasswordTest() - //{ - // var manager = CreateManager(); - // var login = new UserLoginInfo("Provider", "key"); - // var user = new InMemoryUser("CreateUserLoginAddPasswordTest"); - // UnitTestHelper.IsSuccess(await manager.Create(user)); - // UnitTestHelper.IsSuccess(await manager.AddLogin(user.Id, login)); - // UnitTestHelper.IsSuccess(await manager.AddPassword(user.Id, "password")); - // var logins = await manager.GetLogins(user.Id); - // Assert.NotNull(logins); - // Assert.Equal(1, logins.Count()); - // Assert.Equal(user, await manager.Find(login)); - // Assert.Equal(user, await manager.Find(user.UserName, "password")); - //} + [Fact] + public async Task CreateUserLoginAndAddPasswordTest() + { + var manager = CreateManager(); + var login = new UserLoginInfo("Provider", "key"); + var user = new InMemoryUser("CreateUserLoginAddPasswordTest"); + UnitTestHelper.IsSuccess(await manager.Create(user)); + UnitTestHelper.IsSuccess(await manager.AddLogin(user.Id, login)); + UnitTestHelper.IsSuccess(await manager.AddPassword(user.Id, "password")); + var logins = await manager.GetLogins(user.Id); + Assert.NotNull(logins); + Assert.Equal(1, logins.Count()); + Assert.Equal(user, await manager.Find(login)); + Assert.Equal(user, await manager.Find(user.UserName, "password")); + } [Fact] public async Task CreateUserAddRemoveLoginTest() @@ -94,36 +90,36 @@ namespace Microsoft.AspNet.Identity.InMemory.Test Assert.NotEqual(stamp, user.SecurityStamp); } - //[Fact] - //public async Task RemovePasswordTest() - //{ - // var manager = CreateManager(); - // var user = new InMemoryUser("RemovePasswordTest"); - // const string password = "password"; - // UnitTestHelper.IsSuccess(await manager.Create(user, password)); - // var stamp = user.SecurityStamp; - // UnitTestHelper.IsSuccess(await manager.RemovePassword(user.Id)); - // var u = await manager.FindByName(user.UserName); - // Assert.NotNull(u); - // Assert.Null(u.PasswordHash); - // Assert.NotEqual(stamp, user.SecurityStamp); - //} + [Fact] + public async Task RemovePasswordTest() + { + var manager = CreateManager(); + var user = new InMemoryUser("RemovePasswordTest"); + const string password = "password"; + UnitTestHelper.IsSuccess(await manager.Create(user, password)); + var stamp = user.SecurityStamp; + UnitTestHelper.IsSuccess(await manager.RemovePassword(user.Id)); + var u = await manager.FindByName(user.UserName); + Assert.NotNull(u); + Assert.Null(u.PasswordHash); + Assert.NotEqual(stamp, user.SecurityStamp); + } - //[Fact] - //public async Task ChangePasswordTest() - //{ - // var manager = CreateManager(); - // var user = new InMemoryUser("ChangePasswordTest"); - // const string password = "password"; - // const string newPassword = "newpassword"; - // UnitTestHelper.IsSuccess(await manager.Create(user, password)); - // var stamp = user.SecurityStamp; - // Assert.NotNull(stamp); - // UnitTestHelper.IsSuccess(await manager.ChangePassword(user.Id, password, newPassword)); - // Assert.Null(await manager.Find(user.UserName, password)); - // Assert.Equal(user, await manager.Find(user.UserName, newPassword)); - // Assert.NotEqual(stamp, user.SecurityStamp); - //} + [Fact] + public async Task ChangePasswordTest() + { + var manager = CreateManager(); + var user = new InMemoryUser("ChangePasswordTest"); + const string password = "password"; + const string newPassword = "newpassword"; + UnitTestHelper.IsSuccess(await manager.Create(user, password)); + var stamp = user.SecurityStamp; + Assert.NotNull(stamp); + UnitTestHelper.IsSuccess(await manager.ChangePassword(user.Id, password, newPassword)); + Assert.Null(await manager.Find(user.UserName, password)); + Assert.Equal(user, await manager.Find(user.UserName, newPassword)); + Assert.NotEqual(stamp, user.SecurityStamp); + } [Fact] public async Task AddRemoveUserClaimTest() @@ -149,26 +145,15 @@ namespace Microsoft.AspNet.Identity.InMemory.Test Assert.Equal(0, userClaims.Count); } - //[Fact] - //public async Task ChangePasswordFallsIfPasswordTooShortTest() - //{ - // var manager = CreateManager(); - // var user = new InMemoryUser("user"); - // const string password = "password"; - // UnitTestHelper.IsSuccess(await manager.Create(user, password)); - // var result = await manager.ChangePassword(user.Id, password, "n"); - // UnitTestHelper.IsFailure(result, "Passwords must be at least 6 characters."); - //} - - //[Fact] - //public async Task ChangePasswordFallsIfPasswordWrongTest() - //{ - // var manager = CreateManager(); - // var user = new InMemoryUser("user"); - // UnitTestHelper.IsSuccess(await manager.Create(user, "password")); - // var result = await manager.ChangePassword(user.Id, "bogus", "newpassword"); - // UnitTestHelper.IsFailure(result, "Incorrect password."); - //} + [Fact] + public async Task ChangePasswordFallsIfPasswordWrongTest() + { + var manager = CreateManager(); + var user = new InMemoryUser("user"); + UnitTestHelper.IsSuccess(await manager.Create(user, "password")); + var result = await manager.ChangePassword(user.Id, "bogus", "newpassword"); + UnitTestHelper.IsFailure(result, "Incorrect password."); + } [Fact] public async Task AddDupeUserFailsTest() @@ -193,17 +178,17 @@ namespace Microsoft.AspNet.Identity.InMemory.Test Assert.NotEqual(stamp, user.SecurityStamp); } - //[Fact] - //public async Task AddDupeLoginFailsTest() - //{ - // var manager = CreateManager(); - // var user = new InMemoryUser("DupeLogin"); - // var login = new UserLoginInfo("provder", "key"); - // UnitTestHelper.IsSuccess(await manager.Create(user)); - // UnitTestHelper.IsSuccess(await manager.AddLogin(user.Id, login)); - // var result = await manager.AddLogin(user.Id, login); - // UnitTestHelper.IsFailure(result, "A user with that external login already exists."); - //} + [Fact] + public async Task AddDupeLoginFailsTest() + { + var manager = CreateManager(); + var user = new InMemoryUser("DupeLogin"); + var login = new UserLoginInfo("provder", "key"); + UnitTestHelper.IsSuccess(await manager.Create(user)); + UnitTestHelper.IsSuccess(await manager.AddLogin(user.Id, login)); + var result = await manager.AddLogin(user.Id, login); + UnitTestHelper.IsFailure(result, "A user with that external login already exists."); + } // Lockout tests @@ -533,7 +518,7 @@ namespace Microsoft.AspNet.Identity.InMemory.Test new InMemoryUser("1"), new InMemoryUser("2"), new InMemoryUser("3"), new InMemoryUser("4") }; - foreach (InMemoryUser u in users) + foreach (var u in users) { UnitTestHelper.IsSuccess(await manager.Create(u)); UnitTestHelper.IsSuccess(await manager.AddToRole(u.Id, role.Name));