From ca932a74dba137900629b5d55ce63cd919737f96 Mon Sep 17 00:00:00 2001 From: Aliwoto Date: Thu, 30 Sep 2021 18:07:36 +0000 Subject: [PATCH] Add `Socialvoid.Security.Otp` namespace with its classes. Signed-off-by: Aliwoto --- Socialvoid/Client/HttpClientMessageHandler.cs | 214 ------------ Socialvoid/Client/SocialvoidClient.cs | 51 ++- Socialvoid/Client/SvClient.cs | 18 +- Socialvoid/Security/Otp/Base32Encoding.cs | 184 ++++++++++ Socialvoid/Security/Otp/Hotp.cs | 153 ++++++++ Socialvoid/Security/Otp/IKeyProvider.cs | 57 +++ Socialvoid/Security/Otp/InMemoryKey.cs | 167 +++++++++ Socialvoid/Security/Otp/KeyGeneration.cs | 132 +++++++ Socialvoid/Security/Otp/KeyUtilities.cs | 96 ++++++ Socialvoid/Security/Otp/Otp.cs | 194 +++++++++++ Socialvoid/Security/Otp/OtpHashMode.cs | 52 +++ Socialvoid/Security/Otp/TimeCorrection.cs | 144 ++++++++ Socialvoid/Security/Otp/Totp.cs | 326 ++++++++++++++++++ Socialvoid/Security/Otp/VerificationWindow.cs | 100 ++++++ Socialvoid/Socialvoid.csproj | 13 +- 15 files changed, 1663 insertions(+), 238 deletions(-) delete mode 100644 Socialvoid/Client/HttpClientMessageHandler.cs create mode 100644 Socialvoid/Security/Otp/Base32Encoding.cs create mode 100644 Socialvoid/Security/Otp/Hotp.cs create mode 100644 Socialvoid/Security/Otp/IKeyProvider.cs create mode 100644 Socialvoid/Security/Otp/InMemoryKey.cs create mode 100644 Socialvoid/Security/Otp/KeyGeneration.cs create mode 100644 Socialvoid/Security/Otp/KeyUtilities.cs create mode 100644 Socialvoid/Security/Otp/Otp.cs create mode 100644 Socialvoid/Security/Otp/OtpHashMode.cs create mode 100644 Socialvoid/Security/Otp/TimeCorrection.cs create mode 100644 Socialvoid/Security/Otp/Totp.cs create mode 100644 Socialvoid/Security/Otp/VerificationWindow.cs diff --git a/Socialvoid/Client/HttpClientMessageHandler.cs b/Socialvoid/Client/HttpClientMessageHandler.cs deleted file mode 100644 index c71bead..0000000 --- a/Socialvoid/Client/HttpClientMessageHandler.cs +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Buffers; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using Microsoft; -using Microsoft.VisualStudio.Threading; -using Nerdbank.Streams; -using StreamJsonRpc; -using StreamJsonRpc.Protocol; - -/// -/// A that sends requests and receives responses over HTTP using . -/// -/// -/// See the spec for JSON-RPC over HTTP here: https://www.jsonrpc.org/historical/json-rpc-over-http.html. -/// Only the POST method is supported. -/// -public class HttpClientMessageHandler : IJsonRpcMessageHandler -{ - #nullable enable - private static readonly ReadOnlyCollection AllowedContentTypes = new ReadOnlyCollection(new string[] - { - "application/json-rpc", - "application/json", - "application/jsonrequest", - }); - - /// - /// The Content-Type header to use in requests. - /// - private static readonly MediaTypeHeaderValue ContentTypeHeader = new MediaTypeHeaderValue(AllowedContentTypes[0]); - - /// - /// The Accept header to use in requests. - /// - private static readonly MediaTypeWithQualityHeaderValue AcceptHeader = new MediaTypeWithQualityHeaderValue(AllowedContentTypes[0]); - - private readonly HttpClient httpClient; - private readonly Uri requestUri; - private readonly AsyncQueue incomingMessages = new AsyncQueue(); - - /// - /// Backing field for the property. - /// - private TraceSource traceSource = new TraceSource(nameof(JsonRpc)); - - /// - /// Initializes a new instance of the class - /// with the default . - /// - /// The to use for transmitting JSON-RPC requests. - /// The URI to POST to where the entity will be the JSON-RPC message. - public HttpClientMessageHandler(HttpClient httpClient, Uri requestUri) - : this(httpClient, requestUri, new JsonMessageFormatter()) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The to use for transmitting JSON-RPC requests. - /// The URI to POST to where the entity will be the JSON-RPC message. - /// The message formatter. - public HttpClientMessageHandler(HttpClient httpClient, Uri requestUri, IJsonRpcMessageFormatter formatter) - { - this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - this.requestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri)); - this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); - } - - /// - /// Event IDs raised to our . - /// - public enum TraceEvent - { - /// - /// An HTTP response with an error status code was received. - /// - HttpErrorStatusCodeReceived, - } - - /// - /// Gets or sets the used to trace details about the HTTP transport operations. - /// - /// The value can never be null. - /// Thrown by the setter if a null value is provided. - public TraceSource TraceSource - { - get => this.traceSource; - set - { - Requires.NotNull(value, nameof(value)); - this.traceSource = value; - } - } - - /// - public bool CanRead => true; - - /// - public bool CanWrite => true; - - /// - public IJsonRpcMessageFormatter Formatter { get; } - - /// - public async ValueTask ReadAsync(CancellationToken cancellationToken) - { - var response = await this.incomingMessages.DequeueAsync(cancellationToken).ConfigureAwait(false); - - var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - using (var sequence = new Sequence()) - { -#if NETCOREAPP2_1 - int bytesRead; - do - { - var memory = sequence.GetMemory(4096); - bytesRead = await responseStream.ReadAsync(memory, cancellationToken).ConfigureAwait(false); - sequence.Advance(bytesRead); - } - while (bytesRead > 0); -#else - var buffer = ArrayPool.Shared.Rent(4096); - try - { - int bytesRead; - while (true) - { - bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - if (bytesRead == 0) - { - break; - } - - var memory = sequence.GetMemory(bytesRead); - buffer.AsMemory(0, bytesRead).CopyTo(memory); - sequence.Advance(bytesRead); - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } -#endif - - return this.Formatter.Deserialize(sequence); - } - } - - /// - public async ValueTask WriteAsync(JsonRpcMessage content, CancellationToken cancellationToken) - { - // Cast here because we only support transmitting requests anyway. - var contentAsRequest = (JsonRpcRequest)content; - - // The JSON-RPC over HTTP spec requires that we supply a Content-Length header, so we have to serialize up front - // in order to measure its length. - using (var sequence = new Sequence()) - { - this.Formatter.Serialize(sequence, content); - - var requestMessage = new HttpRequestMessage(HttpMethod.Post, this.requestUri); - requestMessage.Headers.Accept.Add(AcceptHeader); - requestMessage.Content = new StreamContent(sequence.AsReadOnlySequence.AsStream()); - requestMessage.Content.Headers.ContentType = ContentTypeHeader; - requestMessage.Content.Headers.ContentLength = sequence.Length; - - var response = await this.httpClient.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false); - if (response.IsSuccessStatusCode) - { - VerifyThrowStatusCode(contentAsRequest.IsResponseExpected ? HttpStatusCode.OK : HttpStatusCode.NoContent, response.StatusCode); - } - else - { - this.TraceSource.TraceEvent(TraceEventType.Error, (int)TraceEvent.HttpErrorStatusCodeReceived, "Received HTTP {0} {1} response to JSON-RPC request for method \"{2}\".", (int)response.StatusCode, response.StatusCode, contentAsRequest.Method); - } - - // The response is expected to be a success code, or an error code with a content-type that we can deserialize. - if (response.IsSuccessStatusCode || (response.Content?.Headers.ContentType?.MediaType is string mediaType && AllowedContentTypes.Contains(mediaType))) - { - // Some requests don't merit response messages, such as notifications in JSON-RPC. - // Servers may communicate this with 202 or 204 HTTPS status codes in the response. - // Others may (poorly?) send a 200 response but with an empty entity. - if (response.Content?.Headers.ContentLength > 0) - { - // Make the response available for receiving. - this.incomingMessages.Enqueue(response); - } - } - else - { - // Throw an exception because of the unexpected failure from the server without a JSON-RPC message attached. - response.EnsureSuccessStatusCode(); - } - } - } - - private static void VerifyThrowStatusCode(HttpStatusCode expected, HttpStatusCode actual) - { - if (expected != actual) - { - throw new BadRpcHeaderException($"Expected \"{(int)expected} {expected}\" response but received \"{(int)actual} {actual}\" instead."); - } - } -} \ No newline at end of file diff --git a/Socialvoid/Client/SocialvoidClient.cs b/Socialvoid/Client/SocialvoidClient.cs index 07a04a8..8adf7b6 100644 --- a/Socialvoid/Client/SocialvoidClient.cs +++ b/Socialvoid/Client/SocialvoidClient.cs @@ -1,11 +1,24 @@ -using System; -using System.Text.Encodings; -using System.Text; +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +using System; using System.Net.Http; -using System.Collections.Generic; using System.IO; -using StreamJsonRpc; -using StreamJsonRpc.Protocol; using Socialvoid.Security; using Socialvoid.JObjects; using Socialvoid.Errors.ServerErrors; @@ -128,10 +141,6 @@ namespace Socialvoid.Client //------------------------------------------------- #region field's Region /// - /// since: v0.0.0 - /// - protected JsonMessageFormatter _formatter = new(Encoding.UTF8); - /// /// the endpoint url of socialvoid servers. /// since: v0.0.0 /// @@ -256,7 +265,7 @@ namespace Socialvoid.Client {PublicHashKey, PublicHash}, {PrivateHashKey, PrivateHash}, {PlatformKey, Platform}, - //{NameKey, ClientName}, + {NameKey, ClientName}, {VersionKey, Version}, }; @@ -267,8 +276,18 @@ namespace Socialvoid.Client message.Content = SerializeContent(request); message.Content.Headers.ContentType = _contentTypeValue; var jresp = ParseContent(message); + + if (!string.IsNullOrEmpty(jresp.Result.ChallengeSecret)) + { + _should_otp = true; + _otp = GetChallengeAnswer(jresp.Result.ChallengeSecret); + // set challenege secret to null to avoid sending it again. + // this will avoid future conflicts in using old challenge secret. + jresp.Result.ChallengeSecret = null; + } + _session = jresp.Result; - return null; + return _session; } /// /// AuthenticateUser method (session.authenticate_user), @@ -359,6 +378,14 @@ namespace Socialvoid.Client Console.WriteLine(contentStr); } + /// + /// returns a challenge's answer using the session's challenge secret. + /// since: v0.0.0 + /// + protected internal virtual string GetChallengeAnswer(string secret) + { + return null; + } #endregion //------------------------------------------------- diff --git a/Socialvoid/Client/SvClient.cs b/Socialvoid/Client/SvClient.cs index 59a8071..1dbcc6d 100644 --- a/Socialvoid/Client/SvClient.cs +++ b/Socialvoid/Client/SvClient.cs @@ -1,4 +1,20 @@ -using System; +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ namespace Socialvoid.Client { diff --git a/Socialvoid/Security/Otp/Base32Encoding.cs b/Socialvoid/Security/Otp/Base32Encoding.cs new file mode 100644 index 0000000..d880eef --- /dev/null +++ b/Socialvoid/Security/Otp/Base32Encoding.cs @@ -0,0 +1,184 @@ +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +/* + * Credits to Devin Martin and the original OtpSharp library: + * https://github.com/kspearrin/Otp.NET + */ +/* + * Credits to "Shane" from SO answer here: + * http://stackoverflow.com/a/7135008/1090359 + */ + +using System; + +namespace Socialvoid.Security.Otp +{ + /// + /// Base32 encoding/decoding helper class. + /// since: v0.0.0 + /// + public static class Base32Encoding + { + //------------------------------------------------- + #region static Method's Region + /// + /// Converts a string to a byte array using the specified encoding. + /// since: v0.0.0 + /// + /// + /// + public static byte[] ToBytes(string input) + { + if(string.IsNullOrEmpty(input)) + { + throw new ArgumentNullException("input"); + } + + input = input.TrimEnd('='); //remove padding characters + int byteCount = input.Length * 5 / 8; //this must be TRUNCATED + byte[] returnArray = new byte[byteCount]; + + byte curByte = 0, bitsRemaining = 8; + int mask = 0, arrayIndex = 0; + + foreach(char c in input) + { + int cValue = CharToValue(c); + + if(bitsRemaining > 5) + { + mask = cValue << (bitsRemaining - 5); + curByte = (byte)(curByte | mask); + bitsRemaining -= 5; + } + else + { + mask = cValue >> (5 - bitsRemaining); + curByte = (byte)(curByte | mask); + returnArray[arrayIndex++] = curByte; + curByte = (byte)(cValue << (3 + bitsRemaining)); + bitsRemaining += 3; + } + } + + //if we didn't end with a full byte + if(arrayIndex != byteCount) + { + returnArray[arrayIndex] = curByte; + } + + return returnArray; + } + /// + /// Converts an array of byte to a Base32-encoded string. + /// since: v0.0.0 + /// + /// + /// + public static string ToString(byte[] input) + { + if(input == null || input.Length == 0) + { + throw new ArgumentNullException("input"); + } + + int charCount = (int)Math.Ceiling(input.Length / 5d) * 8; + char[] returnArray = new char[charCount]; + + byte nextChar = 0, bitsRemaining = 5; + int arrayIndex = 0; + + foreach(byte b in input) + { + nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining))); + returnArray[arrayIndex++] = ValueToChar(nextChar); + + if(bitsRemaining < 4) + { + nextChar = (byte)((b >> (3 - bitsRemaining)) & 31); + returnArray[arrayIndex++] = ValueToChar(nextChar); + bitsRemaining += 5; + } + + bitsRemaining -= 3; + nextChar = (byte)((b << bitsRemaining) & 31); + } + + //if we didn't end with a full char + if(arrayIndex != charCount) + { + returnArray[arrayIndex++] = ValueToChar(nextChar); + while(arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding + } + + return new string(returnArray); + } + /// + /// Converts a valid base32 character to it's corresponding value. + /// since: v0.0.0 + /// + /// + private static int CharToValue(char c) + { + // 65 - 90 == uppercase letters + if(c < 91 && c > 64) + { + return c - 65; + } + + // 50 - 55 == numbers 2-7 + if(c < 56 && c > 49) + { + return c - 24; + } + + // 97 - 122 == lowercase letters + if(c < 123 && c > 96) + { + return c - 97; + } + + // isn't in any of these chars range? + throw new ArgumentException( + "Character is not a valid Base32 character.", + nameof(c)); + } + /// + /// Converts a valid base32 byte value to its corresponding char. + /// since: v0.0.0 + /// + /// + private static char ValueToChar(byte b) + { + if (b < 26) + { + return (char)(b + 65); + } + + if (b < 32) + { + return (char)(b + 24); + } + + throw new ArgumentException("Byte is not a Base32 value", nameof(b)); + } + #endregion + //------------------------------------------------- + } +} diff --git a/Socialvoid/Security/Otp/Hotp.cs b/Socialvoid/Security/Otp/Hotp.cs new file mode 100644 index 0000000..282b50a --- /dev/null +++ b/Socialvoid/Security/Otp/Hotp.cs @@ -0,0 +1,153 @@ +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +/* + * Credits to Devin Martin and the original OtpSharp library: + * https://github.com/kspearrin/Otp.NET + */ + +using System; + +namespace Socialvoid.Security.Otp +{ + /// + /// Calculate HMAC-Based One-Time-Passwords (HOTP) from a secret key. + /// since: v0.0.0 + /// + /// + /// The specifications for the methods of this class can be found in RFC 4226: + /// http://tools.ietf.org/html/rfc4226 + /// + public sealed class Hotp : Otp + { + //------------------------------------------------- + #region field's Region + /// + /// The HOTP size. + /// since: v0.0.0 + /// + private readonly int _hotpSize; + #endregion + //------------------------------------------------- + #region Constructor's Region + /// + /// Creates an HOTP instance. + /// since: v0.0.0 + /// + /// + /// 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) + { + if(hotpSize < 6 || hotpSize > 8) + { + throw new ArgumentOutOfRangeException(nameof(hotpSize), + "The hotpSize must be between 6 and 8"); + } + + _hotpSize = hotpSize; + } + /// + /// Create a HOTP instance. + /// since: v0.0.0 + /// + /// + /// The key to use in HOTP calculations. + /// + /// + /// The hash mode to use. + /// + /// + /// The number of digits that the returning HOTP should have. + /// The default value is 6. + /// + public Hotp(IKeyProvider key, + OtpHashMode mode = OtpHashMode.Sha1, + int hotpSize = 6) + : base(key, mode) + { + + if(hotpSize < 6 || hotpSize > 8) + { + throw new ArgumentOutOfRangeException(nameof(hotpSize), + "The hotpSize must be between 6 and 8"); + } + + _hotpSize = hotpSize; + } + #endregion + //------------------------------------------------- + #region overrided Method's Region + /// + /// Takes a time step and computes a HOTP code. + /// since: v0.0.0 + /// + /// + /// the counter. This is the number of time steps that have passed. + /// + /// + /// The hash mode to use. + /// + /// + /// HOTP calculated code. + /// + protected override string Compute(long counter, OtpHashMode mode) + { + var data = KeyUtilities.GetBigEndianBytes(counter); + var otp = this.CalculateOtp(data, mode); + return Digits(otp, _hotpSize); + } + #endregion + //------------------------------------------------- + #region Get Method's Region + /// + /// Takes a counter and then computes a HOTP value. + /// since: v0.0.0 + /// + /// + /// The timestamp to use for the HOTP calculation. + /// + /// a HOTP value + public string ComputeHOTP(long counter) + { + return this.Compute(counter, _hashMode); + } + /// + /// Verify a value that has been provided with the calculated value. + /// since: v0.0.0 + /// + /// the trial HOTP value. + /// + /// + /// The counter value to verify + /// + /// + /// true if there is a match; otherwise false. + /// + public bool VerifyHotp(string hotp, long counter) => + hotp == ComputeHOTP(counter); + + #endregion + //------------------------------------------------- + } +} diff --git a/Socialvoid/Security/Otp/IKeyProvider.cs b/Socialvoid/Security/Otp/IKeyProvider.cs new file mode 100644 index 0000000..89c580c --- /dev/null +++ b/Socialvoid/Security/Otp/IKeyProvider.cs @@ -0,0 +1,57 @@ +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +/* + * Credits to Devin Martin and the original OtpSharp library: + * https://github.com/kspearrin/Otp.NET + */ + +namespace Socialvoid.Security.Otp +{ + /// + /// Interface used to interact with a key. + /// since: v0.0.0 + /// + public interface IKeyProvider + { + //------------------------------------------------- + #region Get Method's Region + /// + /// Uses the key to get an HMAC using the specified algorithm and data. + /// since: v0.0.0 + /// + /// + /// 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); + #endregion + //------------------------------------------------- + } +} diff --git a/Socialvoid/Security/Otp/InMemoryKey.cs b/Socialvoid/Security/Otp/InMemoryKey.cs new file mode 100644 index 0000000..9ed378d --- /dev/null +++ b/Socialvoid/Security/Otp/InMemoryKey.cs @@ -0,0 +1,167 @@ +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +/* + * Credits to Devin Martin and the original OtpSharp library: + * https://github.com/kspearrin/Otp.NET + */ + +using System; +using System.Security.Cryptography; + +namespace Socialvoid.Security.Otp +{ + /// + /// Represents a key in memory. + /// since: v0.0.0 + /// + /// + /// 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 + { + //------------------------------------------------- + #region field's Region + /// + /// The key data in memory. + /// since: v0.0.0 + /// + internal readonly byte[] _KeyData; + + /// + /// The key length representing the length of the . + /// since: v0.0.0 + /// + internal readonly int _keyLength; + /// + /// Used for locking. + /// since: v0.0.0 + /// + private readonly object _stateSync = new(); + #endregion + //------------------------------------------------- + #region Constructor's Region + /// + /// Creates an instance of a key. + /// + /// Plaintext key data + public InMemoryKey(byte[] key) + { + if(!(key != null)) + throw new ArgumentNullException("key"); + if(!(key.Length > 0)) + throw new ArgumentException("The key must not be empty"); + + _keyLength = key.Length; + int paddedKeyLength = (int)Math.Ceiling((decimal)key.Length / (decimal)16) * 16; + _KeyData = new byte[paddedKeyLength]; + Array.Copy(key, _KeyData, key.Length); + } + #endregion + //------------------------------------------------- + #region Get Method's Region + /// + /// Gets a copy of the plaintext key. + /// since: v0.0.0 + /// + /// + /// This is internal rather than protected so that the tests can + /// use this method. + /// + /// + /// Plaintext Key + /// + internal byte[] GetCopyOfKey() + { + var plainKey = new byte[_keyLength]; + lock(_stateSync) + { + Array.Copy(_KeyData, plainKey, _keyLength); + } + return plainKey; + } + /// + /// Uses the key to get an HMAC using the specified algorithm and data. + /// since: v0.0.0 + /// + /// + /// 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 = null; + using(HMAC hmac = CreateHmacHash(mode)) + { + byte[] key = this.GetCopyOfKey(); + try + { + hmac.Key = key; + hashedValue = hmac.ComputeHash(data); + } + finally + { + KeyUtilities.Destroy(key); + } + } + + return hashedValue; + } + + #endregion + //------------------------------------------------- + #region static Method's Region + /// + /// Create an HMAC object for the specified algorithm. + /// since: v0.0.0 + /// + private static HMAC CreateHmacHash(OtpHashMode otpHashMode) + { + return otpHashMode switch + { + OtpHashMode.Sha256 => new HMACSHA256(), + OtpHashMode.Sha512 => new HMACSHA512(), + _ => new HMACSHA1() //OtpHashMode.Sha1 + }; + } + #endregion + //------------------------------------------------- + } +} diff --git a/Socialvoid/Security/Otp/KeyGeneration.cs b/Socialvoid/Security/Otp/KeyGeneration.cs new file mode 100644 index 0000000..3cee5a8 --- /dev/null +++ b/Socialvoid/Security/Otp/KeyGeneration.cs @@ -0,0 +1,132 @@ +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +/* + * Credits to Devin Martin and the original OtpSharp library: + * https://github.com/kspearrin/Otp.NET + */ + + +using System; +using System.Security.Cryptography; + +namespace Socialvoid.Security.Otp +{ + /// + /// Helpers to work with key generations. + /// since: v0.0.0 + /// + public static class KeyGeneration + { + //------------------------------------------------- + #region static Method's Region + /// + /// Generates a random key in accordance with the RFC recommened + /// length for each algorithm. + /// since: v0.0.0 + /// + /// the key length + /// The generated key + public static byte[] GenerateRandomKey(int length) + { + byte[] key = new byte[length]; + using(var rnd = RandomNumberGenerator.Create()) + { + rnd.GetBytes(key); + return key; + } + } + /// + /// Generates a random key in accordance with the RFC recommened + /// length for each algorithm. + /// since: v0.0.0 + /// + /// HashMode + /// Key + public static byte[] GenerateRandomKey(OtpHashMode mode = OtpHashMode.Sha1) => + GenerateRandomKey(LengthForMode(mode)); + /// + /// Uses the procedure defined in RFC 4226 section 7.5 to derive a key + /// from the master key. + /// since: v0.0.0 + /// + /// + /// 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 value 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) + { + throw new ArgumentNullException(nameof(masterKey), + "The master key cannot be null"); + } + 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); + + private static HashAlgorithm GetHashAlgorithmForMode(OtpHashMode mode) + { + switch(mode) + { + case OtpHashMode.Sha256: + return SHA256.Create(); + case OtpHashMode.Sha512: + return SHA512.Create(); + default: //case OtpHashMode.Sha1: + return SHA1.Create(); + } + } + + private static int LengthForMode(OtpHashMode mode) + { + switch(mode) + { + case OtpHashMode.Sha256: + return 32; + case OtpHashMode.Sha512: + return 64; + default: //case OtpHashMode.Sha1: + return 20; + } + } + + #endregion + //------------------------------------------------- + } +} diff --git a/Socialvoid/Security/Otp/KeyUtilities.cs b/Socialvoid/Security/Otp/KeyUtilities.cs new file mode 100644 index 0000000..1c0f2cf --- /dev/null +++ b/Socialvoid/Security/Otp/KeyUtilities.cs @@ -0,0 +1,96 @@ +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +/* + * Credits to Devin Martin and the original OtpSharp library: + * https://github.com/kspearrin/Otp.NET + */ + + +using System; + +namespace Socialvoid.Security.Otp +{ + /// + /// Some helper methods to perform common key functions. + /// since: v0.0.0 + /// + internal static class KeyUtilities + { + //------------------------------------------------- + #region static Method's Region + /// + /// Overwrite potentially sensitive data with random junk. + /// since: v0.0.0 + /// + /// + /// 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 || sensitiveData.Length == 0) + { + // if there is no data, there is nothing to destroy; + // don't throw an exception, just return. + return; + } + + new Random().NextBytes(sensitiveData); + } + /// + /// converts a long into a big endian byte array. + /// since: v0.0.0 + /// + /// + /// RFC 4226 specifies big endian as the method for converting the counter + /// to data and then to hash. + /// + static internal byte[] GetBigEndianBytes(long input) + { + // Since .net uses little endian numbers, we need to reverse the byte order to get big endian. + var data = BitConverter.GetBytes(input); + Array.Reverse(data); + return data; + } + /// + /// converts an int into a big endian byte array. + /// since: v0.0.0 + /// + /// + /// RFC 4226 specifies big endian as the method for converting + /// the counter to data and then to hash. + /// + static internal byte[] GetBigEndianBytes(int input) + { + // Since .net uses little endian numbers, we need to reverse the byte order to get big endian. + var data = BitConverter.GetBytes(input); + Array.Reverse(data); + return data; + } + #endregion + //------------------------------------------------- + } +} diff --git a/Socialvoid/Security/Otp/Otp.cs b/Socialvoid/Security/Otp/Otp.cs new file mode 100644 index 0000000..7a1d4c6 --- /dev/null +++ b/Socialvoid/Security/Otp/Otp.cs @@ -0,0 +1,194 @@ +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +/* + * Credits to Devin Martin and the original OtpSharp library: + * https://github.com/kspearrin/Otp.NET + */ + +using System; + +namespace Socialvoid.Security.Otp +{ + /// + /// An abstract class that contains common OTP calculations. + /// since: v0.0.0 + /// + /// + /// https://tools.ietf.org/html/rfc4226 + /// + public abstract class Otp + { + //------------------------------------------------- + #region field's Region + /// + /// the secret key. + /// since: v0.0.0 + /// + protected readonly IKeyProvider _secretKey; + + /// + /// The hash mode to use. + /// since: v0.0.0 + /// + protected readonly OtpHashMode _hashMode; + #endregion + //------------------------------------------------- + #region Constructor's Region + /// + /// Constructor for the abstract class using an explicit secret key. + /// since: v0.0.0 + /// + /// + /// The secret key. + /// + /// + /// The hash mode to use. + /// + public Otp(byte[] secretKey, OtpHashMode mode) + { + if(secretKey == null || secretKey.Length == 0) + { + throw new ArgumentNullException(nameof(secretKey), + "Secret key cannot be null or 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; + } + /// + /// Constructor for the abstract class using a generic key provider. + /// since: v0.0.0 + /// + /// + /// The hash mode to use + public Otp(IKeyProvider key, OtpHashMode mode) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key), "key cannot be null"); + } + + _hashMode = mode; + _secretKey = key; + } + #endregion + //------------------------------------------------- + #region Get Method's Region + /// + /// An abstract definition of a compute method. + /// Takes a counter and runs it through the derived algorithm. + /// since: v0.0.0 + /// + /// Counter or step + /// The hash mode to use + /// OTP calculated code + protected abstract string Compute(long counter, OtpHashMode mode); + /// + /// Helper method that calculates OTPs. + /// since: v0.0.0 + /// + protected internal long CalculateOtp(byte[] data, OtpHashMode mode) + { + byte[] 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] + + 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. + /// since: v0.0.0 + /// + 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. + /// since: v0.0.0 + /// + /// + /// 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; otherwise false. + /// + protected bool Verify(long initialStep, string valueToVerify, out long matchedStep, VerificationWindow window) + { + window ??= new(); + foreach(var frame in window.ValidationCandidates(initialStep)) + { + var comparisonValue = this.Compute(frame, _hashMode); + if(ValuesEqual(comparisonValue, valueToVerify)) + { + matchedStep = frame; + return true; + } + } + + matchedStep = 0; + return false; + } + /// + /// Constant time comparison of two values. + /// since: v0.0.0 + /// + private bool ValuesEqual(string a, string b) + { + if (string.IsNullOrWhiteSpace(a) && string.IsNullOrWhiteSpace(b)) + { + return true; + } + + if(a.Length != b.Length) + { + return false; + } + + var result = 0; + for(int i = 0; i < a.Length; i++) + { + result |= a[i] ^ b[i]; + } + + return result == 0; + } + #endregion + //------------------------------------------------- + } +} diff --git a/Socialvoid/Security/Otp/OtpHashMode.cs b/Socialvoid/Security/Otp/OtpHashMode.cs new file mode 100644 index 0000000..82ce4e9 --- /dev/null +++ b/Socialvoid/Security/Otp/OtpHashMode.cs @@ -0,0 +1,52 @@ +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +/* + * Credits to Devin Martin and the original OtpSharp library: + * https://github.com/kspearrin/Otp.NET + */ + +namespace Socialvoid.Security.Otp +{ + /// + /// Indicates which HMAC hashing algorithm should be used. + /// since: v0.0.0 + /// + public enum OtpHashMode + { + //------------------------------------------------- + #region SHA region + /// + /// Sha1 is used as the HMAC hashing algorithm. + /// since: v0.0.0 + /// + Sha1, + /// + /// Sha256 is used as the HMAC hashing algorithm. + /// since: v0.0.0 + /// + Sha256, + /// + /// Sha512 is used as the HMAC hashing algorithm. + /// since: v0.0.0 + /// + Sha512, + #endregion + //------------------------------------------------- + } +} diff --git a/Socialvoid/Security/Otp/TimeCorrection.cs b/Socialvoid/Security/Otp/TimeCorrection.cs new file mode 100644 index 0000000..af80431 --- /dev/null +++ b/Socialvoid/Security/Otp/TimeCorrection.cs @@ -0,0 +1,144 @@ +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +/* + * Credits to Devin Martin and the original OtpSharp library: + * https://github.com/kspearrin/Otp.NET + */ + +using System; + +namespace Socialvoid.Security.Otp +{ + /// + /// Class to apply a correction factor to the system time. + /// since: v0.0.0 + /// + /// + /// In cases where the local system time is incorrect it is preferable to simply correct the system time. + /// This class is provided to handle cases where it isn't possible for the client, the server, or both, to be on the correct time. + /// + /// This library provides limited facilities to to ping NIST for a correct network time. This class can be used manually however in cases where a server's time is off + /// and the consumer of this library can't control it. In that case create an instance of this class and provide the current server time as the correct time parameter + /// + /// This class is immutable and therefore threadsafe. + /// + public class TimeCorrection + { + //------------------------------------------------- + #region Properties Region + /// + /// Applies the correction factor to the current system UTC time and + /// returns a corrected time. + /// since: v0.0.0 + /// + public DateTime CorrectedUtcNow + { + get => GetCorrectedTime(DateTime.UtcNow); + } + /// + /// The timespan that is used to calculate a corrected time. + /// since: v0.0.0 + /// + public TimeSpan CorrectionFactor + { + get => _timeCorrectionFactor; + } + #endregion + //------------------------------------------------- + #region static field's Region + /// + /// An instance that provides no correction factor. + /// since: v0.0.0 + /// + public static readonly TimeCorrection UncorrectedInstance = new(); + #endregion + //------------------------------------------------- + #region field's Region + /// + /// The timespan that is used as a correction factor. + /// since: v0.0.0 + /// + private readonly TimeSpan _timeCorrectionFactor; + #endregion + //------------------------------------------------- + #region Constructor's Region + /// + /// Constructor used solely for the static + /// field to provide an instance without a correction factor. + /// since: v0.0.0 + /// + private TimeCorrection() + { + _timeCorrectionFactor = TimeSpan.FromSeconds(0); + } + + /// + /// Creates a corrected time object by providing the known correct + /// current UTC time. + /// The current system UTC time will be used as the reference. + /// since: v0.0.0 + /// + /// + /// This overload assumes UTC. + /// If a base and reference time other than UTC are required then use the + /// other overlaod. + /// + /// The current correct UTC time + public TimeCorrection(DateTime correctUtc) + { + _timeCorrectionFactor = DateTime.UtcNow - correctUtc; + } + + /// + /// Creates a corrected time object by providing the known correct current time + /// and the current reference time that needs correction. + /// since: v0.0.0 + /// + /// + /// 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) + { + _timeCorrectionFactor = referenceTime - correctTime; + } + #endregion + //------------------------------------------------- + #region Get Method's Region + /// + /// Applies the correction factor to the reference time and returns a + /// corrected time. + /// since: v0.0.0 + /// + /// + /// The reference time. + /// + /// + /// The reference time with the correction factor applied. + /// + public DateTime GetCorrectedTime(DateTime referenceTime) => + referenceTime - _timeCorrectionFactor; + + #endregion + //------------------------------------------------- + } +} diff --git a/Socialvoid/Security/Otp/Totp.cs b/Socialvoid/Security/Otp/Totp.cs new file mode 100644 index 0000000..0614aa8 --- /dev/null +++ b/Socialvoid/Security/Otp/Totp.cs @@ -0,0 +1,326 @@ +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +/* + * Credits to Devin Martin and the original OtpSharp library: + * https://github.com/kspearrin/Otp.NET + */ + + +using System; + +namespace Socialvoid.Security.Otp +{ + /// + /// Calculate Timed-One-Time-Passwords (TOTP) from a secret key. + /// since: v0.0.0 + /// + /// + /// The specifications for the methods of this class can be found in RFC 6238: + /// http://tools.ietf.org/html/rfc6238 + /// + public sealed class Totp : Otp + { + //------------------------------------------------- + #region Constant's Region + /// + /// The number of ticks as Measured at Midnight Jan 1st 1970. + /// since: v0.0.0 + /// + internal const long unixEpochTicks = 621355968000000000L; + /// + /// A divisor for converting ticks to seconds. + /// since: v0.0.0 + /// + internal const long ticksToSeconds = 10000000L; + #endregion + //------------------------------------------------- + #region field's Region + /// + /// the step value. + /// since: v0.0.0 + /// + private readonly int _step; + /// + /// the TOTP length. + /// since: v0.0.0 + /// + private readonly int _totpSize; + /// + /// the TOTP corrected time. + /// since: v0.0.0 + /// + private readonly TimeCorrection correctedTime; + #endregion + //------------------------------------------------- + #region Constructor's Region + /// + /// Creates a TOTP instance. + /// since: v0.0.0 + /// + /// + /// 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 value of this argument 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) + { + + if (step < 0) + { + throw new ArgumentOutOfRangeException(nameof(step), + "Step must be greater than 0"); + } + if (totpSize < 0 || totpSize > 10) + { + throw new ArgumentOutOfRangeException(nameof(totpSize), + "TOTP size must be greater than 0 and less than 10"); + } + + _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. + correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance; + } + + /// + /// Creates a TOTP instance. + /// since: v0.0.0 + /// + /// + /// 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) + { + if (step < 0) + { + throw new ArgumentOutOfRangeException(nameof(step), + "Step must be greater than 0"); + } + if (totpSize < 0 || totpSize > 10) + { + throw new ArgumentOutOfRangeException(nameof(totpSize), + "TOTP size must be greater than 0 and less than 10"); + } + + _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. + correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance; + } + #endregion + //------------------------------------------------- + #region overrided Method's Region + /// + /// Takes a time step and computes a TOTP code. + /// since: v0.0.0 + /// + /// time step + /// The hash mode to use + /// TOTP calculated code + protected override string Compute(long counter, OtpHashMode mode) + { + var data = KeyUtilities.GetBigEndianBytes(counter); + var otp = this.CalculateOtp(data, mode); + return Digits(otp, _totpSize); + } + #endregion + //------------------------------------------------- + #region Get Method's Region + /// + /// Takes a timestamp and applies correction (if provided) and then computes + /// a TOTP value. + /// since: v0.0.0 + /// + /// 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. + /// since: v0.0.0 + /// + /// + /// 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(this.correctedTime.CorrectedUtcNow); + + /// + /// Verify a value that has been provided with the calculated value. + /// since: v0.0.0 + /// + /// + /// It will be corrected against a corrected UTC time using the provided time correction. If none was provided then simply the current UTC will be used. + /// + /// + /// the trial TOTP value + /// + /// + /// This is an output parameter that gives that time step that was used to find a match. + /// This is useful in cases where a TOTP value should only be used once. This value is a unique identifier of the + /// time step (not the value) that can be used to prevent the same step from being used multiple times + /// + /// + /// The window of steps to verify. + /// + /// True if there is a match. + public bool VerifyTotp(string totp, + out long timeStepMatched, + VerificationWindow window = null) + { + return this.VerifyTotpForSpecificTime(correctedTime.CorrectedUtcNow, + totp, window, out timeStepMatched); + } + + /// + /// Verify a value that has been provided with the calculated value. + /// since: v0.0.0 + /// + /// + /// The timestamp to use. + /// + /// + /// The trial TOTP value. + /// + /// + /// This is an output parameter that gives that time step that was + /// used to find a match. This is usefule in cases where a TOTP value + /// should only be used once. This value is a unique identifier of the + /// time step (not the value) that can be used to prevent the same step + /// from being used multiple times. + /// + /// The window of steps to verify + /// True if there is a match. + public bool VerifyTotp(DateTime timestamp, + string totp, + out long timeStepMatched, VerificationWindow window = null) + { + return this.VerifyTotpForSpecificTime( + this.correctedTime.GetCorrectedTime(timestamp), + totp, window, out timeStepMatched); + } + + /// + /// Remaining seconds in current window based on UtcNow. + /// since: v0.0.0 + /// + /// + /// 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 GetRemainingSeconds() => + RemainingSecondsForSpecificTime(correctedTime.CorrectedUtcNow); + + /// + /// Remaining seconds in current window. + /// since: v0.0.0 + /// + /// The timestamp + /// Number of remaining seconds + public int GetRemainingSeconds(DateTime timestamp) => + RemainingSecondsForSpecificTime(correctedTime.GetCorrectedTime(timestamp)); + /// + /// The Remaining seconds in the current window based on + /// the provided timestamp using value. + /// since: v0.0.0 + /// + private int RemainingSecondsForSpecificTime(DateTime timestamp) + { + return _step - + (int)(((timestamp.Ticks - unixEpochTicks) / ticksToSeconds) % _step); + } + /// + /// Verify a value that has been provided with the calculated value. + /// since: v0.0.0 + /// + private bool VerifyTotpForSpecificTime(DateTime timestamp, + string totp, VerificationWindow window, out long timeStepMatched) + { + var initialStep = CalculateTimeStepFromTimestamp(timestamp); + return this.Verify(initialStep, totp, out timeStepMatched, window); + } + + /// + /// Takes a timestamp and calculates a time step. + /// since: v0.0.0 + /// + private long CalculateTimeStepFromTimestamp(DateTime timestamp) + { + var unixTimestamp = (timestamp.Ticks - unixEpochTicks) / ticksToSeconds; + var window = unixTimestamp / (long)_step; + return window; + } + /// + /// Takes a timestamp and computes a TOTP value for corrected UTC value. + /// since: v0.0.0 + /// + private string ComputeTotpFromSpecificTime(DateTime timestamp) + { + var window = CalculateTimeStepFromTimestamp(timestamp); + return this.Compute(window, _hashMode); + } + + #endregion + //------------------------------------------------- + } +} diff --git a/Socialvoid/Security/Otp/VerificationWindow.cs b/Socialvoid/Security/Otp/VerificationWindow.cs new file mode 100644 index 0000000..9f441d8 --- /dev/null +++ b/Socialvoid/Security/Otp/VerificationWindow.cs @@ -0,0 +1,100 @@ +/* + * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). + * Copyright (c) 2021 Socialvoid.NET Authors. + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code of library. + * If not, see . + */ + +/* + * Credits to Devin Martin and the original OtpSharp library: + * https://github.com/kspearrin/Otp.NET + */ + + +using System.Collections.Generic; + +namespace Socialvoid.Security.Otp +{ + /// + /// A verification window. + /// since: v0.0.0 + /// + public sealed class VerificationWindow + { + //------------------------------------------------- + #region static field's Region + /// + /// The verification window that accomodates network delay that is + /// recommended in the RFC. + /// since: v0.0.0 + /// + public static readonly VerificationWindow RfcSpecifiedNetworkDelay = new(1, 1); + #endregion + //------------------------------------------------- + #region field's Region + /// + /// the previous value in the window. + /// since: v0.0.0 + /// + private readonly int _previous; + /// + /// the future value in the window. + /// since: v0.0.0 + /// + private readonly int _future; + #endregion + //------------------------------------------------- + #region Constructor's Region + /// + /// Creates an instance of a verification window. + /// since: v0.0.0 + /// + /// 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; + } + #endregion + //------------------------------------------------- + #region Get Method's Region + /// + /// Gets an enumberable of all the possible validation candidates. + /// since: v0.0.0 + /// + /// + /// The initial frame to validate. + /// + /// + /// Enumberable of all possible frames that need to be validated. + /// + public IEnumerable ValidationCandidates(long initialFrame) + { + yield return initialFrame; + for(int i = 1; i <= _previous; i++) + { + var val = initialFrame - i; + if(val < 0) + break; + yield return val; + } + + for(int i = 1; i <= _future; i++) + yield return initialFrame + i; + } + #endregion + //------------------------------------------------- + } +} diff --git a/Socialvoid/Socialvoid.csproj b/Socialvoid/Socialvoid.csproj index 530b1a3..bf66576 100644 --- a/Socialvoid/Socialvoid.csproj +++ b/Socialvoid/Socialvoid.csproj @@ -83,18 +83,9 @@ - +