diff options
7 files changed, 57 insertions, 38 deletions
diff --git a/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs b/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs index ead9168..ca6666c 100644 --- a/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs +++ b/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs @@ -44,10 +44,9 @@ namespace TwoStepsAuthenticator.TestWebsite.Controllers public ActionResult DoubleAuth(string code) { WebsiteUser user = (WebsiteUser)Session["AuthenticatedUser"]; - var auth = new TwoStepsAuthenticator.TimeAuthenticator(); - if (auth.CheckCode(user.DoubleAuthKey, code) && usedCodesManager.IsCodeUsed(user.DoubleAuthKey, code)) + var auth = new TwoStepsAuthenticator.TimeAuthenticator(usedCodeManager: usedCodesManager); + if (auth.CheckCode(user.DoubleAuthKey, code)) { - usedCodesManager.AddCode(user.DoubleAuthKey, code); FormsAuthentication.SetAuthCookie(user.Login, true); return RedirectToAction("Welcome"); } diff --git a/TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs b/TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs index 7b2944f..f834178 100644 --- a/TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs +++ b/TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs @@ -14,9 +14,9 @@ namespace TwoStepsAuthenticator.UnitTests { public void CreateKey() { var authenticator = new CounterAuthenticator(); var secret = Authenticator.GenerateKey(); - var code = authenticator.GetCode(secret, 0L); + var code = authenticator.GetCode(secret, 0uL); - Assert.IsTrue(authenticator.CheckCode(secret, code, 0L), "Generated Code doesn't verify"); + Assert.IsTrue(authenticator.CheckCode(secret, code, 0uL), "Generated Code doesn't verify"); } // Test Values from http://www.ietf.org/rfc/rfc4226.txt - Appendix D diff --git a/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs b/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs index 7099c89..15ae91e 100644 --- a/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs +++ b/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs @@ -14,9 +14,9 @@ namespace TwoStepsAuthenticator.UnitTests { 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")); + Assert.IsFalse(manager.IsCodeUsed(42uL, "def")); + manager.AddCode(42uL, "def"); + Assert.IsTrue(manager.IsCodeUsed(42uL, "def")); } } diff --git a/TwoStepsAuthenticator/CounterAuthenticator.cs b/TwoStepsAuthenticator/CounterAuthenticator.cs index e9162fd..d4eafec 100644 --- a/TwoStepsAuthenticator/CounterAuthenticator.cs +++ b/TwoStepsAuthenticator/CounterAuthenticator.cs @@ -11,12 +11,14 @@ namespace TwoStepsAuthenticator { /// </summary> public class CounterAuthenticator : Authenticator { private readonly int WindowSize; + private readonly IUsedCodesManager UsedCodeManager; - public CounterAuthenticator(int windowSize = 10) { + 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) ? new UsedCodesManager() : usedCodeManager; this.WindowSize = windowSize; } @@ -56,9 +58,12 @@ namespace TwoStepsAuthenticator { ulong successfulSequenceNumber = 0uL; for (uint i = 0; i <= WindowSize; i++) { - if (ConstantTimeEquals(GetCode(secret, counter + i), code)) { + ulong checkCounter = counter + i; + if (ConstantTimeEquals(GetCode(secret, checkCounter), code) && !UsedCodeManager.IsCodeUsed(checkCounter, code)) { codeMatch = true; - successfulSequenceNumber = counter + i; + successfulSequenceNumber = checkCounter; + + UsedCodeManager.AddCode(successfulSequenceNumber, code); } } diff --git a/TwoStepsAuthenticator/IUsedCodesManager.cs b/TwoStepsAuthenticator/IUsedCodesManager.cs index 205df9d..461975c 100644 --- a/TwoStepsAuthenticator/IUsedCodesManager.cs +++ b/TwoStepsAuthenticator/IUsedCodesManager.cs @@ -5,21 +5,21 @@ namespace TwoStepsAuthenticator { /// <summary> /// Manages used code to prevent repeated use of a code. /// </summary> - interface IUsedCodesManager { + public interface IUsedCodesManager { /// <summary> /// Adds secret/code pair. /// </summary> - /// <param name="secret"></param> - /// <param name="code"></param> - void AddCode(string secret, string code); + /// <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="secret"></param> - /// <param name="code"></param> + /// <param name="challenge">Used Challenge</param> + /// <param name="code">Used Code</param> /// <returns></returns> - bool IsCodeUsed(string secret, string code); + bool IsCodeUsed(ulong challenge, string code); } } diff --git a/TwoStepsAuthenticator/TimeAuthenticator.cs b/TwoStepsAuthenticator/TimeAuthenticator.cs index 0cfc525..015847d 100644 --- a/TwoStepsAuthenticator/TimeAuthenticator.cs +++ b/TwoStepsAuthenticator/TimeAuthenticator.cs @@ -11,9 +11,13 @@ namespace TwoStepsAuthenticator { /// </summary> public class TimeAuthenticator : Authenticator { private readonly Func<DateTime> NowFunc; + private readonly IUsedCodesManager UsedCodeManager; + private readonly int IntervalSeconds; - public TimeAuthenticator(Func<DateTime> nowFunc = null) { + public TimeAuthenticator(Func<DateTime> nowFunc = null, IUsedCodesManager usedCodeManager = null, int intervalSeconds = 30) { this.NowFunc = (nowFunc == null) ? () => DateTime.Now : nowFunc; + this.UsedCodeManager = (usedCodeManager == null) ? new UsedCodesManager() : usedCodeManager; + this.IntervalSeconds = intervalSeconds; } /// <summary> @@ -32,10 +36,7 @@ namespace TwoStepsAuthenticator { /// <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, (ulong)interval); + return GetCodeInternal(secret, (ulong)GetInterval(date)); } /// <summary> @@ -64,15 +65,24 @@ namespace TwoStepsAuthenticator { // 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)) { + 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/UsedCodesManager.cs b/TwoStepsAuthenticator/UsedCodesManager.cs index 57699c5..6ffd6cf 100644 --- a/TwoStepsAuthenticator/UsedCodesManager.cs +++ b/TwoStepsAuthenticator/UsedCodesManager.cs @@ -7,18 +7,23 @@ using System.Timers; namespace TwoStepsAuthenticator { - public class UsedCodesManager : TwoStepsAuthenticator.IUsedCodesManager + /// <summary> + /// Local, thread-save used codes manager implementation + /// </summary> + public class UsedCodesManager : IUsedCodesManager { - internal 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; private set; } - public String Code { get; private 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) { @@ -27,15 +32,15 @@ namespace TwoStepsAuthenticator } var other = obj as UsedCode; - return (other != null) ? this.Code.Equals(other.Code) : false; + 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; } } @@ -70,12 +75,12 @@ namespace TwoStepsAuthenticator } } - public void AddCode(String secret, String code) + public void AddCode(ulong challenge, String code) { try { rwlock.AcquireWriterLock(lockingTimeout); - codes.Enqueue(new UsedCode(secret, code)); + codes.Enqueue(new UsedCode(challenge, code)); } finally { @@ -83,13 +88,13 @@ namespace TwoStepsAuthenticator } } - public bool IsCodeUsed(String secret, String code) + public bool IsCodeUsed(ulong challenge, String code) { try { rwlock.AcquireReaderLock(lockingTimeout); - return codes.Contains(new UsedCode(secret, code)); + return codes.Contains(new UsedCode(challenge, code)); } finally { |