04 August 2023

MFA - Generate time-based one-time passwords (TOTP)

I had to implement MFA for a web application. I needed to generate a key the user could save in an authenticator app. My application had to use this key to generate a code to validate the authenticator codes the user would enter.

Generate the key

Generate a truly random sequence of bytes and convert them to base32. I'm using the default SHA1 algorithm to generate keys of twenty characters.

const string BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

using (var gen = new RNGCryptoServiceProvider()) //Use a cryptographically secure provider!
{
    byte[] bytes = new byte[HMACSHA1.Create().HashSize / 8];
    gen.GetBytes(bytes); //Fills the array with 20 bytes
    
    byte[] base32 = new byte[bytes.Length];
    double d = (double)byte.MaxValue / (double)BASE32_CHARS.Length;
    
    for (int i = 0; i < bytes.Length; i++)
    {
        double c = (double)bytes[i];
        int j = (int)Math.Floor(c / d); //If c is 255, j is 32, which is out of bounds!
        base32[i] = (byte)BASE32_CHARS[j < BASE32_CHARS.Length ? j : BASE32_CHARS.Length - 1]; //Scale from 255 to # of possible chars
    }
    
    return Encoding.ASCII.GetString(base32);
}

Generate a code

First calculate the number of intervals that passed since the reference time. This time is usually the unix epoch of 1 january 1970 0:00:00 UTC. Then decode the base32 key and use it to hash the interval counter. Finally use this hash to generate a code like an authenticator app would.

const uint INTERVAL = 30; //The default of 30 seconds
const uint CODE_LENGTH = 6; //The default of 6 digits

double seconds = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
ulong counter = (ulong)Math.Floor(seconds / INTERVAL);
byte[] counterBytes = BitConverter.GetBytes(counter);
Array.Reverse(counterBytes); //Must be big-endian while .net is little-endian

var hmac = HMACSHA1.Create();
hmac.Key = DecodeBase32(key);
hmac.ComputeHash(counterBytes);

int offset = hmac.Hash[hmac.Hash.Length - 1] & 0xf;
int bin = (hmac.Hash[offset + 0] & 0x7f) << 24 |
          (hmac.Hash[offset + 1] & 0xff) << 16 |
          (hmac.Hash[offset + 2] & 0xff) << 8 |
          (hmac.Hash[offset + 3] & 0xff);

return bin % (int)Math.Pow(10, CODE_LENGTH);

DecodeBase32(string value):

var base32 = BASE32_CHARS.Select((c, i) => new { c, i }).ToDictionary(ch => ch.c, ch => ch.i); //Each char and it's 0–31 index
string bits = string.Concat(value.Select(c => Convert.ToString(base32[c], 2).PadLeft(5, '0'))); //A string of 0's en 1's
return Enumerable.Range(0, bits.Length / 8).Select(i => Convert.ToByte(bits.Substring(i * 8, 8), 2)).ToArray();

No comments:

Post a Comment