327 lines
11 KiB
C#
327 lines
11 KiB
C#
/*
|
|
* 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
|
|
//-------------------------------------------------
|
|
}
|
|
}
|