From 52a093b41e6ea0cbf755b85afc75229fadc66b21 Mon Sep 17 00:00:00 2001 From: Mees van Dijk Date: Fri, 23 Dec 2022 16:52:28 +0100 Subject: [PATCH 01/48] Add functionality to create compliant OTP URIs (#34) * Add OTPUri class * Add newlines to end of files Co-authored-by: Mees van Dijk --- src/Otp.NET/OTPType.cs | 18 ++++++ src/Otp.NET/OTPUri.cs | 141 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 src/Otp.NET/OTPType.cs create mode 100644 src/Otp.NET/OTPUri.cs diff --git a/src/Otp.NET/OTPType.cs b/src/Otp.NET/OTPType.cs new file mode 100644 index 0000000..ce5b97f --- /dev/null +++ b/src/Otp.NET/OTPType.cs @@ -0,0 +1,18 @@ +namespace OtpNet { + /// + /// Schema types for OTPs + /// + public enum OTPType + { + /// + /// Time-based OTP + /// see https://datatracker.ietf.org/doc/html/rfc6238 + /// + TOTP, + /// + /// HMAC-based (counter) OTP + /// see https://datatracker.ietf.org/doc/html/rfc4226 + /// + HOTP, + } +} diff --git a/src/Otp.NET/OTPUri.cs b/src/Otp.NET/OTPUri.cs new file mode 100644 index 0000000..f50ab31 --- /dev/null +++ b/src/Otp.NET/OTPUri.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace OtpNet { + public class OTPUri + { + /// + /// What type of OTP is this uri for + /// + /// + public readonly OTPType Type; + + /// + /// The secret parameter is an arbitrary key value encoded in Base32 according to RFC 3548. + /// The padding specified in RFC 3548 section 2.2 is not required and should be omitted. + /// + public readonly byte[] Secret; + + /// + /// Which account a key is associated with + /// + public readonly string User; + + /// + /// The issuer parameter is a string value indicating the provider or service this account is + /// associated with, URL-encoded according to RFC 3986. + /// + public readonly string Issuer; + + /// + /// The algorithm used by the generator + /// + public readonly OtpHashMode Algorithm; + + /// + /// The amount of digits in the final code + /// + public readonly int Digits; + + /// + /// The number of seconds that a code is valid. Only applies to TOTP, not HOTP + /// + public readonly int Period; + + /// + /// Initial counter value for HOTP. This is ignored when using TOTP. + /// + public readonly int Counter; + + + /// + /// Create a new OTPAuthUri + /// + public OTPUri( + OTPType schema, + byte[] secret, + string user, + string issuer = null, + OtpHashMode algorithm = OtpHashMode.Sha1, + int digits = 6, + int period = 30, + int counter = 0 + ) + { + _ = secret ?? throw new ArgumentNullException(nameof(secret)); + _ = user ?? throw new ArgumentNullException(nameof(user)); + Type = schema; + Secret = secret; + User = user; + Issuer = issuer; + Algorithm = algorithm; + if (digits < 0) + { + throw new ArgumentOutOfRangeException(nameof(digits)); + } + Digits = digits; + switch (Type) + { + + case OTPType.TOTP: + Period = period; + break; + case OTPType.HOTP: + Counter = counter; + break; + } + } + + /// + /// Generates a Uri String according to the parameters + /// + /// a Uri String according to the parameters + public override string ToString() + { + Dictionary parameters = new Dictionary(); + + parameters.Add("secret", Base32Encoding.ToString(Secret)); + if (Issuer != null) + { + parameters.Add("issuer", Issuer); + } + parameters.Add("algorithm", Algorithm.ToString().ToUpper()); + parameters.Add("digits", Digits.ToString()); + + switch (Type) + { + case OTPType.TOTP: + parameters.Add("period", Period.ToString()); + break; + case OTPType.HOTP: + parameters.Add("counter", Counter.ToString()); + break; + } + + StringBuilder uriBuilder = new StringBuilder("otpauth://"); + uriBuilder.Append(Type.ToString().ToLower()); + uriBuilder.Append("/"); + // The label + if (Issuer != null) + { + uriBuilder.Append(Issuer); + uriBuilder.Append(":"); + } + uriBuilder.Append(User); + // Start of the parameters + uriBuilder.Append("?"); + + foreach (KeyValuePair pair in parameters) + { + uriBuilder.Append(pair.Key); + uriBuilder.Append("="); + uriBuilder.Append(pair.Value); + uriBuilder.Append("&"); + } + uriBuilder.Remove(uriBuilder.Length - 1, 1); // Remove last "&" + + return Uri.EscapeUriString(uriBuilder.ToString()); + } + } +} From 01999d77011906413997f89c7d870bd30d7b74c0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 11:03:45 -0500 Subject: [PATCH 02/48] refactor OtpUri some --- src/Otp.NET/{OTPType.cs => OtpType2.cs} | 6 +-- src/Otp.NET/{OTPUri.cs => OtpUri2.cs} | 59 +++++++++++++++---------- 2 files changed, 39 insertions(+), 26 deletions(-) rename src/Otp.NET/{OTPType.cs => OtpType2.cs} (87%) rename src/Otp.NET/{OTPUri.cs => OtpUri2.cs} (79%) diff --git a/src/Otp.NET/OTPType.cs b/src/Otp.NET/OtpType2.cs similarity index 87% rename from src/Otp.NET/OTPType.cs rename to src/Otp.NET/OtpType2.cs index ce5b97f..91219f7 100644 --- a/src/Otp.NET/OTPType.cs +++ b/src/Otp.NET/OtpType2.cs @@ -2,17 +2,17 @@ namespace OtpNet { /// /// Schema types for OTPs /// - public enum OTPType + public enum OtpType2 { /// /// Time-based OTP /// see https://datatracker.ietf.org/doc/html/rfc6238 /// - TOTP, + Totp, /// /// HMAC-based (counter) OTP /// see https://datatracker.ietf.org/doc/html/rfc4226 /// - HOTP, + Hotp, } } diff --git a/src/Otp.NET/OTPUri.cs b/src/Otp.NET/OtpUri2.cs similarity index 79% rename from src/Otp.NET/OTPUri.cs rename to src/Otp.NET/OtpUri2.cs index f50ab31..1adfdbd 100644 --- a/src/Otp.NET/OTPUri.cs +++ b/src/Otp.NET/OtpUri2.cs @@ -2,14 +2,16 @@ using System.Collections.Generic; using System.Text; -namespace OtpNet { - public class OTPUri +namespace OtpNet +{ + // See https://github.com/google/google-authenticator/wiki/Key-Uri-Format + public class OtpUri { /// /// What type of OTP is this uri for - /// + /// /// - public readonly OTPType Type; + public readonly OtpType Type; /// /// The secret parameter is an arbitrary key value encoded in Base32 according to RFC 3548. @@ -52,50 +54,61 @@ public class OTPUri /// /// Create a new OTPAuthUri /// - public OTPUri( - OTPType schema, + public OtpUri( + OtpType schema, byte[] secret, string user, string issuer = null, OtpHashMode algorithm = OtpHashMode.Sha1, int digits = 6, int period = 30, - int counter = 0 - ) + int counter = 0) { _ = secret ?? throw new ArgumentNullException(nameof(secret)); _ = user ?? throw new ArgumentNullException(nameof(user)); + if (digits < 0) + { + throw new ArgumentOutOfRangeException(nameof(digits)); + } + Type = schema; Secret = secret; User = user; Issuer = issuer; Algorithm = algorithm; - if (digits < 0) - { - throw new ArgumentOutOfRangeException(nameof(digits)); - } Digits = digits; + switch (Type) { - - case OTPType.TOTP: + case OtpType.Totp: Period = period; break; - case OTPType.HOTP: + case OtpType.Hotp: Counter = counter; break; } } + /// + /// Generates a Uri according to the parameters + /// + /// a Uri according to the parameters + public Uri ToUri() + { + return new Uri(ToString()); + } + /// /// Generates a Uri String according to the parameters /// /// a Uri String according to the parameters public override string ToString() { - Dictionary parameters = new Dictionary(); + var parameters = new Dictionary + { + { "secret", Base32Encoding.ToString(Secret) } + }; - parameters.Add("secret", Base32Encoding.ToString(Secret)); if (Issuer != null) { parameters.Add("issuer", Issuer); @@ -105,16 +118,16 @@ public override string ToString() switch (Type) { - case OTPType.TOTP: + case OtpType.Totp: parameters.Add("period", Period.ToString()); break; - case OTPType.HOTP: + case OtpType.Hotp: parameters.Add("counter", Counter.ToString()); break; } - StringBuilder uriBuilder = new StringBuilder("otpauth://"); - uriBuilder.Append(Type.ToString().ToLower()); + var uriBuilder = new StringBuilder("otpauth://"); + uriBuilder.Append(Type.ToString().ToLowerInvariant()); uriBuilder.Append("/"); // The label if (Issuer != null) @@ -126,15 +139,15 @@ public override string ToString() // Start of the parameters uriBuilder.Append("?"); - foreach (KeyValuePair pair in parameters) + foreach (var pair in parameters) { uriBuilder.Append(pair.Key); uriBuilder.Append("="); uriBuilder.Append(pair.Value); uriBuilder.Append("&"); } - uriBuilder.Remove(uriBuilder.Length - 1, 1); // Remove last "&" + uriBuilder.Remove(uriBuilder.Length - 1, 1); // Remove last "&" return Uri.EscapeUriString(uriBuilder.ToString()); } } From d363a57648358553d1e467cc61351ca98d886c89 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 11:04:02 -0500 Subject: [PATCH 03/48] rename OtpUri --- src/Otp.NET/{OtpType2.cs => OtpType.cs} | 2 +- src/Otp.NET/{OtpUri2.cs => OtpUri.cs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/Otp.NET/{OtpType2.cs => OtpType.cs} (94%) rename src/Otp.NET/{OtpUri2.cs => OtpUri.cs} (100%) diff --git a/src/Otp.NET/OtpType2.cs b/src/Otp.NET/OtpType.cs similarity index 94% rename from src/Otp.NET/OtpType2.cs rename to src/Otp.NET/OtpType.cs index 91219f7..669c73f 100644 --- a/src/Otp.NET/OtpType2.cs +++ b/src/Otp.NET/OtpType.cs @@ -2,7 +2,7 @@ namespace OtpNet { /// /// Schema types for OTPs /// - public enum OtpType2 + public enum OtpType { /// /// Time-based OTP diff --git a/src/Otp.NET/OtpUri2.cs b/src/Otp.NET/OtpUri.cs similarity index 100% rename from src/Otp.NET/OtpUri2.cs rename to src/Otp.NET/OtpUri.cs From 9c361d3ae3c284cb8b84c39ef5b2e7e8a5dc0f91 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 11:23:12 -0500 Subject: [PATCH 04/48] OtpUri tests --- src/Otp.NET/Otp.NET.csproj | 1 - src/Otp.NET/OtpUri.cs | 10 ++++++---- test/Otp.NET.Test/OtpUriTest.cs | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 test/Otp.NET.Test/OtpUriTest.cs diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index ba24252..c4d73c8 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -17,7 +17,6 @@ For documentation and examples visit the project website on GitHub at https://gi http://i.imgur.com/XFfC64v.png https://github.com/kspearrin/Otp.NET true - vs-signing-key.pfx OtpNet diff --git a/src/Otp.NET/OtpUri.cs b/src/Otp.NET/OtpUri.cs index 1adfdbd..bd8510d 100644 --- a/src/Otp.NET/OtpUri.cs +++ b/src/Otp.NET/OtpUri.cs @@ -109,7 +109,7 @@ public override string ToString() { "secret", Base32Encoding.ToString(Secret) } }; - if (Issuer != null) + if (!string.IsNullOrWhiteSpace(Issuer)) { parameters.Add("issuer", Issuer); } @@ -129,16 +129,17 @@ public override string ToString() var uriBuilder = new StringBuilder("otpauth://"); uriBuilder.Append(Type.ToString().ToLowerInvariant()); uriBuilder.Append("/"); + // The label - if (Issuer != null) + if (!string.IsNullOrWhiteSpace(Issuer)) { uriBuilder.Append(Issuer); uriBuilder.Append(":"); } uriBuilder.Append(User); + // Start of the parameters uriBuilder.Append("?"); - foreach (var pair in parameters) { uriBuilder.Append(pair.Key); @@ -146,8 +147,9 @@ public override string ToString() uriBuilder.Append(pair.Value); uriBuilder.Append("&"); } + // Remove last "&" + uriBuilder.Remove(uriBuilder.Length - 1, 1); - uriBuilder.Remove(uriBuilder.Length - 1, 1); // Remove last "&" return Uri.EscapeUriString(uriBuilder.ToString()); } } diff --git a/test/Otp.NET.Test/OtpUriTest.cs b/test/Otp.NET.Test/OtpUriTest.cs new file mode 100644 index 0000000..d037493 --- /dev/null +++ b/test/Otp.NET.Test/OtpUriTest.cs @@ -0,0 +1,23 @@ +using System; +using NUnit.Framework; + +namespace OtpNet.Test +{ + [TestFixture()] + public class OtpUriTest + { + private const string _baseSecret = "JBSWY3DPEHPK3PXP"; + private const string _baseUser = "alice@google.com"; + private const string _baseIssuer = "ACME Co"; + + [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha1, 6, 30, 0, + "otpauth://totp/ACME%20Co:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20&algorithm=SHA1&digits=6&period=30")] + public void GenerateOtpUriTest(string secret, OtpType otpType, string user, string issuer, + OtpHashMode hash, int digits, int period, int counter, string expectedUri) + { + var sec = Base32Encoding.ToBytes(secret); + var uriString = new OtpUri(otpType, sec, user, issuer, hash, digits, period, counter).ToString(); + Assert.That(uriString, Is.EqualTo(expectedUri)); + } + } +} From 2d5dffd2517c5e67932921b8541a0108a12dbf6b Mon Sep 17 00:00:00 2001 From: Caterina Novak Date: Fri, 23 Dec 2022 18:29:35 +0200 Subject: [PATCH 05/48] .net 5 support + small refactoring (#33) I added .net 5 to projects TargetFrameworks and did a small refactoring changes Co-authored-by: Caterina Novak --- src/Otp.NET/Base32Encoding.cs | 21 ++-- src/Otp.NET/Hotp.cs | 40 +++---- src/Otp.NET/IKeyProvider.cs | 2 +- src/Otp.NET/InMemoryKey.cs | 38 +++---- src/Otp.NET/KeyGeneration.cs | 28 +++-- src/Otp.NET/KeyUtilities.cs | 11 +- src/Otp.NET/Otp.NET.csproj | 3 +- src/Otp.NET/Otp.cs | 33 +++--- src/Otp.NET/OtpHashMode.cs | 2 +- src/Otp.NET/TimeCorrection.cs | 61 +++++------ src/Otp.NET/Totp.cs | 145 +++++++++++++++----------- src/Otp.NET/VerificationWindow.cs | 21 ++-- src/Otp.NET/otp-net.snk | Bin 0 -> 1172 bytes test/Otp.NET.Test/HotpTest.cs | 18 ++-- test/Otp.NET.Test/Otp.NET.Test.csproj | 2 +- test/Otp.NET.Test/TotpTest.cs | 4 +- 16 files changed, 217 insertions(+), 212 deletions(-) create mode 100644 src/Otp.NET/otp-net.snk diff --git a/src/Otp.NET/Base32Encoding.cs b/src/Otp.NET/Base32Encoding.cs index 4a64c5b..d0f4b5d 100644 --- a/src/Otp.NET/Base32Encoding.cs +++ b/src/Otp.NET/Base32Encoding.cs @@ -14,20 +14,21 @@ public static byte[] ToBytes(string input) { if(string.IsNullOrEmpty(input)) { - throw new ArgumentNullException("input"); + throw new ArgumentNullException(nameof(input)); } input = input.TrimEnd('='); //remove padding characters - int byteCount = input.Length * 5 / 8; //this must be TRUNCATED - byte[] returnArray = new byte[byteCount]; + var byteCount = input.Length * 5 / 8; //this must be TRUNCATED + var returnArray = new byte[byteCount]; byte curByte = 0, bitsRemaining = 8; - int mask = 0, arrayIndex = 0; + var arrayIndex = 0; - foreach(char c in input) + foreach(var c in input) { - int cValue = CharToValue(c); + var cValue = CharToValue(c); + int mask; if(bitsRemaining > 5) { mask = cValue << (bitsRemaining - 5); @@ -57,7 +58,7 @@ public static string ToString(byte[] input) { if(input == null || input.Length == 0) { - throw new ArgumentNullException("input"); + throw new ArgumentNullException(nameof(input)); } int charCount = (int)Math.Ceiling(input.Length / 5d) * 8; @@ -94,7 +95,7 @@ public static string ToString(byte[] input) private static int CharToValue(char c) { - int value = (int)c; + int value = c; //65-90 == uppercase letters if(value < 91 && value > 64) @@ -112,7 +113,7 @@ private static int CharToValue(char c) return value - 97; } - throw new ArgumentException("Character is not a Base32 character.", "c"); + throw new ArgumentException("Character is not a Base32 character.", nameof(c)); } private static char ValueToChar(byte b) @@ -127,7 +128,7 @@ private static char ValueToChar(byte b) return (char)(b + 24); } - throw new ArgumentException("Byte is not a Base32 value.", "b"); + throw new ArgumentException("Byte is not a Base32 value.", nameof(b)); } } } diff --git a/src/Otp.NET/Hotp.cs b/src/Otp.NET/Hotp.cs index 705b74b..ac12e48 100644 --- a/src/Otp.NET/Hotp.cs +++ b/src/Otp.NET/Hotp.cs @@ -16,7 +16,7 @@ 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 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. 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 @@ -36,7 +36,7 @@ namespace OtpNet /// public class Hotp : Otp { - private readonly int hotpSize; + private readonly int _hotpSize; /// /// Create a HOTP instance @@ -49,7 +49,7 @@ public Hotp(byte[] secretKey, OtpHashMode mode = OtpHashMode.Sha1, int hotpSize { VerifyParameters(hotpSize); - this.hotpSize = hotpSize; + _hotpSize = hotpSize; } /// @@ -63,44 +63,32 @@ public Hotp(IKeyProvider key, OtpHashMode mode = OtpHashMode.Sha1, int hotpSize { VerifyParameters(hotpSize); - this.hotpSize = hotpSize; + _hotpSize = hotpSize; } private static void VerifyParameters(int hotpSize) { - if(!(hotpSize >= 6)) - throw new ArgumentOutOfRangeException("hotpSize"); - if(!(hotpSize <= 8)) - throw new ArgumentOutOfRangeException("hotpSize"); + if(hotpSize < 6) + throw new ArgumentOutOfRangeException(nameof(hotpSize)); + if(hotpSize > 8) + throw new ArgumentOutOfRangeException(nameof(hotpSize)); } /// /// Takes a counter and then computes a HOTP value /// /// The timestamp to use for the HOTP calculation + /// /// a HOTP value - public string ComputeHOTP(long counter) - { - return this.Compute(counter, this.hashMode); - } + public string ComputeHOTP(long counter) => Compute(counter, _hashMode); /// /// Verify a value that has been provided with the calculated value /// /// the trial HOTP value - /// The counter value to verify/param> + /// The counter value to verify /// True if there is a match. - public bool VerifyHotp(string hotp, long counter) - { - if(hotp == ComputeHOTP(counter)) - { - return true; - } - else - { - return false; - } - } + public bool VerifyHotp(string hotp, long counter) => hotp == ComputeHOTP(counter); /// /// Takes a time step and computes a HOTP code @@ -111,8 +99,8 @@ public bool VerifyHotp(string hotp, long counter) protected override string Compute(long counter, OtpHashMode mode) { var data = KeyUtilities.GetBigEndianBytes(counter); - var otp = this.CalculateOtp(data, mode); - return Digits(otp, this.hotpSize); + var otp = CalculateOtp(data, mode); + return Digits(otp, _hotpSize); } } } diff --git a/src/Otp.NET/IKeyProvider.cs b/src/Otp.NET/IKeyProvider.cs index 611d086..9a5541e 100644 --- a/src/Otp.NET/IKeyProvider.cs +++ b/src/Otp.NET/IKeyProvider.cs @@ -16,7 +16,7 @@ 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 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. 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 diff --git a/src/Otp.NET/InMemoryKey.cs b/src/Otp.NET/InMemoryKey.cs index 14dc694..4b89aed 100644 --- a/src/Otp.NET/InMemoryKey.cs +++ b/src/Otp.NET/InMemoryKey.cs @@ -16,7 +16,7 @@ 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 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. 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 @@ -49,11 +49,11 @@ namespace OtpNet /// public class InMemoryKey : IKeyProvider { - static readonly object platformSupportSync = new object(); + private static readonly object _platformSupportSync = new object(); - readonly object stateSync = new object(); - readonly byte[] KeyData; - readonly int keyLength; + private readonly object _stateSync = new object(); + private readonly byte[] _keyData; + private readonly int _keyLength; /// /// Creates an instance of a key. @@ -61,15 +61,15 @@ public class InMemoryKey : IKeyProvider /// Plaintext key data public InMemoryKey(byte[] key) { - if(!(key != null)) - throw new ArgumentNullException("key"); - if(!(key.Length > 0)) + if(key == null) + throw new ArgumentNullException(nameof(key)); + if(key.Length <= 0) throw new ArgumentException("The key must not be empty"); - this.keyLength = key.Length; - int paddedKeyLength = (int)Math.Ceiling((decimal)key.Length / (decimal)16) * 16; - this.KeyData = new byte[paddedKeyLength]; - Array.Copy(key, this.KeyData, key.Length); + _keyLength = key.Length; + var paddedKeyLength = (int)Math.Ceiling((decimal)key.Length / (decimal)16) * 16; + _keyData = new byte[paddedKeyLength]; + Array.Copy(key, _keyData, key.Length); } /// @@ -81,10 +81,10 @@ public InMemoryKey(byte[] key) /// Plaintext Key internal byte[] GetCopyOfKey() { - var plainKey = new byte[this.keyLength]; - lock(this.stateSync) + var plainKey = new byte[_keyLength]; + lock(_stateSync) { - Array.Copy(this.KeyData, plainKey, this.keyLength); + Array.Copy(_keyData, plainKey, _keyLength); } return plainKey; } @@ -97,10 +97,10 @@ internal byte[] GetCopyOfKey() /// HMAC of the key and data public byte[] ComputeHmac(OtpHashMode mode, byte[] data) { - byte[] hashedValue = null; - using(HMAC hmac = CreateHmacHash(mode)) + byte[] hashedValue; + using(var hmac = CreateHmacHash(mode)) { - byte[] key = this.GetCopyOfKey(); + var key = GetCopyOfKey(); try { hmac.Key = key; @@ -120,7 +120,7 @@ public byte[] ComputeHmac(OtpHashMode mode, byte[] data) /// private static HMAC CreateHmacHash(OtpHashMode otpHashMode) { - HMAC hmacAlgorithm = null; + HMAC hmacAlgorithm; switch(otpHashMode) { case OtpHashMode.Sha256: diff --git a/src/Otp.NET/KeyGeneration.cs b/src/Otp.NET/KeyGeneration.cs index 13ad7c4..1483a18 100644 --- a/src/Otp.NET/KeyGeneration.cs +++ b/src/Otp.NET/KeyGeneration.cs @@ -16,7 +16,7 @@ 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 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. 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 @@ -34,13 +34,13 @@ namespace OtpNet public static class KeyGeneration { /// - /// Generates a random key in accordance with the RFC recommened length for each algorithm + /// Generates a random key in accordance with the RFC recommended length for each algorithm /// /// Key length /// The generated key public static byte[] GenerateRandomKey(int length) { - byte[] key = new byte[length]; + var key = new byte[length]; using(var rnd = RandomNumberGenerator.Create()) { rnd.GetBytes(key); @@ -49,7 +49,7 @@ public static byte[] GenerateRandomKey(int length) } /// - /// Generates a random key in accordance with the RFC recommened length for each algorithm + /// Generates a random key in accordance with the RFC recommended length for each algorithm /// /// HashMode /// Key @@ -65,10 +65,14 @@ public static byte[] GenerateRandomKey(OtpHashMode mode = OtpHashMode.Sha1) /// The public identifier that is unique to the authenticating device /// The hash mode to use. This will determine the resulting key lenght. The default is sha-1 (as per the RFC) which is 20 bytes /// Derived key - public static byte[] DeriveKeyFromMaster(IKeyProvider masterKey, byte[] publicIdentifier, OtpHashMode mode = OtpHashMode.Sha1) + public static byte[] DeriveKeyFromMaster( + IKeyProvider masterKey, + byte[] publicIdentifier, + OtpHashMode mode = OtpHashMode.Sha1) { if(masterKey == null) - throw new ArgumentNullException("masterKey"); + throw new ArgumentNullException(nameof(masterKey)); + return masterKey.ComputeHmac(mode, publicIdentifier); } @@ -77,12 +81,14 @@ public static byte[] DeriveKeyFromMaster(IKeyProvider masterKey, byte[] publicId /// /// The master key from which to derive a device specific key /// A serial number that is unique to the authenticating device - /// The hash mode to use. This will determine the resulting key lenght. The default is sha-1 (as per the RFC) which is 20 bytes + /// The hash mode to use. This will determine the resulting key lenght. + /// The default is sha-1 (as per the RFC) which is 20 bytes /// Derived key - public static byte[] DeriveKeyFromMaster(IKeyProvider masterKey, int serialNumber, OtpHashMode mode = OtpHashMode.Sha1) - { - return DeriveKeyFromMaster(masterKey, KeyUtilities.GetBigEndianBytes(serialNumber), mode); - } + public static byte[] DeriveKeyFromMaster( + IKeyProvider masterKey, + int serialNumber, + OtpHashMode mode = OtpHashMode.Sha1) => + DeriveKeyFromMaster(masterKey, KeyUtilities.GetBigEndianBytes(serialNumber), mode); private static HashAlgorithm GetHashAlgorithmForMode(OtpHashMode mode) { diff --git a/src/Otp.NET/KeyUtilities.cs b/src/Otp.NET/KeyUtilities.cs index f81dfb3..24283ca 100644 --- a/src/Otp.NET/KeyUtilities.cs +++ b/src/Otp.NET/KeyUtilities.cs @@ -16,7 +16,7 @@ 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 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. 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 @@ -41,12 +41,13 @@ internal class KeyUtilities /// 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 + /// 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"); + throw new ArgumentNullException(nameof(sensitiveData)); new Random().NextBytes(sensitiveData); } @@ -56,7 +57,7 @@ internal static void Destroy(byte[] sensitiveData) /// /// RFC 4226 specifies big endian as the method for converting the counter to data to hash. /// - static internal byte[] GetBigEndianBytes(long input) + internal static 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); @@ -70,7 +71,7 @@ static internal byte[] GetBigEndianBytes(long input) /// /// RFC 4226 specifies big endian as the method for converting the counter to data to hash. /// - static internal byte[] GetBigEndianBytes(int input) + internal static 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); diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index c4d73c8..e61e990 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -2,7 +2,7 @@ 1.2.2 - netstandard2.0;netstandard1.3;net45 + net45;net5.0;netstandard1.3;netstandard2.0 Otp.NET Otp.NET Kyle Spearrin @@ -18,6 +18,7 @@ For documentation and examples visit the project website on GitHub at https://gi https://github.com/kspearrin/Otp.NET true OtpNet + otp-net.snk diff --git a/src/Otp.NET/Otp.cs b/src/Otp.NET/Otp.cs index fcd23cf..a0b2e4f 100644 --- a/src/Otp.NET/Otp.cs +++ b/src/Otp.NET/Otp.cs @@ -16,7 +16,7 @@ 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 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. 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 @@ -39,12 +39,12 @@ public abstract class Otp /// /// Secret key /// - protected readonly IKeyProvider secretKey; + protected readonly IKeyProvider _secretKey; /// /// The hash mode to use /// - protected readonly OtpHashMode hashMode; + protected readonly OtpHashMode _hashMode; /// /// Constructor for the abstract class using an explicit secret key @@ -53,15 +53,15 @@ public abstract class Otp /// The hash mode to use public Otp(byte[] secretKey, OtpHashMode mode) { - if(!(secretKey != null)) - throw new ArgumentNullException("secretKey"); - if(!(secretKey.Length > 0)) + if(secretKey == null) + throw new ArgumentNullException(nameof(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 = new InMemoryKey(secretKey); + _secretKey = new InMemoryKey(secretKey); - this.hashMode = mode; + _hashMode = mode; } /// @@ -71,12 +71,9 @@ public Otp(byte[] secretKey, OtpHashMode mode) /// The hash mode to use public Otp(IKeyProvider key, OtpHashMode mode) { - if (key == null) - throw new ArgumentNullException("key"); + _secretKey = key ?? throw new ArgumentNullException(nameof(key)); - this.secretKey = key; - - this.hashMode = mode; + _hashMode = mode; } /// @@ -92,7 +89,7 @@ public Otp(IKeyProvider key, OtpHashMode mode) /// protected internal long CalculateOtp(byte[] data, OtpHashMode mode) { - byte[] hmacComputedHash = this.secretKey.ComputeHmac(mode, data); + var hmacComputedHash = _secretKey.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 @@ -110,7 +107,7 @@ protected internal long CalculateOtp(byte[] data, OtpHashMode mode) /// protected internal static string Digits(long input, int digitCount) { - var truncatedValue = ((int)input % (int)Math.Pow(10, digitCount)); + var truncatedValue = (int)input % (int)Math.Pow(10, digitCount); return truncatedValue.ToString().PadLeft(digitCount, '0'); } @@ -126,9 +123,11 @@ protected bool Verify(long initialStep, string valueToVerify, out long matchedSt { if(window == null) window = new VerificationWindow(); + foreach(var frame in window.ValidationCandidates(initialStep)) { - var comparisonValue = this.Compute(frame, this.hashMode); + var comparisonValue = Compute(frame, _hashMode); + if(ValuesEqual(comparisonValue, valueToVerify)) { matchedStep = frame; @@ -149,7 +148,7 @@ private bool ValuesEqual(string a, string b) } var result = 0; - for(int i = 0; i < a.Length; i++) + for(var i = 0; i < a.Length; i++) { result |= a[i] ^ b[i]; } diff --git a/src/Otp.NET/OtpHashMode.cs b/src/Otp.NET/OtpHashMode.cs index a613ff5..11600f6 100644 --- a/src/Otp.NET/OtpHashMode.cs +++ b/src/Otp.NET/OtpHashMode.cs @@ -16,7 +16,7 @@ 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 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. 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 diff --git a/src/Otp.NET/TimeCorrection.cs b/src/Otp.NET/TimeCorrection.cs index 9654080..7001618 100644 --- a/src/Otp.NET/TimeCorrection.cs +++ b/src/Otp.NET/TimeCorrection.cs @@ -16,7 +16,7 @@ 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 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. 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 @@ -32,12 +32,15 @@ namespace OtpNet /// /// /// 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 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 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 + /// This class is immutable and therefore thread-safe /// public class TimeCorrection { @@ -46,62 +49,48 @@ public class TimeCorrection /// 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. + /// Constructor used solely for the UncorrectedInstance static field to provide an instance without + /// a correction factor. /// - private TimeCorrection() - { - this.timeCorrectionFactor = TimeSpan.FromSeconds(0); - } + private TimeCorrection() => CorrectionFactor = 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 + /// 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. + /// This overload assumes UTC. If a base and reference time other than UTC are required + /// then use the other overload. /// /// The current correct UTC time - public TimeCorrection(DateTime correctUtc) - { - this.timeCorrectionFactor = DateTime.UtcNow - correctUtc; - } + public TimeCorrection(DateTime correctUtc) => CorrectionFactor = DateTime.UtcNow - correctUtc; /// - /// Creates a corrected time object by providing the known correct current time and the current reference time that needs correction + /// 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; - } + /// The current reference time (time that will have the correction + /// factor applied in subsequent calls) + public TimeCorrection(DateTime correctTime, DateTime referenceTime) => + CorrectionFactor = 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; - } + public DateTime GetCorrectedTime(DateTime referenceTime) => referenceTime - CorrectionFactor; /// /// Applies the correction factor to the current system UTC time and returns a corrected time /// - public DateTime CorrectedUtcNow - { - get { return GetCorrectedTime(DateTime.UtcNow); } - } + public DateTime CorrectedUtcNow => GetCorrectedTime(DateTime.UtcNow); /// /// The timespan that is used to calculate a corrected time /// - public TimeSpan CorrectionFactor - { - get { return this.timeCorrectionFactor; } - } + public TimeSpan CorrectionFactor { get; } } } \ No newline at end of file diff --git a/src/Otp.NET/Totp.cs b/src/Otp.NET/Totp.cs index 9ba2337..c3a401c 100644 --- a/src/Otp.NET/Totp.cs +++ b/src/Otp.NET/Totp.cs @@ -16,7 +16,7 @@ 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 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. 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 @@ -40,64 +40,82 @@ public class Totp : Otp /// /// The number of ticks as Measured at Midnight Jan 1st 1970; /// - const long unixEpochTicks = 621355968000000000L; + private const long UNIX_EPOCH_TICKS = 621355968000000000L; + /// /// A divisor for converting ticks to seconds /// - const long ticksToSeconds = 10000000L; + private const long TICKS_TO_SECONDS = 10000000L; - private readonly int step; - private readonly int totpSize; - private readonly TimeCorrection correctedTime; + 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 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) + /// 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; + _step = step; + _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; + // we never null check the corrected time object. Since it's readonly, we'll + // ensure that it isn't null here and provide neutral functionality in this case. + _correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance; } /// /// 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 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(IKeyProvider key, int step = 30, OtpHashMode mode = OtpHashMode.Sha1, int totpSize = 6, TimeCorrection timeCorrection = null) + /// 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( + IKeyProvider key, + int step = 30, + OtpHashMode mode = OtpHashMode.Sha1, + int totpSize = 6, + TimeCorrection timeCorrection = null) : base(key, mode) { VerifyParameters(step, totpSize); - this.step = step; - this.totpSize = totpSize; + _step = step; + _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; + // we never null check the corrected time object. + // Since it's readonly, we'll ensure that it isn't null here and provide neutral functionality in this case. + _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"); + if (step <= 0) + throw new ArgumentOutOfRangeException(nameof(step)); + if (totpSize <= 0) + throw new ArgumentOutOfRangeException(nameof(totpSize)); + if (totpSize > 10) + throw new ArgumentOutOfRangeException(nameof(totpSize)); } /// @@ -105,27 +123,23 @@ private static void VerifyParameters(int step, int totpSize) /// /// The timestamp to use for the TOTP calculation /// a TOTP value - public string ComputeTotp(DateTime timestamp) - { - return ComputeTotpFromSpecificTime(this.correctedTime.GetCorrectedTime(timestamp)); - } + public string ComputeTotp(DateTime timestamp) => + ComputeTotpFromSpecificTime(_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. + /// 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); - } + public string ComputeTotp() => ComputeTotpFromSpecificTime(_correctedTime.CorrectedUtcNow); private string ComputeTotpFromSpecificTime(DateTime timestamp) { var window = CalculateTimeStepFromTimestamp(timestamp); - return this.Compute(window, this.hashMode); + return Compute(window, _hashMode); } /// @@ -142,10 +156,8 @@ private string ComputeTotpFromSpecificTime(DateTime timestamp) /// /// 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); - } + public bool VerifyTotp(string totp, out long timeStepMatched, VerificationWindow window = null) => + VerifyTotpForSpecificTime(_correctedTime.CorrectedUtcNow, totp, window, out timeStepMatched); /// /// Verify a value that has been provided with the calculated value @@ -154,20 +166,31 @@ public bool VerifyTotp(string totp, out long timeStepMatched, VerificationWindow /// 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 + /// 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(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) + public bool VerifyTotp( + DateTime timestamp, + string totp, + out long timeStepMatched, + VerificationWindow window = null) => + VerifyTotpForSpecificTime( + _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); + return Verify(initialStep, totp, out timeStepMatched, window); } /// @@ -175,8 +198,8 @@ private bool VerifyTotpForSpecificTime(DateTime timestamp, string totp, Verifica /// private long CalculateTimeStepFromTimestamp(DateTime timestamp) { - var unixTimestamp = (timestamp.Ticks - unixEpochTicks) / ticksToSeconds; - var window = unixTimestamp / (long)this.step; + var unixTimestamp = (timestamp.Ticks - UNIX_EPOCH_TICKS) / TICKS_TO_SECONDS; + var window = unixTimestamp / (long) _step; return window; } @@ -189,7 +212,7 @@ private long CalculateTimeStepFromTimestamp(DateTime timestamp) /// Number of remaining seconds public int RemainingSeconds() { - return RemainingSecondsForSpecificTime(this.correctedTime.CorrectedUtcNow); + return RemainingSecondsForSpecificTime(_correctedTime.CorrectedUtcNow); } /// @@ -197,15 +220,11 @@ public int RemainingSeconds() /// /// The timestamp /// Number of remaining seconds - public int RemainingSeconds(DateTime timestamp) - { - return RemainingSecondsForSpecificTime(this.correctedTime.GetCorrectedTime(timestamp)); - } + public int RemainingSeconds(DateTime timestamp) => + RemainingSecondsForSpecificTime(_correctedTime.GetCorrectedTime(timestamp)); - private int RemainingSecondsForSpecificTime(DateTime timestamp) - { - return this.step - (int)(((timestamp.Ticks - unixEpochTicks) / ticksToSeconds) % this.step); - } + private int RemainingSecondsForSpecificTime(DateTime timestamp) => + _step - (int) (((timestamp.Ticks - UNIX_EPOCH_TICKS) / TICKS_TO_SECONDS) % _step); /// /// Takes a time step and computes a TOTP code @@ -216,8 +235,8 @@ private int RemainingSecondsForSpecificTime(DateTime timestamp) protected override string Compute(long counter, OtpHashMode mode) { var data = KeyUtilities.GetBigEndianBytes(counter); - var otp = this.CalculateOtp(data, mode); - return Digits(otp, this.totpSize); + var otp = CalculateOtp(data, mode); + return Digits(otp, _totpSize); } } } \ No newline at end of file diff --git a/src/Otp.NET/VerificationWindow.cs b/src/Otp.NET/VerificationWindow.cs index d3d8591..46ef01e 100644 --- a/src/Otp.NET/VerificationWindow.cs +++ b/src/Otp.NET/VerificationWindow.cs @@ -32,8 +32,8 @@ namespace OtpNet /// public class VerificationWindow { - private readonly int previous; - private readonly int future; + private readonly int _previous; + private readonly int _future; /// /// Create an instance of a verification window @@ -42,19 +42,19 @@ public class VerificationWindow /// The number of future frames to accept public VerificationWindow(int previous = 0, int future = 0) { - this.previous = previous; - this.future = future; + _previous = previous; + _future = future; } /// - /// Gets an enumberable of all the possible validation candidates + /// Gets an enumerable of all the possible validation candidates /// /// The initial frame to validate - /// Enumberable of all possible frames that need to be validated + /// Enumerable of all possible frames that need to be validated public IEnumerable ValidationCandidates(long initialFrame) { yield return initialFrame; - for(int i = 1; i <= previous; i++) + for(var i = 1; i <= _previous; i++) { var val = initialFrame - i; if(val < 0) @@ -62,13 +62,14 @@ public IEnumerable ValidationCandidates(long initialFrame) yield return val; } - for(int i = 1; i <= future; i++) + for(var i = 1; i <= _future; i++) yield return initialFrame + i; } /// - /// The verification window that accomodates network delay that is recommended in the RFC + /// The verification window that accommodates network delay that is recommended in the RFC /// - public static readonly VerificationWindow RfcSpecifiedNetworkDelay = new VerificationWindow(previous: 1, future: 1); + public static readonly VerificationWindow RfcSpecifiedNetworkDelay = + new VerificationWindow(previous: 1, future: 1); } } diff --git a/src/Otp.NET/otp-net.snk b/src/Otp.NET/otp-net.snk new file mode 100644 index 0000000000000000000000000000000000000000..b74969a49730ec432260838a32fb1b6633bd6efc GIT binary patch literal 1172 zcmV;F1Z(>T0ssI2Bme+XQ$aES2mk;90096W^I1qmYDl`A)M<@&B;fw6tx*+S&Oj9FleNEEEP(u7szid zf56T}h1pd#nlRo4qK}8f98PRkjz|^ZzrpeiEpnX7v^UMyVmL)0IF&xQTHg(lB?G4q z3AV1LO5&KEEx*^yscH-XDckpq+rM6ykQ8M#`EG%W54-=NHQO8|83?Cu7r8WdT${Xv zwRJX%)~wK-Ma+&2)u@4wmbD(!H|#Oo z$_la-nRQ%wG&0uYAeW*{DD3;Io$smjvx%B!h;yjEdqb*ouhDEAYr}LtYH4k(GsUXj za=<}WA9oF6VpRsUYU!B=nHTZXH7JPiS}b{lk&RNG&20kFes!b_H8!yiXg$z4jqv5@ zji&x+>F`8vm{OOg5f!LU8zp6)Ss5NH68=f6^4BQsfQpFxR%Y6N;3H-w%j)h!GD6&; z%G@coNAbQX%h@qW{qA+~-6Ck7LV{U7;Z4$gKcwi_^HI+aC)I&a*4tEzm$Z_Oe{@K@ zwABzXvG~BuBDb15twFr&c8~{HFJ_OdNc{em{5~scu#vsKK@5q|o8G7crLz}-_C`HZ zcIEztM6>basniCP?Ip4SEEcxm`2S21xYt>(>A1O$g>d+;= z1+K%>Aw8~AglzzngVeEC>}FU7wzK1TNL<4jjGy4W2-w#6Z=m)@lMK`31M- z=I|sBBpX2m^8r=Mt+Q~hs4#aj*BX=?8}#b?nW{2=DA%swjl7PZ{&$Lwl7>G8t{JFb7pQ*J;6_1wG(kV2-rw*vnQ%TP@% z{sxQ5TzxRmz9E-|a)Biz;TaT5{0j#{-TCzB7nW=c6uG?tbj+^S2QD7k2Zw`soT>vC m&)RM38o{W+oD{vD`c6JfWq4#H*>-%OhJ!0(HNr->#eNUBgh=iH literal 0 HcmV?d00001 diff --git a/test/Otp.NET.Test/HotpTest.cs b/test/Otp.NET.Test/HotpTest.cs index 041e33d..e880450 100644 --- a/test/Otp.NET.Test/HotpTest.cs +++ b/test/Otp.NET.Test/HotpTest.cs @@ -1,4 +1,4 @@ -using Moq; +using Moq; using NUnit.Framework; namespace OtpNet.Test @@ -6,7 +6,7 @@ namespace OtpNet.Test [TestFixture] public class HotpTest { - private static readonly byte[] rfc4226Secret = new byte[] { + private static readonly byte[] rfc4226Secret = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30 @@ -28,16 +28,16 @@ public void ComputeHOTPRfc4226Test(OtpHashMode hash, long counter, string expect string otp = otpCalc.ComputeHOTP(counter); Assert.That(otp, Is.EqualTo(expectedOtp)); } - - [Test()] + + [Test] public void ContructorWithKeyProviderTest() - { + { //Mock a key provider which always returns an all-zero HMAC (causing an all-zero OTP) - Mock keyMock = new Mock(); - keyMock.Setup(key => key.ComputeHmac(It.Is(m => m == OtpHashMode.Sha1), It.IsAny())).Returns(new byte[20]); + Mock keyMock = new Mock(); + keyMock.Setup(key => key.ComputeHmac(It.Is(m => m == OtpHashMode.Sha1), It.IsAny())).Returns(new byte[20]); - var otp = new Hotp(keyMock.Object, OtpHashMode.Sha1, 6); - Assert.That(otp.ComputeHOTP(0), Is.EqualTo("000000")); + var otp = new Hotp(keyMock.Object, OtpHashMode.Sha1, 6); + Assert.That(otp.ComputeHOTP(0), Is.EqualTo("000000")); Assert.That(otp.ComputeHOTP(1), Is.EqualTo("000000")); } } diff --git a/test/Otp.NET.Test/Otp.NET.Test.csproj b/test/Otp.NET.Test/Otp.NET.Test.csproj index f55592a..ecd4270 100644 --- a/test/Otp.NET.Test/Otp.NET.Test.csproj +++ b/test/Otp.NET.Test/Otp.NET.Test.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2 + net5.0;netcoreapp2.2 false OtpNet.Test diff --git a/test/Otp.NET.Test/TotpTest.cs b/test/Otp.NET.Test/TotpTest.cs index 5e5299a..4b359d1 100644 --- a/test/Otp.NET.Test/TotpTest.cs +++ b/test/Otp.NET.Test/TotpTest.cs @@ -5,7 +5,7 @@ namespace OtpNet.Test { - [TestFixture()] + [TestFixture] public class TotpTest { private const string rfc6238SecretSha1 = "12345678901234567890"; @@ -38,7 +38,7 @@ public void ComputeTOTPTest(string secret, OtpHashMode hash, long timestamp, str Assert.That(otp, Is.EqualTo(expectedOtp)); } - [Test()] + [Test] public void ContructorWithKeyProviderTest() { //Mock a key provider which always returns an all-zero HMAC (causing an all-zero OTP) From c0d4b5a3541a14198d338141c19fdeeb3aafb737 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 11:48:38 -0500 Subject: [PATCH 06/48] refactoring --- src/Otp.NET/Base32Encoding.cs | 56 +++++++++++++++----------- src/Otp.NET/Hotp.cs | 9 +++-- src/Otp.NET/InMemoryKey.cs | 19 +++++---- src/Otp.NET/KeyGeneration.cs | 25 +++++++----- src/Otp.NET/KeyUtilities.cs | 4 +- src/Otp.NET/Otp.NET.csproj | 3 +- src/Otp.NET/Otp.cs | 16 ++++---- src/Otp.NET/OtpType.cs | 2 +- src/Otp.NET/TimeCorrection.cs | 2 +- src/Otp.NET/Totp.cs | 37 ++++++++++------- src/Otp.NET/VerificationWindow.cs | 10 +++-- src/Otp.NET/otp-net.snk | Bin 1172 -> 0 bytes test/Otp.NET.Test/Otp.NET.Test.csproj | 2 +- test/Otp.NET.Test/OtpUriTest.cs | 2 +- 14 files changed, 108 insertions(+), 79 deletions(-) delete mode 100644 src/Otp.NET/otp-net.snk diff --git a/src/Otp.NET/Base32Encoding.cs b/src/Otp.NET/Base32Encoding.cs index d0f4b5d..7cac603 100644 --- a/src/Otp.NET/Base32Encoding.cs +++ b/src/Otp.NET/Base32Encoding.cs @@ -12,24 +12,26 @@ public class Base32Encoding { public static byte[] ToBytes(string input) { - if(string.IsNullOrEmpty(input)) + if (string.IsNullOrEmpty(input)) { throw new ArgumentNullException(nameof(input)); } - input = input.TrimEnd('='); //remove padding characters - var byteCount = input.Length * 5 / 8; //this must be TRUNCATED + // remove padding characters + input = input.TrimEnd('='); + // this must be TRUNCATED + var byteCount = input.Length * 5 / 8; var returnArray = new byte[byteCount]; byte curByte = 0, bitsRemaining = 8; var arrayIndex = 0; - foreach(var c in input) + foreach (var c in input) { var cValue = CharToValue(c); int mask; - if(bitsRemaining > 5) + if (bitsRemaining > 5) { mask = cValue << (bitsRemaining - 5); curByte = (byte)(curByte | mask); @@ -45,8 +47,8 @@ public static byte[] ToBytes(string input) } } - //if we didn't end with a full byte - if(arrayIndex != byteCount) + // if we didn't end with a full byte + if (arrayIndex != byteCount) { returnArray[arrayIndex] = curByte; } @@ -56,23 +58,23 @@ public static byte[] ToBytes(string input) public static string ToString(byte[] input) { - if(input == null || input.Length == 0) + if (input == null || input.Length == 0) { throw new ArgumentNullException(nameof(input)); } - int charCount = (int)Math.Ceiling(input.Length / 5d) * 8; - char[] returnArray = new char[charCount]; + var charCount = (int)Math.Ceiling(input.Length / 5d) * 8; + var returnArray = new char[charCount]; byte nextChar = 0, bitsRemaining = 5; - int arrayIndex = 0; + var arrayIndex = 0; - foreach(byte b in input) + foreach (byte b in input) { nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining))); returnArray[arrayIndex++] = ValueToChar(nextChar); - if(bitsRemaining < 4) + if (bitsRemaining < 4) { nextChar = (byte)((b >> (3 - bitsRemaining)) & 31); returnArray[arrayIndex++] = ValueToChar(nextChar); @@ -83,11 +85,15 @@ public static string ToString(byte[] input) nextChar = (byte)((b << bitsRemaining) & 31); } - //if we didn't end with a full char - if(arrayIndex != charCount) + // if we didn't end with a full char + if (arrayIndex != charCount) { returnArray[arrayIndex++] = ValueToChar(nextChar); - while(arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding + // padding + while (arrayIndex != charCount) + { + returnArray[arrayIndex++] = '='; + } } return new string(returnArray); @@ -97,18 +103,20 @@ private static int CharToValue(char c) { int value = c; - //65-90 == uppercase letters - if(value < 91 && value > 64) + // 65-90 == uppercase letters + if (value < 91 && value > 64) { return value - 65; } - //50-55 == numbers 2-7 - if(value < 56 && value > 49) + + // 50-55 == numbers 2-7 + if (value < 56 && value > 49) { return value - 24; } - //97-122 == lowercase letters - if(value < 123 && value > 96) + + // 97-122 == lowercase letters + if (value < 123 && value > 96) { return value - 97; } @@ -118,12 +126,12 @@ private static int CharToValue(char c) private static char ValueToChar(byte b) { - if(b < 26) + if (b < 26) { return (char)(b + 65); } - if(b < 32) + if (b < 32) { return (char)(b + 24); } diff --git a/src/Otp.NET/Hotp.cs b/src/Otp.NET/Hotp.cs index ac12e48..54b6e59 100644 --- a/src/Otp.NET/Hotp.cs +++ b/src/Otp.NET/Hotp.cs @@ -48,7 +48,6 @@ public Hotp(byte[] secretKey, OtpHashMode mode = OtpHashMode.Sha1, int hotpSize : base(secretKey, mode) { VerifyParameters(hotpSize); - _hotpSize = hotpSize; } @@ -68,10 +67,14 @@ public Hotp(IKeyProvider key, OtpHashMode mode = OtpHashMode.Sha1, int hotpSize private static void VerifyParameters(int hotpSize) { - if(hotpSize < 6) + if (hotpSize < 6) + { throw new ArgumentOutOfRangeException(nameof(hotpSize)); - if(hotpSize > 8) + } + if (hotpSize > 8) + { throw new ArgumentOutOfRangeException(nameof(hotpSize)); + } } /// diff --git a/src/Otp.NET/InMemoryKey.cs b/src/Otp.NET/InMemoryKey.cs index 4b89aed..1e20995 100644 --- a/src/Otp.NET/InMemoryKey.cs +++ b/src/Otp.NET/InMemoryKey.cs @@ -49,8 +49,6 @@ namespace OtpNet /// public class InMemoryKey : IKeyProvider { - private static readonly object _platformSupportSync = new object(); - private readonly object _stateSync = new object(); private readonly byte[] _keyData; private readonly int _keyLength; @@ -61,10 +59,14 @@ public class InMemoryKey : IKeyProvider /// Plaintext key data public InMemoryKey(byte[] key) { - if(key == null) + if (key == null) + { throw new ArgumentNullException(nameof(key)); - if(key.Length <= 0) + } + if (key.Length <= 0) + { throw new ArgumentException("The key must not be empty"); + } _keyLength = key.Length; var paddedKeyLength = (int)Math.Ceiling((decimal)key.Length / (decimal)16) * 16; @@ -82,7 +84,7 @@ public InMemoryKey(byte[] key) internal byte[] GetCopyOfKey() { var plainKey = new byte[_keyLength]; - lock(_stateSync) + lock (_stateSync) { Array.Copy(_keyData, plainKey, _keyLength); } @@ -98,7 +100,7 @@ internal byte[] GetCopyOfKey() public byte[] ComputeHmac(OtpHashMode mode, byte[] data) { byte[] hashedValue; - using(var hmac = CreateHmacHash(mode)) + using (var hmac = CreateHmacHash(mode)) { var key = GetCopyOfKey(); try @@ -121,7 +123,7 @@ public byte[] ComputeHmac(OtpHashMode mode, byte[] data) private static HMAC CreateHmacHash(OtpHashMode otpHashMode) { HMAC hmacAlgorithm; - switch(otpHashMode) + switch (otpHashMode) { case OtpHashMode.Sha256: hmacAlgorithm = new HMACSHA256(); @@ -129,7 +131,8 @@ private static HMAC CreateHmacHash(OtpHashMode otpHashMode) case OtpHashMode.Sha512: hmacAlgorithm = new HMACSHA512(); break; - default: //case OtpHashMode.Sha1: + // case OtpHashMode.Sha1: + default: hmacAlgorithm = new HMACSHA1(); break; } diff --git a/src/Otp.NET/KeyGeneration.cs b/src/Otp.NET/KeyGeneration.cs index 1483a18..95dc2ec 100644 --- a/src/Otp.NET/KeyGeneration.cs +++ b/src/Otp.NET/KeyGeneration.cs @@ -41,7 +41,7 @@ public static class KeyGeneration public static byte[] GenerateRandomKey(int length) { var key = new byte[length]; - using(var rnd = RandomNumberGenerator.Create()) + using (var rnd = RandomNumberGenerator.Create()) { rnd.GetBytes(key); return key; @@ -66,13 +66,14 @@ public static byte[] GenerateRandomKey(OtpHashMode mode = OtpHashMode.Sha1) /// The hash mode to use. This will determine the resulting key lenght. The default is sha-1 (as per the RFC) which is 20 bytes /// Derived key public static byte[] DeriveKeyFromMaster( - IKeyProvider masterKey, - byte[] publicIdentifier, + IKeyProvider masterKey, + byte[] publicIdentifier, OtpHashMode mode = OtpHashMode.Sha1) { - if(masterKey == null) + if (masterKey == null) + { throw new ArgumentNullException(nameof(masterKey)); - + } return masterKey.ComputeHmac(mode, publicIdentifier); } @@ -86,32 +87,34 @@ public static byte[] DeriveKeyFromMaster( /// Derived key public static byte[] DeriveKeyFromMaster( IKeyProvider masterKey, - int serialNumber, - OtpHashMode mode = OtpHashMode.Sha1) => + int serialNumber, + OtpHashMode mode = OtpHashMode.Sha1) => DeriveKeyFromMaster(masterKey, KeyUtilities.GetBigEndianBytes(serialNumber), mode); private static HashAlgorithm GetHashAlgorithmForMode(OtpHashMode mode) { - switch(mode) + switch (mode) { case OtpHashMode.Sha256: return SHA256.Create(); case OtpHashMode.Sha512: return SHA512.Create(); - default: //case OtpHashMode.Sha1: + // case OtpHashMode.Sha1: + default: return SHA1.Create(); } } private static int LengthForMode(OtpHashMode mode) { - switch(mode) + switch (mode) { case OtpHashMode.Sha256: return 32; case OtpHashMode.Sha512: return 64; - default: //case OtpHashMode.Sha1: + // case OtpHashMode.Sha1: + default: return 20; } } diff --git a/src/Otp.NET/KeyUtilities.cs b/src/Otp.NET/KeyUtilities.cs index 24283ca..da661eb 100644 --- a/src/Otp.NET/KeyUtilities.cs +++ b/src/Otp.NET/KeyUtilities.cs @@ -46,8 +46,10 @@ internal class KeyUtilities /// internal static void Destroy(byte[] sensitiveData) { - if(sensitiveData == null) + if (sensitiveData == null) + { throw new ArgumentNullException(nameof(sensitiveData)); + } new Random().NextBytes(sensitiveData); } diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index e61e990..0e37b38 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -2,7 +2,7 @@ 1.2.2 - net45;net5.0;netstandard1.3;netstandard2.0 + net45;net6.0;netstandard1.3;netstandard2.0 Otp.NET Otp.NET Kyle Spearrin @@ -18,7 +18,6 @@ For documentation and examples visit the project website on GitHub at https://gi https://github.com/kspearrin/Otp.NET true OtpNet - otp-net.snk diff --git a/src/Otp.NET/Otp.cs b/src/Otp.NET/Otp.cs index a0b2e4f..d6cc454 100644 --- a/src/Otp.NET/Otp.cs +++ b/src/Otp.NET/Otp.cs @@ -24,7 +24,6 @@ DEALINGS IN THE SOFTWARE. */ using System; -using System.Security.Cryptography; namespace OtpNet { @@ -53,14 +52,17 @@ public abstract class Otp /// The hash mode to use public Otp(byte[] secretKey, OtpHashMode mode) { - if(secretKey == null) + if (secretKey == null) + { throw new ArgumentNullException(nameof(secretKey)); - if(secretKey.Length <= 0) + } + 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. _secretKey = new InMemoryKey(secretKey); - _hashMode = mode; } @@ -72,7 +74,6 @@ public Otp(byte[] secretKey, OtpHashMode mode) public Otp(IKeyProvider key, OtpHashMode mode) { _secretKey = key ?? throw new ArgumentNullException(nameof(key)); - _hashMode = mode; } @@ -121,13 +122,14 @@ protected internal static string Digits(long input, int digitCount) /// True if a match is found protected bool Verify(long initialStep, string valueToVerify, out long matchedStep, VerificationWindow window) { - if(window == null) + if (window == null) + { window = new VerificationWindow(); + } foreach(var frame in window.ValidationCandidates(initialStep)) { var comparisonValue = Compute(frame, _hashMode); - if(ValuesEqual(comparisonValue, valueToVerify)) { matchedStep = frame; diff --git a/src/Otp.NET/OtpType.cs b/src/Otp.NET/OtpType.cs index 669c73f..d9daf5d 100644 --- a/src/Otp.NET/OtpType.cs +++ b/src/Otp.NET/OtpType.cs @@ -13,6 +13,6 @@ public enum OtpType /// HMAC-based (counter) OTP /// see https://datatracker.ietf.org/doc/html/rfc4226 /// - Hotp, + Hotp } } diff --git a/src/Otp.NET/TimeCorrection.cs b/src/Otp.NET/TimeCorrection.cs index 7001618..a3609d7 100644 --- a/src/Otp.NET/TimeCorrection.cs +++ b/src/Otp.NET/TimeCorrection.cs @@ -73,7 +73,7 @@ public class TimeCorrection /// 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) => + public TimeCorrection(DateTime correctTime, DateTime referenceTime) => CorrectionFactor = referenceTime - correctTime; /// diff --git a/src/Otp.NET/Totp.cs b/src/Otp.NET/Totp.cs index c3a401c..2c1c80c 100644 --- a/src/Otp.NET/Totp.cs +++ b/src/Otp.NET/Totp.cs @@ -24,7 +24,6 @@ DEALINGS IN THE SOFTWARE. */ using System; -using System.Globalization; namespace OtpNet { @@ -40,12 +39,12 @@ public class Totp : Otp /// /// The number of ticks as Measured at Midnight Jan 1st 1970; /// - private const long UNIX_EPOCH_TICKS = 621355968000000000L; + private const long UnicEpocTicks = 621355968000000000L; /// /// A divisor for converting ticks to seconds /// - private const long TICKS_TO_SECONDS = 10000000L; + private const long TicksToSeconds = 10000000L; private readonly int _step; private readonly int _totpSize; @@ -111,11 +110,17 @@ public Totp( private static void VerifyParameters(int step, int totpSize) { if (step <= 0) + { throw new ArgumentOutOfRangeException(nameof(step)); + } if (totpSize <= 0) + { throw new ArgumentOutOfRangeException(nameof(totpSize)); + } if (totpSize > 10) + { throw new ArgumentOutOfRangeException(nameof(totpSize)); + } } /// @@ -123,7 +128,7 @@ private static void VerifyParameters(int step, int totpSize) /// /// The timestamp to use for the TOTP calculation /// a TOTP value - public string ComputeTotp(DateTime timestamp) => + public string ComputeTotp(DateTime timestamp) => ComputeTotpFromSpecificTime(_correctedTime.GetCorrectedTime(timestamp)); /// @@ -146,7 +151,8 @@ private string ComputeTotpFromSpecificTime(DateTime timestamp) /// 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. + /// 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 /// @@ -156,7 +162,7 @@ private string ComputeTotpFromSpecificTime(DateTime timestamp) /// /// The window of steps to verify /// True if there is a match. - public bool VerifyTotp(string totp, out long timeStepMatched, VerificationWindow window = null) => + public bool VerifyTotp(string totp, out long timeStepMatched, VerificationWindow window = null) => VerifyTotpForSpecificTime(_correctedTime.CorrectedUtcNow, totp, window, out timeStepMatched); /// @@ -173,8 +179,8 @@ public bool VerifyTotp(string totp, out long timeStepMatched, VerificationWindow /// The window of steps to verify /// True if there is a match. public bool VerifyTotp( - DateTime timestamp, - string totp, + DateTime timestamp, + string totp, out long timeStepMatched, VerificationWindow window = null) => VerifyTotpForSpecificTime( @@ -184,8 +190,8 @@ public bool VerifyTotp( out timeStepMatched); private bool VerifyTotpForSpecificTime( - DateTime timestamp, - string totp, + DateTime timestamp, + string totp, VerificationWindow window, out long timeStepMatched) { @@ -198,8 +204,8 @@ private bool VerifyTotpForSpecificTime( /// private long CalculateTimeStepFromTimestamp(DateTime timestamp) { - var unixTimestamp = (timestamp.Ticks - UNIX_EPOCH_TICKS) / TICKS_TO_SECONDS; - var window = unixTimestamp / (long) _step; + var unixTimestamp = (timestamp.Ticks - UnicEpocTicks) / TicksToSeconds; + var window = unixTimestamp / (long)_step; return window; } @@ -207,7 +213,8 @@ private long CalculateTimeStepFromTimestamp(DateTime timestamp) /// 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. + /// 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() @@ -223,8 +230,8 @@ public int RemainingSeconds() public int RemainingSeconds(DateTime timestamp) => RemainingSecondsForSpecificTime(_correctedTime.GetCorrectedTime(timestamp)); - private int RemainingSecondsForSpecificTime(DateTime timestamp) => - _step - (int) (((timestamp.Ticks - UNIX_EPOCH_TICKS) / TICKS_TO_SECONDS) % _step); + private int RemainingSecondsForSpecificTime(DateTime timestamp) => + _step - (int)(((timestamp.Ticks - UnicEpocTicks) / TicksToSeconds) % _step); /// /// Takes a time step and computes a TOTP code diff --git a/src/Otp.NET/VerificationWindow.cs b/src/Otp.NET/VerificationWindow.cs index 46ef01e..5ffa220 100644 --- a/src/Otp.NET/VerificationWindow.cs +++ b/src/Otp.NET/VerificationWindow.cs @@ -54,22 +54,24 @@ public VerificationWindow(int previous = 0, int future = 0) public IEnumerable ValidationCandidates(long initialFrame) { yield return initialFrame; - for(var i = 1; i <= _previous; i++) + for (var i = 1; i <= _previous; i++) { var val = initialFrame - i; - if(val < 0) + if (val < 0) break; yield return val; } - for(var i = 1; i <= _future; i++) + for (var i = 1; i <= _future; i++) + { yield return initialFrame + i; + } } /// /// The verification window that accommodates network delay that is recommended in the RFC /// - public static readonly VerificationWindow RfcSpecifiedNetworkDelay = + public static readonly VerificationWindow RfcSpecifiedNetworkDelay = new VerificationWindow(previous: 1, future: 1); } } diff --git a/src/Otp.NET/otp-net.snk b/src/Otp.NET/otp-net.snk deleted file mode 100644 index b74969a49730ec432260838a32fb1b6633bd6efc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1172 zcmV;F1Z(>T0ssI2Bme+XQ$aES2mk;90096W^I1qmYDl`A)M<@&B;fw6tx*+S&Oj9FleNEEEP(u7szid zf56T}h1pd#nlRo4qK}8f98PRkjz|^ZzrpeiEpnX7v^UMyVmL)0IF&xQTHg(lB?G4q z3AV1LO5&KEEx*^yscH-XDckpq+rM6ykQ8M#`EG%W54-=NHQO8|83?Cu7r8WdT${Xv zwRJX%)~wK-Ma+&2)u@4wmbD(!H|#Oo z$_la-nRQ%wG&0uYAeW*{DD3;Io$smjvx%B!h;yjEdqb*ouhDEAYr}LtYH4k(GsUXj za=<}WA9oF6VpRsUYU!B=nHTZXH7JPiS}b{lk&RNG&20kFes!b_H8!yiXg$z4jqv5@ zji&x+>F`8vm{OOg5f!LU8zp6)Ss5NH68=f6^4BQsfQpFxR%Y6N;3H-w%j)h!GD6&; z%G@coNAbQX%h@qW{qA+~-6Ck7LV{U7;Z4$gKcwi_^HI+aC)I&a*4tEzm$Z_Oe{@K@ zwABzXvG~BuBDb15twFr&c8~{HFJ_OdNc{em{5~scu#vsKK@5q|o8G7crLz}-_C`HZ zcIEztM6>basniCP?Ip4SEEcxm`2S21xYt>(>A1O$g>d+;= z1+K%>Aw8~AglzzngVeEC>}FU7wzK1TNL<4jjGy4W2-w#6Z=m)@lMK`31M- z=I|sBBpX2m^8r=Mt+Q~hs4#aj*BX=?8}#b?nW{2=DA%swjl7PZ{&$Lwl7>G8t{JFb7pQ*J;6_1wG(kV2-rw*vnQ%TP@% z{sxQ5TzxRmz9E-|a)Biz;TaT5{0j#{-TCzB7nW=c6uG?tbj+^S2QD7k2Zw`soT>vC m&)RM38o{W+oD{vD`c6JfWq4#H*>-%OhJ!0(HNr->#eNUBgh=iH diff --git a/test/Otp.NET.Test/Otp.NET.Test.csproj b/test/Otp.NET.Test/Otp.NET.Test.csproj index ecd4270..2c0081a 100644 --- a/test/Otp.NET.Test/Otp.NET.Test.csproj +++ b/test/Otp.NET.Test/Otp.NET.Test.csproj @@ -1,7 +1,7 @@  - net5.0;netcoreapp2.2 + net6.0 false OtpNet.Test diff --git a/test/Otp.NET.Test/OtpUriTest.cs b/test/Otp.NET.Test/OtpUriTest.cs index d037493..32d819d 100644 --- a/test/Otp.NET.Test/OtpUriTest.cs +++ b/test/Otp.NET.Test/OtpUriTest.cs @@ -11,7 +11,7 @@ public class OtpUriTest private const string _baseIssuer = "ACME Co"; [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha1, 6, 30, 0, - "otpauth://totp/ACME%20Co:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20&algorithm=SHA1&digits=6&period=30")] + "otpauth://totp/ACME%20Co:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30")] public void GenerateOtpUriTest(string secret, OtpType otpType, string user, string issuer, OtpHashMode hash, int digits, int period, int counter, string expectedUri) { From b4a15ac4b531b29fc137da84783f0122906c639b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 11:52:08 -0500 Subject: [PATCH 07/48] build status --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5888976..a780a3f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ An implementation TOTP [RFC 6238](http://tools.ietf.org/html/rfc6238) and HOTP [RFC 4226](http://tools.ietf.org/html/rfc4226) in C#. +[![Build status](https://ci.appveyor.com/api/projects/status/renwlv60h7cfxs34?svg=true)](https://ci.appveyor.com/project/kspearrin/otp-net) + ## Get it on NuGet https://www.nuget.org/packages/Otp.NET From 9b2e0795ea013d1899ed225800865802fe7850e1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 12:04:15 -0500 Subject: [PATCH 08/48] bump version --- src/Otp.NET/Otp.NET.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index 0e37b38..981d13a 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -1,7 +1,7 @@  - 1.2.2 + 1.3.0 net45;net6.0;netstandard1.3;netstandard2.0 Otp.NET Otp.NET From 2bd90dd35923b0129e54e260697ba1883f68b989 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 12:25:04 -0500 Subject: [PATCH 09/48] Update README.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index a780a3f..aaed98d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ PM> Install-Package Otp.NET - [TOTP (Timed One Time Password)](#totp-timed-one-time-password) - [HOTP (HMAC-based One Time Password)](#hotp-hmac-based-one-time-password) +- [OTP Uri](#otp-uri) - [Base32 Encoding](#base32-encoding) ### TOTP (Timed One Time Password) @@ -197,6 +198,15 @@ The HOTP implementation provides a mechanism for verifying HOTP codes that are p public bool VerifyHotp(string totp, long counter); ``` +### OTP Uri + +You can use the OtpUri class to generate OTP style uris in the "Key Uri Format" as defined here: https://github.com/google/google-authenticator/wiki/Key-Uri-Format + +```c# +var uriString = new OtpUri(OtpType.Totp, "JBSWY3DPEHPK3PXP", "alice@google.com", "ACME Co").ToString(); +// uriString is otpauth://totp/ACME%20Co:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 +``` + ### Base32 Encoding Also included is a Base32 helper. From a9f3111306fd17b966acc32ee8ab14eecfe388e6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 12:25:45 -0500 Subject: [PATCH 10/48] accept string secret for otp uri --- src/Otp.NET/OtpUri.cs | 24 ++++++++++++++++++++---- test/Otp.NET.Test/OtpUriTest.cs | 3 +-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Otp.NET/OtpUri.cs b/src/Otp.NET/OtpUri.cs index bd8510d..59d0366 100644 --- a/src/Otp.NET/OtpUri.cs +++ b/src/Otp.NET/OtpUri.cs @@ -17,7 +17,7 @@ public class OtpUri /// The secret parameter is an arbitrary key value encoded in Base32 according to RFC 3548. /// The padding specified in RFC 3548 section 2.2 is not required and should be omitted. /// - public readonly byte[] Secret; + public readonly string Secret; /// /// Which account a key is associated with @@ -52,11 +52,11 @@ public class OtpUri /// - /// Create a new OTPAuthUri + /// Create a new OTP Auth Uri /// public OtpUri( OtpType schema, - byte[] secret, + string secret, string user, string issuer = null, OtpHashMode algorithm = OtpHashMode.Sha1, @@ -89,6 +89,22 @@ public OtpUri( } } + /// + /// Create a new OTP Auth Uri + /// + public OtpUri( + OtpType schema, + byte[] secret, + string user, + string issuer = null, + OtpHashMode algorithm = OtpHashMode.Sha1, + int digits = 6, + int period = 30, + int counter = 0) + : this(schema, Base32Encoding.ToString(secret), user, issuer, + algorithm, digits, period, counter) + { } + /// /// Generates a Uri according to the parameters /// @@ -106,7 +122,7 @@ public override string ToString() { var parameters = new Dictionary { - { "secret", Base32Encoding.ToString(Secret) } + { "secret", Secret } }; if (!string.IsNullOrWhiteSpace(Issuer)) diff --git a/test/Otp.NET.Test/OtpUriTest.cs b/test/Otp.NET.Test/OtpUriTest.cs index 32d819d..7f9747f 100644 --- a/test/Otp.NET.Test/OtpUriTest.cs +++ b/test/Otp.NET.Test/OtpUriTest.cs @@ -15,8 +15,7 @@ public class OtpUriTest public void GenerateOtpUriTest(string secret, OtpType otpType, string user, string issuer, OtpHashMode hash, int digits, int period, int counter, string expectedUri) { - var sec = Base32Encoding.ToBytes(secret); - var uriString = new OtpUri(otpType, sec, user, issuer, hash, digits, period, counter).ToString(); + var uriString = new OtpUri(otpType, secret, user, issuer, hash, digits, period, counter).ToString(); Assert.That(uriString, Is.EqualTo(expectedUri)); } } From 347afe759acbd9fbe3b27ba0cc787a0f416d0b90 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 14:02:38 -0500 Subject: [PATCH 11/48] snk signing key --- .gitignore | 3 ++- Otp.NET.sln | 5 +++-- src/Otp.NET/Otp.NET.csproj | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 726cc91..b0d39c8 100644 --- a/.gitignore +++ b/.gitignore @@ -252,4 +252,5 @@ paket-files/ *.sln.iml .nuget/ -node_modules/ \ No newline at end of file +node_modules/ +*.snk \ No newline at end of file diff --git a/Otp.NET.sln b/Otp.NET.sln index a607f5e..862cec5 100644 --- a/Otp.NET.sln +++ b/Otp.NET.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29411.108 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33110.190 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D0E9D08A-7F66-426A-8645-C2D92DB4D200}" EndProject @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{81C89CBC-2 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2A4A70D0-4968-4E0C-9E42-4AF2C076D38B}" ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore appveyor.yml = appveyor.yml README.md = README.md EndProjectSection diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index 981d13a..5f5625c 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -18,6 +18,7 @@ For documentation and examples visit the project website on GitHub at https://gi https://github.com/kspearrin/Otp.NET true OtpNet + vs-signing-key.snk From 92ab19dbeddb905b2f3e146942a89f9bc33cbba2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 14:08:16 -0500 Subject: [PATCH 12/48] AssemblyOriginatorKeyFile only on release --- appveyor.yml | 4 ++-- src/Otp.NET/Otp.NET.csproj | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index aed33b8..43d2f73 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,8 +2,8 @@ before_build: - dotnet restore build_script: - - dotnet build --no-restore - - dotnet pack src\Otp.NET\Otp.NET.csproj --no-build -o ../dist + - dotnet build -c "Release" --no-restore + - dotnet pack ./src/Otp.NET/Otp.NET.csproj --no-build -o ../dist -c "Release" test_script: - dotnet test --no-build diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index 5f5625c..af894db 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -18,6 +18,9 @@ For documentation and examples visit the project website on GitHub at https://gi https://github.com/kspearrin/Otp.NET true OtpNet + + + vs-signing-key.snk From 2778bd332e8f9d9564693b7f9d4b25b4a3f39027 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 14:16:34 -0500 Subject: [PATCH 13/48] update license file reference --- Otp.NET.sln | 1 + src/Otp.NET/Otp.NET.csproj | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Otp.NET.sln b/Otp.NET.sln index 862cec5..e3bed79 100644 --- a/Otp.NET.sln +++ b/Otp.NET.sln @@ -14,6 +14,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .gitignore = .gitignore appveyor.yml = appveyor.yml + LICENSE.txt = LICENSE.txt README.md = README.md EndProjectSection EndProject diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index af894db..6483620 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -8,7 +8,7 @@ Kyle Spearrin Kyle Spearrin https://github.com/kspearrin/Otp.NET - https://raw.githubusercontent.com/kspearrin/Otp.NET/master/LICENSE.txt + LICENSE.txt An implementation of TOTP and HOTP which are 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 and examples visit the project website on GitHub at https://github.com/kspearrin/Otp.NET @@ -29,4 +29,8 @@ For documentation and examples visit the project website on GitHub at https://gi + + + + From be11196eb30ce4b700ab1bf91a8e34cf011fb6ae Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 14:21:33 -0500 Subject: [PATCH 14/48] package icon --- icon.png | Bin 0 -> 819 bytes src/Otp.NET/Otp.NET.csproj | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 icon.png diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..65708d89fe79a255b03411c47bb69114b4cd3741 GIT binary patch literal 819 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz$hEw6XFWwo)wlGMOZ`NjrKWE z$ilhfBSXkTIIXXGJ_yz`r zg-1rm#3m-ErDx~l7FJZ(H#N6*PMJM->FTxXH*DR$^Wf1_XU<)`eD(UhNAEv=|M~mx z{rSc_fl-p~>Eaj?aro`jn_-6xL|iXlTg204vDo6gbD*P$`y6(ci(DL6pZ&K#ynMEq zs^@IGT}5)6-`~u=yiP3gXJ(gWLrZV^Onv~iva_pZ>R0GY=87iBn zUHRw1ZT#6*a_#*82j}eY4*bO(F=6Y6e_OPL9ymqV-xU3}aWbc%NzLK;N9U|mG~WID zUG$#|Eq`_IAK&~&_k4Zj>uD?2f8WEO-oRAO@<)xON!F-Lde3F=raqP?KAycV9hcZL zzy4UB*c79BmtUCgK;*gw9LniByZE=9616$x(dFv9z;_+Py~eZ0N)0}k9Tc!*bYeeH zseVO(=gr2SRv!}k6j+&8=fpA0d(3gkV8fwj#Rra0eQ=-a*s1U*S2AOaKQsMV820k_ z#n25YdN2Mh%8`HU>lH9ve2Ye-vi<`9R*Bj*+KoaQVDy0{kB9MV`lhG9W9#K3KQ#q= rE!cbN+ibQ)ywh|y^S?E^dC0!8BH7om)qD{!av3~b{an^LB{Ts5chN#W literal 0 HcmV?d00001 diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index 6483620..171c7e0 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -14,7 +14,7 @@ For documentation and examples visit the project website on GitHub at https://github.com/kspearrin/Otp.NET An implementation of TOTP which is commonly used for multi factor authentication. otp,totp,hotp,2fa,two factor - http://i.imgur.com/XFfC64v.png + icon.png https://github.com/kspearrin/Otp.NET true OtpNet @@ -30,7 +30,8 @@ For documentation and examples visit the project website on GitHub at https://gi - + + From 168f5cfe23362b6f267c9b70cbe31b48e8a08fbb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 14:29:05 -0500 Subject: [PATCH 15/48] change to EscapeDataString --- src/Otp.NET/OtpUri.cs | 8 ++++---- test/Otp.NET.Test/OtpUriTest.cs | 10 ++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Otp.NET/OtpUri.cs b/src/Otp.NET/OtpUri.cs index 59d0366..d1d3296 100644 --- a/src/Otp.NET/OtpUri.cs +++ b/src/Otp.NET/OtpUri.cs @@ -127,7 +127,7 @@ public override string ToString() if (!string.IsNullOrWhiteSpace(Issuer)) { - parameters.Add("issuer", Issuer); + parameters.Add("issuer", Uri.EscapeDataString(Issuer)); } parameters.Add("algorithm", Algorithm.ToString().ToUpper()); parameters.Add("digits", Digits.ToString()); @@ -149,10 +149,10 @@ public override string ToString() // The label if (!string.IsNullOrWhiteSpace(Issuer)) { - uriBuilder.Append(Issuer); + uriBuilder.Append(Uri.EscapeDataString(Issuer)); uriBuilder.Append(":"); } - uriBuilder.Append(User); + uriBuilder.Append(Uri.EscapeDataString(User)); // Start of the parameters uriBuilder.Append("?"); @@ -166,7 +166,7 @@ public override string ToString() // Remove last "&" uriBuilder.Remove(uriBuilder.Length - 1, 1); - return Uri.EscapeUriString(uriBuilder.ToString()); + return uriBuilder.ToString(); } } } diff --git a/test/Otp.NET.Test/OtpUriTest.cs b/test/Otp.NET.Test/OtpUriTest.cs index 7f9747f..835b505 100644 --- a/test/Otp.NET.Test/OtpUriTest.cs +++ b/test/Otp.NET.Test/OtpUriTest.cs @@ -3,7 +3,7 @@ namespace OtpNet.Test { - [TestFixture()] + [TestFixture] public class OtpUriTest { private const string _baseSecret = "JBSWY3DPEHPK3PXP"; @@ -11,7 +11,13 @@ public class OtpUriTest private const string _baseIssuer = "ACME Co"; [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha1, 6, 30, 0, - "otpauth://totp/ACME%20Co:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30")] + "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30")] + [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha256, 6, 30, 0, + "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA256&digits=6&period=30")] + [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha512, 6, 30, 0, + "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA512&digits=6&period=30")] + [TestCase(_baseSecret, OtpType.Hotp, _baseUser, _baseIssuer, OtpHashMode.Sha512, 6, 30, 0, + "otpauth://hotp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA512&digits=6&counter=0")] public void GenerateOtpUriTest(string secret, OtpType otpType, string user, string issuer, OtpHashMode hash, int digits, int period, int counter, string expectedUri) { From 6c057152d44003002d646dff92dc2955379aa6ba Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 14:29:56 -0500 Subject: [PATCH 16/48] fix OTP uri example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aaed98d..33b2955 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ You can use the OtpUri class to generate OTP style uris in the "Key Uri Format" ```c# var uriString = new OtpUri(OtpType.Totp, "JBSWY3DPEHPK3PXP", "alice@google.com", "ACME Co").ToString(); -// uriString is otpauth://totp/ACME%20Co:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 +// uriString is otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 ``` ### Base32 Encoding From d84651ebacbbf48fb8517e6d296ecf4cdd65b52d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 14:31:05 -0500 Subject: [PATCH 17/48] package readme --- src/Otp.NET/Otp.NET.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index 171c7e0..3c397a0 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -15,6 +15,7 @@ For documentation and examples visit the project website on GitHub at https://gi An implementation of TOTP which is commonly used for multi factor authentication. otp,totp,hotp,2fa,two factor icon.png + README.md https://github.com/kspearrin/Otp.NET true OtpNet @@ -31,6 +32,7 @@ For documentation and examples visit the project website on GitHub at https://gi + From 159b4c72f0f53c22eb108c266dd19b2d4f5518d4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Dec 2022 14:42:37 -0500 Subject: [PATCH 18/48] update dist path --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 43d2f73..f52679b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ before_build: build_script: - dotnet build -c "Release" --no-restore - - dotnet pack ./src/Otp.NET/Otp.NET.csproj --no-build -o ../dist -c "Release" + - dotnet pack ./src/Otp.NET/Otp.NET.csproj --no-build -o ./dist -c "Release" test_script: - dotnet test --no-build From 505c3e8175dee64696165cafa1be1418df1b8a48 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Jan 2023 08:40:15 -0500 Subject: [PATCH 19/48] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 33b2955..6322876 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,14 @@ An implementation TOTP [RFC 6238](http://tools.ietf.org/html/rfc6238) and HOTP [ https://www.nuget.org/packages/Otp.NET +```powershell +Install-Package Otp.NET ``` -PM> Install-Package Otp.NET + +or + +```bash +dotnet add package Otp.NET ``` ## Documentation From 675daf5c2f545cb12f28d67b53ecb1fc98616f24 Mon Sep 17 00:00:00 2001 From: Braydon Davis <113562987+DSC-bdavis@users.noreply.github.com> Date: Sat, 8 Apr 2023 08:01:09 -0600 Subject: [PATCH 20/48] Add support for .NET 7 (#42) Change support for .NET 4.5 to .NET 4.8 --- src/Otp.NET/Otp.NET.csproj | 6 +++--- test/Otp.NET.Test/Otp.NET.Test.csproj | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index 3c397a0..6f2afa6 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -2,7 +2,7 @@ 1.3.0 - net45;net6.0;netstandard1.3;netstandard2.0 + net48;net6.0;net7.0;netstandard1.3;netstandard2.0 Otp.NET Otp.NET Kyle Spearrin @@ -31,8 +31,8 @@ For documentation and examples visit the project website on GitHub at https://gi - - + + diff --git a/test/Otp.NET.Test/Otp.NET.Test.csproj b/test/Otp.NET.Test/Otp.NET.Test.csproj index 2c0081a..9b6a256 100644 --- a/test/Otp.NET.Test/Otp.NET.Test.csproj +++ b/test/Otp.NET.Test/Otp.NET.Test.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 false OtpNet.Test From 6a62bf1cfe4dc360fbcb37bd2047f6aaaa89ce62 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 8 Apr 2023 10:10:26 -0400 Subject: [PATCH 21/48] trim end of secret, resolves #40 --- src/Otp.NET/OtpUri.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Otp.NET/OtpUri.cs b/src/Otp.NET/OtpUri.cs index d1d3296..2d19c03 100644 --- a/src/Otp.NET/OtpUri.cs +++ b/src/Otp.NET/OtpUri.cs @@ -50,7 +50,6 @@ public class OtpUri /// public readonly int Counter; - /// /// Create a new OTP Auth Uri /// @@ -122,7 +121,7 @@ public override string ToString() { var parameters = new Dictionary { - { "secret", Secret } + { "secret", Secret.TrimEnd('=') } }; if (!string.IsNullOrWhiteSpace(Issuer)) From e3b561305102594de49f306e25e264650d5e3843 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 8 Apr 2023 10:21:44 -0400 Subject: [PATCH 22/48] update target frameworks --- src/Otp.NET/Otp.NET.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index 6f2afa6..591947d 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -2,7 +2,7 @@ 1.3.0 - net48;net6.0;net7.0;netstandard1.3;netstandard2.0 + net461;net5.0;net6.0;net7.0;netstandard2.0 Otp.NET Otp.NET Kyle Spearrin From af9e4bf575dfc149f581bde55bc32039d286fba0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 10 Apr 2023 09:54:21 -0400 Subject: [PATCH 23/48] move to latest c# lang features --- src/Otp.NET/Base32Encoding.cs | 189 +++++++------- src/Otp.NET/Hotp.cs | 133 +++++----- src/Otp.NET/IKeyProvider.cs | 35 ++- src/Otp.NET/InMemoryKey.cs | 187 +++++++------- src/Otp.NET/KeyGeneration.cs | 151 ++++++----- src/Otp.NET/KeyUtilities.cs | 93 ++++--- src/Otp.NET/Otp.NET.csproj | 6 +- src/Otp.NET/Otp.cs | 217 ++++++++-------- src/Otp.NET/OtpHashMode.cs | 33 ++- src/Otp.NET/OtpType.cs | 29 ++- src/Otp.NET/OtpUri.cs | 309 ++++++++++++----------- src/Otp.NET/TimeCorrection.cs | 117 +++++---- src/Otp.NET/Totp.cs | 401 +++++++++++++++--------------- src/Otp.NET/VerificationWindow.cs | 77 +++--- test/Otp.NET.Test/HotpTest.cs | 69 +++-- test/Otp.NET.Test/OtpUriTest.cs | 41 ++- test/Otp.NET.Test/TotpTest.cs | 81 +++--- 17 files changed, 1074 insertions(+), 1094 deletions(-) diff --git a/src/Otp.NET/Base32Encoding.cs b/src/Otp.NET/Base32Encoding.cs index 7cac603..982058a 100644 --- a/src/Otp.NET/Base32Encoding.cs +++ b/src/Otp.NET/Base32Encoding.cs @@ -6,137 +6,136 @@ using System; -namespace OtpNet +namespace OtpNet; + +public class Base32Encoding { - public class Base32Encoding + public static byte[] ToBytes(string input) { - public static byte[] ToBytes(string input) + if (string.IsNullOrEmpty(input)) { - if (string.IsNullOrEmpty(input)) - { - throw new ArgumentNullException(nameof(input)); - } + throw new ArgumentNullException(nameof(input)); + } + + // remove padding characters + input = input.TrimEnd('='); + // this must be TRUNCATED + var byteCount = input.Length * 5 / 8; + var returnArray = new byte[byteCount]; - // remove padding characters - input = input.TrimEnd('='); - // this must be TRUNCATED - var byteCount = input.Length * 5 / 8; - var returnArray = new byte[byteCount]; + byte curByte = 0, bitsRemaining = 8; + var arrayIndex = 0; - byte curByte = 0, bitsRemaining = 8; - var arrayIndex = 0; + foreach (var c in input) + { + var cValue = CharToValue(c); - foreach (var c in input) + int mask; + if (bitsRemaining > 5) { - var cValue = CharToValue(c); - - int mask; - 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; - } + mask = cValue << (bitsRemaining - 5); + curByte = (byte)(curByte | mask); + bitsRemaining -= 5; } - - // if we didn't end with a full byte - if (arrayIndex != byteCount) + else { - returnArray[arrayIndex] = curByte; + mask = cValue >> (5 - bitsRemaining); + curByte = (byte)(curByte | mask); + returnArray[arrayIndex++] = curByte; + curByte = (byte)(cValue << (3 + bitsRemaining)); + bitsRemaining += 3; } - - return returnArray; } - public static string ToString(byte[] input) + // if we didn't end with a full byte + if (arrayIndex != byteCount) { - if (input == null || input.Length == 0) - { - throw new ArgumentNullException(nameof(input)); - } + returnArray[arrayIndex] = curByte; + } - var charCount = (int)Math.Ceiling(input.Length / 5d) * 8; - var returnArray = new char[charCount]; + return returnArray; + } - byte nextChar = 0, bitsRemaining = 5; - var arrayIndex = 0; + public static string ToString(byte[] input) + { + if (input == null || input.Length == 0) + { + throw new ArgumentNullException(nameof(input)); + } - foreach (byte b in input) - { - nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining))); - returnArray[arrayIndex++] = ValueToChar(nextChar); + var charCount = (int)Math.Ceiling(input.Length / 5d) * 8; + var returnArray = new char[charCount]; - if (bitsRemaining < 4) - { - nextChar = (byte)((b >> (3 - bitsRemaining)) & 31); - returnArray[arrayIndex++] = ValueToChar(nextChar); - bitsRemaining += 5; - } + byte nextChar = 0, bitsRemaining = 5; + var arrayIndex = 0; - bitsRemaining -= 3; - nextChar = (byte)((b << bitsRemaining) & 31); - } + foreach (byte b in input) + { + nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining))); + returnArray[arrayIndex++] = ValueToChar(nextChar); - // if we didn't end with a full char - if (arrayIndex != charCount) + if (bitsRemaining < 4) { + nextChar = (byte)((b >> (3 - bitsRemaining)) & 31); returnArray[arrayIndex++] = ValueToChar(nextChar); - // padding - while (arrayIndex != charCount) - { - returnArray[arrayIndex++] = '='; - } + bitsRemaining += 5; } - return new string(returnArray); + bitsRemaining -= 3; + nextChar = (byte)((b << bitsRemaining) & 31); } - private static int CharToValue(char c) + // if we didn't end with a full char + if (arrayIndex != charCount) { - int value = c; - - // 65-90 == uppercase letters - if (value < 91 && value > 64) + returnArray[arrayIndex++] = ValueToChar(nextChar); + // padding + while (arrayIndex != charCount) { - return value - 65; + returnArray[arrayIndex++] = '='; } + } - // 50-55 == numbers 2-7 - if (value < 56 && value > 49) - { - return value - 24; - } + return new string(returnArray); + } - // 97-122 == lowercase letters - if (value < 123 && value > 96) - { - return value - 97; - } + private static int CharToValue(char c) + { + int value = c; - throw new ArgumentException("Character is not a Base32 character.", nameof(c)); + // 65-90 == uppercase letters + if (value < 91 && value > 64) + { + return value - 65; } - private static char ValueToChar(byte b) + // 50-55 == numbers 2-7 + if (value < 56 && value > 49) { - if (b < 26) - { - return (char)(b + 65); - } + return value - 24; + } - if (b < 32) - { - return (char)(b + 24); - } + // 97-122 == lowercase letters + if (value < 123 && value > 96) + { + return value - 97; + } + + throw new ArgumentException("Character is not a Base32 character.", nameof(c)); + } + + private static char ValueToChar(byte b) + { + if (b < 26) + { + return (char)(b + 65); + } - throw new ArgumentException("Byte is not a Base32 value.", nameof(b)); + if (b < 32) + { + return (char)(b + 24); } + + throw new ArgumentException("Byte is not a Base32 value.", nameof(b)); } } diff --git a/src/Otp.NET/Hotp.cs b/src/Otp.NET/Hotp.cs index 54b6e59..fefbe6c 100644 --- a/src/Otp.NET/Hotp.cs +++ b/src/Otp.NET/Hotp.cs @@ -25,85 +25,84 @@ DEALINGS IN THE SOFTWARE. using System; -namespace OtpNet +namespace OtpNet; + +/// +/// Calculate HMAC-Based One-Time-Passwords (HOTP) from a secret key +/// +/// +/// The specifications for this are found in RFC 4226 +/// http://tools.ietf.org/html/rfc4226 +/// +public class Hotp : Otp { + private readonly int _hotpSize; + /// - /// Calculate HMAC-Based One-Time-Passwords (HOTP) from a secret key + /// Create a HOTP instance /// - /// - /// The specifications for this are found in RFC 4226 - /// http://tools.ietf.org/html/rfc4226 - /// - public class Hotp : Otp + /// The secret key to use in HOTP calculations + /// The hash mode to use + /// The number of digits that the returning HOTP should have. The default is 6. + public Hotp(byte[] secretKey, OtpHashMode mode = OtpHashMode.Sha1, int hotpSize = 6) + : base(secretKey, mode) { - private readonly int _hotpSize; + VerifyParameters(hotpSize); + _hotpSize = hotpSize; + } - /// - /// Create a HOTP instance - /// - /// The secret key to use in HOTP calculations - /// The hash mode to use - /// The number of digits that the returning HOTP should have. The default is 6. - public Hotp(byte[] secretKey, OtpHashMode mode = OtpHashMode.Sha1, int hotpSize = 6) - : base(secretKey, mode) - { - VerifyParameters(hotpSize); - _hotpSize = hotpSize; - } + /// + /// Create a HOTP instance + /// + /// The key to use in HOTP calculations + /// The hash mode to use + /// The number of digits that the returning HOTP should have. The default is 6. + public Hotp(IKeyProvider key, OtpHashMode mode = OtpHashMode.Sha1, int hotpSize = 6) + : base(key, mode) + { + VerifyParameters(hotpSize); - /// - /// Create a HOTP instance - /// - /// The key to use in HOTP calculations - /// The hash mode to use - /// The number of digits that the returning HOTP should have. The default is 6. - public Hotp(IKeyProvider key, OtpHashMode mode = OtpHashMode.Sha1, int hotpSize = 6) - : base(key, mode) - { - VerifyParameters(hotpSize); + _hotpSize = hotpSize; + } - _hotpSize = hotpSize; + private static void VerifyParameters(int hotpSize) + { + if (hotpSize < 6) + { + throw new ArgumentOutOfRangeException(nameof(hotpSize)); } - - private static void VerifyParameters(int hotpSize) + if (hotpSize > 8) { - if (hotpSize < 6) - { - throw new ArgumentOutOfRangeException(nameof(hotpSize)); - } - if (hotpSize > 8) - { - throw new ArgumentOutOfRangeException(nameof(hotpSize)); - } + throw new ArgumentOutOfRangeException(nameof(hotpSize)); } + } - /// - /// Takes a counter and then computes a HOTP value - /// - /// The timestamp to use for the HOTP calculation - /// - /// a HOTP value - public string ComputeHOTP(long counter) => Compute(counter, _hashMode); + /// + /// Takes a counter and then computes a HOTP value + /// + /// The timestamp to use for the HOTP calculation + /// + /// a HOTP value + public string ComputeHOTP(long counter) => Compute(counter, _hashMode); - /// - /// Verify a value that has been provided with the calculated value - /// - /// the trial HOTP value - /// The counter value to verify - /// True if there is a match. - public bool VerifyHotp(string hotp, long counter) => hotp == ComputeHOTP(counter); + /// + /// Verify a value that has been provided with the calculated value + /// + /// the trial HOTP value + /// The counter value to verify + /// True if there is a match. + public bool VerifyHotp(string hotp, long counter) => hotp == ComputeHOTP(counter); - /// - /// Takes a time step and computes a HOTP code - /// - /// counter - /// The hash mode to use - /// HOTP calculated code - protected override string Compute(long counter, OtpHashMode mode) - { - var data = KeyUtilities.GetBigEndianBytes(counter); - var otp = CalculateOtp(data, mode); - return Digits(otp, _hotpSize); - } + /// + /// Takes a time step and computes a HOTP code + /// + /// counter + /// The hash mode to use + /// HOTP calculated code + protected override string Compute(long counter, OtpHashMode mode) + { + var data = KeyUtilities.GetBigEndianBytes(counter); + var otp = CalculateOtp(data, mode); + return Digits(otp, _hotpSize); } } diff --git a/src/Otp.NET/IKeyProvider.cs b/src/Otp.NET/IKeyProvider.cs index 9a5541e..8e54bb4 100644 --- a/src/Otp.NET/IKeyProvider.cs +++ b/src/Otp.NET/IKeyProvider.cs @@ -23,25 +23,24 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL DEALINGS IN THE SOFTWARE. */ -namespace OtpNet +namespace OtpNet; + +/// +/// Interface used to interact with a key +/// +public interface IKeyProvider { /// - /// Interface used to interact with a key + /// Uses the key to get an HMAC using the specified algorithm and data /// - public interface IKeyProvider - { - /// - /// Uses the key to get an HMAC using the specified algorithm and data - /// - /// - /// This is a much better API than the previous API which would briefly expose the key for all derived types. - /// - /// Now a derived type could be bound to an HSM/smart card/etc if required and a lot of the security limitations - /// of in app/memory exposure of the key can be eliminated. - /// - /// The HMAC algorithm to use - /// The data used to compute the HMAC - /// HMAC of the key and data - byte[] ComputeHmac(OtpHashMode mode, byte[] data); - } + /// + /// This is a much better API than the previous API which would briefly expose the key for all derived types. + /// + /// Now a derived type could be bound to an HSM/smart card/etc if required and a lot of the security limitations + /// of in app/memory exposure of the key can be eliminated. + /// + /// The HMAC algorithm to use + /// The data used to compute the HMAC + /// HMAC of the key and data + byte[] ComputeHmac(OtpHashMode mode, byte[] data); } diff --git a/src/Otp.NET/InMemoryKey.cs b/src/Otp.NET/InMemoryKey.cs index 1e20995..9144f0c 100644 --- a/src/Otp.NET/InMemoryKey.cs +++ b/src/Otp.NET/InMemoryKey.cs @@ -26,117 +26,116 @@ DEALINGS IN THE SOFTWARE. using System; using System.Security.Cryptography; -namespace OtpNet +namespace OtpNet; + +/// +/// Represents a key in memory +/// +/// +/// This will attempt to use the Windows data protection api to encrypt the key in memory. +/// However, this type favors working over memory protection. This is an attempt to minimize +/// exposure in memory, nothing more. This protection is flawed in many ways and is limited +/// to Windows. +/// +/// In order to use the key to compute an hmac it must be temporarily decrypted, used, +/// then re-encrypted. This does expose the key in memory for a time. If a memory dump occurs in this time +/// the plaintext key will be part of it. Furthermore, there are potentially +/// artifacts from the hmac computation, GC compaction, or any number of other leaks even after +/// the key is re-encrypted. +/// +/// This type favors working over memory protection. If the particular platform isn't supported then, +/// unless forced by modifying the IsPlatformSupported method, it will just store the key in a standard +/// byte array. +/// +public class InMemoryKey : IKeyProvider { + private readonly object _stateSync = new object(); + private readonly byte[] _keyData; + private readonly int _keyLength; + /// - /// Represents a key in memory + /// Creates an instance of a key. + /// + /// Plaintext key data + public InMemoryKey(byte[] key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + if (key.Length <= 0) + { + throw new ArgumentException("The key must not be empty"); + } + + _keyLength = key.Length; + var paddedKeyLength = (int)Math.Ceiling((decimal)key.Length / (decimal)16) * 16; + _keyData = new byte[paddedKeyLength]; + Array.Copy(key, _keyData, key.Length); + } + + /// + /// Gets a copy of the plaintext key /// /// - /// This will attempt to use the Windows data protection api to encrypt the key in memory. - /// However, this type favors working over memory protection. This is an attempt to minimize - /// exposure in memory, nothing more. This protection is flawed in many ways and is limited - /// to Windows. - /// - /// In order to use the key to compute an hmac it must be temporarily decrypted, used, - /// then re-encrypted. This does expose the key in memory for a time. If a memory dump occurs in this time - /// the plaintext key will be part of it. Furthermore, there are potentially - /// artifacts from the hmac computation, GC compaction, or any number of other leaks even after - /// the key is re-encrypted. - /// - /// This type favors working over memory protection. If the particular platform isn't supported then, - /// unless forced by modifying the IsPlatformSupported method, it will just store the key in a standard - /// byte array. + /// This is internal rather than protected so that the tests can use this method /// - public class InMemoryKey : IKeyProvider + /// Plaintext Key + internal byte[] GetCopyOfKey() { - private readonly object _stateSync = new object(); - private readonly byte[] _keyData; - private readonly int _keyLength; - - /// - /// Creates an instance of a key. - /// - /// Plaintext key data - public InMemoryKey(byte[] key) + var plainKey = new byte[_keyLength]; + lock (_stateSync) { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - if (key.Length <= 0) - { - throw new ArgumentException("The key must not be empty"); - } - - _keyLength = key.Length; - var paddedKeyLength = (int)Math.Ceiling((decimal)key.Length / (decimal)16) * 16; - _keyData = new byte[paddedKeyLength]; - Array.Copy(key, _keyData, key.Length); + Array.Copy(_keyData, plainKey, _keyLength); } + return plainKey; + } - /// - /// Gets a copy of the plaintext key - /// - /// - /// This is internal rather than protected so that the tests can use this method - /// - /// Plaintext Key - internal byte[] GetCopyOfKey() + /// + /// 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 + public byte[] ComputeHmac(OtpHashMode mode, byte[] data) + { + byte[] hashedValue; + using (var hmac = CreateHmacHash(mode)) { - var plainKey = new byte[_keyLength]; - lock (_stateSync) + var key = GetCopyOfKey(); + try { - Array.Copy(_keyData, plainKey, _keyLength); + hmac.Key = key; + hashedValue = hmac.ComputeHash(data); } - return plainKey; - } - - /// - /// 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 - public byte[] ComputeHmac(OtpHashMode mode, byte[] data) - { - byte[] hashedValue; - using (var hmac = CreateHmacHash(mode)) + finally { - var key = GetCopyOfKey(); - try - { - hmac.Key = key; - hashedValue = hmac.ComputeHash(data); - } - finally - { - KeyUtilities.Destroy(key); - } + KeyUtilities.Destroy(key); } - - return hashedValue; } - /// - /// Create an HMAC object for the specified algorithm - /// - private static HMAC CreateHmacHash(OtpHashMode otpHashMode) + return hashedValue; + } + + /// + /// Create an HMAC object for the specified algorithm + /// + private static HMAC CreateHmacHash(OtpHashMode otpHashMode) + { + HMAC hmacAlgorithm; + switch (otpHashMode) { - HMAC hmacAlgorithm; - switch (otpHashMode) - { - case OtpHashMode.Sha256: - hmacAlgorithm = new HMACSHA256(); - break; - case OtpHashMode.Sha512: - hmacAlgorithm = new HMACSHA512(); - break; - // case OtpHashMode.Sha1: - default: - hmacAlgorithm = new HMACSHA1(); - break; - } - return hmacAlgorithm; + case OtpHashMode.Sha256: + hmacAlgorithm = new HMACSHA256(); + break; + case OtpHashMode.Sha512: + hmacAlgorithm = new HMACSHA512(); + break; + // case OtpHashMode.Sha1: + default: + hmacAlgorithm = new HMACSHA1(); + break; } + return hmacAlgorithm; } } \ No newline at end of file diff --git a/src/Otp.NET/KeyGeneration.cs b/src/Otp.NET/KeyGeneration.cs index 95dc2ec..53c817b 100644 --- a/src/Otp.NET/KeyGeneration.cs +++ b/src/Otp.NET/KeyGeneration.cs @@ -26,97 +26,94 @@ DEALINGS IN THE SOFTWARE. using System; using System.Security.Cryptography; -namespace OtpNet +namespace OtpNet; + +/// +/// Helpers to work with keys +/// +public static class KeyGeneration { /// - /// Helpers to work with keys + /// Generates a random key in accordance with the RFC recommended length for each algorithm /// - public static class KeyGeneration + /// Key length + /// The generated key + public static byte[] GenerateRandomKey(int length) { - /// - /// Generates a random key in accordance with the RFC recommended length for each algorithm - /// - /// Key length - /// The generated key - public static byte[] GenerateRandomKey(int length) - { - var key = new byte[length]; - using (var rnd = RandomNumberGenerator.Create()) - { - rnd.GetBytes(key); - return key; - } - } + var key = new byte[length]; + using var rnd = RandomNumberGenerator.Create(); + rnd.GetBytes(key); + return key; + } - /// - /// Generates a random key in accordance with the RFC recommended length for each algorithm - /// - /// HashMode - /// Key - public static byte[] GenerateRandomKey(OtpHashMode mode = OtpHashMode.Sha1) - { - return GenerateRandomKey(LengthForMode(mode)); - } + /// + /// Generates a random key in accordance with the RFC recommended length for each algorithm + /// + /// HashMode + /// Key + public static byte[] GenerateRandomKey(OtpHashMode mode = OtpHashMode.Sha1) + { + return GenerateRandomKey(LengthForMode(mode)); + } - /// - /// Uses the procedure defined in RFC 4226 section 7.5 to derive a key from the master key - /// - /// The master key from which to derive a device specific key - /// The public identifier that is unique to the authenticating device - /// The hash mode to use. This will determine the resulting key lenght. The default is sha-1 (as per the RFC) which is 20 bytes - /// Derived key - public static byte[] DeriveKeyFromMaster( - IKeyProvider masterKey, - byte[] publicIdentifier, - OtpHashMode mode = OtpHashMode.Sha1) + /// + /// Uses the procedure defined in RFC 4226 section 7.5 to derive a key from the master key + /// + /// The master key from which to derive a device specific key + /// The public identifier that is unique to the authenticating device + /// The hash mode to use. This will determine the resulting key lenght. The default is sha-1 (as per the RFC) which is 20 bytes + /// Derived key + public static byte[] DeriveKeyFromMaster( + IKeyProvider masterKey, + byte[] publicIdentifier, + OtpHashMode mode = OtpHashMode.Sha1) + { + if (masterKey == null) { - if (masterKey == null) - { - throw new ArgumentNullException(nameof(masterKey)); - } - return masterKey.ComputeHmac(mode, publicIdentifier); + throw new ArgumentNullException(nameof(masterKey)); } + return masterKey.ComputeHmac(mode, publicIdentifier); + } - /// - /// Uses the procedure defined in RFC 4226 section 7.5 to derive a key from the master key - /// - /// The master key from which to derive a device specific key - /// A serial number that is unique to the authenticating device - /// The hash mode to use. This will determine the resulting key lenght. - /// The default is sha-1 (as per the RFC) which is 20 bytes - /// Derived key - public static byte[] DeriveKeyFromMaster( - IKeyProvider masterKey, - int serialNumber, - OtpHashMode mode = OtpHashMode.Sha1) => - DeriveKeyFromMaster(masterKey, KeyUtilities.GetBigEndianBytes(serialNumber), mode); + /// + /// Uses the procedure defined in RFC 4226 section 7.5 to derive a key from the master key + /// + /// The master key from which to derive a device specific key + /// A serial number that is unique to the authenticating device + /// The hash mode to use. This will determine the resulting key lenght. + /// The default is sha-1 (as per the RFC) which is 20 bytes + /// Derived key + public static byte[] DeriveKeyFromMaster( + IKeyProvider masterKey, + int serialNumber, + OtpHashMode mode = OtpHashMode.Sha1) => + DeriveKeyFromMaster(masterKey, KeyUtilities.GetBigEndianBytes(serialNumber), mode); - private static HashAlgorithm GetHashAlgorithmForMode(OtpHashMode mode) + private static HashAlgorithm GetHashAlgorithmForMode(OtpHashMode mode) + { + switch (mode) { - switch (mode) - { - case OtpHashMode.Sha256: - return SHA256.Create(); - case OtpHashMode.Sha512: - return SHA512.Create(); - // case OtpHashMode.Sha1: - default: - return SHA1.Create(); - } + case OtpHashMode.Sha256: + return SHA256.Create(); + case OtpHashMode.Sha512: + return SHA512.Create(); + // case OtpHashMode.Sha1: + default: + return SHA1.Create(); } + } - private static int LengthForMode(OtpHashMode mode) + private static int LengthForMode(OtpHashMode mode) + { + switch (mode) { - switch (mode) - { - case OtpHashMode.Sha256: - return 32; - case OtpHashMode.Sha512: - return 64; - // case OtpHashMode.Sha1: - default: - return 20; - } + case OtpHashMode.Sha256: + return 32; + case OtpHashMode.Sha512: + return 64; + // case OtpHashMode.Sha1: + default: + return 20; } } } \ No newline at end of file diff --git a/src/Otp.NET/KeyUtilities.cs b/src/Otp.NET/KeyUtilities.cs index da661eb..b27165c 100644 --- a/src/Otp.NET/KeyUtilities.cs +++ b/src/Otp.NET/KeyUtilities.cs @@ -25,60 +25,59 @@ DEALINGS IN THE SOFTWARE. using System; -namespace OtpNet +namespace OtpNet; + +/// +/// Some helper methods to perform common key functions +/// +internal class KeyUtilities { /// - /// Some helper methods to perform common key functions + /// Overwrite potentially sensitive data with random junk /// - internal class KeyUtilities + /// + /// 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) { - /// - /// 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) { - if (sensitiveData == null) - { - throw new ArgumentNullException(nameof(sensitiveData)); - } - new Random().NextBytes(sensitiveData); + throw new ArgumentNullException(nameof(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. - /// - internal static 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 a long into a big endian byte array. + /// + /// + /// RFC 4226 specifies big endian as the method for converting the counter to data to hash. + /// + internal static 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. - /// - internal static 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; - } + /// + /// 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. + /// + internal static 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.csproj b/src/Otp.NET/Otp.NET.csproj index 591947d..46004a5 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -1,6 +1,7 @@  + latest 1.3.0 net461;net5.0;net6.0;net7.0;netstandard2.0 Otp.NET @@ -25,11 +26,6 @@ For documentation and examples visit the project website on GitHub at https://gi vs-signing-key.snk - - - - - diff --git a/src/Otp.NET/Otp.cs b/src/Otp.NET/Otp.cs index d6cc454..dd37a3b 100644 --- a/src/Otp.NET/Otp.cs +++ b/src/Otp.NET/Otp.cs @@ -25,137 +25,136 @@ DEALINGS IN THE SOFTWARE. using System; -namespace OtpNet +namespace OtpNet; + +/// +/// An abstract class that contains common OTP calculations +/// +/// +/// https://tools.ietf.org/html/rfc4226 +/// +public abstract class Otp { /// - /// An abstract class that contains common OTP calculations + /// Secret key /// - /// - /// https://tools.ietf.org/html/rfc4226 - /// - public abstract class Otp + protected readonly IKeyProvider _secretKey; + + /// + /// The hash mode to use + /// + protected readonly OtpHashMode _hashMode; + + /// + /// Constructor for the abstract class using an explicit secret key + /// + /// + /// The hash mode to use + public Otp(byte[] secretKey, OtpHashMode mode) { - /// - /// Secret key - /// - protected readonly IKeyProvider _secretKey; - - /// - /// The hash mode to use - /// - protected readonly OtpHashMode _hashMode; - - /// - /// Constructor for the abstract class using an explicit secret key - /// - /// - /// The hash mode to use - public Otp(byte[] secretKey, OtpHashMode mode) + if (secretKey == null) { - if (secretKey == null) - { - throw new ArgumentNullException(nameof(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. - _secretKey = new InMemoryKey(secretKey); - _hashMode = mode; + throw new ArgumentNullException(nameof(secretKey)); } - - /// - /// Constructor for the abstract class using a generic key provider - /// - /// - /// The hash mode to use - public Otp(IKeyProvider key, OtpHashMode mode) + if (secretKey.Length <= 0) { - _secretKey = key ?? throw new ArgumentNullException(nameof(key)); - _hashMode = mode; + throw new ArgumentException("secretKey empty"); } - /// - /// 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) - { - var hmacComputedHash = _secretKey.ComputeHmac(mode, data); + // when passing a key into the constructor the caller may depend on the reference to the key remaining intact. + _secretKey = new InMemoryKey(secretKey); + _hashMode = mode; + } - // 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] + /// + /// Constructor for the abstract class using a generic key provider + /// + /// + /// The hash mode to use + public Otp(IKeyProvider key, OtpHashMode mode) + { + _secretKey = key ?? throw new ArgumentNullException(nameof(key)); + _hashMode = mode; + } - 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; - } + /// + /// 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) + { + var hmacComputedHash = _secretKey.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] - /// - /// truncates a number down to the specified number of digits - /// - protected internal static string Digits(long input, int digitCount) + 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) { - var truncatedValue = (int)input % (int)Math.Pow(10, digitCount); - return truncatedValue.ToString().PadLeft(digitCount, '0'); + window = new VerificationWindow(); } - - /// - /// 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) + + foreach(var frame in window.ValidationCandidates(initialStep)) { - if (window == null) - { - window = new VerificationWindow(); - } - - foreach(var frame in window.ValidationCandidates(initialStep)) + var comparisonValue = Compute(frame, _hashMode); + if(ValuesEqual(comparisonValue, valueToVerify)) { - var comparisonValue = Compute(frame, _hashMode); - if(ValuesEqual(comparisonValue, valueToVerify)) - { - matchedStep = frame; - return true; - } + matchedStep = frame; + return true; } + } + + matchedStep = 0; + return false; + } - matchedStep = 0; + // Constant time comparison of two values + private bool ValuesEqual(string a, string b) + { + if(a.Length != b.Length) + { return false; } - // Constant time comparison of two values - private bool ValuesEqual(string a, string b) + var result = 0; + for(var i = 0; i < a.Length; i++) { - if(a.Length != b.Length) - { - return false; - } - - var result = 0; - for(var i = 0; i < a.Length; i++) - { - result |= a[i] ^ b[i]; - } - - return result == 0; + result |= a[i] ^ b[i]; } + + return result == 0; } } \ No newline at end of file diff --git a/src/Otp.NET/OtpHashMode.cs b/src/Otp.NET/OtpHashMode.cs index 11600f6..4ef1d7a 100644 --- a/src/Otp.NET/OtpHashMode.cs +++ b/src/Otp.NET/OtpHashMode.cs @@ -23,24 +23,23 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL DEALINGS IN THE SOFTWARE. */ -namespace OtpNet +namespace OtpNet; + +/// +/// Indicates which HMAC hashing algorithm should be used +/// +public enum OtpHashMode { /// - /// Indicates which HMAC hashing algorithm should be used + /// 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 /// - 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 - } + Sha512 } diff --git a/src/Otp.NET/OtpType.cs b/src/Otp.NET/OtpType.cs index d9daf5d..ce14913 100644 --- a/src/Otp.NET/OtpType.cs +++ b/src/Otp.NET/OtpType.cs @@ -1,18 +1,17 @@ -namespace OtpNet { +namespace OtpNet; +/// +/// Schema types for OTPs +/// +public enum OtpType +{ /// - /// Schema types for OTPs + /// Time-based OTP + /// see https://datatracker.ietf.org/doc/html/rfc6238 /// - public enum OtpType - { - /// - /// Time-based OTP - /// see https://datatracker.ietf.org/doc/html/rfc6238 - /// - Totp, - /// - /// HMAC-based (counter) OTP - /// see https://datatracker.ietf.org/doc/html/rfc4226 - /// - Hotp - } + Totp, + /// + /// HMAC-based (counter) OTP + /// see https://datatracker.ietf.org/doc/html/rfc4226 + /// + Hotp } diff --git a/src/Otp.NET/OtpUri.cs b/src/Otp.NET/OtpUri.cs index 2d19c03..590b810 100644 --- a/src/Otp.NET/OtpUri.cs +++ b/src/Otp.NET/OtpUri.cs @@ -2,170 +2,169 @@ using System.Collections.Generic; using System.Text; -namespace OtpNet +namespace OtpNet; + +// See https://github.com/google/google-authenticator/wiki/Key-Uri-Format +public class OtpUri { - // See https://github.com/google/google-authenticator/wiki/Key-Uri-Format - public class OtpUri + /// + /// What type of OTP is this uri for + /// + /// + public readonly OtpType Type; + + /// + /// The secret parameter is an arbitrary key value encoded in Base32 according to RFC 3548. + /// The padding specified in RFC 3548 section 2.2 is not required and should be omitted. + /// + public readonly string Secret; + + /// + /// Which account a key is associated with + /// + public readonly string User; + + /// + /// The issuer parameter is a string value indicating the provider or service this account is + /// associated with, URL-encoded according to RFC 3986. + /// + public readonly string Issuer; + + /// + /// The algorithm used by the generator + /// + public readonly OtpHashMode Algorithm; + + /// + /// The amount of digits in the final code + /// + public readonly int Digits; + + /// + /// The number of seconds that a code is valid. Only applies to TOTP, not HOTP + /// + public readonly int Period; + + /// + /// Initial counter value for HOTP. This is ignored when using TOTP. + /// + public readonly int Counter; + + /// + /// Create a new OTP Auth Uri + /// + public OtpUri( + OtpType schema, + string secret, + string user, + string issuer = null, + OtpHashMode algorithm = OtpHashMode.Sha1, + int digits = 6, + int period = 30, + int counter = 0) + { + _ = secret ?? throw new ArgumentNullException(nameof(secret)); + _ = user ?? throw new ArgumentNullException(nameof(user)); + if (digits < 0) + { + throw new ArgumentOutOfRangeException(nameof(digits)); + } + + Type = schema; + Secret = secret; + User = user; + Issuer = issuer; + Algorithm = algorithm; + Digits = digits; + + switch (Type) + { + case OtpType.Totp: + Period = period; + break; + case OtpType.Hotp: + Counter = counter; + break; + } + } + + /// + /// Create a new OTP Auth Uri + /// + public OtpUri( + OtpType schema, + byte[] secret, + string user, + string issuer = null, + OtpHashMode algorithm = OtpHashMode.Sha1, + int digits = 6, + int period = 30, + int counter = 0) + : this(schema, Base32Encoding.ToString(secret), user, issuer, + algorithm, digits, period, counter) + { } + + /// + /// Generates a Uri according to the parameters + /// + /// a Uri according to the parameters + public Uri ToUri() { - /// - /// What type of OTP is this uri for - /// - /// - public readonly OtpType Type; - - /// - /// The secret parameter is an arbitrary key value encoded in Base32 according to RFC 3548. - /// The padding specified in RFC 3548 section 2.2 is not required and should be omitted. - /// - public readonly string Secret; - - /// - /// Which account a key is associated with - /// - public readonly string User; - - /// - /// The issuer parameter is a string value indicating the provider or service this account is - /// associated with, URL-encoded according to RFC 3986. - /// - public readonly string Issuer; - - /// - /// The algorithm used by the generator - /// - public readonly OtpHashMode Algorithm; - - /// - /// The amount of digits in the final code - /// - public readonly int Digits; - - /// - /// The number of seconds that a code is valid. Only applies to TOTP, not HOTP - /// - public readonly int Period; - - /// - /// Initial counter value for HOTP. This is ignored when using TOTP. - /// - public readonly int Counter; - - /// - /// Create a new OTP Auth Uri - /// - public OtpUri( - OtpType schema, - string secret, - string user, - string issuer = null, - OtpHashMode algorithm = OtpHashMode.Sha1, - int digits = 6, - int period = 30, - int counter = 0) + return new Uri(ToString()); + } + + /// + /// Generates a Uri String according to the parameters + /// + /// a Uri String according to the parameters + public override string ToString() + { + var parameters = new Dictionary + { + { "secret", Secret.TrimEnd('=') } + }; + + if (!string.IsNullOrWhiteSpace(Issuer)) { - _ = secret ?? throw new ArgumentNullException(nameof(secret)); - _ = user ?? throw new ArgumentNullException(nameof(user)); - if (digits < 0) - { - throw new ArgumentOutOfRangeException(nameof(digits)); - } - - Type = schema; - Secret = secret; - User = user; - Issuer = issuer; - Algorithm = algorithm; - Digits = digits; - - switch (Type) - { - case OtpType.Totp: - Period = period; - break; - case OtpType.Hotp: - Counter = counter; - break; - } + parameters.Add("issuer", Uri.EscapeDataString(Issuer)); } + parameters.Add("algorithm", Algorithm.ToString().ToUpper()); + parameters.Add("digits", Digits.ToString()); - /// - /// Create a new OTP Auth Uri - /// - public OtpUri( - OtpType schema, - byte[] secret, - string user, - string issuer = null, - OtpHashMode algorithm = OtpHashMode.Sha1, - int digits = 6, - int period = 30, - int counter = 0) - : this(schema, Base32Encoding.ToString(secret), user, issuer, - algorithm, digits, period, counter) - { } - - /// - /// Generates a Uri according to the parameters - /// - /// a Uri according to the parameters - public Uri ToUri() + switch (Type) { - return new Uri(ToString()); + case OtpType.Totp: + parameters.Add("period", Period.ToString()); + break; + case OtpType.Hotp: + parameters.Add("counter", Counter.ToString()); + break; } - /// - /// Generates a Uri String according to the parameters - /// - /// a Uri String according to the parameters - public override string ToString() + var uriBuilder = new StringBuilder("otpauth://"); + uriBuilder.Append(Type.ToString().ToLowerInvariant()); + uriBuilder.Append("/"); + + // The label + if (!string.IsNullOrWhiteSpace(Issuer)) { - var parameters = new Dictionary - { - { "secret", Secret.TrimEnd('=') } - }; - - if (!string.IsNullOrWhiteSpace(Issuer)) - { - parameters.Add("issuer", Uri.EscapeDataString(Issuer)); - } - parameters.Add("algorithm", Algorithm.ToString().ToUpper()); - parameters.Add("digits", Digits.ToString()); - - switch (Type) - { - case OtpType.Totp: - parameters.Add("period", Period.ToString()); - break; - case OtpType.Hotp: - parameters.Add("counter", Counter.ToString()); - break; - } - - var uriBuilder = new StringBuilder("otpauth://"); - uriBuilder.Append(Type.ToString().ToLowerInvariant()); - uriBuilder.Append("/"); - - // The label - if (!string.IsNullOrWhiteSpace(Issuer)) - { - uriBuilder.Append(Uri.EscapeDataString(Issuer)); - uriBuilder.Append(":"); - } - uriBuilder.Append(Uri.EscapeDataString(User)); - - // Start of the parameters - uriBuilder.Append("?"); - foreach (var pair in parameters) - { - uriBuilder.Append(pair.Key); - uriBuilder.Append("="); - uriBuilder.Append(pair.Value); - uriBuilder.Append("&"); - } - // Remove last "&" - uriBuilder.Remove(uriBuilder.Length - 1, 1); - - return uriBuilder.ToString(); + uriBuilder.Append(Uri.EscapeDataString(Issuer)); + uriBuilder.Append(":"); } + uriBuilder.Append(Uri.EscapeDataString(User)); + + // Start of the parameters + uriBuilder.Append("?"); + foreach (var pair in parameters) + { + uriBuilder.Append(pair.Key); + uriBuilder.Append("="); + uriBuilder.Append(pair.Value); + uriBuilder.Append("&"); + } + // Remove last "&" + uriBuilder.Remove(uriBuilder.Length - 1, 1); + + return uriBuilder.ToString(); } } diff --git a/src/Otp.NET/TimeCorrection.cs b/src/Otp.NET/TimeCorrection.cs index a3609d7..5ad7ef3 100644 --- a/src/Otp.NET/TimeCorrection.cs +++ b/src/Otp.NET/TimeCorrection.cs @@ -25,72 +25,71 @@ DEALINGS IN THE SOFTWARE. using System; -namespace OtpNet +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 thread-safe +/// +public class TimeCorrection { /// - /// Class to apply a correction factor to the system time + /// An instance that provides no correction factor /// - /// - /// 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 thread-safe - /// - public class TimeCorrection - { - /// - /// An instance that provides no correction factor - /// - public static readonly TimeCorrection UncorrectedInstance = new TimeCorrection(); + public static readonly TimeCorrection UncorrectedInstance = new TimeCorrection(); - /// - /// Constructor used solely for the UncorrectedInstance static field to provide an instance without - /// a correction factor. - /// - private TimeCorrection() => CorrectionFactor = TimeSpan.FromSeconds(0); + /// + /// Constructor used solely for the UncorrectedInstance static field to provide an instance without + /// a correction factor. + /// + private TimeCorrection() => CorrectionFactor = 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 overload. - /// - /// The current correct UTC time - public TimeCorrection(DateTime correctUtc) => CorrectionFactor = DateTime.UtcNow - correctUtc; + /// + /// 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 overload. + /// + /// The current correct UTC time + public TimeCorrection(DateTime correctUtc) => CorrectionFactor = 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) => - CorrectionFactor = referenceTime - correctTime; + /// + /// 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) => + CorrectionFactor = 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) => referenceTime - CorrectionFactor; + /// + /// 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) => referenceTime - CorrectionFactor; - /// - /// Applies the correction factor to the current system UTC time and returns a corrected time - /// - public DateTime CorrectedUtcNow => GetCorrectedTime(DateTime.UtcNow); + /// + /// Applies the correction factor to the current system UTC time and returns a corrected time + /// + public DateTime CorrectedUtcNow => GetCorrectedTime(DateTime.UtcNow); - /// - /// The timespan that is used to calculate a corrected time - /// - public TimeSpan CorrectionFactor { get; } - } + /// + /// The timespan that is used to calculate a corrected time + /// + public TimeSpan CorrectionFactor { get; } } \ No newline at end of file diff --git a/src/Otp.NET/Totp.cs b/src/Otp.NET/Totp.cs index 2c1c80c..a919d8c 100644 --- a/src/Otp.NET/Totp.cs +++ b/src/Otp.NET/Totp.cs @@ -25,225 +25,224 @@ DEALINGS IN THE SOFTWARE. using System; -namespace OtpNet +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 { /// - /// Calculate Timed-One-Time-Passwords (TOTP) from a secret key + /// The number of ticks as Measured at Midnight Jan 1st 1970; /// - /// - /// The specifications for this are found in RFC 6238 - /// http://tools.ietf.org/html/rfc6238 - /// - public class Totp : Otp + private const long UnicEpocTicks = 621355968000000000L; + + /// + /// A divisor for converting ticks to seconds + /// + private 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) { - /// - /// The number of ticks as Measured at Midnight Jan 1st 1970; - /// - private const long UnicEpocTicks = 621355968000000000L; - - /// - /// A divisor for converting ticks to seconds - /// - private 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); + VerifyParameters(step, totpSize); - _step = step; - _totpSize = totpSize; + _step = step; + _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 neutral functionality in this case. - _correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance; - } + // we never null check the corrected time object. Since it's readonly, we'll + // ensure that it isn't null here and provide neutral functionality in this case. + _correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance; + } - /// - /// 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( - IKeyProvider key, - int step = 30, - OtpHashMode mode = OtpHashMode.Sha1, - int totpSize = 6, - TimeCorrection timeCorrection = null) - : base(key, mode) - { - VerifyParameters(step, totpSize); + /// + /// 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( + IKeyProvider key, + int step = 30, + OtpHashMode mode = OtpHashMode.Sha1, + int totpSize = 6, + TimeCorrection timeCorrection = null) + : base(key, mode) + { + VerifyParameters(step, totpSize); - _step = step; - _totpSize = totpSize; + _step = step; + _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 neutral functionality in this case. - _correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance; - } + // we never null check the corrected time object. + // Since it's readonly, we'll ensure that it isn't null here and provide neutral functionality in this case. + _correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance; + } - private static void VerifyParameters(int step, int totpSize) + private static void VerifyParameters(int step, int totpSize) + { + if (step <= 0) { - if (step <= 0) - { - throw new ArgumentOutOfRangeException(nameof(step)); - } - if (totpSize <= 0) - { - throw new ArgumentOutOfRangeException(nameof(totpSize)); - } - if (totpSize > 10) - { - throw new ArgumentOutOfRangeException(nameof(totpSize)); - } + throw new ArgumentOutOfRangeException(nameof(step)); } - - /// - /// 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) => - ComputeTotpFromSpecificTime(_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() => ComputeTotpFromSpecificTime(_correctedTime.CorrectedUtcNow); - - private string ComputeTotpFromSpecificTime(DateTime timestamp) + if (totpSize <= 0) { - var window = CalculateTimeStepFromTimestamp(timestamp); - return Compute(window, _hashMode); + throw new ArgumentOutOfRangeException(nameof(totpSize)); } - - /// - /// 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) => - VerifyTotpForSpecificTime(_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 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( - DateTime timestamp, - string totp, - out long timeStepMatched, - VerificationWindow window = null) => - VerifyTotpForSpecificTime( - _correctedTime.GetCorrectedTime(timestamp), - totp, - window, - out timeStepMatched); - - private bool VerifyTotpForSpecificTime( - DateTime timestamp, - string totp, - VerificationWindow window, - out long timeStepMatched) + if (totpSize > 10) { - var initialStep = CalculateTimeStepFromTimestamp(timestamp); - return Verify(initialStep, totp, out timeStepMatched, window); + throw new ArgumentOutOfRangeException(nameof(totpSize)); } + } - /// - /// Takes a timestamp and calculates a time step - /// - private long CalculateTimeStepFromTimestamp(DateTime timestamp) - { - var unixTimestamp = (timestamp.Ticks - UnicEpocTicks) / TicksToSeconds; - var window = unixTimestamp / (long)_step; - return window; - } + /// + /// 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) => + ComputeTotpFromSpecificTime(_correctedTime.GetCorrectedTime(timestamp)); - /// - /// 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(_correctedTime.CorrectedUtcNow); - } + /// + /// 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() => ComputeTotpFromSpecificTime(_correctedTime.CorrectedUtcNow); - /// - /// Remaining seconds in current window - /// - /// The timestamp - /// Number of remaining seconds - public int RemainingSeconds(DateTime timestamp) => - RemainingSecondsForSpecificTime(_correctedTime.GetCorrectedTime(timestamp)); - - private int RemainingSecondsForSpecificTime(DateTime timestamp) => - _step - (int)(((timestamp.Ticks - UnicEpocTicks) / TicksToSeconds) % _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 = CalculateOtp(data, mode); - return Digits(otp, _totpSize); - } + private string ComputeTotpFromSpecificTime(DateTime timestamp) + { + var window = CalculateTimeStepFromTimestamp(timestamp); + return Compute(window, _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) => + VerifyTotpForSpecificTime(_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 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( + DateTime timestamp, + string totp, + out long timeStepMatched, + VerificationWindow window = null) => + VerifyTotpForSpecificTime( + _correctedTime.GetCorrectedTime(timestamp), + totp, + window, + out timeStepMatched); + + private bool VerifyTotpForSpecificTime( + DateTime timestamp, + string totp, + VerificationWindow window, + out long timeStepMatched) + { + var initialStep = CalculateTimeStepFromTimestamp(timestamp); + return Verify(initialStep, totp, out timeStepMatched, window); + } + + /// + /// Takes a timestamp and calculates a time step + /// + private long CalculateTimeStepFromTimestamp(DateTime timestamp) + { + var unixTimestamp = (timestamp.Ticks - UnicEpocTicks) / TicksToSeconds; + var window = unixTimestamp / (long)_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(_correctedTime.CorrectedUtcNow); + } + + /// + /// Remaining seconds in current window + /// + /// The timestamp + /// Number of remaining seconds + public int RemainingSeconds(DateTime timestamp) => + RemainingSecondsForSpecificTime(_correctedTime.GetCorrectedTime(timestamp)); + + private int RemainingSecondsForSpecificTime(DateTime timestamp) => + _step - (int)(((timestamp.Ticks - UnicEpocTicks) / TicksToSeconds) % _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 = CalculateOtp(data, mode); + return Digits(otp, _totpSize); } } \ No newline at end of file diff --git a/src/Otp.NET/VerificationWindow.cs b/src/Otp.NET/VerificationWindow.cs index 5ffa220..f6af050 100644 --- a/src/Otp.NET/VerificationWindow.cs +++ b/src/Otp.NET/VerificationWindow.cs @@ -25,53 +25,54 @@ DEALINGS IN THE SOFTWARE. using System.Collections.Generic; -namespace OtpNet +namespace OtpNet; + +/// +/// A verification window +/// +public class VerificationWindow { + private readonly int _previous; + private readonly int _future; + /// - /// A verification window + /// Create an instance of a verification window /// - public class VerificationWindow + /// The number of previous frames to accept + /// The number of future frames to accept + public VerificationWindow(int previous = 0, int future = 0) { - 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) - { - _previous = previous; - _future = future; - } + _previous = previous; + _future = future; + } - /// - /// Gets an enumerable of all the possible validation candidates - /// - /// The initial frame to validate - /// Enumerable of all possible frames that need to be validated - public IEnumerable ValidationCandidates(long initialFrame) + /// + /// Gets an enumerable of all the possible validation candidates + /// + /// The initial frame to validate + /// Enumerable of all possible frames that need to be validated + public IEnumerable ValidationCandidates(long initialFrame) + { + yield return initialFrame; + for (var i = 1; i <= _previous; i++) { - yield return initialFrame; - for (var i = 1; i <= _previous; i++) + var val = initialFrame - i; + if (val < 0) { - var val = initialFrame - i; - if (val < 0) - break; - yield return val; - } - - for (var i = 1; i <= _future; i++) - { - yield return initialFrame + i; + break; } + yield return val; } - /// - /// The verification window that accommodates network delay that is recommended in the RFC - /// - public static readonly VerificationWindow RfcSpecifiedNetworkDelay = - new VerificationWindow(previous: 1, future: 1); + for (var i = 1; i <= _future; i++) + { + yield return initialFrame + i; + } } + + /// + /// The verification window that accommodates network delay that is recommended in the RFC + /// + public static readonly VerificationWindow RfcSpecifiedNetworkDelay = + new VerificationWindow(previous: 1, future: 1); } diff --git a/test/Otp.NET.Test/HotpTest.cs b/test/Otp.NET.Test/HotpTest.cs index e880450..ec1a77b 100644 --- a/test/Otp.NET.Test/HotpTest.cs +++ b/test/Otp.NET.Test/HotpTest.cs @@ -1,44 +1,43 @@ using Moq; using NUnit.Framework; -namespace OtpNet.Test +namespace OtpNet.Test; + +[TestFixture] +public class HotpTest { - [TestFixture] - public class HotpTest - { - private static readonly byte[] rfc4226Secret = { - 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, - 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, - 0x37, 0x38, 0x39, 0x30 - }; + private static readonly byte[] rfc4226Secret = { + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x30 + }; - [TestCase(OtpHashMode.Sha1, 0, "755224")] - [TestCase(OtpHashMode.Sha1, 1, "287082")] - [TestCase(OtpHashMode.Sha1, 2, "359152")] - [TestCase(OtpHashMode.Sha1, 3, "969429")] - [TestCase(OtpHashMode.Sha1, 4, "338314")] - [TestCase(OtpHashMode.Sha1, 5, "254676")] - [TestCase(OtpHashMode.Sha1, 6, "287922")] - [TestCase(OtpHashMode.Sha1, 7, "162583")] - [TestCase(OtpHashMode.Sha1, 8, "399871")] - [TestCase(OtpHashMode.Sha1, 9, "520489")] - public void ComputeHOTPRfc4226Test(OtpHashMode hash, long counter, string expectedOtp) - { - Hotp otpCalc = new Hotp(rfc4226Secret, hash, expectedOtp.Length); - string otp = otpCalc.ComputeHOTP(counter); - Assert.That(otp, Is.EqualTo(expectedOtp)); - } + [TestCase(OtpHashMode.Sha1, 0, "755224")] + [TestCase(OtpHashMode.Sha1, 1, "287082")] + [TestCase(OtpHashMode.Sha1, 2, "359152")] + [TestCase(OtpHashMode.Sha1, 3, "969429")] + [TestCase(OtpHashMode.Sha1, 4, "338314")] + [TestCase(OtpHashMode.Sha1, 5, "254676")] + [TestCase(OtpHashMode.Sha1, 6, "287922")] + [TestCase(OtpHashMode.Sha1, 7, "162583")] + [TestCase(OtpHashMode.Sha1, 8, "399871")] + [TestCase(OtpHashMode.Sha1, 9, "520489")] + public void ComputeHOTPRfc4226Test(OtpHashMode hash, long counter, string expectedOtp) + { + Hotp otpCalc = new Hotp(rfc4226Secret, hash, expectedOtp.Length); + string otp = otpCalc.ComputeHOTP(counter); + Assert.That(otp, Is.EqualTo(expectedOtp)); + } - [Test] - public void ContructorWithKeyProviderTest() - { - //Mock a key provider which always returns an all-zero HMAC (causing an all-zero OTP) - Mock keyMock = new Mock(); - keyMock.Setup(key => key.ComputeHmac(It.Is(m => m == OtpHashMode.Sha1), It.IsAny())).Returns(new byte[20]); + [Test] + public void ContructorWithKeyProviderTest() + { + //Mock a key provider which always returns an all-zero HMAC (causing an all-zero OTP) + Mock keyMock = new Mock(); + keyMock.Setup(key => key.ComputeHmac(It.Is(m => m == OtpHashMode.Sha1), It.IsAny())).Returns(new byte[20]); - var otp = new Hotp(keyMock.Object, OtpHashMode.Sha1, 6); - Assert.That(otp.ComputeHOTP(0), Is.EqualTo("000000")); - Assert.That(otp.ComputeHOTP(1), Is.EqualTo("000000")); - } + var otp = new Hotp(keyMock.Object, OtpHashMode.Sha1, 6); + Assert.That(otp.ComputeHOTP(0), Is.EqualTo("000000")); + Assert.That(otp.ComputeHOTP(1), Is.EqualTo("000000")); } } diff --git a/test/Otp.NET.Test/OtpUriTest.cs b/test/Otp.NET.Test/OtpUriTest.cs index 835b505..e8f75ab 100644 --- a/test/Otp.NET.Test/OtpUriTest.cs +++ b/test/Otp.NET.Test/OtpUriTest.cs @@ -1,28 +1,27 @@ using System; using NUnit.Framework; -namespace OtpNet.Test +namespace OtpNet.Test; + +[TestFixture] +public class OtpUriTest { - [TestFixture] - public class OtpUriTest - { - private const string _baseSecret = "JBSWY3DPEHPK3PXP"; - private const string _baseUser = "alice@google.com"; - private const string _baseIssuer = "ACME Co"; + private const string _baseSecret = "JBSWY3DPEHPK3PXP"; + private const string _baseUser = "alice@google.com"; + private const string _baseIssuer = "ACME Co"; - [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha1, 6, 30, 0, - "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30")] - [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha256, 6, 30, 0, - "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA256&digits=6&period=30")] - [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha512, 6, 30, 0, - "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA512&digits=6&period=30")] - [TestCase(_baseSecret, OtpType.Hotp, _baseUser, _baseIssuer, OtpHashMode.Sha512, 6, 30, 0, - "otpauth://hotp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA512&digits=6&counter=0")] - public void GenerateOtpUriTest(string secret, OtpType otpType, string user, string issuer, - OtpHashMode hash, int digits, int period, int counter, string expectedUri) - { - var uriString = new OtpUri(otpType, secret, user, issuer, hash, digits, period, counter).ToString(); - Assert.That(uriString, Is.EqualTo(expectedUri)); - } + [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha1, 6, 30, 0, + "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30")] + [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha256, 6, 30, 0, + "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA256&digits=6&period=30")] + [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha512, 6, 30, 0, + "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA512&digits=6&period=30")] + [TestCase(_baseSecret, OtpType.Hotp, _baseUser, _baseIssuer, OtpHashMode.Sha512, 6, 30, 0, + "otpauth://hotp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA512&digits=6&counter=0")] + public void GenerateOtpUriTest(string secret, OtpType otpType, string user, string issuer, + OtpHashMode hash, int digits, int period, int counter, string expectedUri) + { + var uriString = new OtpUri(otpType, secret, user, issuer, hash, digits, period, counter).ToString(); + Assert.That(uriString, Is.EqualTo(expectedUri)); } } diff --git a/test/Otp.NET.Test/TotpTest.cs b/test/Otp.NET.Test/TotpTest.cs index 4b359d1..7a35bf4 100644 --- a/test/Otp.NET.Test/TotpTest.cs +++ b/test/Otp.NET.Test/TotpTest.cs @@ -3,50 +3,49 @@ using Moq; using NUnit.Framework; -namespace OtpNet.Test +namespace OtpNet.Test; + +[TestFixture] +public class TotpTest { - [TestFixture] - public class TotpTest - { - private const string rfc6238SecretSha1 = "12345678901234567890"; - private const string rfc6238SecretSha256 = "12345678901234567890123456789012"; - private const string rfc6238SecretSha512 = "1234567890123456789012345678901234567890123456789012345678901234"; + private const string rfc6238SecretSha1 = "12345678901234567890"; + private const string rfc6238SecretSha256 = "12345678901234567890123456789012"; + private const string rfc6238SecretSha512 = "1234567890123456789012345678901234567890123456789012345678901234"; - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 59, "94287082")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 59, "46119246")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 59, "90693936")] - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 1111111109, "07081804")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 1111111109, "68084774")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 1111111109, "25091201")] - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 1111111111, "14050471")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 1111111111, "67062674")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 1111111111, "99943326")] - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 1234567890, "89005924")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 1234567890, "91819424")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 1234567890, "93441116")] - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 2000000000, "69279037")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 2000000000, "90698825")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 2000000000, "38618901")] - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 20000000000, "65353130")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 20000000000, "77737706")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 20000000000, "47863826")] - public void ComputeTOTPTest(string secret, OtpHashMode hash, long timestamp, string expectedOtp) - { - Totp otpCalc = new Totp(Encoding.UTF8.GetBytes(secret), 30, hash, expectedOtp.Length); - DateTime time = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; - string otp = otpCalc.ComputeTotp(time); - Assert.That(otp, Is.EqualTo(expectedOtp)); - } + [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 59, "94287082")] + [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 59, "46119246")] + [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 59, "90693936")] + [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 1111111109, "07081804")] + [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 1111111109, "68084774")] + [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 1111111109, "25091201")] + [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 1111111111, "14050471")] + [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 1111111111, "67062674")] + [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 1111111111, "99943326")] + [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 1234567890, "89005924")] + [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 1234567890, "91819424")] + [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 1234567890, "93441116")] + [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 2000000000, "69279037")] + [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 2000000000, "90698825")] + [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 2000000000, "38618901")] + [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 20000000000, "65353130")] + [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 20000000000, "77737706")] + [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 20000000000, "47863826")] + public void ComputeTOTPTest(string secret, OtpHashMode hash, long timestamp, string expectedOtp) + { + Totp otpCalc = new Totp(Encoding.UTF8.GetBytes(secret), 30, hash, expectedOtp.Length); + DateTime time = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; + string otp = otpCalc.ComputeTotp(time); + Assert.That(otp, Is.EqualTo(expectedOtp)); + } - [Test] - public void ContructorWithKeyProviderTest() - { - //Mock a key provider which always returns an all-zero HMAC (causing an all-zero OTP) - Mock keyMock = new Mock(); - keyMock.Setup(key => key.ComputeHmac(It.Is(m => m == OtpHashMode.Sha1), It.IsAny())).Returns(new byte[20]); + [Test] + public void ContructorWithKeyProviderTest() + { + //Mock a key provider which always returns an all-zero HMAC (causing an all-zero OTP) + Mock keyMock = new Mock(); + keyMock.Setup(key => key.ComputeHmac(It.Is(m => m == OtpHashMode.Sha1), It.IsAny())).Returns(new byte[20]); - var otp = new Totp(keyMock.Object, 30, OtpHashMode.Sha1, 6); - Assert.That(otp.ComputeTotp(), Is.EqualTo("000000")); - } + var otp = new Totp(keyMock.Object, 30, OtpHashMode.Sha1, 6); + Assert.That(otp.ComputeTotp(), Is.EqualTo("000000")); } } From 7af3a19ed91819247169649d4fbb28b020275ee5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 10 Apr 2023 10:00:19 -0400 Subject: [PATCH 24/48] update test project lang syntax --- test/Otp.NET.Test/HotpTest.cs | 10 +++---- test/Otp.NET.Test/OtpUriTest.cs | 14 ++++----- test/Otp.NET.Test/TotpTest.cs | 52 ++++++++++++++++----------------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/test/Otp.NET.Test/HotpTest.cs b/test/Otp.NET.Test/HotpTest.cs index ec1a77b..5c77604 100644 --- a/test/Otp.NET.Test/HotpTest.cs +++ b/test/Otp.NET.Test/HotpTest.cs @@ -6,7 +6,7 @@ namespace OtpNet.Test; [TestFixture] public class HotpTest { - private static readonly byte[] rfc4226Secret = { + private static readonly byte[] _rfc4226Secret = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30 @@ -24,16 +24,16 @@ public class HotpTest [TestCase(OtpHashMode.Sha1, 9, "520489")] public void ComputeHOTPRfc4226Test(OtpHashMode hash, long counter, string expectedOtp) { - Hotp otpCalc = new Hotp(rfc4226Secret, hash, expectedOtp.Length); - string otp = otpCalc.ComputeHOTP(counter); + var otpCalc = new Hotp(_rfc4226Secret, hash, expectedOtp.Length); + var otp = otpCalc.ComputeHOTP(counter); Assert.That(otp, Is.EqualTo(expectedOtp)); } [Test] public void ContructorWithKeyProviderTest() { - //Mock a key provider which always returns an all-zero HMAC (causing an all-zero OTP) - Mock keyMock = new Mock(); + // Mock a key provider which always returns an all-zero HMAC (causing an all-zero OTP) + var keyMock = new Mock(); keyMock.Setup(key => key.ComputeHmac(It.Is(m => m == OtpHashMode.Sha1), It.IsAny())).Returns(new byte[20]); var otp = new Hotp(keyMock.Object, OtpHashMode.Sha1, 6); diff --git a/test/Otp.NET.Test/OtpUriTest.cs b/test/Otp.NET.Test/OtpUriTest.cs index e8f75ab..92da2c8 100644 --- a/test/Otp.NET.Test/OtpUriTest.cs +++ b/test/Otp.NET.Test/OtpUriTest.cs @@ -6,17 +6,17 @@ namespace OtpNet.Test; [TestFixture] public class OtpUriTest { - private const string _baseSecret = "JBSWY3DPEHPK3PXP"; - private const string _baseUser = "alice@google.com"; - private const string _baseIssuer = "ACME Co"; + private const string BaseSecret = "JBSWY3DPEHPK3PXP"; + private const string BaseUser = "alice@google.com"; + private const string BaseIssuer = "ACME Co"; - [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha1, 6, 30, 0, + [TestCase(BaseSecret, OtpType.Totp, BaseUser, BaseIssuer, OtpHashMode.Sha1, 6, 30, 0, "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30")] - [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha256, 6, 30, 0, + [TestCase(BaseSecret, OtpType.Totp, BaseUser, BaseIssuer, OtpHashMode.Sha256, 6, 30, 0, "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA256&digits=6&period=30")] - [TestCase(_baseSecret, OtpType.Totp, _baseUser, _baseIssuer, OtpHashMode.Sha512, 6, 30, 0, + [TestCase(BaseSecret, OtpType.Totp, BaseUser, BaseIssuer, OtpHashMode.Sha512, 6, 30, 0, "otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA512&digits=6&period=30")] - [TestCase(_baseSecret, OtpType.Hotp, _baseUser, _baseIssuer, OtpHashMode.Sha512, 6, 30, 0, + [TestCase(BaseSecret, OtpType.Hotp, BaseUser, BaseIssuer, OtpHashMode.Sha512, 6, 30, 0, "otpauth://hotp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA512&digits=6&counter=0")] public void GenerateOtpUriTest(string secret, OtpType otpType, string user, string issuer, OtpHashMode hash, int digits, int period, int counter, string expectedUri) diff --git a/test/Otp.NET.Test/TotpTest.cs b/test/Otp.NET.Test/TotpTest.cs index 7a35bf4..5fa530a 100644 --- a/test/Otp.NET.Test/TotpTest.cs +++ b/test/Otp.NET.Test/TotpTest.cs @@ -8,41 +8,41 @@ namespace OtpNet.Test; [TestFixture] public class TotpTest { - private const string rfc6238SecretSha1 = "12345678901234567890"; - private const string rfc6238SecretSha256 = "12345678901234567890123456789012"; - private const string rfc6238SecretSha512 = "1234567890123456789012345678901234567890123456789012345678901234"; + private const string Rfc6238SecretSha1 = "12345678901234567890"; + private const string Rfc6238SecretSha256 = "12345678901234567890123456789012"; + private const string Rfc6238SecretSha512 = "1234567890123456789012345678901234567890123456789012345678901234"; - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 59, "94287082")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 59, "46119246")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 59, "90693936")] - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 1111111109, "07081804")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 1111111109, "68084774")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 1111111109, "25091201")] - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 1111111111, "14050471")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 1111111111, "67062674")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 1111111111, "99943326")] - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 1234567890, "89005924")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 1234567890, "91819424")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 1234567890, "93441116")] - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 2000000000, "69279037")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 2000000000, "90698825")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 2000000000, "38618901")] - [TestCase(rfc6238SecretSha1, OtpHashMode.Sha1, 20000000000, "65353130")] - [TestCase(rfc6238SecretSha256, OtpHashMode.Sha256, 20000000000, "77737706")] - [TestCase(rfc6238SecretSha512, OtpHashMode.Sha512, 20000000000, "47863826")] + [TestCase(Rfc6238SecretSha1, OtpHashMode.Sha1, 59, "94287082")] + [TestCase(Rfc6238SecretSha256, OtpHashMode.Sha256, 59, "46119246")] + [TestCase(Rfc6238SecretSha512, OtpHashMode.Sha512, 59, "90693936")] + [TestCase(Rfc6238SecretSha1, OtpHashMode.Sha1, 1111111109, "07081804")] + [TestCase(Rfc6238SecretSha256, OtpHashMode.Sha256, 1111111109, "68084774")] + [TestCase(Rfc6238SecretSha512, OtpHashMode.Sha512, 1111111109, "25091201")] + [TestCase(Rfc6238SecretSha1, OtpHashMode.Sha1, 1111111111, "14050471")] + [TestCase(Rfc6238SecretSha256, OtpHashMode.Sha256, 1111111111, "67062674")] + [TestCase(Rfc6238SecretSha512, OtpHashMode.Sha512, 1111111111, "99943326")] + [TestCase(Rfc6238SecretSha1, OtpHashMode.Sha1, 1234567890, "89005924")] + [TestCase(Rfc6238SecretSha256, OtpHashMode.Sha256, 1234567890, "91819424")] + [TestCase(Rfc6238SecretSha512, OtpHashMode.Sha512, 1234567890, "93441116")] + [TestCase(Rfc6238SecretSha1, OtpHashMode.Sha1, 2000000000, "69279037")] + [TestCase(Rfc6238SecretSha256, OtpHashMode.Sha256, 2000000000, "90698825")] + [TestCase(Rfc6238SecretSha512, OtpHashMode.Sha512, 2000000000, "38618901")] + [TestCase(Rfc6238SecretSha1, OtpHashMode.Sha1, 20000000000, "65353130")] + [TestCase(Rfc6238SecretSha256, OtpHashMode.Sha256, 20000000000, "77737706")] + [TestCase(Rfc6238SecretSha512, OtpHashMode.Sha512, 20000000000, "47863826")] public void ComputeTOTPTest(string secret, OtpHashMode hash, long timestamp, string expectedOtp) { - Totp otpCalc = new Totp(Encoding.UTF8.GetBytes(secret), 30, hash, expectedOtp.Length); - DateTime time = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; - string otp = otpCalc.ComputeTotp(time); + var otpCalc = new Totp(Encoding.UTF8.GetBytes(secret), 30, hash, expectedOtp.Length); + var time = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; + var otp = otpCalc.ComputeTotp(time); Assert.That(otp, Is.EqualTo(expectedOtp)); } [Test] public void ContructorWithKeyProviderTest() { - //Mock a key provider which always returns an all-zero HMAC (causing an all-zero OTP) - Mock keyMock = new Mock(); + // Mock a key provider which always returns an all-zero HMAC (causing an all-zero OTP) + var keyMock = new Mock(); keyMock.Setup(key => key.ComputeHmac(It.Is(m => m == OtpHashMode.Sha1), It.IsAny())).Returns(new byte[20]); var otp = new Totp(keyMock.Object, 30, OtpHashMode.Sha1, 6); From d2afcc69c33ab2a3828a6135ece177936144ddba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ku=C4=8Dera?= <10546952+miloush@users.noreply.github.com> Date: Thu, 20 Jul 2023 15:50:04 +0100 Subject: [PATCH 25/48] OTP properties (#49) Co-authored-by: miloush --- src/Otp.NET/Hotp.cs | 5 +++++ src/Otp.NET/Otp.cs | 5 +++++ src/Otp.NET/Totp.cs | 15 +++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/Otp.NET/Hotp.cs b/src/Otp.NET/Hotp.cs index fefbe6c..c8523d6 100644 --- a/src/Otp.NET/Hotp.cs +++ b/src/Otp.NET/Hotp.cs @@ -38,6 +38,11 @@ public class Hotp : Otp { private readonly int _hotpSize; + /// + /// Gets the number of diigts that the returning HOTP should have. + /// + public int HotpSize => _hotpSize; + /// /// Create a HOTP instance /// diff --git a/src/Otp.NET/Otp.cs b/src/Otp.NET/Otp.cs index dd37a3b..d445c31 100644 --- a/src/Otp.NET/Otp.cs +++ b/src/Otp.NET/Otp.cs @@ -45,6 +45,11 @@ public abstract class Otp /// protected readonly OtpHashMode _hashMode; + /// + /// Gets the hash mode to use + /// + public OtpHashMode HashMode => _hashMode; + /// /// Constructor for the abstract class using an explicit secret key /// diff --git a/src/Otp.NET/Totp.cs b/src/Otp.NET/Totp.cs index a919d8c..05b08d5 100644 --- a/src/Otp.NET/Totp.cs +++ b/src/Otp.NET/Totp.cs @@ -50,6 +50,21 @@ public class Totp : Otp private readonly int _totpSize; private readonly TimeCorrection _correctedTime; + /// + /// Gets the time window step amount to use in calculating time windows + /// + public int Step => _step; + + /// + /// Gets the number of digits that the returning TOTP should have + /// + public int TotpSize => _totpSize; + + /// + /// Gets the time correction to componensate out of sync local clock + /// + public TimeCorrection TimeCorrection => _correctedTime; + /// /// Create a TOTP instance /// From 0ac3c315d3d64aa7a89361167fe2e8be8b512ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ku=C4=8Dera?= <10546952+miloush@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:47:45 +0100 Subject: [PATCH 26/48] accessible TOTP window start (#50) Co-authored-by: miloush --- src/Otp.NET/Totp.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Otp.NET/Totp.cs b/src/Otp.NET/Totp.cs index 05b08d5..e527073 100644 --- a/src/Otp.NET/Totp.cs +++ b/src/Otp.NET/Totp.cs @@ -248,6 +248,25 @@ public int RemainingSeconds(DateTime timestamp) => private int RemainingSecondsForSpecificTime(DateTime timestamp) => _step - (int)(((timestamp.Ticks - UnicEpocTicks) / TicksToSeconds) % _step); + /// + /// Start of the 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. + /// + /// Start of the current window + public DateTime WindowStart() + { + return WindowStartForSpecificTime(_correctedTime.CorrectedUtcNow); + } + + public DateTime WindowStart(DateTime timestamp) => + WindowStartForSpecificTime(_correctedTime.GetCorrectedTime(timestamp)); + + private DateTime WindowStartForSpecificTime(DateTime timestamp) => + timestamp.AddTicks(-(timestamp.Ticks - UnicEpocTicks) % (TicksToSeconds * _step)); + /// /// Takes a time step and computes a TOTP code /// From 887f3f166aa6d98ffe49448a9dc0664882472908 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 22 Jan 2024 08:58:56 -0500 Subject: [PATCH 27/48] update to .net 8 --- src/Otp.NET/Otp.NET.csproj | 2 +- test/Otp.NET.Test/Otp.NET.Test.csproj | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index 46004a5..e1ef4e2 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -3,7 +3,7 @@ latest 1.3.0 - net461;net5.0;net6.0;net7.0;netstandard2.0 + net461;net5.0;net6.0;net7.0;net8.0;netstandard2.0 Otp.NET Otp.NET Kyle Spearrin diff --git a/test/Otp.NET.Test/Otp.NET.Test.csproj b/test/Otp.NET.Test/Otp.NET.Test.csproj index 9b6a256..cc4fa28 100644 --- a/test/Otp.NET.Test/Otp.NET.Test.csproj +++ b/test/Otp.NET.Test/Otp.NET.Test.csproj @@ -1,16 +1,16 @@  - net7.0 + net8.0 false OtpNet.Test - - - - + + + + From 924d771caae3b8bfd541919bc4b0d7ca7a0e7443 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 22 Jan 2024 10:00:29 -0500 Subject: [PATCH 28/48] appveyor fixes --- appveyor.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index f52679b..2f150b7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,9 +1,11 @@ +image: Visual Studio 2022 + before_build: - dotnet restore build_script: - - dotnet build -c "Release" --no-restore - - dotnet pack ./src/Otp.NET/Otp.NET.csproj --no-build -o ./dist -c "Release" + - dotnet build -c "Debug" --no-restore + - dotnet pack ./src/Otp.NET/Otp.NET.csproj --no-build -o ./dist -c "Debug" test_script: - dotnet test --no-build From 14a0a1cc605a333ba9e2d16e60898ba4ce6bf394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Will=20=E4=BF=9D=E5=93=A5?= Date: Tue, 23 Jan 2024 09:51:17 +0800 Subject: [PATCH 29/48] Update README.md (#51) fix a typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6322876..1afc860 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ var hotp = new Hotp(secretKey, mode: OtpHashMode.Sha512); Finally the truncation level can be specified. Basically this is how many digits do you want your HOTP code to be. The tests in the RFC specify 8, but 6 has become a de-facto standard if not an actual one. For this reason the default is 6 but you can set it to something else. There aren't a lot of tests around this either so use at your own risk (other than the fact that the RFC test table uses HOTP values that are 8 digits). ```c# -var hotp = new Hotp(secretKey, totpSize: 8); +var hotp = new Hotp(secretKey, hotpSize: 8); ``` #### Verification From 7b7da32a0867a6106dd9285c7d928939b9d44cc1 Mon Sep 17 00:00:00 2001 From: Todd Miranda Date: Sun, 18 Feb 2024 08:11:58 -0600 Subject: [PATCH 30/48] remove forced truncation to 6 digits (#31) Co-authored-by: Kyle Spearrin --- src/Otp.NET/Otp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Otp.NET/Otp.cs b/src/Otp.NET/Otp.cs index d445c31..37362a4 100644 --- a/src/Otp.NET/Otp.cs +++ b/src/Otp.NET/Otp.cs @@ -105,7 +105,7 @@ protected internal long CalculateOtp(byte[] data, OtpHashMode mode) return (hmacComputedHash[offset] & 0x7f) << 24 | (hmacComputedHash[offset + 1] & 0xff) << 16 | (hmacComputedHash[offset + 2] & 0xff) << 8 - | (hmacComputedHash[offset + 3] & 0xff) % 1000000; + | (hmacComputedHash[offset + 3] & 0xff); } /// From df2abdd3e9608f5821a0d967a9f14d9b32941f94 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sun, 18 Feb 2024 09:40:21 -0500 Subject: [PATCH 31/48] added 6 digit tests --- test/Otp.NET.Test/TotpTest.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/Otp.NET.Test/TotpTest.cs b/test/Otp.NET.Test/TotpTest.cs index 5fa530a..cb036c4 100644 --- a/test/Otp.NET.Test/TotpTest.cs +++ b/test/Otp.NET.Test/TotpTest.cs @@ -30,6 +30,9 @@ public class TotpTest [TestCase(Rfc6238SecretSha1, OtpHashMode.Sha1, 20000000000, "65353130")] [TestCase(Rfc6238SecretSha256, OtpHashMode.Sha256, 20000000000, "77737706")] [TestCase(Rfc6238SecretSha512, OtpHashMode.Sha512, 20000000000, "47863826")] + [TestCase(Rfc6238SecretSha1, OtpHashMode.Sha1, 20000000000, "353130")] + [TestCase(Rfc6238SecretSha256, OtpHashMode.Sha256, 20000000000, "737706")] + [TestCase(Rfc6238SecretSha512, OtpHashMode.Sha512, 20000000000, "863826")] public void ComputeTOTPTest(string secret, OtpHashMode hash, long timestamp, string expectedOtp) { var otpCalc = new Totp(Encoding.UTF8.GetBytes(secret), 30, hash, expectedOtp.Length); From 8ed6a96150b684d9e41788d46369557906e6d7e1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sun, 18 Feb 2024 09:41:44 -0500 Subject: [PATCH 32/48] add base32 comments --- test/Otp.NET.Test/TotpTest.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/Otp.NET.Test/TotpTest.cs b/test/Otp.NET.Test/TotpTest.cs index cb036c4..ea4d69b 100644 --- a/test/Otp.NET.Test/TotpTest.cs +++ b/test/Otp.NET.Test/TotpTest.cs @@ -8,8 +8,11 @@ namespace OtpNet.Test; [TestFixture] public class TotpTest { + // Base32 = GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ private const string Rfc6238SecretSha1 = "12345678901234567890"; + // Base32 = GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA private const string Rfc6238SecretSha256 = "12345678901234567890123456789012"; + // Base32 = GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA private const string Rfc6238SecretSha512 = "1234567890123456789012345678901234567890123456789012345678901234"; [TestCase(Rfc6238SecretSha1, OtpHashMode.Sha1, 59, "94287082")] From 4f228107fae9ad46751753ccfe8e12d0be02a837 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sun, 18 Feb 2024 09:44:54 -0500 Subject: [PATCH 33/48] update libs --- test/Otp.NET.Test/Otp.NET.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Otp.NET.Test/Otp.NET.Test.csproj b/test/Otp.NET.Test/Otp.NET.Test.csproj index cc4fa28..ddb46df 100644 --- a/test/Otp.NET.Test/Otp.NET.Test.csproj +++ b/test/Otp.NET.Test/Otp.NET.Test.csproj @@ -9,7 +9,7 @@ - + From 48475dff7816f01874e452b7a1575dce9f40af96 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sun, 18 Feb 2024 15:13:29 -0500 Subject: [PATCH 34/48] cleanup syntax --- src/Otp.NET/Hotp.cs | 41 ++++----- src/Otp.NET/InMemoryKey.cs | 19 ++-- src/Otp.NET/KeyGeneration.cs | 27 +----- src/Otp.NET/OtpUri.cs | 86 ++++++++--------- src/Otp.NET/TimeCorrection.cs | 14 +-- src/Otp.NET/Totp.cs | 158 ++++++++++++++++---------------- test/Otp.NET.Test/OtpUriTest.cs | 3 +- 7 files changed, 158 insertions(+), 190 deletions(-) diff --git a/src/Otp.NET/Hotp.cs b/src/Otp.NET/Hotp.cs index c8523d6..be53750 100644 --- a/src/Otp.NET/Hotp.cs +++ b/src/Otp.NET/Hotp.cs @@ -36,13 +36,6 @@ namespace OtpNet; /// public class Hotp : Otp { - private readonly int _hotpSize; - - /// - /// Gets the number of diigts that the returning HOTP should have. - /// - public int HotpSize => _hotpSize; - /// /// Create a HOTP instance /// @@ -53,7 +46,7 @@ public Hotp(byte[] secretKey, OtpHashMode mode = OtpHashMode.Sha1, int hotpSize : base(secretKey, mode) { VerifyParameters(hotpSize); - _hotpSize = hotpSize; + HotpSize = hotpSize; } /// @@ -66,21 +59,13 @@ public Hotp(IKeyProvider key, OtpHashMode mode = OtpHashMode.Sha1, int hotpSize : base(key, mode) { VerifyParameters(hotpSize); - - _hotpSize = hotpSize; + HotpSize = hotpSize; } - private static void VerifyParameters(int hotpSize) - { - if (hotpSize < 6) - { - throw new ArgumentOutOfRangeException(nameof(hotpSize)); - } - if (hotpSize > 8) - { - throw new ArgumentOutOfRangeException(nameof(hotpSize)); - } - } + /// + /// Gets the number of digits that the returning HOTP should have. + /// + public int HotpSize { get; private set; } /// /// Takes a counter and then computes a HOTP value @@ -108,6 +93,18 @@ protected override string Compute(long counter, OtpHashMode mode) { var data = KeyUtilities.GetBigEndianBytes(counter); var otp = CalculateOtp(data, mode); - return Digits(otp, _hotpSize); + return Digits(otp, HotpSize); + } + + private static void VerifyParameters(int hotpSize) + { + if (hotpSize < 6) + { + throw new ArgumentOutOfRangeException(nameof(hotpSize)); + } + if (hotpSize > 8) + { + throw new ArgumentOutOfRangeException(nameof(hotpSize)); + } } } diff --git a/src/Otp.NET/InMemoryKey.cs b/src/Otp.NET/InMemoryKey.cs index 9144f0c..e822ea7 100644 --- a/src/Otp.NET/InMemoryKey.cs +++ b/src/Otp.NET/InMemoryKey.cs @@ -49,7 +49,7 @@ namespace OtpNet; /// public class InMemoryKey : IKeyProvider { - private readonly object _stateSync = new object(); + private readonly object _stateSync = new(); private readonly byte[] _keyData; private readonly int _keyLength; @@ -122,20 +122,13 @@ public byte[] ComputeHmac(OtpHashMode mode, byte[] data) /// private static HMAC CreateHmacHash(OtpHashMode otpHashMode) { - HMAC hmacAlgorithm; - switch (otpHashMode) + HMAC hmacAlgorithm = otpHashMode switch { - case OtpHashMode.Sha256: - hmacAlgorithm = new HMACSHA256(); - break; - case OtpHashMode.Sha512: - hmacAlgorithm = new HMACSHA512(); - break; + OtpHashMode.Sha256 => new HMACSHA256(), + OtpHashMode.Sha512 => new HMACSHA512(), // case OtpHashMode.Sha1: - default: - hmacAlgorithm = new HMACSHA1(); - break; - } + _ => new HMACSHA1(), + }; return hmacAlgorithm; } } \ No newline at end of file diff --git a/src/Otp.NET/KeyGeneration.cs b/src/Otp.NET/KeyGeneration.cs index 53c817b..143d35e 100644 --- a/src/Otp.NET/KeyGeneration.cs +++ b/src/Otp.NET/KeyGeneration.cs @@ -89,31 +89,14 @@ public static byte[] DeriveKeyFromMaster( OtpHashMode mode = OtpHashMode.Sha1) => DeriveKeyFromMaster(masterKey, KeyUtilities.GetBigEndianBytes(serialNumber), mode); - private static HashAlgorithm GetHashAlgorithmForMode(OtpHashMode mode) - { - switch (mode) - { - case OtpHashMode.Sha256: - return SHA256.Create(); - case OtpHashMode.Sha512: - return SHA512.Create(); - // case OtpHashMode.Sha1: - default: - return SHA1.Create(); - } - } - private static int LengthForMode(OtpHashMode mode) { - switch (mode) + return mode switch { - case OtpHashMode.Sha256: - return 32; - case OtpHashMode.Sha512: - return 64; + OtpHashMode.Sha256 => 32, + OtpHashMode.Sha512 => 64, // case OtpHashMode.Sha1: - default: - return 20; - } + _ => 20, + }; } } \ No newline at end of file diff --git a/src/Otp.NET/OtpUri.cs b/src/Otp.NET/OtpUri.cs index 590b810..49004ec 100644 --- a/src/Otp.NET/OtpUri.cs +++ b/src/Otp.NET/OtpUri.cs @@ -7,49 +7,6 @@ namespace OtpNet; // See https://github.com/google/google-authenticator/wiki/Key-Uri-Format public class OtpUri { - /// - /// What type of OTP is this uri for - /// - /// - public readonly OtpType Type; - - /// - /// The secret parameter is an arbitrary key value encoded in Base32 according to RFC 3548. - /// The padding specified in RFC 3548 section 2.2 is not required and should be omitted. - /// - public readonly string Secret; - - /// - /// Which account a key is associated with - /// - public readonly string User; - - /// - /// The issuer parameter is a string value indicating the provider or service this account is - /// associated with, URL-encoded according to RFC 3986. - /// - public readonly string Issuer; - - /// - /// The algorithm used by the generator - /// - public readonly OtpHashMode Algorithm; - - /// - /// The amount of digits in the final code - /// - public readonly int Digits; - - /// - /// The number of seconds that a code is valid. Only applies to TOTP, not HOTP - /// - public readonly int Period; - - /// - /// Initial counter value for HOTP. This is ignored when using TOTP. - /// - public readonly int Counter; - /// /// Create a new OTP Auth Uri /// @@ -104,6 +61,49 @@ public OtpUri( algorithm, digits, period, counter) { } + /// + /// What type of OTP is this uri for + /// + /// + public OtpType Type { get; private set; } + + /// + /// The secret parameter is an arbitrary key value encoded in Base32 according to RFC 3548. + /// The padding specified in RFC 3548 section 2.2 is not required and should be omitted. + /// + public string Secret { get; private set; } + + /// + /// Which account a key is associated with + /// + public string User { get; private set; } + + /// + /// The issuer parameter is a string value indicating the provider or service this account is + /// associated with, URL-encoded according to RFC 3986. + /// + public string Issuer { get; private set; } + + /// + /// The algorithm used by the generator + /// + public OtpHashMode Algorithm { get; private set; } + + /// + /// The amount of digits in the final code + /// + public int Digits { get; private set; } + + /// + /// The number of seconds that a code is valid. Only applies to TOTP, not HOTP + /// + public int Period { get; private set; } + + /// + /// Initial counter value for HOTP. This is ignored when using TOTP. + /// + public int Counter { get; private set; } + /// /// Generates a Uri according to the parameters /// diff --git a/src/Otp.NET/TimeCorrection.cs b/src/Otp.NET/TimeCorrection.cs index 5ad7ef3..f88c5d6 100644 --- a/src/Otp.NET/TimeCorrection.cs +++ b/src/Otp.NET/TimeCorrection.cs @@ -47,7 +47,7 @@ public class TimeCorrection /// /// An instance that provides no correction factor /// - public static readonly TimeCorrection UncorrectedInstance = new TimeCorrection(); + public static readonly TimeCorrection UncorrectedInstance = new(); /// /// Constructor used solely for the UncorrectedInstance static field to provide an instance without @@ -77,11 +77,9 @@ public TimeCorrection(DateTime correctTime, DateTime referenceTime) => CorrectionFactor = referenceTime - correctTime; /// - /// Applies the correction factor to the reference time and returns a corrected time + /// The timespan that is used to calculate a corrected time /// - /// The reference time - /// The reference time with the correction factor applied - public DateTime GetCorrectedTime(DateTime referenceTime) => referenceTime - CorrectionFactor; + public TimeSpan CorrectionFactor { get; } /// /// Applies the correction factor to the current system UTC time and returns a corrected time @@ -89,7 +87,9 @@ public TimeCorrection(DateTime correctTime, DateTime referenceTime) => public DateTime CorrectedUtcNow => GetCorrectedTime(DateTime.UtcNow); /// - /// The timespan that is used to calculate a corrected time + /// Applies the correction factor to the reference time and returns a corrected time /// - public TimeSpan CorrectionFactor { get; } + /// The reference time + /// The reference time with the correction factor applied + public DateTime GetCorrectedTime(DateTime referenceTime) => referenceTime - CorrectionFactor; } \ No newline at end of file diff --git a/src/Otp.NET/Totp.cs b/src/Otp.NET/Totp.cs index e527073..a6cfb9a 100644 --- a/src/Otp.NET/Totp.cs +++ b/src/Otp.NET/Totp.cs @@ -46,25 +46,6 @@ public class Totp : Otp /// private const long TicksToSeconds = 10000000L; - private readonly int _step; - private readonly int _totpSize; - private readonly TimeCorrection _correctedTime; - - /// - /// Gets the time window step amount to use in calculating time windows - /// - public int Step => _step; - - /// - /// Gets the number of digits that the returning TOTP should have - /// - public int TotpSize => _totpSize; - - /// - /// Gets the time correction to componensate out of sync local clock - /// - public TimeCorrection TimeCorrection => _correctedTime; - /// /// Create a TOTP instance /// @@ -85,12 +66,12 @@ public Totp( { VerifyParameters(step, totpSize); - _step = step; - _totpSize = totpSize; + Step = step; + 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 neutral functionality in this case. - _correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance; + TimeCorrection = timeCorrection ?? TimeCorrection.UncorrectedInstance; } /// @@ -114,29 +95,28 @@ public Totp( { VerifyParameters(step, totpSize); - _step = step; - _totpSize = totpSize; + Step = step; + 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 neutral functionality in this case. - _correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance; + TimeCorrection = timeCorrection ?? TimeCorrection.UncorrectedInstance; } - private static void VerifyParameters(int step, int totpSize) - { - if (step <= 0) - { - throw new ArgumentOutOfRangeException(nameof(step)); - } - if (totpSize <= 0) - { - throw new ArgumentOutOfRangeException(nameof(totpSize)); - } - if (totpSize > 10) - { - throw new ArgumentOutOfRangeException(nameof(totpSize)); - } - } + /// + /// Gets the time window step amount to use in calculating time windows + /// + public int Step { get; private set; } + + /// + /// Gets the number of digits that the returning TOTP should have + /// + public int TotpSize { get; private set; } + + /// + /// Gets the time correction to componensate out of sync local clock + /// + public TimeCorrection TimeCorrection { get; private set; } /// /// Takes a timestamp and applies correction (if provided) and then computes a TOTP value @@ -144,7 +124,7 @@ private static void VerifyParameters(int step, int totpSize) /// The timestamp to use for the TOTP calculation /// a TOTP value public string ComputeTotp(DateTime timestamp) => - ComputeTotpFromSpecificTime(_correctedTime.GetCorrectedTime(timestamp)); + ComputeTotpFromSpecificTime(TimeCorrection.GetCorrectedTime(timestamp)); /// /// Takes a timestamp and computes a TOTP value for corrected UTC now @@ -154,13 +134,7 @@ public string ComputeTotp(DateTime timestamp) => /// If none was provided then simply the current UTC will be used. /// /// a TOTP value - public string ComputeTotp() => ComputeTotpFromSpecificTime(_correctedTime.CorrectedUtcNow); - - private string ComputeTotpFromSpecificTime(DateTime timestamp) - { - var window = CalculateTimeStepFromTimestamp(timestamp); - return Compute(window, _hashMode); - } + public string ComputeTotp() => ComputeTotpFromSpecificTime(TimeCorrection.CorrectedUtcNow); /// /// Verify a value that has been provided with the calculated value. @@ -178,7 +152,7 @@ private string ComputeTotpFromSpecificTime(DateTime timestamp) /// The window of steps to verify /// True if there is a match. public bool VerifyTotp(string totp, out long timeStepMatched, VerificationWindow window = null) => - VerifyTotpForSpecificTime(_correctedTime.CorrectedUtcNow, totp, window, out timeStepMatched); + VerifyTotpForSpecificTime(TimeCorrection.CorrectedUtcNow, totp, window, out timeStepMatched); /// /// Verify a value that has been provided with the calculated value @@ -199,31 +173,11 @@ public bool VerifyTotp( out long timeStepMatched, VerificationWindow window = null) => VerifyTotpForSpecificTime( - _correctedTime.GetCorrectedTime(timestamp), + TimeCorrection.GetCorrectedTime(timestamp), totp, window, out timeStepMatched); - private bool VerifyTotpForSpecificTime( - DateTime timestamp, - string totp, - VerificationWindow window, - out long timeStepMatched) - { - var initialStep = CalculateTimeStepFromTimestamp(timestamp); - return Verify(initialStep, totp, out timeStepMatched, window); - } - - /// - /// Takes a timestamp and calculates a time step - /// - private long CalculateTimeStepFromTimestamp(DateTime timestamp) - { - var unixTimestamp = (timestamp.Ticks - UnicEpocTicks) / TicksToSeconds; - var window = unixTimestamp / (long)_step; - return window; - } - /// /// Remaining seconds in current window based on UtcNow /// @@ -234,7 +188,7 @@ private long CalculateTimeStepFromTimestamp(DateTime timestamp) /// Number of remaining seconds public int RemainingSeconds() { - return RemainingSecondsForSpecificTime(_correctedTime.CorrectedUtcNow); + return RemainingSecondsForSpecificTime(TimeCorrection.CorrectedUtcNow); } /// @@ -243,10 +197,7 @@ public int RemainingSeconds() /// The timestamp /// Number of remaining seconds public int RemainingSeconds(DateTime timestamp) => - RemainingSecondsForSpecificTime(_correctedTime.GetCorrectedTime(timestamp)); - - private int RemainingSecondsForSpecificTime(DateTime timestamp) => - _step - (int)(((timestamp.Ticks - UnicEpocTicks) / TicksToSeconds) % _step); + RemainingSecondsForSpecificTime(TimeCorrection.GetCorrectedTime(timestamp)); /// /// Start of the current window based on UtcNow @@ -258,14 +209,11 @@ private int RemainingSecondsForSpecificTime(DateTime timestamp) => /// Start of the current window public DateTime WindowStart() { - return WindowStartForSpecificTime(_correctedTime.CorrectedUtcNow); + return WindowStartForSpecificTime(TimeCorrection.CorrectedUtcNow); } public DateTime WindowStart(DateTime timestamp) => - WindowStartForSpecificTime(_correctedTime.GetCorrectedTime(timestamp)); - - private DateTime WindowStartForSpecificTime(DateTime timestamp) => - timestamp.AddTicks(-(timestamp.Ticks - UnicEpocTicks) % (TicksToSeconds * _step)); + WindowStartForSpecificTime(TimeCorrection.GetCorrectedTime(timestamp)); /// /// Takes a time step and computes a TOTP code @@ -277,6 +225,54 @@ protected override string Compute(long counter, OtpHashMode mode) { var data = KeyUtilities.GetBigEndianBytes(counter); var otp = CalculateOtp(data, mode); - return Digits(otp, _totpSize); + return Digits(otp, TotpSize); + } + + private static void VerifyParameters(int step, int totpSize) + { + if (step <= 0) + { + throw new ArgumentOutOfRangeException(nameof(step)); + } + if (totpSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(totpSize)); + } + if (totpSize > 10) + { + throw new ArgumentOutOfRangeException(nameof(totpSize)); + } } + + private string ComputeTotpFromSpecificTime(DateTime timestamp) + { + var window = CalculateTimeStepFromTimestamp(timestamp); + return Compute(window, _hashMode); + } + + private bool VerifyTotpForSpecificTime( + DateTime timestamp, + string totp, + VerificationWindow window, + out long timeStepMatched) + { + var initialStep = CalculateTimeStepFromTimestamp(timestamp); + return Verify(initialStep, totp, out timeStepMatched, window); + } + + /// + /// Takes a timestamp and calculates a time step + /// + private long CalculateTimeStepFromTimestamp(DateTime timestamp) + { + var unixTimestamp = (timestamp.Ticks - UnicEpocTicks) / TicksToSeconds; + var window = unixTimestamp / (long)Step; + return window; + } + + private int RemainingSecondsForSpecificTime(DateTime timestamp) => + Step - (int)(((timestamp.Ticks - UnicEpocTicks) / TicksToSeconds) % Step); + + private DateTime WindowStartForSpecificTime(DateTime timestamp) => + timestamp.AddTicks(-(timestamp.Ticks - UnicEpocTicks) % (TicksToSeconds * Step)); } \ No newline at end of file diff --git a/test/Otp.NET.Test/OtpUriTest.cs b/test/Otp.NET.Test/OtpUriTest.cs index 92da2c8..4e4a574 100644 --- a/test/Otp.NET.Test/OtpUriTest.cs +++ b/test/Otp.NET.Test/OtpUriTest.cs @@ -1,5 +1,4 @@ -using System; -using NUnit.Framework; +using NUnit.Framework; namespace OtpNet.Test; From 8246823c93c103b5ff03ad07a484eb878bfad712 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2024 07:35:49 -0400 Subject: [PATCH 35/48] bump version --- src/Otp.NET/Otp.NET.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index e1ef4e2..def8521 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -2,7 +2,7 @@ latest - 1.3.0 + 1.4.0 net461;net5.0;net6.0;net7.0;net8.0;netstandard2.0 Otp.NET Otp.NET From 1f63a82b96f82e62a7082d657593415316074633 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2024 07:43:58 -0400 Subject: [PATCH 36/48] GeneratePackageOnBuild --- src/Otp.NET/Otp.NET.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index def8521..714f2eb 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -20,6 +20,7 @@ For documentation and examples visit the project website on GitHub at https://gi https://github.com/kspearrin/Otp.NET true OtpNet + True From a5b437f672997840bfdaaccc6d8dcb9199c8b551 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2024 08:21:16 -0400 Subject: [PATCH 37/48] Create action workflow --- .github/workflows/dotnet.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/dotnet.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..c62b908 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,28 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: .NET + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal From 65758d7b57fe9de0a11c439621cf8028994e5f91 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2024 08:52:30 -0400 Subject: [PATCH 38/48] workflows --- .github/workflows/dotnet.yml | 8 +++++--- .github/workflows/nuget.yml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/nuget.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index c62b908..c4ea109 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,6 +1,3 @@ -# This workflow will build a .NET project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net - name: .NET on: @@ -26,3 +23,8 @@ jobs: run: dotnet build --no-restore - name: Test run: dotnet test --no-build --verbosity normal + - name: Upload package + uses: actions/upload-artifact@v4 + with: + name: otpnet-nuget-package + path: src/Otp.NET/bin/Debug/*.nupkg diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml new file mode 100644 index 0000000..ddc796f --- /dev/null +++ b/.github/workflows/nuget.yml @@ -0,0 +1,32 @@ +name: Upload dotnet package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + permissions: + packages: write + contents: read + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore --configuration Release + - name: Publish to nuget.org + run: dotnet nuget push src/Otp.NET/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json + - name: Upload package + uses: actions/upload-artifact@v4 + with: + name: otpnet-nuget-package + path: src/Otp.NET/bin/Release/*.nupkg \ No newline at end of file From 78ee49e033269da9160f5ed2c938e903e1b73a47 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2024 08:56:31 -0400 Subject: [PATCH 39/48] rename nuget workflow --- .github/workflows/nuget.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index ddc796f..6c1ec7d 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -1,4 +1,4 @@ -name: Upload dotnet package +name: Upload NuGet package on: release: From 04e1b8c4decd004d985803936b66f66061e75224 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2024 08:58:44 -0400 Subject: [PATCH 40/48] NUGET_AUTH_TOKEN env --- .github/workflows/nuget.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index 6c1ec7d..c5a3832 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -25,6 +25,8 @@ jobs: run: dotnet build --no-restore --configuration Release - name: Publish to nuget.org run: dotnet nuget push src/Otp.NET/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json + env: + NUGET_AUTH_TOKEN: ${{ secrets.NUGET_TOKEN }} - name: Upload package uses: actions/upload-artifact@v4 with: From 08f8fabfd8153fde9012445888fa15ac7d630e36 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2024 09:11:49 -0400 Subject: [PATCH 41/48] restore signing key --- .github/workflows/nuget.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index c5a3832..c1c1d1b 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -19,6 +19,11 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x + - name: Restore signing key + env: + SIGNING_KEY: ${{ secrets.VS_SIGNING_KEY }} + run: | + echo $SIGNING_KEY | base64 --decode > src/Otp.NET/vs-signing-key.snk - name: Restore dependencies run: dotnet restore - name: Build From 16f92e21af5392bdc543da8ee3fc3bb7e557a8eb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2024 09:13:35 -0400 Subject: [PATCH 42/48] remove appveyor --- Otp.NET.sln | 1 - appveyor.yml | 16 ---------------- 2 files changed, 17 deletions(-) delete mode 100644 appveyor.yml diff --git a/Otp.NET.sln b/Otp.NET.sln index e3bed79..de4627f 100644 --- a/Otp.NET.sln +++ b/Otp.NET.sln @@ -13,7 +13,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2A4A70D0-4968-4E0C-9E42-4AF2C076D38B}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore - appveyor.yml = appveyor.yml LICENSE.txt = LICENSE.txt README.md = README.md EndProjectSection diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 2f150b7..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,16 +0,0 @@ -image: Visual Studio 2022 - -before_build: - - dotnet restore - -build_script: - - dotnet build -c "Debug" --no-restore - - dotnet pack ./src/Otp.NET/Otp.NET.csproj --no-build -o ./dist -c "Debug" - -test_script: - - dotnet test --no-build - -deploy: off - -artifacts: - - path: 'dist\*.nupkg' From bcffc90eb1820d6f5e8d7e86b4f957f86b9c13e8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2024 09:16:57 -0400 Subject: [PATCH 43/48] github action badge --- .github/workflows/dotnet.yml | 2 +- Otp.NET.sln | 2 ++ README.md | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index c4ea109..1346475 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,4 +1,4 @@ -name: .NET +name: .NET Build on: push: diff --git a/Otp.NET.sln b/Otp.NET.sln index de4627f..e0e2363 100644 --- a/Otp.NET.sln +++ b/Otp.NET.sln @@ -13,7 +13,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2A4A70D0-4968-4E0C-9E42-4AF2C076D38B}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore + .github\workflows\dotnet.yml = .github\workflows\dotnet.yml LICENSE.txt = LICENSE.txt + .github\workflows\nuget.yml = .github\workflows\nuget.yml README.md = README.md EndProjectSection EndProject diff --git a/README.md b/README.md index 1afc860..c571414 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ An implementation TOTP [RFC 6238](http://tools.ietf.org/html/rfc6238) and HOTP [RFC 4226](http://tools.ietf.org/html/rfc4226) in C#. -[![Build status](https://ci.appveyor.com/api/projects/status/renwlv60h7cfxs34?svg=true)](https://ci.appveyor.com/project/kspearrin/otp-net) +![.NET build status (master)](https://github.com/kspearrin/Otp.NET/actions/workflows/dotnet.yml/badge.svg?branch=master) ## Get it on NuGet From c7545fa8acc035f96bd5ebb764b6358ab67a82ad Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2024 09:18:40 -0400 Subject: [PATCH 44/48] link badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c571414..b4334c8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ An implementation TOTP [RFC 6238](http://tools.ietf.org/html/rfc6238) and HOTP [RFC 4226](http://tools.ietf.org/html/rfc4226) in C#. -![.NET build status (master)](https://github.com/kspearrin/Otp.NET/actions/workflows/dotnet.yml/badge.svg?branch=master) +[![.NET build status (master)](https://github.com/kspearrin/Otp.NET/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/kspearrin/Otp.NET/actions/workflows/dotnet.yml) ## Get it on NuGet From 16e2a5e3f6e932321d63151f1cd00deee39ea8b3 Mon Sep 17 00:00:00 2001 From: Kiril Karagyozov Date: Thu, 18 Apr 2024 14:24:51 +0300 Subject: [PATCH 45/48] Use long for counter in OtpUri (#61) --- src/Otp.NET/OtpUri.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Otp.NET/OtpUri.cs b/src/Otp.NET/OtpUri.cs index 49004ec..ca801e1 100644 --- a/src/Otp.NET/OtpUri.cs +++ b/src/Otp.NET/OtpUri.cs @@ -18,7 +18,7 @@ public OtpUri( OtpHashMode algorithm = OtpHashMode.Sha1, int digits = 6, int period = 30, - int counter = 0) + long counter = 0) { _ = secret ?? throw new ArgumentNullException(nameof(secret)); _ = user ?? throw new ArgumentNullException(nameof(user)); @@ -56,7 +56,7 @@ public OtpUri( OtpHashMode algorithm = OtpHashMode.Sha1, int digits = 6, int period = 30, - int counter = 0) + long counter = 0) : this(schema, Base32Encoding.ToString(secret), user, issuer, algorithm, digits, period, counter) { } @@ -102,7 +102,7 @@ public OtpUri( /// /// Initial counter value for HOTP. This is ignored when using TOTP. /// - public int Counter { get; private set; } + public long Counter { get; private set; } /// /// Generates a Uri according to the parameters From 71f0be434ec194ecd8e6ab02b1d765ff04545a90 Mon Sep 17 00:00:00 2001 From: Ahmed Adewale Date: Tue, 14 Oct 2025 14:16:55 +0100 Subject: [PATCH 46/48] Fix typo in VerifyHotp method parameter name (#70) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b4334c8..3e0af8a 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ var hotp = new Hotp(secretKey, hotpSize: 8); The HOTP implementation provides a mechanism for verifying HOTP codes that are passed in. There is a method called VerifyHotp with an overload that takes a counter value. ```c# -public bool VerifyHotp(string totp, long counter); +public bool VerifyHotp(string hotp, long counter); ``` ### OTP Uri From e680dd0a87d132945f27636bc747613350ba8e69 Mon Sep 17 00:00:00 2001 From: Federico Arredondo Date: Fri, 28 Nov 2025 22:24:40 -0300 Subject: [PATCH 47/48] .NET 9 and .NET 10 support (#71) * chore: add .NET9 & .NET10 support, run tests on .NET10 * chore: update CI/CD to use .NET 10 --- .github/workflows/dotnet.yml | 2 +- .github/workflows/nuget.yml | 2 +- src/Otp.NET/Otp.NET.csproj | 2 +- test/Otp.NET.Test/Otp.NET.Test.csproj | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 1346475..8fc44d2 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index c1c1d1b..3d64248 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -18,7 +18,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Restore signing key env: SIGNING_KEY: ${{ secrets.VS_SIGNING_KEY }} diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index 714f2eb..70c562a 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -3,7 +3,7 @@ latest 1.4.0 - net461;net5.0;net6.0;net7.0;net8.0;netstandard2.0 + net461;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0 Otp.NET Otp.NET Kyle Spearrin diff --git a/test/Otp.NET.Test/Otp.NET.Test.csproj b/test/Otp.NET.Test/Otp.NET.Test.csproj index ddb46df..3233f8b 100644 --- a/test/Otp.NET.Test/Otp.NET.Test.csproj +++ b/test/Otp.NET.Test/Otp.NET.Test.csproj @@ -1,16 +1,16 @@  - net8.0 + net10.0 false OtpNet.Test - - - - + + + + From 83b39e1f2a5bfc87b6d1af15b91bffc6f1638429 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 1 Dec 2025 14:26:25 -0500 Subject: [PATCH 48/48] bump version --- src/Otp.NET/Otp.NET.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Otp.NET/Otp.NET.csproj b/src/Otp.NET/Otp.NET.csproj index 70c562a..01f84ec 100755 --- a/src/Otp.NET/Otp.NET.csproj +++ b/src/Otp.NET/Otp.NET.csproj @@ -2,7 +2,7 @@ latest - 1.4.0 + 1.4.1 net461;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0 Otp.NET Otp.NET