Add `Socialvoid.Security.Otp` namespace with its classes.
Signed-off-by: Aliwoto <aminnimaj@gmail.com>
This commit is contained in:
parent
739b8baa8a
commit
ca932a74db
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//-------------------------------------------------
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
<!--===================================================-->
|
||||
<!--
|
||||
|
|
Loading…
Reference in New Issue