diff options
author | Brennan <brecon@microsoft.com> | 2022-10-24 21:03:37 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-24 21:03:37 +0300 |
commit | c5ee07392f0b5bb2ccbfd2ee38d484724ad5bbf3 (patch) | |
tree | 6931f2c129a230e2ed917bf10432fdd28c2d62b3 /src | |
parent | 2f12b2a14ed5061a77daedc791ae921714759945 (diff) |
Remove temporary allocations from Identity (#44557)
Diffstat (limited to 'src')
12 files changed, 158 insertions, 65 deletions
diff --git a/src/Identity/Extensions.Core/src/AuthenticatorTokenProvider.cs b/src/Identity/Extensions.Core/src/AuthenticatorTokenProvider.cs index 659fea8634..2d62fac472 100644 --- a/src/Identity/Extensions.Core/src/AuthenticatorTokenProvider.cs +++ b/src/Identity/Extensions.Core/src/AuthenticatorTokenProvider.cs @@ -68,9 +68,9 @@ public class AuthenticatorTokenProvider<TUser> : IUserTwoFactorTokenProvider<TUs for (int i = -2; i <= 2; i++) { #if NET6_0_OR_GREATER - var expectedCode = Rfc6238AuthenticationService.ComputeTotp(keyBytes, (ulong)(timestep + i), modifier: null); + var expectedCode = Rfc6238AuthenticationService.ComputeTotp(keyBytes, (ulong)(timestep + i), modifierBytes: null); #else - var expectedCode = Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)(timestep + i), modifier: null); + var expectedCode = Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)(timestep + i), modifierBytes: null); #endif if (expectedCode == code) { diff --git a/src/Identity/Extensions.Core/src/Base32.cs b/src/Identity/Extensions.Core/src/Base32.cs index 6a30a647fd..13effd61a0 100644 --- a/src/Identity/Extensions.Core/src/Base32.cs +++ b/src/Identity/Extensions.Core/src/Base32.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Security.Cryptography; using System.Text; namespace Microsoft.AspNetCore.Identity; @@ -11,6 +12,41 @@ internal static class Base32 { private const string _base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +#if NET6_0_OR_GREATER + public static string GenerateBase32() + { + const int length = 20; + // base32 takes 5 bytes and converts them into 8 characters, which would be (byte length / 5) * 8 + // except that it also pads ('=') for the last processed chunk if it's less than 5 bytes. + // So in order to handle the padding we add 1 less than the chunk size to our byte length + // which will either be removed due to integer division truncation if the length was already a multiple of 5 + // or it will increase the divided length by 1 meaning that a 1-4 byte length chunk will be 1 instead of 0 + // so the padding is now included in our string length calculation + return string.Create(((length + 4) / 5) * 8, 0, static (buffer, _) => + { + Span<byte> bytes = stackalloc byte[length]; + RandomNumberGenerator.Fill(bytes); + + var index = 0; + for (int offset = 0; offset < bytes.Length;) + { + byte a, b, c, d, e, f, g, h; + int numCharsToOutput = GetNextGroup(bytes, ref offset, out a, out b, out c, out d, out e, out f, out g, out h); + + buffer[index + 7] = ((numCharsToOutput >= 8) ? _base32Chars[h] : '='); + buffer[index + 6] = ((numCharsToOutput >= 7) ? _base32Chars[g] : '='); + buffer[index + 5] = ((numCharsToOutput >= 6) ? _base32Chars[f] : '='); + buffer[index + 4] = ((numCharsToOutput >= 5) ? _base32Chars[e] : '='); + buffer[index + 3] = ((numCharsToOutput >= 4) ? _base32Chars[d] : '='); + buffer[index + 2] = (numCharsToOutput >= 3) ? _base32Chars[c] : '='; + buffer[index + 1] = (numCharsToOutput >= 2) ? _base32Chars[b] : '='; + buffer[index] = (numCharsToOutput >= 1) ? _base32Chars[a] : '='; + index += 8; + } + }); + } +#endif + public static string ToBase32(byte[] input) { if (input == null) @@ -84,7 +120,7 @@ internal static class Base32 } // returns the number of bytes that were output - private static int GetNextGroup(byte[] input, ref int offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h) + private static int GetNextGroup(Span<byte> input, ref int offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h) { uint b1, b2, b3, b4, b5; diff --git a/src/Identity/Extensions.Core/src/IdentityResult.cs b/src/Identity/Extensions.Core/src/IdentityResult.cs index 82010539d2..3ae3368372 100644 --- a/src/Identity/Extensions.Core/src/IdentityResult.cs +++ b/src/Identity/Extensions.Core/src/IdentityResult.cs @@ -49,6 +49,16 @@ public class IdentityResult return result; } + internal static IdentityResult Failed(List<IdentityError>? errors) + { + var result = new IdentityResult { Succeeded = false }; + if (errors != null) + { + result._errors.AddRange(errors); + } + return result; + } + /// <summary> /// Converts the value of the current <see cref="IdentityResult"/> object to its equivalent string representation. /// </summary> diff --git a/src/Identity/Extensions.Core/src/PasswordHasher.cs b/src/Identity/Extensions.Core/src/PasswordHasher.cs index 4ac4eae30d..9164e1ef70 100644 --- a/src/Identity/Extensions.Core/src/PasswordHasher.cs +++ b/src/Identity/Extensions.Core/src/PasswordHasher.cs @@ -35,13 +35,15 @@ public class PasswordHasher<TUser> : IPasswordHasher<TUser> where TUser : class private readonly int _iterCount; private readonly RandomNumberGenerator _rng; + private static readonly PasswordHasherOptions DefaultOptions = new PasswordHasherOptions(); + /// <summary> /// Creates a new instance of <see cref="PasswordHasher{TUser}"/>. /// </summary> /// <param name="optionsAccessor">The options for this instance.</param> public PasswordHasher(IOptions<PasswordHasherOptions>? optionsAccessor = null) { - var options = optionsAccessor?.Value ?? new PasswordHasherOptions(); + var options = optionsAccessor?.Value ?? DefaultOptions; _compatibilityMode = options.CompatibilityMode; switch (_compatibilityMode) diff --git a/src/Identity/Extensions.Core/src/PasswordValidator.cs b/src/Identity/Extensions.Core/src/PasswordValidator.cs index 4180eda13d..3675bebb66 100644 --- a/src/Identity/Extensions.Core/src/PasswordValidator.cs +++ b/src/Identity/Extensions.Core/src/PasswordValidator.cs @@ -46,36 +46,42 @@ public class PasswordValidator<TUser> : IPasswordValidator<TUser> where TUser : { throw new ArgumentNullException(nameof(manager)); } - var errors = new List<IdentityError>(); + List<IdentityError>? errors = null; var options = manager.Options.Password; if (string.IsNullOrWhiteSpace(password) || password.Length < options.RequiredLength) { + errors ??= new List<IdentityError>(); errors.Add(Describer.PasswordTooShort(options.RequiredLength)); } if (options.RequireNonAlphanumeric && password.All(IsLetterOrDigit)) { + errors ??= new List<IdentityError>(); errors.Add(Describer.PasswordRequiresNonAlphanumeric()); } if (options.RequireDigit && !password.Any(IsDigit)) { + errors ??= new List<IdentityError>(); errors.Add(Describer.PasswordRequiresDigit()); } if (options.RequireLowercase && !password.Any(IsLower)) { + errors ??= new List<IdentityError>(); errors.Add(Describer.PasswordRequiresLower()); } if (options.RequireUppercase && !password.Any(IsUpper)) { + errors ??= new List<IdentityError>(); errors.Add(Describer.PasswordRequiresUpper()); } if (options.RequiredUniqueChars >= 1 && password.Distinct().Count() < options.RequiredUniqueChars) { + errors ??= new List<IdentityError>(); errors.Add(Describer.PasswordRequiresUniqueChars(options.RequiredUniqueChars)); } return - Task.FromResult(errors.Count == 0 - ? IdentityResult.Success - : IdentityResult.Failed(errors.ToArray())); + Task.FromResult(errors?.Count > 0 + ? IdentityResult.Failed(errors) + : IdentityResult.Success); } /// <summary> diff --git a/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs b/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs index 5f13a9be6d..aabd8b2d1e 100644 --- a/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs +++ b/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs @@ -17,21 +17,8 @@ internal static class Rfc6238AuthenticationService private static readonly Encoding _encoding = new UTF8Encoding(false, true); #if NETSTANDARD2_0 || NETFRAMEWORK private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); #endif - // Generates a new 80-bit security token - public static byte[] GenerateRandomKey() - { - byte[] bytes = new byte[20]; -#if NETSTANDARD2_0 || NETFRAMEWORK - _rng.GetBytes(bytes); -#else - RandomNumberGenerator.Fill(bytes); -#endif - return bytes; - } - internal static int ComputeTotp( #if NET6_0_OR_GREATER byte[] key, @@ -39,19 +26,33 @@ internal static class Rfc6238AuthenticationService HashAlgorithm hashAlgorithm, #endif ulong timestepNumber, - string? modifier) + byte[]? modifierBytes) { // # of 0's = length of pin const int Mod = 1000000; // See https://tools.ietf.org/html/rfc4226 // We can add an optional modifier +#if NET6_0_OR_GREATER + Span<byte> timestepAsBytes = stackalloc byte[sizeof(long)]; + var res = BitConverter.TryWriteBytes(timestepAsBytes, IPAddress.HostToNetworkOrder((long)timestepNumber)); + Debug.Assert(res); +#else var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber)); +#endif #if NET6_0_OR_GREATER - var hash = HMACSHA1.HashData(key, ApplyModifier(timestepAsBytes, modifier)); + Span<byte> modifierCombinedBytes = timestepAsBytes; + if (modifierBytes is not null) + { + modifierCombinedBytes = ApplyModifier(timestepAsBytes, modifierBytes); + } + Span<byte> hash = stackalloc byte[HMACSHA1.HashSizeInBytes]; + res = HMACSHA1.TryHashData(key, modifierCombinedBytes, hash, out var written); + Debug.Assert(res); + Debug.Assert(written == hash.Length); #else - var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier)); + var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifierBytes)); #endif // Generate DT string @@ -65,16 +66,10 @@ internal static class Rfc6238AuthenticationService return binaryCode % Mod; } - private static byte[] ApplyModifier(byte[] input, string? modifier) + private static byte[] ApplyModifier(Span<byte> input, byte[] modifierBytes) { - if (string.IsNullOrEmpty(modifier)) - { - return input; - } - - var modifierBytes = _encoding.GetBytes(modifier); var combined = new byte[checked(input.Length + modifierBytes.Length)]; - Buffer.BlockCopy(input, 0, combined, 0, input.Length); + input.CopyTo(combined); Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); return combined; } @@ -100,12 +95,13 @@ internal static class Rfc6238AuthenticationService // Allow a variance of no greater than 9 minutes in either direction var currentTimeStep = GetCurrentTimeStepNumber(); + var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; #if NET6_0_OR_GREATER - return ComputeTotp(securityToken, currentTimeStep, modifier); + return ComputeTotp(securityToken, currentTimeStep, modifierBytes); #else using (var hashAlgorithm = new HMACSHA1(securityToken)) { - return ComputeTotp(hashAlgorithm, currentTimeStep, modifier); + return ComputeTotp(hashAlgorithm, currentTimeStep, modifierBytes); } #endif } @@ -124,12 +120,13 @@ internal static class Rfc6238AuthenticationService using (var hashAlgorithm = new HMACSHA1(securityToken)) #endif { + var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; for (var i = -2; i <= 2; i++) { #if NET6_0_OR_GREATER - var computedTotp = ComputeTotp(securityToken, (ulong)((long)currentTimeStep + i), modifier); + var computedTotp = ComputeTotp(securityToken, (ulong)((long)currentTimeStep + i), modifierBytes); #else - var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier); + var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifierBytes); #endif if (computedTotp == code) { diff --git a/src/Identity/Extensions.Core/src/RoleManager.cs b/src/Identity/Extensions.Core/src/RoleManager.cs index 93f0014629..82dc0010ba 100644 --- a/src/Identity/Extensions.Core/src/RoleManager.cs +++ b/src/Identity/Extensions.Core/src/RoleManager.cs @@ -425,19 +425,23 @@ public class RoleManager<TRole> : IDisposable where TRole : class /// <returns>A <see cref="IdentityResult"/> representing whether validation was successful.</returns> protected virtual async Task<IdentityResult> ValidateRoleAsync(TRole role) { - var errors = new List<IdentityError>(); + List<IdentityError>? errors = null; foreach (var v in RoleValidators) { var result = await v.ValidateAsync(this, role).ConfigureAwait(false); if (!result.Succeeded) { + errors ??= new List<IdentityError>(); errors.AddRange(result.Errors); } } - if (errors.Count > 0) + if (errors?.Count > 0) { - Logger.LogWarning(LoggerEventIds.RoleValidationFailed, "Role {roleId} validation failed: {errors}.", await GetRoleIdAsync(role).ConfigureAwait(false), string.Join(";", errors.Select(e => e.Code))); - return IdentityResult.Failed(errors.ToArray()); + if (Logger.IsEnabled(LogLevel.Warning)) + { + Logger.LogWarning(LoggerEventIds.RoleValidationFailed, "Role {roleId} validation failed: {errors}.", await GetRoleIdAsync(role).ConfigureAwait(false), string.Join(";", errors.Select(e => e.Code))); + } + return IdentityResult.Failed(errors); } return IdentityResult.Success; } diff --git a/src/Identity/Extensions.Core/src/RoleValidator.cs b/src/Identity/Extensions.Core/src/RoleValidator.cs index 3385018e34..26e44558b8 100644 --- a/src/Identity/Extensions.Core/src/RoleValidator.cs +++ b/src/Identity/Extensions.Core/src/RoleValidator.cs @@ -40,21 +40,21 @@ public class RoleValidator<TRole> : IRoleValidator<TRole> where TRole : class { throw new ArgumentNullException(nameof(role)); } - var errors = new List<IdentityError>(); - await ValidateRoleName(manager, role, errors).ConfigureAwait(false); - if (errors.Count > 0) + var errors = await ValidateRoleName(manager, role).ConfigureAwait(false); + if (errors?.Count > 0) { - return IdentityResult.Failed(errors.ToArray()); + return IdentityResult.Failed(errors); } return IdentityResult.Success; } - private async Task ValidateRoleName(RoleManager<TRole> manager, TRole role, - ICollection<IdentityError> errors) + private async Task<List<IdentityError>?> ValidateRoleName(RoleManager<TRole> manager, TRole role) { + List<IdentityError>? errors = null; var roleName = await manager.GetRoleNameAsync(role).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(roleName)) { + errors ??= new List<IdentityError>(); errors.Add(Describer.InvalidRoleName(roleName)); } else @@ -63,8 +63,11 @@ public class RoleValidator<TRole> : IRoleValidator<TRole> where TRole : class if (owner != null && !string.Equals(await manager.GetRoleIdAsync(owner).ConfigureAwait(false), await manager.GetRoleIdAsync(role).ConfigureAwait(false))) { + errors ??= new List<IdentityError>(); errors.Add(Describer.DuplicateRoleName(roleName)); } } + + return errors; } } diff --git a/src/Identity/Extensions.Core/src/UserManager.cs b/src/Identity/Extensions.Core/src/UserManager.cs index 4b1708e71e..668dc024ac 100644 --- a/src/Identity/Extensions.Core/src/UserManager.cs +++ b/src/Identity/Extensions.Core/src/UserManager.cs @@ -2278,6 +2278,22 @@ public class UserManager<TUser> : IDisposable where TUser : class /// <returns></returns> protected virtual string CreateTwoFactorRecoveryCode() { +#if NET6_0_OR_GREATER + return string.Create(11, 0, static (buffer, _) => + { + buffer[10] = GetRandomRecoveryCodeChar(); + buffer[9] = GetRandomRecoveryCodeChar(); + buffer[8] = GetRandomRecoveryCodeChar(); + buffer[7] = GetRandomRecoveryCodeChar(); + buffer[6] = GetRandomRecoveryCodeChar(); + buffer[5] = '-'; + buffer[4] = GetRandomRecoveryCodeChar(); + buffer[3] = GetRandomRecoveryCodeChar(); + buffer[2] = GetRandomRecoveryCodeChar(); + buffer[1] = GetRandomRecoveryCodeChar(); + buffer[0] = GetRandomRecoveryCodeChar(); + }); +#else var recoveryCode = new StringBuilder(11); recoveryCode.Append(GetRandomRecoveryCodeChar()); recoveryCode.Append(GetRandomRecoveryCodeChar()); @@ -2291,6 +2307,7 @@ public class UserManager<TUser> : IDisposable where TUser : class recoveryCode.Append(GetRandomRecoveryCodeChar()); recoveryCode.Append(GetRandomRecoveryCodeChar()); return recoveryCode.ToString(); +#endif } // We don't want to use any confusing characters like 0/O 1/I/L/l @@ -2487,13 +2504,13 @@ public class UserManager<TUser> : IDisposable where TUser : class private static string NewSecurityStamp() { - byte[] bytes = new byte[20]; #if NETSTANDARD2_0 || NETFRAMEWORK + byte[] bytes = new byte[20]; _rng.GetBytes(bytes); + return Base32.ToBase32(bytes); #else - RandomNumberGenerator.Fill(bytes); + return Base32.GenerateBase32(); #endif - return Base32.ToBase32(bytes); } // IUserLoginStore methods @@ -2550,19 +2567,20 @@ public class UserManager<TUser> : IDisposable where TUser : class throw new InvalidOperationException(Resources.NullSecurityStamp); } } - var errors = new List<IdentityError>(); + List<IdentityError>? errors = null; foreach (var v in UserValidators) { var result = await v.ValidateAsync(this, user).ConfigureAwait(false); if (!result.Succeeded) { + errors ??= new List<IdentityError>(); errors.AddRange(result.Errors); } } - if (errors.Count > 0) + if (errors?.Count > 0) { Logger.LogDebug(LoggerEventIds.UserValidationFailed, "User validation failed: {errors}.", string.Join(";", errors.Select(e => e.Code))); - return IdentityResult.Failed(errors.ToArray()); + return IdentityResult.Failed(errors); } return IdentityResult.Success; } @@ -2576,7 +2594,7 @@ public class UserManager<TUser> : IDisposable where TUser : class /// <returns>A <see cref="IdentityResult"/> representing whether validation was successful.</returns> protected async Task<IdentityResult> ValidatePasswordAsync(TUser user, string? password) { - var errors = new List<IdentityError>(); + List<IdentityError>? errors = null; var isValid = true; foreach (var v in PasswordValidators) { @@ -2585,6 +2603,7 @@ public class UserManager<TUser> : IDisposable where TUser : class { if (result.Errors.Any()) { + errors ??= new List<IdentityError>(); errors.AddRange(result.Errors); } @@ -2593,8 +2612,8 @@ public class UserManager<TUser> : IDisposable where TUser : class } if (!isValid) { - Logger.LogDebug(LoggerEventIds.PasswordValidationFailed, "User password validation failed: {errors}.", string.Join(";", errors.Select(e => e.Code))); - return IdentityResult.Failed(errors.ToArray()); + Logger.LogDebug(LoggerEventIds.PasswordValidationFailed, "User password validation failed: {errors}.", string.Join(";", errors?.Select(e => e.Code) ?? Array.Empty<string>())); + return IdentityResult.Failed(errors); } return IdentityResult.Success; } diff --git a/src/Identity/Extensions.Core/src/UserValidator.cs b/src/Identity/Extensions.Core/src/UserValidator.cs index 81820d0142..1d67942d3c 100644 --- a/src/Identity/Extensions.Core/src/UserValidator.cs +++ b/src/Identity/Extensions.Core/src/UserValidator.cs @@ -46,25 +46,27 @@ public class UserValidator<TUser> : IUserValidator<TUser> where TUser : class { throw new ArgumentNullException(nameof(user)); } - var errors = new List<IdentityError>(); - await ValidateUserName(manager, user, errors).ConfigureAwait(false); + var errors = await ValidateUserName(manager, user).ConfigureAwait(false); if (manager.Options.User.RequireUniqueEmail) { - await ValidateEmail(manager, user, errors).ConfigureAwait(false); + errors = await ValidateEmail(manager, user, errors).ConfigureAwait(false); } - return errors.Count > 0 ? IdentityResult.Failed(errors.ToArray()) : IdentityResult.Success; + return errors?.Count > 0 ? IdentityResult.Failed(errors) : IdentityResult.Success; } - private async Task ValidateUserName(UserManager<TUser> manager, TUser user, ICollection<IdentityError> errors) + private async Task<List<IdentityError>?> ValidateUserName(UserManager<TUser> manager, TUser user) { + List<IdentityError>? errors = null; var userName = await manager.GetUserNameAsync(user).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(userName)) { + errors ??= new List<IdentityError>(); errors.Add(Describer.InvalidUserName(userName)); } else if (!string.IsNullOrEmpty(manager.Options.User.AllowedUserNameCharacters) && userName.Any(c => !manager.Options.User.AllowedUserNameCharacters.Contains(c))) { + errors ??= new List<IdentityError>(); errors.Add(Describer.InvalidUserName(userName)); } else @@ -73,30 +75,37 @@ public class UserValidator<TUser> : IUserValidator<TUser> where TUser : class if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner).ConfigureAwait(false), await manager.GetUserIdAsync(user).ConfigureAwait(false))) { + errors ??= new List<IdentityError>(); errors.Add(Describer.DuplicateUserName(userName)); } } + + return errors; } // make sure email is not empty, valid, and unique - private async Task ValidateEmail(UserManager<TUser> manager, TUser user, List<IdentityError> errors) + private async Task<List<IdentityError>?> ValidateEmail(UserManager<TUser> manager, TUser user, List<IdentityError>? errors) { var email = await manager.GetEmailAsync(user).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(email)) { + errors ??= new List<IdentityError>(); errors.Add(Describer.InvalidEmail(email)); - return; + return errors; } if (!new EmailAddressAttribute().IsValid(email)) { + errors ??= new List<IdentityError>(); errors.Add(Describer.InvalidEmail(email)); - return; + return errors; } var owner = await manager.FindByEmailAsync(email).ConfigureAwait(false); if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner).ConfigureAwait(false), await manager.GetUserIdAsync(user).ConfigureAwait(false))) { + errors ??= new List<IdentityError>(); errors.Add(Describer.DuplicateEmail(email)); } + return errors; } } diff --git a/src/Identity/test/Identity.FunctionalTests/Pages/Account/Manage/EnableAuthenticator.cs b/src/Identity/test/Identity.FunctionalTests/Pages/Account/Manage/EnableAuthenticator.cs index 1e3a5ad435..e888c22d04 100644 --- a/src/Identity/test/Identity.FunctionalTests/Pages/Account/Manage/EnableAuthenticator.cs +++ b/src/Identity/test/Identity.FunctionalTests/Pages/Account/Manage/EnableAuthenticator.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; @@ -48,7 +48,7 @@ internal class EnableAuthenticator : DefaultUIPage var keyBytes = Base32.FromBase32(key); var unixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var timestep = Convert.ToInt64(unixTimestamp / 30); - var topt = Rfc6238AuthenticationService.ComputeTotp(keyBytes, (ulong)timestep, modifier: null); + var topt = Rfc6238AuthenticationService.ComputeTotp(keyBytes, (ulong)timestep, modifierBytes: null); return topt.ToString("D6", CultureInfo.InvariantCulture); } } diff --git a/src/Identity/test/Identity.Test/Base32Test.cs b/src/Identity/test/Identity.Test/Base32Test.cs index 8686193c2b..f188c23219 100644 --- a/src/Identity/test/Identity.Test/Base32Test.cs +++ b/src/Identity/test/Identity.Test/Base32Test.cs @@ -26,6 +26,13 @@ public class Base32Test Assert.Equal<byte>(data, Base32.FromBase32(Base32.ToBase32(data))); } + [Fact] + public void GenerateBase32IsValid() + { + var output = Base32.FromBase32(Base32.GenerateBase32()); + Assert.Equal(20, output.Length); + } + private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); private static byte[] GetRandomByteArray(int length) |