(feature): ttop generator and base32 encoder

This commit is contained in:
Maksym Sadovnychyy 2024-10-03 20:22:19 +02:00
parent f80aa1dd95
commit 885f3d80a4
5 changed files with 281 additions and 1 deletions

View File

@ -0,0 +1,53 @@
using System.Text;
using MaksIT.Core.Security;
namespace MaksIT.Core.Tests.Security;
public class Base32EncoderTests {
[Fact]
public void Encode_ValidInput_ReturnsExpectedBase32String() {
// Arrange
var input = Encoding.UTF8.GetBytes("Hello World");
var expected = "JBSWY3DPEBLW64TMMQ======";
// Act
var result = Base32Encoder.Encode(input);
// Assert
Assert.Equal(expected, result);
}
[Fact]
public void Decode_ValidBase32String_ReturnsExpectedByteArray() {
// Arrange
var input = "JBSWY3DPEBLW64TMMQ======";
var expected = Encoding.UTF8.GetBytes("Hello World");
// Act
var result = Base32Encoder.Decode(input);
// Assert
Assert.Equal(expected, result);
}
[Fact]
public void Decode_InvalidBase32String_ThrowsFormatException() {
// Act & Assert
Assert.Throws<FormatException>(() => Base32Encoder.Decode("InvalidBase32String"));
}
[Fact]
public void EncodeDecode_RoundTrip_ReturnsOriginalData() {
// Arrange
var originalData = Encoding.UTF8.GetBytes("RoundTripTest");
// Act
var encoded = Base32Encoder.Encode(originalData);
var decoded = Base32Encoder.Decode(encoded);
// Assert
Assert.Equal(originalData, decoded);
}
}

View File

@ -0,0 +1,69 @@
using MaksIT.Core.Security;
using System;
using Xunit;
namespace MaksIT.Core.Tests.Security {
public class TotpGeneratorTests {
private const string Base32Secret = "JBSWY3DPEHPK3PXP"; // Example Base32 secret
[Fact]
public void Validate_ValidTotpCode_ReturnsTrue() {
// Arrange
var timestep = TotpGenerator.GetCurrentTimeStepNumber();
var validTotpCode = TotpGenerator.Generate(Base32Secret, timestep);
// Act
var isValid = TotpGenerator.Validate(validTotpCode, Base32Secret);
// Assert
Assert.True(isValid);
}
[Fact]
public void Validate_InvalidTotpCode_ReturnsFalse() {
// Arrange
var invalidTotpCode = "123456"; // Example invalid TOTP code
// Act
var isValid = TotpGenerator.Validate(invalidTotpCode, Base32Secret);
// Assert
Assert.False(isValid);
}
[Fact]
public void Validate_TotpCodeWithTimeTolerance_ReturnsTrue() {
// Arrange
var timestep = TotpGenerator.GetCurrentTimeStepNumber() - 1; // One timestep in the past
var validTotpCode = TotpGenerator.Generate(Base32Secret, timestep);
// Act
var isValid = TotpGenerator.Validate(validTotpCode, Base32Secret, timeTolerance: 1);
// Assert
Assert.True(isValid);
}
[Fact]
public void Generate_WithBase32Secret_ReturnsTotpCode() {
// Arrange
var timestep = TotpGenerator.GetCurrentTimeStepNumber();
// Act
var totpCode = TotpGenerator.Generate(Base32Secret, timestep);
// Assert
Assert.False(string.IsNullOrEmpty(totpCode));
Assert.Equal(6, totpCode.Length);
}
[Fact]
public void GetCurrentTimeStepNumber_ReturnsNonNegativeValue() {
// Act
var timestep = TotpGenerator.GetCurrentTimeStepNumber();
// Assert
Assert.True(timestep >= 0);
}
}
}

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata -->
<PackageId>MaksIT.Core</PackageId>
<Version>1.0.5</Version>
<Version>1.0.6</Version>
<Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company>
<Product>MaksIT.Core</Product>

