/* * This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET). * Copyright (c) 2021 Socialvoid.NET Authors. * * This library is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this source code of library. * If not, see . */ /* * Credits to Devin Martin and the original OtpSharp library: * https://github.com/kspearrin/Otp.NET */ using System; namespace Socialvoid.Security.Otp { /// /// Calculate Timed-One-Time-Passwords (TOTP) from a secret key. /// since: v0.0.0 /// /// /// The specifications for the methods of this class can be found in RFC 6238: /// http://tools.ietf.org/html/rfc6238 /// public sealed class Totp : Otp { //------------------------------------------------- #region Constant's Region /// /// The number of ticks as Measured at Midnight Jan 1st 1970. /// since: v0.0.0 /// internal const long unixEpochTicks = 621355968000000000L; /// /// A divisor for converting ticks to seconds. /// since: v0.0.0 /// internal const long ticksToSeconds = 10000000L; #endregion //------------------------------------------------- #region field's Region /// /// the step value. /// since: v0.0.0 /// private readonly int _step; /// /// the TOTP length. /// since: v0.0.0 /// private readonly int _totpSize; /// /// the TOTP corrected time. /// since: v0.0.0 /// private readonly TimeCorrection correctedTime; #endregion //------------------------------------------------- #region Constructor's Region /// /// Creates a TOTP instance. /// since: v0.0.0 /// /// /// The secret key to use in TOTP calculations /// /// /// The time window step amount to use in calculating time windows. /// The default is 30 as recommended in the RFC /// /// /// The hash mode to use /// /// /// The number of digits that the returning TOTP should have. /// The default value of this argument is 6. /// /// /// If required, a time correction can be specified to compensate of /// an out of sync local clock. /// public Totp(byte[] secretKey, int step = 30, OtpHashMode mode = OtpHashMode.Sha1, int totpSize = 6, TimeCorrection timeCorrection = null) : base(secretKey, mode) { if (step < 0) { throw new ArgumentOutOfRangeException(nameof(step), "Step must be greater than 0"); } if (totpSize < 0 || totpSize > 10) { throw new ArgumentOutOfRangeException(nameof(totpSize), "TOTP size must be greater than 0 and less than 10"); } _step = step; _totpSize = totpSize; // we never null check the corrected time object. // Since it's readonly, we'll ensure that it isn't null here // and provide neatral functionality in this case. correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance; } /// /// Creates a TOTP instance. /// since: v0.0.0 /// /// /// The secret key to use in TOTP calculations. /// /// /// The time window step amount to use in calculating time windows. /// The default is 30 as recommended in the RFC /// /// /// The hash mode to use. /// /// /// The number of digits that the returning TOTP should have. /// The default is 6. /// /// If required, a time correction can be specified to compensate of an /// out of sync local clock. public Totp(IKeyProvider key, int step = 30, OtpHashMode mode = OtpHashMode.Sha1, int totpSize = 6, TimeCorrection timeCorrection = null) : base(key, mode) { if (step < 0) { throw new ArgumentOutOfRangeException(nameof(step), "Step must be greater than 0"); } if (totpSize < 0 || totpSize > 10) { throw new ArgumentOutOfRangeException(nameof(totpSize), "TOTP size must be greater than 0 and less than 10"); } _step = step; _totpSize = totpSize; // we never null check the corrected time object. // Since it's readonly, we'll ensure that it isn't null here and // provide neatral functionality in this case. correctedTime = timeCorrection ?? TimeCorrection.UncorrectedInstance; } #endregion //------------------------------------------------- #region overrided Method's Region /// /// Takes a time step and computes a TOTP code. /// since: v0.0.0 /// /// time step /// The hash mode to use /// TOTP calculated code protected override string Compute(long counter, OtpHashMode mode) { var data = KeyUtilities.GetBigEndianBytes(counter); var otp = this.CalculateOtp(data, mode); return Digits(otp, _totpSize); } #endregion //------------------------------------------------- #region Get Method's Region /// /// Takes a timestamp and applies correction (if provided) and then computes /// a TOTP value. /// since: v0.0.0 /// /// The timestamp to use for the TOTP calculation /// a TOTP value public string ComputeTotp(DateTime timestamp) => ComputeTotpFromSpecificTime(correctedTime.GetCorrectedTime(timestamp)); /// /// Takes a timestamp and computes a TOTP value for corrected UTC now. /// since: v0.0.0 /// /// /// It will be corrected against a corrected UTC time using the provided time correction. If none was provided then simply the current UTC will be used. /// /// a TOTP value public string ComputeTotp() => ComputeTotpFromSpecificTime(this.correctedTime.CorrectedUtcNow); /// /// Verify a value that has been provided with the calculated value. /// since: v0.0.0 /// /// /// It will be corrected against a corrected UTC time using the provided time correction. If none was provided then simply the current UTC will be used. /// /// /// the trial TOTP value /// /// /// This is an output parameter that gives that time step that was used to find a match. /// This is useful in cases where a TOTP value should only be used once. This value is a unique identifier of the /// time step (not the value) that can be used to prevent the same step from being used multiple times /// /// /// The window of steps to verify. /// /// True if there is a match. public bool VerifyTotp(string totp, out long timeStepMatched, VerificationWindow window = null) { return this.VerifyTotpForSpecificTime(correctedTime.CorrectedUtcNow, totp, window, out timeStepMatched); } /// /// Verify a value that has been provided with the calculated value. /// since: v0.0.0 /// /// /// The timestamp to use. /// /// /// The trial TOTP value. /// /// /// This is an output parameter that gives that time step that was /// used to find a match. This is usefule in cases where a TOTP value /// should only be used once. This value is a unique identifier of the /// time step (not the value) that can be used to prevent the same step /// from being used multiple times. /// /// The window of steps to verify /// True if there is a match. public bool VerifyTotp(DateTime timestamp, string totp, out long timeStepMatched, VerificationWindow window = null) { return this.VerifyTotpForSpecificTime( this.correctedTime.GetCorrectedTime(timestamp), totp, window, out timeStepMatched); } /// /// Remaining seconds in current window based on UtcNow. /// since: v0.0.0 /// /// /// It will be corrected against a corrected UTC time using the provided time correction. If none was provided then simply the current UTC will be used. /// /// Number of remaining seconds public int GetRemainingSeconds() => RemainingSecondsForSpecificTime(correctedTime.CorrectedUtcNow); /// /// Remaining seconds in current window. /// since: v0.0.0 /// /// The timestamp /// Number of remaining seconds public int GetRemainingSeconds(DateTime timestamp) => RemainingSecondsForSpecificTime(correctedTime.GetCorrectedTime(timestamp)); /// /// The Remaining seconds in the current window based on /// the provided timestamp using value. /// since: v0.0.0 /// private int RemainingSecondsForSpecificTime(DateTime timestamp) { return _step - (int)(((timestamp.Ticks - unixEpochTicks) / ticksToSeconds) % _step); } /// /// Verify a value that has been provided with the calculated value. /// since: v0.0.0 /// private bool VerifyTotpForSpecificTime(DateTime timestamp, string totp, VerificationWindow window, out long timeStepMatched) { var initialStep = CalculateTimeStepFromTimestamp(timestamp); return this.Verify(initialStep, totp, out timeStepMatched, window); } /// /// Takes a timestamp and calculates a time step. /// since: v0.0.0 /// private long CalculateTimeStepFromTimestamp(DateTime timestamp) { var unixTimestamp = (timestamp.Ticks - unixEpochTicks) / ticksToSeconds; var window = unixTimestamp / (long)_step; return window; } /// /// Takes a timestamp and computes a TOTP value for corrected UTC value. /// since: v0.0.0 /// private string ComputeTotpFromSpecificTime(DateTime timestamp) { var window = CalculateTimeStepFromTimestamp(timestamp); return this.Compute(window, _hashMode); } #endregion //------------------------------------------------- } }