Time-based One Time Password

 

A nice easy-to-use RFC-6238 based algorithm, that provides a HMAC-based One-Time Password authentication. The algorithm is based on one outlined in this article here.

The interval is in seconds. It dictates how long before a TOTP expires. For example, if a TOTP is generated at 14:20 and the interval is 120 seconds. The TOTP will expire at 14:22.

The process in which this is implemented is quite simple:

  1. A Shared Secret Key is generated by the host (line 7).
  2. The Shared Secret Key, along with a predetermined interval is shared with the client (line 8).
  3. The client then uses the Shared Secret Key to generate a TOTP each time they need to authenticate (line 13). This will be passed to the host system that requires authentication.
  4. On the request the host will need to generate a TOTP using the same Shared Secret Key. This is then compared to the one provided by the client (line 17).
  5. If they match, the authentication was successful (line 18).

The code below can be copied and pasted into LinqPad as a C# program.


void Main()
{
	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// This information only needs to be generated and distributed once
	const int interval = 240;
	string sharedSecretKey = TotpHelper.GenerateRandomSharedSecret();
	Console.WriteLine("The Shared Secret Key is: {0}", sharedSecretKey);
	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	
	
	// Client - Generates a TOTP
	string totpClient = TotpHelper.GenerateTotp(interval, sharedSecretKey);
	Console.WriteLine("The TOTP is: {0}", totpClient);
	
	// Host - Generates and checks TOTP
	string totpHost = TotpHelper.GenerateTotp(interval, sharedSecretKey);
	Console.WriteLine("The client TOTP matches the Host TOTP: {0}", totpClient == totpHost);
}


// using System.Security.Cryptography
// using System.Globalization
public static class TotpHelper
{
	/// <summary>
	/// Generates a Shared Secret Key that can be used to generate a TOTP. 
	/// This is normally used ONCE by the host to generate a key for the client that can be used over and over again.
	/// </summary>
	/// <returns>Returns an 32-digit long hexadecimal number</returns>
	public static string GenerateRandomSharedSecret()
	{
		var rng = new RNGCryptoServiceProvider(); // Less pseudo-random than Random()!
		string hexKey = string.Join(string.Empty, Enumerable.Range(0, 16)
			.Select(i =>
			{
				var data = new byte[4];
				rng.GetBytes(data);
				int generatedValue = Math.Abs(BitConverter.ToInt32(data, startIndex: 0));
				int rndNum = (generatedValue % 127);
				return PrependZeros(rndNum.ToString("X"), 2);
			})
			.ToArray());

		return hexKey;
	}

	/// <summary>
	/// Generates the Time-Based One-Time Password. This is based on the RFC-6238 algorithm.
	/// </summary>
	/// <param name="intervalSeconds">The length of time in seconds the password will be valid</param>
	/// <param name="sharedSecretKey">The key shared between parties using the algorithm. It is a 32 char long hex string.</param>
	/// <returns>Returns an 8-digit long number string. e.g. 12345678</returns>
	public static string GenerateTotp(int intervalSeconds, string sharedSecretKey)
	{
		if (IsValidSharedKey(sharedSecretKey) == false)
		{
			throw new ArgumentException("Invalid Shared Secret Key");
		}

		double epochSeconds = GetEpochSeconds();
		double stepTime = Math.Truncate(epochSeconds / intervalSeconds);
		string stepTimeHex = DoubleToHexString(stepTime, 16);

		stepTimeHex = PrependZeros(stepTimeHex, 16);

		byte[] stepB = ConvertHexStringToByteArray(stepTimeHex);
		byte[] keyB = ConvertHexStringToByteArray(sharedSecretKey);

		HMACSHA1 shaHasher = new HMACSHA1(keyB);
		byte[] hashKeyB = shaHasher.ComputeHash(stepB);

		int offset = hashKeyB[hashKeyB.Length - 1] & 15;

		int binary =
			((hashKeyB[offset] & 127) << 24) |
			((hashKeyB[offset + 1] & 255) << 16) |
			((hashKeyB[offset + 2] & 255) << 8) |
			(hashKeyB[offset + 3] & 255);

		int otp = binary % 100000000;
		string result = PrependZeros(Convert.ToString(otp), 8);
		return result;
	}

	/// <summary>
	/// Checks whether the Shared Secret Key is valid.
	/// </summary>
	/// <param name="value">The key must be a 32 character long hexadecimal string</param>
	/// <returns>IsValid = true / IsNotValid = false</returns>
	public static bool IsValidSharedKey(this string value)
	{
		return !String.IsNullOrWhiteSpace(value)
			   && Regex.IsMatch(value, @"[0-9a-fA-F]{32}");
	}

	private static double GetEpochSeconds()
	{
		DateTime startDate = new DateTime(1970, 1, 1, 0, 0, 0);
		TimeSpan span = DateTime.UtcNow - startDate;
		return span.TotalSeconds;
	}

	private static byte[] ConvertHexStringToByteArray(string hexString)
	{
		return Enumerable
			.Range(0, hexString.Length / 2)
			.Select(i => Convert.ToByte(hexString.Substring(i * 2, 2), 16))
			.ToArray();
	}

	private static string PrependZeros(string value, int reqLen)
	{
		string zeros = String.Join(String.Empty
			, Enumerable.Range(0, Math.Abs(reqLen - value.Length)).Select(i => "0"));

		return zeros + value;
	}

	private static string DoubleToHexString(double value, int maxDecimals)
	{
		if (value == 0 || double.IsInfinity(value) || double.IsNaN(value))
			return value.ToString(CultureInfo.InvariantCulture);

		var hex = new StringBuilder();
		long bytes = BitConverter.ToInt64(BitConverter.GetBytes(value), 0);

		bool negative = bytes < 0;
		bytes &= long.MaxValue;

		int exp = ((int)(bytes >> 52) & 2047) - 1023;
		bytes = (bytes & 0xFFFFFFFFFFFFF) | 0x10000000000000;

		if (exp < 0)
		{
			exp = -exp - 1;
			hex.Append("0.").Append('0', exp / 4);
			bytes <<= 3 - exp % 4;
			hex.Append(bytes.ToString("X").TrimEnd('0'));
		}
		else
		{
			bytes <<= (exp % 4);
			exp &= ~3;
			hex.Append(bytes.ToString("X"));

			if (exp >= 52)
				hex.Append('0', (exp - 52) / 4);
			else
			{
				hex.Insert(exp / 4 + 1, '.');
				hex = new StringBuilder(hex.ToString().TrimEnd('0').TrimEnd('.'));
			}
		}

		if (negative) hex.Insert(0, '-');
		return hex.ToString();
	}
}

 

Post Categories