summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristoph Enzmann <christoph.enzmann@confer.ch>2013-12-05 11:19:11 +0100
committerChristoph Enzmann <christoph.enzmann@confer.ch>2013-12-05 11:19:11 +0100
commitb52682a9b3216a7d966238e3c8ed6b4da3920313 (patch)
tree9cd5e32a7dd65b1297dfe9655173f3a0c512be05
parent99c53801f3d4269b8058e87fa48b4bc37c7533fd (diff)
downloadTwoStepsAuthenticator-b52682a9b3216a7d966238e3c8ed6b4da3920313.zip
TwoStepsAuthenticator-b52682a9b3216a7d966238e3c8ed6b4da3920313.tar.gz
TwoStepsAuthenticator-b52682a9b3216a7d966238e3c8ed6b4da3920313.tar.bz2
Counter based OTP added, removed UsedCodesManager from authenticator, UsedCodesManager refactoring
-rw-r--r--TwoStepsAuthenticator.TestApp/ViewModel.cs4
-rw-r--r--TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs7
-rw-r--r--TwoStepsAuthenticator.TestWebsite/Users/WebsiteUser.cs1
-rw-r--r--TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs42
-rw-r--r--TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs (renamed from TwoStepsAuthenticator.UnitTests/AuthenticatorTests.cs)6
-rw-r--r--TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj4
-rw-r--r--TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs23
-rw-r--r--TwoStepsAuthenticator/Authenticator.cs67
-rw-r--r--TwoStepsAuthenticator/CounterAuthenticator.cs51
-rw-r--r--TwoStepsAuthenticator/TimeAuthenticator.cs62
-rw-r--r--TwoStepsAuthenticator/TwoStepsAuthenticator.csproj2
-rw-r--r--TwoStepsAuthenticator/UsedCodesManager.cs51
-rw-r--r--packages/repositories.config5
13 files changed, 249 insertions, 76 deletions
diff --git a/TwoStepsAuthenticator.TestApp/ViewModel.cs b/TwoStepsAuthenticator.TestApp/ViewModel.cs
index 8bfeb9b..5a2e7b6 100644
--- a/TwoStepsAuthenticator.TestApp/ViewModel.cs
+++ b/TwoStepsAuthenticator.TestApp/ViewModel.cs
@@ -72,7 +72,7 @@ namespace TwoStepsAuthenticatorTestApp
public ViewModel()
{
- var authenticator = new TwoStepsAuthenticator.Authenticator();
+ var authenticator = new TwoStepsAuthenticator.TimeAuthenticator();
this.Key = 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..ead9168 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,9 +44,10 @@ namespace TwoStepsAuthenticator.TestWebsite.Controllers
public ActionResult DoubleAuth(string code)
{
WebsiteUser user = (WebsiteUser)Session["AuthenticatedUser"];
- var auth = new TwoStepsAuthenticator.Authenticator();
- if (auth.CheckCode(user.DoubleAuthKey, code))
+ var auth = new TwoStepsAuthenticator.TimeAuthenticator();
+ if (auth.CheckCode(user.DoubleAuthKey, code) && usedCodesManager.IsCodeUsed(user.DoubleAuthKey, code))
{
+ usedCodesManager.AddCode(user.DoubleAuthKey, code);
FormsAuthentication.SetAuthCookie(user.Login, true);
return RedirectToAction("Welcome");
}
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/CounterAuthenticatorTests.cs b/TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs
new file mode 100644
index 0000000..ebe7493
--- /dev/null
+++ b/TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs
@@ -0,0 +1,42 @@
+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 {
+
+ [Test]
+ public void CreateKey() {
+ var authenticator = new CounterAuthenticator();
+ var secret = authenticator.GenerateKey();
+ var code = authenticator.GetCode(secret, 0L);
+
+ Assert.IsTrue(authenticator.CheckCode(secret, code, 0L), "Generated Code doesn't verify");
+ }
+
+ // Test Values from http://www.ietf.org/rfc/rfc4226.txt - Appendix D
+ [TestCase("12345678901234567890", 0L, "755224")]
+ [TestCase("12345678901234567890", 1L, "287082")]
+ [TestCase("12345678901234567890", 2L, "359152")]
+ [TestCase("12345678901234567890", 3L, "969429")]
+ [TestCase("12345678901234567890", 4L, "338314")]
+ [TestCase("12345678901234567890", 5L, "254676")]
+ [TestCase("12345678901234567890", 6L, "287922")]
+ [TestCase("12345678901234567890", 7L, "162583")]
+ [TestCase("12345678901234567890", 8L, "399871")]
+ [TestCase("12345678901234567890", 9L, "520489")]
+ public void VerifyKeys(string secret, long counter, string code) {
+ var authenticator = new CounterAuthenticator();
+ var base32Secret = Base32Encoding.ToString(Encoding.ASCII.GetBytes(secret));
+
+ Assert.IsTrue(authenticator.CheckCode(base32Secret, code, counter));
+
+ }
+
+ }
+}
diff --git a/TwoStepsAuthenticator.UnitTests/AuthenticatorTests.cs b/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs
index 5c250b8..1f8c9ab 100644
--- a/TwoStepsAuthenticator.UnitTests/AuthenticatorTests.cs
+++ b/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs
@@ -7,11 +7,11 @@ using NUnit.Framework;
namespace TwoStepsAuthenticator.UnitTests {
[TestFixture]
- public class AuthenticatorTests {
+ public class TimeAuthenticatorTests {
[Test]
public void CreateKey() {
- var authenticator = new Authenticator();
+ var authenticator = new TimeAuthenticator();
var secret = authenticator.GenerateKey();
var code = authenticator.GetCode(secret);
@@ -26,7 +26,7 @@ namespace TwoStepsAuthenticator.UnitTests {
public void VerifyKeys(string secret, string timeString, string code) {
var date = DateTime.Parse(timeString);
- var authenticator = new Authenticator(() => date);
+ var authenticator = new TimeAuthenticator(() => date);
Assert.IsTrue(authenticator.CheckCode(secret, code));
}
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..7099c89
--- /dev/null
+++ b/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs
@@ -0,0 +1,23 @@
+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("abc", "def"));
+ manager.AddCode("abc", "def");
+ Assert.IsTrue(manager.IsCodeUsed("abc", "def"));
+ }
+
+ }
+}
diff --git a/TwoStepsAuthenticator/Authenticator.cs b/TwoStepsAuthenticator/Authenticator.cs
index 8ab1747..bee9b8b 100644
--- a/TwoStepsAuthenticator/Authenticator.cs
+++ b/TwoStepsAuthenticator/Authenticator.cs
@@ -7,59 +7,40 @@ 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;
- public Authenticator(Func<DateTime> nowFunc = null) {
- this.NowFunc = (nowFunc == null) ? () => DateTime.Now : nowFunc;
- }
-
- public string GenerateKey()
- {
+ public 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, long challengeValue) {
+ long 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 +52,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 +62,7 @@ namespace TwoStepsAuthenticator
return diff == 0;
}
- private int RandomInt(int max) {
+ protected 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..90b0ee4
--- /dev/null
+++ b/TwoStepsAuthenticator/CounterAuthenticator.cs
@@ -0,0 +1,51 @@
+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;
+
+ public CounterAuthenticator(int windowSize = 10) {
+ if (windowSize <= 0) {
+ throw new ArgumentException("look-ahead window size must be positive");
+ }
+
+ 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, long 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, long counter) {
+ var codeMatch = false;
+ for (int i = 0; i <= WindowSize; i++) {
+ if (ConstantTimeEquals(GetCode(secret, counter + i), code)) {
+ codeMatch = true;
+ }
+ }
+
+ return codeMatch;
+ }
+ }
+}
diff --git a/TwoStepsAuthenticator/TimeAuthenticator.cs b/TwoStepsAuthenticator/TimeAuthenticator.cs
new file mode 100644
index 0000000..a461520
--- /dev/null
+++ b/TwoStepsAuthenticator/TimeAuthenticator.cs
@@ -0,0 +1,62 @@
+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;
+
+ public TimeAuthenticator(Func<DateTime> nowFunc = null) {
+ this.NowFunc = (nowFunc == null) ? () => DateTime.Now : nowFunc;
+ }
+
+ /// <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) {
+ TimeSpan ts = (date.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc));
+ var interval = (long)ts.TotalSeconds / 30;
+
+ return GetCodeInternal(secret, interval);
+ }
+
+ /// <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) {
+ 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;
+ }
+ }
+
+ return codeMatch;
+ }
+ }
+}
diff --git a/TwoStepsAuthenticator/TwoStepsAuthenticator.csproj b/TwoStepsAuthenticator/TwoStepsAuthenticator.csproj
index b628bfd..467679a 100644
--- a/TwoStepsAuthenticator/TwoStepsAuthenticator.csproj
+++ b/TwoStepsAuthenticator/TwoStepsAuthenticator.csproj
@@ -41,7 +41,9 @@
<ItemGroup>
<Compile Include="Base32Encoding.cs" />
<Compile Include="Authenticator.cs" />
+ <Compile Include="CounterAuthenticator.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..0359acb 100644
--- a/TwoStepsAuthenticator/UsedCodesManager.cs
+++ b/TwoStepsAuthenticator/UsedCodesManager.cs
@@ -7,9 +7,9 @@ using System.Timers;
namespace TwoStepsAuthenticator
{
- internal class UsedCodesManager
+ public class UsedCodesManager
{
- private class UsedCode
+ internal class UsedCode
{
public UsedCode(String secret, String code)
{
@@ -17,12 +17,17 @@ namespace TwoStepsAuthenticator
this.Code = secret + code;
}
- public DateTime UseDate { get; set; }
- public String Code { get; set; }
+ public DateTime UseDate { get; private set; }
+ public 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) : false;
}
public override string ToString()
{
@@ -34,9 +39,10 @@ namespace TwoStepsAuthenticator
}
}
- 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 +55,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)
{
- lock (codeLock)
- {
+ try {
+ rwlock.AcquireWriterLock(lockingTimeout);
+
codes.Enqueue(new UsedCode(secret, code));
+ }
+ finally
+ {
+ rwlock.ReleaseWriterLock();
}
}
public bool IsCodeUsed(String secret, String code)
{
- lock (codeLock)
+ try
{
+ rwlock.AcquireReaderLock(lockingTimeout);
+
return codes.Contains(new UsedCode(secret, code));
+ }
+ finally
+ {
+ 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