C# OTP Implementation with TOTP and HOTP
/* | |
* Copyright (C) 2016 BravoTango86 | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
using System; | |
using System.Collections.Generic; | |
using System.Security.Cryptography; | |
using System.Text; | |
using static OtpAuthenticator; | |
public class OtpAuthenticator : IDisposable { | |
public ICachingProvider CachingProvider { get; set; } | |
public int CodeLength { get; set; } = 6; | |
public long HotpCounter { get; set; } = 0; | |
public int TotpInterval { get; set; } = 30; | |
public long TotpTimeOffsetSeconds { get; set; } = 0; | |
private OtpType Type; | |
private HMAC HMAC; | |
public OtpAuthenticator(OtpType type, OtpAlgorithm algorithm = OtpAlgorithm.SHA1, byte[] key = null) { | |
HMAC = GetHMAC(algorithm, key ?? GenerateKey(algorithm)); | |
Type = type; | |
} | |
public bool VerifyOtp(string code, byte[] challenge = null, int forward = 1, int back = 1) { | |
if (string.IsNullOrEmpty(code) || code.Length != CodeLength || (CachingProvider != null && !CachingProvider.ValidateToken(code))) return false; | |
long State = GetState(-back); | |
List<string> Tried = new List<string>(); | |
for (int I = 0; I <= (forward + back); I++) { | |
string Code = GetOtp(state: State + I, challenge: challenge); | |
Tried.Add(code); | |
if (Code == code) { | |
if (CachingProvider != null) CachingProvider.CancelTokens(Type, Tried); | |
if (Type == OtpType.HOTP) HotpCounter = State + I + 1; | |
return true; | |
} | |
} | |
return false; | |
} | |
private long GetState(int offset) { | |
return (Type == OtpType.HOTP ? HotpCounter : (DateTimeOffset.UtcNow.ToUnixTimeSeconds() + TotpTimeOffsetSeconds) / TotpInterval) + offset; | |
} | |
public string GetOtp(int offset = 0, byte[] challenge = null) { | |
return GetOtp(GetState(offset), challenge); | |
} | |
private string GetOtp(long state, byte[] challenge = null) { | |
byte[] Input = BitConverter.GetBytes(state); | |
if (BitConverter.IsLittleEndian) Array.Reverse(Input); | |
if (challenge != null) { | |
Array.Resize(ref Input, Input.Length + challenge.Length); | |
Buffer.BlockCopy(challenge, 0, Input, 8, challenge.Length); | |
} | |
byte[] Hash = HMAC.ComputeHash(Input); | |
int offset = Hash[Hash.Length - 1] & 0xf; | |
int binary = ((Hash[offset] & 0x7f) << 24) | ((Hash[offset + 1] & 0xff) << 16) | | |
((Hash[offset + 2] & 0xff) << 8) | (Hash[offset + 3] & 0xff); | |
return (binary % (int)Math.Pow(10, CodeLength)).ToString(new string('0', CodeLength)); | |
} | |
public string GetIntegrityValue() { | |
return GetOtp(0); | |
} | |
public static string GetUri(OtpType type, byte[] key, string accountName, string issuer = "", OtpAlgorithm algorithm = OtpAlgorithm.SHA1, | |
int codeLength = 6, long counter = 0, int period = 30) { | |
StringBuilder SB = new StringBuilder(); | |
SB.AppendFormat("otpauth://{0}/", type.ToString().ToLower()); | |
if (!string.IsNullOrEmpty(issuer)) SB.AppendFormat("{0}:{1}?issuer={0}&", Uri.EscapeUriString(issuer), Uri.EscapeUriString(accountName)); | |
else SB.AppendFormat("{0}?", Uri.EscapeUriString(accountName)); | |
SB.AppendFormat("secret={0}&algorithm={1}&digits={2}&", Base32.Encode(key), algorithm, codeLength); | |
if (type == OtpType.HOTP) SB.AppendFormat("counter={0}", counter); | |
else SB.AppendFormat("period={0}", period); | |
return SB.ToString(); | |
} | |
public string GetUri(string accountName, string issuer = "") { | |
return GetUri(Type, HMAC.Key, accountName, issuer = "", (OtpAlgorithm)Enum.Parse(typeof(OtpAlgorithm), HMAC.HashName), | |
CodeLength, HotpCounter, TotpInterval); | |
} | |
public override string ToString() { | |
return GetUri("OtpGenerator"); | |
} | |
public static byte[] GenerateKey(OtpAlgorithm algorithm) { | |
return GenerateKey(GetHashLength(algorithm)); | |
} | |
public static byte[] GenerateKey(int length) { | |
using (RandomNumberGenerator RNG = RandomNumberGenerator.Create()) { | |
byte[] Output = new byte[length]; | |
RNG.GetBytes(Output); | |
return Output; | |
} | |
} | |
public void Dispose() { | |
HMAC.Dispose(); | |
} | |
private static HMAC GetHMAC(OtpAlgorithm algorithm, byte[] key) { | |
switch (algorithm) { | |
case OtpAlgorithm.MD5: return new HMACMD5(key); | |
case OtpAlgorithm.SHA1: return new HMACSHA1(key); | |
case OtpAlgorithm.SHA256: return new HMACSHA256(key); | |
case OtpAlgorithm.SHA512: return new HMACSHA512(key); | |
} | |
throw new InvalidOperationException(); | |
} | |
private static int GetHashLength(OtpAlgorithm algorithm) { | |
switch (algorithm) { | |
case OtpAlgorithm.MD5: return 32; | |
case OtpAlgorithm.SHA1: return 20; | |
case OtpAlgorithm.SHA256: return 32; | |
case OtpAlgorithm.SHA512: return 64; | |
} | |
throw new InvalidOperationException(); | |
} | |
} | |
public enum OtpType { | |
HOTP = 0, | |
TOTP = 1 | |
} | |
public enum OtpAlgorithm { | |
MD5 = 10, | |
SHA1 = 1, | |
SHA256 = 2, | |
SHA512 = 3 | |
} | |
public interface ICachingProvider { | |
void CancelToken(OtpType type, string token); | |
void CancelTokens(OtpType type, IEnumerable<string> tokens); | |
bool ValidateToken(string token); | |
} | |
public class LocalCachingProvider : ICachingProvider { | |
private List<string> Used = new List<string>(); | |
public void CancelToken(OtpType type, string token) { | |
Used.Add(token); | |
} | |
public void CancelTokens(OtpType type, IEnumerable<string> tokens) { | |
Used.AddRange(tokens); | |
} | |
public bool ValidateToken(string token) { | |
return !Used.Contains(token); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment