A nice easy-to-use RFC-6238 based algorithm, that provides a HMAC-based One-Time Password authentication. The algorithm is based on one outlined in this article here.
The interval is in seconds. It dictates how long before a TOTP expires. For example, if a TOTP is generated at 14:20 and the interval is 120 seconds. The TOTP will expire at 14:22.
The process in which this is implemented is quite simple:
- A Shared Secret Key is generated by the host (line 7).
- The Shared Secret Key, along with a predetermined interval is shared with the client (line 8).
- The client then uses the Shared Secret Key to generate a TOTP each time they need to authenticate (line 13). This will be passed to the host system that requires authentication.
- On the request the host will need to generate a TOTP using the same Shared Secret Key. This is then compared to the one provided by the client (line 17).
- If they match, the authentication was successful (line 18).
The code below can be copied and pasted into LinqPad as a C# program.
void Main()
{
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// This information only needs to be generated and distributed once
const int interval = 240;
string sharedSecretKey = TotpHelper.GenerateRandomSharedSecret();
Console.WriteLine("The Shared Secret Key is: {0}", sharedSecretKey);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Client - Generates a TOTP
string totpClient = TotpHelper.GenerateTotp(interval, sharedSecretKey);
Console.WriteLine("The TOTP is: {0}", totpClient);
// Host - Generates and checks TOTP
string totpHost = TotpHelper.GenerateTotp(interval, sharedSecretKey);
Console.WriteLine("The client TOTP matches the Host TOTP: {0}", totpClient == totpHost);
}
// using System.Security.Cryptography
// using System.Globalization
public static class TotpHelper
{
/// <summary>
/// Generates a Shared Secret Key that can be used to generate a TOTP.
/// This is normally used ONCE by the host to generate a key for the client that can be used over and over again.
/// </summary>
/// <returns>Returns an 32-digit long hexadecimal number</returns>
public static string GenerateRandomSharedSecret()
{
var rng = new RNGCryptoServiceProvider(); // Less pseudo-random than Random()!
string hexKey = string.Join(string.Empty, Enumerable.Range(0, 16)
.Select(i =>
{
var data = new byte[4];
rng.GetBytes(data);
int generatedValue = Math.Abs(BitConverter.ToInt32(data, startIndex: 0));
int rndNum = (generatedValue % 127);
return PrependZeros(rndNum.ToString("X"), 2);
})
.ToArray());
return hexKey;
}
/// <summary>
/// Generates the Time-Based One-Time Password. This is based on the RFC-6238 algorithm.
/// </summary>
/// <param name="intervalSeconds">The length of time in seconds the password will be valid</param>
/// <param name="sharedSecretKey">The key shared between parties using the algorithm. It is a 32 char long hex string.</param>
/// <returns>Returns an 8-digit long number string. e.g. 12345678</returns>
public static string GenerateTotp(int intervalSeconds, string sharedSecretKey)
{
if (IsValidSharedKey(sharedSecretKey) == false)
{
throw new ArgumentException("Invalid Shared Secret Key");
}
double epochSeconds = GetEpochSeconds();
double stepTime = Math.Truncate(epochSeconds / intervalSeconds);
string stepTimeHex = DoubleToHexString(stepTime, 16);
stepTimeHex = PrependZeros(stepTimeHex, 16);
byte[] stepB = ConvertHexStringToByteArray(stepTimeHex);
byte[] keyB = ConvertHexStringToByteArray(sharedSecretKey);
HMACSHA1 shaHasher = new HMACSHA1(keyB);
byte[] hashKeyB = shaHasher.ComputeHash(stepB);
int offset = hashKeyB[hashKeyB.Length - 1] & 15;
int binary =
((hashKeyB[offset] & 127) << 24) |
((hashKeyB[offset + 1] & 255) << 16) |
((hashKeyB[offset + 2] & 255) << 8) |
(hashKeyB[offset + 3] & 255);
int otp = binary % 100000000;
string result = PrependZeros(Convert.ToString(otp), 8);
return result;
}
/// <summary>
/// Checks whether the Shared Secret Key is valid.
/// </summary>
/// <param name="value">The key must be a 32 character long hexadecimal string</param>
/// <returns>IsValid = true / IsNotValid = false</returns>
public static bool IsValidSharedKey(this string value)
{
return !String.IsNullOrWhiteSpace(value)
&& Regex.IsMatch(value, @"[0-9a-fA-F]{32}");
}
private static double GetEpochSeconds()
{
DateTime startDate = new DateTime(1970, 1, 1, 0, 0, 0);
TimeSpan span = DateTime.UtcNow - startDate;
return span.TotalSeconds;
}
private static byte[] ConvertHexStringToByteArray(string hexString)
{
return Enumerable
.Range(0, hexString.Length / 2)
.Select(i => Convert.ToByte(hexString.Substring(i * 2, 2), 16))
.ToArray();
}
private static string PrependZeros(string value, int reqLen)
{
string zeros = String.Join(String.Empty
, Enumerable.Range(0, Math.Abs(reqLen - value.Length)).Select(i => "0"));
return zeros + value;
}
private static string DoubleToHexString(double value, int maxDecimals)
{
if (value == 0 || double.IsInfinity(value) || double.IsNaN(value))
return value.ToString(CultureInfo.InvariantCulture);
var hex = new StringBuilder();
long bytes = BitConverter.ToInt64(BitConverter.GetBytes(value), 0);
bool negative = bytes < 0;
bytes &= long.MaxValue;
int exp = ((int)(bytes >> 52) & 2047) - 1023;
bytes = (bytes & 0xFFFFFFFFFFFFF) | 0x10000000000000;
if (exp < 0)
{
exp = -exp - 1;
hex.Append("0.").Append('0', exp / 4);
bytes <<= 3 - exp % 4;
hex.Append(bytes.ToString("X").TrimEnd('0'));
}
else
{
bytes <<= (exp % 4);
exp &= ~3;
hex.Append(bytes.ToString("X"));
if (exp >= 52)
hex.Append('0', (exp - 52) / 4);
else
{
hex.Insert(exp / 4 + 1, '.');
hex = new StringBuilder(hex.ToString().TrimEnd('0').TrimEnd('.'));
}
}
if (negative) hex.Insert(0, '-');
return hex.ToString();
}
}
Post Categories