summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorKyle Spearrin <kspearrin@imobile3.com>2017-01-13 21:40:51 -0500
committerKyle Spearrin <kspearrin@imobile3.com>2017-01-13 21:40:51 -0500
commiteb78321ca6cf7c5eaee11fee16f4f4d294ba7eb5 (patch)
treee7bd4b9ff3b847e6db2ff5f8ae6f6de71883d4d4 /src
parent354040e8834e80fb64737ff0b2607945530678b3 (diff)
downloadOtp.NET-eb78321ca6cf7c5eaee11fee16f4f4d294ba7eb5.zip
Otp.NET-eb78321ca6cf7c5eaee11fee16f4f4d294ba7eb5.tar.gz
Otp.NET-eb78321ca6cf7c5eaee11fee16f4f4d294ba7eb5.tar.bz2
initial port of otpsharpv1.0.0
Diffstat (limited to 'src')
-rw-r--r--src/Otp.NET/Base32Encoding.cs133
-rw-r--r--src/Otp.NET/KeyGeneration.cs71
-rw-r--r--src/Otp.NET/KeyUtilities.cs81
-rw-r--r--src/Otp.NET/Otp.NET.xproj19
-rw-r--r--src/Otp.NET/Otp.cs175
-rw-r--r--src/Otp.NET/OtpHashMode.cs46
-rw-r--r--src/Otp.NET/Properties/AssemblyInfo.cs19
-rw-r--r--src/Otp.NET/TimeCorrection.cs107
-rw-r--r--src/Otp.NET/Totp.cs203
-rw-r--r--src/Otp.NET/VerificationWindow.cs74
-rw-r--r--src/Otp.NET/project.json14
11 files changed, 942 insertions, 0 deletions
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
+{
+ /// <summary>
+ /// Helpers to work with keys
+ /// </summary>
+ public static class KeyGeneration
+ {
+ /// <summary>
+ /// Generates a random key in accordance with the RFC recommened length for each algorithm
+ /// </summary>
+ /// <param name="length">Key length</param>
+ /// <returns>The generated key</returns>
+ public static byte[] GenerateRandomKey(int length)
+ {
+ byte[] key = new byte[length];
+ using(var rnd = System.Security.Cryptography.RandomNumberGenerator.Create())
+ {
+ rnd.GetBytes(key);
+ return key;
+ }
+ }
+
+ /// <summary>
+ /// Generates a random key in accordance with the RFC recommened length for each algorithm
+ /// </summary>
+ /// <param name="mode">HashMode</param>
+ /// <returns>Key</returns>
+ 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
+{
+ /// <summary>
+ /// Some helper methods to perform common key functions
+ /// </summary>
+ internal class KeyUtilities
+ {
+ /// <summary>
+ /// Overwrite potentially sensitive data with random junk
+ /// </summary>
+ /// <remarks>
+ /// 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
+ /// </remarks>
+ internal static void Destroy(byte[] sensitiveData)
+ {
+ if(sensitiveData == null)
+ throw new ArgumentNullException("sensitiveData");
+ new Random().NextBytes(sensitiveData);
+ }
+
+ /// <summary>
+ /// converts a long into a big endian byte array.
+ /// </summary>
+ /// <remarks>
+ /// RFC 4226 specifies big endian as the method for converting the counter to data to hash.
+ /// </remarks>
+ 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;
+ }
+
+ /// <summary>
+ /// converts an int into a big endian byte array.
+ /// </summary>
+ /// <remarks>
+ /// RFC 4226 specifies big endian as the method for converting the counter to data to hash.
+ /// </remarks>
+ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
+ <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
+ </PropertyGroup>
+ <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
+ <PropertyGroup Label="Globals">
+ <ProjectGuid>e630b67f-150a-4978-a2dd-51b8d8e783ef</ProjectGuid>
+ <RootNamespace>OtpNet</RootNamespace>
+ <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
+ <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
+ <TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
+ </PropertyGroup>
+ <PropertyGroup>
+ <SchemaVersion>2.0</SchemaVersion>
+ </PropertyGroup>
+ <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
+</Project> \ 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
+{
+ /// <summary>
+ /// An abstract class that contains common OTP calculations
+ /// </summary>
+ /// <remarks>
+ /// https://tools.ietf.org/html/rfc4226
+ /// </remarks>
+ public abstract class Otp
+ {
+ /// <summary>
+ /// Secret key
+ /// </summary>
+ protected readonly byte[] secretKey;
+
+ /// <summary>
+ /// The hash mode to use
+ /// </summary>
+ protected readonly OtpHashMode hashMode;
+
+ /// <summary>
+ /// Constructor for the abstract class. This is to guarantee that all implementations have a secret key
+ /// </summary>
+ /// <param name="secretKey"></param>
+ /// <param name="mode">The hash mode to use</param>
+ 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;
+ }
+
+ /// <summary>
+ /// An abstract definition of a compute method. Takes a counter and runs it through the derived algorithm.
+ /// </summary>
+ /// <param name="counter">Counter or step</param>
+ /// <param name="mode">The hash mode to use</param>
+ /// <returns>OTP calculated code</returns>
+ protected abstract string Compute(long counter, OtpHashMode mode);
+
+ /// <summary>
+ /// Helper method that calculates OTPs
+ /// </summary>
+ 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;
+ }
+
+ /// <summary>
+ /// truncates a number down to the specified number of digits
+ /// </summary>
+ protected internal static string Digits(long input, int digitCount)
+ {
+ var truncatedValue = ((int)input % (int)Math.Pow(10, digitCount));
+ return truncatedValue.ToString().PadLeft(digitCount, '0');
+ }
+
+ /// <summary>
+ /// Verify an OTP value
+ /// </summary>
+ /// <param name="initialStep">The initial step to try</param>
+ /// <param name="valueToVerify">The value to verify</param>
+ /// <param name="matchedStep">Output parameter that provides the step where the match was found. If no match was found it will be 0</param>
+ /// <param name="window">The window to verify</param>
+ /// <returns>True if a match is found</returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Uses the key to get an HMAC using the specified algorithm and data
+ /// </summary>
+ /// <param name="mode">The HMAC algorithm to use</param>
+ /// <param name="data">The data used to compute the HMAC</param>
+ /// <returns>HMAC of the key and data</returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Create an HMAC object for the specified algorithm
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// Indicates which HMAC hashing algorithm should be used
+ /// </summary>
+ public enum OtpHashMode
+ {
+ /// <summary>
+ /// Sha1 is used as the HMAC hashing algorithm
+ /// </summary>
+ Sha1,
+ /// <summary>
+ /// Sha256 is used as the HMAC hashing algorithm
+ /// </summary>
+ Sha256,
+ /// <summary>
+ /// Sha512 is used as the HMAC hashing algorithm
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// Class to apply a correction factor to the system time
+ /// </summary>
+ /// <remarks>
+ /// 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
+ /// </remarks>
+ public class TimeCorrection
+ {
+ /// <summary>
+ /// An instance that provides no correction factor
+ /// </summary>
+ public static readonly TimeCorrection UncorrectedInstance = new TimeCorrection();
+
+ private readonly TimeSpan timeCorrectionFactor;
+
+ /// <summary>
+ /// Constructor used solely for the UncorrectedInstance static field to provide an instance without a correction factor.
+ /// </summary>
+ private TimeCorrection()
+ {
+ this.timeCorrectionFactor = TimeSpan.FromSeconds(0);
+ }
+
+ /// <summary>
+ /// Creates a corrected time object by providing the known correct current UTC time. The current system UTC time will be used as the reference
+ /// </summary>
+ /// <remarks>
+ /// This overload assumes UTC. If a base and reference time other than UTC are required then use the other overlaod.
+ /// </remarks>
+ /// <param name="correctUtc">The current correct UTC time</param>
+ public TimeCorrection(DateTime correctUtc)
+ {
+ this.timeCorrectionFactor = DateTime.UtcNow - correctUtc;
+ }
+
+ /// <summary>
+ /// Creates a corrected time object by providing the known correct current time and the current reference time that needs correction
+ /// </summary>
+ /// <param name="correctTime">The current correct time</param>
+ /// <param name="referenceTime">The current reference time (time that will have the correction factor applied in subsequent calls)</param>
+ public TimeCorrection(DateTime correctTime, DateTime referenceTime)
+ {
+ this.timeCorrectionFactor = referenceTime - correctTime;
+ }
+
+ /// <summary>
+ /// Applies the correction factor to the reference time and returns a corrected time
+ /// </summary>
+ /// <param name="referenceTime">The reference time</param>
+ /// <returns>The reference time with the correction factor applied</returns>
+ public DateTime GetCorrectedTime(DateTime referenceTime)
+ {
+ return referenceTime - timeCorrectionFactor;
+ }
+
+ /// <summary>
+ /// Applies the correction factor to the current system UTC time and returns a corrected time
+ /// </summary>
+ public DateTime CorrectedUtcNow
+ {
+ get { return GetCorrectedTime(DateTime.UtcNow); }
+ }
+
+ /// <summary>
+ /// The timespan that is used to calculate a corrected time
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// Calculate Timed-One-Time-Passwords (TOTP) from a secret key
+ /// </summary>
+ /// <remarks>
+ /// The specifications for this are found in RFC 6238
+ /// http://tools.ietf.org/html/rfc6238
+ /// </remarks>
+ public class Totp : Otp
+ {
+ /// <summary>
+ /// The number of ticks as Measured at Midnight Jan 1st 1970;
+ /// </summary>
+ const long unixEpochTicks = 621355968000000000L;
+ /// <summary>
+ /// A divisor for converting ticks to seconds
+ /// </summary>
+ const long ticksToSeconds = 10000000L;
+
+ private readonly int step;
+ private readonly int totpSize;
+ private readonly TimeCorrection correctedTime;
+
+ /// <summary>
+ /// Create a TOTP instance
+ /// </summary>
+ /// <param name="secretKey">The secret key to use in TOTP calculations</param>
+ /// <param name="step">The time window step amount to use in calculating time windows. The default is 30 as recommended in the RFC</param>
+ /// <param name="mode">The hash mode to use</param>
+ /// <param name="totpSize">The number of digits that the returning TOTP should have. The default is 6.</param>
+ /// <param name="timeCorrection">If required, a time correction can be specified to compensate of an out of sync local clock</param>
+ 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");
+ }
+
+ /// <summary>
+ /// Takes a timestamp and applies correction (if provided) and then computes a TOTP value
+ /// </summary>
+ /// <param name="timestamp">The timestamp to use for the TOTP calculation</param>
+ /// <returns>a TOTP value</returns>
+ public string ComputeTotp(DateTime timestamp)
+ {
+ return ComputeTotpFromSpecificTime(this.correctedTime.GetCorrectedTime(timestamp));
+ }
+
+ /// <summary>
+ /// Takes a timestamp and computes a TOTP value for corrected UTC now
+ /// </summary>
+ /// <remarks>
+ /// 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.
+ /// </remarks>
+ /// <returns>a TOTP value</returns>
+ public string ComputeTotp()
+ {
+ return this.ComputeTotpFromSpecificTime(this.correctedTime.CorrectedUtcNow);
+ }
+
+ private string ComputeTotpFromSpecificTime(DateTime timestamp)
+ {
+ var window = CalculateTimeStepFromTimestamp(timestamp);
+ return this.Compute(window, this.hashMode);
+ }
+
+ /// <summary>
+ /// Verify a value that has been provided with the calculated value.
+ /// </summary>
+ /// <remarks>
+ /// 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.
+ /// </remarks>
+ /// <param name="totp">the trial TOTP value</param>
+ /// <param name="timeStepMatched">
+ /// 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
+ /// </param>
+ /// <param name="window">The window of steps to verify</param>
+ /// <returns>True if there is a match.</returns>
+ public bool VerifyTotp(string totp, out long timeStepMatched, VerificationWindow window = null)
+ {
+ return this.VerifyTotpForSpecificTime(this.correctedTime.CorrectedUtcNow, totp, window, out timeStepMatched);
+ }
+
+ /// <summary>
+ /// Verify a value that has been provided with the calculated value
+ /// </summary>
+ /// <param name="timestamp">The timestamp to use</param>
+ /// <param name="totp">the trial TOTP value</param>
+ /// <param name="timeStepMatched">
+ /// 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
+ /// </param>
+ /// <param name="window">The window of steps to verify</param>
+ /// <returns>True if there is a match.</returns>
+ 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);
+ }
+
+ /// <summary>
+ /// Takes a timestamp and calculates a time step
+ /// </summary>
+ private long CalculateTimeStepFromTimestamp(DateTime timestamp)
+ {
+ var unixTimestamp = (timestamp.Ticks - unixEpochTicks) / ticksToSeconds;
+ var window = unixTimestamp / (long)this.step;
+ return window;
+ }
+
+ /// <summary>
+ /// Remaining seconds in current window based on UtcNow
+ /// </summary>
+ /// <remarks>
+ /// 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.
+ /// </remarks>
+ /// <returns>Number of remaining seconds</returns>
+ public int RemainingSeconds()
+ {
+ return RemainingSecondsForSpecificTime(this.correctedTime.CorrectedUtcNow);
+ }
+
+ /// <summary>
+ /// Remaining seconds in current window
+ /// </summary>
+ /// <param name="timestamp">The timestamp</param>
+ /// <returns>Number of remaining seconds</returns>
+ 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);
+ }
+
+ /// <summary>
+ /// Takes a time step and computes a TOTP code
+ /// </summary>
+ /// <param name="counter">time step</param>
+ /// <param name="mode">The hash mode to use</param>
+ /// <returns>TOTP calculated code</returns>
+ 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
+{
+ /// <summary>
+ /// A verification window
+ /// </summary>
+ public class VerificationWindow
+ {
+ private readonly int previous;
+ private readonly int future;
+
+ /// <summary>
+ /// Create an instance of a verification window
+ /// </summary>
+ /// <param name="previous">The number of previous frames to accept</param>
+ /// <param name="future">The number of future frames to accept</param>
+ public VerificationWindow(int previous = 0, int future = 0)
+ {
+ this.previous = previous;
+ this.future = future;
+ }
+
+ /// <summary>
+ /// Gets an enumberable of all the possible validation candidates
+ /// </summary>
+ /// <param name="initialFrame">The initial frame to validate</param>
+ /// <returns>Enumberable of all possible frames that need to be validated</returns>
+ public IEnumerable<long> 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;
+ }
+
+ /// <summary>
+ /// The verification window that accomodates network delay that is recommended in the RFC
+ /// </summary>
+ 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": {}
+ }
+}