1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
| public class Totp { private Byte[] secret; private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private TimeSpan defaultClockDriftTolerance { get; set; } = TimeSpan.FromMinutes(1);
public Totp(String secret, Boolean isBase32 = true) => this.secret = ConvertSecretToBytes(secret, isBase32); public static String GenerateProvisionUrl( String account, Byte[] secret, String issuer) { if (String.IsNullOrWhiteSpace(account)) { throw new ArgumentNullException(nameof(account)); } var UrlEncode = (String url) => Uri.EscapeDataString(url); var RemoveWhiteSpace = (String str) => new String(str.Where(c => !Char.IsWhiteSpace(c)).ToArray());
account = RemoveWhiteSpace(UrlEncode(account)); if (!String.IsNullOrEmpty(issuer)) { issuer = RemoveWhiteSpace(UrlEncode(issuer)); } var encodedSecret = Base32Encoding.ToString(secret).Trim('=');
return String.IsNullOrWhiteSpace(issuer) ? $"otpauth://totp/{account}?secret={encodedSecret}" : $"otpauth://totp/{issuer}:{account}?secret={encodedSecret}&issuer={issuer}";
}
public Boolean ValidatePIN(String pin) => ValidatePIN(pin, defaultClockDriftTolerance);
public Boolean ValidatePIN( String pin, TimeSpan timeTolerance) { return GetCurrentPINs(timeTolerance).Any(c => c == pin); }
public String GetCurrentPIN() => GeneratePINAtInterval(GetCurrentCounter());
public String GetCurrentPIN(DateTime now) => GeneratePINAtInterval(GetCurrentCounter(now, epoch, 30));
private String[] GetCurrentPINs( TimeSpan timeTolerance) { var codes = new List<String>(); var iterationCounter = GetCurrentCounter(); var iterationOffset = 0;
if (timeTolerance.TotalSeconds > 30) { iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / 30.00); }
var iterationStart = iterationCounter - iterationOffset; var iterationEnd = iterationCounter + iterationOffset;
for (var counter = iterationStart; counter <= iterationEnd; counter++) { codes.Add(GeneratePINAtInterval(counter)); }
return codes.ToArray(); }
private static Byte[] ConvertSecretToBytes( String secret, Boolean secretIsBase32) => secretIsBase32 ? Base32Encoding.ToBytes(secret) : Encoding.UTF8.GetBytes(secret);
private String GeneratePINAtInterval(Int64 counter, Int32 digits = 6) => GenerateHashedCode(counter, digits);
private String GenerateHashedCode(Int64 iterationNumber, Int32 digits = 6) { var counter = BitConverter.GetBytes(iterationNumber);
if (BitConverter.IsLittleEndian) Array.Reverse(counter);
var hmac = new HMACSHA1(this.secret); var hash = hmac.ComputeHash(counter); var offset = hash[hash.Length - 1] & 0xf;
var binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8) | ((hash[offset + 3] & 0xff));
var password = binary % (Int32)Math.Pow(10, digits); return password.ToString(new String('0', digits)); }
private Int64 GetCurrentCounter() => GetCurrentCounter(DateTime.UtcNow, epoch, 30);
private Int64 GetCurrentCounter(DateTime now, DateTime epoch, Int32 timeStep) => (Int64)(now - epoch).TotalSeconds / timeStep; }
|