diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a5ea64 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +otpnet +====== + +Google Authenticator OTP in C# (.Net) \ No newline at end of file diff --git a/otpnet/LICENSE.txt b/otpnet/LICENSE.txt new file mode 100644 index 0000000..ef257f3 --- /dev/null +++ b/otpnet/LICENSE.txt @@ -0,0 +1,37 @@ +OTPNet is licensed under the terms of Apache 2.0 + +Copyright [2012] [Nathan Adams] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +This project was based off of OTPHP (https://github.com/lelag/otphp) by Le Lag + +Copyright (c) 2011 Le Lag + +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. \ No newline at end of file diff --git a/otpnet/Libraries/Base32Encoding.cs b/otpnet/Libraries/Base32Encoding.cs new file mode 100644 index 0000000..f27acff --- /dev/null +++ b/otpnet/Libraries/Base32Encoding.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OTPNet +{ + // http://stackoverflow.com/a/7135008/195722 + 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/otpnet/Libraries/StringExtensions.cs b/otpnet/Libraries/StringExtensions.cs new file mode 100644 index 0000000..8d8d2f6 --- /dev/null +++ b/otpnet/Libraries/StringExtensions.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OTPNet +{ + public static class StringExtensions + { + /// Returns a string array that contains the substrings in this string that are seperated a given fixed length. + /// This string object. + /// Size of each substring. + /// CASE: length > 0 , RESULT: String is split from left to right. + /// CASE: length == 0 , RESULT: String is returned as the only entry in the array. + /// CASE: length < 0 , RESULT: String is split from right to left. + /// + /// String array that has been split into substrings of equal length. + /// + /// + /// string s = "1234567890"; + /// string[] a = s.Split(4); // a == { "1234", "5678", "90" } + /// + /// + public static string[] Split(this string s, int length) + { + System.Globalization.StringInfo str = new System.Globalization.StringInfo(s); + + int lengthAbs = Math.Abs(length); + + if (str == null || str.LengthInTextElements == 0 || lengthAbs == 0 || str.LengthInTextElements <= lengthAbs) + return new string[] { str.ToString() }; + + string[] array = new string[(str.LengthInTextElements % lengthAbs == 0 ? str.LengthInTextElements / lengthAbs : (str.LengthInTextElements / lengthAbs) + 1)]; + + if (length > 0) + for (int iStr = 0, iArray = 0; iStr < str.LengthInTextElements && iArray < array.Length; iStr += lengthAbs, iArray++) + array[iArray] = str.SubstringByTextElements(iStr, (str.LengthInTextElements - iStr < lengthAbs ? str.LengthInTextElements - iStr : lengthAbs)); + else // if (length < 0) + for (int iStr = str.LengthInTextElements - 1, iArray = array.Length - 1; iStr >= 0 && iArray >= 0; iStr -= lengthAbs, iArray--) + array[iArray] = str.SubstringByTextElements((iStr - lengthAbs < 0 ? 0 : iStr - lengthAbs + 1), (iStr - lengthAbs < 0 ? iStr + 1 : lengthAbs)); + + return array; + } + } +} diff --git a/otpnet/Libraries/Unixtime.cs b/otpnet/Libraries/Unixtime.cs new file mode 100644 index 0000000..3df6da1 --- /dev/null +++ b/otpnet/Libraries/Unixtime.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OTPNet +{ + // Issue reported by larry.ellis + // Patch provided by andrea.bertin + // https://code.google.com/p/otpnet/issues/detail?id=1 + class Unixtime + { + private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private readonly DateTime date; + + public Unixtime() + { + this.date = DateTime.UtcNow; + } + + public Unixtime(int timestamp) + { + this.date = Epoch.AddSeconds(timestamp); + } + + public DateTime toDateTime() + { + return this.date; + } + + public long toTimeStamp() + { + return (long)this.date.Subtract(Epoch).TotalSeconds; + } + } +} diff --git a/otpnet/OTP/HOTP.cs b/otpnet/OTP/HOTP.cs new file mode 100644 index 0000000..43340e5 --- /dev/null +++ b/otpnet/OTP/HOTP.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OTPNet +{ + /** + * HOTP - One time password generator + * + * The HOTP class allow for the generation + * and verification of one-time password using + * the HOTP specified algorithm. + * + * This class is meant to be compatible with + * Google Authenticator + * + * This class was originally ported from the rotp + * ruby library available at https://github.com/mdp/rotp + */ + public class HOTP : OTP + { + + public HOTP(string secret) + : base(secret, 6, HashAlgorithm.SHA1) + { + + } + + /** + * Get the password for a specific counter value + * @param integer $count the counter which is used to + * seed the hmac hash function. + * @return integer the One Time Password + */ + public int at(int count) + { + return this.generateOTP(count); + } + + /** + * Verify if a password is valid for a specific counter value + * + * @param integer $otp the one-time password + * @param integer $counter the counter value + * @return bool true if the counter is valid, false otherwise + */ + public bool verify(int otp, int counter) + { + return (otp == this.at(counter)); + } + + public string provisioning_uri(string name, int initial_count) + { + return "otpauth://hotp/" + name + "?secret=" + this.secret + " &counter=" + initial_count.ToString(); + } + } +} diff --git a/otpnet/OTP/OTP.cs b/otpnet/OTP/OTP.cs new file mode 100644 index 0000000..f4f359a --- /dev/null +++ b/otpnet/OTP/OTP.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Security.Cryptography; + +namespace OTPNet +{ + + // Please see LICENSE.txt for information on what license this is released under + public enum HashAlgorithm + { + SHA1 + } + + // This class was ported from this PHP library https://github.com/lelag/otphp which was ported from a Ruby library + /** + * One Time Password Generator + * + * The OTP class allow the generation of one-time + * password that is described in rfc 4xxx. + * + * This is class is meant to be compatible with + * Google Authenticator. + * + * This class was originally ported from the rotp + * ruby library available at https://github.com/mdp/rotp + */ + public class OTP + { + /** + * The base32 encoded secret key + * @var string + */ + public string secret; + + /** + * The algorithm used for the hmac hash function + * @var string + */ + public HashAlgorithm digest; + + /** + * The number of digits in the one-time password + * @var integer + */ + public int digits; + + /** + * Constructor for the OTP class + * @param string $secret the secret key + * @param array $opt options array can contain the + * following keys : + * @param integer digits : the number of digits in the one time password + * Currently Google Authenticator only support 6. Defaults to 6. + * @param string digest : the algorithm used for the hmac hash function + * Google Authenticator only support sha1. Defaults to sha1 + * + * @return new OTP class. + */ + public OTP(string secret) + { + this.secret = secret; + this.digits = 6; + this.digest = HashAlgorithm.SHA1; + } + + public OTP(string secret, int digits) + { + // Issue reported by scox.nz + // https://code.google.com/p/otpnet/issues/detail?id=2 + this.secret = secret; + this.digits = digits; + this.digest = HashAlgorithm.SHA1; + } + + public OTP(string secret, int digits, HashAlgorithm digest) + { + this.secret = secret; + this.digits = digits; + this.digest = digest; + } + + /** + * Generate a one-time password + * + * @param integer $input : number used to seed the hmac hash function. + * This number is usually a counter (HOTP) or calculated based on the current + * timestamp (see TOTP class). + * @return integer the one-time password + */ + public int generateOTP(Int64 input) + { + + HMAC hashgenerator = null; + switch (digest) + { + case HashAlgorithm.SHA1: + hashgenerator = new HMACSHA1(Base32Encoding.ToBytes(secret)); + break; + } + + + hashgenerator.ComputeHash(intToByteString(input)); + string hash = ""; + + foreach(byte b in hashgenerator.Hash) + { + hash += b.ToString("x2"); + } + List hmac = new List(); + foreach (string s in hash.Split(2)) + { + hmac.Add(Int32.Parse(s, System.Globalization.NumberStyles.HexNumber)); + } + + int offset = hmac[19] & 0xf; + int code = (hmac[offset + 0] & 0x7F) << 24 | + (hmac[offset + 1] & 0xFF) << 16 | + (hmac[offset + 2] & 0xFF) << 8 | + (hmac[offset + 3] & 0xFF); + + return code % (int)Math.Pow((double)10, (double)this.digits); + + } + + public byte[] intToByteString(Int64 i) + { + List res = new List(); + + while (i != 0) + { + res.Add((byte)(i & 0xFF)); + i >>= 8; + } + int rcount = res.Count; + for (int z = 0; z < 8 - rcount; z++) + res.Add((byte)0); + res.Reverse(); + + return res.ToArray(); + } + + + } + + + + +} diff --git a/otpnet/OTP/TOTP.cs b/otpnet/OTP/TOTP.cs new file mode 100644 index 0000000..2b4435a --- /dev/null +++ b/otpnet/OTP/TOTP.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OTPNet +{ + /** + * TOTP - One time password generator + * + * The TOTP class allow for the generation + * and verification of one-time password using + * the TOTP specified algorithm. + * + * This class is meant to be compatible with + * Google Authenticator + * + * This class was originally ported from the rotp + * ruby library available at https://github.com/mdp/rotp + */ + public class TOTP : OTP + { + + /** + * The interval in seconds for a one-time password timeframe + * Defaults to 30 + * @var integer + */ + public double interval; + + + public TOTP(string secret) + : base(secret, 6, HashAlgorithm.SHA1) + { + this.interval = 30; + } + + public TOTP(string secret, double interval) + : base(secret, 6, HashAlgorithm.SHA1) + { + this.interval = interval; + } + + public TOTP(string secret, double interval, int digits) + : base(secret, digits, HashAlgorithm.SHA1) + { + this.interval = interval; + } + + public TOTP(string secret, double interval, int digits, HashAlgorithm algo) + : base(secret, digits, algo) + { + this.interval = interval; + } + + /** + * Get the password for a specific timestamp value + * + * @param integer $timestamp the timestamp which is timecoded and + * used to seed the hmac hash function. + * @return integer the One Time Password + */ + public int at(double timestamp) + { + return this.generateOTP(this.timecode(timestamp)); + } + + /** + * Get the password for the current timestamp value + * + * @return integer the current One Time Password + */ + public int now() + { + return this.at(new Unixtime().toTimeStamp()); + } + + /** + * Verify if a password is valid for a specific counter value + * + * @param integer $otp the one-time password + * @param integer $timestamp the timestamp for the a given time, defaults to current time. + * @return bool true if the counter is valid, false otherwise + */ + public bool verify(int otp, double timestamp) + { + return (otp == this.at(timestamp)); + } + + public bool verify(int otp) + { + //calls verify(int, int) + return this.verify(otp, new Unixtime().toTimeStamp()); + } + + /** + * Returns the uri for a specific secret for totp method. + * Can be encoded as a image for simple configuration in + * Google Authenticator. + * + * @param string $name the name of the account / profile + * @return string the uri for the hmac secret + */ + public string provisitioning_uri(string name) + { + return "otpauth://totp/" + name + "?secret=" + this.secret; + } + + /** + * Transform a timestamp in a counter based on specified internal + * + * @param integer $timestamp + * @return integer the timecode + */ + public Int64 timecode(double timestamp) + { + return (Int64)(((((timestamp * 1000)) / (this.interval * 1000)))); + } + } +} diff --git a/otpnet/OTPNet.csproj b/otpnet/OTPNet.csproj new file mode 100644 index 0000000..811448f --- /dev/null +++ b/otpnet/OTPNet.csproj @@ -0,0 +1,59 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {F1FD321D-6F2A-4431-8307-6BF959419DBE} + Library + Properties + OTPNet + OTPNet + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/otpnet/OTPNet.sln b/otpnet/OTPNet.sln new file mode 100644 index 0000000..d3035cc --- /dev/null +++ b/otpnet/OTPNet.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OTPNet", "OTPNet.csproj", "{F1FD321D-6F2A-4431-8307-6BF959419DBE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F1FD321D-6F2A-4431-8307-6BF959419DBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1FD321D-6F2A-4431-8307-6BF959419DBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1FD321D-6F2A-4431-8307-6BF959419DBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1FD321D-6F2A-4431-8307-6BF959419DBE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/otpnet/Properties/AssemblyInfo.cs b/otpnet/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a289cc7 --- /dev/null +++ b/otpnet/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("OTPNet")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("OTPNet")] +[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ff624b9a-844a-4ede-9cdd-87025ddd4201")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/otpnet/obj/Debug/OTPNet.csproj.FileListAbsolute.txt b/otpnet/obj/Debug/OTPNet.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..c1f5672 --- /dev/null +++ b/otpnet/obj/Debug/OTPNet.csproj.FileListAbsolute.txt @@ -0,0 +1,4 @@ +C:\repos\otpnet-google\otpnet\bin\Debug\OTPNet.dll +C:\repos\otpnet-google\otpnet\bin\Debug\OTPNet.pdb +C:\repos\otpnet-google\otpnet\obj\Debug\OTPNet.dll +C:\repos\otpnet-google\otpnet\obj\Debug\OTPNet.pdb diff --git a/otpnettest/Program.cs b/otpnettest/Program.cs new file mode 100644 index 0000000..1ab090e --- /dev/null +++ b/otpnettest/Program.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using OTPNet; + +namespace otpnettest +{ + class Program + { + static void Main(string[] args) + { + TOTP t = new TOTP(args[0]); + Console.WriteLine("Your OTP = " + t.now().ToString("D6")); + } + } +} diff --git a/otpnettest/Properties/AssemblyInfo.cs b/otpnettest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0cf6e35 --- /dev/null +++ b/otpnettest/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("otpnettest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("otpnettest")] +[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9b7c314e-7892-4cf6-8ca8-01ef7420f6a3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/otpnettest/obj/x86/Debug/otpnettest.csproj.FileListAbsolute.txt b/otpnettest/obj/x86/Debug/otpnettest.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..e712899 --- /dev/null +++ b/otpnettest/obj/x86/Debug/otpnettest.csproj.FileListAbsolute.txt @@ -0,0 +1,7 @@ +C:\repos\otpnet-google\otpnettest\bin\Debug\otpnettest.exe +C:\repos\otpnet-google\otpnettest\bin\Debug\otpnettest.pdb +C:\repos\otpnet-google\otpnettest\bin\Debug\OTPNet.dll +C:\repos\otpnet-google\otpnettest\bin\Debug\OTPNet.pdb +C:\repos\otpnet-google\otpnettest\obj\x86\Debug\otpnettest.csprojResolveAssemblyReference.cache +C:\repos\otpnet-google\otpnettest\obj\x86\Debug\otpnettest.exe +C:\repos\otpnet-google\otpnettest\obj\x86\Debug\otpnettest.pdb diff --git a/otpnettest/otpnettest.csproj b/otpnettest/otpnettest.csproj new file mode 100644 index 0000000..5a1e00d --- /dev/null +++ b/otpnettest/otpnettest.csproj @@ -0,0 +1,60 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {7F244138-C2C0-4A59-894F-276F97A0029A} + Exe + Properties + otpnettest + otpnettest + v4.0 + Client + 512 + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\otpnet\bin\Debug\OTPNet.dll + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/otpnettest/otpnettest.sln b/otpnettest/otpnettest.sln new file mode 100644 index 0000000..a799c72 --- /dev/null +++ b/otpnettest/otpnettest.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "otpnettest", "otpnettest.csproj", "{7F244138-C2C0-4A59-894F-276F97A0029A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7F244138-C2C0-4A59-894F-276F97A0029A}.Debug|x86.ActiveCfg = Debug|x86 + {7F244138-C2C0-4A59-894F-276F97A0029A}.Debug|x86.Build.0 = Debug|x86 + {7F244138-C2C0-4A59-894F-276F97A0029A}.Release|x86.ActiveCfg = Release|x86 + {7F244138-C2C0-4A59-894F-276F97A0029A}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal