diff --git a/.gitignore b/.gitignore
index f1e3d20..726cc91 100644
--- a/.gitignore
+++ b/.gitignore
@@ -250,3 +250,6 @@ paket-files/
# JetBrains Rider
.idea/
*.sln.iml
+
+.nuget/
+node_modules/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE.txt
similarity index 100%
rename from LICENSE
rename to LICENSE.txt
diff --git a/Otp.NET.nuspec b/Otp.NET.nuspec
new file mode 100644
index 0000000..296f9f1
--- /dev/null
+++ b/Otp.NET.nuspec
@@ -0,0 +1,16 @@
+
+
+ Otp.NET
+ 1.0.0
+ Kyle Spearrin
+ https://github.com/kspearrin/Otp.NET
+ https://raw.githubusercontent.com/kspearrin/Otp.NET/master/LICENSE.txt
+ Otp.NET
+ An implementation of TOTP which is commonly used for multi factor authentication by using a shared key between the client and the server to generate and verify one time use codes. For documentation, visit https://github.com/kspearrin/BerTlv.NET
+ otp totp 2fa
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Otp.NET.sln b/Otp.NET.sln
new file mode 100644
index 0000000..f5557d1
--- /dev/null
+++ b/Otp.NET.sln
@@ -0,0 +1,32 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.25420.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{57F82DBE-510A-4E78-ADCD-7A18DB80AA87}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E5579496-CD66-4961-8DD1-A53BA74229E3}"
+ ProjectSection(SolutionItems) = preProject
+ global.json = global.json
+ EndProjectSection
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Otp.NET", "src\Otp.NET\Otp.NET.xproj", "{E630B67F-150A-4978-A2DD-51B8D8E783EF}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {E630B67F-150A-4978-A2DD-51B8D8E783EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E630B67F-150A-4978-A2DD-51B8D8E783EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E630B67F-150A-4978-A2DD-51B8D8E783EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E630B67F-150A-4978-A2DD-51B8D8E783EF}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {E630B67F-150A-4978-A2DD-51B8D8E783EF} = {57F82DBE-510A-4E78-ADCD-7A18DB80AA87}
+ EndGlobalSection
+EndGlobal
diff --git a/build.cmd b/build.cmd
new file mode 100644
index 0000000..25cb3b5
--- /dev/null
+++ b/build.cmd
@@ -0,0 +1,19 @@
+@echo off
+cd %~dp0
+
+SETLOCAL
+SET CACHED_NUGET=%LocalAppData%\NuGet\NuGet.exe
+
+IF EXIST %CACHED_NUGET% goto copynuget
+IF NOT EXIST %LocalAppData%\NuGet md %LocalAppData%\NuGet
+@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://www.nuget.org/nuget.exe' -OutFile '%CACHED_NUGET%'"
+
+:copynuget
+IF EXIST .nuget\nuget.exe goto build
+md .nuget
+copy %CACHED_NUGET% .nuget\nuget.exe > nul
+
+:build
+call npm install -g gulp
+call npm install
+call gulp
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..9d09ab5
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "projects": [ "src", "test" ],
+ "sdk": {
+ "version": "1.0.0-preview2-003131"
+ }
+}
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000..b2400b0
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,52 @@
+var p = require('./package.json'),
+ gulp = require('gulp'),
+ assemblyInfo = require('gulp-dotnet-assembly-info'),
+ xmlpoke = require('gulp-xmlpoke'),
+ msbuild = require('gulp-msbuild'),
+ nuget = require('nuget-runner')({
+ apiKey: process.env.NUGET_API_KEY,
+ nugetPath: '.nuget/nuget.exe'
+ });
+
+gulp.task('default', ['nuget']);
+
+gulp.task('restore', [], function () {
+ return nuget
+ .restore({
+ packages: 'Otp.NET.sln',
+ verbosity: 'normal'
+ });
+});
+
+gulp.task('build', ['restore'], function () {
+ return gulp
+ .src('Otp.NET.sln')
+ .pipe(msbuild({
+ toolsVersion: 14.0,
+ targets: ['Clean', 'Build'],
+ errorOnFail: true,
+ configuration: 'Release'
+ }));
+});
+
+gulp.task('nuspec', ['build'], function () {
+ return gulp
+ .src('Otp.NET.nuspec')
+ .pipe(xmlpoke({
+ replacements: [{
+ xpath: "//package:version",
+ namespaces: { "package": "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd" },
+ value: p.version
+ }]
+ }))
+ .pipe(gulp.dest('.'));
+});
+
+gulp.task('nuget', ['nuspec'], function () {
+ return nuget
+ .pack({
+ spec: 'Otp.NET.nuspec',
+ outputDirectory: 'src/Otp.NET/bin/Release',
+ version: p.version
+ });
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..a6d3b25
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "otpnet",
+ "version": "1.0.0",
+ "description": "An implementation of TOTP which is commonly used for multi factor authentication by using a shared key between the client and the server to generate and verify one time use codes.",
+ "homepage": "https://github.com/kspearrin/Otp.NET",
+ "author": "Kyle Spearrin",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/kspearrin/Otp.NET"
+ },
+ "dependencies": {
+ "gulp": "^3.9.0",
+ "gulp-dotnet-assembly-info": "^0.1.10",
+ "gulp-msbuild": "^0.2.11",
+ "gulp-xmlpoke": "^0.2.0",
+ "nuget-runner": "^0.1.5"
+ }
+}
diff --git a/src/Otp.NET/Base32Encoding.cs b/src/Otp.NET/Base32Encoding.cs
new file mode 100644
index 0000000..aafff9b
--- /dev/null
+++ b/src/Otp.NET/Base32Encoding.cs
@@ -0,0 +1,133 @@
+/*
+Credits to "Shane" from SO answer here:
+http://stackoverflow.com/a/7135008/1090359
+*/
+
+using System;
+
+namespace OtpNet
+{
+ public class Base32Encoding
+ {
+ public static byte[] ToBytes(string input)
+ {
+ if(string.IsNullOrEmpty(input))
+ {
+ throw new ArgumentNullException("input");
+ }
+
+ input = input.TrimEnd('='); //remove padding characters
+ int byteCount = input.Length * 5 / 8; //this must be TRUNCATED
+ byte[] returnArray = new byte[byteCount];
+
+ byte curByte = 0, bitsRemaining = 8;
+ int mask = 0, arrayIndex = 0;
+
+ foreach(char c in input)
+ {
+ int cValue = CharToValue(c);
+
+ if(bitsRemaining > 5)
+ {
+ mask = cValue << (bitsRemaining - 5);
+ curByte = (byte)(curByte | mask);
+ bitsRemaining -= 5;
+ }
+ else
+ {
+ mask = cValue >> (5 - bitsRemaining);
+ curByte = (byte)(curByte | mask);
+ returnArray[arrayIndex++] = curByte;
+ curByte = (byte)(cValue << (3 + bitsRemaining));
+ bitsRemaining += 3;
+ }
+ }
+
+ //if we didn't end with a full byte
+ if(arrayIndex != byteCount)
+ {
+ returnArray[arrayIndex] = curByte;
+ }
+
+ return returnArray;
+ }
+
+ public static string ToString(byte[] input)
+ {
+ if(input == null || input.Length == 0)
+ {
+ throw new ArgumentNullException("input");
+ }
+
+ int charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
+ char[] returnArray = new char[charCount];
+
+ byte nextChar = 0, bitsRemaining = 5;
+ int arrayIndex = 0;
+
+ foreach(byte b in input)
+ {
+ nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
+ returnArray[arrayIndex++] = ValueToChar(nextChar);
+
+ if(bitsRemaining < 4)
+ {
+ nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
+ returnArray[arrayIndex++] = ValueToChar(nextChar);
+ bitsRemaining += 5;
+ }
+
+ bitsRemaining -= 3;
+ nextChar = (byte)((b << bitsRemaining) & 31);
+ }
+
+ //if we didn't end with a full char
+ if(arrayIndex != charCount)
+ {
+ returnArray[arrayIndex++] = ValueToChar(nextChar);
+ while(arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding
+ }
+
+ return new string(returnArray);
+ }
+
+ private static int CharToValue(char c)
+ {
+ int value = (int)c;
+
+ //65-90 == uppercase letters
+ if(value < 91 && value > 64)
+ {
+ return value - 65;
+ }
+ //50-55 == numbers 2-7
+ if(value < 56 && value > 49)
+ {
+ return value - 24;
+ }
+ //97-122 == lowercase letters
+ if(value < 123 && value > 96)
+ {
+ return value - 97;
+ }
+
+ throw new ArgumentException("Character is not a Base32 character.", "c");
+ }
+
+ private static char ValueToChar(byte b)
+ {
+ if(b < 26)
+ {
+ return (char)(b + 65);
+ }
+
+ if(b < 32)
+ {
+ return (char)(b + 24);
+ }
+
+ throw new ArgumentException("Byte is not a value Base32 value.", "b");
+ }
+
+ }
+}
diff --git a/src/Otp.NET/KeyGeneration.cs b/src/Otp.NET/KeyGeneration.cs
new file mode 100644
index 0000000..7e5b613
--- /dev/null
+++ b/src/Otp.NET/KeyGeneration.cs
@@ -0,0 +1,71 @@
+/*
+Credits to Devin Martin and the original OtpSharp library:
+https://bitbucket.org/devinmartin/otp-sharp/overview
+
+Copyright (C) 2012 Devin Martin
+
+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.
+*/
+
+namespace OtpNet
+{
+ ///
+ /// Helpers to work with keys
+ ///
+ public static class KeyGeneration
+ {
+ ///
+ /// Generates a random key in accordance with the RFC recommened length for each algorithm
+ ///
+ /// Key length
+ /// The generated key
+ public static byte[] GenerateRandomKey(int length)
+ {
+ byte[] key = new byte[length];
+ using(var rnd = System.Security.Cryptography.RandomNumberGenerator.Create())
+ {
+ rnd.GetBytes(key);
+ return key;
+ }
+ }
+
+ ///
+ /// Generates a random key in accordance with the RFC recommened length for each algorithm
+ ///
+ /// HashMode
+ /// Key
+ public static byte[] GenerateRandomKey(OtpHashMode mode = OtpHashMode.Sha1)
+ {
+ return GenerateRandomKey(LengthForMode(mode));
+ }
+
+ private static int LengthForMode(OtpHashMode mode)
+ {
+ switch(mode)
+ {
+ case OtpHashMode.Sha256:
+ return 32;
+ case OtpHashMode.Sha512:
+ return 64;
+ default: //case OtpHashMode.Sha1:
+ return 20;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Otp.NET/KeyUtilities.cs b/src/Otp.NET/KeyUtilities.cs
new file mode 100644
index 0000000..f81dfb3
--- /dev/null
+++ b/src/Otp.NET/KeyUtilities.cs
@@ -0,0 +1,81 @@
+/*
+Credits to Devin Martin and the original OtpSharp library:
+https://bitbucket.org/devinmartin/otp-sharp/overview
+
+Copyright (C) 2012 Devin Martin
+
+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.
+*/
+
+using System;
+
+namespace OtpNet
+{
+ ///
+ /// Some helper methods to perform common key functions
+ ///
+ internal class KeyUtilities
+ {
+ ///
+ /// Overwrite potentially sensitive data with random junk
+ ///
+ ///
+ /// Warning!
+ ///
+ /// This isn't foolproof by any means. The garbage collector could have moved the actual
+ /// location in memory to another location during a collection cycle and left the old data in place
+ /// simply marking it as available. We can't control this or even detect it.
+ /// This method is simply a good faith effort to limit the exposure of sensitive data in memory as much as possible
+ ///
+ internal static void Destroy(byte[] sensitiveData)
+ {
+ if(sensitiveData == null)
+ throw new ArgumentNullException("sensitiveData");
+ new Random().NextBytes(sensitiveData);
+ }
+
+ ///
+ /// converts a long into a big endian byte array.
+ ///
+ ///
+ /// RFC 4226 specifies big endian as the method for converting the counter to data to hash.
+ ///
+ static internal byte[] GetBigEndianBytes(long input)
+ {
+ // Since .net uses little endian numbers, we need to reverse the byte order to get big endian.
+ var data = BitConverter.GetBytes(input);
+ Array.Reverse(data);
+ return data;
+ }
+
+ ///
+ /// converts an int into a big endian byte array.
+ ///
+ ///
+ /// RFC 4226 specifies big endian as the method for converting the counter to data to hash.
+ ///
+ static internal byte[] GetBigEndianBytes(int input)
+ {
+ // Since .net uses little endian numbers, we need to reverse the byte order to get big endian.
+ var data = BitConverter.GetBytes(input);
+ Array.Reverse(data);
+ return data;
+ }
+ }
+}
diff --git a/src/Otp.NET/Otp.NET.xproj b/src/Otp.NET/Otp.NET.xproj
new file mode 100644
index 0000000..5197c3e
--- /dev/null
+++ b/src/Otp.NET/Otp.NET.xproj
@@ -0,0 +1,19 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ e630b67f-150a-4978-a2dd-51b8d8e783ef
+ OtpNet
+ .\obj
+ .\bin\
+ v4.6
+
+
+ 2.0
+
+
+
\ No newline at end of file
diff --git a/src/Otp.NET/Otp.cs b/src/Otp.NET/Otp.cs
new file mode 100644
index 0000000..513e7f5
--- /dev/null
+++ b/src/Otp.NET/Otp.cs
@@ -0,0 +1,175 @@
+/*
+Credits to Devin Martin and the original OtpSharp library:
+https://bitbucket.org/devinmartin/otp-sharp/overview
+
+Copyright (C) 2012 Devin Martin
+
+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.
+*/
+
+using System;
+using System.Security.Cryptography;
+
+namespace OtpNet
+{
+ ///
+ /// An abstract class that contains common OTP calculations
+ ///
+ ///
+ /// https://tools.ietf.org/html/rfc4226
+ ///
+ public abstract class Otp
+ {
+ ///
+ /// Secret key
+ ///
+ protected readonly byte[] secretKey;
+
+ ///
+ /// The hash mode to use
+ ///
+ protected readonly OtpHashMode hashMode;
+
+ ///
+ /// Constructor for the abstract class. This is to guarantee that all implementations have a secret key
+ ///
+ ///
+ /// The hash mode to use
+ public Otp(byte[] secretKey, OtpHashMode mode)
+ {
+ if(!(secretKey != null))
+ throw new ArgumentNullException("secretKey");
+ if(!(secretKey.Length > 0))
+ throw new ArgumentException("secretKey empty");
+
+ // when passing a key into the constructor the caller may depend on the reference to the key remaining intact.
+ this.secretKey = secretKey;
+
+ this.hashMode = mode;
+ }
+
+ ///
+ /// An abstract definition of a compute method. Takes a counter and runs it through the derived algorithm.
+ ///
+ /// Counter or step
+ /// The hash mode to use
+ /// OTP calculated code
+ protected abstract string Compute(long counter, OtpHashMode mode);
+
+ ///
+ /// Helper method that calculates OTPs
+ ///
+ protected internal long CalculateOtp(byte[] data, OtpHashMode mode)
+ {
+ byte[] hmacComputedHash = ComputeHmac(mode, data);
+
+ // The RFC has a hard coded index 19 in this value.
+ // This is the same thing but also accomodates SHA256 and SHA512
+ // hmacComputedHash[19] => hmacComputedHash[hmacComputedHash.Length - 1]
+
+ int offset = hmacComputedHash[hmacComputedHash.Length - 1] & 0x0F;
+ return (hmacComputedHash[offset] & 0x7f) << 24
+ | (hmacComputedHash[offset + 1] & 0xff) << 16
+ | (hmacComputedHash[offset + 2] & 0xff) << 8
+ | (hmacComputedHash[offset + 3] & 0xff) % 1000000;
+ }
+
+ ///
+ /// truncates a number down to the specified number of digits
+ ///
+ protected internal static string Digits(long input, int digitCount)
+ {
+ var truncatedValue = ((int)input % (int)Math.Pow(10, digitCount));
+ return truncatedValue.ToString().PadLeft(digitCount, '0');
+ }
+
+ ///
+ /// Verify an OTP value
+ ///
+ /// The initial step to try
+ /// The value to verify
+ /// Output parameter that provides the step where the match was found. If no match was found it will be 0
+ /// The window to verify
+ /// True if a match is found
+ protected bool Verify(long initialStep, string valueToVerify, out long matchedStep, VerificationWindow window)
+ {
+ if(window == null)
+ window = new VerificationWindow();
+ foreach(var frame in window.ValidationCandidates(initialStep))
+ {
+ var comparisonValue = this.Compute(frame, this.hashMode);
+ if(comparisonValue == valueToVerify)
+ {
+ matchedStep = frame;
+ return true;
+ }
+ }
+
+ matchedStep = 0;
+ return false;
+ }
+
+ ///
+ /// Uses the key to get an HMAC using the specified algorithm and data
+ ///
+ /// The HMAC algorithm to use
+ /// The data used to compute the HMAC
+ /// HMAC of the key and data
+ private byte[] ComputeHmac(OtpHashMode mode, byte[] data)
+ {
+ byte[] hashedValue = null;
+ using(HMAC hmac = CreateHmacHash(mode))
+ {
+ try
+ {
+ hmac.Key = this.secretKey;
+ hashedValue = hmac.ComputeHash(data);
+ }
+ finally
+ {
+ KeyUtilities.Destroy(this.secretKey);
+ }
+ }
+
+ return hashedValue;
+ }
+
+ ///
+ /// Create an HMAC object for the specified algorithm
+ ///
+ private static HMAC CreateHmacHash(OtpHashMode otpHashMode)
+ {
+ HMAC hmacAlgorithm = null;
+ switch(otpHashMode)
+ {
+ case OtpHashMode.Sha256:
+ hmacAlgorithm = new HMACSHA256();
+ break;
+ case OtpHashMode.Sha512:
+ hmacAlgorithm = new HMACSHA512();
+ break;
+ default: //case OtpHashMode.Sha1:
+ hmacAlgorithm = new HMACSHA1();
+ break;
+ }
+ return hmacAlgorithm;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Otp.NET/OtpHashMode.cs b/src/Otp.NET/OtpHashMode.cs
new file mode 100644
index 0000000..a613ff5
--- /dev/null
+++ b/src/Otp.NET/OtpHashMode.cs
@@ -0,0 +1,46 @@
+/*
+Credits to Devin Martin and the original OtpSharp library:
+https://bitbucket.org/devinmartin/otp-sharp/overview
+
+Copyright (C) 2012 Devin Martin
+
+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.
+*/
+
+namespace OtpNet
+{
+ ///
+ /// Indicates which HMAC hashing algorithm should be used
+ ///
+ public enum OtpHashMode
+ {
+ ///
+ /// Sha1 is used as the HMAC hashing algorithm
+ ///
+ Sha1,
+ ///
+ /// Sha256 is used as the HMAC hashing algorithm
+ ///
+ Sha256,
+ ///
+ /// Sha512 is used as the HMAC hashing algorithm
+ ///
+ Sha512
+ }
+}
diff --git a/src/Otp.NET/Properties/AssemblyInfo.cs b/src/Otp.NET/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..d6dbf19
--- /dev/null
+++ b/src/Otp.NET/Properties/AssemblyInfo.cs
@@ -0,0 +1,19 @@
+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: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Otp.NET")]
+[assembly: AssemblyTrademark("")]
+
+// 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("e630b67f-150a-4978-a2dd-51b8d8e783ef")]
diff --git a/src/Otp.NET/TimeCorrection.cs b/src/Otp.NET/TimeCorrection.cs
new file mode 100644
index 0000000..9654080
--- /dev/null
+++ b/src/Otp.NET/TimeCorrection.cs
@@ -0,0 +1,107 @@
+/*
+Credits to Devin Martin and the original OtpSharp library:
+https://bitbucket.org/devinmartin/otp-sharp/overview
+
+Copyright (C) 2012 Devin Martin
+
+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.
+*/
+
+using System;
+
+namespace OtpNet
+{
+ ///
+ /// Class to apply a correction factor to the system time
+ ///
+ ///
+ /// In cases where the local system time is incorrect it is preferable to simply correct the system time.
+ /// This class is provided to handle cases where it isn't possible for the client, the server, or both, to be on the correct time.
+ ///
+ /// This library provides limited facilities to to ping NIST for a correct network time. This class can be used manually however in cases where a server's time is off
+ /// and the consumer of this library can't control it. In that case create an instance of this class and provide the current server time as the correct time parameter
+ ///
+ /// This class is immutable and therefore threadsafe
+ ///
+ public class TimeCorrection
+ {
+ ///
+ /// An instance that provides no correction factor
+ ///
+ public static readonly TimeCorrection UncorrectedInstance = new TimeCorrection();
+
+ private readonly TimeSpan timeCorrectionFactor;
+
+ ///
+ /// Constructor used solely for the UncorrectedInstance static field to provide an instance without a correction factor.
+ ///
+ private TimeCorrection()
+ {
+ this.timeCorrectionFactor = TimeSpan.FromSeconds(0);
+ }
+
+ ///
+ /// Creates a corrected time object by providing the known correct current UTC time. The current system UTC time will be used as the reference
+ ///
+ ///
+ /// This overload assumes UTC. If a base and reference time other than UTC are required then use the other overlaod.
+ ///
+ /// The current correct UTC time
+ public TimeCorrection(DateTime correctUtc)
+ {
+ this.timeCorrectionFactor = DateTime.UtcNow - correctUtc;
+ }
+
+ ///
+ /// Creates a corrected time object by providing the known correct current time and the current reference time that needs correction
+ ///
+ /// The current correct time
+ /// The current reference time (time that will have the correction factor applied in subsequent calls)
+ public TimeCorrection(DateTime correctTime, DateTime referenceTime)
+ {
+ this.timeCorrectionFactor = referenceTime - correctTime;
+ }
+
+ ///
+ /// Applies the correction factor to the reference time and returns a corrected time
+ ///
+ /// The reference time
+ /// The reference time with the correction factor applied
+ public DateTime GetCorrectedTime(DateTime referenceTime)
+ {
+ return referenceTime - timeCorrectionFactor;
+ }
+
+ ///
+ /// Applies the correction factor to the current system UTC time and returns a corrected time
+ ///
+ public DateTime CorrectedUtcNow
+ {
+ get { return GetCorrectedTime(DateTime.UtcNow); }
+ }
+
+ ///
+ /// The timespan that is used to calculate a corrected time
+ ///
+ public TimeSpan CorrectionFactor
+ {
+ get { return this.timeCorrectionFactor; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Otp.NET/Totp.cs b/src/Otp.NET/Totp.cs
new file mode 100644
index 0000000..ffcbafd
--- /dev/null
+++ b/src/Otp.NET/Totp.cs
@@ -0,0 +1,203 @@
+/*
+Credits to Devin Martin and the original OtpSharp library:
+https://bitbucket.org/devinmartin/otp-sharp/overview
+
+Copyright (C) 2012 Devin Martin
+
+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.
+*/
+
+using System;
+using System.Globalization;
+
+namespace OtpNet
+{
+ ///
+ /// Calculate Timed-One-Time-Passwords (TOTP) from a secret key
+ ///
+ ///
+ /// The specifications for this are found in RFC 6238
+ /// http://tools.ietf.org/html/rfc6238
+ ///
+ public class Totp : Otp
+ {
+ ///
+ /// The number of ticks as Measured at Midnight Jan 1st 1970;
+ ///
+ const long unixEpochTicks = 621355968000000000L;
+ ///
+ /// A divisor for converting ticks to seconds
+ ///
+ const long ticksToSeconds = 10000000L;
+
+ private readonly int step;
+ private readonly int totpSize;
+ private readonly TimeCorrection correctedTime;
+
+ ///
+ /// Create a TOTP instance
+ ///
+ /// The secret key to use in TOTP calculations
+ /// The time window step amount to use in calculating time windows. The default is 30 as recommended in the RFC
+ /// The hash mode to use
+ /// The number of digits that the returning TOTP should have. The default is 6.
+ /// If required, a time correction can be specified to compensate of an out of sync local clock
+ public Totp(byte[] secretKey, int step = 30, OtpHashMode mode = OtpHashMode.Sha1, int totpSize = 6, TimeCorrection timeCorrection = null)
+ : base(secretKey, mode)
+ {
+ VerifyParameters(step, totpSize);
+
+ this.step = step;
+ this.totpSize = totpSize;
+
+ // we never null check the corrected time object. Since it's readonly, we'll ensure that it isn't null here and provide neatral functionality in this case.
+ this.correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance;
+ }
+
+ private static void VerifyParameters(int step, int totpSize)
+ {
+ if(!(step > 0))
+ throw new ArgumentOutOfRangeException("step");
+ if(!(totpSize > 0))
+ throw new ArgumentOutOfRangeException("totpSize");
+ if(!(totpSize <= 10))
+ throw new ArgumentOutOfRangeException("totpSize");
+ }
+
+ ///
+ /// Takes a timestamp and applies correction (if provided) and then computes a TOTP value
+ ///
+ /// The timestamp to use for the TOTP calculation
+ /// a TOTP value
+ public string ComputeTotp(DateTime timestamp)
+ {
+ return ComputeTotpFromSpecificTime(this.correctedTime.GetCorrectedTime(timestamp));
+ }
+
+ ///
+ /// Takes a timestamp and computes a TOTP value for corrected UTC now
+ ///
+ ///
+ /// It will be corrected against a corrected UTC time using the provided time correction. If none was provided then simply the current UTC will be used.
+ ///
+ /// a TOTP value
+ public string ComputeTotp()
+ {
+ return this.ComputeTotpFromSpecificTime(this.correctedTime.CorrectedUtcNow);
+ }
+
+ private string ComputeTotpFromSpecificTime(DateTime timestamp)
+ {
+ var window = CalculateTimeStepFromTimestamp(timestamp);
+ return this.Compute(window, this.hashMode);
+ }
+
+ ///
+ /// Verify a value that has been provided with the calculated value.
+ ///
+ ///
+ /// It will be corrected against a corrected UTC time using the provided time correction. If none was provided then simply the current UTC will be used.
+ ///
+ /// the trial TOTP value
+ ///
+ /// This is an output parameter that gives that time step that was used to find a match.
+ /// This is useful in cases where a TOTP value should only be used once. This value is a unique identifier of the
+ /// time step (not the value) that can be used to prevent the same step from being used multiple times
+ ///
+ /// The window of steps to verify
+ /// True if there is a match.
+ public bool VerifyTotp(string totp, out long timeStepMatched, VerificationWindow window = null)
+ {
+ return this.VerifyTotpForSpecificTime(this.correctedTime.CorrectedUtcNow, totp, window, out timeStepMatched);
+ }
+
+ ///
+ /// Verify a value that has been provided with the calculated value
+ ///
+ /// The timestamp to use
+ /// the trial TOTP value
+ ///
+ /// This is an output parameter that gives that time step that was used to find a match.
+ /// This is usefule in cases where a TOTP value should only be used once. This value is a unique identifier of the
+ /// time step (not the value) that can be used to prevent the same step from being used multiple times
+ ///
+ /// The window of steps to verify
+ /// True if there is a match.
+ public bool VerifyTotp(DateTime timestamp, string totp, out long timeStepMatched, VerificationWindow window = null)
+ {
+ return this.VerifyTotpForSpecificTime(this.correctedTime.GetCorrectedTime(timestamp), totp, window, out timeStepMatched);
+ }
+
+ private bool VerifyTotpForSpecificTime(DateTime timestamp, string totp, VerificationWindow window, out long timeStepMatched)
+ {
+ var initialStep = CalculateTimeStepFromTimestamp(timestamp);
+ return this.Verify(initialStep, totp, out timeStepMatched, window);
+ }
+
+ ///
+ /// Takes a timestamp and calculates a time step
+ ///
+ private long CalculateTimeStepFromTimestamp(DateTime timestamp)
+ {
+ var unixTimestamp = (timestamp.Ticks - unixEpochTicks) / ticksToSeconds;
+ var window = unixTimestamp / (long)this.step;
+ return window;
+ }
+
+ ///
+ /// Remaining seconds in current window based on UtcNow
+ ///
+ ///
+ /// It will be corrected against a corrected UTC time using the provided time correction. If none was provided then simply the current UTC will be used.
+ ///
+ /// Number of remaining seconds
+ public int RemainingSeconds()
+ {
+ return RemainingSecondsForSpecificTime(this.correctedTime.CorrectedUtcNow);
+ }
+
+ ///
+ /// Remaining seconds in current window
+ ///
+ /// The timestamp
+ /// Number of remaining seconds
+ public int RemainingSeconds(DateTime timestamp)
+ {
+ return RemainingSecondsForSpecificTime(this.correctedTime.GetCorrectedTime(timestamp));
+ }
+
+ private int RemainingSecondsForSpecificTime(DateTime timestamp)
+ {
+ return this.step - (int)(((timestamp.Ticks - unixEpochTicks) / ticksToSeconds) % this.step);
+ }
+
+ ///
+ /// Takes a time step and computes a TOTP code
+ ///
+ /// time step
+ /// The hash mode to use
+ /// TOTP calculated code
+ protected override string Compute(long counter, OtpHashMode mode)
+ {
+ var data = KeyUtilities.GetBigEndianBytes(counter);
+ var otp = this.CalculateOtp(data, mode);
+ return Digits(otp, this.totpSize);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Otp.NET/VerificationWindow.cs b/src/Otp.NET/VerificationWindow.cs
new file mode 100644
index 0000000..d3d8591
--- /dev/null
+++ b/src/Otp.NET/VerificationWindow.cs
@@ -0,0 +1,74 @@
+/*
+Credits to Devin Martin and the original OtpSharp library:
+https://bitbucket.org/devinmartin/otp-sharp/overview
+
+Copyright (C) 2012 Devin Martin
+
+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.
+*/
+
+using System.Collections.Generic;
+
+namespace OtpNet
+{
+ ///
+ /// A verification window
+ ///
+ public class VerificationWindow
+ {
+ private readonly int previous;
+ private readonly int future;
+
+ ///
+ /// Create an instance of a verification window
+ ///
+ /// The number of previous frames to accept
+ /// The number of future frames to accept
+ public VerificationWindow(int previous = 0, int future = 0)
+ {
+ this.previous = previous;
+ this.future = future;
+ }
+
+ ///
+ /// Gets an enumberable of all the possible validation candidates
+ ///
+ /// The initial frame to validate
+ /// Enumberable of all possible frames that need to be validated
+ public IEnumerable ValidationCandidates(long initialFrame)
+ {
+ yield return initialFrame;
+ for(int i = 1; i <= previous; i++)
+ {
+ var val = initialFrame - i;
+ if(val < 0)
+ break;
+ yield return val;
+ }
+
+ for(int i = 1; i <= future; i++)
+ yield return initialFrame + i;
+ }
+
+ ///
+ /// The verification window that accomodates network delay that is recommended in the RFC
+ ///
+ public static readonly VerificationWindow RfcSpecifiedNetworkDelay = new VerificationWindow(previous: 1, future: 1);
+ }
+}
diff --git a/src/Otp.NET/project.json b/src/Otp.NET/project.json
new file mode 100644
index 0000000..881e381
--- /dev/null
+++ b/src/Otp.NET/project.json
@@ -0,0 +1,14 @@
+{
+ "version": "1.0.0-*",
+
+ "dependencies": {
+ "NETStandard.Library": "1.6.1"
+ },
+
+ "frameworks": {
+ "netstandard1.3": {
+ "imports": "dnxcore50"
+ },
+ "net45": {}
+ }
+}