| File: Rfc6238AuthenticationService.cs | Web Access |
| Project: src\aspnetcore\src\Identity\Extensions.Core\src\Microsoft.Extensions.Identity.Core.csproj (Microsoft.Extensions.Identity.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable enable using System; using System.Diagnostics; using System.Net; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Shared; namespace Microsoft.AspNetCore.Identity; internal static class Rfc6238AuthenticationService { private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3); 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); #endif internal static int ComputeTotp( #if NET6_0_OR_GREATER byte[] key, #else HashAlgorithm hashAlgorithm, #endif ulong timestepNumber, 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 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, modifierBytes)); #endif // Generate DT string var offset = hash[hash.Length - 1] & 0xf; Debug.Assert(offset + 4 < hash.Length); var binaryCode = (hash[offset] & 0x7f) << 24 | (hash[offset + 1] & 0xff) << 16 | (hash[offset + 2] & 0xff) << 8 | (hash[offset + 3] & 0xff); return binaryCode % Mod; } private static byte[] ApplyModifier(Span<byte> input, byte[] modifierBytes) { var combined = new byte[checked(input.Length + modifierBytes.Length)]; input.CopyTo(combined); Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); return combined; } // More info: https://tools.ietf.org/html/rfc6238#section-4 private static ulong GetCurrentTimeStepNumber() { #if NETSTANDARD2_0 || NETFRAMEWORK var delta = DateTime.UtcNow - _unixEpoch; #else var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch; #endif return (ulong)(delta.Ticks / _timestep.Ticks); } public static int GenerateCode(byte[] securityToken, string? modifier = null) { ArgumentNullThrowHelper.ThrowIfNull(securityToken); // 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, modifierBytes); #else using (var hashAlgorithm = new HMACSHA1(securityToken)) { return ComputeTotp(hashAlgorithm, currentTimeStep, modifierBytes); } #endif } public static bool ValidateCode(byte[] securityToken, int code, string? modifier = null) { ArgumentNullThrowHelper.ThrowIfNull(securityToken); // Allow a variance of no greater than 9 minutes in either direction var currentTimeStep = GetCurrentTimeStepNumber(); #if !NET6_0_OR_GREATER 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), modifierBytes); #else var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifierBytes); #endif if (computedTotp == code) { return true; } } } // No match return false; } }