Skip to content

Instantly share code, notes, and snippets.

@exceedsystem
Last active October 18, 2023 16:35
Show Gist options
  • Save exceedsystem/6d67a7354aaea14c307c895f621a896b to your computer and use it in GitHub Desktop.
Save exceedsystem/6d67a7354aaea14c307c895f621a896b to your computer and use it in GitHub Desktop.
A simple example of implementing TOTP in C#
// https://www.exceedsystem.net/2021/10/04/how-to-implement-otp-in-csharp-so-easy/
// License:MIT
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
namespace _2FAS
{
internal static class Program
{
/// <summary>
/// Interval time(sec)
/// </summary>
private const int INTERVAL_SEC = 30;
/// <summary>
/// Digits()
/// </summary>
private const int NUM_OF_DIGITS = 6;
/// <summary>
/// Entry
/// </summary>
/// <param name="_">unused</param>
private static void Main(string[] _)
{
Console.WriteLine("Enter the TOTP private key encoded in base32.");
var privateKey = DecodeBase32(Console.ReadLine()).ToArray();
var isFirstTime = true;
while (true)
{
var remainingSec = (long)TimeSpan.FromTicks(DateTime.UtcNow.Ticks).TotalSeconds % INTERVAL_SEC;
if (isFirstTime || remainingSec == 0)
{
isFirstTime = false;
var totp = GetTOTP(privateKey, NUM_OF_DIGITS, INTERVAL_SEC);
Console.WriteLine(totp);
}
Console.Title = $"TOTP Client: TIME REMAINING:{INTERVAL_SEC - remainingSec,2}";
Thread.Sleep(1000);
}
}
/// <summary>
/// Get TOTP password
/// </summary>
/// <param name="privateKey">Private key</param>
/// <param name="numOfDigits">Number of digits in generated password</param>
/// <param name="interval">Password generation interval(sec)</param>
/// <returns>TOTP password</returns>
private static string GetTOTP(byte[] privateKey, int numOfDigits, int interval)
{
var counter = (long)(DateTime.UtcNow - DateTime.UnixEpoch).TotalSeconds / interval;
return GetOTP(privateKey, counter, numOfDigits);
}
/// <summary>
/// Get OTP password
/// </summary>
/// <param name="privateKey">Private key</param>
/// <param name="iteration">Iteration number</param>
/// <param name="numOfDigits">Number of digits in generated password</param>
/// <returns>OTP</returns>
private static string GetOTP(byte[] privateKey, long iteration, int numOfDigits)
{
var counter = BitConverter.GetBytes(iteration);
if (BitConverter.IsLittleEndian)
Array.Reverse(counter);
var hmacSha1 = new HMACSHA1(privateKey, true);
var hmacResult = hmacSha1.ComputeHash(counter);
var offset = hmacResult[^1] & 0xf;
var binCode = ((hmacResult[offset] & 0x7f) << 24)
| ((hmacResult[offset + 1] & 0xff) << 16)
| ((hmacResult[offset + 2] & 0xff) << 8)
| (hmacResult[offset + 3] & 0xff);
var password = binCode % (int)Math.Pow(10, numOfDigits);
return password.ToString(new string('0', numOfDigits));
}
/// <summary>
/// Decode Base32 string into enumerable byte data
/// </summary>
/// <param name="encodedString">Base32 encorded string</param>
/// <returns>Decoded byte data</returns>
private static IEnumerable<byte> DecodeBase32(string encodedString)
{
var numOfBit = 0;
byte decoded = 0;
foreach (var base32Char in encodedString.ToUpper())
{
var base32Val = 0;
switch (base32Char)
{
case >= 'A' and <= 'Z':
base32Val = base32Char - 65;
break;
case >= '2' and <= '7':
base32Val = base32Char - 24;
break;
}
var bitMask = 0x10;
for (var i = 0; i < 5; ++i)
{
decoded |= (byte)((base32Val & bitMask) != 0 ? 1 : 0);
if (++numOfBit == 8)
{
yield return decoded;
numOfBit = 0;
decoded = 0;
}
decoded <<= 1;
bitMask >>= 1;
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment