-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Farhad Zamani
committed
Apr 11, 2024
0 parents
commit 1f51b80
Showing
4 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
bin | ||
obj | ||
.vs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |