Skip to content

Instantly share code, notes, and snippets.

@ddjerqq
Last active November 13, 2023 17:15
Show Gist options
  • Save ddjerqq/af2c9a7d4d26c70a7e98c4e422f426eb to your computer and use it in GitHub Desktop.
Save ddjerqq/af2c9a7d4d26c70a7e98c4e422f426eb to your computer and use it in GitHub Desktop.
TOTP: Time-Based One-Time Password Algorithm Implementation in C#
using System.Security.Cryptography;
namespace Rfc6238;
public enum HmacAlgo
{
Sha1,
Sha256,
Sha512,
}
internal static class Extensions
{
internal static byte[] ComputeHash(this HmacAlgo algo, byte[] key, byte[] payload)
{
HMAC hmac = algo switch
{
HmacAlgo.Sha1 => new HMACSHA1(key),
HmacAlgo.Sha256 => new HMACSHA256(key),
HmacAlgo.Sha512 => new HMACSHA512(key),
_ => throw new ArgumentOutOfRangeException(nameof(algo), "Unknown HMAC algorithm"),
};
return hmac.ComputeHash(payload);
}
internal static byte[] ToBytes(this string hex)
{
if (hex.Length % 2 == 1)
throw new Exception("The binary key cannot have an odd number of digits");
byte[] arr = new byte[hex.Length >> 1];
for (int i = 0; i < hex.Length >> 1; ++i)
arr[i] = (byte)((GetHexVal(hex[i << 1]) << 4) + GetHexVal(hex[(i << 1) + 1]));
return arr;
}
private static int GetHexVal(char hex)
{
// cast to int
int val = hex;
return val - (val < 58 ? 48 : val < 97 ? 55 : 87);
}
}
public static class Totp
{
public const long T0 = 0;
public const long X = 30;
public static string Generate(byte[] key, long timestamp, int digits, HmacAlgo crypto)
{
long T = (timestamp - T0) / X;
// convert to hex and pad with 16 0s
byte[] payload = T
.ToString("X016")
.ToBytes();
byte[] hash = crypto.ComputeHash(key, payload);
int offset = hash[^1] & 0xf;
int binary =
((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
int otp = binary % (int)Math.Pow(10, digits);
return otp.ToString($"D{digits}");
}
}
@ddjerqq
Copy link
Author

ddjerqq commented Nov 13, 2023

tests:

[EditorBrowsable(EditorBrowsableState.Never)]
internal class TestTotp
{
    // key for HMAC-SHA1 - 20 bytes
    private const string Key = "3132333435363738393031323334353637383930";

    // key for HMAC-SHA256 - 32 bytes
    private const string Key32 = "3132333435363738393031323334353637383930313233343536373839303132";

    // key for HMAC-SHA512 - 64 bytes
    private const string Key64 = "31323334353637383930313233343536373839303132333435363738393031323334353637383930" +
                                  "313233343536373839303132333435363738393031323334";

    private static readonly long[] TestTimes = { 59, 1111111109, 1111111111, 1234567890, 2000000000, 20000000000 };
    private static readonly string[] ExpectedSha1Totps = { "94287082", "07081804", "14050471", "89005924", "69279037", "65353130" };
    private static readonly string[] ExpectedSha256Totps = { "46119246", "68084774", "67062674", "91819424", "90698825", "77737706" };
    private static readonly string[] ExpectedSha512Totps = { "90693936", "25091201", "99943326", "93441116", "38618901", "47863826" };

    [Test]
    public void PrintTable()
    {
        foreach (long t in TestTimes)
        {
            Console.WriteLine($"steps: {t}");
            Console.WriteLine($"Sha1: {Totp.Generate(Key.ToBytes(), t, 8, HmacAlgo.Sha1)}");
            Console.WriteLine($"Sha256: {Totp.Generate(Key32.ToBytes(), t, 8, HmacAlgo.Sha256)}");
            Console.WriteLine($"Sha512: {Totp.Generate(Key64.ToBytes(), t, 8, HmacAlgo.Sha512)}");
            Console.WriteLine();
        }
    }

    [Test]
    public void TestSha1()
    {
        foreach ((long time, string expectedTotp) in TestTimes.Zip(ExpectedSha1Totps))
        {
            var totp = Totp.Generate(Key.ToBytes(), time, 8, HmacAlgo.Sha1);
            Assert.That(expectedTotp, Is.EqualTo(totp));
        }
    }

    [Test]
    public void TestSha256()
    {
        foreach ((long time, string expectedTotp) in TestTimes.Zip(ExpectedSha256Totps))
        {
            var totp = Totp.Generate(Key32.ToBytes(), time, 8, HmacAlgo.Sha256);
            Assert.That(expectedTotp, Is.EqualTo(totp));
        }
    }

    [Test]
    public void TestSha512()
    {
        foreach ((long time, string expectedTotp) in TestTimes.Zip(ExpectedSha512Totps))
        {
            var totp = Totp.Generate(Key64.ToBytes(), time, 8, HmacAlgo.Sha512);
            Assert.That(expectedTotp, Is.EqualTo(totp));
        }
    }

    [Test]
    public void TestSha1RandomKey()
    {
        var key = Guid.NewGuid().ToString().Replace("-", "").ToBytes();
        var date = new DateTime(2021, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var timestamp = new DateTimeOffset(date, TimeSpan.Zero).ToUnixTimeSeconds();

        var totp = Totp.Generate(key, timestamp, 6, HmacAlgo.Sha1);

        Console.WriteLine($"timestamp: {timestamp}");
        Console.WriteLine($"totp: {totp}");
    }

    [Test]
    public void TestSha256RandomKey()
    {
        var key = Guid.NewGuid().ToString().Replace("-", "").ToBytes();
        var date = new DateTime(2021, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var timestamp = new DateTimeOffset(date, TimeSpan.Zero).ToUnixTimeSeconds();

        var totp = Totp.Generate(key, timestamp, 6, HmacAlgo.Sha256);

        Console.WriteLine($"timestamp: {timestamp}");
        Console.WriteLine($"totp: {totp}");
    }

    [Test]
    public void TestSha512RandomKey()
    {
        var key = Guid.NewGuid().ToString().Replace("-", "").ToBytes();
        var date = new DateTime(2021, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var timestamp = new DateTimeOffset(date, TimeSpan.Zero).ToUnixTimeSeconds();

        var totp = Totp.Generate(key, timestamp, 6, HmacAlgo.Sha512);

        Console.WriteLine($"timestamp: {timestamp}");
        Console.WriteLine($"totp: {totp}");
    }
}

@ddjerqq
Copy link
Author

ddjerqq commented Nov 13, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment