About.

Introduction

A time-based, One-time Password Algorithm (RFC-6238, TOTP - HMAC-based One-time Password Algorithm) based token, implemented by e.g. Microsoft or Google Authenticator mobile applications. Mobile application allows you to register your account with Microsoft / Google or any other TOTP authenticator application (via a specially generated QR code). After successful registration, the authenticator application will generate a new code every 30 seconds which could be used to implement MFA based sign-in. To make it a complete MFA, a PIN is added as a prefix to the application generated code. The sign-in password or some call it Passcode will be the PIN + Code.

Background

To secure access to any C#, Java or C++ (Windows or Linux) web or normal application, MFA is a best and easy option without creating a custom mobile application of your own. It completes the scenario, that something you know and something you have.  Here something you know is your PIN, and something you have is your mobile app and your bio-matric features forced by the authenticator mobile applications like Microsoft Authenticator.

Registration of the QR Code

The authenticator application (Microsoft and Google) follows a standard. Though, only Google defines the URI and parameters required to register an account with the Authenticator Application.

The first step logically is the ability to generate the QR code to register the required user with the authenticator application. The magic ingredient here is the TOTP seed, Company / Web Application user belongs to and User's UPN or email address.

The code below generates a seed using GUID (I use GUID because there is 1 in 2 billion chance that same GUID will ever be regenerated):

/**
* Converts Hex string to Unsigned Bytes (0 to 256)
*/
public static Byte[] HexToByte(string hexStr)
{
    byte[] bArray = new byte[hexStr.Length / 2];
    for (int i = 0; i < (hexStr.Length / 2); i++)
    {
        byte firstNibble = Byte.Parse(hexStr.Substring((2 * i), 1), System.Globalization.NumberStyles.HexNumber); // [x,y)
        byte secondNibble = Byte.Parse(hexStr.Substring((2 * i) + 1, 1), System.Globalization.NumberStyles.HexNumber);
        int finalByte = (secondNibble) | (firstNibble << 4); // bit-operations only with numbers, not bytes.
        bArray[i] = (byte)finalByte;
    }
    return bArray;
}

/*
 * Generates GUID as a string and remove brackets
 */
public static string getNewId()
{
    string sR = Guid.NewGuid().ToString().ToUpper();
    sR = sR.Replace("{", "");
    sR = sR.Replace("}", "");
    return sR;
}

/*
 * Generates the QR code for authenticator as a base64 encoded svg image
 * You must use something like
 * <img runat="server" id="qrCode" name="qrCode" src="javascript:" alt="Scan this QR code with your mobile application" style="height:300px;width:300px"/>
 */
private void generateQRCode()
{
    //create new key based on hash to be used
    string seed = getNewId() + getNewId();
    seed = seed.Replace("-", "");
    seed = seed.Substring(0, 40);

    byte[] byteSeed = HexToByte(seed);


    //Must save this seed to be able to validate the TOTP
    var KeyString = Base32.ToBase32String(byteSeed);

    string orgDomain = "elogic.synology.me";
    string orgName = "eLogic Builders Inc.";
    string userUPN = "Kashif" + '@' + orgDomain;

    const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&algorithm=SHA1&digits=6&period=30";

    string tokenURI = string.Format(
        AuthenticatorUriFormat,
        HttpUtility.UrlEncode(orgDomain),
        HttpUtility.UrlEncode(userUPN),
        KeyString);


    var qr = QrCode.EncodeText(tokenURI, QrCode.Ecc.High);

    string base64EncodedImage = Convert.ToBase64String(Encoding.UTF8.GetBytes(qr.ToSvgString(4)));

    string imageSrc = "data:image/svg+xml;base64," + base64EncodedImage;

    //Assign image here in your ASP application
    //this.qrCode.Src = imageSrc;
}

The KeyString (TOTP seed) must be saved and linked to the user being authenticated. The same seed will be used to authenticate user's entered TOTP. To generate the QR code, I used Net.Codecrete.QrCodeGenerator nuget.org package. This is good to generate QR code on Windows and Linux (using Mono framework). You can use other implementations which suits your application.

