Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow configuration of TimeStep #207

Merged
merged 3 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions Google.Authenticator.Tests/AuthCodeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ public void BasicAuthCodeTest()

var tfa = new TwoFactorAuthenticator();

var currentTime = 1416643820;
var currentCounter = 1416643820;

// I actually think you are supposed to divide the time by 30 seconds?
// Maybe need an overload that takes a DateTime?
var actual = tfa.GeneratePINAtInterval(secretKey, currentTime, 6);
var actual = tfa.GeneratePINAtInterval(secretKey, currentCounter, 6);

actual.ShouldBe(expected);
}
Expand Down
38 changes: 38 additions & 0 deletions Google.Authenticator.Tests/IntervalTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Xunit;
using Shouldly;
using System.Collections.Generic;
using System.Text;
using System;

namespace Google.Authenticator.Tests
{
public class IntervalTests
{
const string secret = "ggggjhG&^*&^jfSSSddd";

[Theory]
[MemberData(nameof(GetTestValues))]
public void GetCurrentPinsHandlesDifferentIntervals(int timeTolerance, int timeStep, int expectedCount)
{
var subject = new TwoFactorAuthenticator(timeStep);

subject.GetCurrentPINs(secret, TimeSpan.FromSeconds(timeTolerance)).Length.ShouldBe(expectedCount);
}

public static IEnumerable<object[]> GetTestValues()
{
yield return new object[] { 15, 30, 1 };
yield return new object[] { 30, 30, 3 };
yield return new object[] { 60, 30, 5 };

yield return new object[] { 15, 15, 3 };
yield return new object[] { 30, 15, 5 };
yield return new object[] { 60, 15, 9 };

yield return new object[] { 15, 60, 1 };
yield return new object[] { 30, 60, 1 };
yield return new object[] { 60, 60, 3 };
yield return new object[] { 300, 60, 11 };
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Google.Authenticator.Tests
{
public class ValidationTests
public class SecretTypeTests
{
const string secret = "ggggjhG&^*&^jfSSSddd";
private readonly static byte[] secretAsBytes = Encoding.UTF8.GetBytes(secret);
Expand Down Expand Up @@ -72,81 +72,6 @@ public void ValidateWorksWithDifferentSecretTypesSHA512(string pin, int irreleva
subject.ValidateTwoFactorPIN(secretAsBytes, pin, irrelevantNumberToAvoidDuplicatePinsBeingRemoved * 2);
}

[Fact]
public void GetCurrentPinsHandles15SecondInterval()
{
// This is nonsensical, really, as anything less than 30 == 0 in practice.
var subject = new TwoFactorAuthenticator();

subject.GetCurrentPINs(secret, TimeSpan.FromSeconds(15)).Length.ShouldBe(1);
}

[Fact]
public void GetCurrentPinsHandles15SecondIntervalSHA256()
{
// This is nonsensical, really, as anything less than 30 == 0 in practice.
var subject = new TwoFactorAuthenticator(HashType.SHA256);

subject.GetCurrentPINs(secret, TimeSpan.FromSeconds(15)).Length.ShouldBe(1);
}

[Fact]
public void GetCurrentPinsHandles15SecondIntervalSHA512()
{
// This is nonsensical, really, as anything less than 30 == 0 in practice.
var subject = new TwoFactorAuthenticator(HashType.SHA512);

subject.GetCurrentPINs(secret, TimeSpan.FromSeconds(15)).Length.ShouldBe(1);
}

[Fact]
public void GetCurrentPinsHandles30SecondInterval()
{
var subject = new TwoFactorAuthenticator();

subject.GetCurrentPINs(secret, TimeSpan.FromSeconds(30)).Length.ShouldBe(3);
}

[Fact]
public void GetCurrentPinsHandles30SecondIntervalSHA256()
{
var subject = new TwoFactorAuthenticator(HashType.SHA256);

subject.GetCurrentPINs(secret, TimeSpan.FromSeconds(30)).Length.ShouldBe(3);
}

[Fact]
public void GetCurrentPinsHandles30SecondIntervalSHA512()
{
var subject = new TwoFactorAuthenticator(HashType.SHA512);

subject.GetCurrentPINs(secret, TimeSpan.FromSeconds(30)).Length.ShouldBe(3);
}

[Fact]
public void GetCurrentPinsHandles60SecondInterval()
{
var subject = new TwoFactorAuthenticator();

subject.GetCurrentPINs(secret, TimeSpan.FromSeconds(60)).Length.ShouldBe(5);
}

[Fact]
public void GetCurrentPinsHandles60SecondIntervalSHA256()
{
var subject = new TwoFactorAuthenticator(HashType.SHA256);

subject.GetCurrentPINs(secret, TimeSpan.FromSeconds(60)).Length.ShouldBe(5);
}

[Fact]
public void GetCurrentPinsHandles60SecondIntervalSHA512()
{
var subject = new TwoFactorAuthenticator(HashType.SHA512);

subject.GetCurrentPINs(secret, TimeSpan.FromSeconds(60)).Length.ShouldBe(5);
}

public static IEnumerable<object[]> GetPins()
{
var subject = new TwoFactorAuthenticator();
Expand Down
45 changes: 45 additions & 0 deletions Google.Authenticator.Tests/TimeStepTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Xunit;
using Shouldly;
using System;

namespace Google.Authenticator.Tests
{
public class TimeStepTests
{
[Fact]
public void DefaultPINHasNotBeenChangedByAddingTimeStepConfig()
{
var now = new DateTime(2024,1,2,3,4,5,DateTimeKind.Utc);
var secret = "12314241234342342";
var defaultPin = new TwoFactorAuthenticator().GetCurrentPIN(secret, now);

defaultPin.ShouldBe("668182"); // This pin was created with the code from before the timestep config was added
}

[Fact]
public void DifferentTimeStepsReturnsDifferentPINs()
{
var now = new DateTime(2024,1,2,3,4,5,DateTimeKind.Utc);
var secret = "12314241234342342";
var defaultPin = new TwoFactorAuthenticator().GetCurrentPIN(secret, now);
var pinWith15SecondTimeStep = new TwoFactorAuthenticator(15).GetCurrentPIN(secret, now);
var pinWith60SecondTimeStep = new TwoFactorAuthenticator(60).GetCurrentPIN(secret, now);

defaultPin.ShouldNotBe(pinWith15SecondTimeStep);
defaultPin.ShouldNotBe(pinWith60SecondTimeStep);
pinWith15SecondTimeStep.ShouldNotBe(pinWith60SecondTimeStep);
}

[Fact]
public void DefaultTimeStepGivesSamePinAs30()
{
var now = new DateTime(2024,1,2,3,4,5,DateTimeKind.Utc);
var secret = "12314241234342342";
var defaultPin = new TwoFactorAuthenticator().GetCurrentPIN(secret, now);
var pinWith30SecondTimeStep = new TwoFactorAuthenticator(30).GetCurrentPIN(secret, now);

defaultPin.ShouldBe(pinWith30SecondTimeStep);

}
}
}
4 changes: 2 additions & 2 deletions Google.Authenticator/Google.Authenticator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<Description>Google Authenticator Two-Factor Authentication Library (Not officially affiliated with Google.)</Description>
<Authors>Brandon Potter</Authors>
<Company>Brandon Potter</Company>
<Version>3.2.0</Version>
<PackageReleaseNotes>Added support for HMACSHA256 and HMACSHA512 as per the RFC spec. Care should be taken by the developer to ensure compatible apps are used.</PackageReleaseNotes>
<Version>3.3.0-beta1</Version>
<PackageReleaseNotes>Added the ability to use a different Time Step than the default 30 seconds</PackageReleaseNotes>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/BrandonPotter/GoogleAuthenticator</PackageProjectUrl>
<PackageId>GoogleAuthenticator</PackageId>
Expand Down
36 changes: 26 additions & 10 deletions Google.Authenticator/TwoFactorAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,32 @@ public class TwoFactorAuthenticator
private static readonly DateTime _epoch =
new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

private TimeSpan DefaultClockDriftTolerance { get; set; }
private readonly TimeSpan DefaultClockDriftTolerance;

private HashType HashType { get; set; }
private readonly HashType HashType;

public TwoFactorAuthenticator() : this(HashType.SHA1)
private readonly int timeStep;

public TwoFactorAuthenticator() : this(HashType.SHA1)
{}

public TwoFactorAuthenticator(HashType hashType)
public TwoFactorAuthenticator(HashType hashType) : this(hashType, 30)
{
}

public TwoFactorAuthenticator(int timeStep) : this(HashType.SHA1, timeStep)
{}

/// <summary>
/// Initializes a new instance of the <see cref="TwoFactorAuthenticator"/> class.
/// </summary>
/// <param name="hashType">The type of Hash to generate (default is SHA1)</param>
/// <param name="timeStep">The length of the "time step" - i.e. how often the code changes. Default is 30.</param>
public TwoFactorAuthenticator(HashType hashType, int timeStep)
{
HashType = hashType;
DefaultClockDriftTolerance = TimeSpan.FromMinutes(5);
this.timeStep = timeStep;
}

/// <summary>
Expand Down Expand Up @@ -177,11 +191,13 @@ private string GenerateHashedCode(byte[] key, long iterationNumber, int digits =
return password.ToString(new string('0', digits));
}

private long GetCurrentCounter() => GetCurrentCounter(DateTime.UtcNow, _epoch, 30);
private long GetCurrentCounter() => GetCurrentCounter(DateTime.UtcNow, _epoch);

private long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep) =>
private long GetCurrentCounter(DateTime now, DateTime epoch) =>
(long) (now - epoch).TotalSeconds / timeStep;



/// <summary>
/// Given a PIN from a client, check if it is valid at the current time.
/// </summary>
Expand Down Expand Up @@ -262,7 +278,7 @@ public string GetCurrentPIN(string accountSecretKey, bool secretIsBase32 = false
/// <param name="secretIsBase32">Flag saying if accountSecretKey is in Base32 format or original secret</param>
/// <returns>A 6-digit PIN</returns>
public string GetCurrentPIN(string accountSecretKey, DateTime now, bool secretIsBase32 = false) =>
GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(now, _epoch, 30), secretIsBase32: secretIsBase32);
GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(now, _epoch), secretIsBase32: secretIsBase32);

/// <summary>
/// Get the PIN for current time; the same code that a 2FA app would generate for the current time.
Expand All @@ -281,7 +297,7 @@ public string GetCurrentPIN(byte[] accountSecretKey) =>
/// <param name="now">The time you wish to generate the pin for</param>
/// <returns>A 6-digit PIN</returns>
public string GetCurrentPIN(byte[] accountSecretKey, DateTime now) =>
GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(now, _epoch, 30));
GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(now, _epoch));

/// <summary>
/// Get all the PINs that would be valid within the time window allowed for by the default clock drift.
Expand Down Expand Up @@ -320,8 +336,8 @@ public string[] GetCurrentPINs(byte[] accountSecretKey, TimeSpan timeTolerance)
{
var iterationOffset = 0;

if (timeTolerance.TotalSeconds >= 30)
iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / 30.00);
if (timeTolerance.TotalSeconds >= timeStep)
iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / timeStep);

return GetCurrentPINs(accountSecretKey, iterationOffset);
}
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ bool result = tfa.ValidateTwoFactorPIN(key, txtCode.Text)

## Update history

### 3.3.0

Added support for configuring the "time step". This is basically how often the code changes.
The default used by most authenticator apps is 30 seconds, but some hardware devices use 60 seconds. You can now specify this in the constructor.

### 3.2.0

Added support for HMACSHA256 and HMACSHA512 as per the [RFC spec](https://datatracker.ietf.org/doc/html/rfc6238#section-1.2). In testing it was found that several popular apps (such as Authy and Microsoft Authenticator) may not have support for these algorithms so care should be taken by the developer to ensure compatible apps are used.
Expand Down