using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; namespace Google.Authenticator { public class TwoFactorAuthenticator { public static DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); public TimeSpan DefaultClockDriftTolerance { get; set; } public bool UseManagedSha1Algorithm { get; set; } public bool TryUnmanagedAlgorithmOnFailure { get; set; } public TwoFactorAuthenticator() : this(true, true) { } public TwoFactorAuthenticator(bool useManagedSha1, bool useUnmanagedOnFail) { DefaultClockDriftTolerance = TimeSpan.FromMinutes(5); UseManagedSha1Algorithm = useManagedSha1; TryUnmanagedAlgorithmOnFailure = useUnmanagedOnFail; } /// /// Generate a setup code for a Google Authenticator user to scan. /// /// Account Title (no spaces) /// Account Secret Key /// QR Code Width /// QR Code Height /// SetupCode object public SetupCode GenerateSetupCode(string accountTitleNoSpaces, string accountSecretKey, int qrCodeWidth, int qrCodeHeight) { return GenerateSetupCode(null, accountTitleNoSpaces, accountSecretKey, qrCodeWidth, qrCodeHeight); } /// /// Generate a setup code for a Google Authenticator user to scan (with issuer ID). /// /// Issuer ID (the name of the system, i.e. 'MyApp') /// Account Title (no spaces) /// Account Secret Key /// QR Code Width /// QR Code Height /// SetupCode object public SetupCode GenerateSetupCode(string issuer, string accountTitleNoSpaces, string accountSecretKey, int qrCodeWidth, int qrCodeHeight) { return GenerateSetupCode(issuer, accountTitleNoSpaces, accountSecretKey, qrCodeWidth, qrCodeHeight, false); } /// /// Generate a setup code for a Google Authenticator user to scan (with issuer ID). /// /// Issuer ID (the name of the system, i.e. 'MyApp') /// Account Title (no spaces) /// Account Secret Key /// QR Code Width /// QR Code Height /// Use HTTPS instead of HTTP /// SetupCode object public SetupCode GenerateSetupCode(string issuer, string accountTitleNoSpaces, string accountSecretKey, int qrCodeWidth, int qrCodeHeight, bool useHttps) { if (accountTitleNoSpaces == null) { throw new NullReferenceException("Account Title is null"); } accountTitleNoSpaces = accountTitleNoSpaces.Replace(" ", ""); SetupCode sC = new SetupCode(); sC.Account = accountTitleNoSpaces; sC.AccountSecretKey = accountSecretKey; string encodedSecretKey = EncodeAccountSecretKey(accountSecretKey); sC.ManualEntryKey = encodedSecretKey; string provisionUrl = null; if (string.IsNullOrEmpty(issuer)) { provisionUrl = UrlEncode(String.Format("otpauth://totp/{0}?secret={1}", accountTitleNoSpaces, encodedSecretKey)); } else { provisionUrl = UrlEncode(String.Format("otpauth://totp/{0}?secret={1}&issuer={2}", accountTitleNoSpaces, encodedSecretKey, UrlEncode(issuer))); } string protocol = useHttps ? "https" : "http"; string url = String.Format("{0}://chart.googleapis.com/chart?cht=qr&chs={1}x{2}&chl={3}", protocol, qrCodeWidth, qrCodeHeight, provisionUrl); sC.QrCodeSetupImageUrl = url; return sC; } private string UrlEncode(string value) { StringBuilder result = new StringBuilder(); string validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"; foreach (char symbol in value) { if (validChars.IndexOf(symbol) != -1) { result.Append(symbol); } else { result.Append('%' + String.Format("{0:X2}", (int)symbol)); } } return result.ToString().Replace(" ", "%20"); } private string EncodeAccountSecretKey(string accountSecretKey) { //if (accountSecretKey.Length < 10) //{ // accountSecretKey = accountSecretKey.PadRight(10, '0'); //} //if (accountSecretKey.Length > 12) //{ // accountSecretKey = accountSecretKey.Substring(0, 12); //} return Base32Encode(Encoding.UTF8.GetBytes(accountSecretKey)); } private string Base32Encode(byte[] data) { int inByteSize = 8; int outByteSize = 5; char[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray(); int i = 0, index = 0, digit = 0; int current_byte, next_byte; StringBuilder result = new StringBuilder((data.Length + 7) * inByteSize / outByteSize); while (i < data.Length) { current_byte = (data[i] >= 0) ? data[i] : (data[i] + 256); // Unsign /* Is the current digit going to span a byte boundary? */ if (index > (inByteSize - outByteSize)) { if ((i + 1) < data.Length) next_byte = (data[i + 1] >= 0) ? data[i + 1] : (data[i + 1] + 256); else next_byte = 0; digit = current_byte & (0xFF >> index); index = (index + outByteSize) % inByteSize; digit <<= index; digit |= next_byte >> (inByteSize - index); i++; } else { digit = (current_byte >> (inByteSize - (index + outByteSize))) & 0x1F; index = (index + outByteSize) % inByteSize; if (index == 0) i++; } result.Append(alphabet[digit]); } return result.ToString(); } public string GeneratePINAtInterval(string accountSecretKey, long counter, int digits = 6) { return GenerateHashedCode(accountSecretKey, counter, digits); } internal string GenerateHashedCode(string secret, long iterationNumber, int digits = 6) { byte[] key = Encoding.UTF8.GetBytes(secret); return GenerateHashedCode(key, iterationNumber, digits); } internal string GenerateHashedCode(byte[] key, long iterationNumber, int digits = 6) { byte[] counter = BitConverter.GetBytes(iterationNumber); if (BitConverter.IsLittleEndian) { Array.Reverse(counter); } HMACSHA1 hmac = getHMACSha1Algorithm(key); byte[] hash = hmac.ComputeHash(counter); int offset = hash[hash.Length - 1] & 0xf; // Convert the 4 bytes into an integer, ignoring the sign. int binary = ((hash[offset] & 0x7f) << 24) | (hash[offset + 1] << 16) | (hash[offset + 2] << 8) | (hash[offset + 3]); int password = binary % (int)Math.Pow(10, digits); return password.ToString(new string('0', digits)); } private long GetCurrentCounter() { return GetCurrentCounter(DateTime.UtcNow, _epoch, 30); } private long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep) { return (long)(now - epoch).TotalSeconds / timeStep; } /// /// Creates a HMACSHA1 algorithm to use to hash the counter bytes. By default, this will attempt to use /// the managed SHA1 class (SHA1Manager) and on exception (FIPS-compliant machine policy, etc) will attempt /// to use the unmanaged SHA1 class (SHA1CryptoServiceProvider). /// /// User's secret key, in bytes /// HMACSHA1 cryptographic algorithm private HMACSHA1 getHMACSha1Algorithm(byte[] key) { HMACSHA1 hmac; try { hmac = new HMACSHA1(key);//, UseManagedSha1Algorithm); } catch (InvalidOperationException ioe) { if (UseManagedSha1Algorithm && TryUnmanagedAlgorithmOnFailure) { try { hmac = new HMACSHA1(key); } catch (InvalidOperationException ioe2) { throw ioe2; } } else { throw ioe; } } return hmac; } public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient) { return ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance); } public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient, TimeSpan timeTolerance) { var codes = GetCurrentPINs(accountSecretKey, timeTolerance); return codes.Any(c => c == twoFactorCodeFromClient); } public string GetCurrentPIN(string accountSecretKey) { return GeneratePINAtInterval(accountSecretKey, GetCurrentCounter()); } public string GetCurrentPIN(string accountSecretKey, DateTime now) { return GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(now, _epoch, 30)); } public string[] GetCurrentPINs(string accountSecretKey) { return GetCurrentPINs(accountSecretKey, DefaultClockDriftTolerance); } public string[] GetCurrentPINs(string accountSecretKey, TimeSpan timeTolerance) { List codes = new List(); long iterationCounter = GetCurrentCounter(); int iterationOffset = 0; if (timeTolerance.TotalSeconds > 30) { iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / 30.00); } long iterationStart = iterationCounter - iterationOffset; long iterationEnd = iterationCounter + iterationOffset; for (long counter = iterationStart; counter <= iterationEnd; counter++) { codes.Add(GeneratePINAtInterval(accountSecretKey, counter)); } return codes.ToArray(); } } }