New: Migrate user passwords to Pbkdf2

(cherry picked from commit 269e72a2193b584476bec338ef41e6fb2e5cbea6)
(cherry picked from commit 104aadfdb7feb7143c41da790496a384ffb29fc8)
pull/4220/head
Mark McDowall 2 years ago committed by Qstick
parent 092e41264f
commit 8c04df6403

@ -1,4 +1,4 @@
using System; using System;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Authentication namespace NzbDrone.Core.Authentication
@ -8,5 +8,7 @@ namespace NzbDrone.Core.Authentication
public Guid Identifier { get; set; } public Guid Identifier { get; set; }
public string Username { get; set; } public string Username { get; set; }
public string Password { get; set; } public string Password { get; set; }
public string Salt { get; set; }
public int Iterations { get; set; }
} }
} }

@ -1,5 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -21,15 +23,16 @@ namespace NzbDrone.Core.Authentication
public class UserService : IUserService, IHandle<ApplicationStartedEvent> public class UserService : IUserService, IHandle<ApplicationStartedEvent>
{ {
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 IUserRepository _repo;
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
public UserService(IUserRepository repo, public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider)
IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider,
IConfigFileProvider configFileProvider)
{ {
_repo = repo; _repo = repo;
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
@ -39,12 +42,15 @@ namespace NzbDrone.Core.Authentication
public User Add(string username, string password) public User Add(string username, string password)
{ {
return _repo.Insert(new User var user = new User
{ {
Identifier = Guid.NewGuid(), Identifier = Guid.NewGuid(),
Username = username.ToLowerInvariant(), Username = username.ToLowerInvariant()
Password = password.SHA256Hash() };
});
SetUserHashedPassword(user, password);
return _repo.Insert(user);
} }
public User Update(User user) public User Update(User user)
@ -63,7 +69,7 @@ namespace NzbDrone.Core.Authentication
if (user.Password != password) if (user.Password != password)
{ {
user.Password = password.SHA256Hash(); SetUserHashedPassword(user, password);
} }
user.Username = username.ToLowerInvariant(); user.Username = username.ToLowerInvariant();
@ -90,7 +96,20 @@ namespace NzbDrone.Core.Authentication
return null; 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; return user;
} }
@ -103,6 +122,43 @@ namespace NzbDrone.Core.Authentication
return _repo.FindUser(identifier); 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) public void Handle(ApplicationStartedEvent message)
{ {
if (_repo.All().Any()) if (_repo.All().Any())

@ -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();
}
}
}

@ -7,6 +7,7 @@
<PackageReference Include="System.Text.Json" Version="6.0.8" /> <PackageReference Include="System.Text.Json" Version="6.0.8" />
<PackageReference Include="System.Memory" Version="4.5.5" /> <PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" /> <PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />

Loading…
Cancel
Save