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;
|
* This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET).
|
||||||
using System.Text;
|
* 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.Net.Http;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using StreamJsonRpc;
|
|
||||||
using StreamJsonRpc.Protocol;
|
|
||||||
using Socialvoid.Security;
|
using Socialvoid.Security;
|
||||||
using Socialvoid.JObjects;
|
using Socialvoid.JObjects;
|
||||||
using Socialvoid.Errors.ServerErrors;
|
using Socialvoid.Errors.ServerErrors;
|
||||||
|
@ -128,10 +141,6 @@ namespace Socialvoid.Client
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
#region field's Region
|
#region field's Region
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <code> since: v0.0.0 </code>
|
|
||||||
/// </summary>
|
|
||||||
protected JsonMessageFormatter _formatter = new(Encoding.UTF8);
|
|
||||||
/// <summary>
|
|
||||||
/// the endpoint url of socialvoid servers.
|
/// the endpoint url of socialvoid servers.
|
||||||
/// <code> since: v0.0.0 </code>
|
/// <code> since: v0.0.0 </code>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -256,7 +265,7 @@ namespace Socialvoid.Client
|
||||||
{PublicHashKey, PublicHash},
|
{PublicHashKey, PublicHash},
|
||||||
{PrivateHashKey, PrivateHash},
|
{PrivateHashKey, PrivateHash},
|
||||||
{PlatformKey, Platform},
|
{PlatformKey, Platform},
|
||||||
//{NameKey, ClientName},
|
{NameKey, ClientName},
|
||||||
{VersionKey, Version},
|
{VersionKey, Version},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -267,8 +276,18 @@ namespace Socialvoid.Client
|
||||||
message.Content = SerializeContent(request);
|
message.Content = SerializeContent(request);
|
||||||
message.Content.Headers.ContentType = _contentTypeValue;
|
message.Content.Headers.ContentType = _contentTypeValue;
|
||||||
var jresp = ParseContent<SessionEstablished>(message);
|
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>
|
/// <summary>
|
||||||
/// AuthenticateUser method (session.authenticate_user),
|
/// AuthenticateUser method (session.authenticate_user),
|
||||||
|
@ -359,6 +378,14 @@ namespace Socialvoid.Client
|
||||||
Console.WriteLine(contentStr);
|
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
|
#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
|
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.
|
All package references must be added here.
|
||||||
example:
|
example:
|
||||||
<PackageReference Include="SharpDX" Version="4.2.0" Condition="'$(OS)' == 'Windows_NT'" />
|
<PackageReference Include="StreamJsonRpc" Version="2.8.21" />
|
||||||
<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" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!--===================================================-->
|
<!--===================================================-->
|
||||||
<!--
|
<!--
|
||||||
|
|
Loading…
Reference in New Issue