diff --git a/PlexRequests.Core/UserMapper.cs b/PlexRequests.Core/UserMapper.cs index 35c5c3781..f2f397432 100644 --- a/PlexRequests.Core/UserMapper.cs +++ b/PlexRequests.Core/UserMapper.cs @@ -26,11 +26,13 @@ #endregion using System; using System.Linq; +using System.Security; using Nancy; using Nancy.Authentication.Forms; using Nancy.Security; +using PlexRequests.Helpers; using PlexRequests.Store; namespace PlexRequests.Core @@ -44,7 +46,7 @@ namespace PlexRequests.Core private static ISqliteConfiguration Db { get; set; } public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context) { - var repo = new UserRepository(Db); + var repo = new UserRepository(Db); var user = repo.Get(identifier.ToString()); @@ -61,35 +63,65 @@ namespace PlexRequests.Core public static Guid? ValidateUser(string username, string password) { - var repo = new UserRepository(Db); + var repo = new UserRepository(Db); var users = repo.GetAll(); - var userRecord = users.FirstOrDefault(u => u.UserName.Equals(username, StringComparison.InvariantCultureIgnoreCase) && u.Password.Equals(password)); // TODO hashing - if (userRecord == null) + foreach (var u in users) { - return null; + if (username == u.UserName) + { + var passwordMatch = PasswordHasher.VerifyPassword(password, u.Salt, u.Hash); + if (passwordMatch) + { + return new Guid(u.User); + } + } } - - return new Guid(userRecord.User); + return null; } public static bool DoUsersExist() { - var repo = new UserRepository(Db); + var repo = new UserRepository(Db); var users = repo.GetAll(); + return users.Any(); } public static Guid? CreateUser(string username, string password) { - var repo = new UserRepository(Db); + var repo = new UserRepository(Db); + var salt = PasswordHasher.GenerateSalt(); - var userModel = new UserModel { UserName = username, User = Guid.NewGuid().ToString(), Password = password }; + var userModel = new UsersModel { UserName = username, User = Guid.NewGuid().ToString(), Salt = salt, Hash = PasswordHasher.ComputeHash(password, salt)}; repo.Insert(userModel); var userRecord = repo.Get(userModel.User); return new Guid(userRecord.User); } + + public static bool UpdateUser(string username, string oldPassword, string newPassword) + { + var repo = new UserRepository(Db); + var users = repo.GetAll(); + var userToChange = users.FirstOrDefault(x => x.UserName == username); + if (userToChange == null) + return false; + + var passwordMatch = PasswordHasher.VerifyPassword(oldPassword, userToChange.Salt, userToChange.Hash); + if (!passwordMatch) + { + throw new SecurityException("Password does not match"); + } + + var newSalt = PasswordHasher.GenerateSalt(); + var newHash = PasswordHasher.ComputeHash(newPassword, newSalt); + + userToChange.Hash = newHash; + userToChange.Salt = newSalt; + + return repo.Update(userToChange); + } } } diff --git a/PlexRequests.Helpers.Tests/PasswordHasherTests.cs b/PlexRequests.Helpers.Tests/PasswordHasherTests.cs new file mode 100644 index 000000000..500d07534 --- /dev/null +++ b/PlexRequests.Helpers.Tests/PasswordHasherTests.cs @@ -0,0 +1,50 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: AssemblyHelperTests.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System.Diagnostics; + +using NUnit.Framework; + +namespace PlexRequests.Helpers.Tests +{ + [TestFixture] + public class PasswordHasherTests + { + [Test] + public void TestHash() + { + var password = "abcdef"; + var salt = PasswordHasher.GenerateSalt(); + var hash = PasswordHasher.ComputeHash(password, salt); + + Assert.That(hash, Is.Not.EqualTo(password)); + + var match = PasswordHasher.VerifyPassword(password, salt, hash); + + Assert.That(match, Is.True); + } + } +} \ No newline at end of file diff --git a/PlexRequests.Helpers.Tests/PlexRequests.Helpers.Tests.csproj b/PlexRequests.Helpers.Tests/PlexRequests.Helpers.Tests.csproj index 4bacd1b68..bb9e7143b 100644 --- a/PlexRequests.Helpers.Tests/PlexRequests.Helpers.Tests.csproj +++ b/PlexRequests.Helpers.Tests/PlexRequests.Helpers.Tests.csproj @@ -70,6 +70,7 @@ + diff --git a/PlexRequests.Helpers/PasswordHasher.cs b/PlexRequests.Helpers/PasswordHasher.cs new file mode 100644 index 000000000..68d797ae8 --- /dev/null +++ b/PlexRequests.Helpers/PasswordHasher.cs @@ -0,0 +1,69 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PasswordHasher.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System.Collections.Generic; +using System.Security.Cryptography; +namespace PlexRequests.Helpers +{ + + public static class PasswordHasher + { + // 24 = 192 bits + private const int SaltByteSize = 24; + private const int HashByteSize = 24; + private const int HasingIterationsCount = 10101; + + public static byte[] ComputeHash(string password, byte[] salt, int iterations = HasingIterationsCount, int hashByteSize = HashByteSize) + { + var hashGenerator = new Rfc2898DeriveBytes(password, salt) { IterationCount = iterations }; + return hashGenerator.GetBytes(hashByteSize); + } + + public static byte[] GenerateSalt(int saltByteSize = SaltByteSize) + { + var saltGenerator = new RNGCryptoServiceProvider(); + var salt = new byte[saltByteSize]; + saltGenerator.GetBytes(salt); + return salt; + } + + public static bool VerifyPassword(string password, byte[] passwordSalt, byte[] passwordHash) + { + var computedHash = ComputeHash(password, passwordSalt); + return AreHashesEqual(computedHash, passwordHash); + } + + //Length constant verification - prevents timing attack + private static bool AreHashesEqual(IReadOnlyList firstHash, IReadOnlyList secondHash) + { + var minHashLength = firstHash.Count <= secondHash.Count ? firstHash.Count : secondHash.Count; + var xor = firstHash.Count ^ secondHash.Count; + for (var i = 0; i < minHashLength; i++) + xor |= firstHash[i] ^ secondHash[i]; + return 0 == xor; + } + } +} diff --git a/PlexRequests.Helpers/PlexRequests.Helpers.csproj b/PlexRequests.Helpers/PlexRequests.Helpers.csproj index 35d56a192..beaf7c117 100644 --- a/PlexRequests.Helpers/PlexRequests.Helpers.csproj +++ b/PlexRequests.Helpers/PlexRequests.Helpers.csproj @@ -53,6 +53,7 @@ + diff --git a/PlexRequests.Store/PlexRequests.Store.csproj b/PlexRequests.Store/PlexRequests.Store.csproj index 3772c77a4..d9586aba3 100644 --- a/PlexRequests.Store/PlexRequests.Store.csproj +++ b/PlexRequests.Store/PlexRequests.Store.csproj @@ -70,6 +70,8 @@ + + True @@ -77,7 +79,6 @@ Sql.resx - diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index 23d49a87c..1a2f5b32a 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -1,11 +1,12 @@ --Any DB changes need to be made in this file. -CREATE TABLE IF NOT EXISTS User +CREATE TABLE IF NOT EXISTS Users ( Id INTEGER PRIMARY KEY AUTOINCREMENT, - User varchar(50) NOT NULL , + UserGuid varchar(50) NOT NULL , UserName varchar(50) NOT NULL, - Password varchar(100) NOT NULL + Salt BLOB NOT NULL, + Hash BLOB NOT NULL ); diff --git a/PlexRequests.Store/UserModel.cs b/PlexRequests.Store/UserEntity.cs similarity index 90% rename from PlexRequests.Store/UserModel.cs rename to PlexRequests.Store/UserEntity.cs index 8bf3e0fc3..343794896 100644 --- a/PlexRequests.Store/UserModel.cs +++ b/PlexRequests.Store/UserEntity.cs @@ -1,7 +1,7 @@ #region Copyright // /************************************************************************ // Copyright (c) 2016 Jamie Rees -// File: UserModel.cs +// File: UserEntity.cs // Created By: Jamie Rees // // Permission is hereby granted, free of charge, to any person obtaining @@ -28,11 +28,11 @@ using Dapper.Contrib.Extensions; namespace PlexRequests.Store { - [Table("User")] - public class UserModel : Entity + public class UserEntity { + [Key] + public int Id { get; set; } public string User { get; set; } public string UserName { get; set; } - public string Password { get; set; } } -} +} \ No newline at end of file diff --git a/PlexRequests.Store/UserRepository.cs b/PlexRequests.Store/UserRepository.cs index 4f2e6de90..47bea4ce3 100644 --- a/PlexRequests.Store/UserRepository.cs +++ b/PlexRequests.Store/UserRepository.cs @@ -32,7 +32,7 @@ using Dapper.Contrib.Extensions; namespace PlexRequests.Store { - public class UserRepository : IRepository where T : UserModel + public class UserRepository : IRepository where T : UserEntity { public UserRepository(ISqliteConfiguration config) { diff --git a/PlexRequests.Store/UsersModel.cs b/PlexRequests.Store/UsersModel.cs new file mode 100644 index 000000000..e0376b7ba --- /dev/null +++ b/PlexRequests.Store/UsersModel.cs @@ -0,0 +1,37 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: UserModel.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using Dapper.Contrib.Extensions; + +namespace PlexRequests.Store +{ + [Table("Users")] + public class UsersModel : UserEntity + { + public byte[] Hash { get; set; } + public byte[] Salt { get; set; } + } +} diff --git a/PlexRequests.UI/Modules/LoginModule.cs b/PlexRequests.UI/Modules/LoginModule.cs index d24bf19bf..1a10bcc77 100644 --- a/PlexRequests.UI/Modules/LoginModule.cs +++ b/PlexRequests.UI/Modules/LoginModule.cs @@ -30,6 +30,8 @@ using System.Dynamic; using Nancy; using Nancy.Authentication.Forms; using Nancy.Extensions; +using Nancy.Responses.Negotiation; +using Nancy.Security; using PlexRequests.Core; using PlexRequests.UI.Models; @@ -81,7 +83,6 @@ namespace PlexRequests.UI.Modules return View["Login/Register", model]; } - }; Post["/register"] = x => @@ -96,6 +97,30 @@ namespace PlexRequests.UI.Modules Session[SessionKeys.UsernameKey] = username; return this.LoginAndRedirect((Guid)userId); }; + + Get["/changepassword"] = _ => ChangePassword(); + Post["/changepassword"] = _ => ChangePasswordPost(); + } + + private Negotiator ChangePassword() + { + this.RequiresAuthentication(); + return View["ChangePassword"]; + } + + private Negotiator ChangePasswordPost() + { + var username = Context.CurrentUser.UserName; + var oldPass = Request.Form.OldPassword; + var newPassword = Request.Form.NewPassword; + var newPasswordAgain = Request.Form.NewPasswordAgain; + if (!newPassword.Equals(newPasswordAgain)) + { + + } + + var result = UserMapper.UpdateUser(username, oldPass, newPassword); + return View["ChangePassword"]; } } } \ No newline at end of file diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 33633179e..589fc3b9e 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -328,6 +328,9 @@ Always + + Always + web.config diff --git a/PlexRequests.UI/Views/Login/ChangePassword.cshtml b/PlexRequests.UI/Views/Login/ChangePassword.cshtml new file mode 100644 index 000000000..f87dfa76e --- /dev/null +++ b/PlexRequests.UI/Views/Login/ChangePassword.cshtml @@ -0,0 +1,9 @@ +
+
+ Old Password + New Password + New Password again +
+
+ +