DSInternals/Src/DSInternals.Common/Cryptography/WDigestHash.cs
2018-08-06 23:54:18 +02:00

245 lines
12 KiB
C#

namespace DSInternals.Common.Cryptography
{
using DSInternals.Common;
using System;
using System.IO;
using System.Security;
using System.Security.Cryptography;
using System.Text;
/// <summary>
/// Calculates hash forms that are used in the digest authentication protocols.
/// </summary>
/// <see>https://msdn.microsoft.com/en-us/library/cc245680.aspx</see>
public static class WDigestHash
{
/// <summary>
/// The size, in bytes, of the computed hash code.
/// </summary>
public const int HashSize = 16;
/// <summary>
/// Count of MD5 hashes that are calculated.
/// </summary>
public const int HashCount = 29;
/// <summary>
/// This string is used instead of the realm name when calculating some of the hashes.
/// </summary>
private const string MagicRealm = "Digest";
private const byte CurrentVersion = 1;
private const byte DefaultReserved1Value = (byte)'1';
/// <summary>
/// Calculates WDigest hashes of a password.
/// </summary>
/// <param name="password">User's password.</param>
/// <param name="userPrincipalName">The userPrincipalName attribute value.</param>
/// <param name="samAccountName">The sAMAccountName attribute value.</param>
/// <param name="netBiosDomainName">The name attribute of the account domain object.</param>
/// <param name="dnsDomainName">The fully qualified domain name (FQDN) of the domain.</param>
/// <returns>29 MD5 hashes.</returns>
/// <remarks>SecureString is copied into managed memory while calculating the hashes, which is not the best way to deal with it.</remarks>
public static byte[][] ComputeHash(SecureString password, string userPrincipalName, string samAccountName, string netBiosDomainName, string dnsDomainName)
{
// Validate the input
Validator.AssertNotNull(password, "password");
Validator.AssertNotNullOrWhiteSpace(samAccountName, "samAccountName");
Validator.AssertNotNullOrWhiteSpace(netBiosDomainName, "netBiosDomainName");
Validator.AssertNotNullOrWhiteSpace(dnsDomainName, "dnsDomainName");
// Note that a user does not need to have a UPN.
if(String.IsNullOrEmpty(userPrincipalName))
{
// Construct the UPN as samAccountName@dnsDomainName
userPrincipalName = String.Format(@"{0}@{1}", samAccountName, dnsDomainName);
}
// Construct the pre-Windows 2000 logon name as netBiosDomainName\samAccountName
string logonName = String.Format(@"{0}\{1}", netBiosDomainName, samAccountName);
// Array of the resulting 29 hashes
byte[][] result = new byte[HashCount][];
using (var md5 = MD5.Create())
{
// Hash1: MD5(sAMAccountName, NETBIOSDomainName, password)
result[0] = md5.ComputeHash(GetBytes(samAccountName, netBiosDomainName, password));
// Hash2: MD5(LOWER(sAMAccountName), LOWER(NETBIOSDomainName), password)
result[1] = md5.ComputeHash(GetBytes(samAccountName.ToLower(), netBiosDomainName.ToLower(), password));
// Hash3: MD5(UPPER(sAMAccountName), UPPER(NETBIOSDomainName), password)
result[2] = md5.ComputeHash(GetBytes(samAccountName.ToUpper(), netBiosDomainName.ToUpper(), password));
// Hash4: MD5(sAMAccountName, UPPER(NETBIOSDomainName), password)
result[3] = md5.ComputeHash(GetBytes(samAccountName, netBiosDomainName.ToUpper(), password));
// Hash5: MD5(sAMAccountName, LOWER(NETBIOSDomainName), password)
result[4] = md5.ComputeHash(GetBytes(samAccountName, netBiosDomainName.ToLower(), password));
// Hash6: MD5(UPPER(sAMAccountName), LOWER(NETBIOSDomainName), password)
result[5] = md5.ComputeHash(GetBytes(samAccountName.ToUpper(), netBiosDomainName.ToLower(), password));
// Hash7: MD5(LOWER(sAMAccountName), UPPER(NETBIOSDomainName), password)
result[6] = md5.ComputeHash(GetBytes(samAccountName.ToLower(), netBiosDomainName.ToUpper(), password));
// Hash8: MD5(sAMAccountName, DNSDomainName, password)
result[7] = md5.ComputeHash(GetBytes(samAccountName, dnsDomainName, password));
// Hash9: MD5(LOWER(sAMAccountName), LOWER(DNSDomainName), password)
result[8] = md5.ComputeHash(GetBytes(samAccountName.ToLower(), dnsDomainName.ToLower(), password));
// Hash10: MD5(UPPER(sAMAccountName), UPPER(DNSDomainName), password)
result[9] = md5.ComputeHash(GetBytes(samAccountName.ToUpper(), dnsDomainName.ToUpper(), password));
// Hash11: MD5(sAMAccountName, UPPER(DNSDomainName), password)
result[10] = md5.ComputeHash(GetBytes(samAccountName, dnsDomainName.ToUpper(), password));
// Hash12: MD5(sAMAccountName, LOWER(DNSDomainName), password)
result[11] = md5.ComputeHash(GetBytes(samAccountName, dnsDomainName.ToLower(), password));
// Hash13: MD5(UPPER(sAMAccountName), LOWER(DNSDomainName), password)
result[12] = md5.ComputeHash(GetBytes(samAccountName.ToUpper(), dnsDomainName.ToLower(), password));
// Hash14: MD5(LOWER(sAMAccountName), UPPER(DNSDomainName), password)
result[13] = md5.ComputeHash(GetBytes(samAccountName.ToLower(), dnsDomainName.ToUpper(), password));
// Hash15: MD5(userPrincipalName, password)
result[14] = md5.ComputeHash(GetBytes(userPrincipalName, String.Empty, password));
// Hash16: MD5(LOWER(userPrincipalName), password)
result[15] = md5.ComputeHash(GetBytes(userPrincipalName.ToLower(), String.Empty, password));
// Hash17: MD5(UPPER(userPrincipalName), password)
result[16] = md5.ComputeHash(GetBytes(userPrincipalName.ToUpper(), String.Empty, password));
// Hash18: MD5(NETBIOSDomainName\sAMAccountName, password)
result[17] = md5.ComputeHash(GetBytes(logonName, password));
// Hash19: MD5(LOWER(NETBIOSDomainName\sAMAccountName), password)
result[18] = md5.ComputeHash(GetBytes(logonName.ToLower(), password));
// Hash20: MD5(UPPER(NETBIOSDomainName\sAMAccountName), password)
result[19] = md5.ComputeHash(GetBytes(logonName.ToUpper(), password));
// Hash21: MD5(sAMAccountName, "Digest", password)
result[20] = md5.ComputeHash(GetBytes(samAccountName, MagicRealm, password));
// Hash22: MD5(LOWER(sAMAccountName), "Digest", password)
result[21] = md5.ComputeHash(GetBytes(samAccountName.ToLower(), MagicRealm, password));
// Hash23: MD5(UPPER(sAMAccountName), "Digest", password)
result[22] = md5.ComputeHash(GetBytes(samAccountName.ToUpper(), MagicRealm, password));
// Hash24: MD5(userPrincipalName, "Digest", password)
result[23] = md5.ComputeHash(GetBytes(userPrincipalName, MagicRealm, password));
// Hash25: MD5(LOWER(userPrincipalName), "Digest", password)
result[24] = md5.ComputeHash(GetBytes(userPrincipalName.ToLower(), MagicRealm, password));
// Hash26: MD5(UPPER(userPrincipalName), "Digest", password)
result[25] = md5.ComputeHash(GetBytes(userPrincipalName.ToUpper(), MagicRealm, password));
// Hash27: MD5(NETBIOSDomainName\sAMAccountName, "Digest", password)
result[26] = md5.ComputeHash(GetBytes(logonName, MagicRealm, password));
// Hash28: MD5(LOWER(NETBIOSDomainName\sAMAccountName), "Digest", password)
result[27] = md5.ComputeHash(GetBytes(logonName.ToLower(), MagicRealm, password));
// Hash29: MD5(UPPER(NETBIOSDomainName\sAMAccountName), "Digest", password)
result[28] = md5.ComputeHash(GetBytes(logonName.ToUpper(), MagicRealm, password));
}
return result;
}
/// <summary>
/// Parses the WDIGEST_CREDENTIALS structure within the supplementalCredentials attribute.
/// </summary>
/// <see>https://msdn.microsoft.com/en-us/library/cc245502.aspx</see>
public static byte[][] Parse(byte[] blob)
{
using (var stream = new MemoryStream(blob))
{
using (var reader = new BinaryReader(stream))
{
// This value MUST be ignored by the recipient and MAY<22> be set to arbitrary values upon an update to the decryptedSecret attribute.
byte reserved1 = reader.ReadByte();
// This value MUST be ignored by the recipient and MUST be set to zero.
byte reserved2 = reader.ReadByte();
// This value MUST be set to 1.
byte version = reader.ReadByte();
// This value MUST be set to 29 because there are 29 hashes in the array.
byte numberOfHashes = reader.ReadByte();
// This value MUST be ignored by the recipient and MUST be set to zero.
byte[] reserved3 = reader.ReadBytes(12);
// Process hashes:
byte[][] hashes = new byte[numberOfHashes][];
for (int i = 0; i < numberOfHashes; i++)
{
hashes[i] = reader.ReadBytes(WDigestHash.HashSize);
}
return hashes;
}
}
}
/// <summary>
/// Creates the WDIGEST_CREDENTIALS structure for use within the supplementalCredentials attribute.
/// </summary>
public static byte[] Encode(byte[][] hashes)
{
Validator.AssertNotNull(hashes, "hashes");
using (var stream = new MemoryStream())
{
using (var writer = new BinaryWriter(stream))
{
// Reserved1(1 byte): This value MUST be ignored by the recipient and MAY<23 > be set to arbitrary values upon an update to the supplementalCredentials attribute.
writer.Write(DefaultReserved1Value);
// Reserved2(1 byte): This value MUST be ignored by the recipient and MUST be set to zero.
writer.Write(Byte.MinValue);
// Version(1 byte): This value MUST be set to 1.
writer.Write(CurrentVersion);
// NumberOfHashes(1 byte): This value MUST be set to 29 because there are 29 hashes in the array.
writer.Write((byte)hashes.Length);
// Reserved3(12 bytes): This value MUST be ignored by the recipient and MUST be set to zero.
writer.Write(UInt64.MinValue);
writer.Write(UInt32.MinValue);
foreach(byte[] hash in hashes)
{
// HashN (16 bytes)
writer.Write(hash);
}
return stream.ToArray();
}
}
}
private static byte[] GetBytes(string userName, SecureString password)
{
return GetBytes(userName, String.Empty, password);
}
private static byte[] GetBytes(string userName, string realm, SecureString password)
{
var concatenatedString = String.Join(":", userName, realm, password.ToUnicodeString());
// Although the documentation says that strings are converted to ISO Latin I code page prior to the hashing, the AD implementation actually uses UTF-8 instead.
return Encoding.UTF8.GetBytes(concatenatedString);
}
}
}