diff options
author | Kyle Spearrin <kspearrin@imobile3.com> | 2017-01-13 21:40:51 -0500 |
---|---|---|
committer | Kyle Spearrin <kspearrin@imobile3.com> | 2017-01-13 21:40:51 -0500 |
commit | eb78321ca6cf7c5eaee11fee16f4f4d294ba7eb5 (patch) | |
tree | e7bd4b9ff3b847e6db2ff5f8ae6f6de71883d4d4 | |
parent | 354040e8834e80fb64737ff0b2607945530678b3 (diff) | |
download | Otp.NET-eb78321ca6cf7c5eaee11fee16f4f4d294ba7eb5.zip Otp.NET-eb78321ca6cf7c5eaee11fee16f4f4d294ba7eb5.tar.gz Otp.NET-eb78321ca6cf7c5eaee11fee16f4f4d294ba7eb5.tar.bz2 |
initial port of otpsharpv1.0.0
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | LICENSE.txt (renamed from LICENSE) | 0 | ||||
-rw-r--r-- | Otp.NET.nuspec | 16 | ||||
-rw-r--r-- | Otp.NET.sln | 32 | ||||
-rw-r--r-- | build.cmd | 19 | ||||
-rw-r--r-- | global.json | 6 | ||||
-rw-r--r-- | gulpfile.js | 52 | ||||
-rw-r--r-- | package.json | 19 | ||||
-rw-r--r-- | src/Otp.NET/Base32Encoding.cs | 133 | ||||
-rw-r--r-- | src/Otp.NET/KeyGeneration.cs | 71 | ||||
-rw-r--r-- | src/Otp.NET/KeyUtilities.cs | 81 | ||||
-rw-r--r-- | src/Otp.NET/Otp.NET.xproj | 19 | ||||
-rw-r--r-- | src/Otp.NET/Otp.cs | 175 | ||||
-rw-r--r-- | src/Otp.NET/OtpHashMode.cs | 46 | ||||
-rw-r--r-- | src/Otp.NET/Properties/AssemblyInfo.cs | 19 | ||||
-rw-r--r-- | src/Otp.NET/TimeCorrection.cs | 107 | ||||
-rw-r--r-- | src/Otp.NET/Totp.cs | 203 | ||||
-rw-r--r-- | src/Otp.NET/VerificationWindow.cs | 74 | ||||
-rw-r--r-- | src/Otp.NET/project.json | 14 |
19 files changed, 1089 insertions, 0 deletions
@@ -250,3 +250,6 @@ paket-files/ # JetBrains Rider .idea/ *.sln.iml + +.nuget/ +node_modules/
\ No newline at end of file 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 @@ +<?xml version="1.0" encoding="utf-8"?><package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> + <metadata> + <id>Otp.NET</id> + <version>1.0.0</version> + <authors>Kyle Spearrin</authors> + <projectUrl>https://github.com/kspearrin/Otp.NET</projectUrl> + <licenseUrl>https://raw.githubusercontent.com/kspearrin/Otp.NET/master/LICENSE.txt</licenseUrl> + <title>Otp.NET</title> + <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. For documentation, visit https://github.com/kspearrin/BerTlv.NET</description> + <tags>otp totp 2fa</tags> + </metadata> + <files> + <file src="src\Otp.NET\bin\Release\net45\Otp.NET.dll" target="lib\net45"/> + <file src="src\Otp.NET\bin\Release\netstandard1.3\Otp.NET.dll" target="lib\netstandard1.3"/> + </files> +</package>
\ 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 +{ + /// <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": {} + } +} |