using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace TwoStepsAuthenticator { /// /// Implementation of rfc6238 Time-Based One-Time Password Algorithm /// public class TimeAuthenticator : Authenticator { private static readonly Lazy DefaultUsedCodeManager = new Lazy(() => new UsedCodesManager()); private readonly Func NowFunc; private readonly IUsedCodesManager UsedCodeManager; private readonly int IntervalSeconds; public TimeAuthenticator(IUsedCodesManager usedCodeManager = null, Func nowFunc = null, int intervalSeconds = 30) { this.NowFunc = (nowFunc == null) ? () => DateTime.Now : nowFunc; this.UsedCodeManager = (usedCodeManager == null) ? DefaultUsedCodeManager.Value : usedCodeManager; this.IntervalSeconds = intervalSeconds; } /// /// Generates One-Time Password. /// /// Shared Secret /// OTP public string GetCode(string secret) { return GetCode(secret, NowFunc()); } /// /// Generates One-Time Password. /// /// Shared Secret /// Time to use as challenge /// OTP public string GetCode(string secret, DateTime date) { return GetCodeInternal(secret, (ulong)GetInterval(date)); } /// /// Checks if the passed code is valid. /// /// Shared Secret /// OTP /// The user /// true if code matches public bool CheckCode(string secret, string code, object user) { DateTime successfulTime = DateTime.MinValue; return CheckCode(secret, code, user, out successfulTime); } /// /// Checks if the passed code is valid. /// /// Shared Secret /// OTP /// The user /// Matching time if successful /// true if code matches public bool CheckCode(string secret, string code, object user, out DateTime usedDateTime) { var baseTime = NowFunc(); DateTime successfulTime = DateTime.MinValue; // We need to do this in constant time var codeMatch = false; for (int i = -2; i <= 1; i++) { var checkTime = baseTime.AddSeconds(IntervalSeconds * i); var checkInterval = GetInterval(checkTime); if (ConstantTimeEquals(GetCode(secret, checkTime), code) && (user == null || !UsedCodeManager.IsCodeUsed(checkInterval, code, user))) { codeMatch = true; successfulTime = checkTime; UsedCodeManager.AddCode(checkInterval, code, user); } } usedDateTime = successfulTime; return codeMatch; } /// /// Checks if the passed code is valid. /// /// Shared Secret /// OTP /// true if code matches [Obsolete("The CheckCode method should only be used with a user object")] public bool CheckCode(string secret, string code) { DateTime successfulTime = DateTime.MinValue; return CheckCode(secret, code, null, out successfulTime); } private long GetInterval(DateTime dateTime) { TimeSpan ts = (dateTime.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)); return (long)ts.TotalSeconds / IntervalSeconds; } } }