How to do TOTP for 2FA in .NET

.NET 2FA TOTP
Multi-factor authentication is something many of us take for granted. There has been a continuous push towards dependence on smart phones, proprietary apps and identity providers such as Google and Microsoft for two-factor authentication. However, such dependence on these providers leads to privacy issues, whilst the use of SMS to deliver one-time-passwords is hopelessly insecure.

In this article, I will describe how to create Time-based One Time Passwords (TOTP) in .NET

What is multi-factor authentication?



The multi in multi-factor means that we need to use two or more categories of information to authenticate a user. There are three categories:
  • Something you know
    (a username and a password)

  • Something you have
    (a SIM card, a USB key, a secret)

  • Something you are
    (biometric – fingerprint, iris scan, facial recognition)

Personally, I don’t like the idea of providing true biometric information to a server, if that server is beyond your control, so we’ll stick to something you know and have.

Benefits of 2FA



The most obvious reason for utilising 2FA is to increase the cryptographic key space assault vector for an attacker. Even with a simple 6-digit one-time-password, it would still take on average half a million attempts for a malicious attacker who knows the username and password of the victim to gain access to their account.
With a constantly changing time-based 8-digit code, an attacker would need to attempt more than 50 tries per second for an entire year to gain access to an account, where the username and password is known. That is plenty of time to temporarily suspend the account, or force the user to change their password.
Two-factor authentication makes malicious logins almost impossible, if done correctly, using the right technologies.

The perils of Identity Providers



Many of us use “SSO” (Single-Sign On) with services such as Microsoft, Google, Facebook and others, as an easy way to securely login to other websites. Whilst their implementation of 2FA may be secure - such as Microsoft’s “Authenticator” app – they are certainly not private.
Every time a user logs in to a web app with their Microsoft account, Microsoft may store a record which includes the user’s IP address, GPS location (if applicable), timestamp, device, time zone, and much more. They harvest your metadata.
The same applies to other providers, such as Google.

SMS Codes



SMS (Short Message Service) is incredibly insecure, easy to intercept, and unencrypted. Basing 2FA authentication on SMS is done merely for convenience, not for security. It’s better than no 2FA, but not against sophisticated attackers (such as governments, spy agencies, or telecommunications companies).

TOTP – The Best Solution!



Time-based One Time Passwords were described in RFC 6238.
TOTP uses a shared secret which is transmitted only once (it must be done securely!) from server to client.
TOTP uses standard HMAC technologies (HMACSHA1, HMACSHA256 or HMACSHA512) to create highly secure hashes based on a key (the shared secret) and the current time (number of 30-second hops since the Linux epoch).
TOTP codes can be 6, 7 or 8 digits. The more the better!
TOTP codes can be verified in real time on the server, meaning no caching is necessary. TOTP codes don’t “time out”, they just have to be entered correctly before the authentication session times out.

TOTP in a nutshell



The TOTP workflow functions like the following:
  1. Server generates a random 24-character base-32 secret

  2. The secret is sent securely to the client

  3. The client saves the secret and uses it for future logins

  4. When the client authenticates, it generates a TOTP code (using the secret), and sends it to the server

  5. If the server is able to generate the same code, based on the shared secret and time, authentication is successful

  6. The user receives an authentication token


Base-32 … why?



Base-32 is not to be confused with Base-64!
Base-32 is much more user friendly than Base-64 for sharing information. It requires 20% more space than Base-64, so it is typically not used for storing and sending binary data. But it’s perfect for sharing a short secret, because the character set is:
  • The 26 uppercase letters of the alphabet

  • Numbers 2-7

The reason for this is to avoid confusion – 0 with O, 1 with I, and 8 with B. Clever!
A 24-character password is equivalent to 24 * 5 bit = 120 bit
That is more than one trillion trillion trillion combinations, which is strong enough for generating 2FA codes.

Show me the code already!



