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();