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": {} + } +}