You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
121 lines
4.2 KiB
121 lines
4.2 KiB
using System; |
|
using System.Diagnostics; |
|
using System.Net; |
|
using System.Security.Cryptography; |
|
using System.Text; |
|
using Volo.Abp.DependencyInjection; |
|
|
|
namespace Sanhe.Abp.Identity.Security |
|
{ |
|
/// <summary> |
|
/// 微软的实现 |
|
/// See: Microsoft.AspNetCore.Identity.Rfc6238AuthenticationService |
|
/// </summary> |
|
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; |
|
} |
|
} |
|
}
|
|
|