Skip to content

Commit

Permalink
intital project
Browse files Browse the repository at this point in the history
  • Loading branch information
Farhad Zamani committed Apr 11, 2024
0 parents commit 1f51b80
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
bin
obj
.vs
25 changes: 25 additions & 0 deletions TotpGenerator.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TotpGenerator", "TotpGenerator\TotpGenerator.csproj", "{A245C9E9-FC25-401C-9F0E-C19E74CF21E1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A245C9E9-FC25-401C-9F0E-C19E74CF21E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A245C9E9-FC25-401C-9F0E-C19E74CF21E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A245C9E9-FC25-401C-9F0E-C19E74CF21E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A245C9E9-FC25-401C-9F0E-C19E74CF21E1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {69635BFF-0DA4-46E2-911A-254844A23EE1}
EndGlobalSection
EndGlobal
19 changes: 19 additions & 0 deletions TotpGenerator/TotpGenerator.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Genarate Totp Code library.</Description>
<VersionPrefix>1.0.0</VersionPrefix>
<Authors>Farhad Zamani</Authors>
<TargetFrameworks>net7.0;net6.0;net5.0;netstandard2.1;netstandard2.0;</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyName>TotpGenerator</AssemblyName>
<PackageId>TotpGenerator</PackageId>
<PackageTags>Totp;Passwrod;Time-based one-time password;.NET;aspnetcore</PackageTags>
<PackageProjectUrl>https://github.com/farhadzm/totp-generator</PackageProjectUrl>
<RepositoryUrl>https://github.com/farhadzm/totp-generator</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
</PropertyGroup>

</Project>
147 changes: 147 additions & 0 deletions TotpGenerator/TotpService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using System;
using System.Text;
using System.Security.Cryptography;
using System.Diagnostics;
using System.Net;

namespace TotpGenerator
{
/// <summary>
/// A class for generate and validate totp code based on time
/// </summary>
public class TotpService
{
private static readonly Encoding _encoding = new UTF8Encoding(false, true);

private static readonly TimeSpan _timeStep = TimeSpan.FromMinutes(1);
#if NETSTANDARD2_0
private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
#endif

/// <summary>
/// Genarating totp code based on securityStampToken, modifier and time.
/// </summary>
/// <param name="securityStampToken"></param>
/// <param name="modifier"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static int GenerateCode(string securityStampToken, string modifier)
{
if (securityStampToken == null)
{
throw new ArgumentNullException(nameof(securityStampToken));
}

byte[] securityTokenBytes = GetBytes(securityStampToken);

var currentTimeStep = GetCurrentTimeStepNumber();

using (var hashAlgorithm = new HMACSHA1(securityTokenBytes))
{
return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
}
}

/// <summary>
/// Validating totp code based on securityStampToken, code, modifier, expirationInMinutes
/// </summary>
/// <param name="securityStampToken"></param>
/// <param name="code"></param>
/// <param name="modifier"></param>
/// <param name="expirationInMinutes"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static bool ValidateCode(
string securityStampToken,
int code,
string modifier,
int expirationInMinutes)
{
if (securityStampToken == null)
{
throw new ArgumentNullException(nameof(securityStampToken));
}

byte[] securityTokenBytes = GetBytes(securityStampToken);

using (var hashAlgorithm = new HMACSHA1(securityTokenBytes))
{
for (var i = -Math.Abs(expirationInMinutes); i <= 1; i++)
{
var currentTimeStep = GetNextTimeStepNumber(i);

var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep), modifier);
if (computedTotp == code)
{
return true;
}
}
}

// No match
return false;
}

private static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
{
// # of 0's = length of pin
const int Mod = 1000000;

// See https://tools.ietf.org/html/rfc4226
// We can add an optional modifier
var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber));
var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier));

// Generate DT string
var offset = hash[hash.Length - 1] & 0xf;
Debug.Assert(offset + 4 < hash.Length);
var binaryCode = (hash[offset] & 0x7f) << 24
| (hash[offset + 1] & 0xff) << 16
| (hash[offset + 2] & 0xff) << 8
| (hash[offset + 3] & 0xff);

return binaryCode % Mod;
}

private static byte[] ApplyModifier(byte[] input, string modifier)
{
if (String.IsNullOrEmpty(modifier))
{
return input;
}

var modifierBytes = GetBytes(modifier);
var combined = new byte[checked(input.Length + modifierBytes.Length)];
Buffer.BlockCopy(input, 0, combined, 0, input.Length);
Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length);
return combined;
}

// More info: https://tools.ietf.org/html/rfc6238#section-4
private static ulong GetCurrentTimeStepNumber()
{
#if NETSTANDARD2_0
var delta = DateTime.UtcNow - _unixEpoch;
#else
var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch;
#endif
return (ulong)(delta.Ticks / _timeStep.Ticks);
}

private static ulong GetNextTimeStepNumber(int minutes)
{
#if NETSTANDARD2_0
var delta = DateTime.UtcNow - _unixEpoch;
#else
var delta = DateTimeOffset.UtcNow.AddMinutes(minutes) - DateTimeOffset.UnixEpoch;
#endif
return (ulong)(delta.Ticks / _timeStep.Ticks);
}

private static byte[] GetBytes(string securityToken)
{
return _encoding.GetBytes(securityToken);
}
}
}

0 comments on commit 1f51b80

Please sign in to comment.