Created
February 24, 2022 13:48
-
-
Save glenndierckx/b6b8a723449de4e86b42b446aa4bcee5 to your computer and use it in GitHub Desktop.
SimpleAuthenticator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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