134 lines
3.4 KiB
C#
134 lines
3.4 KiB
C#
namespace MyOffice.Web.Identity.Repositories;
|
|
|
|
using System.Security.Cryptography;
|
|
using Domain;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
/// <summary>
|
|
/// Identity V3 hasher for new passwords; still verifies the legacy
|
|
/// (16-byte salt + 20-byte PBKDF2-SHA1 @ 100k) format and signals rehash.
|
|
/// </summary>
|
|
public class PasswordHasher : IPasswordHasher<ApplicationUser<Guid>>
|
|
{
|
|
private const int LegacySaltSize = 16;
|
|
private const int LegacyHashSize = 20;
|
|
private const int LegacyIterations = 100_000;
|
|
private const int LegacyPayloadSize = LegacySaltSize + LegacyHashSize;
|
|
|
|
private readonly PasswordHasher<ApplicationUser<Guid>> _identityHasher = new();
|
|
|
|
public string HashPassword(ApplicationUser<Guid> user, string password) =>
|
|
_identityHasher.HashPassword(user, password);
|
|
|
|
public PasswordVerificationResult VerifyHashedPassword(
|
|
ApplicationUser<Guid> user,
|
|
string hashedPassword,
|
|
string providedPassword
|
|
)
|
|
{
|
|
if (string.IsNullOrEmpty(hashedPassword) || providedPassword == null)
|
|
{
|
|
return PasswordVerificationResult.Failed;
|
|
}
|
|
|
|
// Prefer modern Identity format when payload is not the legacy 36-byte blob.
|
|
if (!IsLegacyPayload(hashedPassword))
|
|
{
|
|
return _identityHasher.VerifyHashedPassword(user, hashedPassword, providedPassword);
|
|
}
|
|
|
|
if (VerifyLegacyHash(providedPassword, hashedPassword))
|
|
{
|
|
return PasswordVerificationResult.SuccessRehashNeeded;
|
|
}
|
|
|
|
return PasswordVerificationResult.Failed;
|
|
}
|
|
|
|
private static bool IsLegacyPayload(string hashedPassword)
|
|
{
|
|
try
|
|
{
|
|
var bytes = Convert.FromBase64String(hashedPassword);
|
|
return bytes.Length == LegacyPayloadSize;
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal static string HashLegacyForTests(string password)
|
|
{
|
|
var salt = RandomNumberGenerator.GetBytes(LegacySaltSize);
|
|
var hash = Rfc2898DeriveBytes.Pbkdf2(
|
|
password,
|
|
salt,
|
|
LegacyIterations,
|
|
HashAlgorithmName.SHA1,
|
|
LegacyHashSize);
|
|
|
|
var payload = new byte[LegacyPayloadSize];
|
|
Buffer.BlockCopy(salt, 0, payload, 0, LegacySaltSize);
|
|
Buffer.BlockCopy(hash, 0, payload, LegacySaltSize, LegacyHashSize);
|
|
return Convert.ToBase64String(payload);
|
|
}
|
|
|
|
private static bool VerifyLegacyHash(string password, string passwordHash)
|
|
{
|
|
byte[] hashBytes;
|
|
try
|
|
{
|
|
hashBytes = Convert.FromBase64String(passwordHash);
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (hashBytes.Length != LegacyPayloadSize)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var salt = hashBytes.AsSpan(0, LegacySaltSize);
|
|
var expected = hashBytes.AsSpan(LegacySaltSize, LegacyHashSize);
|
|
var actual = Rfc2898DeriveBytes.Pbkdf2(
|
|
password,
|
|
salt,
|
|
LegacyIterations,
|
|
HashAlgorithmName.SHA1,
|
|
LegacyHashSize);
|
|
|
|
return CryptographicOperations.FixedTimeEquals(expected, actual);
|
|
}
|
|
}
|
|
|
|
public class AppUserManager : UserManager<ApplicationUser<Guid>>
|
|
{
|
|
public AppUserManager(
|
|
IUserStore<ApplicationUser<Guid>> store,
|
|
IOptions<IdentityOptions> optionsAccessor,
|
|
IPasswordHasher<ApplicationUser<Guid>> passwordHasher,
|
|
IEnumerable<IUserValidator<ApplicationUser<Guid>>> userValidators,
|
|
IEnumerable<IPasswordValidator<ApplicationUser<Guid>>> passwordValidators,
|
|
ILookupNormalizer keyNormalizer,
|
|
IdentityErrorDescriber errors,
|
|
IServiceProvider services,
|
|
ILogger<UserManager<ApplicationUser<Guid>>> logger) :
|
|
base(
|
|
store,
|
|
optionsAccessor,
|
|
passwordHasher,
|
|
userValidators,
|
|
passwordValidators,
|
|
keyNormalizer,
|
|
errors,
|
|
services,
|
|
logger
|
|
)
|
|
{
|
|
}
|
|
}
|