Skip to content

Instantly share code, notes, and snippets.

@rjmholt
Last active July 17, 2018 22:47
Show Gist options
  • Save rjmholt/2d931a712d9b12443f1e3a1db57d088e to your computer and use it in GitHub Desktop.
Save rjmholt/2d931a712d9b12443f1e3a1db57d088e to your computer and use it in GitHub Desktop.
C# snippet for doing HMAC-based authentication
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
public class AuthenticationValidator
{
/// <summary>
/// A fast byte array comparer for tracking seen nonces.
/// </summary>
private static readonly IEqualityComparer<byte[]> s_fastComparer = new FastByteArrayComparer();
/// <summary>
/// A slow byte array comparer for safely comparing HMAC hashes.
/// </summary>
private static readonly IEqualityComparer<byte[]> s_slowComparer = new SlowByteArrayComparer();
private HashAlgorithmName _hashAlgorithmName;
private byte[] _secretKey;
private readonly HashSet<byte[]> _usedNonces;
public AuthenticationValidator(HashAlgorithmName hashAlgorithm, byte[] secretKey)
{
_hashAlgorithmName = hashAlgorithm;
_secretKey = new byte[secretKey.Length];
secretKey.CopyTo(_secretKey, 0);
}
public byte[] IssueChallenge(int size)
{
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
byte[] challenge = new byte[size];
rng.GetBytes(challenge);
return challenge;
}
}
/// <summary>
/// Validate a message in a given encoding against its accompanying authorization header.
/// </summary>
public bool ValidateAuthentication(byte[] receivedHmacHash, params byte[][] msgParts)
{
using (HMAC hmac = HMAC.Create(_hashAlgorithmName.Name))
{
hmac.Key = _secretKey;
byte[] computedHash = hmac.ComputeHash(ConcatBytes(msgParts));
return s_slowComparer.Equals(receivedHmacHash, computedHash);
}
}
private static byte[] ConcatBytes(IEnumerable<byte[]> byteArrs)
{
int totalSize = 0;
foreach (byte[] arr in byteArrs)
{
totalSize += arr.Length;
}
byte[] destArr = new byte[totalSize];
int insertIdx = 0;
foreach (byte[] arr in byteArrs)
{
arr.CopyTo(destArr, insertIdx);
insertIdx += arr.Length;
}
return destArr;
}
}
/// <summary>
/// A fast-failing equality comparer for byte arrays.
/// </summary>
public class FastByteArrayComparer : IEqualityComparer<byte[]>
{
/// <summary>
/// Perform a fast equality comparison of two byte arrays.
/// </summary>
/// <param name="x">First array to compare.</param>
/// <param name="y">Second array to compare.</param>
/// <returns>True if the two arrays are equal, false otherwise.</returns>
public bool Equals(byte[] x, byte[] y)
{
if (x == y)
{
return true;
}
if (x == null || y == null)
{
return false;
}
if (x.Length != y.Length)
{
return false;
}
for (int i = 0; i < x.Length; i++)
{
if (x[i] != y[i])
{
return false;
}
}
return true;
}
/// <summary>
/// Get the hash code of an array of bytes.
/// </summary>
/// <param name="array">The array of bytes to hash.</param>
/// <returns>A hash code that is always the same for a byte array of the same values.</returns>
public int GetHashCode(byte[] array)
{
// Adapted from Jon Skeet's answer at
// https://stackoverflow.com/a/7244729
unchecked
{
if (array == null)
{
return 0;
}
int hash = 17;
foreach (byte b in array)
{
hash = hash * 31 + b.GetHashCode();
}
return hash;
}
}
}
/// <summary>
/// A byte array comparer intended to make timing attacks difficult.
/// </summary>
public class SlowByteArrayComparer : IEqualityComparer<byte[]>
{
/// <summary>
/// A fast array comparer to use for hash codes.
/// </summary>
private static readonly FastByteArrayComparer s_fastComparer = new FastByteArrayComparer();
/// <summary>
/// Compare two arrays for equality, with the number of operations depending
/// on the length of the longest array only (and not on the number of equal elements).
/// </summary>
/// <param name="x">First array to compare.</param>
/// <param name="y">Second array to compare.</param>
/// <returns>True if the arrays are equal, false otherwise.</returns>
public bool Equals(byte[] x, byte[] y)
{
// We want to do an operation for every element in the longer array
byte[] longerHash;
byte[] shorterHash;
if (x.Length >= y.Length)
{
longerHash = x;
shorterHash = y;
}
else
{
longerHash = y;
shorterHash = x;
}
// Now roll down the arrays and flip the accumulator
// bit if two array elements differ or if the shorter
// one runs out of elements
int rollingXor = 0;
for (int i = 0; i < longerHash.Length; i++)
{
if (i >= shorterHash.Length)
{
rollingXor |= 1;
continue;
}
rollingXor |= longerHash[i] ^ shorterHash[i];
}
// The arrays are equal iff the bit is not flipped
return rollingXor == 0;
}
/// <summary>
/// Get the hash code of a byte array using the fast comparer's method.
/// </summary>
/// <param name="array">A byte array to get the hash code of.</param>
/// <returns>A hash code that is the same for all byte arrays with the same values in the same order.</returns>
public int GetHashCode(byte[] array)
{
return s_fastComparer.GetHashCode(array);
}
}
/// <summary>
/// Exception to represent errors that occur with the authentication process
/// </summary>
public class AuthenticationException : Exception
{
/// <summary>
/// Create an authentication exception with just a message.
/// </summary>
/// <param name="message">A message describing the circumstances of the authentication exception.</param>
public AuthenticationException(string message)
: base(message)
{
}
/// <summary>
/// Create an authentication with a message after a lower level exception has occurred.
/// </summary>
/// <param name="message">A message describing the circumstances of the authentication exception.</param>
/// <param name="innerException">The lower level exception that lead to the authentication exception being thrown.</param>
public AuthenticationException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment