From 4c63be6389e9e84cbad0298a2a84ad485f6b2838 Mon Sep 17 00:00:00 2001 From: Eugene Fox Date: Fri, 9 Jul 2021 00:52:00 +0300 Subject: [PATCH] Base32 encoder fix (#19) - Fixed Base32 encoder #6, added test - Updated NuGet packages --- SimpleOTP.Test/Helpers/Base32UnitTest.cs | 29 +++++++++--- .../Models/OTPConfigurationUnitTest.cs | 2 +- SimpleOTP.Test/SimpleOTP.Test.csproj | 4 +- SimpleOTP/Helpers/Base32Encoder.cs | 45 +++++++++---------- SimpleOTP/Helpers/SecretGenerator.cs | 2 +- SimpleOTP/SimpleOTP.csproj | 6 +-- 6 files changed, 51 insertions(+), 37 deletions(-) diff --git a/SimpleOTP.Test/Helpers/Base32UnitTest.cs b/SimpleOTP.Test/Helpers/Base32UnitTest.cs index 98f38c7..aac23d2 100644 --- a/SimpleOTP.Test/Helpers/Base32UnitTest.cs +++ b/SimpleOTP.Test/Helpers/Base32UnitTest.cs @@ -6,6 +6,7 @@ // ------------------------------------------------------------ using System; +using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using SimpleOTP.Helpers; @@ -19,19 +20,33 @@ namespace SimpleOTP.Test.Helpers public class Base32UnitTest { /// - /// Test overall work of the encoder. + /// Test encoder with byte array. /// - [TestMethod("Overall Base32 encoder test")] + [TestMethod("Byte array Base32 encoder test")] public void EncoderTest() { - // byte[] bytes = new byte[new Random().Next(128, 161)]; // FIXME: See SimpleOTP.Helpers.Base32Encoder.Encode() - byte[] bytes = new byte[160]; + byte[] bytes = new byte[new Random().Next(16, 20)]; new Random().NextBytes(bytes); string str = Base32Encoder.Encode(bytes); - bytes = Base32Encoder.Decode(str); - string result = Base32Encoder.Encode(bytes); - Assert.AreEqual(str, result); + byte[] result = Base32Encoder.Decode(str); + Assert.AreEqual(bytes.Length, result.Length); + for (int i = 0; i < bytes.Length; i++) + Assert.AreEqual(bytes[i], result[i]); + } + + /// + /// Test encoder with string content. + /// + [TestMethod("String Base32 encoder test")] + public void EncoderStringTest() + { + string testStr = "Hello, World!"; + string encodedStr = Base32Encoder.Encode(Encoding.UTF8.GetBytes(testStr)); + + byte[] resultBytes = Base32Encoder.Decode(encodedStr); + string result = Encoding.UTF8.GetString(resultBytes); + Assert.AreEqual(testStr, result); } } } \ No newline at end of file diff --git a/SimpleOTP.Test/Models/OTPConfigurationUnitTest.cs b/SimpleOTP.Test/Models/OTPConfigurationUnitTest.cs index 4a2ca9b..99ec584 100644 --- a/SimpleOTP.Test/Models/OTPConfigurationUnitTest.cs +++ b/SimpleOTP.Test/Models/OTPConfigurationUnitTest.cs @@ -87,4 +87,4 @@ namespace SimpleOTP.Test.Models "yEEBMhxEQIMRFCTIQQEyHERAgxEUJMhBATIcRECDERQkyEEBMhxEQIMRFCTIQQEyHE/gcIt5Gg5RNZHAAAAABJRU5ErkJggg==", imageStr); } } -} +} \ No newline at end of file diff --git a/SimpleOTP.Test/SimpleOTP.Test.csproj b/SimpleOTP.Test/SimpleOTP.Test.csproj index 3928089..7b2780f 100644 --- a/SimpleOTP.Test/SimpleOTP.Test.csproj +++ b/SimpleOTP.Test/SimpleOTP.Test.csproj @@ -24,8 +24,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/SimpleOTP/Helpers/Base32Encoder.cs b/SimpleOTP/Helpers/Base32Encoder.cs index f68f109..9aacc65 100644 --- a/SimpleOTP/Helpers/Base32Encoder.cs +++ b/SimpleOTP/Helpers/Base32Encoder.cs @@ -5,7 +5,8 @@ // Licensed under MIT license (https://opensource.org/licenses/MIT) // ------------------------------------------------------------ -using System.Collections.Generic; +using System; +using System.Linq; namespace SimpleOTP.Helpers { @@ -14,6 +15,7 @@ namespace SimpleOTP.Helpers /// internal static class Base32Encoder { + // Standard RFC 4648 Base32 alphabet private const string AllowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; /// @@ -23,16 +25,20 @@ namespace SimpleOTP.Helpers /// Base32 string. internal static string Encode(byte[] data) { - // FIXME: Encoder works correctly only with 160-bit keys + string binary = string.Empty; + foreach (byte b in data) + binary += Convert.ToString(b, 2).PadLeft(8, '0'); // Getting binary sequence to split into 5 digits + + int numberOfBlocks = (binary.Length / 5) + Math.Clamp(binary.Length % 5, 0, 1); + string[] sequence = Enumerable.Range(0, numberOfBlocks) + .Select(i => binary.Substring(i * 5, Math.Min(5, binary.Length - (i * 5))).PadRight(5, '0')) + .ToArray(); // Splitting sequence on groups of 5 + string output = string.Empty; - for (int bitIndex = 0; bitIndex < data.Length * 8; bitIndex += 5) - { - int dualbyte = data[bitIndex / 8] << 8; - if ((bitIndex / 8) + 1 < data.Length) - dualbyte |= data[(bitIndex / 8) + 1]; - dualbyte = 0x1f & (dualbyte >> (16 - (bitIndex % 8) - 5)); - output += AllowedCharacters[dualbyte]; - } + foreach (string str in sequence) + output += AllowedCharacters[Convert.ToInt32(str, 2)]; + + output = output.PadRight(output.Length + (output.Length % 8), '='); return output; } @@ -44,21 +50,14 @@ namespace SimpleOTP.Helpers /// Initial byte array. internal static byte[] Decode(string base32str) { - List output = new (); - char[] bytes = base32str.ToCharArray(); - for (var bitIndex = 0; bitIndex < base32str.Length * 5; bitIndex += 8) - { - var dualbyte = AllowedCharacters.IndexOf(bytes[bitIndex / 5]) << 10; - if ((bitIndex / 5) + 1 < bytes.Length) - dualbyte |= AllowedCharacters.IndexOf(bytes[(bitIndex / 5) + 1]) << 5; - if ((bitIndex / 5) + 2 < bytes.Length) - dualbyte |= AllowedCharacters.IndexOf(bytes[(bitIndex / 5) + 2]); + base32str = base32str.Replace("=", string.Empty); // Removing padding - dualbyte = 0xff & (dualbyte >> (15 - (bitIndex % 5) - 8)); - output.Add((byte)dualbyte); - } + string[] quintets = base32str.Select(i => Convert.ToString(AllowedCharacters.IndexOf(i), 2).PadLeft(5, '0')).ToArray(); // Getting quintets + string binary = string.Join(null, quintets); - return output.ToArray(); + byte[] output = Enumerable.Range(0, binary.Length / 8).Select(i => Convert.ToByte(binary.Substring(i * 8, 8), 2)).ToArray(); + + return output; } } } \ No newline at end of file diff --git a/SimpleOTP/Helpers/SecretGenerator.cs b/SimpleOTP/Helpers/SecretGenerator.cs index 3ddc982..94a6501 100644 --- a/SimpleOTP/Helpers/SecretGenerator.cs +++ b/SimpleOTP/Helpers/SecretGenerator.cs @@ -20,7 +20,7 @@ namespace SimpleOTP.Helpers /// Length of the key in bits
/// It should belong to [128-160] bit span
/// Default is: 160 bits. - /// CURRENTLY THIS GENERATOR WORKS CORRECTLY ONLY WITH 160-BIT LENGTHS. Set at your own risk. + /// Number of bits will be rounded down to the nearest number which divides by 8. /// Base32 encoded alphanumeric string with length form 16 to 20 characters. public static string GenerateSecret(int length = 160) { diff --git a/SimpleOTP/SimpleOTP.csproj b/SimpleOTP/SimpleOTP.csproj index d34c40c..93f655d 100644 --- a/SimpleOTP/SimpleOTP.csproj +++ b/SimpleOTP/SimpleOTP.csproj @@ -12,7 +12,7 @@ SimpleOTP SimpleOTP - 1.2.1 + 1.2.2 .NET library for TOTP/HOTP implementation on server (ASP.NET) or client (Xamarin) side Eugene Fox FoxDev Studio @@ -22,8 +22,8 @@ https://github.com/XFox111/SimpleOTP en-US otp;totp;dotnet;hotp;authenticator;2fa;mfa;security;oath - - Fixed generated secret length -- Fixed URI encoding issues in 'OTPConfiguration.GetUri()' + - Fixed Base32 encoder +- Updated NuGet dependency packages