Skip to content

Commit 4c63be6

Browse files
authored
Base32 encoder fix (#19)
- Fixed Base32 encoder #6, added test - Updated NuGet packages
1 parent a06022b commit 4c63be6

6 files changed

Lines changed: 51 additions & 37 deletions

File tree

SimpleOTP.Test/Helpers/Base32UnitTest.cs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// ------------------------------------------------------------
77

88
using System;
9+
using System.Text;
910

1011
using Microsoft.VisualStudio.TestTools.UnitTesting;
1112
using SimpleOTP.Helpers;
@@ -19,19 +20,33 @@ namespace SimpleOTP.Test.Helpers
1920
public class Base32UnitTest
2021
{
2122
/// <summary>
22-
/// Test overall work of the encoder.
23+
/// Test encoder with byte array.
2324
/// </summary>
24-
[TestMethod("Overall Base32 encoder test")]
25+
[TestMethod("Byte array Base32 encoder test")]
2526
public void EncoderTest()
2627
{
27-
// byte[] bytes = new byte[new Random().Next(128, 161)]; // FIXME: See SimpleOTP.Helpers.Base32Encoder.Encode()
28-
byte[] bytes = new byte[160];
28+
byte[] bytes = new byte[new Random().Next(16, 20)];
2929
new Random().NextBytes(bytes);
3030
string str = Base32Encoder.Encode(bytes);
3131

32-
bytes = Base32Encoder.Decode(str);
33-
string result = Base32Encoder.Encode(bytes);
34-
Assert.AreEqual(str, result);
32+
byte[] result = Base32Encoder.Decode(str);
33+
Assert.AreEqual(bytes.Length, result.Length);
34+
for (int i = 0; i < bytes.Length; i++)
35+
Assert.AreEqual(bytes[i], result[i]);
36+
}
37+
38+
/// <summary>
39+
/// Test encoder with string content.
40+
/// </summary>
41+
[TestMethod("String Base32 encoder test")]
42+
public void EncoderStringTest()
43+
{
44+
string testStr = "Hello, World!";
45+
string encodedStr = Base32Encoder.Encode(Encoding.UTF8.GetBytes(testStr));
46+
47+
byte[] resultBytes = Base32Encoder.Decode(encodedStr);
48+
string result = Encoding.UTF8.GetString(resultBytes);
49+
Assert.AreEqual(testStr, result);
3550
}
3651
}
3752
}

SimpleOTP.Test/Models/OTPConfigurationUnitTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,4 @@ public async Task GetQrImage()
8787
"yEEBMhxEQIMRFCTIQQEyHERAgxEUJMhBATIcRECDERQkyEEBMhxEQIMRFCTIQQEyHE/gcIt5Gg5RNZHAAAAABJRU5ErkJggg==", imageStr);
8888
}
8989
}
90-
}
90+
}

SimpleOTP.Test/SimpleOTP.Test.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424

2525
<ItemGroup>
2626
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
27-
<PackageReference Include="MSTest.TestAdapter" Version="2.2.4" />
28-
<PackageReference Include="MSTest.TestFramework" Version="2.2.4" />
27+
<PackageReference Include="MSTest.TestAdapter" Version="2.2.5" />
28+
<PackageReference Include="MSTest.TestFramework" Version="2.2.5" />
2929
<PackageReference Include="coverlet.collector" Version="3.0.3">
3030
<PrivateAssets>all</PrivateAssets>
3131
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

SimpleOTP/Helpers/Base32Encoder.cs

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
// Licensed under MIT license (https://opensource.org/licenses/MIT)
66
// ------------------------------------------------------------
77

8-
using System.Collections.Generic;
8+
using System;
9+
using System.Linq;
910

