Files

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
)
{
}
}