summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuillaume Lacasa <guillaume.lacasa@ucaya.com>2013-12-06 15:32:54 +0100
committerGuillaume Lacasa <guillaume.lacasa@ucaya.com>2013-12-06 15:32:54 +0100
commit50beaba8291c1e777d5a63bb19400a260712f837 (patch)
tree46b14ab6b93d813e9f40aae760f4b3902e380800
parent33c70541ca1e0ea27ac6a9ba5bbee799f03f9de8 (diff)
parent41cb13ee263104981850a73d4abf4a583df31941 (diff)
downloadTwoStepsAuthenticator-50beaba8291c1e777d5a63bb19400a260712f837.zip
TwoStepsAuthenticator-50beaba8291c1e777d5a63bb19400a260712f837.tar.gz
TwoStepsAuthenticator-50beaba8291c1e777d5a63bb19400a260712f837.tar.bz2
Merge branch 'master' of https://github.com/dusk0r/TwoStepsAuthenticator
-rw-r--r--README.md43
-rw-r--r--TwoStepsAuthenticator.TestApp/ViewModel.cs6
-rw-r--r--TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs4
-rw-r--r--TwoStepsAuthenticator.TestWebsite/Users/WebsiteUser.cs1
-rw-r--r--TwoStepsAuthenticator.UnitTests/AuthenticatorTests.cs34
-rw-r--r--TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs71
-rw-r--r--TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs67
-rw-r--r--TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj4
-rw-r--r--TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs37
-rw-r--r--TwoStepsAuthenticator/Authenticator.cs68
-rw-r--r--TwoStepsAuthenticator/CounterAuthenticator.cs74
-rw-r--r--TwoStepsAuthenticator/IUsedCodesManager.cs25
-rw-r--r--TwoStepsAuthenticator/TimeAuthenticator.cs88
-rw-r--r--TwoStepsAuthenticator/TwoStepsAuthenticator.csproj3
-rw-r--r--TwoStepsAuthenticator/UsedCodesManager.cs70
-rw-r--r--packages/repositories.config5
16 files changed, 475 insertions, 125 deletions
diff --git a/README.md b/README.md
index db96b7f..45bc86f 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,51 @@
TwoStepsAuthenticator
=====================
-.net implementation of the TOTP: Time-Based One-Time Password Algorithm<br/>
-RFC 6238 http://tools.ietf.org/html/rfc6238
+.net implementation of the TOTP: Time-Based One-Time Password Algorithm and HOTP: HMAC-Based One-Time Password Algorithm<br/>
+RFC 6238 http://tools.ietf.org/html/rfc6238<br>
+RFC 4226 http://tools.ietf.org/html/rfc4226
-compatible with Microsoft Authenticator for Windows Phone, and Google Authenticator for Android and iPhone.
+Compatible with Microsoft Authenticator for Windows Phone, and Google Authenticator for Android and iPhone.
You can use this library as well for a client application (if you want to create your own authenticator) or for a server application (add two-step authentication on your asp.net website)
For a client application, you need to save the secret key for your user. <br/>
Then, you only have to call the method GetCode(string) :
-<pre><code>var secret = user.secretAuthToken;
-var authenticator = new TwoStepsAuthenticator.Authenticator();
-var code = authenticator.GetCode(secret);</code></pre>
+<pre><code>
+var secret = user.secretAuthToken;
+var authenticator = new TwoStepsAuthenticator.TimeAuthenticator();
+var code = authenticator.GetCode(secret);
+</code></pre>
On a server application, you will have to generate a secret key, and share it with the user, who will have to enter it in his own authenticator app.
-<pre><code>var authenticator = new TwoStepsAuthenticator.Authenticator();
-var key = authenticator.GenerateKey();</code></pre>
+<pre><code>
+var key = TwoStepsAuthenticator.Authenticator.GenerateKey();
+</code></pre>
When the user will login, he will have to give you the code generated by his authenticator.<br/>
You can check if the code is correct with the method CheckCode(string secret, string code).<br/>
If the code is incorrect, don't log him.
-<pre><code>var secret = user.secretAuthToken;
+<pre><code>
+var secret = user.secretAuthToken;
var code = Request.Form["code"];
-var authenticator = new TwoStepsAuthenticator.Authenticator();
-var isok = authenticator.CheckCode(secret, code);</code></pre>
+var authenticator = new TwoStepsAuthenticator.TimeAuthenticator();
+bool isok = authenticator.CheckCode(secret, code);
+</code></pre>
+
+Every code should only be used once. To prevent repeated use of a code a UsedCodesManager class is provided.<br>
+It should be used as a singleton instance.
+
+<pre><code>
+var usedCodesManager = new UsedCodesManager();
+var secret = user.secretAuthToken;
+var code = Request.Form["code"];
+if (autenticator.CheckCode(secret, code) && usedCodesManager.IsCodeUsed(secret, code)) {
+ usedCodesManager.AddCode(secret, code);
+ // OK
+} else {
+ // Not OK
+}
+</code></pre> \ No newline at end of file
diff --git a/TwoStepsAuthenticator.TestApp/ViewModel.cs b/TwoStepsAuthenticator.TestApp/ViewModel.cs
index 8bfeb9b..adc07a4 100644
--- a/TwoStepsAuthenticator.TestApp/ViewModel.cs
+++ b/TwoStepsAuthenticator.TestApp/ViewModel.cs
@@ -72,8 +72,8 @@ namespace TwoStepsAuthenticatorTestApp
public ViewModel()
{
- var authenticator = new TwoStepsAuthenticator.Authenticator();
- this.Key = authenticator.GenerateKey();
+ var authenticator = new TwoStepsAuthenticator.TimeAuthenticator();
+ this.Key = TwoStepsAuthenticator.Authenticator.GenerateKey();
timer = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Normal, timerCallback, App.Current.Dispatcher);
timer.Start();
}
@@ -95,7 +95,7 @@ namespace TwoStepsAuthenticatorTestApp
internal void GetCode()
{
- var auth = new TwoStepsAuthenticator.Authenticator();
+ var auth = new TwoStepsAuthenticator.TimeAuthenticator();
Code = auth.GetCode(this.Key);
auth.CheckCode(key, Code);
diff --git a/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs b/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs
index c926019..ca6666c 100644
--- a/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs
+++ b/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs
@@ -13,6 +13,8 @@ namespace TwoStepsAuthenticator.TestWebsite.Controllers
//
// GET: /Home/
+ private static readonly UsedCodesManager usedCodesManager = new UsedCodesManager();
+
public ActionResult Index()
{
return View();
@@ -42,7 +44,7 @@ namespace TwoStepsAuthenticator.TestWebsite.Controllers
public ActionResult DoubleAuth(string code)
{
WebsiteUser user = (WebsiteUser)Session["AuthenticatedUser"];
- var auth = new TwoStepsAuthenticator.Authenticator();
+ var auth = new TwoStepsAuthenticator.TimeAuthenticator(usedCodeManager: usedCodesManager);
if (auth.CheckCode(user.DoubleAuthKey, code))
{
FormsAuthentication.SetAuthCookie(user.Login, true);
diff --git a/TwoStepsAuthenticator.TestWebsite/Users/WebsiteUser.cs b/TwoStepsAuthenticator.TestWebsite/Users/WebsiteUser.cs
index 2cc6cc9..41f695a 100644
--- a/TwoStepsAuthenticator.TestWebsite/Users/WebsiteUser.cs
+++ b/TwoStepsAuthenticator.TestWebsite/Users/WebsiteUser.cs
@@ -26,4 +26,5 @@ namespace TwoStepsAuthenticator.TestWebsite.Users
}
}
+
} \ No newline at end of file
diff --git a/TwoStepsAuthenticator.UnitTests/AuthenticatorTests.cs b/TwoStepsAuthenticator.UnitTests/AuthenticatorTests.cs
deleted file mode 100644
index 5c250b8..0000000
--- a/TwoStepsAuthenticator.UnitTests/AuthenticatorTests.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using NUnit.Framework;
-
-namespace TwoStepsAuthenticator.UnitTests {
-
- [TestFixture]
- public class AuthenticatorTests {
-
- [Test]
- public void CreateKey() {
- var authenticator = new Authenticator();
- var secret = authenticator.GenerateKey();
- var code = authenticator.GetCode(secret);
-
- Assert.IsTrue(authenticator.CheckCode(secret, code), "Generated Code doesn't verify");
- }
-
- // Test Vectors from http://tools.ietf.org/html/rfc6238#appendix-B have all length 8. We want a length of 6.
- // This Test Vectors are from a Ruby implementation. They work with the Google Authentificator app.
- [TestCase("DRMK64PPMMC7TDZF", "2013-12-04 18:33:01 +0100", "661188")]
- [TestCase("EQOGSM3XZUH6SE2Y", "2013-12-04 18:34:56 +0100", "256804")]
- [TestCase("4VU7EQACVDMFJSBG", "2013-12-04 18:36:16 +0100", "800872")]
- public void VerifyKeys(string secret, string timeString, string code) {
- var date = DateTime.Parse(timeString);
-
- var authenticator = new Authenticator(() => date);
- Assert.IsTrue(authenticator.CheckCode(secret, code));
-
- }
- }
-}
diff --git a/TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs b/TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs
new file mode 100644
index 0000000..3a81a90
--- /dev/null
+++ b/TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using NUnit.Framework;
+
+namespace TwoStepsAuthenticator.UnitTests {
+
+ [TestFixture]
+ public class CounterAuthenticatorTests {
+ private MockUsedCodesManager mockUsedCodesManager { get; set; }
+
+ [SetUp]
+ public void SetUp() {
+ this.mockUsedCodesManager = new MockUsedCodesManager();
+ }
+
+ [Test]
+ public void Uses_usedCodesManager() {
+ var authenticator = new CounterAuthenticator(usedCodeManager: mockUsedCodesManager);
+ var secret = Authenticator.GenerateKey();
+ var code = authenticator.GetCode(secret, 42uL);
+
+ authenticator.CheckCode(secret, code, 42uL);
+ Assert.AreEqual(mockUsedCodesManager.LastChallenge, 42uL);
+ Assert.AreEqual(mockUsedCodesManager.LastCode, code);
+ }
+
+ [Test]
+ public void CreateKey() {
+ var authenticator = new CounterAuthenticator(usedCodeManager: mockUsedCodesManager);
+ var secret = Authenticator.GenerateKey();
+ var code = authenticator.GetCode(secret, 0uL);
+
+ Assert.IsTrue(authenticator.CheckCode(secret, code, 0uL), "Generated Code doesn't verify");
+ }
+
+ // Test Values from http://www.ietf.org/rfc/rfc4226.txt - Appendix D
+ [TestCase("12345678901234567890", 0uL, "755224")]
+ [TestCase("12345678901234567890", 1uL, "287082")]
+ [TestCase("12345678901234567890", 2uL, "359152")]
+ [TestCase("12345678901234567890", 3uL, "969429")]
+ [TestCase("12345678901234567890", 4uL, "338314")]
+ [TestCase("12345678901234567890", 5uL, "254676")]
+ [TestCase("12345678901234567890", 6uL, "287922")]
+ [TestCase("12345678901234567890", 7uL, "162583")]
+ [TestCase("12345678901234567890", 8uL, "399871")]
+ [TestCase("12345678901234567890", 9uL, "520489")]
+ public void VerifyKeys(string secret, ulong counter, string code) {
+ var authenticator = new CounterAuthenticator(usedCodeManager: mockUsedCodesManager);
+ var base32Secret = Base32Encoding.ToString(Encoding.ASCII.GetBytes(secret));
+
+ Assert.IsTrue(authenticator.CheckCode(base32Secret, code, counter));
+
+ }
+
+ [Test]
+ public void VerifyUsedCounter() {
+ var authenticator = new CounterAuthenticator(usedCodeManager: mockUsedCodesManager);
+
+ // Test Values from http://www.ietf.org/rfc/rfc4226.txt - Appendix D
+ var base32Secret = Base32Encoding.ToString(Encoding.ASCII.GetBytes("12345678901234567890"));
+
+ ulong usedCounter;
+ Assert.True(authenticator.CheckCode(base32Secret, "520489", 0uL, out usedCounter));
+
+ Assert.AreEqual(usedCounter, 9uL);
+ }
+ }
+}
diff --git a/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs b/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs
new file mode 100644
index 0000000..1a1ffc6
--- /dev/null
+++ b/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NUnit.Framework;
+
+namespace TwoStepsAuthenticator.UnitTests {
+
+ [TestFixture]
+ public class TimeAuthenticatorTests {
+ private MockUsedCodesManager mockUsedCodesManager { get; set; }
+
+ [SetUp]
+ public void SetUp() {
+ this.mockUsedCodesManager = new MockUsedCodesManager();
+ }
+
+ [Test]
+ public void CreateKey() {
+ var authenticator = new TimeAuthenticator(usedCodeManager: mockUsedCodesManager);
+ var secret = Authenticator.GenerateKey();
+ var code = authenticator.GetCode(secret);
+
+ Assert.IsTrue(authenticator.CheckCode(secret, code), "Generated Code doesn't verify");
+ }
+
+ [Test]
+ public void Uses_usedCodesManager() {
+ var date = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ var authenticator = new TimeAuthenticator(() => date, usedCodeManager: mockUsedCodesManager);
+ var secret = Authenticator.GenerateKey();
+ var code = authenticator.GetCode(secret);
+
+ authenticator.CheckCode(secret, code);
+ Assert.AreEqual(mockUsedCodesManager.LastChallenge, 0uL);
+ Assert.AreEqual(mockUsedCodesManager.LastCode, code);
+ }
+
+ // Test Vectors from http://tools.ietf.org/html/rfc6238#appendix-B have all length 8. We want a length of 6.
+ // This Test Vectors are from a Ruby implementation. They work with the Google Authentificator app.
+ [TestCase("DRMK64PPMMC7TDZF", "2013-12-04 18:33:01 +0100", "661188")]
+ [TestCase("EQOGSM3XZUH6SE2Y", "2013-12-04 18:34:56 +0100", "256804")]
+ [TestCase("4VU7EQACVDMFJSBG", "2013-12-04 18:36:16 +0100", "800872")]
+ public void VerifyKeys(string secret, string timeString, string code) {
+ var date = DateTime.Parse(timeString);
+
+ var authenticator = new TimeAuthenticator(() => date, usedCodeManager: mockUsedCodesManager);
+ Assert.IsTrue(authenticator.CheckCode(secret, code));
+
+ }
+
+ [Test]
+ public void VerifyUsedTime() {
+ var date = DateTime.Parse("2013-12-05 17:23:50 +0100");
+ var authenticator = new TimeAuthenticator(() => date, usedCodeManager: mockUsedCodesManager);
+
+ DateTime usedTime;
+
+ Assert.True(authenticator.CheckCode("H22Q7WAMQYFZOJ2Q", "696227", out usedTime));
+
+ // 17:23:50 - 30s
+ Assert.AreEqual(usedTime.Hour, 17);
+ Assert.AreEqual(usedTime.Minute, 23);
+ Assert.AreEqual(usedTime.Second, 20);
+ }
+ }
+}
diff --git a/TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj b/TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj
index 54dae80..aa38917 100644
--- a/TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj
+++ b/TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj
@@ -50,8 +50,10 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
- <Compile Include="AuthenticatorTests.cs" />
+ <Compile Include="CounterAuthenticatorTests.cs" />
+ <Compile Include="TimeAuthenticatorTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="UsedCodesManagerTests.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
diff --git a/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs b/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs
new file mode 100644
index 0000000..1138f54
--- /dev/null
+++ b/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using NUnit.Framework;
+
+namespace TwoStepsAuthenticator.UnitTests {
+
+ [TestFixture]
+ public class UsedCodesManagerTests {
+
+ [Test]
+ public void Can_add_codes() {
+ var manager = new UsedCodesManager();
+
+ Assert.IsFalse(manager.IsCodeUsed(42uL, "def"));
+ manager.AddCode(42uL, "def");
+ Assert.IsTrue(manager.IsCodeUsed(42uL, "def"));
+ }
+
+ }
+
+ internal class MockUsedCodesManager : IUsedCodesManager {
+ public ulong? LastChallenge { get; private set; }
+ public string LastCode { get; private set; }
+
+ public void AddCode(ulong challenge, string code) {
+ this.LastChallenge = challenge;
+ this.LastCode = code;
+ }
+
+ public bool IsCodeUsed(ulong challenge, string code) {
+ return false;
+ }
+ }
+}
diff --git a/TwoStepsAuthenticator/Authenticator.cs b/TwoStepsAuthenticator/Authenticator.cs
index 8ab1747..4415e77 100644
--- a/TwoStepsAuthenticator/Authenticator.cs
+++ b/TwoStepsAuthenticator/Authenticator.cs
@@ -7,59 +7,41 @@ using System.Threading.Tasks;
namespace TwoStepsAuthenticator
{
- public class Authenticator
+ public abstract class Authenticator
{
- private static Lazy<UsedCodesManager> usedCodes = new Lazy<UsedCodesManager>();
private static readonly RNGCryptoServiceProvider Random = new RNGCryptoServiceProvider(); // Is Thread-Safe
private static readonly int KeyLength = 16;
private static readonly string AvailableKeyChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
- private readonly Func<DateTime> NowFunc;
+ internal static readonly Lazy<IUsedCodesManager> DefaultUsedCodeManager = new Lazy<IUsedCodesManager>(() => new UsedCodesManager());
- public Authenticator(Func<DateTime> nowFunc = null) {
- this.NowFunc = (nowFunc == null) ? () => DateTime.Now : nowFunc;
- }
-
- public string GenerateKey()
- {
+ public static string GenerateKey() {
var keyChars = new char[KeyLength];
- for (int i = 0; i < keyChars.Length; i++)
- {
+ for (int i = 0; i < keyChars.Length; i++) {
keyChars[i] = AvailableKeyChars[RandomInt(AvailableKeyChars.Length)];
}
return new String(keyChars);
}
- public string GetCode(string secret)
- {
- return GetCode(secret, NowFunc());
- }
-
- public string GetCode(string secret, DateTime date)
- {
- var key = Base32Encoding.ToBytes(secret);
- for (int i = secret.Length; i < key.Length; i++)
- {
- key[i] = 0;
- }
-
- TimeSpan ts = (date.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc));
- var interval = (long)ts.TotalSeconds / 30;
-
- long chlg = interval;
+ protected string GetCodeInternal(string secret, ulong challengeValue) {
+ ulong chlg = challengeValue;
byte[] challenge = new byte[8];
for (int j = 7; j >= 0; j--) {
challenge[j] = (byte)((int)chlg & 0xff);
chlg >>= 8;
}
+ var key = Base32Encoding.ToBytes(secret);
+ for (int i = secret.Length; i < key.Length; i++) {
+ key[i] = 0;
+ }
+
HMACSHA1 mac = new HMACSHA1(key);
var hash = mac.ComputeHash(challenge);
int offset = hash[hash.Length - 1] & 0xf;
int truncatedHash = 0;
- for (int j = 0; j < 4; j++)
- {
+ for (int j = 0; j < 4; j++) {
truncatedHash <<= 8;
truncatedHash |= hash[offset + j];
}
@@ -71,29 +53,7 @@ namespace TwoStepsAuthenticator
return code.PadLeft(6, '0');
}
- public bool CheckCode(string secret, string code)
- {
- if (usedCodes.Value.IsCodeUsed(secret, code))
- return false;
-
- var baseTime = NowFunc();
-
- // We need to do this in constant time
- var codeMatch = false;
- for (int i = -2; i <= 1; i++)
- {
- var checkTime = baseTime.AddSeconds(30 * i);
- if (ConstantTimeEquals(GetCode(secret, checkTime), code))
- {
- codeMatch = true;
- usedCodes.Value.AddCode(secret, code);
- }
- }
-
- return codeMatch;
- }
-
- private bool ConstantTimeEquals(string a, string b) {
+ protected bool ConstantTimeEquals(string a, string b) {
uint diff = (uint)a.Length ^ (uint)b.Length;
for (int i = 0; i < a.Length && i < b.Length; i++) {
@@ -103,7 +63,7 @@ namespace TwoStepsAuthenticator
return diff == 0;
}
- private int RandomInt(int max) {
+ protected static int RandomInt(int max) {
var randomBytes = new byte[4];
Random.GetBytes(randomBytes);
diff --git a/TwoStepsAuthenticator/CounterAuthenticator.cs b/TwoStepsAuthenticator/CounterAuthenticator.cs
new file mode 100644
index 0000000..fffae73
--- /dev/null
+++ b/TwoStepsAuthenticator/CounterAuthenticator.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TwoStepsAuthenticator {
+
+ /// <summary>
+ /// Implementation of RFC 4226 Counter-Based One-Time Password Algorithm
+ /// </summary>
+ public class CounterAuthenticator : Authenticator {
+ private readonly int WindowSize;
+ private readonly IUsedCodesManager UsedCodeManager;
+
+ public CounterAuthenticator(int windowSize = 10, IUsedCodesManager usedCodeManager = null) {
+ if (windowSize <= 0) {
+ throw new ArgumentException("look-ahead window size must be positive");
+ }
+
+ this.UsedCodeManager = (usedCodeManager == null) ? Authenticator.DefaultUsedCodeManager.Value : usedCodeManager;
+ this.WindowSize = windowSize;
+ }
+
+ /// <summary>
+ /// Generates One-Time-Password.
+ /// </summary>
+ /// <param name="secret">Shared Secret</param>
+ /// <param name="counter">Current Counter</param>
+ /// <returns>OTP</returns>
+ public string GetCode(string secret, ulong counter) {
+ return GetCodeInternal(secret, counter);
+ }
+
+ /// <summary>
+ /// Checks if the passed code is valid.
+ /// </summary>
+ /// <param name="secret">Shared Secret</param>
+ /// <param name="code">OTP</param>
+ /// <param name="counter">Current Counter Position</param>
+ /// <returns>true if any code from counter to counter + WindowSize matches</returns>
+ public bool CheckCode(string secret, string code, ulong counter) {
+ ulong successfulSequenceNumber = 0uL;
+
+ return CheckCode(secret, code, counter, out successfulSequenceNumber);
+ }
+
+ /// <summary>
+ /// Checks if the passed code is valid.
+ /// </summary>
+ /// <param name="secret">Shared Secret</param>
+ /// <param name="code">OTP</param>
+ /// <param name="counter">Current Counter Position</param>
+ /// <param name="usedCounter">Matching counter value if successful</param>
+ /// <returns>true if any code from counter to counter + WindowSize matches</returns>
+ public bool CheckCode(string secret, string code, ulong counter, out ulong usedCounter) {
+ var codeMatch = false;
+ ulong successfulSequenceNumber = 0uL;
+
+ for (uint i = 0; i <= WindowSize; i++) {
+ ulong checkCounter = counter + i;
+ if (ConstantTimeEquals(GetCode(secret, checkCounter), code) && !UsedCodeManager.IsCodeUsed(checkCounter, code)) {
+ codeMatch = true;
+ successfulSequenceNumber = checkCounter;
+
+ UsedCodeManager.AddCode(successfulSequenceNumber, code);
+ }
+ }
+
+ usedCounter = successfulSequenceNumber;
+ return codeMatch;
+ }
+ }
+}
diff --git a/TwoStepsAuthenticator/IUsedCodesManager.cs b/TwoStepsAuthenticator/IUsedCodesManager.cs
new file mode 100644
index 0000000..461975c
--- /dev/null
+++ b/TwoStepsAuthenticator/IUsedCodesManager.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace TwoStepsAuthenticator {
+
+ /// <summary>
+ /// Manages used code to prevent repeated use of a code.
+ /// </summary>
+ public interface IUsedCodesManager {
+
+ /// <summary>
+ /// Adds secret/code pair.
+ /// </summary>
+ /// <param name="challenge">Used Challenge</param>
+ /// <param name="code">Used Code</param>
+ void AddCode(ulong challenge, string code);
+
+ /// <summary>
+ /// Checks if code was previously used.
+ /// </summary>
+ /// <param name="challenge">Used Challenge</param>
+ /// <param name="code">Used Code</param>
+ /// <returns></returns>
+ bool IsCodeUsed(ulong challenge, string code);
+ }
+}
diff --git a/TwoStepsAuthenticator/TimeAuthenticator.cs b/TwoStepsAuthenticator/TimeAuthenticator.cs
new file mode 100644
index 0000000..f46b6d0
--- /dev/null
+++ b/TwoStepsAuthenticator/TimeAuthenticator.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TwoStepsAuthenticator {
+
+ /// <summary>
+ /// Implementation of rfc6238 Time-Based One-Time Password Algorithm
+ /// </summary>
+ public class TimeAuthenticator : Authenticator {
+ private readonly Func<DateTime> NowFunc;
+ private readonly IUsedCodesManager UsedCodeManager;
+ private readonly int IntervalSeconds;
+
+ public TimeAuthenticator(Func<DateTime> nowFunc = null, IUsedCodesManager usedCodeManager = null, int intervalSeconds = 30) {
+ this.NowFunc = (nowFunc == null) ? () => DateTime.Now : nowFunc;
+ this.UsedCodeManager = (usedCodeManager == null) ? Authenticator.DefaultUsedCodeManager.Value : usedCodeManager;
+ this.IntervalSeconds = intervalSeconds;
+ }
+
+ /// <summary>
+ /// Generates One-Time Password.
+ /// </summary>
+ /// <param name="secret">Shared Secret</param>
+ /// <returns>OTP</returns>
+ public string GetCode(string secret) {
+ return GetCode(secret, NowFunc());
+ }
+
+ /// <summary>
+ /// Generates One-Time Password.
+ /// </summary>
+ /// <param name="secret">Shared Secret</param>
+ /// <param name="date">Time to use as challenge</param>
+ /// <returns>OTP</returns>
+ public string GetCode(string secret, DateTime date) {
+ return GetCodeInternal(secret, (ulong)GetInterval(date));
+ }
+
+ /// <summary>
+ /// Checks if the passed code is valid.
+ /// </summary>
+ /// <param name="secret">Shared Secret</param>
+ /// <param name="code">OTP</param>
+ /// <returns>true if code matches</returns>
+ public bool CheckCode(string secret, string code) {
+ DateTime successfulTime = DateTime.MinValue;
+
+ return CheckCode(secret, code, out successfulTime);
+ }
+
+ /// <summary>
+ /// Checks if the passed code is valid.
+ /// </summary>
+ /// <param name="secret">Shared Secret</param>
+ /// <param name="code">OTP</param>
+ /// <param name="usedDateTime">Matching time if successful</param>
+ /// <returns>true if code matches</returns>
+ public bool CheckCode(string secret, string code, out DateTime usedDateTime) {
+ var baseTime = NowFunc();
+ DateTime successfulTime = DateTime.MinValue;
+
+ // We need to do this in constant time
+ var codeMatch = false;
+ for (int i = -2; i <= 1; i++) {
+ var checkTime = baseTime.AddSeconds(IntervalSeconds * i);
+ ulong checkInterval = (ulong)GetInterval(checkTime);
+
+ if (ConstantTimeEquals(GetCode(secret, checkTime), code) && !UsedCodeManager.IsCodeUsed(checkInterval, code)) {
+ codeMatch = true;
+ successfulTime = checkTime;
+
+ UsedCodeManager.AddCode(checkInterval, code);
+ }
+ }
+
+ usedDateTime = successfulTime;
+ return codeMatch;
+ }
+
+ private long GetInterval(DateTime dateTime) {
+ TimeSpan ts = (dateTime.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc));
+ return (long)ts.TotalSeconds / IntervalSeconds;
+ }
+ }
+}
diff --git a/TwoStepsAuthenticator/TwoStepsAuthenticator.csproj b/TwoStepsAuthenticator/TwoStepsAuthenticator.csproj
index b628bfd..2ca64ae 100644
--- a/TwoStepsAuthenticator/TwoStepsAuthenticator.csproj
+++ b/TwoStepsAuthenticator/TwoStepsAuthenticator.csproj
@@ -41,7 +41,10 @@
<ItemGroup>
<Compile Include="Base32Encoding.cs" />
<Compile Include="Authenticator.cs" />
+ <Compile Include="CounterAuthenticator.cs" />
+ <Compile Include="IUsedCodesManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="TimeAuthenticator.cs" />
<Compile Include="UsedCodesManager.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
diff --git a/TwoStepsAuthenticator/UsedCodesManager.cs b/TwoStepsAuthenticator/UsedCodesManager.cs
index 3d4247b..6ffd6cf 100644
--- a/TwoStepsAuthenticator/UsedCodesManager.cs
+++ b/TwoStepsAuthenticator/UsedCodesManager.cs
@@ -7,36 +7,47 @@ using System.Timers;
namespace TwoStepsAuthenticator
{
- internal class UsedCodesManager
+ /// <summary>
+ /// Local, thread-save used codes manager implementation
+ /// </summary>
+ public class UsedCodesManager : IUsedCodesManager
{
- private class UsedCode
+ internal sealed class UsedCode
{
- public UsedCode(String secret, String code)
+ public UsedCode(ulong challenge, String code)
{
this.UseDate = DateTime.Now;
- this.Code = secret + code;
+ this.Code = code;
+ this.ChallengeValue = challenge;
}
- public DateTime UseDate { get; set; }
- public String Code { get; set; }
+ internal DateTime UseDate { get; private set; }
+ internal ulong ChallengeValue { get; private set; }
+ internal String Code { get; private set; }
public override bool Equals(object obj)
{
- return obj.ToString().Equals(Code);
+ if (Object.ReferenceEquals(this, obj)) {
+ return true;
+ }
+
+ var other = obj as UsedCode;
+ return (other != null) ? this.Code.Equals(other.Code) && this.ChallengeValue.Equals(other.ChallengeValue) : false;
}
public override string ToString()
{
- return Code;
+ return String.Format("{0}: {1}", ChallengeValue, Code);
}
public override int GetHashCode()
{
- return Code.GetHashCode();
+ return Code.GetHashCode() + ChallengeValue.GetHashCode() * 17;
}
}
- private Queue<UsedCode> codes;
- private object codeLock = new object();
- private Timer cleaner;
+ private readonly Queue<UsedCode> codes;
+ private readonly System.Threading.ReaderWriterLock rwlock = new System.Threading.ReaderWriterLock();
+ private readonly TimeSpan lockingTimeout = TimeSpan.FromSeconds(5);
+ private readonly Timer cleaner;
public UsedCodesManager()
{
@@ -49,28 +60,45 @@ namespace TwoStepsAuthenticator
void cleaner_Elapsed(object sender, ElapsedEventArgs e)
{
var timeToClean = DateTime.Now.AddMinutes(-5);
- lock (codeLock)
+
+ try
{
- while (codes.Count > 0 && codes.Peek().UseDate < timeToClean)
- {
+ rwlock.AcquireWriterLock(lockingTimeout);
+
+ while (codes.Count > 0 && codes.Peek().UseDate < timeToClean) {
codes.Dequeue();
}
+ }
+ finally
+ {
+ rwlock.ReleaseWriterLock();
}
}
- public void AddCode(String secret, String code)
+ public void AddCode(ulong challenge, String code)
{
- lock (codeLock)
+ try {
+ rwlock.AcquireWriterLock(lockingTimeout);
+
+ codes.Enqueue(new UsedCode(challenge, code));
+ }
+ finally
{
- codes.Enqueue(new UsedCode(secret, code));
+ rwlock.ReleaseWriterLock();
}
}
- public bool IsCodeUsed(String secret, String code)
+ public bool IsCodeUsed(ulong challenge, String code)
{
- lock (codeLock)
+ try
+ {
+ rwlock.AcquireReaderLock(lockingTimeout);
+
+ return codes.Contains(new UsedCode(challenge, code));
+ }
+ finally
{
- return codes.Contains(new UsedCode(secret, code));
+ rwlock.ReleaseReaderLock();
}
}
}
diff --git a/packages/repositories.config b/packages/repositories.config
new file mode 100644
index 0000000..ebfdba5
--- /dev/null
+++ b/packages/repositories.config
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<repositories>
+ <repository path="..\TwoStepsAuthenticator.TestWebsite\packages.config" />
+ <repository path="..\TwoStepsAuthenticator.UnitTests\packages.config" />
+</repositories> \ No newline at end of file