OnlineSalesAutoCrop/Api/OnlineSalesAutoCrop.CoreAPI.Models/Objects/TOtpService.cs
2026-06-14 12:46:29 +06:00

276 lines
8.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace OnlineSalesAutoCrop.CoreAPI.Models.Objects
{
/// <summary>
///
/// </summary>
public static class Base32Encoding
{
/// <summary>
/// The different characters allowed in Base32 encoding.
/// </summary>
/// <remarks>
/// This is a 32-character subset of the twenty-six letters AZ and six digits 27.
/// </remarks>
private const string Base32AllowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
/// <summary>
/// Converts a byte array into a Base32 string.
/// </summary>
/// <param name="input">The string to convert to Base32.</param>
/// <param name="addPadding">Whether or not to add RFC3548 '='-padding to the string.</param>
/// <returns>A Base32 string.</returns>
/// <remarks>
/// https://tools.ietf.org/html/rfc3548#section-2.2 indicates padding MUST be added unless the reference to the RFC tells us otherwise.
/// https://github.com/google/google-authenticator/wiki/Key-Uri-Format indicates that padding SHOULD be omitted.
/// To meet both requirements, you can omit padding when required.
/// </remarks>
public static string ToBase32String(this byte[] input, bool addPadding)
{
if (input == null || input.Length == 0)
{
return string.Empty;
}
var bits = input.Select(b => Convert.ToString(b, 2).PadLeft(8, '0')).Aggregate((a, b) => a + b).PadRight((int)(Math.Ceiling((input.Length * 8) / 5d) * 5), '0');
var result = Enumerable.Range(0, bits.Length / 5).Select(i => Base32AllowedCharacters.Substring(Convert.ToInt32(bits.Substring(i * 5, 5), 2), 1)).Aggregate((a, b) => a + b);
if (addPadding)
{
result = result.PadRight((int)(Math.Ceiling(result.Length / 8d) * 8), '=');
}
return result;
}
/// <summary>
///
/// </summary>
/// <param name="input"></param>
/// <param name="addPadding"></param>
/// <returns></returns>
public static string EncodeAsBase32String(this string input, bool addPadding)
{
if (string.IsNullOrEmpty(input))
{
return string.Empty;
}
var bytes = Encoding.UTF8.GetBytes(input);
var result = bytes.ToBase32String(addPadding);
return result;
}
/// <summary>
///
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string DecodeFromBase32String(this string input)
{
if (string.IsNullOrEmpty(input))
{
return string.Empty;
}
var bytes = input.ToByteArray();
var result = Encoding.UTF8.GetString(bytes);
return result;
}
/// <summary>
/// Converts a Base32 string into the corresponding byte array, using 5 bits per character.
/// </summary>
/// <param name="input">The Base32 String</param>
/// <returns>A byte array containing the properly encoded bytes.</returns>
public static byte[] ToByteArray(this string input)
{
if (string.IsNullOrEmpty(input))
{
return [];
}
var bits = input.TrimEnd('=').ToUpper().ToCharArray().Select(c => Convert.ToString(Base32AllowedCharacters.IndexOf(c), 2).PadLeft(5, '0')).Aggregate((a, b) => a + b);
var result = Enumerable.Range(0, bits.Length / 8).Select(i => Convert.ToByte(bits.Substring(i * 8, 8), 2)).ToArray();
return result;
}
}
/// <summary>
///
/// </summary>
public class TOtpService
{
private TimeSpan DefaultClockDriftTolerance { get; set; }
private readonly static DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
///
/// </summary>
public TOtpService()
{
DefaultClockDriftTolerance = TimeSpan.FromMinutes(1);
}
private static string GeneratePINAtInterval(string secretKey, long counter, int digits)
{
return GenerateHashedCode(secretKey, counter, digits);
}
private static string GenerateHashedCode(string secretKey, long iterationNumber, int digits)
{
byte[] key = Base32Encoding.ToByteArray(secretKey.ToUpper());
return GenerateHashedCode(key, iterationNumber, digits);
}
private static string GenerateHashedCode(byte[] key, long iterationNumber, int digits)
{
byte[] counter = BitConverter.GetBytes(iterationNumber);
if (BitConverter.IsLittleEndian)
Array.Reverse(counter);
using HMACSHA1 hmac = new(key);
byte[] hash = hmac.ComputeHash(counter);
int offset = hash[^1] & 0xf;
//Convert the 4 bytes into an integer, ignoring the sign.
int binary = ((hash[offset] & 0x7f) << 24) | (hash[offset + 1] << 16) | (hash[offset + 2] << 8) | (hash[offset + 3]);
int password = binary % (int)Math.Pow(10, digits);
return password.ToString(new string('0', digits));
}
private static long GetCurrentCounter(DateTime now)
{
return GetCurrentCounter(now, _epoch, 30);
}
private static long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep)
{
return (long)(now - epoch).TotalSeconds / timeStep;
}
/// <summary>
///
/// </summary>
/// <param name="secretKey"></param>
/// <param name="otpCode"></param>
/// <returns></returns>
public bool ValidateTwoFactorPIN(string secretKey, string otpCode)
{
return ValidateTwoFactorPIN(secretKey, otpCode, DateTime.UtcNow, DefaultClockDriftTolerance);
}
/// <summary>
///
/// </summary>
/// <param name="secretKey"></param>
/// <param name="otpCode"></param>
/// <param name="now"></param>
/// <returns></returns>
public bool ValidateTwoFactorPIN(string secretKey, string otpCode, DateTime now)
{
return ValidateTwoFactorPIN(secretKey, otpCode, now, DefaultClockDriftTolerance);
}
/// <summary>
///
/// </summary>
/// <param name="secretKey"></param>
/// <param name="otpCode"></param>
/// <param name="timeTolerance"></param>
/// <returns></returns>
public static bool ValidateTwoFactorPIN(string secretKey, string otpCode, TimeSpan timeTolerance)
{
return ValidateTwoFactorPIN(secretKey, otpCode, DateTime.UtcNow, timeTolerance);
}
/// <summary>
///
/// </summary>
/// <param name="secretKey"></param>
/// <param name="otpCode"></param>
/// <param name="now"></param>
/// <param name="timeTolerance"></param>
/// <returns></returns>
public static bool ValidateTwoFactorPIN(string secretKey, string otpCode, DateTime now, TimeSpan timeTolerance)
{
var codes = GetCurrentPINs(secretKey, now, timeTolerance);
return codes.Any(c => c == otpCode);
}
/// <summary>
///
/// </summary>
/// <param name="secretKey"></param>
/// <returns></returns>
public static string GetCurrentPIN(string secretKey)
{
return GetCurrentPIN(secretKey, DateTime.UtcNow);
}
/// <summary>
///
/// </summary>
/// <param name="secretKey"></param>
/// <param name="now"></param>
/// <returns></returns>
public static string GetCurrentPIN(string secretKey, DateTime now)
{
return GeneratePINAtInterval(secretKey, GetCurrentCounter(now, _epoch, 30), 6);
}
/// <summary>
///
/// </summary>
/// <param name="secretKey"></param>
/// <returns></returns>
public string[] GetCurrentPINs(string secretKey)
{
return GetCurrentPINs(secretKey, DateTime.UtcNow, DefaultClockDriftTolerance);
}
/// <summary>
///
/// </summary>
/// <param name="secretKey"></param>
/// <param name="now"></param>
/// <returns></returns>
public string[] GetCurrentPINs(string secretKey, DateTime now)
{
return GetCurrentPINs(secretKey, now, DefaultClockDriftTolerance);
}
/// <summary>
///
/// </summary>
/// <param name="secretKey"></param>
/// <param name="now"></param>
/// <param name="timeTolerance"></param>
/// <returns></returns>
public static string[] GetCurrentPINs(string secretKey, DateTime now, TimeSpan timeTolerance)
{
int iterationOffset = 0;
List<string> codes = [];
long iterationCounter = GetCurrentCounter(now);
if (timeTolerance.TotalSeconds > 30)
{
iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / 30.00);
}
long iterationStart = iterationCounter - iterationOffset;
long iterationEnd = iterationCounter + iterationOffset;
for (long counter = iterationStart; counter <= iterationEnd; counter++)
{
codes.Add(GeneratePINAtInterval(secretKey, counter, 6));
}
return [.. codes];
}
}
}