From b5ea257ce7b646394cb418c4730cc1e216a62218 Mon Sep 17 00:00:00 2001 From: Caterina Novak Date: Thu, 8 Jul 2021 00:20:30 +0300 Subject: [PATCH 1/4] Issue #65 ValidateTwoFactorPin always returns false, if the secretKey parameter is base32 encoded string - fixed Fix for issue # 65; Support for .net5; Small refactoring. --- .gitignore | 8 + Google.Authenticator.Tests/AuthCodeTest.cs | 20 +- .../Google.Authenticator.Tests.csproj | 19 +- Google.Authenticator.Tests/QRCodeTest.cs | 9 +- Google.Authenticator/Base32Encoding.cs | 46 ++-- .../Google.Authenticator.csproj | 10 +- Google.Authenticator/SetupCode.cs | 7 +- .../TwoFactorAuthenticator.cs | 260 +++++++++--------- 8 files changed, 195 insertions(+), 184 deletions(-) diff --git a/.gitignore b/.gitignore index dd53b36..aba7f56 100644 --- a/.gitignore +++ b/.gitignore @@ -194,3 +194,11 @@ FakesAssemblies/ # Visual Studio 6 workspace options file *.opt .vscode/ +.idea/.idea.Google.Authenticator/.idea/.name +.idea/.idea.Google.Authenticator/.idea/codeStyles/codeStyleConfig.xml +.idea/.idea.Google.Authenticator/.idea/encodings.xml +.idea/.idea.Google.Authenticator/.idea/indexLayout.xml +.idea/.idea.Google.Authenticator/.idea/projectSettingsUpdater.xml +.idea/.idea.Google.Authenticator/.idea/vcs.xml +.idea/.idea.Google.Authenticator/.idea/workspace.xml +.idea/config/applicationhost.config diff --git a/Google.Authenticator.Tests/AuthCodeTest.cs b/Google.Authenticator.Tests/AuthCodeTest.cs index f2a3394..db83b9a 100644 --- a/Google.Authenticator.Tests/AuthCodeTest.cs +++ b/Google.Authenticator.Tests/AuthCodeTest.cs @@ -1,6 +1,5 @@ -using System; +using Shouldly; using Xunit; -using Shouldly; namespace Google.Authenticator.Tests { @@ -9,17 +8,18 @@ public class AuthCodeTest [Fact] public void BasicAuthCodeTest() { - string secretKey = "PJWUMZKAUUFQKJBAMD6VGJ6RULFVW4ZH"; - string expected = "551508"; + var secretKey = "PJWUMZKAUUFQKJBAMD6VGJ6RULFVW4ZH"; + var expected = "551508"; - TwoFactorAuthenticator tfa = new TwoFactorAuthenticator(); - - long currentTime = 1416643820; + var tfa = new TwoFactorAuthenticator(); - // I actually think you are supposed to divide the time by 30 seconds? Maybe need an overload that takes a DateTime? + var currentTime = 1416643820; + + // I actually think you are supposed to divide the time by 30 seconds? + // Maybe need an overload that takes a DateTime? var actual = tfa.GeneratePINAtInterval(secretKey, currentTime, 6); - actual.ShouldBe(expected); + actual.ShouldBe(expected); } } -} +} \ No newline at end of file diff --git a/Google.Authenticator.Tests/Google.Authenticator.Tests.csproj b/Google.Authenticator.Tests/Google.Authenticator.Tests.csproj index ee9e1b1..8ee17c7 100644 --- a/Google.Authenticator.Tests/Google.Authenticator.Tests.csproj +++ b/Google.Authenticator.Tests/Google.Authenticator.Tests.csproj @@ -1,21 +1,26 @@  - netcoreapp3.0;net452 + netcoreapp3.0;net452;net5 false - + - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - + \ No newline at end of file diff --git a/Google.Authenticator.Tests/QRCodeTest.cs b/Google.Authenticator.Tests/QRCodeTest.cs index c3bb3ae..7af20d1 100644 --- a/Google.Authenticator.Tests/QRCodeTest.cs +++ b/Google.Authenticator.Tests/QRCodeTest.cs @@ -1,4 +1,3 @@ -using System; using Xunit; using Shouldly; @@ -10,7 +9,13 @@ public class QRCodeTest public void CanGenerateQRCode() { var subject = new TwoFactorAuthenticator(); - var setupCodeInfo = subject.GenerateSetupCode("issuer","a@b.com","secret", false, 2); + var setupCodeInfo = subject.GenerateSetupCode( + "issuer", + "a@b.com", + "secret", + false, + 2); + setupCodeInfo.QrCodeSetupImageUrl.ShouldNotBeNull(); } } diff --git a/Google.Authenticator/Base32Encoding.cs b/Google.Authenticator/Base32Encoding.cs index b9a6b23..ba850f3 100644 --- a/Google.Authenticator/Base32Encoding.cs +++ b/Google.Authenticator/Base32Encoding.cs @@ -15,20 +15,18 @@ public class Base32Encoding public static byte[] ToBytes(string input) { if (string.IsNullOrEmpty(input)) - { - throw new ArgumentNullException("input"); - } + throw new ArgumentNullException(nameof(input)); input = input.TrimEnd('='); //remove padding characters - int byteCount = input.Length * 5 / 8; //this must be TRUNCATED - byte[] returnArray = new byte[byteCount]; + var byteCount = input.Length * 5 / 8; //this must be TRUNCATED + var returnArray = new byte[byteCount]; byte curByte = 0, bitsRemaining = 8; - int mask = 0, arrayIndex = 0; + int mask, arrayIndex = 0; - foreach (char c in input) + foreach (var c in input) { - int cValue = CharToValue(c); + var cValue = CharToValue(c); if (bitsRemaining > 5) { @@ -48,9 +46,7 @@ public static byte[] ToBytes(string input) //if we didn't end with a full byte if (arrayIndex != byteCount) - { returnArray[arrayIndex] = curByte; - } return returnArray; } @@ -63,17 +59,15 @@ public static byte[] ToBytes(string input) public static string ToString(byte[] input) { if (input == null || input.Length == 0) - { - throw new ArgumentNullException("input"); - } + throw new ArgumentNullException(nameof(input)); - int charCount = (int)Math.Ceiling(input.Length / 5d) * 8; - char[] returnArray = new char[charCount]; + var charCount = (int)Math.Ceiling(input.Length / 5d) * 8; + var returnArray = new char[charCount]; byte nextChar = 0, bitsRemaining = 5; - int arrayIndex = 0; + var arrayIndex = 0; - foreach (byte b in input) + foreach (var b in input) { nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining))); returnArray[arrayIndex++] = ValueToChar(nextChar); @@ -101,40 +95,32 @@ public static string ToString(byte[] input) private static int CharToValue(char c) { - int value = (int)c; + var value = (int)c; //65-90 == uppercase letters if (value < 91 && value > 64) - { return value - 65; - } + //50-55 == numbers 2-7 if (value < 56 && value > 49) - { return value - 24; - } + //97-122 == lowercase letters if (value < 123 && value > 96) - { return value - 97; - } - throw new ArgumentException("Character is not a Base32 character.", "c"); + throw new ArgumentException("Character is not a Base32 character.", nameof(c)); } private static char ValueToChar(byte b) { if (b < 26) - { return (char)(b + 65); - } if (b < 32) - { return (char)(b + 24); - } - throw new ArgumentException("Byte is not a value Base32 value.", "b"); + throw new ArgumentException("Byte is not a value Base32 value.", nameof(b)); } } diff --git a/Google.Authenticator/Google.Authenticator.csproj b/Google.Authenticator/Google.Authenticator.csproj index a8d3ce2..74199dd 100644 --- a/Google.Authenticator/Google.Authenticator.csproj +++ b/Google.Authenticator/Google.Authenticator.csproj @@ -1,20 +1,20 @@  - netstandard2.0;net45 + netstandard2.0;net45;net5 Google Authenticator Two-Factor Google Authenticator Two-Factor Authentication Library Google Authenticator Two-Factor Authentication Library (Not officially affiliated with Google.) Brandon Potter Brandon Potter - 2.1.1 + 2.1.2 Apache-2.0 https://github.com/BrandonPotter/GoogleAuthenticator GoogleAuthenticator - + @@ -30,6 +30,10 @@ NET45;NETFULL + + NET5_0;NETCOREAPP + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb diff --git a/Google.Authenticator/SetupCode.cs b/Google.Authenticator/SetupCode.cs index 657ccdf..7964d14 100644 --- a/Google.Authenticator/SetupCode.cs +++ b/Google.Authenticator/SetupCode.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Google.Authenticator +namespace Google.Authenticator { public class SetupCode { diff --git a/Google.Authenticator/TwoFactorAuthenticator.cs b/Google.Authenticator/TwoFactorAuthenticator.cs index a76cc7d..2436f68 100644 --- a/Google.Authenticator/TwoFactorAuthenticator.cs +++ b/Google.Authenticator/TwoFactorAuthenticator.cs @@ -1,7 +1,6 @@ using QRCoder; using System; using System.Collections.Generic; -using System.Drawing; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -17,209 +16,218 @@ namespace Google.Authenticator /// public class TwoFactorAuthenticator { - private readonly static DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private static readonly DateTime _epoch = + new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private TimeSpan DefaultClockDriftTolerance { get; set; } - public TwoFactorAuthenticator() - { - DefaultClockDriftTolerance = TimeSpan.FromMinutes(5); - } + public TwoFactorAuthenticator() => DefaultClockDriftTolerance = TimeSpan.FromMinutes(5); /// /// Generate a setup code for a Google Authenticator user to scan /// - /// Issuer ID (the name of the system, i.e. 'MyApp'), can be omitted but not recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format + /// Issuer ID (the name of the system, i.e. 'MyApp'), + /// can be omitted but not recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format + /// /// Account Title (no spaces) /// Account Secret Key /// Flag saying if accountSecretKey is in Base32 format or original secret - /// Number of pixels per QR Module (2 pixels give ~ 100x100px QRCode, should be 10 or less) + /// Number of pixels per QR Module (2 pixels give ~ 100x100px QRCode, + /// should be 10 or less) /// SetupCode object - public SetupCode GenerateSetupCode(string issuer, string accountTitleNoSpaces, string accountSecretKey, bool secretIsBase32, int QRPixelsPerModule = 3) + public SetupCode GenerateSetupCode( + string issuer, + string accountTitleNoSpaces, + string accountSecretKey, + bool secretIsBase32, + int qrPixelsPerModule = 3) { - byte[] key = secretIsBase32 ? Base32Encoding.ToBytes(accountSecretKey) : Encoding.UTF8.GetBytes(accountSecretKey); - return GenerateSetupCode(issuer, accountTitleNoSpaces, key, QRPixelsPerModule); + var key = secretIsBase32 + ? Base32Encoding.ToBytes(accountSecretKey) + : Encoding.UTF8.GetBytes(accountSecretKey); + + return GenerateSetupCode(issuer, accountTitleNoSpaces, key, qrPixelsPerModule); } /// /// Generate a setup code for a Google Authenticator user to scan /// - /// Issuer ID (the name of the system, i.e. 'MyApp'), can be omitted but not recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format + /// Issuer ID (the name of the system, i.e. 'MyApp'), can be omitted but not + /// recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format /// Account Title (no spaces) /// Account Secret Key as byte[] - /// Number of pixels per QR Module (2 = ~120x120px QRCode, should be 10 or less) + /// Number of pixels per QR Module + /// (2 = ~120x120px QRCode, should be 10 or less) + /// /// SetupCode object - public SetupCode GenerateSetupCode(string issuer, string accountTitleNoSpaces, byte[] accountSecretKey, int QRPixelsPerModule = 3, bool generateQrCode = true) - { - if (String.IsNullOrWhiteSpace(accountTitleNoSpaces)) { throw new NullReferenceException("Account Title is null"); } + public SetupCode GenerateSetupCode( + string issuer, + string accountTitleNoSpaces, + byte[] accountSecretKey, + int qrPixelsPerModule = 3, + bool generateQrCode = true) + { + if (string.IsNullOrWhiteSpace(accountTitleNoSpaces)) + throw new NullReferenceException("Account Title is null"); + accountTitleNoSpaces = RemoveWhitespace(Uri.EscapeUriString(accountTitleNoSpaces)); - string encodedSecretKey = Base32Encoding.ToString(accountSecretKey); - string provisionUrl; - if (String.IsNullOrWhiteSpace(issuer)) - { - provisionUrl = String.Format("otpauth://totp/{0}?secret={1}", accountTitleNoSpaces, encodedSecretKey.Trim('=')); - } - else - { - // https://github.com/google/google-authenticator/wiki/Conflicting-Accounts - // Added additional prefix to account otpauth://totp/Company:joe_example@gmail.com for backwards compatibility - provisionUrl = String.Format("otpauth://totp/{2}:{0}?secret={1}&issuer={2}", accountTitleNoSpaces, encodedSecretKey.Trim('='), UrlEncode(issuer)); - } + var encodedSecretKey = Base32Encoding.ToString(accountSecretKey); + var provisionUrl = string.IsNullOrWhiteSpace(issuer) + ? $"otpauth://totp/{accountTitleNoSpaces}?secret={encodedSecretKey.Trim('=')}" + : $"otpauth://totp/{UrlEncode(issuer)}:{accountTitleNoSpaces}?secret={encodedSecretKey.Trim('=')}&issuer={UrlEncode(issuer)}"; + + if (!generateQrCode) + return new SetupCode(accountTitleNoSpaces, encodedSecretKey.Trim('='), ""); - string qrCodeUrl = string.Empty; - if (generateQrCode) + return new SetupCode( + accountTitleNoSpaces, + encodedSecretKey.Trim('='), + GenerateQrCodeUrl(qrPixelsPerModule, provisionUrl)); + } + + private static string GenerateQrCodeUrl(int qrPixelsPerModule, string provisionUrl) + { + var qrCodeUrl = ""; + try { - try + using (var qrGenerator = new QRCodeGenerator()) + using (var qrCodeData = qrGenerator.CreateQrCode(provisionUrl, QRCodeGenerator.ECCLevel.Q)) + using (var qrCode = new QRCode(qrCodeData)) + using (var qrCodeImage = qrCode.GetGraphic(qrPixelsPerModule)) + using (var ms = new MemoryStream()) { - using (QRCodeGenerator qrGenerator = new QRCodeGenerator()) - using (QRCodeData qrCodeData = qrGenerator.CreateQrCode(provisionUrl, QRCodeGenerator.ECCLevel.Q)) - using (QRCode qrCode = new QRCode(qrCodeData)) - using (Bitmap qrCodeImage = qrCode.GetGraphic(QRPixelsPerModule)) - using (MemoryStream ms = new MemoryStream()) - { - qrCodeImage.Save(ms, System.Drawing.Imaging.ImageFormat.Png); - - qrCodeUrl = String.Format("data:image/png;base64,{0}", Convert.ToBase64String(ms.ToArray())); - } + qrCodeImage.Save(ms, System.Drawing.Imaging.ImageFormat.Png); + + qrCodeUrl = $"data:image/png;base64,{Convert.ToBase64String(ms.ToArray())}"; } - catch (System.TypeInitializationException e) + } + catch (TypeInitializationException e) + { + if (e.InnerException != null + && e.InnerException.GetType() == typeof(DllNotFoundException) + && e.InnerException.Message.Contains("libgdiplus")) { - if (e.InnerException != null - && e.InnerException.GetType() == typeof(System.DllNotFoundException) - && e.InnerException.Message.Contains("libgdiplus")) - { - throw new MissingDependencyException("It looks like libgdiplus has not been installed - see https://github.com/codebude/QRCoder/issues/227", e); - } + throw new MissingDependencyException( + "It looks like libgdiplus has not been installed - see" + + " https://github.com/codebude/QRCoder/issues/227", + e); } - catch (System.Runtime.InteropServices.ExternalException e) + } + catch (System.Runtime.InteropServices.ExternalException e) + { + if (e.Message.Contains("GDI+") && qrPixelsPerModule > 10) { - if (e.Message.Contains("GDI+") && QRPixelsPerModule > 10) - { - throw new QRException($"There was a problem generating a QR code. The value of {nameof(QRPixelsPerModule)} should be set to a value of 10 or less for optimal results.", e); - } + throw new QRException( + $"There was a problem generating a QR code. The value of {nameof(qrPixelsPerModule)}" + + " should be set to a value of 10 or less for optimal results.", + e); } } - return new SetupCode(accountTitleNoSpaces, encodedSecretKey.Trim('='), qrCodeUrl); + return qrCodeUrl; } - private static string RemoveWhitespace(string str) - { - return new string(str.Where(c => !Char.IsWhiteSpace(c)).ToArray()); - } + private static string RemoveWhitespace(string str) => + new string(str.Where(c => !char.IsWhiteSpace(c)).ToArray()); private string UrlEncode(string value) { - StringBuilder result = new StringBuilder(); - string validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"; + var result = new StringBuilder(); + var validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"; - foreach (char symbol in value) + foreach (var symbol in value) { - if (validChars.IndexOf(symbol) != -1) - { - result.Append(symbol); - } + if (validChars.IndexOf(symbol) == -1) + result.AppendFormat("%{0:X2}", (int) symbol); else - { - result.Append('%' + String.Format("{0:X2}", (int)symbol)); - } + result.Append(symbol); } - return result.ToString().Replace(" ", "%20"); + return result.Replace(" ", "%20").ToString(); } - public string GeneratePINAtInterval(string accountSecretKey, long counter, int digits = 6) - { - return GenerateHashedCode(accountSecretKey, counter, digits); - } + public string GeneratePINAtInterval( + string accountSecretKey, + long counter, + int digits = 6, + bool secretIsBase32 = false) + => GenerateHashedCode(accountSecretKey, counter, secretIsBase32, digits); - internal string GenerateHashedCode(string secret, long iterationNumber, int digits = 6) - { - byte[] key = Encoding.UTF8.GetBytes(secret); - return GenerateHashedCode(key, iterationNumber, digits); - } + private string GenerateHashedCode(string secret, + long iterationNumber, + bool secretIsBase32, + int digits = 6) + => GenerateHashedCode( + secretIsBase32 ? Base32Encoding.ToBytes(secret):Encoding.UTF8.GetBytes(secret), + iterationNumber, + digits); - internal string GenerateHashedCode(byte[] key, long iterationNumber, int digits = 6) + private string GenerateHashedCode(byte[] key, long iterationNumber, int digits = 6) { - byte[] counter = BitConverter.GetBytes(iterationNumber); + var counter = BitConverter.GetBytes(iterationNumber); if (BitConverter.IsLittleEndian) - { Array.Reverse(counter); - } - HMACSHA1 hmac = new HMACSHA1(key); + var hmac = new HMACSHA1(key); - byte[] hash = hmac.ComputeHash(counter); + var hash = hmac.ComputeHash(counter); - int offset = hash[hash.Length - 1] & 0xf; + var offset = hash[hash.Length - 1] & 0xf; // Convert the 4 bytes into an integer, ignoring the sign. - int binary = + var binary = ((hash[offset] & 0x7f) << 24) | (hash[offset + 1] << 16) | (hash[offset + 2] << 8) - | (hash[offset + 3]); + | hash[offset + 3]; - int password = binary % (int)Math.Pow(10, digits); + var 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() => GetCurrentCounter(DateTime.UtcNow, _epoch, 30); - private long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep) - { - return (long)(now - epoch).TotalSeconds / timeStep; - } + private long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep) => + (long) (now - epoch).TotalSeconds / timeStep; - public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient) - { - return ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance); - } + public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient) => + 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 bool ValidateTwoFactorPIN( + string accountSecretKey, + string twoFactorCodeFromClient, + TimeSpan timeTolerance) + => GetCurrentPINs(accountSecretKey, timeTolerance).Any(c => c == twoFactorCodeFromClient); - public string GetCurrentPIN(string accountSecretKey) - { - return GeneratePINAtInterval(accountSecretKey, GetCurrentCounter()); - } + public string GetCurrentPIN(string accountSecretKey, bool secretIsBase32 = false) => + GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(), secretIsBase32: secretIsBase32); - public string GetCurrentPIN(string accountSecretKey, DateTime now) - { - return GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(now, _epoch, 30)); - } + public string GetCurrentPIN(string accountSecretKey, DateTime now, bool secretIsBase32 = false) => + GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(now, _epoch, 30)); - public string[] GetCurrentPINs(string accountSecretKey) - { - return GetCurrentPINs(accountSecretKey, DefaultClockDriftTolerance); - } + public string[] GetCurrentPINs(string accountSecretKey, bool secretIsBase32 = false) => + GetCurrentPINs(accountSecretKey, DefaultClockDriftTolerance, secretIsBase32); - public string[] GetCurrentPINs(string accountSecretKey, TimeSpan timeTolerance) + public string[] GetCurrentPINs(string accountSecretKey, TimeSpan timeTolerance, bool secretIsBase32 = false) { - List codes = new List(); - long iterationCounter = GetCurrentCounter(); - int iterationOffset = 0; + var codes = new List(); + var iterationCounter = GetCurrentCounter(); + var iterationOffset = 0; if (timeTolerance.TotalSeconds > 30) { iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / 30.00); } - long iterationStart = iterationCounter - iterationOffset; - long iterationEnd = iterationCounter + iterationOffset; + var iterationStart = iterationCounter - iterationOffset; + var iterationEnd = iterationCounter + iterationOffset; - for (long counter = iterationStart; counter <= iterationEnd; counter++) + for (var counter = iterationStart; counter <= iterationEnd; counter++) { - codes.Add(GeneratePINAtInterval(accountSecretKey, counter)); + codes.Add(GeneratePINAtInterval(accountSecretKey, counter, secretIsBase32: secretIsBase32)); } return codes.ToArray(); } } -} +} \ No newline at end of file From ff44a7932f089254d445c7ea9e0464961aa4ab57 Mon Sep 17 00:00:00 2001 From: Caterina Novak Date: Thu, 8 Jul 2021 00:56:50 +0300 Subject: [PATCH 2/4] Issue #65 - ValidateTwoFactorPin always returns false, if the secretKey parameter is base32 encoded string Added parameter secretIsBase32 into ValidateTwoFactorPIN --- Google.Authenticator/TwoFactorAuthenticator.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Google.Authenticator/TwoFactorAuthenticator.cs b/Google.Authenticator/TwoFactorAuthenticator.cs index 2436f68..60b3c0c 100644 --- a/Google.Authenticator/TwoFactorAuthenticator.cs +++ b/Google.Authenticator/TwoFactorAuthenticator.cs @@ -190,14 +190,18 @@ private string GenerateHashedCode(byte[] key, long iterationNumber, int digits = private long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep) => (long) (now - epoch).TotalSeconds / timeStep; - public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient) => - ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance); + public bool ValidateTwoFactorPIN( + string accountSecretKey, + string twoFactorCodeFromClient, + bool secretIsBase32 = false) => + ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance, secretIsBase32); public bool ValidateTwoFactorPIN( string accountSecretKey, string twoFactorCodeFromClient, - TimeSpan timeTolerance) - => GetCurrentPINs(accountSecretKey, timeTolerance).Any(c => c == twoFactorCodeFromClient); + TimeSpan timeTolerance, + bool secretIsBase32 = false) => + GetCurrentPINs(accountSecretKey, timeTolerance, secretIsBase32).Any(c => c == twoFactorCodeFromClient); public string GetCurrentPIN(string accountSecretKey, bool secretIsBase32 = false) => GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(), secretIsBase32: secretIsBase32); From 3cdfd227837a0a1830323133af0316cb7a5cfad9 Mon Sep 17 00:00:00 2001 From: Caterina Novak Date: Thu, 8 Jul 2021 11:55:21 +0300 Subject: [PATCH 3/4] Issue #65 ValidateTwoFactorPin always returns false, if the secretKey parameter is base32 encoded string - fixes for review --- Google.Authenticator.Tests/AuthCodeTest.cs | 20 +++++- Google.Authenticator/Base32Encoding.cs | 39 ++++++++---- .../TwoFactorAuthenticator.cs | 63 ++++++++++++------- 3 files changed, 84 insertions(+), 38 deletions(-) diff --git a/Google.Authenticator.Tests/AuthCodeTest.cs b/Google.Authenticator.Tests/AuthCodeTest.cs index db83b9a..87f73f3 100644 --- a/Google.Authenticator.Tests/AuthCodeTest.cs +++ b/Google.Authenticator.Tests/AuthCodeTest.cs @@ -1,4 +1,5 @@ -using Shouldly; +using System.Text; +using Shouldly; using Xunit; namespace Google.Authenticator.Tests @@ -21,5 +22,22 @@ public void BasicAuthCodeTest() actual.ShouldBe(expected); } + + [Fact] + public void Base32AuthCodeTest() + { + var secretKey = Base32Encoding.ToString(Encoding.UTF8.GetBytes("PJWUMZKAUUFQKJBAMD6VGJ6RULFVW4ZH")); + var expected = "551508"; + + var tfa = new TwoFactorAuthenticator(); + + var currentTime = 1416643820; + + // I actually think you are supposed to divide the time by 30 seconds? + // Maybe need an overload that takes a DateTime? + var actual = tfa.GeneratePINAtInterval(secretKey, currentTime, 6, true); + + actual.ShouldBe(expected); + } } } \ No newline at end of file diff --git a/Google.Authenticator/Base32Encoding.cs b/Google.Authenticator/Base32Encoding.cs index ba850f3..0ea237f 100644 --- a/Google.Authenticator/Base32Encoding.cs +++ b/Google.Authenticator/Base32Encoding.cs @@ -15,7 +15,9 @@ public class Base32Encoding public static byte[] ToBytes(string input) { if (string.IsNullOrEmpty(input)) + { throw new ArgumentNullException(nameof(input)); + } input = input.TrimEnd('='); //remove padding characters var byteCount = input.Length * 5 / 8; //this must be TRUNCATED @@ -31,15 +33,15 @@ public static byte[] ToBytes(string input) if (bitsRemaining > 5) { mask = cValue << (bitsRemaining - 5); - curByte = (byte)(curByte | mask); + curByte = (byte) (curByte | mask); bitsRemaining -= 5; } else { mask = cValue >> (5 - bitsRemaining); - curByte = (byte)(curByte | mask); + curByte = (byte) (curByte | mask); returnArray[arrayIndex++] = curByte; - curByte = (byte)(cValue << (3 + bitsRemaining)); + curByte = (byte) (cValue << (3 + bitsRemaining)); bitsRemaining += 3; } } @@ -59,9 +61,11 @@ public static byte[] ToBytes(string input) public static string ToString(byte[] input) { if (input == null || input.Length == 0) + { throw new ArgumentNullException(nameof(input)); + } - var charCount = (int)Math.Ceiling(input.Length / 5d) * 8; + var charCount = (int) Math.Ceiling(input.Length / 5d) * 8; var returnArray = new char[charCount]; byte nextChar = 0, bitsRemaining = 5; @@ -69,18 +73,18 @@ public static string ToString(byte[] input) foreach (var b in input) { - nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining))); + nextChar = (byte) (nextChar | (b >> (8 - bitsRemaining))); returnArray[arrayIndex++] = ValueToChar(nextChar); if (bitsRemaining < 4) { - nextChar = (byte)((b >> (3 - bitsRemaining)) & 31); + nextChar = (byte) ((b >> (3 - bitsRemaining)) & 31); returnArray[arrayIndex++] = ValueToChar(nextChar); bitsRemaining += 5; } bitsRemaining -= 3; - nextChar = (byte)((b << bitsRemaining) & 31); + nextChar = (byte) ((b << bitsRemaining) & 31); } //if we didn't end with a full char @@ -95,19 +99,25 @@ public static string ToString(byte[] input) private static int CharToValue(char c) { - var value = (int)c; + var value = (int) c; //65-90 == uppercase letters if (value < 91 && value > 64) + { return value - 65; - + } + //50-55 == numbers 2-7 if (value < 56 && value > 49) + { return value - 24; - + } + //97-122 == lowercase letters if (value < 123 && value > 96) + { return value - 97; + } throw new ArgumentException("Character is not a Base32 character.", nameof(c)); } @@ -115,13 +125,16 @@ private static int CharToValue(char c) private static char ValueToChar(byte b) { if (b < 26) - return (char)(b + 65); + { + return (char) (b + 65); + } if (b < 32) - return (char)(b + 24); + { + return (char) (b + 24); + } throw new ArgumentException("Byte is not a value Base32 value.", nameof(b)); } - } } \ No newline at end of file diff --git a/Google.Authenticator/TwoFactorAuthenticator.cs b/Google.Authenticator/TwoFactorAuthenticator.cs index 60b3c0c..b0a5ac5 100644 --- a/Google.Authenticator/TwoFactorAuthenticator.cs +++ b/Google.Authenticator/TwoFactorAuthenticator.cs @@ -16,8 +16,9 @@ namespace Google.Authenticator /// public class TwoFactorAuthenticator { - private static readonly DateTime _epoch = + private static readonly DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private TimeSpan DefaultClockDriftTolerance { get; set; } public TwoFactorAuthenticator() => DefaultClockDriftTolerance = TimeSpan.FromMinutes(5); @@ -35,16 +36,16 @@ public class TwoFactorAuthenticator /// should be 10 or less) /// SetupCode object public SetupCode GenerateSetupCode( - string issuer, - string accountTitleNoSpaces, + string issuer, + string accountTitleNoSpaces, string accountSecretKey, - bool secretIsBase32, + bool secretIsBase32, int qrPixelsPerModule = 3) { var key = secretIsBase32 ? Base32Encoding.ToBytes(accountSecretKey) : Encoding.UTF8.GetBytes(accountSecretKey); - + return GenerateSetupCode(issuer, accountTitleNoSpaces, key, qrPixelsPerModule); } @@ -67,21 +68,24 @@ public SetupCode GenerateSetupCode( bool generateQrCode = true) { if (string.IsNullOrWhiteSpace(accountTitleNoSpaces)) + { throw new NullReferenceException("Account Title is null"); - + } + accountTitleNoSpaces = RemoveWhitespace(Uri.EscapeUriString(accountTitleNoSpaces)); var encodedSecretKey = Base32Encoding.ToString(accountSecretKey); + var provisionUrl = string.IsNullOrWhiteSpace(issuer) ? $"otpauth://totp/{accountTitleNoSpaces}?secret={encodedSecretKey.Trim('=')}" + // https://github.com/google/google-authenticator/wiki/Conflicting-Accounts + // Added additional prefix to account otpauth://totp/Company:joe_example@gmail.com + // for backwards compatibility : $"otpauth://totp/{UrlEncode(issuer)}:{accountTitleNoSpaces}?secret={encodedSecretKey.Trim('=')}&issuer={UrlEncode(issuer)}"; - if (!generateQrCode) - return new SetupCode(accountTitleNoSpaces, encodedSecretKey.Trim('='), ""); - return new SetupCode( accountTitleNoSpaces, encodedSecretKey.Trim('='), - GenerateQrCodeUrl(qrPixelsPerModule, provisionUrl)); + generateQrCode ? GenerateQrCodeUrl(qrPixelsPerModule, provisionUrl) : ""); } private static string GenerateQrCodeUrl(int qrPixelsPerModule, string provisionUrl) @@ -96,7 +100,6 @@ private static string GenerateQrCodeUrl(int qrPixelsPerModule, string provisionU using (var ms = new MemoryStream()) { qrCodeImage.Save(ms, System.Drawing.Imaging.ImageFormat.Png); - qrCodeUrl = $"data:image/png;base64,{Convert.ToBase64String(ms.ToArray())}"; } } @@ -126,7 +129,7 @@ private static string GenerateQrCodeUrl(int qrPixelsPerModule, string provisionU return qrCodeUrl; } - private static string RemoveWhitespace(string str) => + private static string RemoveWhitespace(string str) => new string(str.Where(c => !char.IsWhiteSpace(c)).ToArray()); private string UrlEncode(string value) @@ -137,29 +140,37 @@ private string UrlEncode(string value) foreach (var symbol in value) { if (validChars.IndexOf(symbol) == -1) + { result.AppendFormat("%{0:X2}", (int) symbol); + } else + { result.Append(symbol); + } } return result.Replace(" ", "%20").ToString(); } public string GeneratePINAtInterval( - string accountSecretKey, + string accountSecretKey, long counter, - int digits = 6, + int digits = 6, bool secretIsBase32 = false) - => GenerateHashedCode(accountSecretKey, counter, secretIsBase32, digits); + { + return GenerateHashedCode(accountSecretKey, counter, secretIsBase32, digits); + } private string GenerateHashedCode(string secret, long iterationNumber, bool secretIsBase32, int digits = 6) - => GenerateHashedCode( - secretIsBase32 ? Base32Encoding.ToBytes(secret):Encoding.UTF8.GetBytes(secret), + { + return GenerateHashedCode( + secretIsBase32 ? Base32Encoding.ToBytes(secret) : Encoding.UTF8.GetBytes(secret), iterationNumber, digits); + } private string GenerateHashedCode(byte[] key, long iterationNumber, int digits = 6) { @@ -169,9 +180,7 @@ private string GenerateHashedCode(byte[] key, long iterationNumber, int digits = Array.Reverse(counter); var hmac = new HMACSHA1(key); - var hash = hmac.ComputeHash(counter); - var offset = hash[hash.Length - 1] & 0xf; // Convert the 4 bytes into an integer, ignoring the sign. @@ -191,17 +200,23 @@ private long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep) => (long) (now - epoch).TotalSeconds / timeStep; public bool ValidateTwoFactorPIN( - string accountSecretKey, + string accountSecretKey, string twoFactorCodeFromClient, - bool secretIsBase32 = false) => - ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance, secretIsBase32); + bool secretIsBase32 = false) + { + return ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance, + secretIsBase32); + } public bool ValidateTwoFactorPIN( string accountSecretKey, string twoFactorCodeFromClient, TimeSpan timeTolerance, - bool secretIsBase32 = false) => - GetCurrentPINs(accountSecretKey, timeTolerance, secretIsBase32).Any(c => c == twoFactorCodeFromClient); + bool secretIsBase32 = false) + { + return GetCurrentPINs(accountSecretKey, timeTolerance, secretIsBase32) + .Any(c => c == twoFactorCodeFromClient); + } public string GetCurrentPIN(string accountSecretKey, bool secretIsBase32 = false) => GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(), secretIsBase32: secretIsBase32); From b759503e70742c5849569cd43ab6a2836f06a564 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 9 Jul 2021 09:54:27 -0600 Subject: [PATCH 4/4] Updating version --- Google.Authenticator/Google.Authenticator.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Google.Authenticator/Google.Authenticator.csproj b/Google.Authenticator/Google.Authenticator.csproj index 74199dd..2bbf2ea 100644 --- a/Google.Authenticator/Google.Authenticator.csproj +++ b/Google.Authenticator/Google.Authenticator.csproj @@ -7,7 +7,7 @@ Google Authenticator Two-Factor Authentication Library (Not officially affiliated with Google.) Brandon Potter Brandon Potter - 2.1.2 + 2.2.0 Apache-2.0 https://github.com/BrandonPotter/GoogleAuthenticator GoogleAuthenticator @@ -41,4 +41,4 @@ true 2.0.1.0 - \ No newline at end of file +