summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--LICENSE24
-rw-r--r--README.md53
-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/CounterAuthenticatorTests.cs54
-rw-r--r--TwoStepsAuthenticator.UnitTests/MockUsedCodesManager.cs16
-rw-r--r--TwoStepsAuthenticator.UnitTests/Properties/AssemblyInfo.cs36
-rw-r--r--TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs87
-rw-r--r--TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj76
-rw-r--r--TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs23
-rw-r--r--TwoStepsAuthenticator.UnitTests/packages.config4
-rw-r--r--TwoStepsAuthenticator.sln12
-rw-r--r--TwoStepsAuthenticator/Authenticator.cs77
-rw-r--r--TwoStepsAuthenticator/CounterAuthenticator.cs76
-rw-r--r--TwoStepsAuthenticator/IUsedCodesManager.cs25
-rw-r--r--TwoStepsAuthenticator/TimeAuthenticator.cs90
-rw-r--r--TwoStepsAuthenticator/TwoStepsAuthenticator.csproj3
-rw-r--r--TwoStepsAuthenticator/UsedCodesManager.cs70
-rw-r--r--packages/repositories.config5
20 files changed, 659 insertions, 83 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d0ec6ec
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+
+The MIT License (MIT)
+
+Copyright (c) 2013 Guillaume Lacasa
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+
diff --git a/README.md b/README.md
index db96b7f..efff01a 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,61 @@
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)
+# TOTP
+
+## 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) :
-<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>
+
+## 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>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>
+
+### 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>
+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.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/CounterAuthenticatorTests.cs b/TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs
new file mode 100644
index 0000000..f834178
--- /dev/null
+++ b/TwoStepsAuthenticator.UnitTests/CounterAuthenticatorTests.cs
@@ -0,0 +1,54 @@
+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, 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();
+ var base32Secret = Base32Encoding.ToString(Encoding.ASCII.GetBytes(secret));
+
+ Assert.IsTrue(authenticator.CheckCode(base32Secret, code, counter));
+
+ }
+
+ [Test]
+ public void VerifyUsedCounter() {
+ var authenticator = new CounterAuthenticator();
+
+ // 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/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/Properties/AssemblyInfo.cs b/TwoStepsAuthenticator.UnitTests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..9654c60
--- /dev/null
+++ b/TwoStepsAuthenticator.UnitTests/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("TwoStepsAuthenticator.UnitTests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("TwoStepsAuthenticator.UnitTests")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2013")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("257d7270-7a19-44d8-afdf-b21267fc0dda")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs b/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs
new file mode 100644
index 0000000..99d1957
--- /dev/null
+++ b/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs
@@ -0,0 +1,87 @@
+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(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(mockUsedCodesManager, () => date);
+ var secret = Authenticator.GenerateKey();
+ var code = authenticator.GetCode(secret);
+
+ authenticator.CheckCode(secret, code);
+
+ Assert.AreEqual(mockUsedCodesManager.LastChallenge, 0uL);
+ Assert.AreEqual(mockUsedCodesManager.LastCode, code);
+ }
+
+ [Test]
+ public void Prevent_code_reuse() {
+ var date = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ var usedCodesManager = new UsedCodesManager();
+ var authenticator = new TimeAuthenticator(() => date, usedCodeManager: usedCodesManager);
+ var secret = Authenticator.GenerateKey();
+ var code = authenticator.GetCode(secret);
+
+ Assert.IsTrue(authenticator.CheckCode(secret, code));
+ Assert.IsFalse(authenticator.CheckCode(secret, 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(mockUsedCodesManager, () => date);
+ 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(mockUsedCodesManager, () => date);
+
+ 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
new file mode 100644
index 0000000..38fce5d
--- /dev/null
+++ b/TwoStepsAuthenticator.UnitTests/TwoStepsAuthenticator.UnitTests.csproj
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{E936FFA0-2E6E-4CA7-9841-FB844A817E0C}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>TwoStepsAuthenticator.UnitTests</RootNamespace>
+ <AssemblyName>TwoStepsAuthenticator.UnitTests</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <TargetFrameworkProfile />
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <Prefer32Bit>false</Prefer32Bit>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <Prefer32Bit>false</Prefer32Bit>
+ </PropertyGroup>
+ <PropertyGroup>
+ <StartupObject />
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="nunit.framework">
+ <HintPath>..\packages\NUnit.2.6.3\lib\nunit.framework.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="CounterAuthenticatorTests.cs" />
+ <Compile Include="MockUsedCodesManager.cs" />
+ <Compile Include="TimeAuthenticatorTests.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="UsedCodesManagerTests.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\TwoStepsAuthenticator\TwoStepsAuthenticator.csproj">
+ <Project>{6c898cd1-0bf3-4711-847e-ad7dac815cd8}</Project>
+ <Name>TwoStepsAuthenticator</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs b/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs
new file mode 100644
index 0000000..d72474c
--- /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(42L, "def"));
+ manager.AddCode(42L, "def");
+ Assert.IsTrue(manager.IsCodeUsed(42L, "def"));
+ }
+
+ }
+}
diff --git a/TwoStepsAuthenticator.UnitTests/packages.config b/TwoStepsAuthenticator.UnitTests/packages.config
new file mode 100644
index 0000000..967502d
--- /dev/null
+++ b/TwoStepsAuthenticator.UnitTests/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="NUnit" version="2.6.3" targetFramework="net40" />
+</packages> \ No newline at end of file
diff --git a/TwoStepsAuthenticator.sln b/TwoStepsAuthenticator.sln
index a23c14e..88c3a3a 100644
--- a/TwoStepsAuthenticator.sln
+++ b/TwoStepsAuthenticator.sln
@@ -7,6 +7,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwoStepsAuthenticator.TestA
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwoStepsAuthenticator.TestWebsite", "TwoStepsAuthenticator.TestWebsite\TwoStepsAuthenticator.TestWebsite.csproj", "{38058CE1-D82E-4C9D-B07D-293EAEC66878}"
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
@@ -25,6 +33,10 @@ Global
{38058CE1-D82E-4C9D-B07D-293EAEC66878}.Debug|Any CPU.Build.0 = Debug|Any CPU
{38058CE1-D82E-4C9D-B07D-293EAEC66878}.Release|Any CPU.ActiveCfg = Release|Any CPU
{38058CE1-D82E-4C9D-B07D-293EAEC66878}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E936FFA0-2E6E-4CA7-9841-FB844A817E0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E936FFA0-2E6E-4CA7-9841-FB844A817E0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E936FFA0-2E6E-4CA7-9841-FB844A817E0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E936FFA0-2E6E-4CA7-9841-FB844A817E0C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/TwoStepsAuthenticator/Authenticator.cs b/TwoStepsAuthenticator/Authenticator.cs
index 0f0e298..eea3612 100644
--- a/TwoStepsAuthenticator/Authenticator.cs
+++ b/TwoStepsAuthenticator/Authenticator.cs
@@ -7,54 +7,40 @@ using System.Threading.Tasks;
namespace TwoStepsAuthenticator
{
- public class Authenticator
+ public abstract class Authenticator
{
- private static Lazy<UsedCodesManager> usedCodes = new Lazy<UsedCodesManager>();
-
- public string GenerateKey()
- {
- var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
- var random = new Random();
- var keyChars = new char[16];
- for (int i = 0; i < 16; i++)
- {
- keyChars[i] = chars[random.Next(chars.Length)];
+ private static readonly RNGCryptoServiceProvider Random = new RNGCryptoServiceProvider(); // Is Thread-Safe
+ private static readonly int KeyLength = 16;
+ private static readonly string AvailableKeyChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+
+ public static string GenerateKey() {
+ var keyChars = new char[KeyLength];
+ 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, DateTime.Now);
- }
-
- 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--)
- {
+ 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];
}
@@ -66,25 +52,22 @@ namespace TwoStepsAuthenticator
return code.PadLeft(6, '0');
}
- public bool CheckCode(string secret, string code)
- {
- if (usedCodes.Value.IsCodeUsed(secret, code))
- return false;
-
- var baseTime = DateTime.Now;
- for (int i = -2; i <= 1; i++)
- {
- var checkTime = baseTime.AddSeconds(30 * i);
- if (GetCode(secret, checkTime) == code)
- {
- usedCodes.Value.AddCode(secret, code);
- return true;
- }
+ 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++) {
+ diff |= (uint)a[i] ^ (uint)b[i];
}
- return false;
+ return diff == 0;
}
+ protected static int RandomInt(int max) {
+ var randomBytes = new byte[4];
+ Random.GetBytes(randomBytes);
+
+ return Math.Abs((int)BitConverter.ToUInt32(randomBytes, 0) % max);
+ }
}
diff --git a/TwoStepsAuthenticator/CounterAuthenticator.cs b/TwoStepsAuthenticator/CounterAuthenticator.cs
new file mode 100644
index 0000000..56f1319
--- /dev/null
+++ b/TwoStepsAuthenticator/CounterAuthenticator.cs
@@ -0,0 +1,76 @@
+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, 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)) {
+ codeMatch = true;
+ successfulSequenceNumber = checkCounter;
+ }
+ }
+
+ usedCounter = successfulSequenceNumber;
+ return codeMatch;
+ }
+ }
+}
diff --git a/TwoStepsAuthenticator/IUsedCodesManager.cs b/TwoStepsAuthenticator/IUsedCodesManager.cs
new file mode 100644
index 0000000..8ff1fe3
--- /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(long timestamp, 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(long timestamp, string code);
+ }
+}
diff --git a/TwoStepsAuthenticator/TimeAuthenticator.cs b/TwoStepsAuthenticator/TimeAuthenticator.cs
new file mode 100644
index 0000000..8c42e41
--- /dev/null
+++ b/TwoStepsAuthenticator/TimeAuthenticator.cs
@@ -0,0 +1,90 @@
+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 static readonly Lazy<IUsedCodesManager> DefaultUsedCodeManager = new Lazy<IUsedCodesManager>(() => new UsedCodesManager());
+
+ private readonly Func<DateTime> NowFunc;
+ private readonly IUsedCodesManager UsedCodeManager;
+ private readonly int IntervalSeconds;
+
+ public TimeAuthenticator(IUsedCodesManager usedCodeManager = null, Func<DateTime> nowFunc = null, int intervalSeconds = 30) {
+ this.NowFunc = (nowFunc == null) ? () => DateTime.Now : nowFunc;
+ this.UsedCodeManager = (usedCodeManager == null) ? 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);
+ var checkInterval = 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..13b3d61 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(long timestamp, String code)
{
this.UseDate = DateTime.Now;
- this.Code = secret + code;
+ this.Code = code;
+ this.Timestamp = timestamp;
}
- public DateTime UseDate { get; set; }
- public String Code { get; set; }
+ internal DateTime UseDate { get; private set; }
+ internal long Timestamp { 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.Timestamp.Equals(other.Timestamp) : false;
}
public override string ToString()
{
- return Code;
+ return String.Format("{0}: {1}", Timestamp, Code);
}
public override int GetHashCode()
{
- return Code.GetHashCode();
+ return Code.GetHashCode() + Timestamp.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(long timestamp, String code)
{
- lock (codeLock)
+ try {
+ rwlock.AcquireWriterLock(lockingTimeout);
+
+ codes.Enqueue(new UsedCode(timestamp, code));
+ }
+ finally
{
- codes.Enqueue(new UsedCode(secret, code));
+ rwlock.ReleaseWriterLock();
}
}
- public bool IsCodeUsed(String secret, String code)
+ public bool IsCodeUsed(long timestamp, String code)
{
- lock (codeLock)
+ try
+ {
+ rwlock.AcquireReaderLock(lockingTimeout);
+
+ return codes.Contains(new UsedCode(timestamp, 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