Last active
September 23, 2017 17:26
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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