Below is the example of a registration link I used to send for registration:

When user follows the link, the QR code generation and registration sequence starts. Here is what is presented to the user:

User scans the QR code with the Microsoft or Google, or with any other RFC-6238 compliant TOTP authenticator application. The application should register the seed and user's UPN and should start generating the TOTPs:

Using the RFC-6238 compliant class below, you could validate the generated TOTP (it is little modified Microsoft code sample version):

using System;
using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using System.Text;


class SecurityToken
{
    private readonly byte[] _data;

    public SecurityToken(byte[] data)
    {
        _data = (byte[])data.Clone();
    }

    internal byte[] GetDataNoClone()
    {
        return _data;
    }
}

public static class Rfc6238AuthenticationService
{
    private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3);
    private static readonly Encoding _encoding = new UTF8Encoding(false, true);

    public static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
    {
        // # of 0's = length of pin
        const int mod = 1000000;

        // See https://tools.ietf.org/html/rfc4226
        // We can add an optional modifier
        var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber));
        var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier));

        // Generate DT string
        var offset = hash[hash.Length - 1] & 0xf;
        Debug.Assert(offset + 4 < hash.Length);
        var binaryCode = (hash[offset] & 0x7f) << 24
                         | (hash[offset + 1] & 0xff) << 16
                         | (hash[offset + 2] & 0xff) << 8
                         | (hash[offset + 3] & 0xff);

        return binaryCode % mod;
    }

    private static byte[] ApplyModifier(byte[] input, string modifier)
    {
        if (String.IsNullOrEmpty(modifier))
        {
            return input;
        }

        var modifierBytes = _encoding.GetBytes(modifier);
        var combined = new byte[checked(input.Length + modifierBytes.Length)];
        Buffer.BlockCopy(input, 0, combined, 0, input.Length);
        Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length);
        return combined;
    }

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

    private static int GenerateCode(SecurityToken securityToken, string modifier = null)
    {
        if (securityToken == null)
        {
            throw new ArgumentNullException("securityToken");
        }

        // Allow a variance of no greater than 9 minutes in either direction
        var currentTimeStep = GetCurrentTimeStepNumber();
        using (var hashAlgorithm = new HMACSHA1(securityToken.GetDataNoClone()))
        {
            return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
        }
    }

    private static bool ValidateCode(SecurityToken securityToken, int code, string modifier = null)
    {
        if (securityToken == null)
        {
            throw new ArgumentNullException("securityToken");
        }

        // Allow a variance of no greater than 9 minutes in either direction
        var currentTimeStep = GetCurrentTimeStepNumber();
        using (var hashAlgorithm = new HMACSHA1(securityToken.GetDataNoClone()))
        {
            for (var i = -2; i <= 2; i++)
            {
                var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier);
                if (computedTotp == code)
                {
                    return true;
                }
            }
        }

        // No match
        return false;
    }
}

Here is the function you can use to validate the generated TOTP:

    public bool CheckTimeBasedOTP_Rfc6238(byte[] byteSeed, string incomingOTP)
    {
        bool bR = false;
        int IntIncomingCode = int.Parse(incomingOTP);

        var hash = new HMACSHA1(byteSeed);
        var unixTimestamp = Convert.ToInt64(Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds));
        var timestep = Convert.ToInt64(unixTimestamp / 30);
        // Allow codes from 90s in each direction (we could make this configurable?)
        for (long i = -2; i <= 2; i++)
        {
            var expectedCode = Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)(timestep + i), modifier: null);
            if (expectedCode == IntIncomingCode)
            {
                bR = true;
                break;
            }
        }

        return bR;
    }

The byteSeed is a byte array you can convert from Base32 encoded and saved seed.

Base32 Encoder / Decoder:

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

public static class Base32
{
    private static readonly char[] _digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
    private const int _mask = 31;
    private const int _shift = 5;

