While playing around with Google Authenticator, I came across this question and in particular the code contributed by Espo. I personally wasn't satisfied with the conversion from Java to C# and so I thought I would share my version. Aside from heavily refactoring the code:
- Introduced check for little-endian byte ordering and convert to big-endian as necessary.
- Introduced parameter for the HMAC key.
For more information on the provisioning url format, see also: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
Feel free to use if you like, and thanks to Espo for the initial work.
using System;
using System.Globalization;
using System.Net;
using System.Security.Cryptography;
using System.Text;
public class GoogleAuthenticator
{
const int IntervalLength = 30;
const int PinLength = 6;
static readonly int PinModulo = (int)Math.Pow(10, PinLength);
static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
///
/// Number of intervals that have elapsed.
///
static long CurrentInterval
{
get
{
var ElapsedSeconds = (long)Math.Floor((DateTime.UtcNow - UnixEpoch).TotalSeconds);
return ElapsedSeconds/IntervalLength;
}
}
///
/// Generates a QR code bitmap for provisioning.
///
public byte[] GenerateProvisioningImage(string identifier, byte[] key, int width, int height)
{
var KeyString = Encoder.Base32Encode(key);
var ProvisionUrl = Encoder.UrlEncode(string.Format("otpauth://totp/{0}?secret={1}&issuer=MyCompany", identifier, KeyString));
var ChartUrl = string.Format("https://chart.apis.google.com/chart?cht=qr&chs={0}x{1}&chl={2}", width, height, ProvisionUrl);
using (var Client = new WebClient())
{
return Client.DownloadData(ChartUrl);
}
}
///
/// Generates a pin for the given key.
///
public string GeneratePin(byte[] key)
{
return GeneratePin(key, CurrentInterval);
}
///
/// Generates a pin by hashing a key and counter.
///
static string GeneratePin(byte[] key, long counter)
{
const int SizeOfInt32 = 4;
var CounterBytes = BitConverter.GetBytes(counter);
if (BitConverter.IsLittleEndian)
{
//spec requires bytes in big-endian order
Array.Reverse(CounterBytes);
}
var Hash = new HMACSHA1(key).ComputeHash(CounterBytes);
var Offset = Hash[Hash.Length - 1] & 0xF;
var SelectedBytes = new byte[SizeOfInt32];
Buffer.BlockCopy(Hash, Offset, SelectedBytes, 0, SizeOfInt32);
if (BitConverter.IsLittleEndian)
{
//spec interprets bytes in big-endian order
Array.Reverse(SelectedBytes);
}
var SelectedInteger = BitConverter.ToInt32(SelectedBytes, 0);
//remove the most significant bit for interoperability per spec
var TruncatedHash = SelectedInteger & 0x7FFFFFFF;
//generate number of digits for given pin length
var Pin = TruncatedHash%PinModulo;
return Pin.ToString(CultureInfo.InvariantCulture).PadLeft(PinLength, '0');
}
#region Nested type: Encoder
static class Encoder
{
///
/// Url Encoding (with upper-case hexadecimal per OATH specification)
///
public static string UrlEncode(string value)
{
const string UrlEncodeAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
var Builder = new StringBuilder();
for (var i = 0; i < value.Length; i++)
{
var Symbol = value[i];
if (UrlEncodeAlphabet.IndexOf(Symbol) != -1)
{
Builder.Append(Symbol);
}
else
{
Builder.Append('%');
Builder.Append(((int)Symbol).ToString("X2"));
}
}
return Builder.ToString();
}
///
/// Base-32 Encoding
///
public static string Base32Encode(byte[] data)
{
const int InByteSize = 8;
const int OutByteSize = 5;
const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
int i = 0, index = 0;
var Builder = new StringBuilder((data.Length + 7)*InByteSize/OutByteSize);
while (i < data.Length)
{
int CurrentByte = data[i];
int Digit;
//Is the current digit going to span a byte boundary?
if (index > (InByteSize - OutByteSize))
{
int NextByte;
if ((i + 1) < data.Length)
{
NextByte = data[i + 1];
}
else
{
NextByte = 0;
}
Digit = CurrentByte & (0xFF >> index);
index = (index + OutByteSize)%InByteSize;
Digit <<= index;
Digit |= NextByte >> (InByteSize - index);
i++;
}
else
{
Digit = (CurrentByte >> (InByteSize - (index + OutByteSize))) & 0x1F;
index = (index + OutByteSize)%InByteSize;
if (index == 0)
{
i++;
}
}
Builder.Append(Base32Alphabet[Digit]);
}
return Builder.ToString();
}
}
#endregion
}