TOTP introduction and simple implementation based on C ා

Keywords: C# encoding SHA1 Unix

Intro

TOTP is a time-based one-time password generation algorithm, which is composed of RFC 6238 Definition. Different from the event based one-time password generation algorithm HOTP TOTP is time-based and has the following relationship with HOTP:

TOTP = HOTP(K, T)
HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))

Among them:

TOTP algorithm is based on HOTP. For HOTP algorithm, when the input of HOTP is consistent, it always outputs the same value, while TOTP is a value calculated based on time, which can be guaranteed to be fixed for a period of time (official recommendation is 30s) and the same value for a period of time, so as to achieve the one-time password generation algorithm based on time As a whole, it's not bad. There's a small problem. If you need to implement a password, you can only verify it once. You need to implement it in the business logic by yourself. TOTP is only responsible for generation and verification.

C ා implementation of TOTP

Implementation code

using System;
using System.Security.Cryptography;
using System.Text;

namespace WeihanLi.Totp
{
    public class Totp
    {
        private readonly OtpHashAlgorithm _hashAlgorithm;
        private readonly int _codeSize;

        public Totp() : this(OtpHashAlgorithm.SHA1, 6)
        {
        }

        public Totp(OtpHashAlgorithm otpHashAlgorithm, int codeSize)
        {
            _hashAlgorithm = otpHashAlgorithm;

            // valid input parameter
            if (codeSize <= 0 || codeSize > 10)
            {
                throw new ArgumentOutOfRangeException(nameof(codeSize), codeSize, "length must between 1 and 9");
            }
            _codeSize = codeSize;
        }

        private static readonly Encoding Encoding = new UTF8Encoding(false, true);

        public virtual string Compute(string securityToken) => Compute(Encoding.GetBytes(securityToken));

        public virtual string Compute(byte[] securityToken) => Compute(securityToken, GetCurrentTimeStepNumber());

        private string Compute(byte[] securityToken, long counter)
        {
            HMAC hmac;
            switch (_hashAlgorithm)
            {
                case OtpHashAlgorithm.SHA1:
                    hmac = new HMACSHA1(securityToken);
                    break;

                case OtpHashAlgorithm.SHA256:
                    hmac = new HMACSHA256(securityToken);
                    break;

                case OtpHashAlgorithm.SHA512:
                    hmac = new HMACSHA512(securityToken);
                    break;

                default:
                    throw new ArgumentOutOfRangeException(nameof(_hashAlgorithm), _hashAlgorithm, null);
            }

            using (hmac)
            {
                var stepBytes = BitConverter.GetBytes(counter);
                if (BitConverter.IsLittleEndian)
                {
                    Array.Reverse(stepBytes); // need BigEndian
                }
                // See https://tools.ietf.org/html/rfc4226
                var hashResult = hmac.ComputeHash(stepBytes);

                var offset = hashResult[hashResult.Length - 1] & 0xf;
                var p = "";
                for (var i = 0; i < 4; i++)
                {
                    p += hashResult[offset + i].ToString("X2");
                }
                var num = Convert.ToInt64(p, 16) & 0x7FFFFFFF;

                //var binaryCode = (hashResult[offset] & 0x7f) << 24
                //                 | (hashResult[offset + 1] & 0xff) << 16
                //                 | (hashResult[offset + 2] & 0xff) << 8
                //                 | (hashResult[offset + 3] & 0xff);

                return (num % (int)Math.Pow(10, _codeSize)).ToString();
            }
        }

        public virtual bool Verify(string securityToken, string code) => Verify(Encoding.GetBytes(securityToken), code);

        public virtual bool Verify(string securityToken, string code, TimeSpan timeToleration) => Verify(Encoding.GetBytes(securityToken), code, timeToleration);

        public virtual bool Verify(byte[] securityToken, string code) => Verify(securityToken, code, TimeSpan.Zero);

        public virtual bool Verify(byte[] securityToken, string code, TimeSpan timeToleration)
        {
            var futureStep = (int)(timeToleration.TotalSeconds / 30);
            var step = GetCurrentTimeStepNumber();
            for (int i = -futureStep; i <= futureStep; i++)
            {
                if (step + i < 0)
                {
                    continue;
                }
                var totp = Compute(securityToken, step + i);
                if (totp == code)
                {
                    return true;
                }
            }
            return false;
        }

        private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        /// <summary>
        /// timestep
        /// 30s(Recommend)
        /// </summary>
        private static readonly long _timeStepTicks = TimeSpan.TicksPerSecond * 30;

        // More info: https://tools.ietf.org/html/rfc6238#section-4
        private static long GetCurrentTimeStepNumber()
        {
            var delta = DateTime.UtcNow - _unixEpoch;
            return delta.Ticks / _timeStepTicks;
        }
    }
}

Usage:

    var otp = new Totp(OtpHashAlgorithm.SHA1, 4); // Output 4 bits using SHA1 algorithm
    var secretKey = "12345678901234567890";
    var output = otp.Compute(secretKey);
    Console.WriteLine($"output: {output}");
    Thread.Sleep(1000 * 30);
    var verifyResult = otp.Verify(secretKey, output); // Use the default verification method, valid within 30s
    Console.WriteLine($"Verify result: {verifyResult}");
    verifyResult = otp.Verify(secretKey, output, TimeSpan.FromSeconds(60)); // Specify tolerable time difference, valid within 60s
    Console.WriteLine($"Verify result: {verifyResult}");

Output example:

Reference

Posted by Yippee on Sun, 24 Nov 2019 13:04:35 -0800