From 8c04df640384602673ad7feab2cc9972ee67c8de Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 11 Jul 2022 20:00:10 -0700 Subject: [PATCH] New: Migrate user passwords to Pbkdf2 (cherry picked from commit 269e72a2193b584476bec338ef41e6fb2e5cbea6) (cherry picked from commit 104aadfdb7feb7143c41da790496a384ffb29fc8) --- src/NzbDrone.Core/Authentication/User.cs | 4 +- .../Authentication/UserService.cs | 76 ++++++++++++++++--- .../Migration/073_add_salt_to_users.cs | 16 ++++ src/NzbDrone.Core/Lidarr.Core.csproj | 1 + 4 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/073_add_salt_to_users.cs diff --git a/src/NzbDrone.Core/Authentication/User.cs b/src/NzbDrone.Core/Authentication/User.cs index 794d4824a..63c67bd5f 100644 --- a/src/NzbDrone.Core/Authentication/User.cs +++ b/src/NzbDrone.Core/Authentication/User.cs @@ -1,4 +1,4 @@ -using System; +using System; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Authentication @@ -8,5 +8,7 @@ namespace NzbDrone.Core.Authentication public Guid Identifier { get; set; } public string Username { get; set; } public string Password { get; set; } + public string Salt { get; set; } + public int Iterations { get; set; } } } diff --git a/src/NzbDrone.Core/Authentication/UserService.cs b/src/NzbDrone.Core/Authentication/UserService.cs index 00a7018de..2a5cb5463 100644 --- a/src/NzbDrone.Core/Authentication/UserService.cs +++ b/src/NzbDrone.Core/Authentication/UserService.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; @@ -21,15 +23,16 @@ namespace NzbDrone.Core.Authentication public class UserService : IUserService, IHandle { + private const int ITERATIONS = 10000; + private const int SALT_SIZE = 128 / 8; + private const int NUMBER_OF_BYTES = 256 / 8; + private readonly IUserRepository _repo; private readonly IAppFolderInfo _appFolderInfo; private readonly IDiskProvider _diskProvider; private readonly IConfigFileProvider _configFileProvider; - public UserService(IUserRepository repo, - IAppFolderInfo appFolderInfo, - IDiskProvider diskProvider, - IConfigFileProvider configFileProvider) + public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider) { _repo = repo; _appFolderInfo = appFolderInfo; @@ -39,12 +42,15 @@ namespace NzbDrone.Core.Authentication public User Add(string username, string password) { - return _repo.Insert(new User + var user = new User { Identifier = Guid.NewGuid(), - Username = username.ToLowerInvariant(), - Password = password.SHA256Hash() - }); + Username = username.ToLowerInvariant() + }; + + SetUserHashedPassword(user, password); + + return _repo.Insert(user); } public User Update(User user) @@ -63,7 +69,7 @@ namespace NzbDrone.Core.Authentication if (user.Password != password) { - user.Password = password.SHA256Hash(); + SetUserHashedPassword(user, password); } user.Username = username.ToLowerInvariant(); @@ -90,7 +96,20 @@ namespace NzbDrone.Core.Authentication return null; } - if (user.Password == password.SHA256Hash()) + if (user.Salt.IsNullOrWhiteSpace()) + { + // If password matches stored SHA256 hash, update to salted hash and verify. + if (user.Password == password.SHA256Hash()) + { + SetUserHashedPassword(user, password); + + return Update(user); + } + + return null; + } + + if (VerifyHashedPassword(user, password)) { return user; } @@ -103,6 +122,43 @@ namespace NzbDrone.Core.Authentication return _repo.FindUser(identifier); } + private User SetUserHashedPassword(User user, string password) + { + var salt = GenerateSalt(); + + user.Iterations = ITERATIONS; + user.Salt = Convert.ToBase64String(salt); + user.Password = GetHashedPassword(password, salt, ITERATIONS); + + return user; + } + + private byte[] GenerateSalt() + { + var salt = new byte[SALT_SIZE]; + RandomNumberGenerator.Create().GetBytes(salt); + + return salt; + } + + private string GetHashedPassword(string password, byte[] salt, int iterations) + { + return Convert.ToBase64String(KeyDerivation.Pbkdf2( + password: password, + salt: salt, + prf: KeyDerivationPrf.HMACSHA512, + iterationCount: iterations, + numBytesRequested: NUMBER_OF_BYTES)); + } + + private bool VerifyHashedPassword(User user, string password) + { + var salt = Convert.FromBase64String(user.Salt); + var hashedPassword = GetHashedPassword(password, salt, user.Iterations); + + return user.Password == hashedPassword; + } + public void Handle(ApplicationStartedEvent message) { if (_repo.All().Any()) diff --git a/src/NzbDrone.Core/Datastore/Migration/073_add_salt_to_users.cs b/src/NzbDrone.Core/Datastore/Migration/073_add_salt_to_users.cs new file mode 100644 index 000000000..03a8e2432 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/073_add_salt_to_users.cs @@ -0,0 +1,16 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(073)] + public class add_salt_to_users : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Users") + .AddColumn("Salt").AsString().Nullable() + .AddColumn("Iterations").AsInt32().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/Lidarr.Core.csproj b/src/NzbDrone.Core/Lidarr.Core.csproj index 9383aca2e..7f0bfcd8f 100644 --- a/src/NzbDrone.Core/Lidarr.Core.csproj +++ b/src/NzbDrone.Core/Lidarr.Core.csproj @@ -7,6 +7,7 @@ +