    private static int CharToInt(char c)
    {
        switch (c)
        {
            case 'A': return 0;
            case 'B': return 1;
            case 'C': return 2;
            case 'D': return 3;
            case 'E': return 4;
            case 'F': return 5;
            case 'G': return 6;
            case 'H': return 7;
            case 'I': return 8;
            case 'J': return 9;
            case 'K': return 10;
            case 'L': return 11;
            case 'M': return 12;
            case 'N': return 13;
            case 'O': return 14;
            case 'P': return 15;
            case 'Q': return 16;
            case 'R': return 17;
            case 'S': return 18;
            case 'T': return 19;
            case 'U': return 20;
            case 'V': return 21;
            case 'W': return 22;
            case 'X': return 23;
            case 'Y': return 24;
            case 'Z': return 25;
            case '2': return 26;
            case '3': return 27;
            case '4': return 28;
            case '5': return 29;
            case '6': return 30;
            case '7': return 31;
        }
        return -1;
    }

    public static byte[] FromBase32String(string encoded)
    {
        if (encoded == null)
            throw new ArgumentNullException(nameof(encoded));

        // Remove whitespace and padding. Note: the padding is used as hint 
        // to determine how many bits to decode from the last incomplete chunk
        // Also, canonicalize to all upper case
        encoded = encoded.Trim().TrimEnd('=').ToUpper();
        if (encoded.Length == 0)
            return new byte[0];

        var outLength = encoded.Length * _shift / 8;
        var result = new byte[outLength];
        var buffer = 0;
        var next = 0;
        var bitsLeft = 0;
        var charValue = 0;
        foreach (var c in encoded)
        {
            charValue = CharToInt(c);
            if (charValue < 0)
                throw new FormatException("Illegal character: `" + c + "`");

            buffer <<= _shift;
            buffer |= charValue & _mask;
            bitsLeft += _shift;
            if (bitsLeft >= 8)
            {
                result[next++] = (byte)(buffer >> (bitsLeft - 8));
                bitsLeft -= 8;
            }
        }

        return result;
    }

    public static string ToBase32String(byte[] data, bool padOutput = false)
    {
        return ToBase32String(data, 0, data.Length, padOutput);
    }

    public static string ToBase32String(byte[] data, int offset, int length, bool padOutput = false)
    {
        if (data == null)
            throw new ArgumentNullException(nameof(data));

        if (offset < 0)
            throw new ArgumentOutOfRangeException(nameof(offset));

        if (length < 0)
            throw new ArgumentOutOfRangeException(nameof(length));

        if ((offset + length) > data.Length)
            throw new ArgumentOutOfRangeException();

        if (length == 0)
            return "";

        // SHIFT is the number of bits per output character, so the length of the
        // output is the length of the input multiplied by 8/SHIFT, rounded up.
        // The computation below will fail, so don't do it.
        if (length >= (1 << 28))
            throw new ArgumentOutOfRangeException(nameof(data));

        var outputLength = (length * 8 + _shift - 1) / _shift;
        var result = new StringBuilder(outputLength);

        var last = offset + length;
        int buffer = data[offset++];
        var bitsLeft = 8;
        while (bitsLeft > 0 || offset < last)
        {
            if (bitsLeft < _shift)
            {
                if (offset < last)
                {
                    buffer <<= 8;
                    buffer |= (data[offset++] & 0xff);
                    bitsLeft += 8;
                }
                else
                {
                    int pad = _shift - bitsLeft;
                    buffer <<= pad;
                    bitsLeft += pad;
                }
            }
            int index = _mask & (buffer >> (bitsLeft - _shift));
            bitsLeft -= _shift;
            result.Append(_digits[index]);
        }
        if (padOutput)
        {
            int padding = 8 - (result.Length % 8);
            if (padding > 0) result.Append('=', padding == 8 ? 0 : padding);
        }
        return result.ToString();
    }
}

1st Version: 08 July 2024