Add `Socialvoid.Security.Otp` namespace with its classes.

Signed-off-by: Aliwoto <aminnimaj@gmail.com>
This commit is contained in:
Aliwoto 2021-09-30 18:07:36 +00:00
parent 739b8baa8a
commit ca932a74db
No known key found for this signature in database
GPG Key ID: 646B4FE4205EC48C
15 changed files with 1663 additions and 238 deletions

View File

@ -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;
/// <summary>
/// A <see cref="IJsonRpcMessageHandler"/> that sends requests and receives responses over HTTP using <see cref="HttpClient"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public class HttpClientMessageHandler : IJsonRpcMessageHandler
{
#nullable enable
private static readonly ReadOnlyCollection<string> AllowedContentTypes = new ReadOnlyCollection<string>(new string[]
{
"application/json-rpc",
"application/json",
"application/jsonrequest",
});
/// <summary>
/// The Content-Type header to use in requests.
/// </summary>
private static readonly MediaTypeHeaderValue ContentTypeHeader = new MediaTypeHeaderValue(AllowedContentTypes[0]);
/// <summary>
/// The Accept header to use in requests.
/// </summary>
private static readonly MediaTypeWithQualityHeaderValue AcceptHeader = new MediaTypeWithQualityHeaderValue(AllowedContentTypes[0]);
private readonly HttpClient httpClient;
private readonly Uri requestUri;
private readonly AsyncQueue<HttpResponseMessage> incomingMessages = new AsyncQueue<HttpResponseMessage>();
/// <summary>
/// Backing field for the <see cref="TraceSource"/> property.
/// </summary>
private TraceSource traceSource = new TraceSource(nameof(JsonRpc));
/// <summary>
/// Initializes a new instance of the <see cref="HttpClientMessageHandler"/> class
/// with the default <see cref="JsonMessageFormatter"/>.
/// </summary>
/// <param name="httpClient">The <see cref="HttpClient"/> to use for transmitting JSON-RPC requests.</param>
/// <param name="requestUri">The URI to POST to where the entity will be the JSON-RPC message.</param>
public HttpClientMessageHandler(HttpClient httpClient, Uri requestUri)
: this(httpClient, requestUri, new JsonMessageFormatter())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpClientMessageHandler"/> class.
/// </summary>
/// <param name="httpClient">The <see cref="HttpClient"/> to use for transmitting JSON-RPC requests.</param>
/// <param name="requestUri">The URI to POST to where the entity will be the JSON-RPC message.</param>
/// <param name="formatter">The message formatter.</param>
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));
}
/// <summary>
/// Event IDs raised to our <see cref="TraceSource"/>.
/// </summary>
public enum TraceEvent
{
/// <summary>
/// An HTTP response with an error status code was received.
/// </summary>
HttpErrorStatusCodeReceived,
}
/// <summary>
/// Gets or sets the <see cref="System.Diagnostics.TraceSource"/> used to trace details about the HTTP transport operations.
/// </summary>
/// <value>The value can never be null.</value>
/// <exception cref="ArgumentNullException">Thrown by the setter if a null value is provided.</exception>
public TraceSource TraceSource
{
get => this.traceSource;
set
{
Requires.NotNull(value, nameof(value));
this.traceSource = value;
}
}
/// <inheritdoc/>
public bool CanRead => true;
/// <inheritdoc/>
public bool CanWrite => true;
/// <inheritdoc/>
public IJsonRpcMessageFormatter Formatter { get; }
/// <inheritdoc/>
public async ValueTask<JsonRpcMessage?> 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<byte>())
{
#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<byte>.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<byte>.Shared.Return(buffer);
}
#endif
return this.Formatter.Deserialize(sequence);
}
}
/// <inheritdoc/>
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<byte>())
{
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.");
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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
/// <summary>
/// <code> since: v0.0.0 </code>
/// </summary>
protected JsonMessageFormatter _formatter = new(Encoding.UTF8);
/// <summary>
/// the endpoint url of socialvoid servers.
/// <code> since: v0.0.0 </code>
/// </summary>
@ -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<SessionEstablished>(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;
}
/// <summary>
/// AuthenticateUser method (session.authenticate_user),
@ -359,6 +378,14 @@ namespace Socialvoid.Client
Console.WriteLine(contentStr);
}
/// <summary>
/// returns a challenge's answer using the session's challenge secret.
/// <code> since: v0.0.0 </code>
/// </summary>
protected internal virtual string GetChallengeAnswer(string secret)
{
return null;
}
#endregion
//-------------------------------------------------

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
namespace Socialvoid.Client
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* 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
{
/// <summary>
/// Base32 encoding/decoding helper class.
/// <code> since: v0.0.0 </code>
/// </summary>
public static class Base32Encoding
{
//-------------------------------------------------
#region static Method's Region
/// <summary>
/// Converts a string to a byte array using the specified encoding.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <exception cref="ArgumentNullException"/>
/// <exception cref="ArgumentException"/>
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;
}
/// <summary>
/// Converts an array of byte to a Base32-encoded string.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <exception cref="ArgumentNullException"/>
/// <exception cref="ArgumentException"/>
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);
}
/// <summary>
/// Converts a valid base32 character to it's corresponding value.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <exception cref="ArgumentException"/>
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));
}
/// <summary>
/// Converts a valid base32 byte value to its corresponding char.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <exception cref="ArgumentException"/>
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
//-------------------------------------------------
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* Credits to Devin Martin and the original OtpSharp library:
* https://github.com/kspearrin/Otp.NET
*/
using System;
namespace Socialvoid.Security.Otp
{
/// <summary>
/// Calculate HMAC-Based One-Time-Passwords (HOTP) from a secret key.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// The specifications for the methods of this class can be found in RFC 4226:
/// http://tools.ietf.org/html/rfc4226
/// </remarks>
public sealed class Hotp : Otp
{
//-------------------------------------------------
#region field's Region
/// <summary>
/// The HOTP size.
/// <code> since: v0.0.0 </code>
/// </summary>
private readonly int _hotpSize;
#endregion
//-------------------------------------------------
#region Constructor's Region
/// <summary>
/// Creates an HOTP instance.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="secretKey">
/// The secret key to use in HOTP calculations.
/// </param>
/// <param name="mode">
/// The hash mode to use.
/// </param>
/// <param name="hotpSize">The number of digits that the returning HOTP should have. The default is 6.</param>
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;
}
/// <summary>
/// Create a HOTP instance.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="key">
/// The key to use in HOTP calculations.
/// </param>
/// <param name="mode">
/// The hash mode to use.
/// </param>
/// <param name="hotpSize">
/// The number of digits that the returning HOTP should have.
/// The default value is 6.
/// </param>
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
/// <summary>
/// Takes a time step and computes a HOTP code.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="counter">
/// the counter. This is the number of time steps that have passed.
/// </param>
/// <param name="mode">
/// The hash mode to use.
/// </param>
/// <returns>
/// HOTP calculated code.
/// </returns>
protected override string Compute(long counter, OtpHashMode mode)
{
var data = KeyUtilities.GetBigEndianBytes(counter);
var otp = this.CalculateOtp(data, mode);
return Digits(otp, _hotpSize);
}
#endregion
//-------------------------------------------------
#region Get Method's Region
/// <summary>
/// Takes a counter and then computes a HOTP value.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="counter">
/// The timestamp to use for the HOTP calculation.
/// </param>
/// <returns>a HOTP value</returns>
public string ComputeHOTP(long counter)
{
return this.Compute(counter, _hashMode);
}
/// <summary>
/// Verify a value that has been provided with the calculated value.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="hotp">the trial HOTP value.
/// </param>
/// <param name="counter">
/// The counter value to verify
/// </param>
/// <returns>
/// <c>true</c> if there is a match; otherwise <c>false</c>.
/// </returns>
public bool VerifyHotp(string hotp, long counter) =>
hotp == ComputeHOTP(counter);
#endregion
//-------------------------------------------------
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* Credits to Devin Martin and the original OtpSharp library:
* https://github.com/kspearrin/Otp.NET
*/
namespace Socialvoid.Security.Otp
{
/// <summary>
/// Interface used to interact with a key.
/// <code> since: v0.0.0 </code>
/// </summary>
public interface IKeyProvider
{
//-------------------------------------------------
#region Get Method's Region
/// <summary>
/// Uses the key to get an HMAC using the specified algorithm and data.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="mode">
/// The HMAC algorithm to use.
/// </param>
/// <param name="data">
/// The data used to compute the HMAC.
/// </param>
/// <returns>HMAC of the key and data</returns>
byte[] ComputeHmac(OtpHashMode mode, byte[] data);
#endregion
//-------------------------------------------------
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* 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
{
/// <summary>
/// Represents a key in memory.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// 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
/// <c>IsPlatformSupported</c> method, it will just store the key in a standard
/// byte array.
/// </remarks>
public class InMemoryKey : IKeyProvider
{
//-------------------------------------------------
#region field's Region
/// <summary>
/// The key data in memory.
/// <code> since: v0.0.0 </code>
/// </summary>
internal readonly byte[] _KeyData;
/// <summary>
/// The key length representing the length of the <see cref="_KeyData"/>.
/// <code> since: v0.0.0 </code>
/// </summary>
internal readonly int _keyLength;
/// <summary>
/// Used for locking.
/// <code> since: v0.0.0 </code>
/// </summary>
private readonly object _stateSync = new();
#endregion
//-------------------------------------------------
#region Constructor's Region
/// <summary>
/// Creates an instance of a key.
/// </summary>
/// <param name="key">Plaintext key data</param>
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
/// <summary>
/// Gets a copy of the plaintext key.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// This is internal rather than protected so that the tests can
/// use this method.
/// </remarks>
/// <returns>
/// Plaintext Key
/// </returns>
internal byte[] GetCopyOfKey()
{
var plainKey = new byte[_keyLength];
lock(_stateSync)
{
Array.Copy(_KeyData, plainKey, _keyLength);
}
return plainKey;
}
/// <summary>
/// Uses the key to get an HMAC using the specified algorithm and data.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="mode">
/// The HMAC algorithm to use
/// </param>
/// <param name="data">
/// The data used to compute the HMAC
/// </param>
/// <returns>
/// HMAC of the key and data
/// </returns>
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
/// <summary>
/// Create an HMAC object for the specified algorithm.
/// <code> since: v0.0.0 </code>
/// </summary>
private static HMAC CreateHmacHash(OtpHashMode otpHashMode)
{
return otpHashMode switch
{
OtpHashMode.Sha256 => new HMACSHA256(),
OtpHashMode.Sha512 => new HMACSHA512(),
_ => new HMACSHA1() //OtpHashMode.Sha1
};
}
#endregion
//-------------------------------------------------
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* 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
{
/// <summary>
/// Helpers to work with key generations.
/// <code> since: v0.0.0 </code>
/// </summary>
public static class KeyGeneration
{
//-------------------------------------------------
#region static Method's Region
/// <summary>
/// Generates a random key in accordance with the RFC recommened
/// length for each algorithm.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="length">the key length</param>
/// <returns>The generated key</returns>
public static byte[] GenerateRandomKey(int length)
{
byte[] key = new byte[length];
using(var rnd = RandomNumberGenerator.Create())
{
rnd.GetBytes(key);
return key;
}
}
/// <summary>
/// Generates a random key in accordance with the RFC recommened
/// length for each algorithm.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="mode">HashMode</param>
/// <returns>Key</returns>
public static byte[] GenerateRandomKey(OtpHashMode mode = OtpHashMode.Sha1) =>
GenerateRandomKey(LengthForMode(mode));
/// <summary>
/// Uses the procedure defined in RFC 4226 section 7.5 to derive a key
/// from the master key.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="masterKey">
/// The master key from which to derive a device specific key.
/// </param>
/// <param name="publicIdentifier">
/// The public identifier that is unique to the authenticating device.
/// </param>
/// <param name="mode">
/// 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
/// </param>
/// <returns>Derived key</returns>
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);
}
/// <summary>
/// Uses the procedure defined in RFC 4226 section 7.5 to derive a key
/// from the master key.
/// </summary>
/// <param name="masterKey">The master key from which to derive a device specific key</param>
/// <param name="serialNumber">A serial number that is unique to the authenticating device</param>
/// <param name="mode">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</param>
/// <returns>Derived key</returns>
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
//-------------------------------------------------
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* Credits to Devin Martin and the original OtpSharp library:
* https://github.com/kspearrin/Otp.NET
*/
using System;
namespace Socialvoid.Security.Otp
{
/// <summary>
/// Some helper methods to perform common key functions.
/// <code> since: v0.0.0 </code>
/// </summary>
internal static class KeyUtilities
{
//-------------------------------------------------
#region static Method's Region
/// <summary>
/// Overwrite potentially sensitive data with random junk.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// Warning!
///
/// This isn't foolproof by any means.
/// The garbage collector could have moved the actual location in memory
/// to another location during a collection cycle and left the old data
/// in place simply marking it as available.
/// We can't control this or even detect it.
/// This method is simply a good faith effort to limit the exposure of
/// sensitive data in memory as much as possible.
/// </remarks>
internal static void Destroy(byte[] sensitiveData)
{
if(sensitiveData == null || 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);
}
/// <summary>
/// converts a long into a big endian byte array.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// RFC 4226 specifies big endian as the method for converting the counter
/// to data and then to hash.
/// </remarks>
static internal byte[] GetBigEndianBytes(long input)
{
// Since .net uses little endian numbers, we need to reverse the byte order to get big endian.
var data = BitConverter.GetBytes(input);
Array.Reverse(data);
return data;
}
/// <summary>
/// converts an int into a big endian byte array.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// RFC 4226 specifies big endian as the method for converting
/// the counter to data and then to hash.
/// </remarks>
static internal byte[] GetBigEndianBytes(int input)
{
// Since .net uses little endian numbers, we need to reverse the byte order to get big endian.
var data = BitConverter.GetBytes(input);
Array.Reverse(data);
return data;
}
#endregion
//-------------------------------------------------
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* Credits to Devin Martin and the original OtpSharp library:
* https://github.com/kspearrin/Otp.NET
*/
using System;
namespace Socialvoid.Security.Otp
{
/// <summary>
/// An abstract class that contains common OTP calculations.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// https://tools.ietf.org/html/rfc4226
/// </remarks>
public abstract class Otp
{
//-------------------------------------------------
#region field's Region
/// <summary>
/// the secret key.
/// <code> since: v0.0.0 </code>
/// </summary>
protected readonly IKeyProvider _secretKey;
/// <summary>
/// The hash mode to use.
/// <code> since: v0.0.0 </code>
/// </summary>
protected readonly OtpHashMode _hashMode;
#endregion
//-------------------------------------------------
#region Constructor's Region
/// <summary>
/// Constructor for the abstract class using an explicit secret key.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="secretKey">
/// The secret key.
/// </param>
/// <param name="mode">
/// The hash mode to use.
/// </param>
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;
}
/// <summary>
/// Constructor for the abstract class using a generic key provider.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="key"></param>
/// <param name="mode">The hash mode to use</param>
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
/// <summary>
/// An abstract definition of a compute method.
/// Takes a counter and runs it through the derived algorithm.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="counter">Counter or step</param>
/// <param name="mode">The hash mode to use</param>
/// <returns>OTP calculated code</returns>
protected abstract string Compute(long counter, OtpHashMode mode);
/// <summary>
/// Helper method that calculates OTPs.
/// <code> since: v0.0.0 </code>
/// </summary>
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;
}
/// <summary>
/// truncates a number down to the specified number of digits.
/// <code> since: v0.0.0 </code>
/// </summary>
protected internal static string Digits(long input, int digitCount)
{
var truncatedValue = ((int)input % (int)Math.Pow(10, digitCount));
return truncatedValue.ToString().PadLeft(digitCount, '0');
}
/// <summary>
/// Verify an OTP value.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="initialStep">
/// The initial step to try.
/// </param>
/// <param name="valueToVerify">
/// The value to verify
/// </param>
/// <param name="matchedStep">
/// Output parameter that provides the step
/// where the match was found. If no match was found it will be 0
/// </param>
/// <param name="window">
/// The window to verify.
/// </param>
/// <returns>
/// <c>true</c> if a match is found; otherwise <c>false</c>.
/// </returns>
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;
}
/// <summary>
/// Constant time comparison of two values.
/// <code> since: v0.0.0 </code>
/// </summary>
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
//-------------------------------------------------
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* Credits to Devin Martin and the original OtpSharp library:
* https://github.com/kspearrin/Otp.NET
*/
namespace Socialvoid.Security.Otp
{
/// <summary>
/// Indicates which HMAC hashing algorithm should be used.
/// <code> since: v0.0.0 </code>
/// </summary>
public enum OtpHashMode
{
//-------------------------------------------------
#region SHA region
/// <summary>
/// Sha1 is used as the HMAC hashing algorithm.
/// <code> since: v0.0.0 </code>
/// </summary>
Sha1,
/// <summary>
/// Sha256 is used as the HMAC hashing algorithm.
/// <code> since: v0.0.0 </code>
/// </summary>
Sha256,
/// <summary>
/// Sha512 is used as the HMAC hashing algorithm.
/// <code> since: v0.0.0 </code>
/// </summary>
Sha512,
#endregion
//-------------------------------------------------
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* Credits to Devin Martin and the original OtpSharp library:
* https://github.com/kspearrin/Otp.NET
*/
using System;
namespace Socialvoid.Security.Otp
{
/// <summary>
/// Class to apply a correction factor to the system time.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// In cases where the local system time is incorrect it is preferable to simply correct the system time.
/// This class is provided to handle cases where it isn't possible for the client, the server, or both, to be on the correct time.
///
/// This library provides limited facilities to to ping NIST for a correct network time. This class can be used manually however in cases where a server's time is off
/// and the consumer of this library can't control it. In that case create an instance of this class and provide the current server time as the correct time parameter
///
/// This class is immutable and therefore threadsafe.
/// </remarks>
public class TimeCorrection
{
//-------------------------------------------------
#region Properties Region
/// <summary>
/// Applies the correction factor to the current system UTC time and
/// returns a corrected time.
/// <code> since: v0.0.0 </code>
/// </summary>
public DateTime CorrectedUtcNow
{
get => GetCorrectedTime(DateTime.UtcNow);
}
/// <summary>
/// The timespan that is used to calculate a corrected time.
/// <code> since: v0.0.0 </code>
/// </summary>
public TimeSpan CorrectionFactor
{
get => _timeCorrectionFactor;
}
#endregion
//-------------------------------------------------
#region static field's Region
/// <summary>
/// An instance that provides no correction factor.
/// <code> since: v0.0.0 </code>
/// </summary>
public static readonly TimeCorrection UncorrectedInstance = new();
#endregion
//-------------------------------------------------
#region field's Region
/// <summary>
/// The timespan that is used as a correction factor.
/// <code> since: v0.0.0 </code>
/// </summary>
private readonly TimeSpan _timeCorrectionFactor;
#endregion
//-------------------------------------------------
#region Constructor's Region
/// <summary>
/// Constructor used solely for the <see cref="UncorrectedInstance"/> static
/// field to provide an instance without a correction factor.
/// <code> since: v0.0.0 </code>
/// </summary>
private TimeCorrection()
{
_timeCorrectionFactor = TimeSpan.FromSeconds(0);
}
/// <summary>
/// Creates a corrected time object by providing the known correct
/// current UTC time.
/// The current system UTC time will be used as the reference.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// This overload assumes UTC.
/// If a base and reference time other than UTC are required then use the
/// other overlaod.
/// </remarks>
/// <param name="correctUtc">The current correct UTC time</param>
public TimeCorrection(DateTime correctUtc)
{
_timeCorrectionFactor = DateTime.UtcNow - correctUtc;
}
/// <summary>
/// Creates a corrected time object by providing the known correct current time
/// and the current reference time that needs correction.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="correctTime">
/// The current correct time.
/// </param>
/// <param name="referenceTime">
/// The current reference time (time that will have the correction factor
/// applied in subsequent calls).
/// </param>
public TimeCorrection(DateTime correctTime, DateTime referenceTime)
{
_timeCorrectionFactor = referenceTime - correctTime;
}
#endregion
//-------------------------------------------------
#region Get Method's Region
/// <summary>
/// Applies the correction factor to the reference time and returns a
/// corrected time.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="referenceTime">
/// The reference time.
/// </param>
/// <returns>
/// The reference time with the correction factor applied.
/// </returns>
public DateTime GetCorrectedTime(DateTime referenceTime) =>
referenceTime - _timeCorrectionFactor;
#endregion
//-------------------------------------------------
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* Credits to Devin Martin and the original OtpSharp library:
* https://github.com/kspearrin/Otp.NET
*/
using System;
namespace Socialvoid.Security.Otp
{
/// <summary>
/// Calculate Timed-One-Time-Passwords (TOTP) from a secret key.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// The specifications for the methods of this class can be found in RFC 6238:
/// http://tools.ietf.org/html/rfc6238
/// </remarks>
public sealed class Totp : Otp
{
//-------------------------------------------------
#region Constant's Region
/// <summary>
/// The number of ticks as Measured at Midnight Jan 1st 1970.
/// <code> since: v0.0.0 </code>
/// </summary>
internal const long unixEpochTicks = 621355968000000000L;
/// <summary>
/// A divisor for converting ticks to seconds.
/// <code> since: v0.0.0 </code>
/// </summary>
internal const long ticksToSeconds = 10000000L;
#endregion
//-------------------------------------------------
#region field's Region
/// <summary>
/// the step value.
/// <code> since: v0.0.0 </code>
/// </summary>
private readonly int _step;
/// <summary>
/// the TOTP length.
/// <code> since: v0.0.0 </code>
/// </summary>
private readonly int _totpSize;
/// <summary>
/// the TOTP corrected time.
/// <code> since: v0.0.0 </code>
/// </summary>
private readonly TimeCorrection correctedTime;
#endregion
//-------------------------------------------------
#region Constructor's Region
/// <summary>
/// Creates a TOTP instance.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="secretKey">
/// The secret key to use in TOTP calculations
/// </param>
/// <param name="step">
/// The time window step amount to use in calculating time windows.
/// The default is 30 as recommended in the RFC
/// </param>
/// <param name="mode">
/// The hash mode to use
/// </param>
/// <param name="totpSize">
/// The number of digits that the returning TOTP should have.
/// The default value of this argument is 6.
/// </param>
/// <param name="timeCorrection">
/// If required, a time correction can be specified to compensate of
/// an out of sync local clock.
/// </param>
public Totp(byte[] secretKey,
int step = 30,
OtpHashMode mode = OtpHashMode.Sha1,
int totpSize = 6,
TimeCorrection timeCorrection = null)
: base(secretKey, mode)
{
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;
}
/// <summary>
/// Creates a TOTP instance.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="key">
/// The secret key to use in TOTP calculations.
/// </param>
/// <param name="step">
/// The time window step amount to use in calculating time windows.
/// The default is 30 as recommended in the RFC
/// </param>
/// <param name="mode">
/// The hash mode to use.
/// </param>
/// <param name="totpSize">
/// The number of digits that the returning TOTP should have.
/// The default is 6.</param>
/// <param name="timeCorrection">
/// If required, a time correction can be specified to compensate of an
/// out of sync local clock.</param>
public Totp(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
/// <summary>
/// Takes a time step and computes a TOTP code.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="counter">time step</param>
/// <param name="mode">The hash mode to use</param>
/// <returns>TOTP calculated code</returns>
protected override string Compute(long counter, OtpHashMode mode)
{
var data = KeyUtilities.GetBigEndianBytes(counter);
var otp = this.CalculateOtp(data, mode);
return Digits(otp, _totpSize);
}
#endregion
//-------------------------------------------------
#region Get Method's Region
/// <summary>
/// Takes a timestamp and applies correction (if provided) and then computes
/// a TOTP value.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="timestamp">The timestamp to use for the TOTP calculation</param>
/// <returns>a TOTP value</returns>
public string ComputeTotp(DateTime timestamp) =>
ComputeTotpFromSpecificTime(correctedTime.GetCorrectedTime(timestamp));
/// <summary>
/// Takes a timestamp and computes a TOTP value for corrected UTC now.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// It will be corrected against a corrected UTC time using the provided time correction. If none was provided then simply the current UTC will be used.
/// </remarks>
/// <returns>a TOTP value</returns>
public string ComputeTotp() =>
ComputeTotpFromSpecificTime(this.correctedTime.CorrectedUtcNow);
/// <summary>
/// Verify a value that has been provided with the calculated value.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// It will be corrected against a corrected UTC time using the provided time correction. If none was provided then simply the current UTC will be used.
/// </remarks>
/// <param name="totp">
/// the trial TOTP value
/// </param>
/// <param name="timeStepMatched">
/// This is an output parameter that gives that time step that was used to find a match.
/// This is useful in cases where a TOTP value should only be used once. This value is a unique identifier of the
/// time step (not the value) that can be used to prevent the same step from being used multiple times
/// </param>
/// <param name="window">
/// The window of steps to verify.
/// </param>
/// <returns>True if there is a match.</returns>
public bool VerifyTotp(string totp,
out long timeStepMatched,
VerificationWindow window = null)
{
return this.VerifyTotpForSpecificTime(correctedTime.CorrectedUtcNow,
totp, window, out timeStepMatched);
}
/// <summary>
/// Verify a value that has been provided with the calculated value.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="timestamp">
/// The timestamp to use.
/// </param>
/// <param name="totp">
/// The trial TOTP value.
/// </param>
/// <param name="timeStepMatched">
/// This is an output parameter that gives that time step that was
/// used to find a match. This is usefule in cases where a TOTP value
/// should only be used once. This value is a unique identifier of the
/// time step (not the value) that can be used to prevent the same step
/// from being used multiple times.
/// </param>
/// <param name="window">The window of steps to verify</param>
/// <returns>True if there is a match.</returns>
public bool VerifyTotp(DateTime timestamp,
string totp,
out long timeStepMatched, VerificationWindow window = null)
{
return this.VerifyTotpForSpecificTime(
this.correctedTime.GetCorrectedTime(timestamp),
totp, window, out timeStepMatched);
}
/// <summary>
/// Remaining seconds in current window based on UtcNow.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <remarks>
/// It will be corrected against a corrected UTC time using the provided time correction. If none was provided then simply the current UTC will be used.
/// </remarks>
/// <returns>Number of remaining seconds</returns>
public int GetRemainingSeconds() =>
RemainingSecondsForSpecificTime(correctedTime.CorrectedUtcNow);
/// <summary>
/// Remaining seconds in current window.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="timestamp">The timestamp</param>
/// <returns>Number of remaining seconds</returns>
public int GetRemainingSeconds(DateTime timestamp) =>
RemainingSecondsForSpecificTime(correctedTime.GetCorrectedTime(timestamp));
/// <summary>
/// The Remaining seconds in the current window based on
/// the provided timestamp using <see cref="_step"/> value.
/// <code> since: v0.0.0 </code>
/// </summary>
private int RemainingSecondsForSpecificTime(DateTime timestamp)
{
return _step -
(int)(((timestamp.Ticks - unixEpochTicks) / ticksToSeconds) % _step);
}
/// <summary>
/// Verify a value that has been provided with the calculated value.
/// <code> since: v0.0.0 </code>
/// </summary>
private bool VerifyTotpForSpecificTime(DateTime timestamp,
string totp, VerificationWindow window, out long timeStepMatched)
{
var initialStep = CalculateTimeStepFromTimestamp(timestamp);
return this.Verify(initialStep, totp, out timeStepMatched, window);
}
/// <summary>
/// Takes a timestamp and calculates a time step.
/// <code> since: v0.0.0 </code>
/// </summary>
private long CalculateTimeStepFromTimestamp(DateTime timestamp)
{
var unixTimestamp = (timestamp.Ticks - unixEpochTicks) / ticksToSeconds;
var window = unixTimestamp / (long)_step;
return window;
}
/// <summary>
/// Takes a timestamp and computes a TOTP value for corrected UTC value.
/// <code> since: v0.0.0 </code>
/// </summary>
private string ComputeTotpFromSpecificTime(DateTime timestamp)
{
var window = CalculateTimeStepFromTimestamp(timestamp);
return this.Compute(window, _hashMode);
}
#endregion
//-------------------------------------------------
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* Credits to Devin Martin and the original OtpSharp library:
* https://github.com/kspearrin/Otp.NET
*/
using System.Collections.Generic;
namespace Socialvoid.Security.Otp
{
/// <summary>
/// A verification window.
/// <code> since: v0.0.0 </code>
/// </summary>
public sealed class VerificationWindow
{
//-------------------------------------------------
#region static field's Region
/// <summary>
/// The verification window that accomodates network delay that is
/// recommended in the RFC.
/// <code> since: v0.0.0 </code>
/// </summary>
public static readonly VerificationWindow RfcSpecifiedNetworkDelay = new(1, 1);
#endregion
//-------------------------------------------------
#region field's Region
/// <summary>
/// the previous value in the window.
/// <code> since: v0.0.0 </code>
/// </summary>
private readonly int _previous;
/// <summary>
/// the future value in the window.
/// <code> since: v0.0.0 </code>
/// </summary>
private readonly int _future;
#endregion
//-------------------------------------------------
#region Constructor's Region
/// <summary>
/// Creates an instance of a verification window.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="previous">The number of previous frames to accept</param>
/// <param name="future">The number of future frames to accept</param>
public VerificationWindow(int previous = 0, int future = 0)
{
_previous = previous;
_future = future;
}
#endregion
//-------------------------------------------------
#region Get Method's Region
/// <summary>
/// Gets an enumberable of all the possible validation candidates.
/// <code> since: v0.0.0 </code>
/// </summary>
/// <param name="initialFrame">
/// The initial frame to validate.
/// </param>
/// <returns>
/// Enumberable of all possible frames that need to be validated.
/// </returns>
public IEnumerable<long> ValidationCandidates(long initialFrame)
{
yield return initialFrame;
for(int i = 1; i <= _previous; i++)
{
var val = initialFrame - i;
if(val < 0)
break;
yield return val;
}
for(int i = 1; i <= _future; i++)
yield return initialFrame + i;
}
#endregion
//-------------------------------------------------
}
}

View File

@ -83,18 +83,9 @@
<!--
All package references must be added here.
example:
<PackageReference Include="SharpDX" Version="4.2.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="SharpDX.Direct3D9" Version="4.2.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="SharpDX.DXGI" Version="4.2.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="SharpDX.MediaFoundation" Version="4.2.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="SharpDX.XAudio2" Version="4.2.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="TextCopy" Version="4.3.1" />
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
<PackageReference Include="FontStashSharp.MonoGame" Version="0.9.5" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
<PackageReference Include="StreamJsonRpc" Version="2.8.21" />
-->
<PackageReference Include="StreamJsonRpc" Version="2.8.21" />
</ItemGroup>
<!--===================================================-->
<!--