C#でGoogleのワンタイムパスワード計算アルゴリズム TOTP (RFC6238)を実装する

Stackoverflowのこの投稿で十分

stackoverflow.com

だが、クリップボードにコピーして即プログラムを終了するように少し修正した。

using System;
using System.Windows.Forms;
using System.Security.Cryptography;

namespace totp
{
    class Program
    {
        public class Totp
        {
            const long unixEpochTicks = 621355968000000000L;
            const long ticksToSeconds = 10000000L;
            private const int step = 30;
            private const int totpSize = 6;
            private byte[] key;

            public Totp(byte[] secretKey)
            {
                key = secretKey;
            }

            public string ComputeTotp()
            {
                var window = CalculateTimeStepFromTimestamp(DateTime.UtcNow);

                var data = GetBigEndianBytes(window);

                var hmac = new HMACSHA1();
                hmac.Key = key;
                var hmacComputedHash = hmac.ComputeHash(data);

                int offset = hmacComputedHash[hmacComputedHash.Length - 1] & 0x0F;
                var otp = (hmacComputedHash[offset] & 0x7f) << 24
                       | (hmacComputedHash[offset + 1] & 0xff) << 16
                       | (hmacComputedHash[offset + 2] & 0xff) << 8
                       | (hmacComputedHash[offset + 3] & 0xff) % 1000000;

                var result = Digits(otp, totpSize);

                return result;
            }

            public int RemainingSeconds()
            {
                return step - (int)(((DateTime.UtcNow.Ticks - unixEpochTicks) / ticksToSeconds) % step);
            }

            private byte[] GetBigEndianBytes(long input)
            {
                // Since .net uses little endian numbers, we need to reverse the byte order to get big endian.
                var data = BitConverter.GetBytes(input);
                Array.Reverse(data);
                return data;
            }

            private long CalculateTimeStepFromTimestamp(DateTime timestamp)
            {
                var unixTimestamp = (timestamp.Ticks - unixEpochTicks) / ticksToSeconds;
                var window = unixTimestamp / (long)step;
                return window;
            }

            private string Digits(long input, int digitCount)
            {
                var truncatedValue = ((int)input % (int)Math.Pow(10, digitCount));
                return truncatedValue.ToString().PadLeft(digitCount, '0');
            }

        }
        public static class Base32Encoding
        {
            public static byte[] ToBytes(string input)
            {
                if (string.IsNullOrEmpty(input))
                {
                    throw new ArgumentNullException("input");
                }

                input = input.TrimEnd('='); //remove padding characters
                int byteCount = input.Length * 5 / 8; //this must be TRUNCATED
                byte[] returnArray = new byte[byteCount];

                byte curByte = 0, bitsRemaining = 8;
                int mask = 0, arrayIndex = 0;

                foreach (char c in input)
                {
                    int cValue = CharToValue(c);

                    if (bitsRemaining > 5)
                    {
                        mask = cValue << (bitsRemaining - 5);
                        curByte = (byte)(curByte | mask);
                        bitsRemaining -= 5;
                    }
                    else
                    {
                        mask = cValue >> (5 - bitsRemaining);
                        curByte = (byte)(curByte | mask);
                        returnArray[arrayIndex++] = curByte;
                        curByte = (byte)(cValue << (3 + bitsRemaining));
                        bitsRemaining += 3;
                    }
                }

                //if we didn't end with a full byte
                if (arrayIndex != byteCount)
                {
                    returnArray[arrayIndex] = curByte;
                }

                return returnArray;
            }

            public static string ToString(byte[] input)
            {
                if (input == null || input.Length == 0)
                {
                    throw new ArgumentNullException("input");
                }

                int charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
                char[] returnArray = new char[charCount];

                byte nextChar = 0, bitsRemaining = 5;
                int arrayIndex = 0;

                foreach (byte b in input)
                {
                    nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
                    returnArray[arrayIndex++] = ValueToChar(nextChar);

                    if (bitsRemaining < 4)
                    {
                        nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
                        returnArray[arrayIndex++] = ValueToChar(nextChar);
                        bitsRemaining += 5;
                    }

                    bitsRemaining -= 3;
                    nextChar = (byte)((b << bitsRemaining) & 31);
                }

                //if we didn't end with a full char
                if (arrayIndex != charCount)
                {
                    returnArray[arrayIndex++] = ValueToChar(nextChar);
                    while (arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding
                }

                return new string(returnArray);
            }

            private static int CharToValue(char c)
            {
                int value = (int)c;

                //65-90 == uppercase letters
                if (value < 91 && value > 64)
                {
                    return value - 65;
                }
                //50-55 == numbers 2-7
                if (value < 56 && value > 49)
                {
                    return value - 24;
                }
                //97-122 == lowercase letters
                if (value < 123 && value > 96)
                {
                    return value - 97;
                }

                throw new ArgumentException("Character is not a Base32 character.", "c");
            }

            private static char ValueToChar(byte b)
            {
                if (b < 26)
                {
                    return (char)(b + 65);
                }

                if (b < 32)
                {
                    return (char)(b + 24);
                }

                throw new ArgumentException("Byte is not a value Base32 value.", "b");
            }

        }
        [STAThread]
        static void Main(string[] args)
        {
            var bytes = Base32Encoding.ToBytes("JBSWY3DPEHPK3PXP");

            var totp = new Totp(bytes);

            var result = totp.ComputeTotp();
            Clipboard.SetText(result.ToString());

            //var remainingTime = totp.RemainingSeconds();
        }
    }
}