namespace MyOffice.Web.Identity.Repositories; using System.Security.Cryptography; using Domain; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; /// /// Identity V3 hasher for new passwords; still verifies the legacy /// (16-byte salt + 20-byte PBKDF2-SHA1 @ 100k) format and signals rehash. /// public class PasswordHasher : IPasswordHasher> { 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> _identityHasher = new(); public string HashPassword(ApplicationUser user, string password) => _identityHasher.HashPassword(user, password); public PasswordVerificationResult VerifyHashedPassword( ApplicationUser 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> { public AppUserManager( IUserStore> store, IOptions optionsAccessor, IPasswordHasher> passwordHasher, IEnumerable>> userValidators, IEnumerable>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger>> logger) : base( store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger ) { } }