diff options
-rw-r--r-- | README.md | 34 | ||||
-rw-r--r-- | TwoStepsAuthenticator.UnitTests/MockUsedCodesManager.cs | 16 | ||||
-rw-r--r-- | TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs | 31 | ||||
-rw-r--r-- | TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj | 1 | ||||
-rw-r--r-- | TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs | 14 | ||||
-rw-r--r-- | TwoStepsAuthenticator.sln | 6 | ||||
-rw-r--r-- | TwoStepsAuthenticator/CounterAuthenticator.cs | 27 | ||||
-rw-r--r-- | TwoStepsAuthenticator/TimeAuthenticator.cs | 2 |
8 files changed, 83 insertions, 48 deletions
@@ -9,6 +9,10 @@ Compatible with Microsoft Authenticator for Windows Phone, and Google Authentica 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) +# TOTP: Time-Based One-Time Password Algorithm + +## Client usage + 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) : @@ -18,6 +22,8 @@ var authenticator = new TwoStepsAuthenticator.TimeAuthenticator(); var code = authenticator.GetCode(secret); </code></pre> +## Server usage + 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> @@ -35,17 +41,21 @@ 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. +### Used codes manager +Every code should only be used once. To prevent repeated use of a code a IUsedCodesManager interface is provided.<br> + +A default implementation is provided : used codes are kept in memory for 5 minutes (long enough for codes to become invalid) + +You can define how the used codes are stored, for example if you want to handle persistence (database storage), or if you have multiple webservers.<br/> +You have to implement the 2 methods of the IUsedCodesManager : <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 +void AddCode(ulong challenge, string code); +bool IsCodeUsed(ulong challenge, string code); +</code></pre> + +When you create a new Authenticator, add the instance of your IUsedCodesManager as the first param +<pre><code> +var usedCodeManager = new CustomUsedCodeManager(); +var authenticator = new TwoStepsAuthenticator.TimeAuthenticator(usedCodeManager); +</code></pre> diff --git a/TwoStepsAuthenticator.UnitTests/MockUsedCodesManager.cs b/TwoStepsAuthenticator.UnitTests/MockUsedCodesManager.cs new file mode 100644 index 0000000..cb0065f --- /dev/null +++ b/TwoStepsAuthenticator.UnitTests/MockUsedCodesManager.cs @@ -0,0 +1,16 @@ +namespace TwoStepsAuthenticator.UnitTests +{ + 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; + } + } +}
\ No newline at end of file diff --git a/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs b/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs index 1a1ffc6..55be883 100644 --- a/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs +++ b/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs @@ -4,20 +4,24 @@ using System.Linq; using System.Text; using NUnit.Framework; -namespace TwoStepsAuthenticator.UnitTests { - +namespace TwoStepsAuthenticator.UnitTests +{ + [TestFixture] - public class TimeAuthenticatorTests { + public class TimeAuthenticatorTests + { private MockUsedCodesManager mockUsedCodesManager { get; set; } [SetUp] - public void SetUp() { + public void SetUp() + { this.mockUsedCodesManager = new MockUsedCodesManager(); } [Test] - public void CreateKey() { - var authenticator = new TimeAuthenticator(usedCodeManager: mockUsedCodesManager); + public void CreateKey() + { + var authenticator = new TimeAuthenticator(mockUsedCodesManager); var secret = Authenticator.GenerateKey(); var code = authenticator.GetCode(secret); @@ -25,9 +29,10 @@ namespace TwoStepsAuthenticator.UnitTests { } [Test] - public void Uses_usedCodesManager() { + public void Uses_usedCodesManager() + { var date = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - var authenticator = new TimeAuthenticator(() => date, usedCodeManager: mockUsedCodesManager); + var authenticator = new TimeAuthenticator(mockUsedCodesManager, () => date); var secret = Authenticator.GenerateKey(); var code = authenticator.GetCode(secret); @@ -41,18 +46,20 @@ namespace TwoStepsAuthenticator.UnitTests { [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) { + public void VerifyKeys(string secret, string timeString, string code) + { var date = DateTime.Parse(timeString); - var authenticator = new TimeAuthenticator(() => date, usedCodeManager: mockUsedCodesManager); + var authenticator = new TimeAuthenticator(mockUsedCodesManager, () => date); Assert.IsTrue(authenticator.CheckCode(secret, code)); } [Test] - public void VerifyUsedTime() { + public void VerifyUsedTime() + { var date = DateTime.Parse("2013-12-05 17:23:50 +0100"); - var authenticator = new TimeAuthenticator(() => date, usedCodeManager: mockUsedCodesManager); + var authenticator = new TimeAuthenticator(mockUsedCodesManager, () => date); DateTime usedTime; diff --git a/TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj b/TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj index aa38917..38fce5d 100644 --- a/TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj +++ b/TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj @@ -51,6 +51,7 @@ </ItemGroup> <ItemGroup> <Compile Include="CounterAuthenticatorTests.cs" /> + <Compile Include="MockUsedCodesManager.cs" /> <Compile Include="TimeAuthenticatorTests.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="UsedCodesManagerTests.cs" /> diff --git a/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs b/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs index 1138f54..15ae91e 100644 --- a/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs +++ b/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs @@ -20,18 +20,4 @@ namespace TwoStepsAuthenticator.UnitTests { } } - - 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.sln b/TwoStepsAuthenticator.sln index 6963eba..88c3a3a 100644 --- a/TwoStepsAuthenticator.sln +++ b/TwoStepsAuthenticator.sln @@ -9,6 +9,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwoStepsAuthenticator.TestW EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwoStepsAuthenticator.UnitTests", "TwoStepsAuthenticator.UnitTests\TwoStepsAuthenticator.UnitTests.csproj", "{E936FFA0-2E6E-4CA7-9841-FB844A817E0C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{378161AA-219B-4470-9339-7D5DC803E4E0}" + ProjectSection(SolutionItems) = preProject + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/TwoStepsAuthenticator/CounterAuthenticator.cs b/TwoStepsAuthenticator/CounterAuthenticator.cs index fffae73..7fad752 100644 --- a/TwoStepsAuthenticator/CounterAuthenticator.cs +++ b/TwoStepsAuthenticator/CounterAuthenticator.cs @@ -4,17 +4,21 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace TwoStepsAuthenticator { +namespace TwoStepsAuthenticator +{ /// <summary> /// Implementation of RFC 4226 Counter-Based One-Time Password Algorithm /// </summary> - public class CounterAuthenticator : Authenticator { + public class CounterAuthenticator : Authenticator + { private readonly int WindowSize; private readonly IUsedCodesManager UsedCodeManager; - public CounterAuthenticator(int windowSize = 10, IUsedCodesManager usedCodeManager = null) { - if (windowSize <= 0) { + public CounterAuthenticator(IUsedCodesManager usedCodeManager = null, int windowSize = 10) + { + if (windowSize <= 0) + { throw new ArgumentException("look-ahead window size must be positive"); } @@ -28,7 +32,8 @@ namespace TwoStepsAuthenticator { /// <param name="secret">Shared Secret</param> /// <param name="counter">Current Counter</param> /// <returns>OTP</returns> - public string GetCode(string secret, ulong counter) { + public string GetCode(string secret, ulong counter) + { return GetCodeInternal(secret, counter); } @@ -39,7 +44,8 @@ namespace TwoStepsAuthenticator { /// <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) { + public bool CheckCode(string secret, string code, ulong counter) + { ulong successfulSequenceNumber = 0uL; return CheckCode(secret, code, counter, out successfulSequenceNumber); @@ -53,13 +59,16 @@ namespace TwoStepsAuthenticator { /// <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) { + 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++) { + for (uint i = 0; i <= WindowSize; i++) + { ulong checkCounter = counter + i; - if (ConstantTimeEquals(GetCode(secret, checkCounter), code) && !UsedCodeManager.IsCodeUsed(checkCounter, code)) { + if (ConstantTimeEquals(GetCode(secret, checkCounter), code) && !UsedCodeManager.IsCodeUsed(checkCounter, code)) + { codeMatch = true; successfulSequenceNumber = checkCounter; diff --git a/TwoStepsAuthenticator/TimeAuthenticator.cs b/TwoStepsAuthenticator/TimeAuthenticator.cs index f46b6d0..4966926 100644 --- a/TwoStepsAuthenticator/TimeAuthenticator.cs +++ b/TwoStepsAuthenticator/TimeAuthenticator.cs @@ -14,7 +14,7 @@ namespace TwoStepsAuthenticator { private readonly IUsedCodesManager UsedCodeManager; private readonly int IntervalSeconds; - public TimeAuthenticator(Func<DateTime> nowFunc = null, IUsedCodesManager usedCodeManager = null, int intervalSeconds = 30) { + public TimeAuthenticator(IUsedCodesManager usedCodeManager = null, Func<DateTime> nowFunc = null, int intervalSeconds = 30) { this.NowFunc = (nowFunc == null) ? () => DateTime.Now : nowFunc; this.UsedCodeManager = (usedCodeManager == null) ? Authenticator.DefaultUsedCodeManager.Value : usedCodeManager; this.IntervalSeconds = intervalSeconds; |