First, a quick library for the Base-32 encoding.

public static class Base32
{
public static byte[] ToBytes(string input)
{
if (string.IsNullOrWhiteSpace(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, arrayIndex = 0;

foreach (var c in input)
{
var 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 (arrayIndex != byteCount) // if we didn't end with a full byte
returnArray[arrayIndex] = curByte;

return returnArray;
}

public static string ToString(byte[] input)
{
if (input is null || input.Length == 0)
throw new ArgumentNullException(nameof(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 (arrayIndex != charCount) // if we didn't end with a full char
{
returnArray[arrayIndex++] = ValueToChar(nextChar);

while (arrayIndex != charCount)
returnArray[arrayIndex++] = '='; // padding
}

return new string(returnArray);
}

private static int CharToValue(char c)
{
int value = 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 " + c + " is not a Base32 character!", nameof(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 " + b + " is not a Base32 value!", nameof(b));
}
}


Now let’s define the allowed HMAC algorithms

public enum HmacHashMode
{
Sha1,
Sha256,
Sha512
}


And finally, here’s the TOTP code generation algorithm

using System.Security.Cryptography;

namespace ACA.Totp;

public static class Totp
{
public const byte TIME_WINDOW_SECONDS = 30;
private const byte DEFAULT_CODE_LENGTH = 6;
private const HmacHashMode DEFAULT_HASH_MODE = HmacHashMode.Sha1;

public static long CalculateStepsFromTime(byte windowLength = TIME_WINDOW_SECONDS)
=> DateTimeOffset.UtcNow.ToUnixTimeSeconds() / windowLength;

///
/// .NET uses Little Endian numbers.
/// This method reverses the byte order to get Big Endian.
///

/// The number to get bytes for
public static byte[] GetBigEndianBytes(long input)
{
var bytes = BitConverter.GetBytes(input);
Array.Reverse(bytes);
return bytes;
}


public static HMAC GetHmac(byte[] key, HmacHashMode mode = DEFAULT_HASH_MODE)
=> mode switch
{
HmacHashMode.Sha1 => new HMACSHA1(key),
HmacHashMode.Sha256 => new HMACSHA256(key),
HmacHashMode.Sha512 => new HMACSHA512(key),
_ => throw new NotImplementedException("Mode " + mode + " is not implemented!"),
};


public static string ConvertHashToCode(byte[] hash, byte codeLength)
{
var offset = hash[hash.Length - 1] & 0x0F;

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

return TruncateAndPad(finalInt, codeLength);
}


private static string TruncateAndPad(int input, byte digitCount)
{
var truncatedValue = input % (int)Math.Pow(10, digitCount);

return truncatedValue.ToString().PadLeft(digitCount, '0');
}


public static string[] GenerateValidCodes(byte[] key, HmacHashMode mode = DEFAULT_HASH_MODE,
byte codeLength = DEFAULT_CODE_LENGTH, byte windowExtension = 1,
byte timeWindowSeconds = TIME_WINDOW_SECONDS)
{
if (codeLength < 6 || codeLength > 8)
throw new ArgumentOutOfRangeException(nameof(codeLength), "The code length must be 6-8 digits");

var array = new string[1 + windowExtension * 2];
ushort index = 0;

var steps = CalculateStepsFromTime(timeWindowSeconds);

using var hmac = GetHmac(key, mode);

for (long i = steps - windowExtension; i <= steps + windowExtension; i++)
{
var output = ConvertHashToCode(hmac.ComputeHash(GetBigEndianBytes(i)), codeLength);

array[index++] = output;
}

return array;
}
}


Happy coding! Please remember to share!
Hey you! I need your help!

Thanks for reading! All the content on this site is free, but I need your help to spread the word. Please support me by:

  1. Sharing my page on Facebook
  2. Tweeting my page on Twitter
  3. Posting my page on LinkedIn
  4. Bookmarking this site and returning in the near future
Thank you!