using System; using System.Diagnostics; using System.Net; using System.Security.Cryptography; using System.Text; using Volo.Abp.DependencyInjection; namespace Sanhe.Abp.Identity.Security { /// /// 微软的实现 /// See: Microsoft.AspNetCore.Identity.Rfc6238AuthenticationService /// internal class DefaultTotpService : ITotpService, ISingletonDependency { private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3); private static readonly Encoding _encoding = new UTF8Encoding(false, true); #if NETSTANDARD2_0 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() { var bytes = new byte[20]; #if NETSTANDARD2_0 _rng.GetBytes(bytes); #else RandomNumberGenerator.Fill(bytes); #endif return bytes; } internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier) { // # of 0's = length of pin const int Mod = 1000000; // See https://tools.ietf.org/html/rfc4226 // We can add an optional modifier var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber)); var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier)); // 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(byte[] input, string modifier) { 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); 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 var delta = DateTime.UtcNow - _unixEpoch; #else var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch; #endif return (ulong)(delta.Ticks / _timestep.Ticks); } public int GenerateCode(byte[] securityToken, string modifier = null) { if (securityToken == null) { throw new ArgumentNullException(nameof(securityToken)); } // Allow a variance of no greater than 9 minutes in either direction var currentTimeStep = GetCurrentTimeStepNumber(); using (var hashAlgorithm = new HMACSHA1(securityToken)) { return ComputeTotp(hashAlgorithm, currentTimeStep, modifier); } } public bool ValidateCode(byte[] securityToken, int code, string modifier = null) { if (securityToken == null) { throw new ArgumentNullException(nameof(securityToken)); } // Allow a variance of no greater than 9 minutes in either direction var currentTimeStep = GetCurrentTimeStepNumber(); using (var hashAlgorithm = new HMACSHA1(securityToken)) { for (var i = -2; i <= 2; i++) { var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier); if (computedTotp == code) { return true; } } } // No match return false; } } }