1011
namespace SimpleOTP.Helpers
1112
{
@@ -14,6 +15,7 @@ namespace SimpleOTP.Helpers
1415
/// </summary>
1516
internal static class Base32Encoder
1617
{
18+
// Standard RFC 4648 Base32 alphabet
1719
private const string AllowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1820

1921
/// <summary>
@@ -23,16 +25,20 @@ internal static class Base32Encoder
2325
/// <returns>Base32 string.</returns>
2426
internal static string Encode(byte[] data)
2527
{
26-
// FIXME: Encoder works correctly only with 160-bit keys
28+
string binary = string.Empty;
29+
foreach (byte b in data)
30+
binary += Convert.ToString(b, 2).PadLeft(8, '0'); // Getting binary sequence to split into 5 digits
31+
32+
int numberOfBlocks = (binary.Length / 5) + Math.Clamp(binary.Length % 5, 0, 1);
33+
string[] sequence = Enumerable.Range(0, numberOfBlocks)
34+
.Select(i => binary.Substring(i * 5, Math.Min(5, binary.Length - (i * 5))).PadRight(5, '0'))
35+
.ToArray(); // Splitting sequence on groups of 5
36+
2737
string output = string.Empty;
28-
for (int bitIndex = 0; bitIndex < data.Length * 8; bitIndex += 5)
29-
{
30-
int dualbyte = data[bitIndex / 8] << 8;
31-
if ((bitIndex / 8) + 1 < data.Length)
32-
dualbyte |= data[(bitIndex / 8) + 1];
33-
dualbyte = 0x1f & (dualbyte >> (16 - (bitIndex % 8) - 5));
34-
output += AllowedCharacters[dualbyte];
35-
}
38+
foreach (string str in sequence)
39+
output += AllowedCharacters[Convert.ToInt32(str, 2)];
40+
41+
output = output.PadRight(output.Length + (output.Length % 8), '=');
3642

3743
return output;
3844
}
@@ -44,21 +50,14 @@ internal static string Encode(byte[] data)
4450
/// <returns>Initial byte array.</returns>
4551
internal static byte[] Decode(string base32str)
4652
{
47-
List<byte> output = new ();
48-
char[] bytes = base32str.ToCharArray();
49-
for (var bitIndex = 0; bitIndex < base32str.Length * 5; bitIndex += 8)
50-
{
51-
var dualbyte = AllowedCharacters.IndexOf(bytes[bitIndex / 5]) << 10;
52-
if ((bitIndex / 5) + 1 < bytes.Length)
53-
dualbyte |= AllowedCharacters.IndexOf(bytes[(bitIndex / 5) + 1]) << 5;
54-
if ((bitIndex / 5) + 2 < bytes.Length)
55-
dualbyte |= AllowedCharacters.IndexOf(bytes[(bitIndex / 5) + 2]);
53+
base32str = base32str.Replace("=", string.Empty); // Removing padding
5654

57-
dualbyte = 0xff & (dualbyte >> (15 - (bitIndex % 5) - 8));
58-
output.Add((byte)dualbyte);
59-
}
55+
string[] quintets = base32str.Select(i => Convert.ToString(AllowedCharacters.IndexOf(i), 2).PadLeft(5, '0')).ToArray(); // Getting quintets
56+
string binary = string.Join(null, quintets);
6057

61-
return output.ToArray();
58+
byte[] output = Enumerable.Range(0, binary.Length / 8).Select(i => Convert.ToByte(binary.Substring(i * 8, 8), 2)).ToArray();
59+
60+
return output;
6261
}
6362
}
6463
}

SimpleOTP/Helpers/SecretGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static class SecretGenerator
2020
/// <param name="length">Length of the key in bits<br/>
2121
/// It should belong to [128-160] bit span<br/>
2222
/// Default is: 160 bits.</param>
23-
/// <remarks>CURRENTLY THIS GENERATOR WORKS CORRECTLY ONLY WITH 160-BIT LENGTHS. Set <paramref name="length"/> at your own risk.</remarks>
23+
/// <remarks>Number of bits will be rounded down to the nearest number which divides by 8.</remarks>
2424
/// <returns>Base32 encoded alphanumeric string with length form 16 to 20 characters.</returns>
2525
public static string GenerateSecret(int length = 160)
2626
{

SimpleOTP/SimpleOTP.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<PropertyGroup>
1313
<PackageId>SimpleOTP</PackageId>
1414
<AssemblyName>SimpleOTP</AssemblyName>
15-
<Version>1.2.1</Version>
15+
<Version>1.2.2</Version>
1616
<Description>.NET library for TOTP/HOTP implementation on server (ASP.NET) or client (Xamarin) side</Description>
1717
<Authors>Eugene Fox</Authors>
1818
<Company>FoxDev Studio</Company>
@@ -22,8 +22,8 @@
2222
<RepositoryUrl>https://github.com/XFox111/SimpleOTP</RepositoryUrl>
2323
<NeutralLanguage>en-US</NeutralLanguage>
2424
<PackageTags>otp;totp;dotnet;hotp;authenticator;2fa;mfa;security;oath</PackageTags>
25-
<PackageReleaseNotes>- Fixed generated secret length
26-
- Fixed URI encoding issues in 'OTPConfiguration.GetUri()'</PackageReleaseNotes>
25+
<PackageReleaseNotes>- Fixed Base32 encoder
26+
- Updated NuGet dependency packages</PackageReleaseNotes>
2727
</PropertyGroup>
2828

2929
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

0 commit comments

Comments
 (0)