View File

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MaksIT.Core.Security;
public static class Base32Encoder {
private static readonly char[] Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
private const string PaddingChar = "=";
public static string Encode(byte[] data) {
if (data == null || data.Length == 0) {
throw new ArgumentNullException(nameof(data));
}
var result = new StringBuilder();
int buffer = data[0];
int next = 1;
int bitsLeft = 8;
while (bitsLeft > 0 || next < data.Length) {
if (bitsLeft < 5) {
if (next < data.Length) {
buffer <<= 8;
buffer |= (data[next++] & 0xFF);
bitsLeft += 8;
}
else {
int pad = 5 - bitsLeft;
buffer <<= pad;
bitsLeft += pad;
}
}
int index = (buffer >> (bitsLeft - 5)) & 0x1F;
bitsLeft -= 5;
result.Append(Base32Alphabet[index]);
}
// Padding for a complete block
int padding = result.Length % 8;
if (padding > 0) {
for (int i = padding; i < 8; i++) {
result.Append(PaddingChar);
}
}
return result.ToString();
}
public static byte[] Decode(string base32) {
if (string.IsNullOrEmpty(base32)) {
throw new ArgumentNullException(nameof(base32));
}
base32 = base32.TrimEnd(PaddingChar.ToCharArray());
int byteCount = base32.Length * 5 / 8;
byte[] result = new byte[byteCount];
int buffer = 0;
int bitsLeft = 0;
int index = 0;
foreach (char c in base32) {
int charValue = CharToValue(c);
if (charValue < 0) {
throw new FormatException("Invalid base32 character.");
}
buffer <<= 5;
buffer |= charValue & 0x1F;
bitsLeft += 5;
if (bitsLeft >= 8) {
result[index++] = (byte)(buffer >> (bitsLeft - 8));
bitsLeft -= 8;
}
}
return result;
}
private static int CharToValue(char c) {
if (c >= 'A' && c <= 'Z') {
return c - 'A';
}
if (c >= '2' && c <= '7') {
return c - '2' + 26;
}
return -1; // Invalid character
}
}

View File

@ -0,0 +1,63 @@
using System.Security.Cryptography;
namespace MaksIT.Core.Security;
public static class TotpGenerator {
private const int Timestep = 30; // Time step in seconds (standard is 30 seconds)
private const int TotpDigits = 6; // Standard TOTP length is 6 digits
public static bool Validate(string totpCode, string base32Secret, int timeTolerance = 1) {
// Convert the Base32 encoded secret to a byte array
byte[] secretBytes = Base32Encoder.Decode(base32Secret);
// Get current timestamp
long timeStepWindow = GetCurrentTimeStepNumber();
// Validate the TOTP code against the valid time windows (current and around it)
for (int i = -timeTolerance; i <= timeTolerance; i++) {
var generatedTotp = Generate(secretBytes, timeStepWindow + i);
if (generatedTotp == totpCode) {
return true;
}
}
return false;
}
public static string Generate(string base32Secret, long timestep) {
// Convert the Base32 encoded secret to a byte array
byte[] secretBytes = Base32Encoder.Decode(base32Secret);
return Generate(secretBytes, timestep);
}
private static string Generate(byte[] secretBytes, long timestep) {
// Convert the time step to byte array (8-byte big-endian)
byte[] timestepBytes = BitConverter.GetBytes(timestep);
if (BitConverter.IsLittleEndian) {
Array.Reverse(timestepBytes);
}
// Generate HMAC-SHA1 hash based on the secret and the time step
using (var hmac = new HMACSHA1(secretBytes)) {
byte[] hash = hmac.ComputeHash(timestepBytes);
// Extract a 4-byte dynamic binary code from the hash
int offset = hash[hash.Length - 1] & 0x0F;
int binaryCode = (hash[offset] & 0x7F) << 24
| (hash[offset + 1] & 0xFF) << 16
| (hash[offset + 2] & 0xFF) << 8
| (hash[offset + 3] & 0xFF);
// Reduce to the desired number of digits
int totp = binaryCode % (int)Math.Pow(10, TotpDigits);
return totp.ToString(new string('0', TotpDigits)); // Ensure leading zeroes
}
}
public static long GetCurrentTimeStepNumber() {
var unixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return unixTimestamp / Timestep;
}
}