From 5086078f62d6cb71d522ed48ac3edb1b767e6c1e Mon Sep 17 00:00:00 2001 From: Jeffrey Stedfast Date: Mon, 16 Aug 2021 20:50:57 -0400 Subject: [PATCH] Rewrote NTLM support based on official specs. Currently only NTLMv2 is implemented. --- MailKit/MailKit.csproj | 11 +- MailKit/Security/Ntlm/BitConverterLE.cs | 17 +- MailKit/Security/Ntlm/ChallengeResponse2.cs | 285 -------- MailKit/Security/Ntlm/MessageBase.cs | 103 --- .../{NtlmAuthLevel.cs => NtlmAttribute.cs} | 48 +- .../Security/Ntlm/NtlmAttributeValuePair.cs | 241 +++++++ MailKit/Security/Ntlm/NtlmFlags.cs | 19 +- MailKit/Security/Ntlm/NtlmMessageBase.cs | 95 +++ MailKit/Security/Ntlm/NtlmSingleHostData.cs | 184 +++++ MailKit/Security/Ntlm/NtlmTargetInfo.cs | 534 +++++++++++++++ MailKit/Security/Ntlm/NtlmUtils.cs | 242 +++++++ MailKit/Security/Ntlm/RC4.cs | 205 ++++++ MailKit/Security/Ntlm/TargetInfo.cs | 288 -------- MailKit/Security/Ntlm/Type1Message.cs | 140 ++-- MailKit/Security/Ntlm/Type2Message.cs | 163 +++-- MailKit/Security/Ntlm/Type3Message.cs | 379 +++++++---- MailKit/Security/SaslMechanism.cs | 2 + MailKit/Security/SaslMechanismNtlm.cs | 122 +++- .../Security/Ntlm/NtlmSingleHostDataTests.cs | 54 ++ .../Security/Ntlm/NtlmTargetInfoTests.cs | 297 +++++++++ UnitTests/Security/Ntlm/RC4Tests.cs | 170 +++++ UnitTests/Security/Ntlm/Type1MessageTests.cs | 85 +++ UnitTests/Security/Ntlm/Type2MessageTests.cs | 121 ++++ UnitTests/Security/Ntlm/Type3MessageTests.cs | 95 +++ UnitTests/Security/SaslMechanismNtlmTests.cs | 626 +++++++++--------- UnitTests/UnitTests.csproj | 6 + 26 files changed, 3209 insertions(+), 1323 deletions(-) delete mode 100644 MailKit/Security/Ntlm/ChallengeResponse2.cs delete mode 100644 MailKit/Security/Ntlm/MessageBase.cs rename MailKit/Security/Ntlm/{NtlmAuthLevel.cs => NtlmAttribute.cs} (55%) create mode 100644 MailKit/Security/Ntlm/NtlmAttributeValuePair.cs create mode 100644 MailKit/Security/Ntlm/NtlmMessageBase.cs create mode 100644 MailKit/Security/Ntlm/NtlmSingleHostData.cs create mode 100644 MailKit/Security/Ntlm/NtlmTargetInfo.cs create mode 100644 MailKit/Security/Ntlm/NtlmUtils.cs create mode 100644 MailKit/Security/Ntlm/RC4.cs delete mode 100644 MailKit/Security/Ntlm/TargetInfo.cs create mode 100644 UnitTests/Security/Ntlm/NtlmSingleHostDataTests.cs create mode 100644 UnitTests/Security/Ntlm/NtlmTargetInfoTests.cs create mode 100644 UnitTests/Security/Ntlm/RC4Tests.cs create mode 100644 UnitTests/Security/Ntlm/Type1MessageTests.cs create mode 100644 UnitTests/Security/Ntlm/Type2MessageTests.cs create mode 100644 UnitTests/Security/Ntlm/Type3MessageTests.cs diff --git a/MailKit/MailKit.csproj b/MailKit/MailKit.csproj index 3563c5a320..a934a7eea4 100644 --- a/MailKit/MailKit.csproj +++ b/MailKit/MailKit.csproj @@ -138,14 +138,17 @@ - - - + + - + + + + + diff --git a/MailKit/Security/Ntlm/BitConverterLE.cs b/MailKit/Security/Ntlm/BitConverterLE.cs index 4db415bb1c..331ea9747a 100644 --- a/MailKit/Security/Ntlm/BitConverterLE.cs +++ b/MailKit/Security/Ntlm/BitConverterLE.cs @@ -26,14 +26,9 @@ using System; -namespace MailKit.Security.Ntlm -{ - sealed class BitConverterLE +namespace MailKit.Security.Ntlm { + static class BitConverterLE { - BitConverterLE () - { - } - unsafe static byte[] GetULongBytes (byte *bytes) { if (BitConverter.IsLittleEndian) @@ -73,7 +68,7 @@ unsafe static void UIntFromBytes (byte *dst, byte[] src, int startIndex) } } - unsafe internal static short ToInt16 (byte[] value, int startIndex) + public unsafe static short ToInt16 (byte[] value, int startIndex) { short ret; @@ -82,7 +77,7 @@ unsafe internal static short ToInt16 (byte[] value, int startIndex) return ret; } - unsafe internal static int ToInt32 (byte[] value, int startIndex) + public unsafe static int ToInt32 (byte[] value, int startIndex) { int ret; @@ -91,7 +86,7 @@ unsafe internal static int ToInt32 (byte[] value, int startIndex) return ret; } - unsafe internal static ushort ToUInt16 (byte[] value, int startIndex) + public unsafe static ushort ToUInt16 (byte[] value, int startIndex) { ushort ret; @@ -100,7 +95,7 @@ unsafe internal static ushort ToUInt16 (byte[] value, int startIndex) return ret; } - unsafe internal static uint ToUInt32 (byte[] value, int startIndex) + public unsafe static uint ToUInt32 (byte[] value, int startIndex) { uint ret; diff --git a/MailKit/Security/Ntlm/ChallengeResponse2.cs b/MailKit/Security/Ntlm/ChallengeResponse2.cs deleted file mode 100644 index 6beae42d2e..0000000000 --- a/MailKit/Security/Ntlm/ChallengeResponse2.cs +++ /dev/null @@ -1,285 +0,0 @@ -// -// Mono.Security.Protocol.Ntlm.ChallengeResponse -// Implements Challenge Response for NTLM v1 and NTLM v2 Session -// -// Authors: Sebastien Pouliot -// Martin Baulig -// Jeffrey Stedfast -// -// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) -// Copyright (c) 2004 Novell (http://www.novell.com) -// Copyright (c) 2012 Xamarin, Inc. (http://www.xamarin.com) -// -// References -// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär -// http://www.innovation.ch/java/ntlm.html -// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass -// http://davenport.sourceforge.net/ntlm.html -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Text; -using System.Security.Cryptography; - -namespace MailKit.Security.Ntlm { - static class ChallengeResponse2 - { - static readonly byte[] Magic = { 0x4B, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25 }; - - // This is the pre-encrypted magic value with a null DES key (0xAAD3B435B51404EE) - // Ref: http://packetstormsecurity.nl/Crackers/NT/l0phtcrack/l0phtcrack2.5-readme.html - static readonly byte[] NullEncMagic = { 0xAA, 0xD3, 0xB4, 0x35, 0xB5, 0x14, 0x04, 0xEE }; - - static byte[] ComputeLM (string password, byte[] challenge) - { - var buffer = new byte[21]; - - // create Lan Manager password - using (var des = DES.Create ()) { - des.Mode = CipherMode.ECB; - - // Note: In .NET DES cannot accept a weak key - // this can happen for a null password - if (string.IsNullOrEmpty (password)) { - Buffer.BlockCopy (NullEncMagic, 0, buffer, 0, 8); - } else { - des.Key = PasswordToKey (password, 0); - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (Magic, 0, 8, buffer, 0); - } - - // and if a password has less than 8 characters - if (password == null || password.Length < 8) { - Buffer.BlockCopy (NullEncMagic, 0, buffer, 8, 8); - } else { - des.Key = PasswordToKey (password, 7); - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (Magic, 0, 8, buffer, 8); - } - } - - return GetResponse (challenge, buffer); - } - - static byte[] ComputeNtlmPassword (string password) - { - var buffer = new byte[21]; - - // create NT password - using (var md4 = new MD4 ()) { - var data = password == null ? new byte[0] : Encoding.Unicode.GetBytes (password); - var hash = md4.ComputeHash (data); - Buffer.BlockCopy (hash, 0, buffer, 0, 16); - - // clean up - Array.Clear (data, 0, data.Length); - Array.Clear (hash, 0, hash.Length); - } - - return buffer; - } - - static byte[] ComputeNtlm (string password, byte[] challenge) - { - var buffer = ComputeNtlmPassword (password); - return GetResponse (challenge, buffer); - } - - static void ComputeNtlmV2Session (string password, byte[] challenge, out byte[] lm, out byte[] ntlm) - { - var nonce = new byte[8]; - - using (var rng = RandomNumberGenerator.Create ()) - rng.GetBytes (nonce); - - var sessionNonce = new byte[challenge.Length + 8]; - challenge.CopyTo (sessionNonce, 0); - nonce.CopyTo (sessionNonce, challenge.Length); - - lm = new byte[24]; - nonce.CopyTo (lm, 0); - - using (var md5 = MD5.Create ()) { - var hash = md5.ComputeHash (sessionNonce); - var newChallenge = new byte[8]; - - Array.Copy (hash, newChallenge, 8); - - ntlm = ComputeNtlm (password, newChallenge); - - // clean up - Array.Clear (newChallenge, 0, newChallenge.Length); - Array.Clear (hash, 0, hash.Length); - } - - // clean up - Array.Clear (sessionNonce, 0, sessionNonce.Length); - Array.Clear (nonce, 0, nonce.Length); - } - - static byte[] ComputeNtlmV2 (Type2Message type2, string username, string password, string domain) - { - var ntlm_hash = ComputeNtlmPassword (password); - - var ubytes = Encoding.Unicode.GetBytes (username.ToUpperInvariant ()); - var tbytes = Encoding.Unicode.GetBytes (domain); - - var bytes = new byte[ubytes.Length + tbytes.Length]; - ubytes.CopyTo (bytes, 0); - Array.Copy (tbytes, 0, bytes, ubytes.Length, tbytes.Length); - - byte[] ntlm_v2_hash; - - using (var md5 = new HMACMD5 (ntlm_hash)) - ntlm_v2_hash = md5.ComputeHash (bytes); - - Array.Clear (ntlm_hash, 0, ntlm_hash.Length); - - using (var md5 = new HMACMD5 (ntlm_v2_hash)) { - var timestamp = DateTime.UtcNow.Ticks - 504911232000000000; - var nonce = new byte[8]; - - if (type2.TargetInfo?.Timestamp != 0) - timestamp = type2.TargetInfo.Timestamp; - - using (var rng = RandomNumberGenerator.Create ()) - rng.GetBytes (nonce); - - var targetInfo = type2.EncodedTargetInfo; - var blob = new byte[28 + targetInfo.Length]; - blob[0] = 0x01; - blob[1] = 0x01; - - Buffer.BlockCopy (BitConverterLE.GetBytes (timestamp), 0, blob, 8, 8); - - Buffer.BlockCopy (nonce, 0, blob, 16, 8); - Buffer.BlockCopy (targetInfo, 0, blob, 28, targetInfo.Length); - - var challenge = type2.Nonce; - - var hashInput = new byte[challenge.Length + blob.Length]; - challenge.CopyTo (hashInput, 0); - blob.CopyTo (hashInput, challenge.Length); - - var blobHash = md5.ComputeHash (hashInput); - - var response = new byte[blob.Length + blobHash.Length]; - blobHash.CopyTo (response, 0); - blob.CopyTo (response, blobHash.Length); - - Array.Clear (ntlm_v2_hash, 0, ntlm_v2_hash.Length); - Array.Clear (hashInput, 0, hashInput.Length); - Array.Clear (blobHash, 0, blobHash.Length); - Array.Clear (nonce, 0, nonce.Length); - Array.Clear (blob, 0, blob.Length); - - return response; - } - } - - public static void Compute (Type2Message type2, NtlmAuthLevel level, string username, string password, string domain, out byte[] lm, out byte[] ntlm) - { - lm = null; - - switch (level) { - case NtlmAuthLevel.LM_and_NTLM: - lm = ComputeLM (password, type2.Nonce); - ntlm = ComputeNtlm (password, type2.Nonce); - break; - case NtlmAuthLevel.LM_and_NTLM_and_try_NTLMv2_Session: - if ((type2.Flags & NtlmFlags.NegotiateNtlm2Key) == 0) - goto case NtlmAuthLevel.LM_and_NTLM; - ComputeNtlmV2Session (password, type2.Nonce, out lm, out ntlm); - break; - case NtlmAuthLevel.NTLM_only: - if ((type2.Flags & NtlmFlags.NegotiateNtlm2Key) != 0) - ComputeNtlmV2Session (password, type2.Nonce, out lm, out ntlm); - else - ntlm = ComputeNtlm (password, type2.Nonce); - break; - case NtlmAuthLevel.NTLMv2_only: - ntlm = ComputeNtlmV2 (type2, username, password, domain); - if (type2.TargetInfo.Timestamp != 0) - lm = new byte[24]; - break; - default: - throw new InvalidOperationException (); - } - } - - static byte[] GetResponse (byte[] challenge, byte[] pwd) - { - var response = new byte[24]; - - using (var des = DES.Create ()) { - des.Mode = CipherMode.ECB; - des.Key = PrepareDESKey (pwd, 0); - - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (challenge, 0, 8, response, 0); - - des.Key = PrepareDESKey (pwd, 7); - - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (challenge, 0, 8, response, 8); - - des.Key = PrepareDESKey (pwd, 14); - - using (var ct = des.CreateEncryptor ()) - ct.TransformBlock (challenge, 0, 8, response, 16); - } - - return response; - } - - static byte[] PrepareDESKey (byte[] key56bits, int position) - { - // convert to 8 bytes - var key = new byte[8]; - - key[0] = key56bits [position]; - key[1] = (byte) ((key56bits[position] << 7) | (key56bits[position + 1] >> 1)); - key[2] = (byte) ((key56bits[position + 1] << 6) | (key56bits[position + 2] >> 2)); - key[3] = (byte) ((key56bits[position + 2] << 5) | (key56bits[position + 3] >> 3)); - key[4] = (byte) ((key56bits[position + 3] << 4) | (key56bits[position + 4] >> 4)); - key[5] = (byte) ((key56bits[position + 4] << 3) | (key56bits[position + 5] >> 5)); - key[6] = (byte) ((key56bits[position + 5] << 2) | (key56bits[position + 6] >> 6)); - key[7] = (byte) (key56bits[position + 6] << 1); - - return key; - } - - static byte[] PasswordToKey (string password, int position) - { - int len = Math.Min (password.Length - position, 7); - var key7 = new byte[7]; - - Encoding.ASCII.GetBytes (password.ToUpper (), position, len, key7, 0); - var key8 = PrepareDESKey (key7, 0); - - // cleanup intermediate key material - Array.Clear (key7, 0, key7.Length); - - return key8; - } - } -} diff --git a/MailKit/Security/Ntlm/MessageBase.cs b/MailKit/Security/Ntlm/MessageBase.cs deleted file mode 100644 index a9c41ac3cc..0000000000 --- a/MailKit/Security/Ntlm/MessageBase.cs +++ /dev/null @@ -1,103 +0,0 @@ -// -// Mono.Security.Protocol.Ntlm.MessageBase -// abstract class for all NTLM messages -// -// Author: -// Sebastien Pouliot -// -// Copyright (C) 2003 Motus Technologies Inc. (http://www.motus.com) -// Copyright (C) 2004 Novell, Inc (http://www.novell.com) -// -// References -// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär -// http://www.innovation.ch/java/ntlm.html -// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass -// http://davenport.sourceforge.net/ntlm.html -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Globalization; - -namespace MailKit.Security.Ntlm { - abstract class MessageBase - { - static readonly byte[] header = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00 }; - - protected MessageBase (int type) - { - Type = type; - } - - public NtlmFlags Flags { - get; set; - } - - public Version OSVersion { - get; protected set; - } - - public int Type { - get; private set; - } - - protected byte[] PrepareMessage (int size) - { - var message = new byte[size]; - - Buffer.BlockCopy (header, 0, message, 0, 8); - - message[ 8] = (byte) Type; - message[ 9] = (byte)(Type >> 8); - message[10] = (byte)(Type >> 16); - message[11] = (byte)(Type >> 24); - - return message; - } - - bool CheckHeader (byte[] message, int startIndex) - { - for (int i = 0; i < header.Length; i++) { - if (message[startIndex + i] != header[i]) - return false; - } - - return BitConverterLE.ToUInt32 (message, startIndex + 8) == Type; - } - - protected void ValidateArguments (byte[] message, int startIndex, int length) - { - if (message == null) - throw new ArgumentNullException (nameof (message)); - - if (startIndex < 0 || startIndex > message.Length) - throw new ArgumentOutOfRangeException (nameof (startIndex)); - - if (length < 12 || length > (message.Length - startIndex)) - throw new ArgumentOutOfRangeException (nameof (length)); - - if (!CheckHeader (message, startIndex)) - throw new ArgumentException (string.Format (CultureInfo.InvariantCulture, "Invalid Type{0} message.", Type), nameof (message)); - } - - public abstract byte[] Encode (); - } -} diff --git a/MailKit/Security/Ntlm/NtlmAuthLevel.cs b/MailKit/Security/Ntlm/NtlmAttribute.cs similarity index 55% rename from MailKit/Security/Ntlm/NtlmAuthLevel.cs rename to MailKit/Security/Ntlm/NtlmAttribute.cs index 584d1646d1..640b4f46a0 100644 --- a/MailKit/Security/Ntlm/NtlmAuthLevel.cs +++ b/MailKit/Security/Ntlm/NtlmAttribute.cs @@ -1,10 +1,9 @@ +// +// NtlmAttribute.cs // -// NtlmAuthLevel.cs +// Author: Jeffrey Stedfast // -// Author: -// Martin Baulig -// -// Copyright (c) 2012 Xamarin Inc. (http://www.xamarin.com) +// Copyright (c) 2013-2021 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -23,29 +22,24 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +// -namespace MailKit.Security.Ntlm { - /* - * On Windows, this is controlled by a registry setting - * (http://msdn.microsoft.com/en-us/library/ms814176.aspx) - * - * This can be configured by setting the static - * Type3Message.DefaultAuthLevel property, the default value - * is LM_and_NTLM_and_try_NTLMv2_Session. - */ - enum NtlmAuthLevel { - /* Use LM and NTLM, never use NTLMv2 session security. */ - LM_and_NTLM, - - /* Use NTLMv2 session security if the server supports it, - * otherwise fall back to LM and NTLM. */ - LM_and_NTLM_and_try_NTLMv2_Session, - - /* Use NTLMv2 session security if the server supports it, - * otherwise fall back to NTLM. Never use LM. */ - NTLM_only, +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 - /* Use NTLMv2 only. */ - NTLMv2_only, +namespace MailKit.Security.Ntlm +{ + enum NtlmAttribute : short + { + EOL = 0, + ServerName = 1, + DomainName = 2, + DnsServerName = 3, + DnsDomainName = 4, + DnsTreeName = 5, + Flags = 6, + Timestamp = 7, + SingleHost = 8, + TargetName = 9, + ChannelBinding = 10 } } diff --git a/MailKit/Security/Ntlm/NtlmAttributeValuePair.cs b/MailKit/Security/Ntlm/NtlmAttributeValuePair.cs new file mode 100644 index 0000000000..ad01801d8a --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmAttributeValuePair.cs @@ -0,0 +1,241 @@ +// +// NtlmAttributeValuePair.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2021 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +namespace MailKit.Security.Ntlm { + /// + /// An abstract NTLM attribute and value pair. + /// + /// + /// An abstract NTLM attribute and value pair. + /// + abstract class NtlmAttributeValuePair + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair. + /// + /// The NTLM attribute. + protected NtlmAttributeValuePair (NtlmAttribute attr) + { + Attribute = attr; + } + + /// + /// Get the NTLM attribute that this pair represents. + /// + /// + /// Gets the NTLM attribute that this pair represents. + /// + /// The NTLM attribute. + public NtlmAttribute Attribute { + get; private set; + } + } + + /// + /// An NTLM attribute and value pair consisting of a string value. + /// + /// + /// An NTLM attribute and value pair consisting of a string value. + /// + sealed class NtlmAttributeStringValuePair : NtlmAttributeValuePair + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a string value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + public NtlmAttributeStringValuePair (NtlmAttribute attr, string value) : base (attr) + { + Value = value; + } + + /// + /// Get or set the value of the attribute. + /// + /// + /// Gets or sets the value of the attribute. + /// + /// The attribute value. + public string Value { + get; set; + } + } + + /// + /// An NTLM attribute and value pair consisting of a flags value. + /// + /// + /// An NTLM attribute and value pair consisting of a flags value. + /// + sealed class NtlmAttributeFlagsValuePair : NtlmAttributeValuePair + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a flags value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + /// The size of the encoded flags value. + internal NtlmAttributeFlagsValuePair (NtlmAttribute attr, int value, short size) : base (attr) + { + Value = value; + Size = size; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a flags value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + public NtlmAttributeFlagsValuePair (NtlmAttribute attr, int value) : this (attr, value, 4) + { + } + + /// + /// Get or set the size of the encoded flags value. + /// + /// + /// Gets or sets the size of the encoded flags value. + /// + public short Size { + get; internal set; + } + + /// + /// Get or set the value of the attribute. + /// + /// + /// Gets or sets the value of the attribute. + /// + /// The attribute value. + public int Value { + get; set; + } + } + + /// + /// An NTLM attribute and value pair consisting of a timestamp value. + /// + /// + /// An NTLM attribute and value pair consisting of a timestamp value. + /// + sealed class NtlmAttributeTimestampValuePair : NtlmAttributeValuePair + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a timestamp value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + /// The size of the encoded flags value. + internal NtlmAttributeTimestampValuePair (NtlmAttribute attr, long value, short size) : base (attr) + { + Value = value; + Size = size; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a timestamp value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + public NtlmAttributeTimestampValuePair (NtlmAttribute attr, long value) : this (attr, value, 8) + { + } + + /// + /// Get or set the size of the encoded timestamp value. + /// + /// + /// Gets or sets the size of the encoded timestamp value. + /// + public short Size { + get; internal set; + } + + /// + /// Get or set the value of the attribute. + /// + /// + /// Gets or sets the value of the attribute. + /// + /// The attribute value. + public long Value { + get; set; + } + } + + /// + /// An NTLM attribute and value pair consisting of a byte array value. + /// + /// + /// An NTLM attribute and value pair consisting of a byte array value. + /// + sealed class NtlmAttributeByteArrayValuePair : NtlmAttributeValuePair + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM attribute and value pair consisting of a byte array value. + /// + /// The NTLM attribute. + /// The NTLM attribute value. + public NtlmAttributeByteArrayValuePair (NtlmAttribute attr, byte[] value) : base (attr) + { + Value = value; + } + + /// + /// Get or set the value of the attribute. + /// + /// + /// Gets or sets the value of the attribute. + /// + /// The attribute value. + public byte[] Value { + get; set; + } + } +} diff --git a/MailKit/Security/Ntlm/NtlmFlags.cs b/MailKit/Security/Ntlm/NtlmFlags.cs index b85121ec90..a0be760258 100644 --- a/MailKit/Security/Ntlm/NtlmFlags.cs +++ b/MailKit/Security/Ntlm/NtlmFlags.cs @@ -24,6 +24,8 @@ // THE SOFTWARE. // +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + using System; namespace MailKit.Security.Ntlm { @@ -148,13 +150,18 @@ enum NtlmFlags { R6 = TargetTypeShare, /// - /// Indicates that the NTLM2 signing and sealing scheme should be used for - /// protecting authenticated communications. Note that this refers to a - /// particular session security scheme, and is not related to the use of - /// NTLMv2 authentication. This flag can, however, have an effect on the - /// response calculations. + /// If set, requests usage of the NTLM v2 session security. NTLM v2 session + /// security is a misnomer because it is not NTLM v2. It is NTLM v1 using the + /// extended session security that is also in NTLM v2. NTLMSSP_NEGOTIATE_LM_KEY + /// and NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY are mutually exclusive. If + /// both NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY and NTLMSSP_NEGOTIATE_LM_KEY + /// are requested, NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY alone MUST be + /// returned to the client. NTLM v2 authentication session key generation MUST + /// be supported by both the client and the DC in order to be used, and extended + /// session security signing and sealing requires support from the client and + /// the server in order to be used. /// - NegotiateNtlm2Key = 0x00080000, + NegotiateExtendedSessionSecurity = 0x00080000, /// /// This flag's usage has not been identified. diff --git a/MailKit/Security/Ntlm/NtlmMessageBase.cs b/MailKit/Security/Ntlm/NtlmMessageBase.cs new file mode 100644 index 0000000000..74176bf33e --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmMessageBase.cs @@ -0,0 +1,95 @@ +// +// NtlmMessageBase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2021 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +using System; +using System.Globalization; + +namespace MailKit.Security.Ntlm { + abstract class NtlmMessageBase + { + static readonly byte[] Signature = { (byte) 'N', (byte) 'T', (byte) 'L', (byte) 'M', (byte) 'S', (byte) 'S', (byte) 'P', 0x00 }; + + protected NtlmMessageBase (int type) + { + Type = type; + } + + public NtlmFlags Flags { + get; protected set; + } + + public Version OSVersion { + get; protected set; + } + + public int Type { + get; private set; + } + + protected byte[] PrepareMessage (int size) + { + var message = new byte[size]; + + Buffer.BlockCopy (Signature, 0, message, 0, 8); + + message[ 8] = (byte) Type; + message[ 9] = (byte)(Type >> 8); + message[10] = (byte)(Type >> 16); + message[11] = (byte)(Type >> 24); + + return message; + } + + bool CheckSignature (byte[] message, int startIndex) + { + for (int i = 0; i < Signature.Length; i++) { + if (message[startIndex + i] != Signature[i]) + return false; + } + + return BitConverterLE.ToUInt32 (message, startIndex + 8) == Type; + } + + protected void ValidateArguments (byte[] message, int startIndex, int length) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (startIndex < 0 || startIndex > message.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 12 || length > (message.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (!CheckSignature (message, startIndex)) + throw new ArgumentException (string.Format (CultureInfo.InvariantCulture, "Invalid Type{0} message.", Type), nameof (message)); + } + + public abstract byte[] Encode (); + } +} diff --git a/MailKit/Security/Ntlm/NtlmSingleHostData.cs b/MailKit/Security/Ntlm/NtlmSingleHostData.cs new file mode 100644 index 0000000000..43bb25125a --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmSingleHostData.cs @@ -0,0 +1,184 @@ +// +// NtlmSingleHostData.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2021 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +using System; + +namespace MailKit.Security.Ntlm { + /// + /// An NTLM SingleHostData structure. + /// + /// + /// An NTLM SingleHostData structure. + /// + class NtlmSingleHostData + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The raw target info buffer to decode. + /// The starting index of the single host data structure. + /// The length of the single host data structure. + public NtlmSingleHostData (byte[] buffer, int startIndex, int length) + { + Decode (buffer, startIndex, length); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The 8-byte platform-specific blob. + /// The 256-bit randomly generated machine id. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not 8 bytes. + /// -or- + /// is not 32 bytes. + /// + public NtlmSingleHostData (byte[] customData, byte[] machineId) + { + if (customData == null) + throw new ArgumentNullException (nameof (customData)); + + if (customData.Length != 8) + throw new ArgumentException (nameof (customData)); + + if (machineId == null) + throw new ArgumentNullException (nameof (machineId)); + + if (machineId.Length != 32) + throw new ArgumentException (nameof (machineId)); + + CustomData = customData; + MachineId = machineId; + Size = 48; + } + + /// + /// Get or set an 8-byte platform-specific blob. + /// + /// + /// Gets or sets an 8-byte platform-specific blob. + /// + public byte[] CustomData { + get; private set; + } + + /// + /// Get the 256-bit randomly generated machine ID. + /// + /// + /// Gets the 256-bit randomly generated machine ID. + /// + /// The 256-bit randomly generated machine ID. + public byte[] MachineId { + get; private set; + } + + /// + /// Get the size of the SingleHostData structure. + /// + /// + /// Gets the size of the SingleHostData structure. + /// + /// The size of the SingleHostData structure. + public int Size { + get; private set; + } + + void Decode (byte[] buffer, int startIndex, int length) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (startIndex < 0 || startIndex > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 48 || length > (buffer.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + int index = startIndex; + + // Size (4 bytes): A 32-bit unsigned integer that defines the length, in bytes, of the Value field in the AV_PAIR (section 2.2.2.1) structure. + Size = BitConverterLE.ToInt32 (buffer, index); + index += 4; + + // Z4 (4 bytes): A 32-bit integer value containing 0x00000000. + index += 4; + + // CustomData (8 bytes): An 8-byte platform-specific blob containing info only relevant when the client and the server are on the same host. + CustomData = new byte[8]; + Buffer.BlockCopy (buffer, index, CustomData, 0, 8); + index += 8; + + // MachineID (32 bytes): A 256-bit random number created at computer startup to identify the calling machine. + MachineId = new byte[32]; + Buffer.BlockCopy (buffer, index, MachineId, 0, 32); + } + + /// + /// Encode the SingleHostData structure. + /// + /// + /// Encodes the SingleHostData structure. + /// + /// The encoded SingleHostData structure. + public byte[] Encode () + { + var buffer = new byte[Size]; + int index = 0; + + // Size (4 bytes): A 32-bit unsigned integer that defines the length, in bytes, of the Value field in the AV_PAIR (section 2.2.2.1) structure. + buffer[index++] = (byte) (Size); + buffer[index++] = (byte) (Size >> 8); + buffer[index++] = (byte) (Size >> 16); + buffer[index++] = (byte) (Size >> 24); + + // Z4 (4 bytes): A 32-bit integer value containing 0x00000000. + index += 4; + + // CustomData (8 bytes): An 8-byte platform-specific blob containing info only relevant when the client and the server are on the same host. + Buffer.BlockCopy (CustomData, 0, buffer, index, 8); + index += 8; + + // MachineID (32 bytes): A 256-bit random number created at computer startup to identify the calling machine. + Buffer.BlockCopy (MachineId, 0, buffer, index, 32); + + return buffer; + } + } +} diff --git a/MailKit/Security/Ntlm/NtlmTargetInfo.cs b/MailKit/Security/Ntlm/NtlmTargetInfo.cs new file mode 100644 index 0000000000..fa1956c20a --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmTargetInfo.cs @@ -0,0 +1,534 @@ +// +// NtlmTargetInfo.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2021 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +using System; +using System.Text; +using System.Collections.Generic; + +namespace MailKit.Security.Ntlm { + /// + /// An NTLM TargetInfo structure. + /// + /// + /// An NTLM TargetInfo structure. + /// + class NtlmTargetInfo + { + readonly List attributes = new List (); + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The raw target info buffer to decode. + /// The starting index of the target info structure. + /// The length of the target info structure. + /// true if the target info strings are unicode; otherwise, false. + public NtlmTargetInfo (byte[] buffer, int startIndex, int length, bool unicode) + { + Decode (buffer, startIndex, length, unicode); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public NtlmTargetInfo () + { + } + + /// + /// Copy the attribute value pairs to another TargetInfo. + /// + /// + /// Copies the attribute value pairs to another TargetInfo. + /// + public void CopyTo (NtlmTargetInfo targetInfo) + { + targetInfo.attributes.Clear (); + + foreach (var attribute in attributes) { + if (attribute is NtlmAttributeTimestampValuePair timestamp) + targetInfo.attributes.Add (new NtlmAttributeTimestampValuePair (timestamp.Attribute, timestamp.Value, timestamp.Size)); + else if (attribute is NtlmAttributeFlagsValuePair flags) + targetInfo.attributes.Add (new NtlmAttributeFlagsValuePair (flags.Attribute, flags.Value, flags.Size)); + else if (attribute is NtlmAttributeByteArrayValuePair array) + targetInfo.attributes.Add (new NtlmAttributeByteArrayValuePair (array.Attribute, array.Value)); + else if (attribute is NtlmAttributeStringValuePair str) + targetInfo.attributes.Add (new NtlmAttributeStringValuePair (str.Attribute, str.Value)); + } + } + + NtlmAttributeValuePair GetAvPair (NtlmAttribute attr) + { + for (int i = 0; i < attributes.Count; i++) { + if (attributes[i].Attribute == attr) + return attributes[i]; + } + + return null; + } + + string GetAvPairString (NtlmAttribute attr) + { + return ((NtlmAttributeStringValuePair) GetAvPair (attr))?.Value; + } + + void SetAvPairString (NtlmAttribute attr, string value) + { + var pair = (NtlmAttributeStringValuePair) GetAvPair (attr); + + if (pair == null) { + if (value != null) + attributes.Add (new NtlmAttributeStringValuePair (attr, value)); + } else if (value != null) { + pair.Value = value; + } else { + attributes.Remove (pair); + } + } + + byte[] GetAvPairByteArray (NtlmAttribute attr) + { + return ((NtlmAttributeByteArrayValuePair) GetAvPair (attr))?.Value; + } + + void SetAvPairByteArray (NtlmAttribute attr, byte[] value) + { + var pair = (NtlmAttributeByteArrayValuePair) GetAvPair (attr); + + if (pair == null) { + if (value != null) + attributes.Add (new NtlmAttributeByteArrayValuePair (attr, value)); + } else if (value != null) { + pair.Value = value; + } else { + attributes.Remove (pair); + } + } + + /// + /// Get or set the server's NetBIOS computer name. + /// + /// + /// Gets or sets the server's NetBIOS computer name. + /// + /// The server's NetBIOS computer name if available; otherwise, null. + public string ServerName { + get { return GetAvPairString (NtlmAttribute.ServerName); } + set { SetAvPairString (NtlmAttribute.ServerName, value); } + } + + /// + /// Get or set the server's NetBIOS domain name. + /// + /// + /// Gets or sets the server's NetBIOS domain name. + /// + /// The server's NetBIOS domain name if available; otherwise, null. + public string DomainName { + get { return GetAvPairString (NtlmAttribute.DomainName); } + set { SetAvPairString (NtlmAttribute.DomainName, value); } + } + + /// + /// Get or set the fully qualified domain name (FQDN) of the server. + /// + /// + /// Gets or sets the fully qualified domain name (FQDN) of the server. + /// + /// The fully qualified domain name (FQDN) of the server if available; otherwise, null. + public string DnsServerName { + get { return GetAvPairString (NtlmAttribute.DnsServerName); } + set { SetAvPairString (NtlmAttribute.DnsServerName, value); } + } + + /// + /// Get or set the fully qualified domain name (FQDN) of the domain. + /// + /// + /// Gets or sets the fully qualified domain name (FQDN) of the domain. + /// + /// The fully qualified domain name (FQDN) of the domain if available; otherwise, null. + public string DnsDomainName { + get { return GetAvPairString (NtlmAttribute.DnsDomainName); } + set { SetAvPairString (NtlmAttribute.DnsDomainName, value); } + } + + /// + /// Get or set the fully qualified domain name (FQDN) of the forest. + /// + /// + /// Gets or sets the fully qualified domain name (FQDN) of the forest. + /// + /// The fully qualified domain name (FQDN) of the forest if available; otherwise, null. + public string DnsTreeName { + get { return GetAvPairString (NtlmAttribute.DnsTreeName); } + set { SetAvPairString (NtlmAttribute.DnsTreeName, value); } + } + + /// + /// Get or set a 32-bit value indicating server or client configuration. + /// + /// + /// Gets or sets a 32-bit value indicating server or client configuration. + /// 0x00000001: Indicates to the client that the account authentication is constrained. + /// 0x00000002: Indicates that the client is providing message integrity in the MIC field (section 2.2.1.3) in the AUTHENTICATE_MESSAGE. + /// 0x00000004: Indicates that the client is providing a target SPN generated from an untrusted source. + /// + /// The 32-bit flags value if available; otherwise, null. + public int? Flags { + get { return ((NtlmAttributeFlagsValuePair) GetAvPair (NtlmAttribute.Flags))?.Value; } + set { + var pair = (NtlmAttributeFlagsValuePair) GetAvPair (NtlmAttribute.Flags); + + if (pair == null) { + if (value != null) + attributes.Add (new NtlmAttributeFlagsValuePair (NtlmAttribute.Flags, value.Value)); + } else if (value != null) { + pair.Size = Math.Max (pair.Size, (short) (value.Value > short.MaxValue ? 4 : 2)); + pair.Value = value.Value; + } else { + attributes.Remove (pair); + } + } + } + + /// + /// Get or set a timestamp that contains the server local time. + /// + /// + /// Gets or sets a timestamp that contains the server local time. + /// A FILETIME structure ([MS-DTYP] section 2.3.3) in little-endian byte order that contains + /// the server local time. This structure is always sent in the CHALLENGE_MESSAGE. + /// + /// The local time of the server, if available; otherwise null. + public long? Timestamp { + get { return ((NtlmAttributeTimestampValuePair) GetAvPair (NtlmAttribute.Timestamp))?.Value; } + set { + var pair = (NtlmAttributeTimestampValuePair) GetAvPair (NtlmAttribute.Timestamp); + + if (pair == null) { + if (value != null) + attributes.Add (new NtlmAttributeTimestampValuePair (NtlmAttribute.Timestamp, value.Value)); + } else if (value != null) { + pair.Size = Math.Max (pair.Size, (short) (value.Value > int.MaxValue ? 8 : 4)); + pair.Value = value.Value; + } else { + attributes.Remove (pair); + } + } + } + + /// + /// Get or set the single host data structure. + /// + /// + /// Gets or sets the single host data structure. + /// The Value field contains a platform-specific blob, as well as a MachineID created at computer startup to identify the calling machine. + /// + /// The single host data structure, if available; otherwise, null. + public byte[] SingleHost { + get { return GetAvPairByteArray (NtlmAttribute.SingleHost); } + set { SetAvPairByteArray (NtlmAttribute.SingleHost, value); } + } + + /// + /// Get or set the Service Principal Name (SPN) of the server. + /// + /// + /// Gets or sets the Service Principal Name (SPN) of the server. + /// + /// The Service Principal Name (SPN) of the server, if available; otherwise, null. + public string TargetName { + get { return GetAvPairString (NtlmAttribute.TargetName); } + set { SetAvPairString (NtlmAttribute.TargetName, value); } + } + + /// + /// Get or set the channel binding hash. + /// + /// + /// Gets or sets the channel binding hash. + /// + /// An MD5 hash of the channel binding data, if available; otherwise null. + public byte[] ChannelBinding { + get { return GetAvPairByteArray (NtlmAttribute.ChannelBinding); } + set { SetAvPairByteArray (NtlmAttribute.ChannelBinding, value); } + } + + static byte[] DecodeByteArray (byte[] buffer, ref int index) + { + var length = BitConverterLE.ToInt16 (buffer, index); + var value = new byte[length]; + + Buffer.BlockCopy (buffer, index + 2, value, 0, length); + + index += 2 + length; + + return value; + } + + static string DecodeString (byte[] buffer, ref int index, bool unicode) + { + var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; + var length = BitConverterLE.ToInt16 (buffer, index); + var value = encoding.GetString (buffer, index + 2, length); + + index += 2 + length; + + return value; + } + + static int DecodeFlags (byte[] buffer, ref int index, out short size) + { + size = BitConverterLE.ToInt16 (buffer, index); + int flags; + + index += 2; + + switch (size) { + case 4: flags = BitConverterLE.ToInt32 (buffer, index); break; + case 2: flags = BitConverterLE.ToInt16 (buffer, index); break; + default: flags = 0; break; + } + + index += size; + + return flags; + } + + static long DecodeTimestamp (byte[] buffer, ref int index, out short size) + { + size = BitConverterLE.ToInt16 (buffer, index); + long value; + + index += 2; + + switch (size) { + case 8: + long lo = BitConverterLE.ToUInt32 (buffer, index); + long hi = BitConverterLE.ToUInt32 (buffer, index + 4); + value = (hi << 32) | lo; + break; + case 4: value = BitConverterLE.ToUInt32 (buffer, index); break; + case 2: value = BitConverterLE.ToUInt16 (buffer, index); break; + default: value = 0; break; + } + + index += size; + + return value; + } + + void Decode (byte[] buffer, int startIndex, int length, bool unicode) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (startIndex < 0 || startIndex > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 12 || length > (buffer.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + int index = startIndex; + + do { + var attr = (NtlmAttribute) BitConverterLE.ToInt16 (buffer, index); + short size; + + index += 2; + + switch (attr) { + case NtlmAttribute.EOL: + index = startIndex + length; + break; + case NtlmAttribute.ServerName: + case NtlmAttribute.DomainName: + case NtlmAttribute.DnsServerName: + case NtlmAttribute.DnsDomainName: + case NtlmAttribute.DnsTreeName: + case NtlmAttribute.TargetName: + attributes.Add (new NtlmAttributeStringValuePair (attr, DecodeString (buffer, ref index, unicode))); + break; + case NtlmAttribute.Flags: + attributes.Add (new NtlmAttributeFlagsValuePair (attr, DecodeFlags (buffer, ref index, out size), size)); + break; + case NtlmAttribute.Timestamp: + attributes.Add (new NtlmAttributeTimestampValuePair (attr, DecodeTimestamp (buffer, ref index, out size), size)); + break; + default: + attributes.Add (new NtlmAttributeByteArrayValuePair (attr, DecodeByteArray (buffer, ref index))); + break; + } + } while (index < startIndex + length); + } + + int CalculateSize (bool unicode) + { + var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; + int length = 4; + + foreach (var attribute in attributes) { + switch (attribute.Attribute) { + case NtlmAttribute.ServerName: + case NtlmAttribute.DomainName: + case NtlmAttribute.DnsServerName: + case NtlmAttribute.DnsDomainName: + case NtlmAttribute.DnsTreeName: + case NtlmAttribute.TargetName: + var str = (NtlmAttributeStringValuePair) attribute; + length += 4 + encoding.GetByteCount (str.Value); + break; + case NtlmAttribute.Flags: + var flags = (NtlmAttributeFlagsValuePair) attribute; + length += 4 + flags.Size; + break; + case NtlmAttribute.Timestamp: + var timestamp = (NtlmAttributeTimestampValuePair) attribute; + length += 4 + timestamp.Size; + break; + default: + var channelBinding = (NtlmAttributeByteArrayValuePair) attribute; + length += 4 + channelBinding.Value.Length; + break; + } + } + + return length; + } + + static void EncodeInt16 (byte[] buf, ref int index, short value) + { + buf[index++] = (byte) (value); + buf[index++] = (byte) (value >> 8); + } + + static void EncodeInt32 (byte[] buf, ref int index, int value) + { + buf[index++] = (byte) (value); + buf[index++] = (byte) (value >> 8); + buf[index++] = (byte) (value >> 16); + buf[index++] = (byte) (value >> 24); + } + + static void EncodeTypeAndLength (byte[] buf, ref int index, NtlmAttribute attr, short length) + { + EncodeInt16 (buf, ref index, (short) attr); + EncodeInt16 (buf, ref index, length); + } + + static void EncodeByteArray (byte[] buf, ref int index, NtlmAttribute attr, byte[] value) + { + EncodeTypeAndLength (buf, ref index, attr, (short) value.Length); + Buffer.BlockCopy (value, 0, buf, index, value.Length); + index += value.Length; + } + + static void EncodeString (byte[] buf, ref int index, NtlmAttribute attr, string value, bool unicode) + { + var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; + int length = encoding.GetByteCount (value); + + EncodeTypeAndLength (buf, ref index, attr, (short) length); + encoding.GetBytes (value, 0, value.Length, buf, index); + index += length; + } + + static void EncodeTimestamp (byte[] buf, ref int index, NtlmAttribute attr, long value, short size) + { + EncodeTypeAndLength (buf, ref index, attr, size); + + switch (size) { + case 2: EncodeInt16 (buf, ref index, (short) (value & 0xffff)); break; + case 4: EncodeInt32 (buf, ref index, (int) (value & 0xffffffff)); break; + default: + EncodeInt32 (buf, ref index, (int) (value & 0xffffffff)); + EncodeInt32 (buf, ref index, (int) (value >> 32)); + break; + } + } + + static void EncodeFlags (byte[] buf, ref int index, NtlmAttribute attr, int value, short size) + { + EncodeTypeAndLength (buf, ref index, attr, size); + + switch (size) { + case 2: EncodeInt16 (buf, ref index, (short) value); break; + default: EncodeInt32 (buf, ref index, value); break; + } + } + + /// + /// Encode the TargetInfo structure. + /// + /// + /// Encodes the TargetInfo structure. + /// + /// true if the strings should be encoded in Unicode; otherwise, false. + /// The encoded TargetInfo. + public byte[] Encode (bool unicode) + { + var buf = new byte[CalculateSize (unicode)]; + int index = 0; + + foreach (var attribute in attributes) { + switch (attribute.Attribute) { + case NtlmAttribute.ServerName: + case NtlmAttribute.DomainName: + case NtlmAttribute.DnsServerName: + case NtlmAttribute.DnsDomainName: + case NtlmAttribute.DnsTreeName: + case NtlmAttribute.TargetName: + var str = (NtlmAttributeStringValuePair) attribute; + EncodeString (buf, ref index, str.Attribute, str.Value, unicode); + break; + case NtlmAttribute.Flags: + var flags = (NtlmAttributeFlagsValuePair) attribute; + EncodeFlags (buf, ref index, flags.Attribute, flags.Value, flags.Size); + break; + case NtlmAttribute.Timestamp: + var timestamp = (NtlmAttributeTimestampValuePair) attribute; + EncodeTimestamp (buf, ref index, timestamp.Attribute, timestamp.Value, timestamp.Size); + break; + default: + var generic = (NtlmAttributeByteArrayValuePair) attribute; + EncodeByteArray (buf, ref index, generic.Attribute, generic.Value); + break; + } + } + + return buf; + } + } +} diff --git a/MailKit/Security/Ntlm/NtlmUtils.cs b/MailKit/Security/Ntlm/NtlmUtils.cs new file mode 100644 index 0000000000..917f8d7197 --- /dev/null +++ b/MailKit/Security/Ntlm/NtlmUtils.cs @@ -0,0 +1,242 @@ +// +// NtlmUtils.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2021 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + +using System; +using System.Text; +using System.Security.Cryptography; + +using SSCMD5 = System.Security.Cryptography.MD5; + +namespace MailKit.Security.Ntlm { + static class NtlmUtils + { + internal static readonly byte[] ClientSealMagic = Encoding.ASCII.GetBytes ("session key to client-to-server sealing key magic constant"); + static readonly byte[] ServerSealMagic = Encoding.ASCII.GetBytes ("session key to server-to-client sealing key magic constant"); + static readonly byte[] ClientSignMagic = Encoding.ASCII.GetBytes ("session key to client-to-server signing key magic constant"); + static readonly byte[] ServerSignMagic = Encoding.ASCII.GetBytes ("session key to server-to-client signing key magic constant"); + static readonly byte[] SealKeySuffix40 = new byte[] { 0xe5, 0x38, 0xb0 }; + static readonly byte[] SealKeySuffix56 = new byte[] { 0xa0 }; + static readonly byte[] Responserversion = new byte[] { 1 }; + static readonly byte[] HiResponserversion = new byte[] { 1 }; + static readonly byte[] Z24 = new byte[24]; + static readonly byte[] Z6 = new byte[6]; + static readonly byte[] Z4 = new byte[4]; + static readonly byte[] Z1 = new byte[1]; + + public static byte[] ConcatenationOf (params string[] values) + { + var concatenatedValue = string.Concat (values); + + return Encoding.Unicode.GetBytes (concatenatedValue); + } + + public static byte[] ConcatenationOf (params byte[][] values) + { + int index = 0, length = 0; + + for (int i = 0; i < values.Length; i++) + length += values[i].Length; + + var concatenated = new byte[length]; + for (int i = 0; i < values.Length; i++) { + length = values[i].Length; + Buffer.BlockCopy (values[i], 0, concatenated, index, length); + index += length; + } + + return concatenated; + } + + static byte[] MD4 (byte[] buffer) + { + using (var md4 = new MD4 ()) { + var hash = md4.ComputeHash (buffer); + Array.Clear (buffer, 0, buffer.Length); + return hash; + } + } + + static byte[] MD4 (string password) + { + return MD4 (Encoding.Unicode.GetBytes (password)); + } + + public static byte[] MD5 (byte[] buffer) + { + using (var md5 = SSCMD5.Create ()) { + var hash = md5.ComputeHash (buffer); + Array.Clear (buffer, 0, buffer.Length); + return hash; + } + } + + public static byte[] HMACMD5 (byte[] key, params byte[][] values) + { + using (var md5 = new HMACMD5 (key)) { + int i; + + for (i = 0; i < values.Length - 1; i++) + md5.TransformBlock (values[i], 0, values[i].Length, null, 0); + + md5.TransformFinalBlock (values[i], 0, values[i].Length); + + return md5.Hash; + } + } + + public static byte[] NONCE (int size) + { + var nonce = new byte[size]; + + using (var rng = RandomNumberGenerator.Create ()) + rng.GetBytes (nonce); + + return nonce; + } + + public static byte[] RC4K (byte[] key, byte[] message) + { + try { + using (var rc4 = new RC4 ()) { + rc4.Key = key; + + return rc4.TransformFinalBlock (message, 0, message.Length); + } + } finally { + Array.Clear (key, 0, key.Length); + } + } + + public static byte[] SEALKEY (NtlmFlags flags, byte[] exportedSessionKey, bool client = true) + { + if ((flags & NtlmFlags.NegotiateExtendedSessionSecurity) != 0) { + byte[] subkey; + + if ((flags & NtlmFlags.Negotiate128) != 0) { + subkey = exportedSessionKey; + } else if ((flags & NtlmFlags.Negotiate56) != 0) { + subkey = new byte[7]; + Buffer.BlockCopy (exportedSessionKey, 0, subkey, 0, subkey.Length); + } else { + subkey = new byte[5]; + Buffer.BlockCopy (exportedSessionKey, 0, subkey, 0, subkey.Length); + } + + var magic = client ? ClientSealMagic : ServerSealMagic; + var sealKey = MD5 (ConcatenationOf (subkey, magic)); + + if (subkey != exportedSessionKey) + Array.Clear (subkey, 0, subkey.Length); + + return sealKey; + } else if ((flags & NtlmFlags.NegotiateLanManagerKey) != 0) { + byte[] suffix; + int length; + + if ((flags & NtlmFlags.Negotiate56) != 0) { + suffix = SealKeySuffix56; + length = 7; + } else { + suffix = SealKeySuffix40; + length = 5; + } + + var sealKey = new byte[length + suffix.Length]; + Buffer.BlockCopy (exportedSessionKey, 0, sealKey, 0, length); + Buffer.BlockCopy (suffix, 0, sealKey, length, suffix.Length); + + return sealKey; + } else { + return exportedSessionKey; + } + } + + public static byte[] SIGNKEY (NtlmFlags flags, byte[] exportedSessionKey, bool client = true) + { + if ((flags & NtlmFlags.NegotiateExtendedSessionSecurity) != 0) { + var magic = client ? ClientSignMagic : ServerSignMagic; + return MD5 (ConcatenationOf (exportedSessionKey, magic)); + } else { + return null; + } + } + + static byte[] NTOWFv2 (string domain, string userName, string password) + { + var hash = MD4 (password); + byte[] responseKey; + + using (var md5 = new HMACMD5 (hash)) { + var userDom = ConcatenationOf (userName.ToUpperInvariant (), domain); + responseKey = md5.ComputeHash (userDom); + } + + Array.Clear (hash, 0, hash.Length); + + return responseKey; + } + + public static void ComputeNtlmV2 (Type2Message type2, string domain, string userName, string password, byte[] targetInfo, byte[] clientChallenge, long? time, out byte[] ntChallengeResponse, out byte[] lmChallengeResponse, out byte[] sessionBaseKey) + { + if (userName.Length == 0 && password.Length == 0) { + // Special case for anonymous authentication + ntChallengeResponse = null; + lmChallengeResponse = Z1; + sessionBaseKey = null; + return; + } + + var timestamp = (time ?? DateTime.UtcNow.Ticks) - 504911232000000000; + var responseKey = NTOWFv2 (domain, userName, password); + + // Note: If NTLM v2 authentication is used, the client SHOULD send the timestamp in the CHALLENGE_MESSAGE. + if (type2.TargetInfo?.Timestamp != null) + timestamp = type2.TargetInfo.Timestamp.Value; + + var temp = ConcatenationOf (Responserversion, HiResponserversion, Z6, BitConverterLE.GetBytes (timestamp), clientChallenge, Z4, targetInfo, Z4); + var proof = HMACMD5 (responseKey, type2.ServerChallenge, temp); + + sessionBaseKey = HMACMD5 (responseKey, proof); + + ntChallengeResponse = ConcatenationOf (proof, temp); + Array.Clear (proof, 0, proof.Length); + Array.Clear (temp, 0, temp.Length); + + var hash = HMACMD5 (responseKey, type2.ServerChallenge, clientChallenge); + Array.Clear (responseKey, 0, responseKey.Length); + + // Note: If NTLM v2 authentication is used and the CHALLENGE_MESSAGE TargetInfo field (section 2.2.1.2) has an + // MsvAvTimestamp present, the client SHOULD NOT send the LmChallengeResponse and SHOULD send Z(24) instead. + if (type2.TargetInfo?.Timestamp == null) + lmChallengeResponse = ConcatenationOf (hash, clientChallenge); + else + lmChallengeResponse = Z24; + Array.Clear (hash, 0, hash.Length); + } + } +} diff --git a/MailKit/Security/Ntlm/RC4.cs b/MailKit/Security/Ntlm/RC4.cs new file mode 100644 index 0000000000..5ab1132935 --- /dev/null +++ b/MailKit/Security/Ntlm/RC4.cs @@ -0,0 +1,205 @@ +// +// ARC4Managed.cs: Alleged RC4(tm) compatible symmetric stream cipher +// RC4 is a trademark of RSA Security +// + +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Security.Cryptography; + +namespace MailKit.Security.Ntlm { + // References: + // a. Usenet 1994 - RC4 Algorithm revealed + // http://www.qrst.de/html/dsds/rc4.htm + + class RC4 : SymmetricAlgorithm, ICryptoTransform + { + byte[] key, state; + byte x, y; + bool disposed; + + public RC4 () : base () + { + state = new byte[256]; + KeySizeValue = 64; + } + + ~RC4 () + { + Dispose (false); + } + + public bool CanReuseTransform { + get { return false; } + } + + public bool CanTransformMultipleBlocks { + get { return true; } + } + + public int InputBlockSize { + get { return 1; } + } + + public int OutputBlockSize { + get { return 1; } + } + + public override byte[] Key { + get { + if (key == null) + throw new InvalidOperationException (); + + return (byte[]) key.Clone (); + } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (value.Length == 0) + throw new ArgumentException ("Invalid key length.", nameof (value)); + + KeySizeValue = value.Length << 3; + key = (byte[]) value.Clone (); + KeySetup (key); + } + } + + public override ICryptoTransform CreateEncryptor (byte[] rgbKey, byte[] rgvIV) + { + return new RC4 { Key = rgbKey }; + } + + public override ICryptoTransform CreateDecryptor (byte[] rgbKey, byte[] rgvIV) + { + return new RC4 { Key = rgbKey }; + } + + public override void GenerateIV () + { + // not used for a stream cipher + IV = new byte[0]; + } + + public override void GenerateKey () + { + key = new byte[KeySizeValue >> 3]; + RandomNumberGenerator.Create ().GetBytes (key); + KeySetup (key); + } + + void KeySetup (byte[] key) + { + byte index1 = 0; + byte index2 = 0; + + for (int counter = 0; counter < 256; counter++) + state[counter] = (byte) counter; + + x = y = 0; + + for (int counter = 0; counter < 256; counter++) { + index2 = (byte) (key[index1] + state[counter] + index2); + // swap byte + byte tmp = state[counter]; + state[counter] = state[index2]; + state[index2] = tmp; + index1 = (byte) ((index1 + 1) % key.Length); + } + } + + void CheckInput (byte[] inputBuffer, int inputOffset, int inputCount) + { + if (inputBuffer == null) + throw new ArgumentNullException (nameof (inputBuffer)); + + if (inputOffset < 0 || inputOffset > inputBuffer.Length) + throw new ArgumentOutOfRangeException (nameof (inputOffset)); + + if (inputCount < 0 || inputOffset > inputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException (nameof (inputCount)); + } + + public int TransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + CheckInput (inputBuffer, inputOffset, inputCount); + + // check output parameters + if (outputBuffer == null) + throw new ArgumentNullException (nameof (outputBuffer)); + + if (outputOffset < 0 || outputOffset > outputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException (nameof (outputOffset)); + + return InternalTransformBlock (inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset); + } + + int InternalTransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + byte xorIndex; + + for (int counter = 0; counter < inputCount; counter++) { + x = (byte) (x + 1); + y = (byte) (state[x] + y); + + // swap byte + byte tmp = state[x]; + state[x] = state[y]; + state[y] = tmp; + + xorIndex = (byte) (state[x] + state[y]); + outputBuffer[outputOffset + counter] = (byte) (inputBuffer[inputOffset + counter] ^ state[xorIndex]); + } + return inputCount; + } + + public byte[] TransformFinalBlock (byte[] inputBuffer, int inputOffset, int inputCount) + { + CheckInput (inputBuffer, inputOffset, inputCount); + + var output = new byte[inputCount]; + InternalTransformBlock (inputBuffer, inputOffset, inputCount, output, 0); + return output; + } + + protected override void Dispose (bool disposing) + { + if (disposed) + return; + + x = y = 0; + + if (key != null) + Array.Clear (key, 0, key.Length); + + Array.Clear (state, 0, state.Length); + + if (disposing) { + state = null; + key = null; + } + + disposed = true; + } + } +} diff --git a/MailKit/Security/Ntlm/TargetInfo.cs b/MailKit/Security/Ntlm/TargetInfo.cs deleted file mode 100644 index dbb2f9a8f4..0000000000 --- a/MailKit/Security/Ntlm/TargetInfo.cs +++ /dev/null @@ -1,288 +0,0 @@ -// -// TargetInfo.cs -// -// Author: Jeffrey Stedfast -// -// Copyright (c) 2013-2021 .NET Foundation and Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -using System; -using System.Text; - -namespace MailKit.Security.Ntlm { - class TargetInfo - { - public TargetInfo (byte[] buffer, int startIndex, int length, bool unicode) - { - Decode (buffer, startIndex, length, unicode); - } - - public TargetInfo () - { - } - - public int? Flags { - get; set; - } - - public byte[] ChannelBinding { - get; set; - } - - public string DomainName { - get; set; - } - - public string ServerName { - get; set; - } - - public string DnsDomainName { - get; set; - } - - public string DnsServerName { - get; set; - } - - public string DnsTreeName { - get; set; - } - - public string TargetName { - get; set; - } - - public long Timestamp { - get; set; - } - - static byte[] DecodeByteArray (byte[] buffer, ref int index) - { - var length = BitConverterLE.ToInt16 (buffer, index); - var value = new byte[length]; - - Buffer.BlockCopy (buffer, index + 2, value, 0, length); - - index += 2 + length; - - return value; - } - - static string DecodeString (byte[] buffer, ref int index, bool unicode) - { - var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; - var length = BitConverterLE.ToInt16 (buffer, index); - var value = encoding.GetString (buffer, index + 2, length); - - index += 2 + length; - - return value; - } - - static int DecodeFlags (byte[] buffer, ref int index) - { - short nbytes = BitConverterLE.ToInt16 (buffer, index); - int flags; - - index += 2; - - switch (nbytes) { - case 4: flags = BitConverterLE.ToInt32 (buffer, index); break; - case 2: flags = BitConverterLE.ToInt16 (buffer, index); break; - default: flags = 0; break; - } - - index += nbytes; - - return flags; - } - - static long DecodeTimestamp (byte[] buffer, ref int index) - { - short nbytes = BitConverterLE.ToInt16 (buffer, index); - long lo, hi; - - index += 2; - - switch (nbytes) { - case 8: - lo = BitConverterLE.ToUInt32 (buffer, index); - index += 4; - hi = BitConverterLE.ToUInt32 (buffer, index); - index += 4; - return (hi << 32) | lo; - case 4: - lo = BitConverterLE.ToUInt32 (buffer, index); - index += 4; - return lo; - case 2: - lo = BitConverterLE.ToUInt16 (buffer, index); - index += 2; - return lo; - default: - index += nbytes; - return 0; - } - } - - void Decode (byte[] buffer, int startIndex, int length, bool unicode) - { - int index = startIndex; - - do { - var type = BitConverterLE.ToInt16 (buffer, index); - - index += 2; - - switch (type) { - case 0: index = startIndex + length; break; // a 'type' of 0 terminates the TargetInfo - case 1: ServerName = DecodeString (buffer, ref index, unicode); break; - case 2: DomainName = DecodeString (buffer, ref index, unicode); break; - case 3: DnsServerName = DecodeString (buffer, ref index, unicode); break; - case 4: DnsDomainName = DecodeString (buffer, ref index, unicode); break; - case 5: DnsTreeName = DecodeString (buffer, ref index, unicode); break; - case 6: Flags = DecodeFlags (buffer, ref index); break; - case 7: Timestamp = DecodeTimestamp (buffer, ref index); break; - case 9: TargetName = DecodeString (buffer, ref index, unicode); break; - case 10: ChannelBinding = DecodeByteArray (buffer, ref index); break; - default: index += 2 + BitConverterLE.ToInt16 (buffer, index); break; - } - } while (index < startIndex + length); - } - - int CalculateSize (bool unicode) - { - var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; - int length = 4; - - if (!string.IsNullOrEmpty (DomainName)) - length += 4 + encoding.GetByteCount (DomainName); - - if (!string.IsNullOrEmpty (ServerName)) - length += 4 + encoding.GetByteCount (ServerName); - - if (!string.IsNullOrEmpty (DnsDomainName)) - length += 4 + encoding.GetByteCount (DnsDomainName); - - if (!string.IsNullOrEmpty (DnsServerName)) - length += 4 + encoding.GetByteCount (DnsServerName); - - if (!string.IsNullOrEmpty (DnsTreeName)) - length += 4 + encoding.GetByteCount (DnsTreeName); - - if (Flags.HasValue) - length += 8; - - if (Timestamp != 0) - length += 12; - - if (!string.IsNullOrEmpty (TargetName)) - length += 4 + encoding.GetByteCount (TargetName); - - if (ChannelBinding != null && ChannelBinding.Length > 0) - length += 4 + ChannelBinding.Length; - - return length; - } - - static void EncodeTypeAndLength (byte[] buf, ref int index, short type, short length) - { - buf[index++] = (byte) (type); - buf[index++] = (byte) (type >> 8); - buf[index++] = (byte) (length); - buf[index++] = (byte) (length >> 8); - } - - static void EncodeByteArray (byte[] buf, ref int index, short type, byte[] value) - { - EncodeTypeAndLength (buf, ref index, type, (short) value.Length); - Buffer.BlockCopy (value, 0, buf, index, value.Length); - index += value.Length; - } - - static void EncodeString (byte[] buf, ref int index, short type, string value, bool unicode) - { - var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; - int length = encoding.GetByteCount (value); - - EncodeTypeAndLength (buf, ref index, type, (short) length); - encoding.GetBytes (value, 0, value.Length, buf, index); - index += length; - } - - static void EncodeInt32 (byte[] buf, ref int index, int value) - { - buf[index++] = (byte) (value); - buf[index++] = (byte) (value >> 8); - buf[index++] = (byte) (value >> 16); - buf[index++] = (byte) (value >> 24); - } - - static void EncodeTimestamp (byte[] buf, ref int index, short type, long value) - { - EncodeTypeAndLength (buf, ref index, type, 8); - EncodeInt32 (buf, ref index, (int) (value & 0xffffffff)); - EncodeInt32 (buf, ref index, (int) (value >> 32)); - } - - static void EncodeFlags (byte[] buf, ref int index, short type, int value) - { - EncodeTypeAndLength (buf, ref index, type, 4); - EncodeInt32 (buf, ref index, value); - } - - public byte[] Encode (bool unicode) - { - var buf = new byte[CalculateSize (unicode)]; - int index = 0; - - if (!string.IsNullOrEmpty (DomainName)) - EncodeString (buf, ref index, 2, DomainName, unicode); - - if (!string.IsNullOrEmpty (ServerName)) - EncodeString (buf, ref index, 1, ServerName, unicode); - - if (!string.IsNullOrEmpty (DnsDomainName)) - EncodeString (buf, ref index, 4, DnsDomainName, unicode); - - if (!string.IsNullOrEmpty (DnsServerName)) - EncodeString (buf, ref index, 3, DnsServerName, unicode); - - if (!string.IsNullOrEmpty (DnsTreeName)) - EncodeString (buf, ref index, 5, DnsTreeName, unicode); - - if (Flags.HasValue) - EncodeFlags (buf, ref index, 6, Flags.Value); - - if (Timestamp != 0) - EncodeTimestamp (buf, ref index, 7, Timestamp); - - if (!string.IsNullOrEmpty (TargetName)) - EncodeString (buf, ref index, 9, TargetName, unicode); - - if (ChannelBinding != null && ChannelBinding.Length > 0) - EncodeByteArray (buf, ref index, 10, ChannelBinding); - - return buf; - } - } -} diff --git a/MailKit/Security/Ntlm/Type1Message.cs b/MailKit/Security/Ntlm/Type1Message.cs index 27af57fb2d..debacce6b0 100644 --- a/MailKit/Security/Ntlm/Type1Message.cs +++ b/MailKit/Security/Ntlm/Type1Message.cs @@ -1,92 +1,89 @@ // -// Mono.Security.Protocol.Ntlm.Type1Message - Negotiation +// Type1Message.cs // -// Authors: Sebastien Pouliot -// Jeffrey Stedfast +// Author: Jeffrey Stedfast // -// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) -// Copyright (c) 2004 Novell, Inc (http://www.novell.com) // Copyright (c) 2013-2021 .NET Foundation and Contributors // -// References -// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär -// http://www.innovation.ch/java/ntlm.html -// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass -// http://davenport.sourceforge.net/ntlm.html +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: // -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. // +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 using System; using System.Text; namespace MailKit.Security.Ntlm { - class Type1Message : MessageBase + class Type1Message : NtlmMessageBase { - internal static readonly NtlmFlags DefaultFlags = NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateOem | NtlmFlags.NegotiateUnicode | NtlmFlags.RequestTarget; + // System.Net.Mail seems to default to: NtlmFlags.Negotiate56 | NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateOem | NtlmFlags.RequestTarget | NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateAlwaysSign | NtlmFlags.NegotiateExtendedSessionSecurity | NtlmFlags.NegotiateVersion | NtlmFlags.Negotiate128 + internal static readonly NtlmFlags DefaultFlags = NtlmFlags.Negotiate56 | NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateOem | NtlmFlags.RequestTarget | NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateAlwaysSign | NtlmFlags.NegotiateExtendedSessionSecurity | NtlmFlags.Negotiate128; - string workstation; - string domain; + byte[] cached; - public Type1Message (string workstation, string domainName, Version osVersion) : base (1) + public Type1Message (NtlmFlags flags, string domain, string workstation, Version osVersion = null) : base (1) { - Flags = DefaultFlags; - Workstation = workstation; - OSVersion = osVersion; - Domain = domainName; + Flags = flags & ~(NtlmFlags.NegotiateDomainSupplied | NtlmFlags.NegotiateWorkstationSupplied | NtlmFlags.NegotiateVersion); - if (osVersion != null) + // Note: If the NTLMSSP_NEGOTIATE_VERSION flag is set by the client application, the Version field + // MUST be set to the current version (section 2.2.2.10), the DomainName field MUST be set to + // a zero-length string, and the Workstation field MUST be set to a zero-length string. + if (osVersion != null) { Flags |= NtlmFlags.NegotiateVersion; + Workstation = string.Empty; + Domain = string.Empty; + OSVersion = osVersion; + } else { + if (!string.IsNullOrEmpty (workstation)) { + Flags |= NtlmFlags.NegotiateWorkstationSupplied; + Workstation = workstation.ToUpperInvariant (); + } else { + Workstation = string.Empty; + } + + if (!string.IsNullOrEmpty (domain)) { + Flags |= NtlmFlags.NegotiateDomainSupplied; + Domain = domain.ToUpperInvariant (); + } else { + Domain = string.Empty; + } + } + } + + public Type1Message (string domain = null, string workstation = null, Version osVersion = null) : this (DefaultFlags, domain, workstation, osVersion) + { } public Type1Message (byte[] message, int startIndex, int length) : base (1) { Decode (message, startIndex, length); + + cached = new byte[length]; + Buffer.BlockCopy (message, startIndex, cached, 0, length); } public string Domain { - get { return domain; } - set { - if (string.IsNullOrEmpty (value)) { - Flags &= ~NtlmFlags.NegotiateDomainSupplied; - value = string.Empty; - } else { - Flags |= NtlmFlags.NegotiateDomainSupplied; - } - - domain = value; - } + get; private set; } public string Workstation { - get { return workstation; } - set { - if (string.IsNullOrEmpty (value)) { - Flags &= ~NtlmFlags.NegotiateWorkstationSupplied; - value = string.Empty; - } else { - Flags |= NtlmFlags.NegotiateWorkstationSupplied; - } - - workstation = value; - } + get; private set; } void Decode (byte[] message, int startIndex, int length) @@ -98,12 +95,12 @@ void Decode (byte[] message, int startIndex, int length) // decode the domain var domainLength = BitConverterLE.ToUInt16 (message, startIndex + 16); var domainOffset = BitConverterLE.ToUInt16 (message, startIndex + 20); - domain = Encoding.UTF8.GetString (message, startIndex + domainOffset, domainLength); + Domain = Encoding.UTF8.GetString (message, startIndex + domainOffset, domainLength); // decode the workstation/host var workstationLength = BitConverterLE.ToUInt16 (message, startIndex + 24); var workstationOffset = BitConverterLE.ToUInt16 (message, startIndex + 28); - workstation = Encoding.UTF8.GetString (message, startIndex + workstationOffset, workstationLength); + Workstation = Encoding.UTF8.GetString (message, startIndex + workstationOffset, workstationLength); if ((Flags & NtlmFlags.NegotiateVersion) != 0 && length >= 40) { // decode the OS Version @@ -117,17 +114,17 @@ void Decode (byte[] message, int startIndex, int length) public override byte[] Encode () { - bool negotiateVersion; - int versionLength = 0; - - if (negotiateVersion = (Flags & NtlmFlags.NegotiateVersion) != 0) - versionLength = 8; + if (cached != null) + return cached; + var negotiateVersion = (Flags & NtlmFlags.NegotiateVersion) != 0; + var workstation = Encoding.UTF8.GetBytes (Workstation); + var domain = Encoding.UTF8.GetBytes (Domain); + int versionLength = negotiateVersion ? 8 : 0; int workstationOffset = 32 + versionLength; int domainOffset = workstationOffset + workstation.Length; var message = PrepareMessage (32 + domain.Length + workstation.Length + versionLength); - byte[] buffer; message[12] = (byte) Flags; message[13] = (byte)((uint) Flags >> 8); @@ -159,11 +156,10 @@ public override byte[] Encode () message[39] = 0x0f; } - buffer = Encoding.UTF8.GetBytes (workstation.ToUpperInvariant ()); - Buffer.BlockCopy (buffer, 0, message, workstationOffset, buffer.Length); + Buffer.BlockCopy (workstation, 0, message, workstationOffset, workstation.Length); + Buffer.BlockCopy (domain, 0, message, domainOffset, domain.Length); - buffer = Encoding.UTF8.GetBytes (domain.ToUpperInvariant ()); - Buffer.BlockCopy (buffer, 0, message, domainOffset, buffer.Length); + cached = message; return message; } diff --git a/MailKit/Security/Ntlm/Type2Message.cs b/MailKit/Security/Ntlm/Type2Message.cs index eeaecc72f2..a270ce077d 100644 --- a/MailKit/Security/Ntlm/Type2Message.cs +++ b/MailKit/Security/Ntlm/Type2Message.cs @@ -1,85 +1,78 @@ // -// Mono.Security.Protocol.Ntlm.Type2Message - Challenge +// Type2Message.cs // -// Authors: Sebastien Pouliot -// Jeffrey Stedfast +// Author: Jeffrey Stedfast // -// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) -// Copyright (c) 2004 Novell, Inc (http://www.novell.com) // Copyright (c) 2013-2021 .NET Foundation and Contributors // -// References -// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär -// http://www.innovation.ch/java/ntlm.html -// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass -// http://davenport.sourceforge.net/ntlm.html +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: // -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. // +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 using System; using System.Text; -using System.Security.Cryptography; namespace MailKit.Security.Ntlm { - class Type2Message : MessageBase + class Type2Message : NtlmMessageBase { - byte[] targetInfo; - byte[] nonce; + static readonly NtlmFlags DefaultFlags = NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateUnicode /*| NtlmFlags.NegotiateAlwaysSign*/; + byte[] serverChallenge; + byte[] cached; - public Type2Message () : base (2) + public Type2Message (NtlmFlags flags, Version osVersion = null) : base (2) { - Flags = NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateUnicode /*| NtlmFlags.NegotiateAlwaysSign*/; - nonce = new byte[8]; - - using (var rng = RandomNumberGenerator.Create ()) - rng.GetBytes (nonce); + serverChallenge = NtlmUtils.NONCE (8); + OSVersion = osVersion; + Flags = flags; } - public Type2Message (Version osVersion) : this () + public Type2Message (Version osVersion = null) : this (DefaultFlags, osVersion) { - OSVersion = osVersion; } public Type2Message (byte[] message, int startIndex, int length) : base (2) { - nonce = new byte[8]; + serverChallenge = new byte[8]; Decode (message, startIndex, length); + + cached = new byte[length]; + Buffer.BlockCopy (message, startIndex, cached, 0, length); } ~Type2Message () { - if (nonce != null) - Array.Clear (nonce, 0, nonce.Length); + if (serverChallenge != null) + Array.Clear (serverChallenge, 0, serverChallenge.Length); } - public byte[] Nonce { - get { return (byte[]) nonce.Clone (); } + public byte[] ServerChallenge { + get { return serverChallenge; } set { if (value == null) throw new ArgumentNullException (nameof (value)); if (value.Length != 8) - throw new ArgumentException ("Invalid Nonce Length (should be 8 bytes).", nameof (value)); + throw new ArgumentException ("Invalid nonce length (should be 8 bytes).", nameof (value)); - nonce = (byte[]) value.Clone (); + Array.Clear (serverChallenge, 0, serverChallenge.Length); + serverChallenge = value; } } @@ -87,17 +80,13 @@ public Type2Message (byte[] message, int startIndex, int length) : base (2) get; set; } - public TargetInfo TargetInfo { + public NtlmTargetInfo TargetInfo { get; set; } - public byte[] EncodedTargetInfo { - get { - if (targetInfo != null) - return (byte[]) targetInfo.Clone (); - - return new byte[0]; - } + public byte[] GetEncodedTargetInfo () + { + return TargetInfo?.Encode ((Flags & NtlmFlags.NegotiateUnicode) != 0); } void Decode (byte[] message, int startIndex, int length) @@ -106,7 +95,7 @@ void Decode (byte[] message, int startIndex, int length) Flags = (NtlmFlags) BitConverterLE.ToUInt32 (message, startIndex + 20); - Buffer.BlockCopy (message, startIndex + 24, nonce, 0, 8); + Buffer.BlockCopy (message, startIndex + 24, serverChallenge, 0, 8); var targetNameLength = BitConverterLE.ToUInt16 (message, startIndex + 12); var targetNameOffset = BitConverterLE.ToUInt16 (message, startIndex + 16); @@ -131,29 +120,23 @@ void Decode (byte[] message, int startIndex, int length) var targetInfoLength = BitConverterLE.ToUInt16 (message, startIndex + 40); var targetInfoOffset = BitConverterLE.ToUInt16 (message, startIndex + 44); - if (targetInfoLength > 0 && targetInfoOffset < length && targetInfoLength <= (length - targetInfoOffset)) { - TargetInfo = new TargetInfo (message, startIndex + targetInfoOffset, targetInfoLength, (Flags & NtlmFlags.NegotiateOem) == 0); - - targetInfo = new byte[targetInfoLength]; - Buffer.BlockCopy (message, startIndex + targetInfoOffset, targetInfo, 0, targetInfoLength); - } + if (targetInfoLength > 0 && targetInfoOffset < length && targetInfoLength <= (length - targetInfoOffset)) + TargetInfo = new NtlmTargetInfo (message, startIndex + targetInfoOffset, targetInfoLength, (Flags & NtlmFlags.NegotiateUnicode) != 0); } } public override byte[] Encode () { + if (cached != null) + return cached; + + var targetInfo = GetEncodedTargetInfo (); int targetNameOffset = 40; int targetInfoOffset = 48; byte[] targetName = null; bool negotiateVersion; int size = 40; - if (negotiateVersion = (Flags & NtlmFlags.NegotiateVersion) != 0) { - targetNameOffset += 16; - targetInfoOffset += 16; - size += 16; - } - if (TargetName != null) { var encoding = (Flags & NtlmFlags.NegotiateUnicode) != 0 ? Encoding.Unicode : Encoding.UTF8; @@ -162,27 +145,21 @@ public override byte[] Encode () size += targetName.Length; } - if (TargetInfo != null || targetInfo != null) { - if (targetInfo == null) - targetInfo = TargetInfo.Encode ((Flags & NtlmFlags.NegotiateUnicode) != 0); - size += targetInfo.Length + 8; + if (targetInfo != null) { + size += 8 + targetInfo.Length; targetNameOffset += 8; } - var message = PrepareMessage (size); - - // message length - message[16] = (byte) size; - message[17] = (byte)(size >> 8); - - // flags - message[20] = (byte) Flags; - message[21] = (byte)((uint) Flags >> 8); - message[22] = (byte)((uint) Flags >> 16); - message[23] = (byte)((uint) Flags >> 24); + if (negotiateVersion = (Flags & NtlmFlags.NegotiateVersion) != 0) { + targetNameOffset += 8; + targetInfoOffset += 8; + size += 8; + } - Buffer.BlockCopy (nonce, 0, message, 24, nonce.Length); + // 12 bytes + var message = PrepareMessage (size); + // TargetName (8 bytes) if (targetName != null) { message[12] = (byte) targetName.Length; message[13] = (byte)(targetName.Length >> 8); @@ -190,10 +167,25 @@ public override byte[] Encode () message[15] = (byte)(targetName.Length >> 8); message[16] = (byte) targetNameOffset; message[17] = (byte)(targetNameOffset >> 8); + //message[18] = (byte) (targetNameOffset >> 16); + //message[19] = (byte) (targetNameOffset >> 24); + // TargetName Payload Buffer.BlockCopy (targetName, 0, message, targetNameOffset, targetName.Length); } + // NegotiateFlags (4 bytes) + message[20] = (byte) Flags; + message[21] = (byte) ((uint) Flags >> 8); + message[22] = (byte) ((uint) Flags >> 16); + message[23] = (byte) ((uint) Flags >> 24); + + // ServerChallenge (8 bytes) + Buffer.BlockCopy (serverChallenge, 0, message, 24, serverChallenge.Length); + + // Reserved (8 bytes) + + // TargetInfo (8 bytes) if (targetInfo != null) { message[40] = (byte) targetInfo.Length; message[41] = (byte)(targetInfo.Length >> 8); @@ -202,6 +194,7 @@ public override byte[] Encode () message[44] = (byte) targetInfoOffset; message[45] = (byte)(targetInfoOffset >> 8); + // TargetInfo Payload Buffer.BlockCopy (targetInfo, 0, message, targetInfoOffset, targetInfo.Length); } @@ -216,6 +209,8 @@ public override byte[] Encode () message[55] = 0x0f; } + cached = message; + return message; } } diff --git a/MailKit/Security/Ntlm/Type3Message.cs b/MailKit/Security/Ntlm/Type3Message.cs index 1a8fff0879..909e4794b6 100644 --- a/MailKit/Security/Ntlm/Type3Message.cs +++ b/MailKit/Security/Ntlm/Type3Message.cs @@ -1,133 +1,190 @@ // -// Mono.Security.Protocol.Ntlm.Type3Message - Authentication +// Type3Message.cs // -// Authors: Sebastien Pouliot -// Jeffrey Stedfast +// Author: Jeffrey Stedfast // -// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) -// Copyright (c) 2004 Novell, Inc (http://www.novell.com) // Copyright (c) 2013-2021 .NET Foundation and Contributors // -// References -// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär -// http://www.innovation.ch/java/ntlm.html -// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass -// http://davenport.sourceforge.net/ntlm.html +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: // -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. // +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 using System; using System.Text; namespace MailKit.Security.Ntlm { - class Type3Message : MessageBase + class Type3Message : NtlmMessageBase { + static readonly byte[] Z16 = new byte[16]; + + readonly Type1Message type1; readonly Type2Message type2; - readonly byte[] challenge; + byte[] clientChallenge; - public Type3Message (byte[] message, int startIndex, int length) : base (3) + public Type3Message (Type1Message type1, Type2Message type2, string userName, string password, string workstation) : base (3) { - Decode (message, startIndex, length); - type2 = null; - } + if (type1 == null) + throw new ArgumentNullException (nameof (type1)); - public Type3Message (Type2Message type2, Version osVersion, NtlmAuthLevel level, string userName, string password, string workstation) : base (3) - { + if (type2 == null) + throw new ArgumentNullException (nameof (type2)); + + if (userName == null) + throw new ArgumentNullException (nameof (userName)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + clientChallenge = NtlmUtils.NONCE (8); + this.type1 = type1; this.type2 = type2; - challenge = type2.Nonce; - Domain = type2.TargetName; - OSVersion = osVersion; - Username = userName; - Password = password; - Level = level; - Workstation = workstation; - Flags = 0; + if ((type2.Flags & NtlmFlags.TargetTypeDomain) != 0) { + // The server is domain-joined, so the TargetName will be the domain. + Domain = type2.TargetName; + } else { + // The server is not domain-joined, so the TargetName will be the machine name of the server. + Domain = type2.TargetInfo?.DomainName; - if (osVersion != null) - Flags |= NtlmFlags.NegotiateVersion; + // TODO: throw if TargetInfo is null? + } - if ((type2.Flags & NtlmFlags.NegotiateUnicode) != 0) - Flags |= NtlmFlags.NegotiateUnicode; - else - Flags |= NtlmFlags.NegotiateOem; + Workstation = workstation; + UserName = userName; + Password = password; - if ((type2.Flags & NtlmFlags.NegotiateNtlm) != 0) - Flags |= NtlmFlags.NegotiateNtlm; + // Use only the features supported by both the client and server. + Flags = type1.Flags & type2.Flags; - if ((type2.Flags & NtlmFlags.NegotiateNtlm2Key) != 0) - Flags |= NtlmFlags.NegotiateNtlm2Key; + // If the client and server both support NEGOTIATE_UNICODE, disable NEGOTIATE_OEM. + if ((Flags & NtlmFlags.NegotiateUnicode) != 0) + Flags &= ~NtlmFlags.NegotiateOem; + // TODO: throw if Unicode && Oem are both unset? - if ((type2.Flags & NtlmFlags.NegotiateTargetInfo) != 0) - Flags |= NtlmFlags.NegotiateTargetInfo; + // If the client and server both support NEGOTIATE_EXTENDED_SESSIONSECURITY, disable NEGOTIATE_LM_KEY. + if ((Flags & NtlmFlags.NegotiateExtendedSessionSecurity) != 0) + Flags &= ~NtlmFlags.NegotiateLanManagerKey; - if ((type2.Flags & NtlmFlags.RequestTarget) != 0) + // Disable NEGOTIATE_KEY_EXCHANGE if neither NEGOTIATE_SIGN nor NEGOTIATE_SEAL are also present. + if ((Flags & NtlmFlags.NegotiateKeyExchange) != 0 && (Flags & (NtlmFlags.NegotiateSign | NtlmFlags.NegotiateSeal)) == 0) + Flags &= ~NtlmFlags.NegotiateKeyExchange; + + // If we had RequestTarget in our initial NEGOTIATE_MESSAGE, include it again in this message(?) + if ((type1.Flags & NtlmFlags.RequestTarget) != 0) Flags |= NtlmFlags.RequestTarget; + + // If NEGOTIATE_VERSION is set, grab the OSVersion from our original negotiate message. + if ((Flags & NtlmFlags.NegotiateVersion) != 0) + OSVersion = type1.OSVersion ?? OSVersion; + } + + public Type3Message (byte[] message, int startIndex, int length) : base (3) + { + Decode (message, startIndex, length); + type2 = null; } ~Type3Message () { - if (challenge != null) - Array.Clear (challenge, 0, challenge.Length); + if (clientChallenge != null) + Array.Clear (clientChallenge, 0, clientChallenge.Length); + + if (LmChallengeResponse != null) + Array.Clear (LmChallengeResponse, 0, LmChallengeResponse.Length); + + if (NtChallengeResponse != null) + Array.Clear (NtChallengeResponse, 0, NtChallengeResponse.Length); + + if (ExportedSessionKey != null) + Array.Clear (ExportedSessionKey, 0, ExportedSessionKey.Length); - if (LM != null) - Array.Clear (LM, 0, LM.Length); + if (EncryptedRandomSessionKey != null) + Array.Clear (EncryptedRandomSessionKey, 0, EncryptedRandomSessionKey.Length); + } + + /// + /// This is only used for unit testing purposes. + /// + internal byte[] ClientChallenge { + get { return clientChallenge; } + set { + if (value == null) + return; + + if (value.Length != 8) + throw new ArgumentException ("Invalid nonce length (should be 8 bytes).", nameof (value)); - if (NT != null) - Array.Clear (NT, 0, NT.Length); + Array.Clear (clientChallenge, 0, clientChallenge.Length); + clientChallenge = value; + } } - public NtlmAuthLevel Level { + /// + /// This is only used for unit testing purposes. + /// + internal long? Timestamp { get; set; } public string Domain { - get; set; + get; private set; } public string Workstation { - get; set; + get; private set; } public string Password { - get; set; + get; private set; } - public string Username { - get; set; + public string UserName { + get; private set; } - public byte[] LM { + public byte[] Mic { get; private set; } - public byte[] NT { - get; set; + public byte[] LmChallengeResponse { + get; private set; + } + + public byte[] NtChallengeResponse { + get; private set; + } + + public byte[] ExportedSessionKey { + get; private set; + } + + public byte[] EncryptedRandomSessionKey { + get; private set; } void Decode (byte[] message, int startIndex, int length) { - ValidateArguments (message, startIndex, length); + int payloadOffset = length; + int micOffset = 64; - Password = null; + ValidateArguments (message, startIndex, length); if (message.Length >= 64) Flags = (NtlmFlags) BitConverterLE.ToUInt32 (message, startIndex + 60); @@ -136,29 +193,36 @@ void Decode (byte[] message, int startIndex, int length) int lmLength = BitConverterLE.ToUInt16 (message, startIndex + 12); int lmOffset = BitConverterLE.ToUInt16 (message, startIndex + 16); - LM = new byte[lmLength]; - Buffer.BlockCopy (message, startIndex + lmOffset, LM, 0, lmLength); + LmChallengeResponse = new byte[lmLength]; + Buffer.BlockCopy (message, startIndex + lmOffset, LmChallengeResponse, 0, lmLength); + payloadOffset = Math.Min (payloadOffset, lmOffset); int ntLength = BitConverterLE.ToUInt16 (message, startIndex + 20); int ntOffset = BitConverterLE.ToUInt16 (message, startIndex + 24); - NT = new byte[ntLength]; - Buffer.BlockCopy (message, startIndex + ntOffset, NT, 0, ntLength); + NtChallengeResponse = new byte[ntLength]; + Buffer.BlockCopy (message, startIndex + ntOffset, NtChallengeResponse, 0, ntLength); + payloadOffset = Math.Min (payloadOffset, ntOffset); int domainLength = BitConverterLE.ToUInt16 (message, startIndex + 28); int domainOffset = BitConverterLE.ToUInt16 (message, startIndex + 32); Domain = DecodeString (message, startIndex + domainOffset, domainLength); + payloadOffset = Math.Min (payloadOffset, domainOffset); int userLength = BitConverterLE.ToUInt16 (message, startIndex + 36); int userOffset = BitConverterLE.ToUInt16 (message, startIndex + 40); - Username = DecodeString (message, startIndex + userOffset, userLength); + UserName = DecodeString (message, startIndex + userOffset, userLength); + payloadOffset = Math.Min (payloadOffset, userOffset); - int hostLength = BitConverterLE.ToUInt16 (message, startIndex + 44); - int hostOffset = BitConverterLE.ToUInt16 (message, startIndex + 48); - Workstation = DecodeString (message, startIndex + hostOffset, hostLength); + int workstationLength = BitConverterLE.ToUInt16 (message, startIndex + 44); + int workstationOffset = BitConverterLE.ToUInt16 (message, startIndex + 48); + Workstation = DecodeString (message, startIndex + workstationOffset, workstationLength); + payloadOffset = Math.Min (payloadOffset, workstationOffset); - // Session key. We don't use it yet. - //int skeyLength = BitConverterLE.ToUInt16 (message, startIndex + 52); - //int skeyOffset = BitConverterLE.ToUInt16 (message, startIndex + 56); + int skeyLength = BitConverterLE.ToUInt16 (message, startIndex + 52); + int skeyOffset = BitConverterLE.ToUInt16 (message, startIndex + 56); + EncryptedRandomSessionKey = new byte[skeyLength]; + Buffer.BlockCopy (message, startIndex + skeyOffset, EncryptedRandomSessionKey, 0, skeyLength); + payloadOffset = Math.Min (payloadOffset, skeyOffset); // OSVersion if ((Flags & NtlmFlags.NegotiateVersion) != 0 && length >= 72) { @@ -168,6 +232,13 @@ void Decode (byte[] message, int startIndex, int length) int build = BitConverterLE.ToUInt16 (message, startIndex + 66); OSVersion = new Version (major, minor, build); + micOffset += 8; + } + + // MIC + if (micOffset + 16 <= payloadOffset) { + Mic = new byte[16]; + Buffer.BlockCopy (message, startIndex + micOffset, Mic, 0, Mic.Length); } } @@ -188,26 +259,94 @@ byte[] EncodeString (string text) return encoding.GetBytes (text); } + public void ComputeNtlmV2 (string targetName, bool unverifiedTargetName, byte[] channelBinding) + { + var targetInfo = new NtlmTargetInfo (); + int avFlags = 0; + + // If the CHALLENGE_MESSAGE contains a TargetInfo field + if (type2.TargetInfo != null) { + type2.TargetInfo.CopyTo (targetInfo); + + if (targetInfo.Flags.HasValue) + avFlags = targetInfo.Flags.Value; + + // If the CHALLENGE_MESSAGE TargetInfo field (section 2.2.1.2) has an MsvAvTimestamp present, the client SHOULD provide a MIC. + if (type2.TargetInfo?.Timestamp != null) { + // If there is an AV_PAIR structure (section 2.2.2.1) with the AvId field set to MsvAvFlags, then in the Value field, set bit 0x2 to 1. + // Else add an AV_PAIR structure and set the AvId field to MsvAvFlags and the Value field bit 0x2 to 1. + targetInfo.Flags = avFlags |= 0x2; + } + + // If ClientSuppliedTargetName (section 3.1.1.2) is not NULL + if (targetName != null) { + // If UnverifiedTargetName (section 3.1.1.2) is TRUE, then in AvId field = MsvAvFlags set 0x00000004 bit. + if (unverifiedTargetName) + targetInfo.Flags = avFlags |= 0x4; + + // Add an AV_PAIR structure and set the AvId field to MsvAvTargetName and the Value field to ClientSuppliedTargetName without + // terminating NULL. + targetInfo.TargetName = targetName; + } else { + // Else add an AV_PAIR structure and set the AvId field to MsvAvTargetName and the Value field to an empty string without terminating NULL. + targetInfo.TargetName = string.Empty; + } + + // The client SHOULD send the channel binding AV_PAIR: + // If the ClientChannelBindingsUnhashed (section 3.1.1.2) is not NULL + if (channelBinding != null) { + // Add an AV_PAIR structure and set the AvId field to MsvAvChannelBindings and the Value field to MD5_HASH(ClientChannelBindingsUnhashed). + targetInfo.ChannelBinding = NtlmUtils.MD5 (channelBinding); + } else { + // Else add an AV_PAIR structure and set the AvId field to MsvAvChannelBindings and the Value field to Z(16). + targetInfo.ChannelBinding = Z16; + } + } + + var encodedTargetInfo = targetInfo.Encode ((Flags & NtlmFlags.NegotiateUnicode) != 0); + + // Note: For NTLMv2, the sessionBaseKey is the same as the keyExchangeKey. + NtlmUtils.ComputeNtlmV2 (type2, Domain, UserName, Password, encodedTargetInfo, clientChallenge, Timestamp, out var ntChallengeResponse, out var lmChallengeResponse, out var keyExchangeKey); + + NtChallengeResponse = ntChallengeResponse; + LmChallengeResponse = lmChallengeResponse; + + if ((Flags & NtlmFlags.NegotiateKeyExchange) != 0 && (Flags & (NtlmFlags.NegotiateSign | NtlmFlags.NegotiateSeal)) != 0) { + ExportedSessionKey = NtlmUtils.NONCE (16); + EncryptedRandomSessionKey = NtlmUtils.RC4K (keyExchangeKey, ExportedSessionKey); + } else { + ExportedSessionKey = keyExchangeKey; + EncryptedRandomSessionKey = null; + } + + // If the CHALLENGE_MESSAGE TargetInfo field (section 2.2.1.2) has an MsvAvTimestamp present, the client SHOULD provide a MIC. + if ((avFlags & 0x2) != 0) + Mic = NtlmUtils.HMACMD5 (ExportedSessionKey, NtlmUtils.ConcatenationOf (type1.Encode (), type2.Encode (), Encode ())); + } + public override byte[] Encode () { var target = EncodeString (Domain); - var user = EncodeString (Username); + var user = EncodeString (UserName); var workstation = EncodeString (Workstation); - var payloadOffset = 64; + int payloadOffset = 64, micOffset = -1; bool negotiateVersion; - byte[] lm, ntlm; - - ChallengeResponse2.Compute (type2, Level, Username, Password, Domain, out lm, out ntlm); if (negotiateVersion = ((type2.Flags & NtlmFlags.NegotiateVersion) != 0 && OSVersion != null)) payloadOffset += 8; - var lmResponseLength = lm != null ? lm.Length : 0; - var ntResponseLength = ntlm != null ? ntlm.Length : 0; + if (Mic != null) { + micOffset = payloadOffset; + payloadOffset += Mic.Length; + } - var message = PrepareMessage (payloadOffset + target.Length + user.Length + workstation.Length + lmResponseLength + ntResponseLength); + var lmResponseLength = LmChallengeResponse != null ? LmChallengeResponse.Length : 0; + var ntResponseLength = NtChallengeResponse != null ? NtChallengeResponse.Length : 0; + int skeyLength = EncryptedRandomSessionKey != null ? EncryptedRandomSessionKey.Length : 0; - // LM response + var message = PrepareMessage (payloadOffset + target.Length + user.Length + workstation.Length + lmResponseLength + ntResponseLength + skeyLength); + + // LmChallengeResponse short lmResponseOffset = (short) (payloadOffset + target.Length + user.Length + workstation.Length); message[12] = (byte) lmResponseLength; message[13] = (byte) 0x00; @@ -215,8 +354,10 @@ public override byte[] Encode () message[15] = message[13]; message[16] = (byte) lmResponseOffset; message[17] = (byte) (lmResponseOffset >> 8); + //message[18] = (byte) (lmResponseOffset >> 16); + //message[19] = (byte) (lmResponseOffset >> 24); - // NT response + // NtChallengeResponse short ntResponseOffset = (short) (lmResponseOffset + lmResponseLength); message[20] = (byte) ntResponseLength; message[21] = (byte) (ntResponseLength >> 8); @@ -224,8 +365,10 @@ public override byte[] Encode () message[23] = message[21]; message[24] = (byte) ntResponseOffset; message[25] = (byte) (ntResponseOffset >> 8); + //message[26] = (byte) (ntResponseOffset >> 16); + //message[27] = (byte) (ntResponseOffset >> 24); - // target + // Target short domainLength = (short) target.Length; short domainOffset = (short) payloadOffset; message[28] = (byte) domainLength; @@ -234,8 +377,10 @@ public override byte[] Encode () message[31] = message[29]; message[32] = (byte) domainOffset; message[33] = (byte) (domainOffset >> 8); + //message[34] = (byte) (domainOffset >> 16); + //message[35] = (byte) (domainOffset >> 24); - // username + // UserName short userLength = (short) user.Length; short userOffset = (short) (domainOffset + domainLength); message[36] = (byte) userLength; @@ -244,8 +389,10 @@ public override byte[] Encode () message[39] = message[37]; message[40] = (byte) userOffset; message[41] = (byte) (userOffset >> 8); + //message[42] = (byte) (userOffset >> 16); + //message[43] = (byte) (userOffset >> 24); - // host + // Workstation short workstationLength = (short) workstation.Length; short workstationOffset = (short) (userOffset + userLength); message[44] = (byte) workstationLength; @@ -254,11 +401,19 @@ public override byte[] Encode () message[47] = message[45]; message[48] = (byte) workstationOffset; message[49] = (byte) (workstationOffset >> 8); - - // message length - short messageLength = (short) message.Length; - message[56] = (byte) messageLength; - message[57] = (byte) (messageLength >> 8); + //message[50] = (byte) (workstationOffset >> 16); + //message[51] = (byte) (workstationOffset >> 24); + + // EncryptedRandomSessionKey + short skeyOffset = (short) (ntResponseOffset + ntResponseLength); + message[52] = (byte) skeyLength; + message[53] = (byte) (skeyLength >> 8); + message[54] = message[52]; + message[55] = message[53]; + message[56] = (byte) skeyOffset; + message[57] = (byte) (skeyOffset >> 8); + //message[58] = (byte) (skeyOffset >> 16); + //message[59] = (byte) (skeyOffset >> 24); // options flags message[60] = (byte) Flags; @@ -277,19 +432,21 @@ public override byte[] Encode () message[71] = 0x0f; } + if (Mic != null) + Buffer.BlockCopy (Mic, 0, message, micOffset, Mic.Length); + Buffer.BlockCopy (target, 0, message, domainOffset, target.Length); Buffer.BlockCopy (user, 0, message, userOffset, user.Length); Buffer.BlockCopy (workstation, 0, message, workstationOffset, workstation.Length); - if (lm != null) { - Buffer.BlockCopy (lm, 0, message, lmResponseOffset, lm.Length); - Array.Clear (lm, 0, lm.Length); - } + if (LmChallengeResponse != null) + Buffer.BlockCopy (LmChallengeResponse, 0, message, lmResponseOffset, LmChallengeResponse.Length); - if (ntlm != null) { - Buffer.BlockCopy (ntlm, 0, message, ntResponseOffset, ntlm.Length); - Array.Clear (ntlm, 0, ntlm.Length); - } + if (NtChallengeResponse != null) + Buffer.BlockCopy (NtChallengeResponse, 0, message, ntResponseOffset, NtChallengeResponse.Length); + + if ((Flags & NtlmFlags.NegotiateKeyExchange) != 0 && EncryptedRandomSessionKey != null) + Buffer.BlockCopy (EncryptedRandomSessionKey, 0, message, skeyOffset, EncryptedRandomSessionKey.Length); return message; } diff --git a/MailKit/Security/SaslMechanism.cs b/MailKit/Security/SaslMechanism.cs index 9a5507af73..ca503c4fa4 100644 --- a/MailKit/Security/SaslMechanism.cs +++ b/MailKit/Security/SaslMechanism.cs @@ -243,6 +243,8 @@ protected byte[] GetChannelBindingToken (ChannelBindingKind kind) } } + channelBinding.Close (); + return token; } diff --git a/MailKit/Security/SaslMechanismNtlm.cs b/MailKit/Security/SaslMechanismNtlm.cs index 095cac55c7..ba1266c1a8 100644 --- a/MailKit/Security/SaslMechanismNtlm.cs +++ b/MailKit/Security/SaslMechanismNtlm.cs @@ -24,8 +24,11 @@ // THE SOFTWARE. // +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 + using System; using System.Net; +using System.Security.Authentication.ExtendedProtection; using MailKit.Security.Ntlm; @@ -39,10 +42,11 @@ namespace MailKit.Security { public class SaslMechanismNtlm : SaslMechanism { enum LoginState { - Initial, + Negotiate, Challenge } + Type1Message type1; LoginState state; /// @@ -57,7 +61,9 @@ enum LoginState { /// public SaslMechanismNtlm (NetworkCredential credentials) : base (credentials) { - Level = NtlmAuthLevel.NTLMv2_only; + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + OSVersion = Environment.OSVersion.Version; + Workstation = Environment.MachineName; } /// @@ -75,7 +81,23 @@ public SaslMechanismNtlm (NetworkCredential credentials) : base (credentials) /// public SaslMechanismNtlm (string userName, string password) : base (userName, password) { - Level = NtlmAuthLevel.NTLMv2_only; + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + OSVersion = Environment.OSVersion.Version; + Workstation = Environment.MachineName; + } + + /// + /// This is only used for unit testing purposes. + /// + internal byte[] Nonce { + get; set; + } + + /// + /// This is only used for unit testing purposes. + /// + internal long? Timestamp { + get; set; } /// @@ -89,6 +111,17 @@ public SaslMechanismNtlm (string userName, string password) : base (userName, pa get { return "NTLM"; } } + /// + /// Get whether or not the SASL mechanism supports channel binding. + /// + /// + /// Gets whether or not the SASL mechanism supports channel binding. + /// + /// true if the SASL mechanism supports channel binding; otherwise, false. + public override bool SupportsChannelBinding { + get { return true; } + } + /// /// Get whether or not the mechanism supports an initial response (SASL-IR). /// @@ -102,16 +135,26 @@ public SaslMechanismNtlm (string userName, string password) : base (userName, pa get { return true; } } - internal NtlmAuthLevel Level { + /// + /// Get or set a value indicating whether or not the NTLM SASL mechanism should allow channel-binding. + /// + /// + /// Gets or sets a value indicating whether or not the NTLM SASL mechanism should allow channel-binding. + /// In the future, this option will disappear as channel-binding will become the default. For now, + /// it is only an option because this feature has not been thoroughly tested. + /// + /// true if the NTLM SASL mechanism should allow channel-binding; otherwise, false. + public bool AllowChannelBinding { get; set; } /// - /// Get or set the Windows OS version to use in the NTLM negotiation (used for debuigging purposes). + /// Get or set the Windows OS version to use in the NTLM negotiation (used for debugging purposes). /// /// - /// Gets or sets the Windows OS version to use in the NTLM negotiation (used for debuigging purposes). + /// Gets or sets the Windows OS version to use in the NTLM negotiation (used for debugging purposes). /// + /// The Windows OS version. public Version OSVersion { get; set; } @@ -127,6 +170,29 @@ public SaslMechanismNtlm (string userName, string password) : base (userName, pa get; set; } + /// + /// Get or set the service principal name (SPN) of the service that the client wishes to authenticate with. + /// + /// + /// Get or set the service principal name (SPN) of the service that the client wishes to authenticate with. + /// This value is optional. + /// + /// The service principal name (SPN) of the service that the client wishes to authenticate with. + public string ServicePrincipalName { + get; set; + } + + /// + /// Get or set a value indicating that the caller generated the target's SPN from an untrusted source. + /// + /// + /// Gets or sets a value indicating that the caller generated the target's SPN from an untrusted source. + /// + /// true if the is unverified; otherwise, false. + public bool IsUnverifiedServicePrincipalName { + get; set; + } + /// /// Parse the server's challenge token and return the next challenge response. /// @@ -147,22 +213,28 @@ protected override byte[] Challenge (byte[] token, int startIndex, int length) string userName = Credentials.UserName; string domain = Credentials.Domain; - MessageBase message = null; + NtlmMessageBase message = null; if (string.IsNullOrEmpty (domain)) { - int index = userName.IndexOf ('\\'); - if (index == -1) - index = userName.IndexOf ('/'); + int index; + + if ((index = userName.LastIndexOf ('@')) != -1) { + userName = userName.Substring (0, index); + domain = userName.Substring (index + 1); + } else { + if ((index = userName.IndexOf ('\\')) == -1) + index = userName.IndexOf ('/'); - if (index >= 0) { - domain = userName.Substring (0, index); - userName = userName.Substring (index + 1); + if (index >= 0) { + domain = userName.Substring (0, index); + userName = userName.Substring (index + 1); + } } } switch (state) { - case LoginState.Initial: - message = new Type1Message (Workstation, domain, OSVersion); + case LoginState.Negotiate: + message = type1 = new Type1Message (domain, Workstation, OSVersion); state = LoginState.Challenge; break; case LoginState.Challenge: @@ -175,11 +247,24 @@ protected override byte[] Challenge (byte[] token, int startIndex, int length) return message?.Encode (); } - MessageBase GetChallengeResponse (string userName, string password, byte[] token, int startIndex, int length) + NtlmMessageBase GetChallengeResponse (string userName, string password, byte[] token, int startIndex, int length) { var type2 = new Type2Message (token, startIndex, length); + var type3 = new Type3Message (type1, type2, userName, password, Workstation) { + ClientChallenge = Nonce, + Timestamp = Timestamp + }; + byte[] channelBinding = null; + + if (AllowChannelBinding && type2.TargetInfo != null) { + // Only bother with attempting to channel-bind if the CHALLENGE_MESSAGE's TargetInfo is not NULL. + channelBinding = GetChannelBindingToken (ChannelBindingKind.Endpoint); + } + + type3.ComputeNtlmV2 (ServicePrincipalName, IsUnverifiedServicePrincipalName, channelBinding); + type1 = null; - return new Type3Message (type2, OSVersion, Level, userName, password, Workstation); + return type3; } /// @@ -190,7 +275,8 @@ MessageBase GetChallengeResponse (string userName, string password, byte[] token /// public override void Reset () { - state = LoginState.Initial; + state = LoginState.Negotiate; + type1 = null; base.Reset (); } } diff --git a/UnitTests/Security/Ntlm/NtlmSingleHostDataTests.cs b/UnitTests/Security/Ntlm/NtlmSingleHostDataTests.cs new file mode 100644 index 0000000000..8615dc7d88 --- /dev/null +++ b/UnitTests/Security/Ntlm/NtlmSingleHostDataTests.cs @@ -0,0 +1,54 @@ +// +// NtlmSingleHostDataTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2021 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using NUnit.Framework; + +using MailKit.Security.Ntlm; + +namespace UnitTests.Security.Ntlm { + [TestFixture] + public class NtlmSingleHostDataTests + { + [Test] + public void TestArgumentExceptions () + { + var customData = new byte[8]; + var machineId = new byte[32]; + var buffer = new byte[48]; + + Assert.Throws (() => new NtlmSingleHostData (null, machineId)); + Assert.Throws (() => new NtlmSingleHostData (machineId, machineId)); + Assert.Throws (() => new NtlmSingleHostData (customData, null)); + Assert.Throws (() => new NtlmSingleHostData (customData, customData)); + + Assert.Throws (() => new NtlmSingleHostData (null, 0, 48)); + Assert.Throws (() => new NtlmSingleHostData (buffer, -1, 48)); + Assert.Throws (() => new NtlmSingleHostData (buffer, 0, 25)); + } + } +} diff --git a/UnitTests/Security/Ntlm/NtlmTargetInfoTests.cs b/UnitTests/Security/Ntlm/NtlmTargetInfoTests.cs new file mode 100644 index 0000000000..9a56373f68 --- /dev/null +++ b/UnitTests/Security/Ntlm/NtlmTargetInfoTests.cs @@ -0,0 +1,297 @@ +// +// NtlmTargetInfoTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2021 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using NUnit.Framework; + +using MailKit.Security.Ntlm; + +namespace UnitTests.Security.Ntlm { + [TestFixture] + public class NtlmTargetInfoTests + { + [Test] + public void TestArgumentExceptions () + { + var buffer = new byte[24]; + + Assert.Throws (() => new NtlmTargetInfo (null, 0, 24, true)); + Assert.Throws (() => new NtlmTargetInfo (buffer, -1, 24, true)); + Assert.Throws (() => new NtlmTargetInfo (buffer, 0, 25, true)); + } + +#if false + static string ToCSharpByteArrayInitializer (string name, byte[] buffer) + { + var builder = new System.Text.StringBuilder (); + int index = 0; + + builder.AppendLine ($"static readonly byte[] {name} = {{"); + while (index < buffer.Length) { + builder.Append ('\t'); + for (int i = 0; i < 16 && index < buffer.Length; i++, index++) + builder.AppendFormat ("0x{0}, ", buffer[index].ToString ("x2")); + builder.Length--; + if (index == buffer.Length) + builder.Length--; + builder.AppendLine (); + } + builder.AppendLine ($"}};"); + + return builder.ToString (); + } +#endif + + static void AssertDecode (byte[] buffer, bool unicode) + { + var channelBinding = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }; + var timestamp = DateTime.FromFileTimeUtc (132737136905945346); + var targetInfo = new NtlmTargetInfo (buffer, 0, buffer.Length, unicode); + + //var buffer1 = targetInfo.Encode (true); + //var csharp = ToCSharpByteArrayInitializer ("NtlmTargetInfoUnorderedUnicode", buffer1); + + Assert.AreEqual ("ServerName", targetInfo.ServerName); + Assert.AreEqual ("DomainName", targetInfo.DomainName); + Assert.AreEqual ("DnsServerName", targetInfo.DnsServerName); + Assert.AreEqual ("DnsDomainName", targetInfo.DnsDomainName); + Assert.AreEqual ("DnsTreeName", targetInfo.DnsTreeName); + Assert.AreEqual (2, targetInfo.Flags, "Flags"); + Assert.AreEqual (timestamp.ToFileTimeUtc (), targetInfo.Timestamp, "Timestamp"); + //Assert.AreEqual ("SingleHost", targetInfo.SingleHost); + Assert.AreEqual (16, targetInfo.ChannelBinding.Length, "ChannelBinding"); + + for (int i = 0; i < channelBinding.Length; i++) + Assert.AreEqual (channelBinding[i], targetInfo.ChannelBinding[i], $"ChannelBinding[{i}]"); + + // Verify that re-encoding the target info results in an exact replica of the input. + var encoded = targetInfo.Encode (unicode); + + Assert.AreEqual (buffer.Length, encoded.Length, "Re-encoded lengths do not match"); + + for (int i = 0; i < buffer.Length; i++) + Assert.AreEqual (buffer[i], encoded[i], $"encoded[{i}]"); + } + + static readonly byte[] NtlmTargetInfoOrderedOem = { + 0x01, 0x00, 0x0a, 0x00, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x02, 0x00, + 0x0a, 0x00, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x03, 0x00, 0x0d, 0x00, + 0x44, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x04, 0x00, 0x0d, + 0x00, 0x44, 0x6e, 0x73, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x05, 0x00, + 0x0b, 0x00, 0x44, 0x6e, 0x73, 0x54, 0x72, 0x65, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x06, 0x00, 0x04, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x08, 0x00, 0x02, 0x01, 0xc8, 0x05, 0xb9, 0x93, 0xd7, + 0x01, 0x08, 0x00, 0x0a, 0x00, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x09, + 0x00, 0x0a, 0x00, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x0a, 0x00, 0x10, + 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, + 0xef, 0x00, 0x00, 0x00, 0x00 + }; + + [Test] + public void TestDecodeOrderedOem () + { + AssertDecode (NtlmTargetInfoOrderedOem, false); + } + + static readonly byte[] NtlmTargetInfoOrderedUnicode = { + 0x01, 0x00, 0x14, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x4e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x02, 0x00, 0x14, 0x00, 0x44, 0x00, 0x6f, 0x00, + 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x4e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, + 0x03, 0x00, 0x1a, 0x00, 0x44, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x4e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x04, 0x00, + 0x1a, 0x00, 0x44, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x44, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, + 0x69, 0x00, 0x6e, 0x00, 0x4e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x05, 0x00, 0x16, 0x00, + 0x44, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x54, 0x00, 0x72, 0x00, 0x65, 0x00, 0x65, 0x00, 0x4e, 0x00, + 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x06, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, + 0x08, 0x00, 0x02, 0x01, 0xc8, 0x05, 0xb9, 0x93, 0xd7, 0x01, 0x08, 0x00, 0x14, 0x00, 0x53, 0x00, + 0x69, 0x00, 0x6e, 0x00, 0x67, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x48, 0x00, 0x6f, 0x00, 0x73, 0x00, + 0x74, 0x00, 0x09, 0x00, 0x14, 0x00, 0x54, 0x00, 0x61, 0x00, 0x72, 0x00, 0x67, 0x00, 0x65, 0x00, + 0x74, 0x00, 0x4e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x01, 0x23, + 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x00, 0x00, + 0x00, 0x00 + }; + + [Test] + public void TestDecodeOrderedUnicode () + { + AssertDecode (NtlmTargetInfoOrderedUnicode, true); + } + + static readonly byte[] NtlmTargetInfoUnorderedOem = { + 0x07, 0x00, 0x08, 0x00, 0x02, 0x01, 0xc8, 0x05, 0xb9, 0x93, 0xd7, 0x01, 0x09, 0x00, 0x0a, 0x00, + 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x08, 0x00, 0x0a, 0x00, 0x53, 0x69, + 0x6e, 0x67, 0x6c, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x01, 0x00, 0x0a, 0x00, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x06, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, + 0x0a, 0x00, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x05, 0x00, 0x0b, 0x00, + 0x44, 0x6e, 0x73, 0x54, 0x72, 0x65, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x03, 0x00, 0x0d, 0x00, 0x44, + 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x04, 0x00, 0x0d, 0x00, + 0x44, 0x6e, 0x73, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x0a, 0x00, 0x10, + 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, + 0xef, 0x00, 0x00, 0x00, 0x00 + }; + + [Test] + public void TestDecodeUnorederedOem () + { + AssertDecode (NtlmTargetInfoUnorderedOem, false); + } + + static readonly byte[] NtlmTargetInfoUnorderedUnicode = { + 0x07, 0x00, 0x08, 0x00, 0x02, 0x01, 0xc8, 0x05, 0xb9, 0x93, 0xd7, 0x01, 0x09, 0x00, 0x14, 0x00, + 0x54, 0x00, 0x61, 0x00, 0x72, 0x00, 0x67, 0x00, 0x65, 0x00, 0x74, 0x00, 0x4e, 0x00, 0x61, 0x00, + 0x6d, 0x00, 0x65, 0x00, 0x08, 0x00, 0x14, 0x00, 0x53, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x67, 0x00, + 0x6c, 0x00, 0x65, 0x00, 0x48, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x74, 0x00, 0x01, 0x00, 0x14, 0x00, + 0x53, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x4e, 0x00, 0x61, 0x00, + 0x6d, 0x00, 0x65, 0x00, 0x06, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x14, 0x00, + 0x44, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x4e, 0x00, 0x61, 0x00, + 0x6d, 0x00, 0x65, 0x00, 0x05, 0x00, 0x16, 0x00, 0x44, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x54, 0x00, + 0x72, 0x00, 0x65, 0x00, 0x65, 0x00, 0x4e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x03, 0x00, + 0x1a, 0x00, 0x44, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x4e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x04, 0x00, 0x1a, 0x00, + 0x44, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x44, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x4e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x01, 0x23, + 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x00, 0x00, + 0x00, 0x00 + }; + + [Test] + public void TestDecodeUnorderedUnocode () + { + AssertDecode (NtlmTargetInfoUnorderedUnicode, true); + } + + static NtlmSingleHostData GenerateSingleHostData () + { + var customData = new byte[8]; + var machineId = new byte[32]; + var rng = new Random (); + + rng.NextBytes (customData); + rng.NextBytes (machineId); + + return new NtlmSingleHostData (customData, machineId); + } + + static void AssertSingleHost (NtlmSingleHostData expected, byte[] actual, string prefix) + { + var singleHost = new NtlmSingleHostData (actual, 0, actual.Length); + + Assert.AreEqual (expected.Size, singleHost.Size, $"{prefix}.Size"); + for (int i = 0; i < 8; i++) + Assert.AreEqual (expected.CustomData[i], singleHost.CustomData[i], $"{prefix}.CustomData[{i}]"); + for (int i = 0; i < 32; i++) + Assert.AreEqual (expected.MachineId[i], singleHost.MachineId[i], $"{prefix}.MachineId[{i}]"); + } + + [Test] + public void TestRemovingAttributes () + { + var channelBinding = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }; + var timestamp = DateTime.FromFileTimeUtc (132737136905945346); + var singleHost = GenerateSingleHostData (); + var targetInfo = new NtlmTargetInfo { + ServerName = "ServerName", + DomainName = "DomainName", + SingleHost = singleHost.Encode (), + Flags = 2, + Timestamp = timestamp.ToFileTimeUtc (), + ChannelBinding = channelBinding + }; + + Assert.AreEqual ("ServerName", targetInfo.ServerName); + Assert.AreEqual ("DomainName", targetInfo.DomainName); + AssertSingleHost (singleHost, targetInfo.SingleHost, "SingleHost"); + Assert.AreEqual (2, targetInfo.Flags, "Flags"); + Assert.AreEqual (timestamp.ToFileTimeUtc (), targetInfo.Timestamp, "Timestamp"); + Assert.AreEqual (16, targetInfo.ChannelBinding.Length, "ChannelBinding"); + + for (int i = 0; i < channelBinding.Length; i++) + Assert.AreEqual (channelBinding[i], targetInfo.ChannelBinding[i], $"ChannelBinding[{i}]"); + + targetInfo.SingleHost = null; + Assert.IsNull (targetInfo.SingleHost, "SingleHost remove attempt #1"); + targetInfo.SingleHost = null; + Assert.IsNull (targetInfo.SingleHost, "SingleHost remove attempt #2"); + + targetInfo.Flags = null; + Assert.IsNull (targetInfo.Flags, "Flags remove attempt #1"); + targetInfo.Flags = null; + Assert.IsNull (targetInfo.Flags, "Flags remove attempt #2"); + + targetInfo.Timestamp = null; + Assert.IsNull (targetInfo.Timestamp, "Timestamp remove attempt #1"); + targetInfo.Timestamp = null; + Assert.IsNull (targetInfo.Timestamp, "Timestamp remove attempt #2"); + + targetInfo.ChannelBinding = null; + Assert.IsNull (targetInfo.ChannelBinding, "ChannelBinding remove attempt #1"); + targetInfo.ChannelBinding = null; + Assert.IsNull (targetInfo.ChannelBinding, "ChannelBinding remove attempt #2"); + } + + [Test] + public void TestUpdatingAttributes () + { + var channelBinding = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }; + var timestamp = DateTime.FromFileTimeUtc (132737136905945346); + var updatedSingleHost = GenerateSingleHostData (); + var singleHost = GenerateSingleHostData (); + var targetInfo = new NtlmTargetInfo { + ServerName = "ServerName", + DomainName = "DomainName", + SingleHost = singleHost.Encode (), + Flags = 2, + Timestamp = timestamp.ToFileTimeUtc (), + ChannelBinding = channelBinding + }; + + Assert.AreEqual ("ServerName", targetInfo.ServerName); + Assert.AreEqual ("DomainName", targetInfo.DomainName); + AssertSingleHost (singleHost, targetInfo.SingleHost, "SingleHost"); + Assert.AreEqual (2, targetInfo.Flags, "Flags"); + Assert.AreEqual (timestamp.ToFileTimeUtc (), targetInfo.Timestamp, "Timestamp"); + Assert.AreEqual (16, targetInfo.ChannelBinding.Length, "ChannelBinding"); + + for (int i = 0; i < channelBinding.Length; i++) + Assert.AreEqual (channelBinding[i], targetInfo.ChannelBinding[i], $"ChannelBinding[{i}]"); + + targetInfo.SingleHost = updatedSingleHost.Encode (); + AssertSingleHost (updatedSingleHost, targetInfo.SingleHost, "Updated SingleHost"); + + targetInfo.Flags = 1; + Assert.AreEqual (1, targetInfo.Flags, "Updated Flags"); + + targetInfo.Timestamp = 123456789; + Assert.AreEqual (123456789, targetInfo.Timestamp, "Updated Timestamp"); + + targetInfo.ChannelBinding = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + Assert.AreEqual (20, targetInfo.ChannelBinding.Length, "Updated ChannelBinding"); + + for (int i = 0; i < channelBinding.Length; i++) + Assert.AreEqual (i, targetInfo.ChannelBinding[i], $"Updated ChannelBinding[{i}]"); + } + } +} diff --git a/UnitTests/Security/Ntlm/RC4Tests.cs b/UnitTests/Security/Ntlm/RC4Tests.cs new file mode 100644 index 0000000000..bcf15fef30 --- /dev/null +++ b/UnitTests/Security/Ntlm/RC4Tests.cs @@ -0,0 +1,170 @@ +// +// RC4Tests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2021 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using NUnit.Framework; + +using MailKit.Security.Ntlm; + +namespace UnitTests.Security.Ntlm { + [TestFixture] + public class RC4Tests + { + [Test] + public void TestArgumentExceptions () + { + using (var rc4 = new RC4 ()) { + var buffer = new byte[16]; + + Assert.AreEqual (1, rc4.InputBlockSize, "InputBlockSize"); + Assert.AreEqual (1, rc4.OutputBlockSize, "OutputBlockSize"); + Assert.IsFalse (rc4.CanReuseTransform, "CanReuseTransform"); + Assert.IsTrue (rc4.CanTransformMultipleBlocks, "CanTransformMultipleBlocks"); + + Assert.Throws (() => { var x = rc4.Key; }); + Assert.Throws (() => { rc4.Key = null; }); + Assert.Throws (() => { rc4.Key = new byte[0]; }); + + rc4.GenerateIV (); + rc4.GenerateKey (); + rc4.CreateDecryptor (); + rc4.CreateEncryptor (); + + // TransformBlock input buffer parameters + Assert.Throws (() => rc4.TransformBlock (null, 0, buffer.Length, buffer, 0)); + Assert.Throws (() => rc4.TransformBlock (buffer, -1, buffer.Length, buffer, 0)); + Assert.Throws (() => rc4.TransformBlock (buffer, 0, -1, buffer, 0)); + + // TransformBlock output buffer parameters + Assert.Throws (() => rc4.TransformBlock (buffer, 0, buffer.Length, null, 0)); + Assert.Throws (() => rc4.TransformBlock (buffer, 0, buffer.Length, buffer, -1)); + + // TransformFinalBlock + Assert.Throws (() => rc4.TransformFinalBlock (null, 0, buffer.Length)); + Assert.Throws (() => rc4.TransformFinalBlock (buffer, -1, buffer.Length)); + Assert.Throws (() => rc4.TransformFinalBlock (buffer, 0, -1)); + } + } + + static void AssertEncrypt (byte[] key, byte[] input, byte[] expected) + { + using (var rc4 = new RC4 ()) { + var output = new byte[input.Length]; + + rc4.Key = key; + + rc4.TransformBlock (input, 0, input.Length, output, 0); + + for (int i = 0; i < output.Length; i++) + Assert.AreEqual (expected[i], output[i], $"output[{i}]"); + } + + using (var rc4 = new RC4 ()) { + rc4.Key = key; + + var output = rc4.TransformFinalBlock (input, 0, input.Length); + + for (int i = 0; i < output.Length; i++) + Assert.AreEqual (expected[i], output[i], $"output[{i}]"); + } + } + + [Test] + public void TestEncryptExample1 () + { + var expected = new byte[] { 0x74, 0x94, 0xC2, 0xE7, 0x10, 0x4B, 0x08, 0x79 }; + var key = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF }; + var text = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + AssertEncrypt (key, text, expected); + } + + [Test] + public void TestEncryptExample2 () + { + byte[] expected = new byte[] { 0xF1, 0x38, 0x29, 0xC9, 0xDE }; + byte[] key = new byte[] { 0x61, 0x8A, 0x63, 0xD2, 0xFB }; + byte[] text = new byte[] { 0xDC, 0xEE, 0x4C, 0xF9, 0x2C }; + + AssertEncrypt (key, text, expected); + } + + [Test] + public void TestEncryptExample3 () + { + byte[] expected = new byte[] { + 0x35, 0x81, 0x86, 0x99, 0x90, 0x01, 0xe6, 0xb5, 0xda, 0xf0, 0x5e, 0xce, 0xeb, 0x7e, 0xee, 0x21, + 0xe0, 0x68, 0x9c, 0x1f, 0x00, 0xee, 0xa8, 0x1f, 0x7d, 0xd2, 0xca, 0xae, 0xe1, 0xd2, 0x76, 0x3e, + 0x68, 0xaf, 0x0e, 0xad, 0x33, 0xd6, 0x6c, 0x26, 0x8b, 0xc9, 0x46, 0xc4, 0x84, 0xfb, 0xe9, 0x4c, + 0x5f, 0x5e, 0x0b, 0x86, 0xa5, 0x92, 0x79, 0xe4, 0xf8, 0x24, 0xe7, 0xa6, 0x40, 0xbd, 0x22, 0x32, + 0x10, 0xb0, 0xa6, 0x11, 0x60, 0xb7, 0xbc, 0xe9, 0x86, 0xea, 0x65, 0x68, 0x80, 0x03, 0x59, 0x6b, + 0x63, 0x0a, 0x6b, 0x90, 0xf8, 0xe0, 0xca, 0xf6, 0x91, 0x2a, 0x98, 0xeb, 0x87, 0x21, 0x76, 0xe8, + 0x3c, 0x20, 0x2c, 0xaa, 0x64, 0x16, 0x6d, 0x2c, 0xce, 0x57, 0xff, 0x1b, 0xca, 0x57, 0xb2, 0x13, + 0xf0, 0xed, 0x1a, 0xa7, 0x2f, 0xb8, 0xea, 0x52, 0xb0, 0xbe, 0x01, 0xcd, 0x1e, 0x41, 0x28, 0x67, + 0x72, 0x0b, 0x32, 0x6e, 0xb3, 0x89, 0xd0, 0x11, 0xbd, 0x70, 0xd8, 0xaf, 0x03, 0x5f, 0xb0, 0xd8, + 0x58, 0x9d, 0xbc, 0xe3, 0xc6, 0x66, 0xf5, 0xea, 0x8d, 0x4c, 0x79, 0x54, 0xc5, 0x0c, 0x3f, 0x34, + 0x0b, 0x04, 0x67, 0xf8, 0x1b, 0x42, 0x59, 0x61, 0xc1, 0x18, 0x43, 0x07, 0x4d, 0xf6, 0x20, 0xf2, + 0x08, 0x40, 0x4b, 0x39, 0x4c, 0xf9, 0xd3, 0x7f, 0xf5, 0x4b, 0x5f, 0x1a, 0xd8, 0xf6, 0xea, 0x7d, + 0xa3, 0xc5, 0x61, 0xdf, 0xa7, 0x28, 0x1f, 0x96, 0x44, 0x63, 0xd2, 0xcc, 0x35, 0xa4, 0xd1, 0xb0, + 0x34, 0x90, 0xde, 0xc5, 0x1b, 0x07, 0x11, 0xfb, 0xd6, 0xf5, 0x5f, 0x79, 0x23, 0x4d, 0x5b, 0x7c, + 0x76, 0x66, 0x22, 0xa6, 0x6d, 0xe9, 0x2b, 0xe9, 0x96, 0x46, 0x1d, 0x5e, 0x4d, 0xc8, 0x78, 0xef, + 0x9b, 0xca, 0x03, 0x05, 0x21, 0xe8, 0x35, 0x1e, 0x4b, 0xae, 0xd2, 0xfd, 0x04, 0xf9, 0x46, 0x73, + 0x68, 0xc4, 0xad, 0x6a, 0xc1, 0x86, 0xd0, 0x82, 0x45, 0xb2, 0x63, 0xa2, 0x66, 0x6d, 0x1f, 0x6c, + 0x54, 0x20, 0xf1, 0x59, 0x9d, 0xfd, 0x9f, 0x43, 0x89, 0x21, 0xc2, 0xf5, 0xa4, 0x63, 0x93, 0x8c, + 0xe0, 0x98, 0x22, 0x65, 0xee, 0xf7, 0x01, 0x79, 0xbc, 0x55, 0x3f, 0x33, 0x9e, 0xb1, 0xa4, 0xc1, + 0xaf, 0x5f, 0x6a, 0x54, 0x7f + }; + byte[] key = new byte[] { + 0x29, 0x04, 0x19, 0x72, 0xFB, 0x42, 0xBA, 0x5F, 0xC7, 0x12, 0x77, 0x12, 0xF1, 0x38, 0x29, 0xC9 + }; + byte[] text = new byte[] { + 0x52, 0x75, 0x69, 0x73, 0x6c, 0x69, 0x6e, 0x6e, 0x75, 0x6e, 0x20, 0x6c, 0x61, 0x75, 0x6c, 0x75, + 0x20, 0x6b, 0x6f, 0x72, 0x76, 0x69, 0x73, 0x73, 0x73, 0x61, 0x6e, 0x69, 0x2c, 0x20, 0x74, 0xe4, + 0x68, 0x6b, 0xe4, 0x70, 0xe4, 0x69, 0x64, 0x65, 0x6e, 0x20, 0x70, 0xe4, 0xe4, 0x6c, 0x6c, 0xe4, + 0x20, 0x74, 0xe4, 0x79, 0x73, 0x69, 0x6b, 0x75, 0x75, 0x2e, 0x20, 0x4b, 0x65, 0x73, 0xe4, 0x79, + 0xf6, 0x6e, 0x20, 0x6f, 0x6e, 0x20, 0x6f, 0x6e, 0x6e, 0x69, 0x20, 0x6f, 0x6d, 0x61, 0x6e, 0x61, + 0x6e, 0x69, 0x2c, 0x20, 0x6b, 0x61, 0x73, 0x6b, 0x69, 0x73, 0x61, 0x76, 0x75, 0x75, 0x6e, 0x20, + 0x6c, 0x61, 0x61, 0x6b, 0x73, 0x6f, 0x74, 0x20, 0x76, 0x65, 0x72, 0x68, 0x6f, 0x75, 0x75, 0x2e, + 0x20, 0x45, 0x6e, 0x20, 0x6d, 0x61, 0x20, 0x69, 0x6c, 0x6f, 0x69, 0x74, 0x73, 0x65, 0x2c, 0x20, + 0x73, 0x75, 0x72, 0x65, 0x20, 0x68, 0x75, 0x6f, 0x6b, 0x61, 0x61, 0x2c, 0x20, 0x6d, 0x75, 0x74, + 0x74, 0x61, 0x20, 0x6d, 0x65, 0x74, 0x73, 0xe4, 0x6e, 0x20, 0x74, 0x75, 0x6d, 0x6d, 0x75, 0x75, + 0x73, 0x20, 0x6d, 0x75, 0x6c, 0x6c, 0x65, 0x20, 0x74, 0x75, 0x6f, 0x6b, 0x61, 0x61, 0x2e, 0x20, + 0x50, 0x75, 0x75, 0x6e, 0x74, 0x6f, 0x20, 0x70, 0x69, 0x6c, 0x76, 0x65, 0x6e, 0x2c, 0x20, 0x6d, + 0x69, 0x20, 0x68, 0x75, 0x6b, 0x6b, 0x75, 0x75, 0x2c, 0x20, 0x73, 0x69, 0x69, 0x6e, 0x74, 0x6f, + 0x20, 0x76, 0x61, 0x72, 0x61, 0x6e, 0x20, 0x74, 0x75, 0x75, 0x6c, 0x69, 0x73, 0x65, 0x6e, 0x2c, + 0x20, 0x6d, 0x69, 0x20, 0x6e, 0x75, 0x6b, 0x6b, 0x75, 0x75, 0x2e, 0x20, 0x54, 0x75, 0x6f, 0x6b, + 0x73, 0x75, 0x74, 0x20, 0x76, 0x61, 0x6e, 0x61, 0x6d, 0x6f, 0x6e, 0x20, 0x6a, 0x61, 0x20, 0x76, + 0x61, 0x72, 0x6a, 0x6f, 0x74, 0x20, 0x76, 0x65, 0x65, 0x6e, 0x2c, 0x20, 0x6e, 0x69, 0x69, 0x73, + 0x74, 0xe4, 0x20, 0x73, 0x79, 0x64, 0xe4, 0x6d, 0x65, 0x6e, 0x69, 0x20, 0x6c, 0x61, 0x75, 0x6c, + 0x75, 0x6e, 0x20, 0x74, 0x65, 0x65, 0x6e, 0x2e, 0x20, 0x2d, 0x20, 0x45, 0x69, 0x6e, 0x6f, 0x20, + 0x4c, 0x65, 0x69, 0x6e, 0x6f + }; + + AssertEncrypt (key, text, expected); + } + } +} diff --git a/UnitTests/Security/Ntlm/Type1MessageTests.cs b/UnitTests/Security/Ntlm/Type1MessageTests.cs new file mode 100644 index 0000000000..794fa29bf0 --- /dev/null +++ b/UnitTests/Security/Ntlm/Type1MessageTests.cs @@ -0,0 +1,85 @@ +// +// Type1MessageTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2021 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using NUnit.Framework; + +using MailKit.Security.Ntlm; + +namespace UnitTests.Security.Ntlm { + [TestFixture] + public class Type1MessageTests + { + [Test] + public void TestArgumentExceptions () + { + byte[] badMessageData = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x01, 0x00, 0x00, 0x00, 0x00 }; + + Assert.Throws (() => new Type1Message (null, 0, 16)); + Assert.Throws (() => new Type1Message (new byte[8], 0, 8)); + Assert.Throws (() => new Type1Message (new byte[8], -1, 8)); + Assert.Throws (() => new Type1Message (badMessageData, 0, badMessageData.Length)); + } + + [Test] + // Example from http://www.innovation.ch/java/ntlm.html + public void TestEncodeJavaExample () + { + var type1 = new Type1Message (NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateOem | NtlmFlags.RequestTarget | NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateAlwaysSign, "Ursa-Minor", "LightCity"); + + Assert.AreEqual (1, type1.Type, "Type"); + Assert.AreEqual ((NtlmFlags) 0xb207, type1.Flags, "Flags"); + Assert.AreEqual ("4E-54-4C-4D-53-53-50-00-01-00-00-00-07-B2-00-00-0A-00-0A-00-29-00-00-00-09-00-09-00-20-00-00-00-4C-49-47-48-54-43-49-54-59-55-52-53-41-2D-4D-49-4E-4F-52", BitConverter.ToString (type1.Encode ()), "Encode"); + } + + [Test] + // Example from http://www.innovation.ch/java/ntlm.html + public void TestDecodeJavaExample () + { + byte[] rawData = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0xb2, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x29, 0x00, 0x00, 0x00, 0x09, 0x00, 0x09, 0x00, 0x20, 0x00, 0x00, 0x00, 0x4c, 0x49, 0x47, 0x48, 0x54, 0x43, 0x49, 0x54, 0x59, 0x55, 0x52, 0x53, 0x41, 0x2d, 0x4d, 0x49, 0x4e, 0x4f, 0x52 }; + var type1 = new Type1Message (rawData, 0, rawData.Length); + + Assert.AreEqual (1, type1.Type, "Type"); + Assert.AreEqual ((NtlmFlags) 0xb203, type1.Flags, "Flags"); + Assert.AreEqual ("URSA-MINOR", type1.Domain, "Domain"); + Assert.AreEqual ("LIGHTCITY", type1.Workstation, "Workstation"); + } + + [Test] + // Example from http://davenport.sourceforge.net/ntlm.html#type1MessageExample + public void TestDecodeDavenportExample () + { + byte[] rawData = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x32, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x0b, 0x00, 0x20, 0x00, 0x00, 0x00, 0x57, 0x4f, 0x52, 0x4b, 0x53, 0x54, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x44, 0x4f, 0x4d, 0x41, 0x49, 0x4e }; + var type1 = new Type1Message (rawData, 0, rawData.Length); + + Assert.AreEqual (1, type1.Type, "Type"); + Assert.AreEqual ((NtlmFlags) 0x3207, type1.Flags, "Flags"); + Assert.AreEqual ("DOMAIN", type1.Domain, "Domain"); + Assert.AreEqual ("WORKSTATION", type1.Workstation, "Workstation"); + } + } +} diff --git a/UnitTests/Security/Ntlm/Type2MessageTests.cs b/UnitTests/Security/Ntlm/Type2MessageTests.cs new file mode 100644 index 0000000000..8951c2e707 --- /dev/null +++ b/UnitTests/Security/Ntlm/Type2MessageTests.cs @@ -0,0 +1,121 @@ +// +// Type2MessageTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2021 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using NUnit.Framework; + +using MailKit.Security.Ntlm; + +namespace UnitTests.Security.Ntlm { + [TestFixture] + public class Type2MessageTests + { + static byte[] DavenportExampleNonce = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }; + static byte[] JavaExampleNonce = { 0x53, 0x72, 0x76, 0x4e, 0x6f, 0x6e, 0x63, 0x65 }; + + [Test] + public void TestArgumentExceptions () + { + byte[] badMessageData = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x01, 0x00, 0x00, 0x00, 0x00 }; + var type2 = new Type2Message (); + + Assert.Throws (() => new Type2Message (null, 0, 16)); + Assert.Throws (() => new Type2Message (new byte[8], 0, 8)); + Assert.Throws (() => new Type2Message (new byte[8], -1, 8)); + Assert.Throws (() => new Type2Message (badMessageData, 0, badMessageData.Length)); + + Assert.Throws (() => type2.ServerChallenge = null); + Assert.Throws (() => type2.ServerChallenge = new byte[9]); + } + + [Test] + // Example from http://www.innovation.ch/java/ntlm.html + public void TestEncodeJavaExample () + { + var type2 = new Type2Message (NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateAlwaysSign) { ServerChallenge = JavaExampleNonce }; + + Assert.AreEqual (2, type2.Type, "Type"); + Assert.AreEqual ((NtlmFlags) 0x8201, type2.Flags, "Flags"); + Assert.AreEqual ("4E-54-4C-4D-53-53-50-00-02-00-00-00-00-00-00-00-00-00-00-00-01-82-00-00-53-72-76-4E-6F-6E-63-65-00-00-00-00-00-00-00-00", BitConverter.ToString (type2.Encode ()), "Encode"); + } + + [Test] + // Example from http://www.innovation.ch/java/ntlm.html + public void TestDecodeJavaExample () + { + byte[] rawData = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x82, 0x00, 0x00, 0x53, 0x72, 0x76, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + var type2 = new Type2Message (rawData, 0, rawData.Length); + + Assert.AreEqual (2, type2.Type, "Type"); + Assert.AreEqual ((NtlmFlags) 0x8201, type2.Flags, "Flags"); + Assert.AreEqual (BitConverter.ToString (JavaExampleNonce), BitConverter.ToString (type2.ServerChallenge), "ServerChallenge"); + } + + [Test] + // Example from http://davenport.sourceforge.net/ntlm.html#type2MessageExample + public void TestEncodeDavenportExample () + { + var type2 = new Type2Message (NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateNtlm | NtlmFlags.TargetTypeDomain | NtlmFlags.NegotiateTargetInfo) { + TargetInfo = new NtlmTargetInfo () { + DomainName = "DOMAIN", + ServerName = "SERVER", + DnsDomainName = "domain.com", + DnsServerName = "server.domain.com" + }, + ServerChallenge = DavenportExampleNonce, + TargetName = "DOMAIN" + }; + + Assert.AreEqual (2, type2.Type, "Type"); + Assert.AreEqual ((NtlmFlags) 0x00810201, type2.Flags, "Flags"); + Assert.AreEqual ("DOMAIN", type2.TargetName, "TargetName"); + Assert.AreEqual ("SERVER", type2.TargetInfo.ServerName, "ServerName"); + Assert.AreEqual ("DOMAIN", type2.TargetInfo.DomainName, "DomainName"); + Assert.AreEqual ("server.domain.com", type2.TargetInfo.DnsServerName, "DnsServerName"); + Assert.AreEqual ("domain.com", type2.TargetInfo.DnsDomainName, "DnsDomainName"); + Assert.AreEqual ("01-23-45-67-89-AB-CD-EF", BitConverter.ToString (type2.ServerChallenge), "ServerChallenge"); + Assert.AreEqual ("4E-54-4C-4D-53-53-50-00-02-00-00-00-0C-00-0C-00-30-00-00-00-01-02-81-00-01-23-45-67-89-AB-CD-EF-00-00-00-00-00-00-00-00-62-00-62-00-3C-00-00-00-44-00-4F-00-4D-00-41-00-49-00-4E-00-02-00-0C-00-44-00-4F-00-4D-00-41-00-49-00-4E-00-01-00-0C-00-53-00-45-00-52-00-56-00-45-00-52-00-04-00-14-00-64-00-6F-00-6D-00-61-00-69-00-6E-00-2E-00-63-00-6F-00-6D-00-03-00-22-00-73-00-65-00-72-00-76-00-65-00-72-00-2E-00-64-00-6F-00-6D-00-61-00-69-00-6E-00-2E-00-63-00-6F-00-6D-00-00-00-00-00", BitConverter.ToString (type2.Encode ()), "Encode"); + } + + [Test] + // Example from http://davenport.sourceforge.net/ntlm.html#type2MessageExample + public void TestDecodeDavenportExample () + { + byte[] rawData = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x30, 0x00, 0x00, 0x00, 0x01, 0x02, 0x81, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x62, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x02, 0x00, 0x0c, 0x00, 0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x53, 0x00, 0x45, 0x00, 0x52, 0x00, 0x56, 0x00, 0x45, 0x00, 0x52, 0x00, 0x04, 0x00, 0x14, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x03, 0x00, 0x22, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00 }; + var type2 = new Type2Message (rawData, 0, rawData.Length); + + Assert.AreEqual (2, type2.Type, "Type"); + Assert.AreEqual ((NtlmFlags) 0x00810201, type2.Flags, "Flags"); + Assert.AreEqual ("DOMAIN", type2.TargetName, "TargetName"); + Assert.AreEqual ("SERVER", type2.TargetInfo.ServerName, "ServerName"); + Assert.AreEqual ("DOMAIN", type2.TargetInfo.DomainName, "DomainName"); + Assert.AreEqual ("server.domain.com", type2.TargetInfo.DnsServerName, "DnsServerName"); + Assert.AreEqual ("domain.com", type2.TargetInfo.DnsDomainName, "DnsDomainName"); + Assert.AreEqual ("01-23-45-67-89-AB-CD-EF", BitConverter.ToString (type2.ServerChallenge), "ServerChallenge"); + } + } +} diff --git a/UnitTests/Security/Ntlm/Type3MessageTests.cs b/UnitTests/Security/Ntlm/Type3MessageTests.cs new file mode 100644 index 0000000000..fe14cee7eb --- /dev/null +++ b/UnitTests/Security/Ntlm/Type3MessageTests.cs @@ -0,0 +1,95 @@ +// +// Type3MessageTests.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2021 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using NUnit.Framework; + +using MailKit.Security.Ntlm; + +namespace UnitTests.Security.Ntlm { + [TestFixture] + public class Type3MessageTests + { + [Test] + public void TestArgumentExceptions () + { + byte[] badMessageData = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x01, 0x00, 0x00, 0x00, 0x00 }; + var type1 = new Type1Message (); + var type2 = new Type2Message (); + var type3 = new Type3Message (type1, type2, "username", "password", "workstation"); + + Assert.Throws (() => new Type3Message (null, type2, "username", "password", "workstation")); + Assert.Throws (() => new Type3Message (type1, null, "username", "password", "workstation")); + Assert.Throws (() => new Type3Message (type1, type2, null, "password", "workstation")); + Assert.Throws (() => new Type3Message (type1, type2, "username", null, "workstation")); + + Assert.Throws (() => new Type3Message (null, 0, 16)); + Assert.Throws (() => new Type3Message (new byte[8], 0, 8)); + Assert.Throws (() => new Type3Message (new byte[8], -1, 8)); + Assert.Throws (() => new Type3Message (badMessageData, 0, badMessageData.Length)); + + Assert.DoesNotThrow (() => type3.ClientChallenge = null); + Assert.Throws (() => type3.ClientChallenge = new byte[9]); + } + + [Test] + // Example from http://www.innovation.ch/java/ntlm.html + public void TestDecodeJavaExample () + { + byte[] rawData = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x03, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x00, 0x72, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x14, 0x00, 0x14, 0x00, 0x40, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x54, 0x00, 0x00, 0x00, 0x12, 0x00, 0x12, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa2, 0x00, 0x00, 0x00, 0x01, 0x82, 0x00, 0x00, 0x55, 0x00, 0x52, 0x00, 0x53, 0x00, 0x41, 0x00, 0x2d, 0x00, 0x4d, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x4f, 0x00, 0x52, 0x00, 0x5a, 0x00, 0x61, 0x00, 0x70, 0x00, 0x68, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x4c, 0x00, 0x49, 0x00, 0x47, 0x00, 0x48, 0x00, 0x54, 0x00, 0x43, 0x00, 0x49, 0x00, 0x54, 0x00, 0x59, 0x00, 0xad, 0x87, 0xca, 0x6d, 0xef, 0xe3, 0x46, 0x85, 0xb9, 0xc4, 0x3c, 0x47, 0x7a, 0x8c, 0x42, 0xd6, 0x00, 0x66, 0x7d, 0x68, 0x92, 0xe7, 0xe8, 0x97, 0xe0, 0xe0, 0x0d, 0xe3, 0x10, 0x4a, 0x1b, 0xf2, 0x05, 0x3f, 0x07, 0xc7, 0xdd, 0xa8, 0x2d, 0x3c, 0x48, 0x9a, 0xe9, 0x89, 0xe1, 0xb0, 0x00, 0xd3 }; + var type3 = new Type3Message (rawData, 0, rawData.Length); + + Assert.AreEqual (3, type3.Type, "Type"); + Assert.AreEqual ((NtlmFlags) 0x8201, type3.Flags, "Flags"); + Assert.AreEqual ("URSA-MINOR", type3.Domain, "Domain"); + Assert.AreEqual ("LIGHTCITY", type3.Workstation, "Workstation"); + Assert.AreEqual ("Zaphod", type3.UserName, "UserName"); + Assert.IsNull (type3.Password, "Password"); + + Assert.AreEqual ("AD-87-CA-6D-EF-E3-46-85-B9-C4-3C-47-7A-8C-42-D6-00-66-7D-68-92-E7-E8-97", BitConverter.ToString (type3.LmChallengeResponse), "LmChallengeResponse"); + Assert.AreEqual ("E0-E0-0D-E3-10-4A-1B-F2-05-3F-07-C7-DD-A8-2D-3C-48-9A-E9-89-E1-B0-00-D3", BitConverter.ToString (type3.NtChallengeResponse), "NtChallengeResponse"); + } + + [Test] + // Example from http://davenport.sourceforge.net/ntlm.html#type3MessageExample + public void TestDecodeDavenportExample () + { + byte[] rawData = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x03, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x00, 0x82, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x40, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x16, 0x00, 0x16, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x75, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x57, 0x00, 0x4f, 0x00, 0x52, 0x00, 0x4b, 0x00, 0x53, 0x00, 0x54, 0x00, 0x41, 0x00, 0x54, 0x00, 0x49, 0x00, 0x4f, 0x00, 0x4e, 0x00, 0xc3, 0x37, 0xcd, 0x5c, 0xbd, 0x44, 0xfc, 0x97, 0x82, 0xa6, 0x67, 0xaf, 0x6d, 0x42, 0x7c, 0x6d, 0xe6, 0x7c, 0x20, 0xc2, 0xd3, 0xe7, 0x7c, 0x56, 0x25, 0xa9, 0x8c, 0x1c, 0x31, 0xe8, 0x18, 0x47, 0x46, 0x6b, 0x29, 0xb2, 0xdf, 0x46, 0x80, 0xf3, 0x99, 0x58, 0xfb, 0x8c, 0x21, 0x3a, 0x9c, 0xc6 }; + Type3Message type3 = new Type3Message (rawData, 0, rawData.Length); + + Assert.AreEqual (3, type3.Type, "Type"); + Assert.AreEqual ((NtlmFlags) 0x201, type3.Flags, "Flags"); + Assert.AreEqual ("DOMAIN", type3.Domain, "Domain"); + Assert.AreEqual ("WORKSTATION", type3.Workstation, "Workstation"); + Assert.AreEqual ("user", type3.UserName, "UserName"); + Assert.IsNull (type3.Password, "Password"); + + Assert.AreEqual ("C3-37-CD-5C-BD-44-FC-97-82-A6-67-AF-6D-42-7C-6D-E6-7C-20-C2-D3-E7-7C-56", BitConverter.ToString (type3.LmChallengeResponse), "LmChallengeResponse"); + Assert.AreEqual ("25-A9-8C-1C-31-E8-18-47-46-6B-29-B2-DF-46-80-F3-99-58-FB-8C-21-3A-9C-C6", BitConverter.ToString (type3.NtChallengeResponse), "NtChallengeResponse"); + } + } +} diff --git a/UnitTests/Security/SaslMechanismNtlmTests.cs b/UnitTests/Security/SaslMechanismNtlmTests.cs index 773d7a5b43..16644645ad 100644 --- a/UnitTests/Security/SaslMechanismNtlmTests.cs +++ b/UnitTests/Security/SaslMechanismNtlmTests.cs @@ -50,6 +50,28 @@ public void TestArgumentExceptions () Assert.Throws (() => new SaslMechanismNtlm ("username", null)); } +#if false + static string ToCSharpByteArrayInitializer (string name, byte[] buffer) + { + var builder = new System.Text.StringBuilder (); + int index = 0; + + builder.AppendLine ($"static readonly byte[] {name} = {{"); + while (index < buffer.Length) { + builder.Append ('\t'); + for (int i = 0; i < 16 && index < buffer.Length; i++, index++) + builder.AppendFormat ("0x{0}, ", buffer[index].ToString ("x2")); + builder.Length--; + if (index == buffer.Length) + builder.Length--; + builder.AppendLine (); + } + builder.AppendLine ($"}};"); + + return builder.ToString (); + } +#endif + static byte ToXDigit (char c) { if (c >= 0x41) { @@ -86,12 +108,33 @@ static string HexEncode (byte[] value) return builder.ToString (); } + static Type1Message DecodeType1Message (string token) + { + var message = Convert.FromBase64String (token); + + return new Type1Message (message, 0, message.Length); + } + + static Type2Message DecodeType2Message (string token) + { + var message = Convert.FromBase64String (token); + + return new Type2Message (message, 0, message.Length); + } + + static Type3Message DecodeType3Message (string token) + { + var message = Convert.FromBase64String (token); + + return new Type3Message (message, 0, message.Length); + } + [Test] public void TestNtlmTargetInfoEncode () { var now = DateTime.Now.Ticks; - var targetInfo = new TargetInfo { + var targetInfo = new NtlmTargetInfo { ChannelBinding = Encoding.ASCII.GetBytes ("channel-binding"), TargetName = "TARGET", DnsTreeName = "target.domain.com", @@ -104,7 +147,7 @@ public void TestNtlmTargetInfoEncode () }; var encoded = targetInfo.Encode (true); - var decoded = new TargetInfo (encoded, 0, encoded.Length, true); + var decoded = new NtlmTargetInfo (encoded, 0, encoded.Length, true); Assert.AreEqual ("channel-binding", Encoding.ASCII.GetString (decoded.ChannelBinding), "ChannelBinding does not match."); Assert.AreEqual (targetInfo.DnsDomainName, decoded.DnsDomainName, "DnsDomainName does not match."); @@ -119,16 +162,24 @@ public void TestNtlmTargetInfoEncode () static readonly byte [] NtlmType1EncodedMessage = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x07, 0x32, 0x00, 0x02, 0x06, 0x00, 0x06, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x0b, 0x00, 0x0b, 0x00, 0x28, 0x00, 0x00, 0x00, 0x05, 0x00, 0x93, 0x08, - 0x00, 0x00, 0x00, 0x0f, 0x57, 0x4f, 0x52, 0x4b, 0x53, 0x54, 0x41, 0x54, - 0x49, 0x4f, 0x4e, 0x44, 0x4f, 0x4d, 0x41, 0x49, 0x4e + 0x07, 0xb2, 0x08, 0x20, 0x06, 0x00, 0x06, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x0b, 0x00, 0x0b, 0x00, 0x20, 0x00, 0x00, 0x00, 0x57, 0x4f, 0x52, 0x4b, + 0x53, 0x54, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x44, 0x4f, 0x4d, 0x41, 0x49, + 0x4e + }; + + static readonly byte[] NtlmType1EncodedMessageWithVersion = { + 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x07, 0x82, 0x08, 0x22, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x05, 0x00, 0x93, 0x08, + 0x00, 0x00, 0x00, 0x0f, }; [Test] public void TestNtlmType1MessageEncode () { - var type1 = new Type1Message ("Workstation", "Domain", new Version (5, 0, 2195)); + var flags = NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateOem | NtlmFlags.RequestTarget | NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateAlwaysSign | NtlmFlags.NegotiateExtendedSessionSecurity | NtlmFlags.Negotiate128; + var type1 = new Type1Message (flags, "Domain", "Workstation"); var encoded = type1.Encode (); string actual, expected; @@ -138,17 +189,46 @@ public void TestNtlmType1MessageEncode () Assert.AreEqual (expected, actual, "The encoded Type1Message did not match the expected result."); } + [Test] + public void TestNtlmType1MessageEncodeWithVersion () + { + var flags = NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateOem | NtlmFlags.RequestTarget | NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateAlwaysSign | NtlmFlags.NegotiateExtendedSessionSecurity | NtlmFlags.Negotiate128; + var type1 = new Type1Message (flags, "Domain", "Workstation", new Version (5, 0, 2195)); + var encoded = type1.Encode (); + string actual, expected; + + expected = HexEncode (NtlmType1EncodedMessageWithVersion); + actual = HexEncode (encoded); + + Assert.AreEqual (expected, actual, "The encoded Type1Message did not match the expected result."); + } + [Test] public void TestNtlmType1MessageDecode () { - var flags = NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateDomainSupplied | NtlmFlags.NegotiateWorkstationSupplied | - NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateOem | NtlmFlags.RequestTarget | NtlmFlags.NegotiateVersion; + var flags = NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateOem | NtlmFlags.RequestTarget | NtlmFlags.NegotiateNtlm | + NtlmFlags.NegotiateDomainSupplied | NtlmFlags.NegotiateWorkstationSupplied | NtlmFlags.NegotiateAlwaysSign | + NtlmFlags.NegotiateExtendedSessionSecurity | NtlmFlags.Negotiate128; var type1 = new Type1Message (NtlmType1EncodedMessage, 0, NtlmType1EncodedMessage.Length); - var osVersion = new Version (5, 0, 2195); Assert.AreEqual (flags, type1.Flags, "The expected flags do not match."); Assert.AreEqual ("WORKSTATION", type1.Workstation, "The expected workstation name does not match."); Assert.AreEqual ("DOMAIN", type1.Domain, "The expected domain does not match."); + Assert.AreEqual (null, type1.OSVersion, "The expected OS Version does not match."); + } + + [Test] + public void TestNtlmType1MessageDecodeWithVersion () + { + var flags = NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateOem | NtlmFlags.RequestTarget | NtlmFlags.NegotiateNtlm | + NtlmFlags.NegotiateAlwaysSign | NtlmFlags.NegotiateExtendedSessionSecurity | NtlmFlags.NegotiateVersion | + NtlmFlags.Negotiate128; + var type1 = new Type1Message (NtlmType1EncodedMessageWithVersion, 0, NtlmType1EncodedMessageWithVersion.Length); + var osVersion = new Version (5, 0, 2195); + + Assert.AreEqual (flags, type1.Flags, "The expected flags do not match."); + Assert.AreEqual (string.Empty, type1.Workstation, "The expected workstation name does not match."); + Assert.AreEqual (string.Empty, type1.Domain, "The expected domain does not match."); Assert.AreEqual (osVersion, type1.OSVersion, "The expected OS Version does not match."); } @@ -172,22 +252,21 @@ public void TestNtlmType1MessageDecode () [Test] public void TestNtlmType2MessageEncode () { - var targetInfo = new TargetInfo { + var targetInfo = new NtlmTargetInfo { DomainName = "DOMAIN", ServerName = "SERVER", DnsDomainName = "domain.com", DnsServerName = "server.domain.com" }; - var type2 = new Type2Message { - Flags = NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateNtlm | NtlmFlags.TargetTypeDomain | NtlmFlags.NegotiateTargetInfo, - Nonce = HexDecode ("0123456789abcdef"), + var type2 = new Type2Message (NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateNtlm | NtlmFlags.TargetTypeDomain | NtlmFlags.NegotiateTargetInfo) { + ServerChallenge = HexDecode ("0123456789abcdef"), TargetInfo = targetInfo, TargetName = "DOMAIN", }; - Assert.Throws (() => type2.Nonce = null); - Assert.Throws (() => type2.Nonce = new byte[0]); + Assert.Throws (() => type2.ServerChallenge = null); + Assert.Throws (() => type2.ServerChallenge = new byte[0]); var encoded = type2.Encode (); string actual, expected; @@ -208,10 +287,10 @@ public void TestNtlmType2MessageDecode () Assert.AreEqual (flags, type2.Flags, "The expected flags do not match."); Assert.AreEqual ("DOMAIN", type2.TargetName, "The expected TargetName does not match."); - var nonce = HexEncode (type2.Nonce); + var nonce = HexEncode (type2.ServerChallenge); Assert.AreEqual ("0123456789abcdef", nonce, "The expected nonce does not match."); - var targetInfo = HexEncode (type2.EncodedTargetInfo); + var targetInfo = HexEncode (type2.GetEncodedTargetInfo ()); Assert.AreEqual (expectedTargetInfo, targetInfo, "The expected TargetInfo does not match."); Assert.AreEqual ("DOMAIN", type2.TargetInfo.DomainName, "The expected TargetInfo domain name does not match."); @@ -224,28 +303,26 @@ public void TestNtlmType2MessageDecode () } static readonly byte[] NtlmType2EncodedMessageWithOSVersion = { - 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x0c, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x02, 0x81, 0x02, - 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x6e, 0x00, 0x4c, 0x00, 0x00, 0x00, - 0x06, 0x03, 0x80, 0x25, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, - 0x49, 0x00, 0x4e, 0x00, 0x02, 0x00, 0x0c, 0x00, 0x44, 0x00, 0x4f, 0x00, - 0x4d, 0x00, 0x41, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x01, 0x00, 0x0c, 0x00, - 0x53, 0x00, 0x45, 0x00, 0x52, 0x00, 0x56, 0x00, 0x45, 0x00, 0x52, 0x00, - 0x04, 0x00, 0x14, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, - 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, - 0x03, 0x00, 0x22, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, - 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, - 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, - 0x6d, 0x00, 0x07, 0x00, 0x08, 0x00, 0xd2, 0x02, 0x96, 0x49, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x0c, 0x00, + 0x38, 0x00, 0x00, 0x00, 0x01, 0x02, 0x81, 0x02, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x6e, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x06, 0x03, 0x80, 0x25, 0x00, 0x00, 0x00, 0x0f, 0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, + 0x49, 0x00, 0x4e, 0x00, 0x02, 0x00, 0x0c, 0x00, 0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, + 0x49, 0x00, 0x4e, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x53, 0x00, 0x45, 0x00, 0x52, 0x00, 0x56, 0x00, + 0x45, 0x00, 0x52, 0x00, 0x04, 0x00, 0x14, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, + 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x03, 0x00, 0x22, 0x00, + 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x64, 0x00, + 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, + 0x6d, 0x00, 0x07, 0x00, 0x08, 0x00, 0xd2, 0x02, 0x96, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 }; [Test] public void TestNtlmType2MessageEncodeWithOSVersion () { - var targetInfo = new TargetInfo { + var flags = NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateNtlm | NtlmFlags.TargetTypeDomain | NtlmFlags.NegotiateTargetInfo | NtlmFlags.NegotiateVersion; + + var targetInfo = new NtlmTargetInfo { DomainName = "DOMAIN", ServerName = "SERVER", DnsDomainName = "domain.com", @@ -253,15 +330,14 @@ public void TestNtlmType2MessageEncodeWithOSVersion () Timestamp = 1234567890 }; - var type2 = new Type2Message (new Version (6, 3, 9600)) { - Flags = NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateNtlm | NtlmFlags.TargetTypeDomain | NtlmFlags.NegotiateTargetInfo | NtlmFlags.NegotiateVersion, - Nonce = HexDecode ("0123456789abcdef"), + var type2 = new Type2Message (flags, new Version (6, 3, 9600)) { + ServerChallenge = HexDecode ("0123456789abcdef"), TargetInfo = targetInfo, TargetName = "DOMAIN", }; - Assert.Throws (() => type2.Nonce = null); - Assert.Throws (() => type2.Nonce = new byte[0]); + Assert.Throws (() => type2.ServerChallenge = null); + Assert.Throws (() => type2.ServerChallenge = new byte[0]); var encoded = type2.Encode (); string actual, expected; @@ -282,10 +358,10 @@ public void TestNtlmType2MessageDecodeWithOSVersion () Assert.AreEqual (flags, type2.Flags, "The expected flags do not match."); Assert.AreEqual ("DOMAIN", type2.TargetName, "The expected TargetName does not match."); - var nonce = HexEncode (type2.Nonce); + var nonce = HexEncode (type2.ServerChallenge); Assert.AreEqual ("0123456789abcdef", nonce, "The expected nonce does not match."); - var targetInfo = HexEncode (type2.EncodedTargetInfo); + var targetInfo = HexEncode (type2.GetEncodedTargetInfo ()); Assert.AreEqual (expectedTargetInfo, targetInfo, "The expected TargetInfo does not match."); Assert.AreEqual ("DOMAIN", type2.TargetInfo.DomainName, "The expected TargetInfo domain name does not match."); @@ -301,13 +377,23 @@ public void TestNtlmType2MessageDecodeWithOSVersion () [Test] public void TestNtlmType3MessageEncode () { - const string expected = "TlRMTVNTUAADAAAAGAAYAGoAAAAYABgAggAAAAwADABAAAAACAAIAEwAAAAWABYAVAAAAAAAAACaAAAAAQKAAEQATwBNAEEASQBOAHUAcwBlAHIAVwBPAFIASwBTAFQAQQBUAEkATwBOAJje97h/iKpdr+Lfd5aIoXLe8Rx9XM3vE91UKLAehvTfyr6sOUlG29Q+6I95TdYyVQ=="; + const string expected = "TlRMTVNTUAADAAAAGAAYAGoAAACqAKoAggAAAAwADABAAAAACAAIAEwAAAAWABYAVAAAAAAAAAAsAQAAAQKBAEQATwBNAEEASQBOAHUAcwBlAHIAVwBPAFIASwBTAFQAQQBUAEkATwBOAAFVaqKdtK6RUUd6vhq2MnkBAgMEBQUGByItsEaz8xYhhLclKBEweI0BAQAAAAAAAACQVAPpkdcBAQIDBAUFBgcAAAAAAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAkAAAAKABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - var token = Convert.FromBase64String (challenge2); - var type2 = new Type2Message (token, 0, token.Length); - var type3 = new Type3Message (type2, null, NtlmAuthLevel.LM_and_NTLM, "user", "password", "WORKSTATION"); + var flags = NtlmFlags.NegotiateUnicode | NtlmFlags.NegotiateNtlm | NtlmFlags.TargetTypeDomain | NtlmFlags.NegotiateTargetInfo; + var timestamp = new DateTime (2021, 08, 15, 15, 20, 00, DateTimeKind.Utc).Ticks; + var nonce = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x05, 0x06, 0x07 }; + var type1 = new Type1Message (flags, null, null, new Version (10, 0, 19043)); + var type2 = DecodeType2Message (challenge2); + var type3 = new Type3Message (type1, type2, "user", "password", "WORKSTATION") { + ClientChallenge = nonce, + Timestamp = timestamp + }; + + type3.ComputeNtlmV2 (null, false, null); var actual = Convert.ToBase64String (type3.Encode ()); + //var expectedType3 = DecodeType3Message (expected); + Assert.AreEqual (expected, actual, "The encoded Type3Message did not match the expected result."); } @@ -316,32 +402,29 @@ public void TestNtlmType3MessageDecode () { const string challenge3 = "TlRMTVNTUAADAAAAGAAYAGoAAAAYABgAggAAAAwADABAAAAACAAIAEwAAAAWABYAVAAAAAAAAACaAAAAAQIAAEQATwBNAEEASQBOAHUAcwBlAHIAVwBPAFIASwBTAFQAQQBUAEkATwBOAJje97h/iKpdr+Lfd5aIoXLe8Rx9XM3vE91UKLAehvTfyr6sOUlG29Q+6I95TdYyVQ=="; var flags = NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateUnicode; - var token = Convert.FromBase64String (challenge3); - var type3 = new Type3Message (token, 0, token.Length); + var type3 = DecodeType3Message (challenge3); Assert.AreEqual (flags, type3.Flags, "The expected flags do not match."); Assert.AreEqual ("DOMAIN", type3.Domain, "The expected Domain does not match."); Assert.AreEqual ("WORKSTATION", type3.Workstation, "The expected Workstation does not match."); - Assert.AreEqual ("user", type3.Username, "The expected Username does not match."); + Assert.AreEqual ("user", type3.UserName, "The expected Username does not match."); - var nt = HexEncode (type3.NT); + var nt = HexEncode (type3.NtChallengeResponse); Assert.AreEqual ("dd5428b01e86f4dfcabeac394946dbd43ee88f794dd63255", nt, "The NT payload does not match."); - var lm = HexEncode (type3.LM); + var lm = HexEncode (type3.LmChallengeResponse); Assert.AreEqual ("98def7b87f88aa5dafe2df779688a172def11c7d5ccdef13", lm, "The LM payload does not match."); } static void AssertNtlmAuthNoDomain (SaslMechanismNtlm sasl, string prefix) { string challenge; - byte [] decoded; Assert.IsTrue (sasl.SupportsInitialResponse, "{0}: SupportsInitialResponse", prefix); challenge = sasl.Challenge (string.Empty); - decoded = Convert.FromBase64String (challenge); - var type1 = new Type1Message (decoded, 0, decoded.Length); + var type1 = DecodeType1Message (challenge); Assert.AreEqual (Type1Message.DefaultFlags, type1.Flags, "{0}: Expected initial NTLM client challenge flags do not match.", prefix); Assert.AreEqual (string.Empty, type1.Domain, "{0}: Expected initial NTLM client challenge domain does not match.", prefix); @@ -353,11 +436,11 @@ static void AssertNtlmAuthNoDomain (SaslMechanismNtlm sasl, string prefix) public void TestNtlmAuthNoDomain () { var credentials = new NetworkCredential ("username", "password"); - var sasl = new SaslMechanismNtlm (credentials); + var sasl = new SaslMechanismNtlm (credentials) { OSVersion = null, Workstation = null }; AssertNtlmAuthNoDomain (sasl, "NetworkCredential"); - sasl = new SaslMechanismNtlm ("username", "password"); + sasl = new SaslMechanismNtlm ("username", "password") { OSVersion = null, Workstation = null }; AssertNtlmAuthNoDomain (sasl, "user/pass"); } @@ -366,14 +449,12 @@ static void AssertNtlmAuthWithDomain (SaslMechanismNtlm sasl, string prefix) { var initialFlags = Type1Message.DefaultFlags | NtlmFlags.NegotiateDomainSupplied; string challenge; - byte [] decoded; Assert.IsTrue (sasl.SupportsInitialResponse, "{0}: SupportsInitialResponse", prefix); challenge = sasl.Challenge (string.Empty); - decoded = Convert.FromBase64String (challenge); - var type1 = new Type1Message (decoded, 0, decoded.Length); + var type1 = DecodeType1Message (challenge); Assert.AreEqual (initialFlags, type1.Flags, "{0}: Expected initial NTLM client challenge flags do not match.", prefix); Assert.AreEqual ("DOMAIN", type1.Domain, "{0}: Expected initial NTLM client challenge domain does not match.", prefix); @@ -385,328 +466,245 @@ static void AssertNtlmAuthWithDomain (SaslMechanismNtlm sasl, string prefix) public void TestNtlmAuthWithDomain () { var credentials = new NetworkCredential ("domain\\username", "password"); - var sasl = new SaslMechanismNtlm (credentials); + var sasl = new SaslMechanismNtlm (credentials) { OSVersion = null, Workstation = null }; AssertNtlmAuthWithDomain (sasl, "NetworkCredential"); - sasl = new SaslMechanismNtlm ("domain\\username", "password"); + sasl = new SaslMechanismNtlm ("domain\\username", "password") { OSVersion = null, Workstation = null }; AssertNtlmAuthWithDomain (sasl, "user/pass"); } - static Type1Message DecodeType1Message (string token) - { - var message = Convert.FromBase64String (token); - - return new Type1Message (message, 0, message.Length); - } - - static Type2Message DecodeType2Message (string token) - { - var message = Convert.FromBase64String (token); - - return new Type2Message (message, 0, message.Length); - } - - static Type3Message DecodeType3Message (string token) - { - var message = Convert.FromBase64String (token); - - return new Type3Message (message, 0, message.Length); - } - - static void AssertLmAndNtlm (SaslMechanismNtlm sasl, string challenge1, string challenge2, string challenge3) + static void AssertNtlmv2 (SaslMechanismNtlm sasl, string challenge1, string challenge2) { var challenge = sasl.Challenge (string.Empty); + var timestamp = DateTime.UtcNow.Ticks; + var nonce = NtlmUtils.NONCE (8); - Assert.AreEqual (challenge1, challenge, "Initial challenge"); - Assert.IsFalse (sasl.IsAuthenticated, "IsAuthenticated"); - - challenge = sasl.Challenge (challenge2); - - Assert.AreEqual (challenge3, challenge, "Final challenge"); - Assert.IsTrue (sasl.IsAuthenticated, "IsAuthenticated"); - } - - [Test] - public void TestAuthenticationLmAndNtlm () - { - const string challenge1 = "TlRMTVNTUAABAAAABwIAAAAAAAAgAAAAAAAAACAAAAA="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - const string challenge3 = "TlRMTVNTUAADAAAAGAAYAFQAAAAYABgAbAAAAAwADABAAAAACAAIAEwAAAAAAAAAVAAAAAAAAACEAAAAAQKAAEQATwBNAEEASQBOAHUAcwBlAHIAmN73uH+Iql2v4t93loihct7xHH1cze8T3VQosB6G9N/Kvqw5SUbb1D7oj3lN1jJV"; - - var credentials = new NetworkCredential ("user", "password"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.LM_and_NTLM }; - - AssertLmAndNtlm (sasl, challenge1, challenge2, challenge3); - } - - [Test] - public void TestAuthenticationLmAndNtlmWithDomain () - { - const string challenge1 = "TlRMTVNTUAABAAAABxIAAAYABgAgAAAAAAAAACAAAABET01BSU4="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - const string challenge3 = "TlRMTVNTUAADAAAAGAAYAFQAAAAYABgAbAAAAAwADABAAAAACAAIAEwAAAAAAAAAVAAAAAAAAACEAAAAAQKAAEQATwBNAEEASQBOAHUAcwBlAHIAmN73uH+Iql2v4t93loihct7xHH1cze8T3VQosB6G9N/Kvqw5SUbb1D7oj3lN1jJV"; - var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.LM_and_NTLM }; - - AssertLmAndNtlm (sasl, challenge1, challenge2, challenge3); - } - - [Test] - public void TestAuthenticationLmAndNtlmWithDomainAndWorkstation () - { - const string challenge1 = "TlRMTVNTUAABAAAABzIAAAYABgArAAAACwALACAAAABXT1JLU1RBVElPTkRPTUFJTg=="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - const string challenge3 = "TlRMTVNTUAADAAAAGAAYAGoAAAAYABgAggAAAAwADABAAAAACAAIAEwAAAAWABYAVAAAAAAAAACaAAAAAQKAAEQATwBNAEEASQBOAHUAcwBlAHIAVwBPAFIASwBTAFQAQQBUAEkATwBOAJje97h/iKpdr+Lfd5aIoXLe8Rx9XM3vE91UKLAehvTfyr6sOUlG29Q+6I95TdYyVQ=="; - var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Workstation = "WORKSTATION", Level = NtlmAuthLevel.LM_and_NTLM }; - - AssertLmAndNtlm (sasl, challenge1, challenge2, challenge3); - } - - [Test] - public void TestAuthenticationLmAndNtlmSessionFallback () - { - // Note: this will fallback to LN_and_NTLM because the type2 message does not contain the NegotiateNtlm2Key flag - const string challenge1 = "TlRMTVNTUAABAAAABwIAAAAAAAAgAAAAAAAAACAAAAA="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - const string challenge3 = "TlRMTVNTUAADAAAAGAAYAFQAAAAYABgAbAAAAAwADABAAAAACAAIAEwAAAAAAAAAVAAAAAAAAACEAAAAAQKAAEQATwBNAEEASQBOAHUAcwBlAHIAmN73uH+Iql2v4t93loihct7xHH1cze8T3VQosB6G9N/Kvqw5SUbb1D7oj3lN1jJV"; - - var credentials = new NetworkCredential ("user", "password"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.LM_and_NTLM_and_try_NTLMv2_Session }; - - AssertLmAndNtlm (sasl, challenge1, challenge2, challenge3); - } - - [Test] - public void TestAuthenticationLmAndNtlmSessionFallbackWithDomain () - { - // Note: this will fallback to LN_and_NTLM because the type2 message does not contain the NegotiateNtlm2Key flag - const string challenge1 = "TlRMTVNTUAABAAAABxIAAAYABgAgAAAAAAAAACAAAABET01BSU4="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - const string challenge3 = "TlRMTVNTUAADAAAAGAAYAFQAAAAYABgAbAAAAAwADABAAAAACAAIAEwAAAAAAAAAVAAAAAAAAACEAAAAAQKAAEQATwBNAEEASQBOAHUAcwBlAHIAmN73uH+Iql2v4t93loihct7xHH1cze8T3VQosB6G9N/Kvqw5SUbb1D7oj3lN1jJV"; - var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.LM_and_NTLM_and_try_NTLMv2_Session }; - - AssertLmAndNtlm (sasl, challenge1, challenge2, challenge3); - } - - [Test] - public void TestAuthenticationLmAndNtlmSessionFallbackWithDomainAndWorkstation () - { - // Note: this will fallback to LN_and_NTLM because the type2 message does not contain the NegotiateNtlm2Key flag - const string challenge1 = "TlRMTVNTUAABAAAABzIAAAYABgArAAAACwALACAAAABXT1JLU1RBVElPTkRPTUFJTg=="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - const string challenge3 = "TlRMTVNTUAADAAAAGAAYAGoAAAAYABgAggAAAAwADABAAAAACAAIAEwAAAAWABYAVAAAAAAAAACaAAAAAQKAAEQATwBNAEEASQBOAHUAcwBlAHIAVwBPAFIASwBTAFQAQQBUAEkATwBOAJje97h/iKpdr+Lfd5aIoXLe8Rx9XM3vE91UKLAehvTfyr6sOUlG29Q+6I95TdYyVQ=="; - var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Workstation = "WORKSTATION", Level = NtlmAuthLevel.LM_and_NTLM_and_try_NTLMv2_Session }; - - AssertLmAndNtlm (sasl, challenge1, challenge2, challenge3); - } - - static void AssertNtlm2Key (SaslMechanismNtlm sasl, string challenge1, string challenge2) - { - var challenge = sasl.Challenge (string.Empty); + sasl.Timestamp = timestamp; + sasl.Nonce = nonce; Assert.AreEqual (challenge1, challenge, "Initial challenge"); Assert.IsFalse (sasl.IsAuthenticated, "IsAuthenticated"); challenge = sasl.Challenge (challenge2); - var token = Convert.FromBase64String (challenge2); - var type2 = new Type2Message (token, 0, token.Length); - var type3 = new Type3Message (type2, null, sasl.Level, sasl.Credentials.UserName, sasl.Credentials.Password, sasl.Workstation); - var ignoreLength = 48; + var type1 = DecodeType1Message (challenge1); + var type2 = DecodeType2Message (challenge2); + var type3 = new Type3Message (type1, type2, sasl.Credentials.UserName, sasl.Credentials.Password, sasl.Workstation) { + ClientChallenge = nonce, + Timestamp = timestamp + }; + type3.ComputeNtlmV2 (null, false, null); var actual = Convert.FromBase64String (challenge); var expected = type3.Encode (); Assert.AreEqual (expected.Length, actual.Length, "Final challenge differs in length: {0} vs {1}", expected.Length, actual.Length); - for (int i = 0; i < expected.Length - ignoreLength; i++) + for (int i = 0; i < expected.Length; i++) Assert.AreEqual (expected[i], actual[i], "Final challenge differs at index {0}", i); Assert.IsTrue (sasl.IsAuthenticated, "IsAuthenticated"); } [Test] - public void TestAuthenticationLmAndNtlmSessionNegotiateNtlm2Key () + public void TestAuthenticationNtlmv2 () { - const string challenge1 = "TlRMTVNTUAABAAAABwIAAAAAAAAgAAAAAAAAACAAAAA="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAokAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; + const string challenge1 = "TlRMTVNTUAABAAAAB4IIoAAAAAAgAAAAAAAAACAAAAA="; + const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; var credentials = new NetworkCredential ("user", "password"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.LM_and_NTLM_and_try_NTLMv2_Session }; + var sasl = new SaslMechanismNtlm (credentials) { OSVersion = null, Workstation = null }; - AssertNtlm2Key (sasl, challenge1, challenge2); + AssertNtlmv2 (sasl, challenge1, challenge2); } [Test] - public void TestAuthenticationLmAndNtlmSessionNegotiateNtlm2KeyWithDomain () + public void TestAuthenticationNtlmv2WithDomain () { - const string challenge1 = "TlRMTVNTUAABAAAABxIAAAYABgAgAAAAAAAAACAAAABET01BSU4="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAokAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; + const string challenge1 = "TlRMTVNTUAABAAAAB5IIoAYABgAgAAAAAAAAACAAAABET01BSU4="; + const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.LM_and_NTLM_and_try_NTLMv2_Session }; + var sasl = new SaslMechanismNtlm (credentials) { OSVersion = null, Workstation = null }; - AssertNtlm2Key (sasl, challenge1, challenge2); + AssertNtlmv2 (sasl, challenge1, challenge2); } [Test] - public void TestAuthenticationLmAndNtlmSessionNegotiateNtlm2KeyWithDomainAndWorkstation () + public void TestAuthenticationNtlmv2WithDomainAndWorkstation () { - const string challenge1 = "TlRMTVNTUAABAAAABzIAAAYABgArAAAACwALACAAAABXT1JLU1RBVElPTkRPTUFJTg=="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAokAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; + const string challenge1 = "TlRMTVNTUAABAAAAB7IIoAYABgArAAAACwALACAAAABXT1JLU1RBVElPTkRPTUFJTg=="; + const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Workstation = "WORKSTATION", Level = NtlmAuthLevel.LM_and_NTLM_and_try_NTLMv2_Session }; + var sasl = new SaslMechanismNtlm (credentials) { OSVersion = null, Workstation = "WORKSTATION" }; - AssertNtlm2Key (sasl, challenge1, challenge2); + AssertNtlmv2 (sasl, challenge1, challenge2); } - [Test] - public void TestAuthenticationNtlmNegotiateNtlm2Key () - { - const string challenge1 = "TlRMTVNTUAABAAAABwIAAAAAAAAgAAAAAAAAACAAAAA="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAokAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - var credentials = new NetworkCredential ("user", "password"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.NTLM_only }; - - AssertNtlm2Key (sasl, challenge1, challenge2); - } + // From Section 4.2.4.3 + static byte[] ExampleNtlmV2ChallengeMessage = new byte[] { + 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x0c, 0x00, + 0x38, 0x00, 0x00, 0x00, 0x33, 0x82, 0x8a, 0xe2, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x24, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x70, 0x17, 0x00, 0x00, 0x00, 0x0f, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x02, 0x00, 0x0c, 0x00, 0x44, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, + 0x69, 0x00, 0x6e, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00 + }; - [Test] - public void TestAuthenticationNtlmNegotiateNtlm2KeyWithDomain () - { - const string challenge1 = "TlRMTVNTUAABAAAABxIAAAYABgAgAAAAAAAAACAAAABET01BSU4="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAokAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.NTLM_only }; + // From Section 4.2.4.3 + static byte[] ExampleNtlmV2AuthenticateMessageOriginal = new byte[] { + 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x03, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x00, + 0x6c, 0x00, 0x00, 0x00, 0x54, 0x00, 0x54, 0x00, 0x84, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x0c, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x54, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, + 0x5c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0xd8, 0x00, 0x00, 0x00, 0x35, 0x82, 0x88, 0xe2, + 0x05, 0x01, 0x28, 0x0a, 0x00, 0x00, 0x00, 0x0f, 0x44, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, + 0x69, 0x00, 0x6e, 0x00, 0x55, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x43, 0x00, 0x4f, 0x00, + 0x4d, 0x00, 0x50, 0x00, 0x55, 0x00, 0x54, 0x00, 0x45, 0x00, 0x52, 0x00, 0x86, 0xc3, 0x50, 0x97, + 0xac, 0x9c, 0xec, 0x10, 0x25, 0x54, 0x76, 0x4a, 0x57, 0xcc, 0xcc, 0x19, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0x68, 0xcd, 0x0a, 0xb8, 0x51, 0xe5, 0x1c, 0x96, 0xaa, 0xbc, 0x92, 0x7b, + 0xeb, 0xef, 0x6a, 0x1c, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x0c, 0x00, 0x44, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, + 0x01, 0x00, 0x0c, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc5, 0xda, 0xd2, 0x54, 0x4f, 0xc9, 0x79, 0x90, + 0x94, 0xce, 0x1c, 0xe9, 0x0b, 0xc9, 0xd0, 0x3e + }; - AssertNtlm2Key (sasl, challenge1, challenge2); - } + // This is the modified version that includes the ChannelBinding=Z16 and TargetName="" string in the TargetInfo embedded in the NTChallengeResponse. + static readonly byte[] ExampleNtlmV2AuthenticateMessage = { + 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x03, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x00, + 0x6c, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x84, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x0c, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x54, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, + 0x5c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x35, 0x82, 0x88, 0xe2, + 0x05, 0x01, 0x28, 0x0a, 0x00, 0x00, 0x00, 0x0f, 0x44, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, + 0x69, 0x00, 0x6e, 0x00, 0x55, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x43, 0x00, 0x4f, 0x00, + 0x4d, 0x00, 0x50, 0x00, 0x55, 0x00, 0x54, 0x00, 0x45, 0x00, 0x52, 0x00, 0x86, 0xc3, 0x50, 0x97, + 0xac, 0x9c, 0xec, 0x10, 0x25, 0x54, 0x76, 0x4a, 0x57, 0xcc, 0xcc, 0x19, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xf3, 0x37, 0xab, 0x75, 0x7a, 0xbc, 0x12, 0xf7, 0x68, 0x10, 0x55, 0x60, + 0xb4, 0xcb, 0x30, 0x17, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x0c, 0x00, 0x44, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, + 0x01, 0x00, 0x0c, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; - [Test] - public void TestAuthenticationNtlmNegotiateNtlm2KeyWithDomainAndWorkstation () + static NtlmTargetInfo GetNtChallengeResponseTargetInfo (byte[] ntChallengeResponse) { - const string challenge1 = "TlRMTVNTUAABAAAABzIAAAYABgArAAAACwALACAAAABXT1JLU1RBVElPTkRPTUFJTg=="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAokAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Workstation = "WORKSTATION", Level = NtlmAuthLevel.NTLM_only }; + int index = 0; - AssertNtlm2Key (sasl, challenge1, challenge2); - } - - static void AssertNtlm (SaslMechanismNtlm sasl, string challenge1, string challenge2, string challenge3) - { - var challenge = sasl.Challenge (string.Empty); + // Proof (16-bytes) HMACMD5 of the following data + index += 16; - Assert.AreEqual (challenge1, challenge, "Initial challenge"); - Assert.IsFalse (sasl.IsAuthenticated, "IsAuthenticated"); + // 2 bytes of version info + index += 2; - challenge = sasl.Challenge (challenge2); + // Z6 + index += 6; - Assert.AreEqual (challenge3, challenge, "Final challenge"); - Assert.IsTrue (sasl.IsAuthenticated, "IsAuthenticated"); - } + // Timestamp + index += 8; - [Test] - public void TestAuthenticationNtlm () - { - const string challenge1 = "TlRMTVNTUAABAAAABwIAAAAAAAAgAAAAAAAAACAAAAA="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - const string challenge3 = "TlRMTVNTUAADAAAAAAAAAFQAAAAYABgAVAAAAAwADABAAAAACAAIAEwAAAAAAAAAVAAAAAAAAABsAAAAAQKAAEQATwBNAEEASQBOAHUAcwBlAHIA3VQosB6G9N/Kvqw5SUbb1D7oj3lN1jJV"; - var credentials = new NetworkCredential ("user", "password"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.NTLM_only }; + // ClientChallenge + index += 8; - AssertNtlm (sasl, challenge1, challenge2, challenge3); - } + // Z4 + index += 4; - [Test] - public void TestAuthenticationNtlmWithDomain () - { - const string challenge1 = "TlRMTVNTUAABAAAABxIAAAYABgAgAAAAAAAAACAAAABET01BSU4="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - const string challenge3 = "TlRMTVNTUAADAAAAAAAAAFQAAAAYABgAVAAAAAwADABAAAAACAAIAEwAAAAAAAAAVAAAAAAAAABsAAAAAQKAAEQATwBNAEEASQBOAHUAcwBlAHIA3VQosB6G9N/Kvqw5SUbb1D7oj3lN1jJV"; - var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.NTLM_only }; + // TargetInfo followed by Z4 + int targetInfoLength = (ntChallengeResponse.Length - 4) - index; - AssertNtlm (sasl, challenge1, challenge2, challenge3); + return new NtlmTargetInfo (ntChallengeResponse, index, targetInfoLength, true); } [Test] - public void TestAuthenticationNtlmWithDomainAndWorkstation () - { - const string challenge1 = "TlRMTVNTUAABAAAABzIAAAYABgArAAAACwALACAAAABXT1JLU1RBVElPTkRPTUFJTg=="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - const string challenge3 = "TlRMTVNTUAADAAAAAAAAAGoAAAAYABgAagAAAAwADABAAAAACAAIAEwAAAAWABYAVAAAAAAAAACCAAAAAQKAAEQATwBNAEEASQBOAHUAcwBlAHIAVwBPAFIASwBTAFQAQQBUAEkATwBOAN1UKLAehvTfyr6sOUlG29Q+6I95TdYyVQ=="; - var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Workstation = "WORKSTATION", Level = NtlmAuthLevel.NTLM_only }; - - AssertNtlm (sasl, challenge1, challenge2, challenge3); - } - - static void AssertNtlmv2 (SaslMechanismNtlm sasl, string challenge1, string challenge2) - { - var challenge = sasl.Challenge (string.Empty); - - Assert.AreEqual (challenge1, challenge, "Initial challenge"); - Assert.IsFalse (sasl.IsAuthenticated, "IsAuthenticated"); - - challenge = sasl.Challenge (challenge2); - - var token = Convert.FromBase64String (challenge2); - var type2 = new Type2Message (token, 0, token.Length); - var type3 = new Type3Message (type2, null, sasl.Level, sasl.Credentials.UserName, sasl.Credentials.Password, sasl.Workstation); - var ignoreLength = type2.EncodedTargetInfo.Length + 28 + 16; - - var actual = Convert.FromBase64String (challenge); - var expected = type3.Encode (); - var ntlmBufferIndex = expected.Length - ignoreLength; - var targetInfoIndex = ntlmBufferIndex + 16 /* md5 hash */ + 28; - - Assert.AreEqual (expected.Length, actual.Length, "Final challenge differs in length: {0} vs {1}", expected.Length, actual.Length); - - for (int i = 0; i < expected.Length - ignoreLength; i++) - Assert.AreEqual (expected[i], actual[i], "Final challenge differs at index {0}", i); - - // now compare the TargetInfo blobs - for (int i = targetInfoIndex; i < expected.Length; i++) - Assert.AreEqual (expected[i], actual[i], "Final challenge differs at index {0}", i); - - Assert.IsTrue (sasl.IsAuthenticated, "IsAuthenticated"); - } + public void TestNtlmv2Example () + { + var flags = NtlmFlags.RequestTarget | NtlmFlags.NegotiateKeyExchange | NtlmFlags.Negotiate56 | NtlmFlags.Negotiate128 | NtlmFlags.NegotiateVersion | NtlmFlags.NegotiateTargetInfo | NtlmFlags.NegotiateExtendedSessionSecurity | + NtlmFlags.NegotiateAlwaysSign | NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateSeal | NtlmFlags.NegotiateSign | NtlmFlags.NegotiateOem | NtlmFlags.NegotiateUnicode; + var type1 = new Type1Message (flags, "", "", new Version (5, 1, 2600)); + + var type2 = new Type2Message (ExampleNtlmV2ChallengeMessage, 0, ExampleNtlmV2ChallengeMessage.Length); + Assert.AreEqual ("Server", type2.TargetName, "TargetName"); + Assert.AreEqual ("Server", type2.TargetInfo.ServerName, "ServerName"); + Assert.AreEqual ("Domain", type2.TargetInfo.DomainName, "DomainName"); + + // Note: Had to reverse engineer these values from the example. The nonce is the last 8 bytes of the lmChallengeResponse + // and the timestamp was bytes 8-16 of the 'temp' buffer. + var nonce = new byte[] { 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa }; + var timestamp = DateTime.FromFileTimeUtc (0).Ticks; + + var expectedType3 = new Type3Message (ExampleNtlmV2AuthenticateMessage, 0, ExampleNtlmV2AuthenticateMessage.Length); + var expectedTargetInfo = GetNtChallengeResponseTargetInfo (expectedType3.NtChallengeResponse); + var type3 = new Type3Message (type1, type2, "User", "Password", "COMPUTER") { + ClientChallenge = nonce, + Timestamp = timestamp + }; + type3.ComputeNtlmV2 (null, false, null); - [Test] - public void TestAuthenticationNtlmv2 () - { - const string challenge1 = "TlRMTVNTUAABAAAABwIAAAAAAAAgAAAAAAAAACAAAAA="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - var credentials = new NetworkCredential ("user", "password"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.NTLMv2_only }; + var actualTargetInfo = GetNtChallengeResponseTargetInfo (type3.NtChallengeResponse); + var actual = type3.Encode (); - AssertNtlmv2 (sasl, challenge1, challenge2); - } + //var initializer = ToCSharpByteArrayInitializer ("ExampleNtlmV2AuthenticateMessage", actual); - [Test] - public void TestAuthenticationNtlmv2WithDomain () - { - const string challenge1 = "TlRMTVNTUAABAAAABxIAAAYABgAgAAAAAAAAACAAAABET01BSU4="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Level = NtlmAuthLevel.NTLMv2_only }; + Assert.AreEqual (ExampleNtlmV2AuthenticateMessage.Length, actual.Length, "Raw message lengths differ."); - AssertNtlmv2 (sasl, challenge1, challenge2); + /// Note: The EncryptedRandomSessionKey is random and is the last 16 bytes of the message. + for (int i = 0; i < ExampleNtlmV2AuthenticateMessage.Length - 16; i++) + Assert.AreEqual (ExampleNtlmV2AuthenticateMessage[i], actual[i], $"Messages differ at index [{i}]"); } [Test] - public void TestAuthenticationNtlmv2WithDomainAndWorkstation () - { - const string challenge1 = "TlRMTVNTUAABAAAABzIAAAYABgArAAAACwALACAAAABXT1JLU1RBVElPTkRPTUFJTg=="; - const string challenge2 = "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; - var credentials = new NetworkCredential ("user", "password", "DOMAIN"); - var sasl = new SaslMechanismNtlm (credentials) { Workstation = "WORKSTATION", Level = NtlmAuthLevel.NTLMv2_only }; - - AssertNtlmv2 (sasl, challenge1, challenge2); + public void TestSystemNetMailNtlmNegotiation () + { + const string challenge1 = "TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAKAO5CAAAADw=="; + const string challenge2 = "TlRMTVNTUAACAAAADAAMADgAAAAFgomi18THmUUjMM4AAAAAAAAAAMoAygBEAAAABgOAJQAAAA9EAEUAVgBEAEkAVgACAAwARABFAFYARABJAFYAAQAQAEUAWABDAEgAQQBOAEcARQAEACgAZABlAHYAZABpAHYALgBtAGkAYwByAG8AcwBvAGYAdAAuAGMAbwBtAAMAOgBlAHgAYwBoAGEAbgBnAGUALgBkAGUAdgBkAGkAdgAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ABQAoAGQAZQB2AGQAaQB2AC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAHAAgAS9aGGn7B1gEAAAAATlRMTVNTUAACAAAADAAMADgAAAAFgomi18THmUUjMM4AAAAAAAAAAMoAygBEAAAABgOAJQAAAA9EAEUAVgBEAEkAVgACAAwARABFAFYARABJAFYAAQAQAEUAWABDAEgAQQBOAEcARQAEACgAZABlAHYAZABpAHYALgBtAGkAYwByAG8AcwBvAGYAdAAuAGMAbwBtAAMAOgBlAHgAYwBoAGEAbgBnAGUALgBkAGUAdgBkAGkAdgAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ABQAoAGQAZQB2AGQAaQB2AC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAHAAgAS9aGGn7B1gEAAAAA"; + const string challenge3 = "TlRMTVNTUAADAAAAGAAYAIoAAABAAUABogAAAAwADABYAAAAEAAQAGQAAAAWABYAdAAAAAAAAADiAQAABYIIogoA7kIAAAAPDYFh2Vjzwk5e9YHnWRvYnUQARQBWAEQASQBWAHUAcwBlAHIAbgBhAG0AZQBXAG8AcgBrAHMAdABhAHQAaQBvAG4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbqz8wqlkKSdjmeI+rX9+lwEBAAAAAAAAS9aGGn7B1gGZ26Mto5srdQAAAAACAAwARABFAFYARABJAFYAAQAQAEUAWABDAEgAQQBOAEcARQAEACgAZABlAHYAZABpAHYALgBtAGkAYwByAG8AcwBvAGYAdAAuAGMAbwBtAAMAOgBlAHgAYwBoAGEAbgBnAGUALgBkAGUAdgBkAGkAdgAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ABQAoAGQAZQB2AGQAaQB2AC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAHAAgAS9aGGn7B1gEGAAQAAgAAAAkAJgBTAE0AVABQAFMAVgBDAC8AMQA5ADIALgAxADYAOAAuADEALgAxAAoAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + var type1 = DecodeType1Message (challenge1); + //var type2 = DecodeType2Message (challenge2); + var type3 = DecodeType3Message (challenge3); + + // This is what System.Net.Mail sends as the initial challenge. + Assert.AreEqual (Type1Message.DefaultFlags | NtlmFlags.NegotiateAlwaysSign | NtlmFlags.NegotiateVersion | NtlmFlags.Negotiate56, type1.Flags, "System.Net.Mail Initial Flags"); + + var ntlm = new SaslMechanismNtlm ("username", "password") { + ServicePrincipalName = "SMTPSVC/192.168.1.1", + OSVersion = new Version (10, 0, 17134), + Workstation = "Workstation" + }; + var challenge = ntlm.Challenge (null); + + Assert.AreEqual ("TlRMTVNTUAABAAAAB4IIogAAAAAoAAAAAAAAACgAAAAKAO5CAAAADw==", challenge, "MailKit Initial Challenge"); + + challenge = ntlm.Challenge (challenge2); + var auth = DecodeType3Message (challenge); + + //Assert.AreEqual (type3.Domain, auth.Domain, "Domain"); + Assert.AreEqual (type3.UserName, auth.UserName, "UserName"); + Assert.AreEqual (type3.Workstation, auth.Workstation, "Workstation"); + Assert.AreEqual (type3.OSVersion, auth.OSVersion, "OSVersion"); + + Assert.AreEqual (type3.LmChallengeResponse.Length, auth.LmChallengeResponse.Length, "LmChallengeResponseLength"); + for (int i = 0; i < auth.LmChallengeResponse.Length; i++) + Assert.AreEqual (0, auth.LmChallengeResponse[i], $"LmChallengeResponse[{i}]"); + Assert.NotNull (auth.Mic, "Mic"); + Assert.AreEqual (type3.Mic.Length, auth.Mic.Length, "Mic"); + + var targetInfo = GetNtChallengeResponseTargetInfo (auth.NtChallengeResponse); + var expected = GetNtChallengeResponseTargetInfo (type3.NtChallengeResponse); + Assert.NotNull (targetInfo.ChannelBinding, "ChannelBinding"); + Assert.AreEqual (expected.ChannelBinding.Length, targetInfo.ChannelBinding.Length, "ChannelBinding"); + Assert.AreEqual (expected.ServerName, targetInfo.ServerName, "ServerName"); + Assert.AreEqual (expected.DomainName, targetInfo.DomainName, "DomainName"); + Assert.AreEqual (expected.DnsServerName, targetInfo.DnsServerName, "DnsServerName"); + Assert.AreEqual (expected.DnsDomainName, targetInfo.DnsDomainName, "DnsDomainName"); + Assert.AreEqual (expected.DnsTreeName, targetInfo.DnsTreeName, "DnsTreeName"); + Assert.AreEqual (expected.Flags, targetInfo.Flags, "Flags"); + Assert.AreEqual (expected.Timestamp, targetInfo.Timestamp, "Timestamp"); + + Console.WriteLine (); } } } diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 209f620472..1be06a9cb2 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -79,6 +79,12 @@ + + + + + +