Skip to content

Instantly share code, notes, and snippets.

@glenndierckx
Created February 24, 2022 13:48
Show Gist options
  • Save glenndierckx/b6b8a723449de4e86b42b446aa4bcee5 to your computer and use it in GitHub Desktop.
Save glenndierckx/b6b8a723449de4e86b42b446aa4bcee5 to your computer and use it in GitHub Desktop.
SimpleAuthenticator.cs
using System;
using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using System.Text;
namespace SimpleAuthenticator;
public class Authenticator
{
public string GetCode(string secret)
{
using var hash = new HMACSHA1(Base32.FromBase32(secret));
var unixTimestamp = Convert.ToInt64(Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds));
var timeStep = Convert.ToInt64(unixTimestamp / 30);
return Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)timeStep, modifier: null).ToString("000000");
}
// See http://tools.ietf.org/html/rfc3548#section-5
private static class Base32
{
private const string Base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
public static byte[] FromBase32(string input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
input = input.TrimEnd('=').ToUpperInvariant();
if (input.Length == 0)
{
return Array.Empty<byte>();
}
var output = new byte[input.Length * 5 / 8];
var bitIndex = 0;
var inputIndex = 0;
var outputBits = 0;
var outputIndex = 0;
while (outputIndex < output.Length)
{
var byteIndex = Base32Chars.IndexOf(input[inputIndex]);
if (byteIndex < 0)
{
throw new FormatException();
}
var bits = Math.Min(5 - bitIndex, 8 - outputBits);
output[outputIndex] <<= bits;
output[outputIndex] |= (byte)(byteIndex >> (5 - (bitIndex + bits)));
bitIndex += bits;
if (bitIndex >= 5)
{
inputIndex++;
bitIndex = 0;
}
outputBits += bits;
if (outputBits >= 8)
{
outputIndex++;
outputBits = 0;
}
}
return output;
}
// returns the number of bytes that were output
}
private static class Rfc6238AuthenticationService
{
private static readonly Encoding Encoding = new UTF8Encoding(false, true);
internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
{
// # of 0's = length of pin
const int mod = 1000000;
// See https://tools.ietf.org/html/rfc4226
// We can add an optional modifier
var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber));
var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier));
// Generate DT string
var offset = hash[hash.Length - 1] & 0xf;
Debug.Assert(offset + 4 < hash.Length);
var binaryCode = (hash[offset] & 0x7f) << 24
| (hash[offset + 1] & 0xff) << 16
| (hash[offset + 2] & 0xff) << 8
| (hash[offset + 3] & 0xff);
return binaryCode % mod;
}
private static byte[] ApplyModifier(byte[] input, string modifier)
{
if (String.IsNullOrEmpty(modifier))
{
return input;
}
var modifierBytes = Encoding.GetBytes(modifier);
var combined = new byte[checked(input.Length + modifierBytes.Length)];
Buffer.BlockCopy(input, 0, combined, 0, input.Length);
Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length);
return combined;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment