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.
122 lines
4.2 KiB
122 lines
4.2 KiB
3 years ago
|
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;
|
||
|
}
|
||
|
}
|
||
|
}
|