-
-
Save sefgit/4e4a39ec781cd4a0f81d0cce29e1d6d5 to your computer and use it in GitHub Desktop.
TOTP: Time-Based One-Time Password Algorithm Implementation in C#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}"); | |
| } | |
| } | |
| /*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}"); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment