Skip to content

Instantly share code, notes, and snippets.

@trcio
Last active September 23, 2017 17:26
Show Gist options
  • Save trcio/32254962855297657de6 to your computer and use it in GitHub Desktop.
Save trcio/32254962855297657de6 to your computer and use it in GitHub Desktop.
A small, lightweight class to create codes for use with the Google Authenticator app
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Web;
namespace GAuthWrapper
{
public static class GAuthProvider
{
public static int CodeLength { get; set; }
private const string GoogleQrCodeEndpoint = "https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=";
private static readonly Random Rand;
static GAuthProvider()
{
CodeLength = 6;
Rand = new Random();
}
public static string GetQrCodeUrl(string secret, string issuer, string description)
{
return string.Concat(GoogleQrCodeEndpoint,
HttpUtility.UrlEncode(string.Format("otpauth://totp/{0}?secret={1}&issuer={2}", description, secret,
issuer)));
}
public static string CreateSecret(int length = 16)
{
var secret = new char[length];
for (var i = 0; i < secret.Length; i++)
{
secret[i] = Base32.Base32Alphabet[Rand.Next(0, Base32.Base32Alphabet.Length)];
}
return new string(secret);
}
public static string GetCode(string secret)
{
return GetCode(secret, GetUnixTime()/30);
}
public static string GetCode(string secret, int time)
{
var secretKey = Base32.FromBase32String(secret);
var timeBytes = ConcatArrays(new byte[] {0, 0, 0, 0}, ToBigEndian(time));
var hmacHash = Sha1Hmac(timeBytes, secretKey);
var offset = (byte)(hmacHash[hmacHash.Length - 1] & 0x0F);
var hashPart = CopyArray(hmacHash, offset, 4);
var value = ToBigEndian(hashPart) & 0x7FFFFFFF;
var modulo = Math.Pow(10, CodeLength);
var code = (long)(value%modulo);
return code.ToString().PadLeft(CodeLength, '0');
}
public static bool VerifyCode(string secret, string code, int discrepancy = -1)
{
return VerifyCode(secret, code, discrepancy, GetUnixTime()/30);
}
public static bool VerifyCode(string secret, string code, int discrepancy, int time)
{
if (discrepancy == -1)
return code.Equals(GetCode(secret, time));
for (var i = -discrepancy; i <= discrepancy; i++)
if (code.Equals(GetCode(secret, time + i)))
return true;
return false;
}
#region Private Methods
private static byte[] ToBigEndian(int value)
{
var retval = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(retval);
}
return retval;
}
private static int ToBigEndian(byte[] value)
{
if (BitConverter.IsLittleEndian)
{
Array.Reverse(value);
}
return BitConverter.ToInt32(value, 0);
}
private static byte[] Sha1Hmac(byte[] data, byte[] key)
{
using (var sha = new HMACSHA1(key))
{
return sha.ComputeHash(data);
}
}
private static T[] ConcatArrays<T>(params T[][] arrays)
{
var result = new T[arrays.Sum(arr => arr.Length)];
var offset = 0;
foreach (var arr in arrays)
{
Buffer.BlockCopy(arr, 0, result, offset, arr.Length);
offset += arr.Length;
}
return result;
}
private static T[] CopyArray<T>(T[] array, int index, int length)
{
var holder = new T[length];
Buffer.BlockCopy(array, index, holder, 0, length);
return holder;
}
private static int GetUnixTime()
{
return (int)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
}
#endregion
}
// The following code is from (http://scottless.com/blog/archive/2014/02/15/base32-encoder-and-decoder-in-c.aspx)
// Licensed under the Creative Commons Attribution license (http://creativecommons.org/licenses/by/2.0/)
// Code comments and some line spaces have been removed
public static class Base32
{
public const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
private const int InByteSize = 8;
private const int OutByteSize = 5;
public static byte[] FromBase32String(string base32String)
{
if (base32String == null)
{
return null;
}
if (base32String == string.Empty)
{
return new byte[0];
}
var base32StringUpperCase = base32String.ToUpperInvariant();
var outputBytes = new byte[base32StringUpperCase.Length * OutByteSize / InByteSize];
if (outputBytes.Length == 0)
{
throw new ArgumentException("Specified string is not valid Base32 format because it doesn't have enough data to construct a complete byte array");
}
var base32Position = 0;
var base32SubPosition = 0;
var outputBytePosition = 0;
var outputByteSubPosition = 0;
while (outputBytePosition < outputBytes.Length)
{
var currentBase32Byte = Base32Alphabet.IndexOf(base32StringUpperCase[base32Position]);
if (currentBase32Byte < 0)
{
throw new ArgumentException(string.Format("Specified string is not valid Base32 format because character \"{0}\" does not exist in Base32 alphabet", base32String[base32Position]));
}
var bitsAvailableInByte = Math.Min(OutByteSize - base32SubPosition, InByteSize - outputByteSubPosition);
outputBytes[outputBytePosition] <<= bitsAvailableInByte;
outputBytes[outputBytePosition] |= (byte)(currentBase32Byte >> (OutByteSize - (base32SubPosition + bitsAvailableInByte)));
outputByteSubPosition += bitsAvailableInByte;
if (outputByteSubPosition >= InByteSize)
{
outputBytePosition++;
outputByteSubPosition = 0;
}
base32SubPosition += bitsAvailableInByte;
if (base32SubPosition < OutByteSize) continue;
base32Position++;
base32SubPosition = 0;
}
return outputBytes;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment