commit 235d86668cc12cce2a3e291f34a8c77e5fd2d78b Author: smiley22 Date: Mon Jan 6 09:27:48 2014 +0100 Initial Commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..412eeda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e985ccd --- /dev/null +++ b/.gitignore @@ -0,0 +1,167 @@ +# Ignore nuspec files from NuGet +*.nuspec +Tools/ + +################# +## Eclipse +################# + +*.pydevproject +.project +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + + +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Rr]elease/ +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.vspscc +.builds +*.dotCover + +## TODO: If you have NuGet Package Restore enabled, uncomment this +#packages/ + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp + +# ReSharper is a .NET coding add-in +_ReSharper* + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish + +# Others +[Bb]in +[Oo]bj +sql +TestResults +*.Cache +ClientBin +stylecop.* +~$* +*.dbmdl +Generated_Code #added for RIA/Silverlight projects + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML + + + +############ +## Windows +############ + +# Windows image file caches +Thumbs.db + +# Folder config file +Desktop.ini + + +############# +## Python +############# + +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg + +# Mac crap +.DS_Store diff --git a/Extensions.cs b/Extensions.cs new file mode 100644 index 0000000..ef50cf7 --- /dev/null +++ b/Extensions.cs @@ -0,0 +1,55 @@ +using System; + +namespace S22.Sasl { + internal static class Extensions { + /// + /// Adds a couple of useful extensions to reference types. + /// + /// + /// Throws an ArgumentNullException if the given data item is null. + /// + /// The item to check for nullity. + /// The name to use when throwing an + /// exception, if necessary + public static void ThrowIfNull(this T data, string name) + where T : class { + if (data == null) + throw new ArgumentNullException(name); + } + + /// + /// Throws an ArgumentNullException if the given data item is null. + /// + /// The item to check for nullity. + public static void ThrowIfNull(this T data) + where T : class { + if (data == null) + throw new ArgumentNullException(); + } + + /// + /// Throws an ArgumentException if the given string is null or + /// empty. + /// + /// The string to check for nullity and + /// emptiness. + public static void ThrowIfNullOrEmpty(this string data) { + if (String.IsNullOrEmpty(data)) + throw new ArgumentException(); + } + + /// + /// Throws an ArgumentException if the given string is null or + /// empty. + /// + /// The string to check for nullity and + /// emptiness. + /// The name to use when throwing an + /// exception, if necessary + public static void ThrowIfNullOrEmpty(this string data, string name) { + if (String.IsNullOrEmpty(data)) + throw new ArgumentException("The " + name + + " parameter must not be null or empty"); + } + } +} diff --git a/License.md b/License.md new file mode 100644 index 0000000..6c1a4ba --- /dev/null +++ b/License.md @@ -0,0 +1,22 @@ +### The MIT License + +Copyright (c) 2013-2014 Torben Könke + +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. \ No newline at end of file diff --git a/Mechanisms/ByteBuilder.cs b/Mechanisms/ByteBuilder.cs new file mode 100644 index 0000000..33e14f6 --- /dev/null +++ b/Mechanisms/ByteBuilder.cs @@ -0,0 +1,195 @@ +using System; +using System.Text; + +namespace S22.Sasl { + /// + /// A utility class modeled after the BCL StringBuilder to simplify + /// building binary-data messages. + /// + internal class ByteBuilder { + /// + /// The actual byte buffer. + /// + byte[] buffer = new byte[1024]; + + /// + /// The current position in the buffer. + /// + int position = 0; + + /// + /// The length of the underlying data buffer. + /// + public int Length { + get { + return position; + } + } + + /// + /// Resizes the internal byte buffer. + /// + /// Amount in bytes by which to increase the + /// size of the buffer. + void Resize(int amount = 1024) { + byte[] newBuffer = new byte[buffer.Length + amount]; + Array.Copy(buffer, newBuffer, buffer.Length); + buffer = newBuffer; + } + + /// + /// Appends one or several byte values to this instance. + /// + /// Byte values to append. + /// A reference to the calling instance. + public ByteBuilder Append(params byte[] values) { + if ((position + values.Length) >= buffer.Length) + Resize(); + foreach (byte b in values) + buffer[position++] = b; + return this; + } + + /// + /// Appends the specified number of bytes from the specified buffer + /// starting at the specified offset to this instance. + /// + /// The buffer to append bytes from. + /// The offset into the buffert at which to start + /// reading bytes from. + /// The number of bytes to read from the buffer. + /// A reference to the calling instance. + public ByteBuilder Append(byte[] buffer, int offset, int count) { + if ((position + count) >= buffer.Length) + Resize(); + for (int i = 0; i < count; i++) + this.buffer[position++] = buffer[offset + i]; + return this; + } + + /// + /// Appends the specified 32-bit integer value to this instance. + /// + /// A 32-bit integer value to append. + /// Set this to true, to append the value as + /// big-endian. + /// A reference to the calling instance. + public ByteBuilder Append(int value, bool bigEndian = false) { + if ((position + 4) >= buffer.Length) + Resize(); + int[] o = bigEndian ? new int[4] { 3, 2, 1, 0 } : + new int[4] { 0, 1, 2, 3 }; + for (int i = 0; i < 4; i++) + buffer[position++] = (byte) ((value >> (o[i] * 8)) & 0xFF); + return this; + } + + /// + /// Appends the specified 16-bit short value to this instance. + /// + /// A 16-bit short value to append. + /// Set this to true, to append the value as + /// big-endian. + /// A reference to the calling instance. + public ByteBuilder Append(short value, bool bigEndian = false) { + if ((position + 2) >= buffer.Length) + Resize(); + int[] o = bigEndian ? new int[2] { 1, 0 } : + new int[2] { 0, 1 }; + for (int i = 0; i < 2; i++) + buffer[position++] = (byte) ((value >> (o[i] * 8)) & 0xFF); + return this; + } + + /// + /// Appends the specified 16-bit unsigend short value to this instance. + /// + /// A 16-bit unsigend short value to append. + /// Set this to true, to append the value as + /// big-endian. + /// A reference to the calling instance. + public ByteBuilder Append(ushort value, bool bigEndian = false) { + if ((position + 2) >= buffer.Length) + Resize(); + int[] o = bigEndian ? new int[2] { 1, 0 } : + new int[2] { 0, 1 }; + for (int i = 0; i < 2; i++) + buffer[position++] = (byte) ((value >> (o[i] * 8)) & 0xFF); + return this; + } + + /// + /// Appends the specified 32-bit unsigned integer value to this instance. + /// + /// A 32-bit unsigned integer value to append. + /// Set this to true, to append the value as + /// big-endian. + /// A reference to the calling instance. + public ByteBuilder Append(uint value, bool bigEndian = false) { + if ((position + 4) >= buffer.Length) + Resize(); + int[] o = bigEndian ? new int[4] { 3, 2, 1, 0 } : + new int[4] { 0, 1, 2, 3 }; + for (int i = 0; i < 4; i++) + buffer[position++] = (byte) ((value >> (o[i] * 8)) & 0xFF); + return this; + } + + /// + /// Appends the specified 64-bit integer value to this instance. + /// + /// A 64-bit integer value to append. + /// Set this to true, to append the value as + /// big-endian. + /// A reference to the calling instance. + public ByteBuilder Append(long value, bool bigEndian = false) { + if ((position + 8) >= buffer.Length) + Resize(); + int[] o = bigEndian ? new int[8] { 7, 6, 5, 4, 3, 2, 1, 0 } : + new int[8] { 0, 1, 2, 3, 4, 5, 6, 7 }; + for (int i = 0; i < 8; i++) + buffer[position++] = (byte) ((value >> (o[i] * 8)) & 0xFF); + return this; + + } + + /// + /// Appends the specified string using the specified encoding to this + /// instance. + /// + /// The string vale to append. + /// The encoding to use for decoding the string value + /// into a sequence of bytes. If this is null, ASCII encoding is used as a + /// default. + /// A reference to the calling instance. + public ByteBuilder Append(string value, Encoding encoding = null) { + if (encoding == null) + encoding = Encoding.ASCII; + byte[] bytes = encoding.GetBytes(value); + if ((position + bytes.Length) >= buffer.Length) + Resize(); + foreach (byte b in bytes) + buffer[position++] = b; + return this; + } + + /// + /// Returns the ByteBuilder's content as an array of bytes. + /// + /// An array of bytes. + public byte[] ToArray() { + // Fixme: Do this properly. + byte[] b = new byte[position]; + Array.Copy(buffer, b, position); + return b; + } + + /// + /// Removes all bytes from the current ByteBuilder instance. + /// + public void Clear() { + buffer = new byte[1024]; + position = 0; + } + } +} diff --git a/Mechanisms/Ntlm/Extensions.cs b/Mechanisms/Ntlm/Extensions.cs new file mode 100644 index 0000000..b5999c9 --- /dev/null +++ b/Mechanisms/Ntlm/Extensions.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Text; + +namespace S22.Sasl.Mechanisms.Ntlm { + /// + /// Adds extension methods to the BinaryReader class to simplify the + /// deserialization of NTLM messages. + /// + internal static class BinaryReaderExtensions { + /// + /// Reads an ASCII-string of the specified length from this instance. + /// + /// Extension method for the BinaryReader class. + /// The number of bytes to read from the underlying + /// stream. + /// A string decoded from the bytes read from the underlying + /// stream using the ASCII character set. + public static string ReadASCIIString(this BinaryReader reader, int count) { + ByteBuilder builder = new ByteBuilder(); + int read = 0; + while (true) { + if (read++ >= count) + break; + byte b = reader.ReadByte(); + builder.Append(b); + } + return Encoding.ASCII.GetString(builder.ToArray()).TrimEnd('\0'); + } + } +} diff --git a/Mechanisms/Ntlm/Flags.cs b/Mechanisms/Ntlm/Flags.cs new file mode 100644 index 0000000..9efcbc3 --- /dev/null +++ b/Mechanisms/Ntlm/Flags.cs @@ -0,0 +1,139 @@ +using System; + +namespace S22.Sasl.Mechanisms.Ntlm { + /// + /// The NTLM flags which are contained in a bitfield within the header of + /// an NTLM message. + /// + [Flags] + internal enum Flags { + /// + /// Indicates that Unicode strings are supported for use in security + /// buffer data. + /// + NegotiateUnicode = 0x00000001, + /// + /// Indicates that OEM strings are supported for use in security + /// buffer data. + /// + NegotiateOEM = 0x00000002, + /// + /// Requests that the server's authentication realm be included in + /// the Type 2 message. + /// + RequestTarget = 0x00000004, + /// + /// Specifies that authenticated communication between the client and + /// server should carry a digital signature (message integrity). + /// + NegotiateSign = 0x00000010, + /// + /// Specifies that authenticated communication between the client and + /// server should be encrypted (message confidentiality). + /// + NegotiateSeal = 0x00000020, + /// + /// Indicates that datagram authentication is being used. + /// + NegotiateDatagramStyle = 0x00000040, + /// + /// Indicates that the Lan Manager Session Key should be used for signing + /// and sealing authenticated communications. + /// + NegotiateLanManagerKey = 0x00000080, + /// + /// This flag's usage has not been identified. + /// + NegotiateNetware = 0x00000100, + /// + /// Indicates that NTLM authentication is being used. + /// + NegotiateNTLM = 0x00000200, + /// + /// Sent by the client in the Type 3 message to indicate that an anonymous + /// context has been established. This also affects the response fields. + /// + NegotiateAnonymous = 0x00000800, + /// + /// Sent by the client in the Type 1 message to indicate that the name of + /// the domain in which the client workstation has membership is included + /// in the message. This is used by the server to determine whether the + /// client is eligible for local authentication. + /// + NegotiateDomainSupplied = 0x00001000, + /// + /// Sent by the client in the Type 1 message to indicate that the client + /// workstation's name is included in the message. This is used by the + /// server to determine whether the client is eligible for local + /// authentication. + /// + NegotiateWorkstationSupplied = 0x00002000, + /// + /// Sent by the server to indicate that the server and client are on the + /// same machine. Implies that the client may use the established local + /// credentials for authentication instead of calculating a response to + /// the challenge. + /// + NegotiateLocalCall = 0x00004000, + /// + /// Indicates that authenticated communication between the client and + /// server should be signed with a "dummy" signature. + /// + NegotiateAlwaysSign = 0x00008000, + /// + /// Sent by the server in the Type 2 message to indicate that the target + /// authentication realm is a domain. + /// + TargetTypeDomain = 0x00010000, + /// + /// Sent by the server in the Type 2 message to indicate that the target + /// authentication realm is a server. + /// + TargetTypeServer = 0x00020000, + /// + /// Sent by the server in the Type 2 message to indicate that the target + /// authentication realm is a share. Presumably, this is for share-level + /// authentication. Usage is unclear. + /// + TargetTypeShare = 0x00040000, + /// + /// 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. + /// + NegotiateNTLM2Key = 0x00080000, + /// + /// This flag's usage has not been identified. + /// + RequestInitResponse = 0x00100000, + /// + /// This flag's usage has not been identified. + /// + RequestAcceptResponse = 0x00200000, + /// + /// This flag's usage has not been identified. + /// + RequestNonNTSessionKey = 0x00400000, + /// + /// Sent by the server in the Type 2 message to indicate that it is including + /// a Target Information block in the message. The Target Information block + /// is used in the calculation of the NTLMv2 response. + /// + NegotiateTargetInfo = 0x00800000, + /// + /// Indicates that 128-bit encryption is supported. + /// + Negotiate128 = 0x20000000, + /// + /// Indicates that the client will provide an encrypted master key in the + /// "Session Key" field of the Type 3 message. + /// + NegotiateKeyExchange = 0x40000000, + /// + /// Indicates that 56-bit encryption is supported. + /// + Negotiate56 + } +} diff --git a/Mechanisms/Ntlm/Helpers.cs b/Mechanisms/Ntlm/Helpers.cs new file mode 100644 index 0000000..427d464 --- /dev/null +++ b/Mechanisms/Ntlm/Helpers.cs @@ -0,0 +1,91 @@ + +namespace S22.Sasl.Mechanisms.Ntlm { + /// + /// Represents the data contained in the target information block of an + /// NTLM type 2 message. + /// + internal class Type2TargetInformation { + /// + /// The server name. + /// + public string ServerName { + get; + set; + } + + /// + /// The domain name. + /// + public string DomainName { + get; + set; + } + + /// + /// The fully-qualified DNS host name. + /// + public string DnsHostname { + get; + set; + } + + /// + /// The fully-qualified DNS domain name. + /// + public string DnsDomainName { + get; + set; + } + } + + /// + /// Describes the different versions of the Type 2 message that have + /// been observed. + /// + internal enum Type2Version { + /// + /// The version is unknown. + /// + Unknown = 0, + /// + /// This form is seen in older Win9x-based systems. + /// + Version1 = 32, + /// + /// This form is seen in most out-of-box shipping versions of Windows. + /// + Version2 = 48, + /// + /// This form was introduced in a relatively recent Service Pack, and + /// is seen on currently-patched versions of Windows 2000, Windows XP, + /// and Windows 2003. + /// + Version3 = 56, + } + + /// + /// Indicates the type of data in Type 2 target information blocks. + /// + internal enum Type2InformationType { + /// + /// Signals the end of the target information block. + /// + TerminatorBlock = 0, + /// + /// The data in the information block contains the server name. + /// + ServerName = 1, + /// + /// The data in the information block contains the domain name. + /// + DomainName = 2, + /// + /// The data in the information block contains the DNS hostname. + /// + DnsHostname = 3, + /// + /// The data in the information block contans the DNS domain name. + /// + DnsDomainName = 4 + } +} diff --git a/Mechanisms/Ntlm/MD4.cs b/Mechanisms/Ntlm/MD4.cs new file mode 100644 index 0000000..23072a7 --- /dev/null +++ b/Mechanisms/Ntlm/MD4.cs @@ -0,0 +1,146 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; + +namespace S22.Sasl.Mechanisms.Ntlm { + /// + /// Computes the MD4 hash value for the input data. + /// Courtesy of Keith Wood. + /// + internal class MD4 : HashAlgorithm { + private uint _a; + private uint _b; + private uint _c; + private uint _d; + private uint[] _x; + private int _bytesProcessed; + + public MD4() { + _x = new uint[16]; + + Initialize(); + } + + public override void Initialize() { + _a = 0x67452301; + _b = 0xefcdab89; + _c = 0x98badcfe; + _d = 0x10325476; + + _bytesProcessed = 0; + } + + protected override void HashCore(byte[] array, int offset, int length) { + ProcessMessage(Bytes(array, offset, length)); + } + + protected override byte[] HashFinal() { + try { + ProcessMessage(Padding()); + + return new[] { _a, _b, _c, _d }.SelectMany(word => Bytes(word)).ToArray(); + } finally { + Initialize(); + } + } + + private void ProcessMessage(IEnumerable bytes) { + foreach (byte b in bytes) { + int c = _bytesProcessed & 63; + int i = c >> 2; + int s = (c & 3) << 3; + + _x[i] = (_x[i] & ~((uint) 255 << s)) | ((uint) b << s); + + if (c == 63) { + Process16WordBlock(); + } + + _bytesProcessed++; + } + } + + private static IEnumerable Bytes(byte[] bytes, int offset, int length) { + for (int i = offset; i < length; i++) { + yield return bytes[i]; + } + } + + private IEnumerable Bytes(uint word) { + yield return (byte) (word & 255); + yield return (byte) ((word >> 8) & 255); + yield return (byte) ((word >> 16) & 255); + yield return (byte) ((word >> 24) & 255); + } + + private IEnumerable Repeat(byte value, int count) { + for (int i = 0; i < count; i++) { + yield return value; + } + } + + private IEnumerable Padding() { + return Repeat(128, 1) + .Concat(Repeat(0, ((_bytesProcessed + 8) & 0x7fffffc0) + 55 - _bytesProcessed)) + .Concat(Bytes((uint) _bytesProcessed << 3)) + .Concat(Repeat(0, 4)); + } + + private void Process16WordBlock() { + uint aa = _a; + uint bb = _b; + uint cc = _c; + uint dd = _d; + + foreach (int k in new[] { 0, 4, 8, 12 }) { + aa = Round1Operation(aa, bb, cc, dd, _x[k], 3); + dd = Round1Operation(dd, aa, bb, cc, _x[k + 1], 7); + cc = Round1Operation(cc, dd, aa, bb, _x[k + 2], 11); + bb = Round1Operation(bb, cc, dd, aa, _x[k + 3], 19); + } + + foreach (int k in new[] { 0, 1, 2, 3 }) { + aa = Round2Operation(aa, bb, cc, dd, _x[k], 3); + dd = Round2Operation(dd, aa, bb, cc, _x[k + 4], 5); + cc = Round2Operation(cc, dd, aa, bb, _x[k + 8], 9); + bb = Round2Operation(bb, cc, dd, aa, _x[k + 12], 13); + } + + foreach (int k in new[] { 0, 2, 1, 3 }) { + aa = Round3Operation(aa, bb, cc, dd, _x[k], 3); + dd = Round3Operation(dd, aa, bb, cc, _x[k + 8], 9); + cc = Round3Operation(cc, dd, aa, bb, _x[k + 4], 11); + bb = Round3Operation(bb, cc, dd, aa, _x[k + 12], 15); + } + + unchecked { + _a += aa; + _b += bb; + _c += cc; + _d += dd; + } + } + + private static uint ROL(uint value, int numberOfBits) { + return (value << numberOfBits) | (value >> (32 - numberOfBits)); + } + + private static uint Round1Operation(uint a, uint b, uint c, uint d, uint xk, int s) { + unchecked { + return ROL(a + ((b & c) | (~b & d)) + xk, s); + } + } + + private static uint Round2Operation(uint a, uint b, uint c, uint d, uint xk, int s) { + unchecked { + return ROL(a + ((b & c) | (b & d) | (c & d)) + xk + 0x5a827999, s); + } + } + + private static uint Round3Operation(uint a, uint b, uint c, uint d, uint xk, int s) { + unchecked { + return ROL(a + (b ^ c ^ d) + xk + 0x6ed9eba1, s); + } + } + } +} diff --git a/Mechanisms/Ntlm/MessageType.cs b/Mechanisms/Ntlm/MessageType.cs new file mode 100644 index 0000000..7fb3850 --- /dev/null +++ b/Mechanisms/Ntlm/MessageType.cs @@ -0,0 +1,23 @@ + +namespace S22.Sasl.Mechanisms.Ntlm { + /// + /// Describes the different types of NTLM messages. + /// + internal enum MessageType { + /// + /// An NTLM type 1 message is the initial client response to the + /// server. + /// + Type1 = 0x01, + /// + /// An NTLM type 2 message is the challenge sent by the server in + /// response to an NTLM type 1 message. + /// + Type2 = 0x02, + /// + /// An NTLM type 3 message is the challenge response sent by the client + /// in response to an NTLM type 2 message. + /// + Type3 = 0x03 + } +} diff --git a/Mechanisms/Ntlm/OSVersion.cs b/Mechanisms/Ntlm/OSVersion.cs new file mode 100644 index 0000000..c5127a0 --- /dev/null +++ b/Mechanisms/Ntlm/OSVersion.cs @@ -0,0 +1,67 @@ + +namespace S22.Sasl.Mechanisms.Ntlm { + /// + /// Indicates the version and build number of the operating system. + /// + internal class OSVersion { + /// + /// The major version number of the operating system. + /// + public byte MajorVersion { + get; + set; + } + + /// + /// The minor version number of the operating system. + /// + public byte MinorVersion { + get; + set; + } + + /// + /// The build number of the operating system. + /// + public short BuildNumber { + get; + set; + } + + /// + /// Default constructor. + /// + public OSVersion() { + } + + /// + /// Creates a new instance of the OSVersion class using the specified + /// values. + /// + /// The major version of the operating + /// system. + /// The minor version of the operating + /// system. + /// The build number of the operating systen. + public OSVersion(byte majorVersion, byte minorVersion, short buildNumber) { + MajorVersion = majorVersion; + MinorVersion = minorVersion; + BuildNumber = buildNumber; + } + + /// + /// Serializes this instance of the OSVersion class to an array of + /// bytes. + /// + /// An array of bytes representing this instance of the OSVersion + /// class. + public byte[] Serialize() { + return new ByteBuilder() + .Append(MajorVersion) + .Append(MinorVersion) + .Append(BuildNumber) + .Append(0, 0, 0, 0x0F) + .ToArray(); + } + } +} diff --git a/Mechanisms/Ntlm/Responses.cs b/Mechanisms/Ntlm/Responses.cs new file mode 100644 index 0000000..edd31d1 --- /dev/null +++ b/Mechanisms/Ntlm/Responses.cs @@ -0,0 +1,295 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace S22.Sasl.Mechanisms.Ntlm { + /// + /// Contains methods for calculating the various Type 3 challenge + /// responses. + /// + internal static class Responses { + /// + /// Computes the LM-response to the challenge sent as part of an + /// NTLM type 2 message. + /// + /// The challenge sent by the server. + /// The user account password. + /// An array of bytes representing the response to the + /// specified challenge. + internal static byte[] ComputeLMResponse(byte[] challenge, + string password) { + byte[] lmHash = LMHash(password); + return LMResponse(lmHash, challenge); + } + + /// + /// Computes the NTLM-response to the challenge sent as part of an + /// NTLM type 2 message. + /// + /// The challenge sent by the server. + /// The user account password. + /// An array of bytes representing the response to the + /// specified challenge. + internal static byte[] ComputeNtlmResponse(byte[] challenge, + string password) { + byte[] ntlmHash = NtlmHash(password); + return LMResponse(ntlmHash, challenge); + } + + /// + /// Computes the NTLMv2-response to the challenge sent as part of an + /// NTLM type 2 message. + /// + /// The name of the authentication target. + /// The user account name to authenticate with. + /// The user account password. + /// The target information block from + /// the NTLM type 2 message. + /// The challenge sent by the server. + /// A random 8-byte client nonce. + /// An array of bytes representing the response to the + /// specified challenge. + internal static byte[] ComputeNtlmv2Response(string target, string username, + string password, byte[] targetInformation, byte[] challenge, + byte[] clientNonce) { + byte[] ntlmv2Hash = Ntlmv2Hash(target, username, password), + blob = CreateBlob(targetInformation, clientNonce); + return LMv2Response(ntlmv2Hash, blob, challenge); + } + + /// + /// Computes the LMv2-response to the challenge sent as part of an + /// NTLM type 2 message. + /// + /// The name of the authentication target. + /// The user account to authenticate with. + /// The user account password. + /// The challenge sent by the server. + /// A random 8-byte client nonce. + /// An array of bytes representing the response to the + /// specified challenge. + internal static byte[] ComputeLMv2Response(string target, string username, + string password, byte[] challenge, byte[] clientNonce) { + byte[] ntlmv2Hash = Ntlmv2Hash(target, username, password); + return LMv2Response(ntlmv2Hash, clientNonce, challenge); + } + + /// + /// Creates the LM Hash of the specified password. + /// + /// The password to create the LM Hash of. + /// The LM Hash of the given password, used in the calculation + /// of the LM Response. + /// Thrown if the password argument + /// is null. + private static byte[] LMHash(string password) { + // Precondition: password != null. + password.ThrowIfNull("password"); + byte[] oemPassword = + Encoding.ASCII.GetBytes(password.ToUpperInvariant()), + magic = new byte[] { 0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25 }, + // This is the pre-encrypted magic value with a null DES key. + nullEncMagic = { 0xAA, 0xD3, 0xB4, 0x35, 0xB5, 0x14, 0x04, 0xEE }, + keyBytes = new byte[14], lmHash = new byte[16]; + int length = Math.Min(oemPassword.Length, 14); + Array.Copy(oemPassword, keyBytes, length); + byte[] lowKey = CreateDESKey(keyBytes, 0), highKey = + CreateDESKey(keyBytes, 7); + + using (DES des = DES.Create("DES")) { + byte[] output = new byte[8]; + des.Mode = CipherMode.ECB; + // Note: In .NET DES cannot accept a weak key. This can happen for + // an empty password or if the password is shorter than 8 characters. + if (password.Length < 1) { + Buffer.BlockCopy(nullEncMagic, 0, lmHash, 0, 8); + } else { + des.Key = lowKey; + using (var encryptor = des.CreateEncryptor()) { + encryptor.TransformBlock(magic, 0, magic.Length, lmHash, 0); + } + } + if (password.Length < 8) { + Buffer.BlockCopy(nullEncMagic, 0, lmHash, 8, 8); + } else { + des.Key = highKey; + using (var encryptor = des.CreateEncryptor()) { + encryptor.TransformBlock(magic, 0, magic.Length, lmHash, 8); + } + } + return lmHash; + } + } + + /// + /// Creates a DES encryption key from the specified key material. + /// + /// The key material to create the DES encryption + /// key from. + /// An offset into the byte array at which to + /// extract the key material from. + /// A 56-bit DES encryption key as an array of bytes. + private static byte[] CreateDESKey(byte[] bytes, int offset) { + byte[] keyBytes = new byte[7]; + Array.Copy(bytes, offset, keyBytes, 0, 7); + byte[] material = new byte[8]; + material[0] = keyBytes[0]; + material[1] = (byte) (keyBytes[0] << 7 | (keyBytes[1] & 0xff) >> 1); + material[2] = (byte) (keyBytes[1] << 6 | (keyBytes[2] & 0xff) >> 2); + material[3] = (byte) (keyBytes[2] << 5 | (keyBytes[3] & 0xff) >> 3); + material[4] = (byte) (keyBytes[3] << 4 | (keyBytes[4] & 0xff) >> 4); + material[5] = (byte) (keyBytes[4] << 3 | (keyBytes[5] & 0xff) >> 5); + material[6] = (byte) (keyBytes[5] << 2 | (keyBytes[6] & 0xff) >> 6); + material[7] = (byte) (keyBytes[6] << 1); + + return OddParity(material); + } + + /// + /// Applies odd parity to the specified byte array. + /// + /// The byte array to apply odd parity to. + /// A reference to the byte array. + private static byte[] OddParity(byte[] bytes) { + for (int i = 0; i < bytes.Length; i++) { + byte b = bytes[i]; + bool needsParity = (((b >> 7) ^ (b >> 6) ^ (b >> 5) ^ + (b >> 4) ^ (b >> 3) ^ (b >> 2) ^ + (b >> 1)) & 0x01) == 0; + if (needsParity) + bytes[i] |= (byte) 0x01; + else + bytes[i] &= (byte) 0xFE; + } + return bytes; + } + + /// + /// Creates the LM Response from the specified hash and Type 2 challenge. + /// + /// An LM or NTLM hash. + /// The server challenge from the Type 2 + /// message. + /// The challenge response as an array of bytes. + /// Thrown if the hash or the + /// challenge parameter is null. + private static byte[] LMResponse(byte[] hash, byte[] challenge) { + hash.ThrowIfNull("hash"); + challenge.ThrowIfNull("challenge"); + byte[] keyBytes = new byte[21], lmResponse = new byte[24]; + Array.Copy(hash, 0, keyBytes, 0, 16); + byte[] lowKey = CreateDESKey(keyBytes, 0), middleKey = + CreateDESKey(keyBytes, 7), highKey = + CreateDESKey(keyBytes, 14); + using (DES des = DES.Create("DES")) { + des.Mode = CipherMode.ECB; + des.Key = lowKey; + using (var encryptor = des.CreateEncryptor()) { + encryptor.TransformBlock(challenge, 0, challenge.Length, + lmResponse, 0); + } + des.Key = middleKey; + using (var encryptor = des.CreateEncryptor()) { + encryptor.TransformBlock(challenge, 0, challenge.Length, + lmResponse, 8); + } + des.Key = highKey; + using (var encryptor = des.CreateEncryptor()) { + encryptor.TransformBlock(challenge, 0, challenge.Length, + lmResponse, 16); + } + return lmResponse; + } + } + + /// + /// Creates the NTLM Hash of the specified password. + /// + /// The password to create the NTLM hash of. + /// The NTLM hash for the specified password. + /// Thrown if the password + /// parameter is null. + private static byte[] NtlmHash(String password) { + password.ThrowIfNull("password"); + byte[] data = Encoding.Unicode.GetBytes(password); + using (MD4 md4 = new MD4()) { + return md4.ComputeHash(data); + } + } + + /// + /// Creates the NTLMv2 Hash of the specified target, username + /// and password values. + /// + /// The name of the authentication target as is + /// specified in the target name field of the NTLM type 3 message. + /// The user account name. + /// The password for the user account. + /// The NTLMv2 hash for the specified input values. + /// Thrown if the username or + /// the password parameter is null. + private static byte[] Ntlmv2Hash(string target, string username, + string password) { + username.ThrowIfNull("username"); + password.ThrowIfNull("password"); + byte[] ntlmHash = NtlmHash(password); + string identity = username.ToUpperInvariant() + target; + using (var hmac = new HMACMD5(ntlmHash)) + return hmac.ComputeHash(Encoding.Unicode.GetBytes(identity)); + } + + /// + /// Returns the current time as the number of tenths of a microsecond + /// since January 1, 1601. + /// + /// The current time as the number of tenths of a microsecond + /// since January 1, 1601. + private static long GetTimestamp() { + return DateTime.Now.ToFileTimeUtc(); + } + + /// + /// Creates the "blob" data block which is part of the NTLMv2 challenge + /// response. + /// + /// The target information block from + /// the NTLM type 2 message. + /// A random 8-byte client nonce. + /// The blob, used in the calculation of the NTLMv2 Response. + private static byte[] CreateBlob(byte[] targetInformation, + byte[] clientNonce) { + return new ByteBuilder() + .Append(0x00000101) + .Append(0) + .Append(GetTimestamp()) + .Append(clientNonce) + .Append(0) + .Append(targetInformation) + .Append(0) + .ToArray(); + } + + /// + /// Creates the LMv2 Response from the given NTLMv2 hash, client data, and + /// Type 2 challenge. + /// + /// The NTLMv2 Hash. + /// The client data (blob or client nonce). + /// The server challenge from the Type 2 message. + /// The response which is either for NTLMv2 or LMv2, depending + /// on the client data. + private static byte[] LMv2Response(byte[] hash, byte[] clientData, + byte[] challenge) { + byte[] data = new ByteBuilder() + .Append(challenge) + .Append(clientData) + .ToArray(); + using (var hmac = new HMACMD5(hash)) { + return new ByteBuilder() + .Append(hmac.ComputeHash(data)) + .Append(clientData) + .ToArray(); + } + } + } +} diff --git a/Mechanisms/Ntlm/SecurityBuffer.cs b/Mechanisms/Ntlm/SecurityBuffer.cs new file mode 100644 index 0000000..36bb642 --- /dev/null +++ b/Mechanisms/Ntlm/SecurityBuffer.cs @@ -0,0 +1,78 @@ +using System; + +namespace S22.Sasl.Mechanisms.Ntlm { + /// + /// Represents an NTLM security buffer, which is a structure used to point + /// to a buffer of binary data within an NTLM message. + /// + internal class SecurityBuffer { + /// + /// The length of the buffer content in bytes (may be zero). + /// + public short Length { + get; + private set; + } + + /// + /// The allocated space for the buffer in bytes (typically the same as + /// the length). + /// + public short AllocatedSpace { + get { + return Length; + } + } + + /// + /// The offset from the beginning of the NTLM message to the start of + /// the buffer, in bytes. + /// + public int Offset { + get; + private set; + } + + /// + /// Creates a new instance of the SecurityBuffer class using the specified + /// values. + /// + /// The length of the buffer described by this instance + /// of the SecurityBuffer class. + /// The offset at which the buffer starts, in bytes. + /// Thrown if the length value exceeds + /// the maximum value allowed. The security buffer structure stores the + /// length value as a 2-byte short value. + public SecurityBuffer(int length, int offset) { + Length = Convert.ToInt16(length); + Offset = offset; + } + + /// + /// Creates a new instance of the SecurityBuffer class using the specified + /// values. + /// + /// The data of the buffer described by this instance + /// of the SecurityBuffer class. + /// The offset at which the buffer starts, in bytes. + /// Thrown if the length of the data + /// buffer exceeds the maximum value allowed. The security buffer structure + /// stores the buffer length value as a 2-byte short value. + public SecurityBuffer(byte[] data, int offset) + : this(data.Length, offset) { + } + + /// + /// Serializes this instance of the SecurityBuffer into an array of bytes. + /// + /// A byte array representing this instance of the SecurityBuffer + /// class. + public byte[] Serialize() { + return new ByteBuilder() + .Append(Length) + .Append(AllocatedSpace) + .Append(Offset) + .ToArray(); + } + } +} diff --git a/Mechanisms/Ntlm/Type1Message.cs b/Mechanisms/Ntlm/Type1Message.cs new file mode 100644 index 0000000..f21bda2 --- /dev/null +++ b/Mechanisms/Ntlm/Type1Message.cs @@ -0,0 +1,136 @@ +using System; +using System.Text; + +namespace S22.Sasl.Mechanisms.Ntlm { + /// + /// Represents an NTLM Type 1 Message. + /// + internal class Type1Message { + /// + /// The NTLM message signature which is always "NTLMSSP". + /// + static readonly string signature = "NTLMSSP"; + + /// + /// The NTML message type which is always 1 for an NTLM Type 1 message. + /// + static readonly MessageType type = MessageType.Type1; + + /// + /// The NTLM flags set on this instance. + /// + internal Flags Flags { + get; + set; + } + + /// + /// The supplied domain name as an array of bytes in the ASCII + /// range. + /// + byte[] domain { + get; + set; + } + + /// + /// The offset within the message where the domain name data starts. + /// + int domainOffset { + get { + // We send a version 3 NTLM type 1 message. + return 40; + } + } + + /// + /// The supplied workstation name as an array of bytes in the + /// ASCII range. + /// + byte[] workstation { + get; + set; + } + + /// + /// The offset within the message where the workstation name data starts. + /// + int workstationOffset { + get { + return domainOffset + domain.Length; + } + } + + /// + /// The length of the supplied workstation name as a 16-bit short value. + /// + short workstationLength { + get { + return Convert.ToInt16(workstation.Length); + } + } + + /// + /// Contains information about the client's OS version. + /// + OSVersion OSVersion { + get; + set; + } + + /// + /// Creates a new instance of the Type1Message class using the specified + /// domain and workstation names. + /// + /// The domain in which the client's workstation has + /// membership. + /// The client's workstation name. + /// Thrown if the domain or the + /// workstation parameter is null. + /// Thrown if the domain + /// or the workstation name exceeds the maximum allowed string + /// length. + /// The domain as well as the workstation name is restricted + /// to ASCII characters and must not be longer than 65536 characters. + /// + public Type1Message(string domain, string workstation) { + // Fixme: Is domain mandatory? + domain.ThrowIfNull("domain"); + workstation.ThrowIfNull("workstation"); + + this.domain = Encoding.ASCII.GetBytes(domain); + if (this.domain.Length >= Int16.MaxValue) { + throw new ArgumentOutOfRangeException("The supplied domain name must " + + "not be longer than " + Int16.MaxValue); + } + this.workstation = Encoding.ASCII.GetBytes(workstation); + if (this.workstation.Length >= Int16.MaxValue) { + throw new ArgumentOutOfRangeException("The supplied workstation name " + + "must not be longer than " + Int16.MaxValue); + } + + Flags = Flags.NegotiateUnicode | Flags.RequestTarget | Flags.NegotiateNTLM | + Flags.NegotiateDomainSupplied | Flags.NegotiateWorkstationSupplied; + // We spoof an OS version of Windows 7 Build 7601. + OSVersion = new OSVersion(6, 1, 7601); + } + + /// + /// Serializes this instance of the Type1 class to an array of bytes. + /// + /// An array of bytes representing this instance of the Type1 + /// class. + public byte[] Serialize() { + return new ByteBuilder() + .Append(signature + "\0") + .Append((int) type) + .Append((int) Flags) + .Append(new SecurityBuffer(domain, domainOffset).Serialize()) + .Append(new SecurityBuffer(workstation, workstationOffset).Serialize()) + .Append(OSVersion.Serialize()) + .Append(domain) + .Append(workstation) + .ToArray(); + } + } +} diff --git a/Mechanisms/Ntlm/Type2Message.cs b/Mechanisms/Ntlm/Type2Message.cs new file mode 100644 index 0000000..c8be147 --- /dev/null +++ b/Mechanisms/Ntlm/Type2Message.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; +using System.Text; + +namespace S22.Sasl.Mechanisms.Ntlm { + /// + /// Represents an NTLM Type 2 Message. + /// + internal class Type2Message { + /// + /// The NTLM message signature which is always "NTLMSSP". + /// + static readonly string signature = "NTLMSSP"; + + /// + /// The NTML message type which is always 2 for an NTLM Type 2 message. + /// + static readonly MessageType type = MessageType.Type2; + + /// + /// The challenge is an 8-byte block of random data. + /// + public byte[] Challenge { + get; + private set; + } + + /// + /// The target name of the authentication target. + /// + public string TargetName { + get; + private set; + } + + /// + /// The NTLM flags set on this message. + /// + public Flags Flags { + get; + private set; + } + + /// + /// The SSPI context handle when a local call is being made, + /// otherwise null. + /// + public Int64 Context { + get; + private set; + } + + /// + /// Contains the data present in the OS version structure. + /// + public OSVersion OSVersion { + get; + private set; + } + + /// + /// The version of this Type 2 message instance. + /// + public Type2Version Version { + get; + private set; + } + + /// + /// Contains the data present in the target information block. + /// + public Type2TargetInformation TargetInformation { + get; + private set; + } + + /// + /// Contains the raw data present in the target information block. + /// + public byte[] RawTargetInformation { + get; + private set; + } + + /// + /// Private constructor. + /// + private Type2Message() { + TargetInformation = new Type2TargetInformation(); + OSVersion = new OSVersion(); + } + + /// + /// Deserializes a Type 2 message instance from the specified buffer + /// of bytes. + /// + /// The buffer containing a sequence of bytes + /// representing an NTLM Type 2 message. + /// An initialized instance of the Type2 class. + /// Thrown if an error occurs + /// during deserialization of the Type 2 message. + static internal Type2Message Deserialize(byte[] buffer) { + try { + Type2Message t2 = new Type2Message(); + using (var ms = new MemoryStream(buffer)) { + using (var r = new BinaryReader(ms)) { + if (r.ReadASCIIString(8) != signature) + throw new InvalidDataException("Invalid signature."); + if (r.ReadInt32() != (int) type) + throw new InvalidDataException("Unexpected message type."); + int targetLength = r.ReadInt16(), targetSpace = + r.ReadInt16(), targetOffset = r.ReadInt32(); + t2.Flags = (Flags) r.ReadInt32(); + t2.Challenge = r.ReadBytes(8); + // Figure out, which of the several versions of Type 2 we're + // dealing with. + t2.Version = GetType2Version(targetOffset); + if (t2.Version > Type2Version.Version1) { + t2.Context = r.ReadInt64(); + // Read the target information security buffer + int informationLength = r.ReadInt16(), informationSpace = + r.ReadInt16(), informationOffset = r.ReadInt32(); + t2.RawTargetInformation = new byte[informationLength]; + Array.Copy(buffer, informationOffset, + t2.RawTargetInformation, 0, informationLength); + // Version 3 has an additional OS version structure. + if (t2.Version > Type2Version.Version2) + t2.OSVersion = ReadOSVersion(r); + } + t2.TargetName = GetTargetName(r.ReadBytes(targetLength), + t2.Flags.HasFlag(Flags.NegotiateUnicode)); + if (t2.Version > Type2Version.Version1) { + t2.TargetInformation = ReadTargetInformation(r); + } + } + } + return t2; + } catch (Exception e) { + throw new SerializationException("NTLM Type 2 message could not be " + + "deserialized.", e); + } + } + + /// + /// Determines the version of an NTLM type 2 message. + /// + /// The target offset field of the NTLM + /// type 2 message. + /// A value from the Type2Version enumeration. + static Type2Version GetType2Version(int targetOffset) { + var dict = new Dictionary() { + { 32, Type2Version.Version1 }, + { 48, Type2Version.Version2 }, + { 56, Type2Version.Version3 } + }; + return dict.ContainsKey(targetOffset) ? dict[targetOffset] : + Type2Version.Unknown; + } + + /// + /// Reads the OS information data present in version 3 of an NTLM + /// type 2 message from the specified BinaryReader. + /// + /// The BinaryReader instance to read from. + /// An initialized instance of the OSVersion class. + static OSVersion ReadOSVersion(BinaryReader r) { + OSVersion version = new OSVersion(); + version.MajorVersion = r.ReadByte(); + version.MinorVersion = r.ReadByte(); + version.BuildNumber = r.ReadInt16(); + // Swallow the reserved 32-bit word. + r.ReadInt32(); + + return version; + } + + /// + /// Reads the target information data present in version 2 and 3 of + /// an NTLM type 2 message from the specified BinaryReader. + /// + /// The BinaryReader instance to read from. + /// An initialized instance of the Type2TargetInformation + /// class. + static Type2TargetInformation ReadTargetInformation(BinaryReader r) { + Type2TargetInformation info = new Type2TargetInformation(); + while (true) { + var _type = (Type2InformationType) r.ReadInt16(); + if (_type == Type2InformationType.TerminatorBlock) + break; + short length = r.ReadInt16(); + string content = Encoding.Unicode.GetString(r.ReadBytes(length)); + switch (_type) { + case Type2InformationType.ServerName: + info.ServerName = content; + break; + case Type2InformationType.DomainName: + info.DomainName = content; + break; + case Type2InformationType.DnsHostname: + info.DnsHostname = content; + break; + case Type2InformationType.DnsDomainName: + info.DnsDomainName = content; + break; + } + } + return info; + } + + /// + /// Retrieves the target name from the specified byte array. + /// + /// A byte array containing the target name. + /// If true the target name will be decoded + /// using UTF-16 unicode encoding. + /// + static string GetTargetName(byte[] data, bool isUnicode) { + Encoding enc = isUnicode ? Encoding.Unicode : Encoding.ASCII; + + return enc.GetString(data); + } + } +} diff --git a/Mechanisms/Ntlm/Type3Message.cs b/Mechanisms/Ntlm/Type3Message.cs new file mode 100644 index 0000000..9b17be2 --- /dev/null +++ b/Mechanisms/Ntlm/Type3Message.cs @@ -0,0 +1,268 @@ +using System; +using System.Text; + +namespace S22.Sasl.Mechanisms.Ntlm { + /// + /// Represents an NTLM Type 3 Message. + /// + internal class Type3Message { + /// + /// The NTLM message signature which is always "NTLMSSP". + /// + static readonly string signature = "NTLMSSP"; + + /// + /// The NTML message type which is always 3 for an NTLM Type 3 message. + /// + static readonly MessageType type = MessageType.Type3; + + /// + /// The NTLM flags set on this instance. + /// + public Flags Flags { + get; + set; + } + + /// + /// The "Lan Manager" challenge response. + /// + byte[] LMResponse { + get; + set; + } + + /// + /// The offset at which the LM challenge response data starts. + /// + int LMOffset { + get { + // We send a version 3 NTLM type 3 message so the start of the data + // block is at offset 72. + return 72; + } + } + + /// + /// The NTLM challenge response. + /// + byte[] NtlmResponse { + get; + set; + } + + /// + /// The offset at which the NTLM challenge response data starts. + /// + int NtlmOffset { + get { + return LMOffset + LMResponse.Length; + } + } + + /// + /// The authentication realm in which the authenticating account + /// has membership. + /// + byte[] targetName { + get; + set; + } + + /// + /// The offset at which the target name data starts. + /// + int targetOffset { + get { + return NtlmOffset + NtlmResponse.Length; + } + } + + /// + /// The authenticating account name. + /// + byte[] username { + get; + set; + } + + /// + /// The offset at which the username data starts. + /// + int usernameOffset { + get { + return targetOffset + targetName.Length; + } + } + + /// + /// The client workstation's name. + /// + byte[] workstation { + get; + set; + } + + /// + /// The offset at which the client workstation's name data starts. + /// + int workstationOffset { + get { + return usernameOffset + username.Length; + } + } + + /// + /// The session key value which is used by the session security mechanism + /// during key exchange. + /// + byte[] sessionKey { + get; + set; + } + + /// + /// The offset at which the session key data starts. + /// + int sessionKeyOffset { + get { + return workstationOffset + workstation.Length; + } + } + + /// + /// Contains the data present in the OS version structure. + /// + OSVersion OSVersion { + get; + set; + } + + /// + /// The encoding used for transmitting the contents of the various + /// security buffers. + /// + Encoding encoding { + get; + set; + } + + /// + /// Creates a new instance of an NTLM type 3 message using the specified + /// values. + /// + /// The Windows account name to use for + /// authentication. + /// The Windows account password to use for + /// authentication. + /// The challenge received from the server as part + /// of the NTLM type 2 message. + /// The client's workstation name. + /// Set to true to send an NTLMv2 challenge + /// response. + /// The authentication realm in which the + /// authenticating account has membership. + /// The target information block from + /// the NTLM type 2 message. + /// The target name is a domain name for domain accounts, or + /// a server name for local machine accounts. All security buffers will + /// be encoded as Unicode. + public Type3Message(string username, string password, byte[] challenge, + string workstation, bool ntlmv2 = false, string targetName = null, + byte[] targetInformation = null) + : this(username, password, challenge, true, workstation, ntlmv2, + targetName, targetInformation) + { + } + + /// + /// Creates a new instance of an NTLM type 3 message using the specified + /// values. + /// + /// The Windows account name to use for + /// authentication. + /// The Windows account password to use for + /// authentication. + /// The challenge received from the server as part + /// of the NTLM type 2 message. + /// Set this to true, if Unicode encoding has been + /// negotiated between client and server. + /// The client's workstation name. + /// Set to true to send an NTLMv2 challenge + /// response. + /// The authentication realm in which the + /// authenticating account has membership. + /// The target information block from + /// the NTLM type 2 message. + /// The target name is a domain name for domain accounts, or + /// a server name for local machine accounts. + /// Thrown if the username, password + /// or challenge parameters are null. + public Type3Message(string username, string password, byte[] challenge, + bool useUnicode, string workstation, bool ntlmv2 = false, + string targetName = null, byte[] targetInformation = null) { + // Preconditions. + username.ThrowIfNull("username"); + password.ThrowIfNull("password"); + challenge.ThrowIfNull("challenge"); + encoding = useUnicode ? Encoding.Unicode : Encoding.ASCII; + + // Setup the security buffers contents. + this.username = encoding.GetBytes(username); + this.workstation = encoding.GetBytes(workstation); + this.targetName = String.IsNullOrEmpty(targetName) ? new byte[0] : + encoding.GetBytes(targetName); + // The session key is not relevant to authentication. + this.sessionKey = new byte[0]; + // Compute the actual challenge response data. + if (!ntlmv2) { + LMResponse = Responses.ComputeLMResponse(challenge, password); + NtlmResponse = Responses.ComputeNtlmResponse(challenge, password); + } else { + byte[] cnonce = GetCNonce(); + LMResponse = Responses.ComputeLMv2Response(targetName, username, + password, challenge, cnonce); + NtlmResponse = Responses.ComputeNtlmv2Response(targetName, + username, password, targetInformation, challenge, cnonce); + } + // We spoof an OS version of Windows 7 Build 7601. + OSVersion = new OSVersion(6, 1, 7601); + } + + /// + /// Serializes this instance of the Type3 class to an array of bytes. + /// + /// An array of bytes representing this instance of the Type3 + /// class. + public byte[] Serialize() { + return new ByteBuilder() + .Append(signature + "\0") + .Append((int) type) + .Append(new SecurityBuffer(LMResponse, LMOffset).Serialize()) + .Append(new SecurityBuffer(NtlmResponse, NtlmOffset).Serialize()) + .Append(new SecurityBuffer(targetName, targetOffset).Serialize()) + .Append(new SecurityBuffer(username, usernameOffset).Serialize()) + .Append(new SecurityBuffer(workstation, workstationOffset).Serialize()) + .Append(new SecurityBuffer(sessionKey, sessionKeyOffset).Serialize()) + .Append((int) Flags) + .Append(OSVersion.Serialize()) + .Append(LMResponse) + .Append(NtlmResponse) + .Append(targetName) + .Append(username) + .Append(workstation) + .Append(sessionKey) + .ToArray(); + } + + /// + /// Returns a random 8-byte cnonce value. + /// + /// A random 8-byte cnonce value. + private static byte[] GetCNonce() { + byte[] b = new byte[8]; + new Random().NextBytes(b); + return b; + } + } +} diff --git a/Mechanisms/SaslCramMd5.cs b/Mechanisms/SaslCramMd5.cs new file mode 100644 index 0000000..608aa46 --- /dev/null +++ b/Mechanisms/SaslCramMd5.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace S22.Sasl.Mechanisms { + /// + /// Implements the Sasl Cram-Md5 authentication method as described in + /// RFC 2195. + /// + internal class SaslCramMd5 : SaslMechanism { + bool Completed = false; + + /// + /// Server sends the first message in the authentication exchange. + /// + public override bool HasInitial { + get { + return false; + } + } + + /// + /// True if the authentication exchange between client and server + /// has been completed. + /// + public override bool IsCompleted { + get { + return Completed; + } + } + + /// + /// The IANA name for the Cram-Md5 authentication mechanism as described + /// in RFC 2195. + /// + public override string Name { + get { + return "CRAM-MD5"; + } + } + + /// + /// The username to authenticate with. + /// + string Username { + get { + return Properties.ContainsKey("Username") ? + Properties["Username"] as string : null; + } + set { + Properties["Username"] = value; + } + } + + /// + /// The password to authenticate with. + /// + string Password { + get { + return Properties.ContainsKey("Password") ? + Properties["Password"] as string : null; + } + set { + Properties["Password"] = value; + } + } + + /// + /// Private constructor for use with Sasl.SaslFactory. + /// + private SaslCramMd5() { + // Nothing to do here. + } + + /// + /// Creates and initializes a new instance of the SaslCramMd5 class + /// using the specified username and password. + /// + /// The username to authenticate with. + /// The plaintext password to authenticate + /// with. + /// Thrown if the username + /// or the password parameter is null. + /// Thrown if the username + /// parameter is empty. + public SaslCramMd5(string username, string password) { + username.ThrowIfNull("username"); + if (username == String.Empty) + throw new ArgumentException("The username must not be empty."); + password.ThrowIfNull("password"); + + Username = username; + Password = password; + } + + /// + /// Computes the client response to the specified Cram-Md5 challenge. + /// + /// The challenge sent by the server + /// The response to the Cram-Md5 challenge. + /// Thrown if the response could not + /// be computed. + protected override byte[] ComputeResponse(byte[] challenge) { + // Precondition: Ensure username and password are not null and + // username is not empty. + if (String.IsNullOrEmpty(Username) || Password == null) { + throw new SaslException("The username must not be null or empty and " + + "the password must not be null."); + } + // Sasl Cram-Md5 does not involve another roundtrip. + Completed = true; + // Compute the encrypted challenge as a hex-string. + string hex = String.Empty; + using (var hmac = new HMACMD5(Encoding.ASCII.GetBytes(Password))) { + byte[] encrypted = hmac.ComputeHash(challenge); + hex = BitConverter.ToString(encrypted).ToLower().Replace("-", + String.Empty); + } + return Encoding.ASCII.GetBytes(Username + " " + hex); + } + } +} diff --git a/Mechanisms/SaslDigestMd5.cs b/Mechanisms/SaslDigestMd5.cs new file mode 100644 index 0000000..78749af --- /dev/null +++ b/Mechanisms/SaslDigestMd5.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace S22.Sasl.Mechanisms { + /// + /// Implements the Sasl Cram-Md5 authentication method as described in + /// RFC 2831. + /// + internal class SaslDigestMd5 : SaslMechanism { + bool Completed = false; + + /// + /// The client nonce value used during authentication. + /// + string Cnonce = GenerateCnonce(); + + /// + /// Cram-Md5 involves several steps. + /// + int Step = 0; + + /// + /// The server sends the first message in the authentication exchange. + /// + public override bool HasInitial { + get { + return false; + } + } + + /// + /// True if the authentication exchange between client and server + /// has been completed. + /// + public override bool IsCompleted { + get { + return Completed; + } + } + + /// + /// The IANA name for the Digest-Md5 authentication mechanism as described + /// in RFC 2195. + /// + public override string Name { + get { + return "DIGEST-MD5"; + } + } + + /// + /// The username to authenticate with. + /// + string Username { + get { + return Properties.ContainsKey("Username") ? + Properties["Username"] as string : null; + } + set { + Properties["Username"] = value; + } + } + + /// + /// The password to authenticate with. + /// + string Password { + get { + return Properties.ContainsKey("Password") ? + Properties["Password"] as string : null; + } + set { + Properties["Password"] = value; + } + } + + /// + /// Private constructor for use with Sasl.SaslFactory. + /// + private SaslDigestMd5() { + // Nothing to do here. + } + + /// + /// Internal constructor used for unit testing. + /// + /// The username to authenticate with. + /// The plaintext password to authenticate + /// with. + /// The client nonce value to use. + /// Thrown if the username + /// or the password parameter is null. + /// Thrown if the username + /// parameter is empty. + internal SaslDigestMd5(string username, string password, string cnonce) + : this(username, password) { + Cnonce = cnonce; + } + + /// + /// Creates and initializes a new instance of the SaslDigestMd5 class + /// using the specified username and password. + /// + /// The username to authenticate with. + /// The plaintext password to authenticate + /// with. + /// Thrown if the username + /// or the password parameter is null. + /// Thrown if the username + /// parameter is empty. + public SaslDigestMd5(string username, string password) { + username.ThrowIfNull("username"); + if (username == String.Empty) + throw new ArgumentException("The username must not be empty."); + password.ThrowIfNull("password"); + + Username = username; + Password = password; + } + + /// + /// Computes the client response to the specified Digest-Md5 challenge. + /// + /// The challenge sent by the server + /// The response to the Digest-Md5 challenge. + /// Thrown if the response could not + /// be computed. + protected override byte[] ComputeResponse(byte[] challenge) { + if (Step == 1) + Completed = true; + // If authentication succeeded, the server responds with another + // challenge (which we ignore) which the client must acknowledge + // with a CRLF. + byte[] ret = Step == 0 ? ComputeDigestResponse(challenge) : + new byte[0]; + Step = Step + 1; + return ret; + } + + private byte[] ComputeDigestResponse(byte[] challenge) { + // Precondition: Ensure username and password are not null and + // username is not empty. + if (String.IsNullOrEmpty(Username) || Password == null) { + throw new SaslException("The username must not be null or empty and " + + "the password must not be null."); + } + // Parse the challenge-string and construct the "response-value" from it. + string decoded = Encoding.ASCII.GetString(challenge); + NameValueCollection fields = ParseDigestChallenge(decoded); + string digestUri = "imap/" + fields["realm"]; + string responseValue = ComputeDigestResponseValue(fields, Cnonce, digestUri, + Username, Password); + + // Create the challenge-response string. + string[] directives = new string[] { + // We don't use UTF-8 in the current implementation. + //"charset=utf-8", + "username=" + Dquote(Username), + "realm=" + Dquote(fields["realm"]), + "nonce="+ Dquote(fields["nonce"]), + "nc=00000001", + "cnonce=" + Dquote(Cnonce), + "digest-uri=" + Dquote(digestUri), + "response=" + responseValue, + "qop=" + fields["qop"] + }; + string challengeResponse = String.Join(",", directives); + // Finally, return the response as a byte array. + return Encoding.ASCII.GetBytes(challengeResponse); + } + + /// + /// Parses the challenge string sent by the server in response to a Digest-Md5 + /// authentication request. + /// + /// The challenge sent by the server as part of + /// "Step One" of the Digest-Md5 authentication mechanism. + /// An initialized NameValueCollection instance made up of the + /// attribute/value pairs contained in the challenge. + /// Thrown if the challenge parameter + /// is null. + /// Refer to RFC 2831 section 2.1.1 for a detailed description of the + /// format of the challenge sent by the server. + private static NameValueCollection ParseDigestChallenge(string challenge) { + challenge.ThrowIfNull("challenge"); + NameValueCollection coll = new NameValueCollection(); + string[] parts = challenge.Split(','); + foreach (string p in parts) { + string[] kv = p.Split(new char[] { '=' }, 2); + if (kv.Length == 2) + coll.Add(kv[0], kv[1].Trim('"')); + } + return coll; + } + + /// + /// Computes the "response-value" hex-string which is part of the + /// Digest-MD5 challenge-response. + /// + /// A collection containing the attributes + /// and values of the challenge sent by the server. + /// The cnonce value to use for computing + /// the response-value. + /// The "digest-uri" string to use for + /// computing the response-value. + /// The username to use for computing the + /// response-value. + /// The password to use for computing the + /// response-value. + /// A string containing a hash-value which is part of the + /// response sent by the client. + /// Refer to RFC 2831, section 2.1.2.1 for a detailed + /// description of the computation of the response-value. + private static string ComputeDigestResponseValue(NameValueCollection challenge, + string cnonce, string digestUri, string username, string password) { + // The username, realm and password are encoded with ISO-8859-1 + // (Compare RFC 2831, p. 10). + Encoding enc = Encoding.GetEncoding("ISO-8859-1"); + string ncValue = "00000001", realm = challenge["realm"]; + // Construct A1. + using (var md5p = new MD5CryptoServiceProvider()) { + byte[] data = enc.GetBytes(username + ":" + realm + ":" + password); + data = md5p.ComputeHash(data); + string A1 = enc.GetString(data) + ":" + challenge["nonce"] + ":" + + cnonce; + // Construct A2. + string A2 = "AUTHENTICATE:" + digestUri; + if (!"auth".Equals(challenge["qop"])) + A2 = A2 + ":00000000000000000000000000000000"; + string ret = MD5(A1, enc) + ":" + challenge["nonce"] + ":" + ncValue + + ":" + cnonce + ":" + challenge["qop"] + ":" + MD5(A2, enc); + return MD5(ret, enc); + } + } + + /// + /// Calculates the MD5 hash value for the specified string. + /// + /// The string to calculate the MD5 hash value for. + /// The encoding to employ for encoding the + /// characters in the specified string into a sequence of bytes for + /// which the MD5 hash will be calculated. + /// An MD5 hash as a 32-character hex-string. + /// Thrown if the input string + /// is null. + private static string MD5(string s, Encoding encoding = null) { + if (s == null) + throw new ArgumentNullException("s"); + if (encoding == null) + encoding = Encoding.UTF8; + byte[] data = encoding.GetBytes(s); + byte[] hash = (new MD5CryptoServiceProvider()).ComputeHash(data); + StringBuilder builder = new StringBuilder(); + foreach (byte h in hash) + builder.Append(h.ToString("x2")); + return builder.ToString(); + } + + /// + /// Encloses the specified string in double-quotes. + /// + /// The string to enclose in double-quote characters. + /// The enclosed string. + private static string Dquote(string s) { + return "\"" + s + "\""; + } + + /// + /// Generates a random cnonce-value which is a "client-specified data string + /// which must be different each time a digest-response is sent". + /// + /// A random "cnonce-value" string. + private static string GenerateCnonce() { + return Guid.NewGuid().ToString("N").Substring(0, 16); + } + } +} diff --git a/Mechanisms/SaslNtlm.cs b/Mechanisms/SaslNtlm.cs new file mode 100644 index 0000000..4b85e82 --- /dev/null +++ b/Mechanisms/SaslNtlm.cs @@ -0,0 +1,161 @@ +using S22.Sasl.Mechanisms.Ntlm; +using System; + +namespace S22.Sasl.Mechanisms { + /// + /// Implements the Sasl NTLM authentication method which is used in various + /// Microsoft network protocol implementations. + /// + /// Implemented with the help of the excellent documentation on + /// NTLM composed by Eric Glass. + internal class SaslNtlm : SaslMechanism { + protected bool completed = false; + + /// + /// NTLM involves several steps. + /// + protected int step = 0; + + /// + /// Client sends the first message in the authentication exchange. + /// + public override bool HasInitial { + get { + return true; + } + } + + /// + /// True if the authentication exchange between client and server + /// has been completed. + /// + public override bool IsCompleted { + get { + return completed; + } + } + + /// + /// The IANA name for the NTLM authentication mechanism. + /// + public override string Name { + get { + return "NTLM"; + } + } + + /// + /// The username to authenticate with. + /// + protected string Username { + get { + return Properties.ContainsKey("Username") ? + Properties["Username"] as string : null; + } + set { + Properties["Username"] = value; + } + } + + /// + /// The password to authenticate with. + /// + protected string Password { + get { + return Properties.ContainsKey("Password") ? + Properties["Password"] as string : null; + } + set { + Properties["Password"] = value; + } + } + + /// + /// Private constructor for use with Sasl.SaslFactory. + /// + protected SaslNtlm() { + // Nothing to do here. + } + + /// + /// Creates and initializes a new instance of the SaslNtlm class + /// using the specified username and password. + /// + /// The username to authenticate with. + /// The plaintext password to authenticate + /// with. + /// Thrown if the username + /// or the password parameter is null. + /// Thrown if the username + /// parameter is empty. + public SaslNtlm(string username, string password) { + username.ThrowIfNull("username"); + if (username == String.Empty) + throw new ArgumentException("The username must not be empty."); + password.ThrowIfNull("password"); + + Username = username; + Password = password; + } + + /// + /// Computes the client response to the specified NTLM challenge. + /// + /// The challenge sent by the server + /// The response to the NTLM challenge. + /// Thrown if the response could not + /// be computed. + protected override byte[] ComputeResponse(byte[] challenge) { + if (step == 1) + completed = true; + byte[] ret = step == 0 ? ComputeInitialResponse(challenge) : + ComputeChallengeResponse(challenge); + step = step + 1; + return ret; + } + + /// + /// Computes the initial client response to an NTLM challenge. + /// + /// The challenge sent by the server. Since + /// NTLM expects an initial client response, this will usually be + /// empty. + /// The initial response to the NTLM challenge. + /// Thrown if the response could not + /// be computed. + protected byte[] ComputeInitialResponse(byte[] challenge) { + try { + string domain = Properties.ContainsKey("Domain") ? + Properties["Domain"] as string : "domain"; + string workstation = Properties.ContainsKey("Workstation") ? + Properties["Workstation"] as string : "workstation"; + Type1Message msg = new Type1Message(domain, workstation); + + return msg.Serialize(); + } catch (Exception e) { + throw new SaslException("The initial client response could not " + + "be computed.", e); + } + } + + /// + /// Computes the actual challenge response to an NTLM challenge + /// which is sent as part of an NTLM type 2 message. + /// + /// The challenge sent by the server. + /// The response to the NTLM challenge. + /// Thrown if the challenge + /// response could not be computed. + protected byte[] ComputeChallengeResponse(byte[] challenge) { + try { + Type2Message msg = Type2Message.Deserialize(challenge); + byte[] data = new Type3Message(Username, Password, msg.Challenge, + "Workstation").Serialize(); + return data; + } catch (Exception e) { + throw new SaslException("The challenge response could not be " + + "computed.", e); + } + } + } +} diff --git a/Mechanisms/SaslNtlmv2.cs b/Mechanisms/SaslNtlmv2.cs new file mode 100644 index 0000000..6514cbb --- /dev/null +++ b/Mechanisms/SaslNtlmv2.cs @@ -0,0 +1,70 @@ +using S22.Sasl.Mechanisms.Ntlm; +using System; + +namespace S22.Sasl.Mechanisms { + /// + /// Implements the Sasl NTLMv2 authentication method which addresses + /// some of the security issues present in NTLM version 1. + /// + internal class SaslNtlmv2 : SaslNtlm { + /// + /// Private constructor for use with Sasl.SaslFactory. + /// + protected SaslNtlmv2() { + // Nothing to do here. + } + + /// + /// Creates and initializes a new instance of the SaslNtlmv2 class + /// using the specified username and password. + /// + /// The username to authenticate with. + /// The plaintext password to authenticate + /// with. + /// Thrown if the username + /// or the password parameter is null. + /// Thrown if the username + /// parameter is empty. + public SaslNtlmv2(string username, string password) + : base(username, password) { + } + + /// + /// Computes the client response to the specified NTLM challenge. + /// + /// The challenge sent by the server + /// The response to the NTLM challenge. + /// Thrown if the response could not + /// be computed. + protected override byte[] ComputeResponse(byte[] challenge) { + if (step == 1) + completed = true; + byte[] ret = step == 0 ? ComputeInitialResponse(challenge) : + ComputeChallengeResponse(challenge); + step = step + 1; + return ret; + } + + /// + /// Computes the actual challenge response to an NTLM challenge + /// which is sent as part of an NTLM type 2 message. + /// + /// The challenge sent by the server. + /// The response to the NTLM challenge. + /// Thrown if the challenge + /// response could not be computed. + protected new byte[] ComputeChallengeResponse(byte[] challenge) { + try { + Type2Message msg = Type2Message.Deserialize(challenge); + // This creates an NTLMv2 challenge response. + byte[] data = new Type3Message(Username, Password, msg.Challenge, + Username, true, msg.TargetName, + msg.RawTargetInformation).Serialize(); + return data; + } catch (Exception e) { + throw new SaslException("The challenge response could not be " + + "computed.", e); + } + } + } +} diff --git a/Mechanisms/SaslOAuth.cs b/Mechanisms/SaslOAuth.cs new file mode 100644 index 0000000..7402703 --- /dev/null +++ b/Mechanisms/SaslOAuth.cs @@ -0,0 +1,93 @@ +using System; +using System.Text; + +namespace S22.Sasl.Mechanisms { + /// + /// Implements the Sasl OAuth authentication method. + /// + internal class SaslOAuth : SaslMechanism { + bool Completed = false; + + /// + /// Client sends the first message in the authentication exchange. + /// + public override bool HasInitial { + get { + return true; + } + } + + /// + /// True if the authentication exchange between client and server + /// has been completed. + /// + public override bool IsCompleted { + get { + return Completed; + } + } + + /// + /// The IANA name for the OAuth authentication mechanism. + /// + public override string Name { + get { + return "XOAUTH"; + } + } + + /// + /// The access token to authenticate with. + /// + string AccessToken { + get { + return Properties.ContainsKey("AccessToken") ? + Properties["AccessToken"] as string : null; + } + set { + Properties["AccessToken"] = value; + } + } + + /// + /// Private constructor for use with Sasl.SaslFactory. + /// + private SaslOAuth() { + // Nothing to do here. + } + + /// + /// Creates and initializes a new instance of the SaslOAuth class + /// using the specified username and password. + /// + /// The username to authenticate with. + /// Thrown if the accessToken + /// parameter is null. + /// Thrown if the accessToken + /// parameter is empty. + public SaslOAuth(string accessToken) { + accessToken.ThrowIfNull("accessToken"); + if (accessToken == String.Empty) + throw new ArgumentException("The access token must not be empty."); + + AccessToken = accessToken; + } + + /// + /// Computes the client response for a OAuth challenge. + /// + /// The challenge sent by the server. + /// The response to the OAuth challenge. + /// Thrown if the response could not + /// be computed. + protected override byte[] ComputeResponse(byte[] challenge) { + // Precondition: Ensure access token is not null and is not empty. + if (String.IsNullOrEmpty(AccessToken)) + throw new SaslException("The access token must not be null or empty."); + + // Sasl OAuth does not involve another roundtrip. + Completed = true; + return Encoding.ASCII.GetBytes(AccessToken); + } + } +} diff --git a/Mechanisms/SaslOAuth2.cs b/Mechanisms/SaslOAuth2.cs new file mode 100644 index 0000000..d8bd904 --- /dev/null +++ b/Mechanisms/SaslOAuth2.cs @@ -0,0 +1,138 @@ +using System; +using System.Text; + +namespace S22.Sasl.Mechanisms { + /// + /// Implements the Sasl OAuth 2.0 authentication method. + /// + internal class SaslOAuth2 : SaslMechanism { + bool Completed = false; + + /// + /// The server sends an error response in case authentication fails + /// which must be acknowledged. + /// + int Step = 0; + + /// + /// Client sends the first message in the authentication exchange. + /// + public override bool HasInitial { + get { + return true; + } + } + + /// + /// True if the authentication exchange between client and server + /// has been completed. + /// + public override bool IsCompleted { + get { + return Completed; + } + } + + /// + /// The IANA name for the OAuth 2.0 authentication mechanism. + /// + public override string Name { + get { + return "XOAUTH2"; + } + } + + /// + /// The username to authenticate with. + /// + string Username { + get { + return Properties.ContainsKey("Username") ? + Properties["Username"] as string : null; + } + set { + Properties["Username"] = value; + } + } + + /// + /// The access token to authenticate with. + /// + string AccessToken { + get { + return Properties.ContainsKey("AccessToken") ? + Properties["AccessToken"] as string : null; + } + set { + Properties["AccessToken"] = value; + } + } + + /// + /// Private constructor for use with Sasl.SaslFactory. + /// + private SaslOAuth2() { + // Nothing to do here. + } + + /// + /// Creates and initializes a new instance of the SaslOAuth class + /// using the specified username and password. + /// + /// The username to authenticate with. + /// The username to authenticate with. + /// Thrown if the username + /// or the accessToken parameter is null. + /// Thrown if the username or + /// the accessToken parameter is empty. + public SaslOAuth2(string username, string accessToken) { + username.ThrowIfNull("username"); + accessToken.ThrowIfNull("accessToken"); + if (username == String.Empty) + throw new ArgumentException("The username must not be empty."); + if(accessToken == String.Empty) + throw new ArgumentException("The access token must not be empty."); + Username = username; + AccessToken = accessToken; + } + + /// + /// Computes the client response to an XOAUTH2 challenge. + /// + /// The challenge sent by the server. + /// The response to the OAuth2 challenge. + /// Thrown if the response could not + /// be computed. + protected override byte[] ComputeResponse(byte[] challenge) { + if (Step == 1) + Completed = true; + // If authentication fails, the server responds with another + // challenge (error response) which the client must acknowledge + // with a CRLF. + byte[] ret = Step == 0 ? ComputeInitialResponse(challenge) : + new byte[0]; + Step = Step + 1; + return ret; + } + + /// + /// Computes the initial client response to an XOAUTH2 challenge. + /// + /// The challenge sent by the server. + /// The response to the OAuth2 challenge. + /// Thrown if the response could not + /// be computed. + private byte[] ComputeInitialResponse(byte[] challenge) { + // Precondition: Ensure access token is not null and is not empty. + if (String.IsNullOrEmpty(Username) || String.IsNullOrEmpty(AccessToken)) { + throw new SaslException("The username and access token must not be" + + " null or empty."); + } + // ^A = Control A = (U+0001) + char A = '\u0001'; + string s = "user=" + Username + A + "auth=Bearer " + AccessToken + A + A; + // The response is encoded as ASCII. + return Encoding.ASCII.GetBytes(s); + } + } +} diff --git a/Mechanisms/SaslPlain.cs b/Mechanisms/SaslPlain.cs new file mode 100644 index 0000000..e12b4c2 --- /dev/null +++ b/Mechanisms/SaslPlain.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace S22.Sasl.Mechanisms { + /// + /// Implements the Sasl Plain authentication method as described in + /// RFC 4616. + /// + internal class SaslPlain : SaslMechanism { + bool Completed = false; + + /// + /// Sasl Plain just sends one initial response. + /// + public override bool HasInitial { + get { + return true; + } + } + + /// + /// True if the authentication exchange between client and server + /// has been completed. + /// + public override bool IsCompleted { + get { + return Completed; + } + } + + /// + /// The IANA name for the Plain authentication mechanism as described + /// in RFC 4616. + /// + public override string Name { + get { + return "PLAIN"; + } + } + + /// + /// The username to authenticate with. + /// + string Username { + get { + return Properties.ContainsKey("Username") ? + Properties["Username"] as string : null; + } + set { + Properties["Username"] = value; + } + } + + /// + /// The plain-text password to authenticate with. + /// + string Password { + get { + return Properties.ContainsKey("Password") ? + Properties["Password"] as string : null; + } + set { + Properties["Password"] = value; + } + } + + /// + /// Private constructor for use with Sasl.SaslFactory. + /// + private SaslPlain() { + // Nothing to do here. + } + + /// + /// Creates and initializes a new instance of the SaslPlain class + /// using the specified username and password. + /// + /// The username to authenticate with. + /// The plaintext password to authenticate + /// with. + /// Thrown if the username + /// or the password parameter is null. + /// Thrown if the username + /// parameter is empty. + public SaslPlain(string username, string password) { + username.ThrowIfNull("username"); + if (username == String.Empty) + throw new ArgumentException("The username must not be empty."); + password.ThrowIfNull("password"); + + Username = username; + Password = password; + } + + /// + /// Computes the client response for a plain-challenge. + /// + /// The challenge sent by the server. For the + /// "plain" mechanism this will usually be empty. + /// The response for the "plain"-challenge. + /// Thrown if the response could not + /// be computed. + protected override byte[] ComputeResponse(byte[] challenge) { + // Precondition: Ensure username and password are not null and + // username is not empty. + if (String.IsNullOrEmpty(Username) || Password == null) { + throw new SaslException("The username must not be null or empty and " + + "the password must not be null."); + } + // Sasl Plain does not involve another roundtrip. + Completed = true; + // Username and password are delimited by a NUL (U+0000) character + // and the response shall be encoded as UTF-8. + return Encoding.UTF8.GetBytes("\0" + Username + "\0" + Password); + } + } +} diff --git a/Mechanisms/SaslScramSha1.cs b/Mechanisms/SaslScramSha1.cs new file mode 100644 index 0000000..88dd2c7 --- /dev/null +++ b/Mechanisms/SaslScramSha1.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections.Specialized; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace S22.Sasl.Mechanisms { + /// + /// Implements the Sasl SCRAM-SHA-1 authentication method as described in + /// RFC 5802. + /// + internal class SaslScramSha1 : SaslMechanism { + bool Completed = false; + + /// + /// The client nonce value used during authentication. + /// + string Cnonce = GenerateCnonce(); + + /// + /// Scram-Sha-1 involves several steps. + /// + int Step = 0; + + /// + /// The salted password. This is needed for client authentication and later + /// on again for verifying the server signature. + /// + byte[] SaltedPassword; + + /// + /// The auth message is part of the authentication exchange and is needed for + /// authentication as well as for verifying the server signature. + /// + string AuthMessage; + + /// + /// Client sends the first message in the authentication exchange. + /// + public override bool HasInitial { + get { + return true; + } + } + + /// + /// True if the authentication exchange between client and server + /// has been completed. + /// + public override bool IsCompleted { + get { + return Completed; + } + } + + /// + /// The IANA name for the Scram-Sha-1 authentication mechanism as described + /// in RFC 5802. + /// + public override string Name { + get { + return "SCRAM-SHA-1"; + } + } + + /// + /// The username to authenticate with. + /// + string Username { + get { + return Properties.ContainsKey("Username") ? + Properties["Username"] as string : null; + } + set { + Properties["Username"] = value; + } + } + + /// + /// The password to authenticate with. + /// + string Password { + get { + return Properties.ContainsKey("Password") ? + Properties["Password"] as string : null; + } + set { + Properties["Password"] = value; + } + } + + /// + /// Private constructor for use with Sasl.SaslFactory. + /// + private SaslScramSha1() { + // Nothing to do here. + } + + /// + /// Internal constructor used for unit testing. + /// + /// The username to authenticate with. + /// The plaintext password to authenticate + /// with. + /// The client nonce value to use. + /// Thrown if the username + /// or the password parameter is null. + /// Thrown if the username + /// parameter is empty. + internal SaslScramSha1(string username, string password, string cnonce) + : this(username, password) { + Cnonce = cnonce; + } + + /// + /// Creates and initializes a new instance of the SaslScramSha1 + /// class using the specified username and password. + /// + /// The username to authenticate with. + /// The plaintext password to authenticate + /// with. + /// Thrown if the username + /// or the password parameter is null. + /// Thrown if the username + /// parameter is empty. + public SaslScramSha1(string username, string password) { + username.ThrowIfNull("username"); + if (username == String.Empty) + throw new ArgumentException("The username must not be empty."); + password.ThrowIfNull("password"); + + Username = username; + Password = password; + } + + /// + /// Computes the client response to the specified SCRAM-SHA-1 challenge. + /// + /// The challenge sent by the server + /// The response to the SCRAM-SHA-1 challenge. + /// Thrown if the response could not + /// be computed. + protected override byte[] ComputeResponse(byte[] challenge) { + // Precondition: Ensure username and password are not null and + // username is not empty. + if (String.IsNullOrEmpty(Username) || Password == null) { + throw new SaslException("The username must not be null or empty and " + + "the password must not be null."); + } + if (Step == 2) + Completed = true; + byte[] ret = Step == 0 ? ComputeInitialResponse() : + (Step == 1 ? ComputeFinalResponse(challenge) : + VerifyServerSignature(challenge)); + Step = Step + 1; + return ret; + } + + /// + /// Computes the initial response sent by the client to the server. + /// + /// An array of bytes containing the initial client + /// response. + private byte[] ComputeInitialResponse() { + // We don't support channel binding. + return Encoding.UTF8.GetBytes("n,,n=" + SaslPrep(Username) + ",r=" + + Cnonce); + } + + /// + /// Computes the "client-final-message" which completes the authentication + /// process. + /// + /// The "server-first-message" challenge received + /// from the server in response to the initial client response. + /// An array of bytes containing the client's challenge + /// response. + private byte[] ComputeFinalResponse(byte[] challenge) { + NameValueCollection nv = ParseServerFirstMessage(challenge); + // Extract the server data needed to calculate the client proof. + string salt = nv["s"], nonce = nv["r"]; + int iterationCount = Int32.Parse(nv["i"]); + if (!VerifyServerNonce(nonce)) + throw new SaslException("Invalid server nonce: " + nonce); + // Calculate the client proof (refer to RFC 5802, p.7). + string clientFirstBare = "n=" + SaslPrep(Username) + ",r=" + Cnonce, + serverFirstMessage = Encoding.UTF8.GetString(challenge), + withoutProof = "c=" + + Convert.ToBase64String(Encoding.UTF8.GetBytes("n,,")) + ",r=" + + nonce; + AuthMessage = clientFirstBare + "," + serverFirstMessage + "," + + withoutProof; + SaltedPassword = Hi(Password, salt, iterationCount); + byte[] clientKey = HMAC(SaltedPassword, "Client Key"), + storedKey = H(clientKey), + clientSignature = HMAC(storedKey, AuthMessage), + clientProof = Xor(clientKey, clientSignature); + // Return the client final message. + return Encoding.UTF8.GetBytes(withoutProof + ",p=" + + Convert.ToBase64String(clientProof)); + } + + /// + /// Verifies the nonce value sent by the server. + /// + /// The nonce value sent by the server as part of the + /// server-first-message. + /// True if the nonce value is valid, otherwise false. + bool VerifyServerNonce(string nonce) { + // The first part of the server nonce must be the nonce sent by the + // client in its initial response. + return nonce.StartsWith(Cnonce); + } + + /// + /// Verifies the server signature which is sent by the server as the final + /// step of the authentication process. + /// + /// The server signature as a base64-encoded + /// string. + /// The client's response to the server. This will be an empty + /// byte array if verification was successful, or the '*' SASL cancellation + /// token. + private byte[] VerifyServerSignature(byte[] challenge) { + string s = Encoding.UTF8.GetString(challenge); + // The server must respond with a "v=signature" message. + if (!s.StartsWith("v=")) { + // Cancel authentication process. + return Encoding.UTF8.GetBytes("*"); + } + byte[] serverSignature = Convert.FromBase64String(s.Substring(2)); + // Verify server's signature. + byte[] serverKey = HMAC(SaltedPassword, "Server Key"), + calculatedSignature = HMAC(serverKey, AuthMessage); + // If both signatures are equal, server has been authenticated. Otherwise + // cancel the authentication process. + return serverSignature.SequenceEqual(calculatedSignature) ? + new byte[0] : Encoding.UTF8.GetBytes("*"); + } + + /// + /// Parses the "server-first-message" received from the server. + /// + /// The challenge received from the server. + /// A collection of key/value pairs contained extracted from + /// the server message. + /// Thrown if the message parameter + /// is null. + private NameValueCollection ParseServerFirstMessage(byte[] challenge) { + challenge.ThrowIfNull("challenge"); + string message = Encoding.UTF8.GetString(challenge); + NameValueCollection coll = new NameValueCollection(); + foreach (string s in message.Split(',')) { + int delimiter = s.IndexOf('='); + if (delimiter < 0) + continue; + string name = s.Substring(0, delimiter), value = + s.Substring(delimiter + 1); + coll.Add(name, value); + } + return coll; + } + + /// + /// Computes the "Hi()"-formula which is part of the client's response + /// to the server challenge. + /// + /// The supplied password to use. + /// The salt received from the server. + /// The iteration count. + /// An array of bytes containing the result of the + /// computation of the "Hi()"-formula. + /// Hi is, essentially, PBKDF2 with HMAC as the + /// pseudorandom function (PRF) and with dkLen == output length of + /// HMAC() == output length of H(). (Refer to RFC 5802, p.6) + private byte[] Hi(string password, string salt, int count) { + // The salt is sent by the server as a base64-encoded string. + byte[] saltBytes = Convert.FromBase64String(salt); + using (var db = new Rfc2898DeriveBytes(password, saltBytes, count)) { + // Generate 20 key bytes, which is the size of the hash result of SHA-1. + return db.GetBytes(20); + } + } + + /// + /// Applies the HMAC keyed hash algorithm using the specified key to + /// the specified input data. + /// + /// The key to use for initializing the HMAC + /// provider. + /// The input to compute the hashcode for. + /// The hashcode of the specified data input. + private byte[] HMAC(byte[] key, byte[] data) { + using (var hmac = new HMACSHA1(key)) { + return hmac.ComputeHash(data); + } + } + + /// + /// Applies the HMAC keyed hash algorithm using the specified key to + /// the specified input string. + /// + /// The key to use for initializing the HMAC + /// provider. + /// The input string to compute the hashcode for. + /// The hashcode of the specified string. + private byte[] HMAC(byte[] key, string data) { + return HMAC(key, Encoding.UTF8.GetBytes(data)); + } + + /// + /// Applies the cryptographic hash function SHA-1 to the specified data + /// array. + /// + /// The data array to apply the hash function to. + /// The hash value for the specified byte array. + private byte[] H(byte[] data) { + using (var sha1 = new SHA1Managed()) { + return sha1.ComputeHash(data); + } + } + + /// + /// Applies the exclusive-or operation to combine the specified byte array + /// a with the specified byte array b. + /// + /// The first byte array. + /// The second byte array. + /// An array of bytes of the same length as the input arrays + /// containing the result of the exclusive-or operation. + /// Thrown if either argument is + /// null. + /// Thrown if the input arrays + /// are not of the same length. + private byte[] Xor(byte[] a, byte[] b) { + a.ThrowIfNull("a"); + b.ThrowIfNull("b"); + if (a.Length != b.Length) + throw new InvalidOperationException(); + byte[] ret = new byte[a.Length]; + for (int i = 0; i < a.Length; i++) { + ret[i] = (byte)(a[i] ^ b[i]); + } + return ret; + } + + /// + /// Generates a random cnonce-value which is a "client-specified data string + /// which must be different each time a digest-response is sent". + /// + /// A random "cnonce-value" string. + private static string GenerateCnonce() { + return Guid.NewGuid().ToString("N").Substring(0, 16); + } + + /// + /// Prepares the specified string as is described in RFC 5802. + /// + /// A string value. + /// A "Saslprepped" string. + private static string SaslPrep(string s) { + // Fixme: Do this properly? + return s + .Replace("=", "=3D") + .Replace(",", "=2C"); + } + } +} diff --git a/Mechanisms/SaslSrp.cs b/Mechanisms/SaslSrp.cs new file mode 100644 index 0000000..0b1328c --- /dev/null +++ b/Mechanisms/SaslSrp.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using S22.Sasl.Mechanisms.Srp; +using System.Security.Cryptography; + +namespace S22.Sasl.Mechanisms { + /// + /// Implements the Sasl Secure Remote Password (SRP) authentication + /// mechanism as is described in the IETF SRP 08 draft. + /// + /// + /// Some notes: + /// - Don't bother with the example given in the IETF 08 draft + /// document (7.5 Example); It is broken. + /// - Integrity and confidentiality protection is not implemented. + /// In fact, the "mandatory"-option is not supported at all. + /// + internal class SaslSrp : SaslMechanism { + bool Completed = false; + + /// + /// SRP involves several steps. + /// + int Step = 0; + + /// + /// The negotiated hash algorithm which will be used to perform any + /// message digest calculations. + /// + HashAlgorithm HashAlgorithm; + + /// + /// The public key computed as part of the authentication exchange. + /// + Mpi PublicKey; + + /// + /// The client's private key used for calculating the client evidence. + /// + Mpi PrivateKey = Helper.GenerateClientPrivateKey(); + + /// + /// The secret key shared between client and server. + /// + Mpi SharedKey; + + /// + /// The client evidence calculated as part of the authentication exchange. + /// + byte[] ClientProof; + + /// + /// The options chosen by the client, picked from the list of options + /// advertised by the server. + /// + string Options; + + /// + /// Client sends the first message in the authentication exchange. + /// + public override bool HasInitial { + get { + return true; + } + } + + /// + /// True if the authentication exchange between client and server + /// has been completed. + /// + public override bool IsCompleted { + get { + return Completed; + } + } + + /// + /// The IANA name for the SRP authentication mechanism. + /// + public override string Name { + get { + return "SRP"; + } + } + + /// + /// The username to authenticate with. + /// + string Username { + get { + return Properties.ContainsKey("Username") ? + Properties["Username"] as string : null; + } + set { + Properties["Username"] = value; + } + } + + /// + /// The password to authenticate with. + /// + string Password { + get { + return Properties.ContainsKey("Password") ? + Properties["Password"] as string : null; + } + set { + Properties["Password"] = value; + } + } + + /// + /// The authorization id (userid in draft jargon). + /// + string AuthId { + get { + return Properties.ContainsKey("AuthId") ? + Properties["AuthId"] as string : Username; + } + set { + Properties["AuthId"] = value; + } + } + + /// + /// Private constructor for use with Sasl.SaslFactory. + /// + private SaslSrp() { + // Nothing to do here. + } + + /// + /// Internal constructor used for unit testing. + /// + /// The username to authenticate with. + /// The plaintext password to authenticate + /// with. + /// The client private key to use. + /// Thrown if the username + /// or the password parameter is null. + /// Thrown if the username + /// parameter is empty. + internal SaslSrp(string username, string password, byte[] privateKey) + : this(username, password) { + PrivateKey = new Mpi(privateKey); + } + + /// + /// Creates and initializes a new instance of the SaslSrp class using + /// the specified username and password. + /// + /// The username to authenticate with. + /// The plaintext password to authenticate + /// with. + /// Thrown if the username + /// or the password parameter is null. + /// Thrown if the username + /// parameter is empty. + public SaslSrp(string username, string password) { + username.ThrowIfNull("username"); + if (username == String.Empty) + throw new ArgumentException("The username must not be empty."); + password.ThrowIfNull("password"); + + Username = username; + Password = password; + } + + /// + /// Computes the client response to the specified SRP challenge. + /// + /// The challenge sent by the server + /// The response to the SRP challenge. + /// Thrown if the response could not + /// be computed. + protected override byte[] ComputeResponse(byte[] challenge) { + // Precondition: Ensure username and password are not null and + // username is not empty. + if (String.IsNullOrEmpty(Username) || Password == null) { + throw new SaslException("The username must not be null or empty and " + + "the password must not be null."); + } + if (Step == 2) + Completed = true; + byte[] ret = Step == 0 ? ComputeInitialResponse() : + (Step == 1 ? ComputeFinalResponse(challenge) : + VerifyServerSignature(challenge)); + Step = Step + 1; + return ret; + } + + /// + /// Computes the initial response sent by the client to the server. + /// + /// An array of bytes containing the initial client + /// response. + private byte[] ComputeInitialResponse() { + return new ClientMessage1(Username, AuthId).Serialize(); + } + + /// + /// Computes the client response containing the client's public key and + /// evidence. + /// + /// The challenge containing the protocol elements + /// received from the server in response to the initial client + /// response. + /// An array of bytes containing the client's challenge + /// response. + /// Thrown if the server specified any + /// mandatory options which are not supported. + private byte[] ComputeFinalResponse(byte[] challenge) { + ServerMessage1 m = ServerMessage1.Deserialize(challenge); + // We don't support integrity protection or confidentiality. + if (!String.IsNullOrEmpty(m.Options["mandatory"])) + throw new SaslException("Mandatory options are not supported."); + // Set up the message digest algorithm. + var mda = SelectHashAlgorithm(m.Options["mda"]); + HashAlgorithm = Activator.CreateInstance(mda.Item2) as HashAlgorithm; + + // Compute public and private key. + PublicKey = Helper.ComputeClientPublicKey(m.Generator, + m.SafePrimeModulus, PrivateKey); + // Compute the shared key and client evidence. + SharedKey = Helper.ComputeSharedKey(m.Salt, Username, Password, + PublicKey, m.PublicKey, PrivateKey, m.Generator, m.SafePrimeModulus, + HashAlgorithm); + ClientProof = Helper.ComputeClientProof(m.SafePrimeModulus, + m.Generator, Username, m.Salt, PublicKey, m.PublicKey, SharedKey, + AuthId, m.RawOptions, HashAlgorithm); + + ClientMessage2 response = new ClientMessage2(PublicKey, ClientProof); + // Let the server know which hash algorithm we are using. + response.Options["mda"] = mda.Item1; + // Remember the raw options string because we'll need it again + // when verifying the server signature. + Options = response.BuildOptionsString(); + + return response.Serialize(); + } + + /// + /// Verifies the server signature which is sent by the server as the final + /// step of the authentication process. + /// + /// The server signature as a base64-encoded + /// string. + /// The client's response to the server. This will be an empty + /// byte array if verification was successful, or the '*' SASL cancellation + /// token. + private byte[] VerifyServerSignature(byte[] challenge) { + ServerMessage2 m = ServerMessage2.Deserialize(challenge); + // Compute the proof and compare it with the one sent by the server. + byte[] proof = Helper.ComputeServerProof(PublicKey, ClientProof, SharedKey, + AuthId, Options, m.SessionId, m.Ttl, HashAlgorithm); + return proof.SequenceEqual(m.Proof) ? new byte[0] : + Encoding.UTF8.GetBytes("*"); + } + + /// + /// Selects a message digest algorithm from the specified list of + /// supported algorithms. + /// + /// A tuple containing the name of the selected message digest + /// algorithm as well as the type. + /// Thrown if none of the algorithms + /// specified in the list parameter is supported. + private Tuple SelectHashAlgorithm(string list) { + string[] supported = list.Split(','); + var l = new Dictionary(StringComparer.OrdinalIgnoreCase) { + { "SHA-1", typeof(SHA1Managed) }, + { "SHA-256", typeof(SHA256Managed) }, + { "SHA-384", typeof(SHA384Managed) }, + { "SHA-512", typeof(SHA512Managed) }, + { "RIPEMD-160", typeof(RIPEMD160Managed) }, + { "MD5", typeof(MD5CryptoServiceProvider) } + }; + foreach (KeyValuePair p in l) { + if (supported.Contains(p.Key, StringComparer.InvariantCultureIgnoreCase)) + return new Tuple(p.Key, p.Value); + } + throw new NotSupportedException(); + } + } +} diff --git a/Mechanisms/Srp/ClientMessage1.cs b/Mechanisms/Srp/ClientMessage1.cs new file mode 100644 index 0000000..9b70d0e --- /dev/null +++ b/Mechanisms/Srp/ClientMessage1.cs @@ -0,0 +1,95 @@ +using System; + +namespace S22.Sasl.Mechanisms.Srp { + /// + /// Represents the initial client-response sent to the server to initiate + /// the authentication exchange. + /// + internal class ClientMessage1 { + /// + /// The username to authenticate with. + /// + /// SRP specification imposes a limit of 65535 bytes + /// on this field. + public string Username { + get; + set; + } + + /// + /// The authorization identity to authenticate with. + /// + /// SRP specification imposes a limit of 65535 bytes + /// on this field. + public string AuthId { + get; + set; + } + + /// + /// The session identifier of a previous session whose parameters the + /// client wishes to re-use. + /// + /// SRP specification imposes a limit of 65535 bytes + /// on this field. If the client wishes to initialize a new session, + /// this parameter must be set to the empty string. + public string SessionId { + get; + set; + } + + /// + /// The client's nonce used in deriving a new shared context key from + /// the shared context key of the previous session. + /// + /// SRP specification imposes a limit of 255 bytes on this + /// field. If not needed, it must be set to an empty byte array. + public byte[] ClientNonce { + get; + set; + } + + /// + /// Creates a new instance of the ClientMessage1 class using the specified + /// username. + /// + /// The username to authenticate with. + /// The authorization id to authenticate with. + /// Thrown if the username parameter + /// is null. + public ClientMessage1(string username, string authId = null) { + username.ThrowIfNull("username"); + Username = username; + AuthId = authId ?? String.Empty; + SessionId = String.Empty; + ClientNonce = new byte[0]; + } + + /// + /// Serializes this instance of the ClientMessage1 class into a sequence of + /// bytes according to the requirements of the SRP specification. + /// + /// A sequence of bytes representing this instance of the + /// ClientMessage1 class. + /// Thrown if the cummultative length + /// of the serialized data fields exceeds the maximum number of bytes + /// allowed as per SRP specification. + /// SRP specification imposes a limit of 2,147,483,643 bytes on + /// the serialized data. + public byte[] Serialize() { + byte[] username = new Utf8String(Username).Serialize(), + authId = new Utf8String(AuthId).Serialize(), + sessionId = new Utf8String(SessionId).Serialize(), + nonce = new OctetSequence(ClientNonce).Serialize(); + int length = username.Length + + authId.Length + sessionId.Length + nonce.Length; + return new ByteBuilder() + .Append(length, true) + .Append(username) + .Append(authId) + .Append(sessionId) + .Append(nonce) + .ToArray(); + } + } +} diff --git a/Mechanisms/Srp/ClientMessage2.cs b/Mechanisms/Srp/ClientMessage2.cs new file mode 100644 index 0000000..46bb9ae --- /dev/null +++ b/Mechanisms/Srp/ClientMessage2.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; + +namespace S22.Sasl.Mechanisms.Srp { + /// + /// Represents the second client-response sent to the server as part of + /// the SRP authentication exchange. + /// + internal class ClientMessage2 { + /// + /// The client's ephemeral public key. + /// + public Mpi PublicKey { + get; + set; + } + + /// + /// The evidence which proves to the server client-knowledge of the shared + /// context key. + /// + public byte[] Proof { + get; + set; + } + + /// + /// The options list indicating the security services chosen by the client. + /// + public NameValueCollection Options { + get; + private set; + } + + /// + /// The initial vector the server will use to set up its encryption + /// context, if confidentiality protection will be employed. + /// + public byte[] InitialVector { + get; + set; + } + + /// + /// Creates and initializes a new instance of the ClientMessage2 class. + /// + private ClientMessage2() { + Options = new NameValueCollection(); + InitialVector = new byte[0]; + } + + /// + /// Creates and initializes a new instance of the ClientMessage2 class using + /// the specified public key and client proof. + /// + /// The client's public key. + /// The calculated client proof. + /// Thrown if either the public key + /// or the proof parameter is null. + public ClientMessage2(Mpi publicKey, byte[] proof) + : this() { + publicKey.ThrowIfNull("publicKey"); + proof.ThrowIfNull("proof"); + + PublicKey = publicKey; + Proof = proof; + } + + /// + /// Serializes this instance of the ClientMessage2 class into a sequence of + /// bytes according to the requirements of the SRP specification. + /// + /// A sequence of bytes representing this instance of the + /// ClientMessage2 class. + /// Thrown if the cummultative length + /// of the serialized data fields exceeds the maximum number of bytes + /// allowed as per SRP specification. + /// SRP specification imposes a limit of 2,147,483,643 bytes on + /// the serialized data. + public byte[] Serialize() { + byte[] publicKey = PublicKey.Serialize(), + M1 = new OctetSequence(Proof).Serialize(), + cIV = new OctetSequence(InitialVector).Serialize(), + options = new Utf8String(BuildOptionsString()).Serialize(); + int length = publicKey.Length + M1.Length + cIV.Length + + options.Length; + return new ByteBuilder() + .Append(length, true) + .Append(publicKey) + .Append(M1) + .Append(options) + .Append(cIV) + .ToArray(); + } + + /// + /// Serializes the client's options collection into a comma-seperated + /// options string. + /// + /// A comma-seperated string containing the client's chosen + /// options. + public string BuildOptionsString() { + List list = new List(); + foreach (string key in Options) { + if (String.IsNullOrEmpty(Options[key]) || "true".Equals( + Options[key], StringComparison.InvariantCultureIgnoreCase)) + list.Add(key); + else + list.Add(key + "=" + Options[key]); + } + return String.Join(",", list.ToArray()); + } + } +} diff --git a/Mechanisms/Srp/Extensions.cs b/Mechanisms/Srp/Extensions.cs new file mode 100644 index 0000000..fc19b2d --- /dev/null +++ b/Mechanisms/Srp/Extensions.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using System.Text; +using System.Linq; + +namespace S22.Sasl.Mechanisms.Srp { + /// + /// Adds extension methods to the BinaryReader class to simplify the + /// deserialization of SRP messages. + /// + internal static class BinaryReaderExtensions { + /// + /// Reads an unsigned integer value from the underlying stream, + /// optionally using big endian byte ordering. + /// + /// Extension method for BinaryReader. + /// Set to true to interpret the integer value + /// as big endian value. + /// The 32-byte unsigned integer value read from the underlying + /// stream. + public static uint ReadUInt32(this BinaryReader reader, bool bigEndian) { + if (!bigEndian) + return reader.ReadUInt32(); + int ret = 0; + ret |= (reader.ReadByte() << 24); + ret |= (reader.ReadByte() << 16); + ret |= (reader.ReadByte() << 8); + ret |= (reader.ReadByte() << 0); + return (uint) ret; + } + + /// + /// Reads an unsigned short value from the underlying stream, optionally + /// using big endian byte ordering. + /// + /// Extension method for BinaryReader. + /// Set to true to interpret the short value + /// as big endian value. + /// The 16-byte unsigned short value read from the underlying + /// stream. + public static ushort ReadUInt16(this BinaryReader reader, bool bigEndian) { + if (!bigEndian) + return reader.ReadUInt16(); + int ret = 0; + ret |= (reader.ReadByte() << 8); + ret |= (reader.ReadByte() << 0); + return (ushort) ret; + } + + /// + /// Reads a "multi-precision integer" from this instance. + /// + /// Extension method for the BinaryReader class. + /// An instance of the Mpi class decoded from the bytes read + /// from the underlying stream. + public static Mpi ReadMpi(this BinaryReader reader) { + ushort length = reader.ReadUInt16(true); + byte[] data = reader.ReadBytes(length); + + return new Mpi(data); + } + + /// + /// Reads an "octet-sequence" from this instance. + /// + /// Extension method for the BinaryReader class. + /// An instance of the OctetSequence class decoded from the bytes + /// read from the underlying stream. + public static OctetSequence ReadOs(this BinaryReader reader) { + byte length = reader.ReadByte(); + byte[] data = reader.ReadBytes(length); + + return new OctetSequence(data); + } + + /// + /// Reads an UTF-8 string from this instance. + /// + /// Extension method for the BinaryReader class. + /// An instance of the Utf8String class decoded from the bytes + /// read from the underlying stream. + public static Utf8String ReadUtf8String(this BinaryReader reader) { + ushort length = reader.ReadUInt16(true); + byte[] data = reader.ReadBytes(length); + + return new Utf8String(Encoding.UTF8.GetString(data)); + } + } +} diff --git a/Mechanisms/Srp/Helper.cs b/Mechanisms/Srp/Helper.cs new file mode 100644 index 0000000..7c15429 --- /dev/null +++ b/Mechanisms/Srp/Helper.cs @@ -0,0 +1,321 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; + +namespace S22.Sasl.Mechanisms.Srp { + /// + /// Contains helper methods for calculating the various components of the + /// SRP authentication exchange. + /// + internal static class Helper { + /// + /// The trace source used for informational and debug messages. + /// + static TraceSource ts = new TraceSource("S22.Imap.Sasl.Srp"); + + /// + /// Determines whether the specified modulus is valid. + /// + /// The modulus to validate. + /// True if the specified modulus is valid, otherwise + /// false. + public static bool IsValidModulus(Mpi N) { + foreach (string s in moduli) { + BigInteger a = BigInteger.Parse(s, NumberStyles.HexNumber); + if (BigInteger.Compare(a, N.Value) == 0) + return true; + } + // Fixme: Perform proper validation? + return false; + } + + /// + /// Determines whether the specified generator is valid. + /// + /// The generator to validate. + /// True if the specified generator is valid, otherwise + /// false. + public static bool IsValidGenerator(Mpi g) { + return BigInteger.Compare(new BigInteger(2), g.Value) == 0; + } + + /// + /// Generates a random "multi-precision integer" which will act as the + /// client's private key. + /// + /// The client's ephemeral private key as a "multi-precision + /// integer". + public static Mpi GenerateClientPrivateKey() { + using (var rng = new RNGCryptoServiceProvider()) { + byte[] data = new byte[16]; + rng.GetBytes(data); + + return new Mpi(data); + } + } + + /// + /// Calculates the client's ephemeral public key. + /// + /// The generator sent by the server. + /// The safe prime modulus sent by + /// the server. + /// The client's private key. + /// The client's ephemeral public key as a + /// "multi-precision integer". + /// + /// A = Client Public Key + /// g = Generator + /// a = Client Private Key + /// N = Safe Prime Modulus + /// + public static Mpi ComputeClientPublicKey(Mpi generator, Mpi safePrimeModulus, + Mpi privateKey) { + // A = g ^ a % N + BigInteger result = BigInteger.ModPow(generator.Value, privateKey.Value, + safePrimeModulus.Value); + + return new Mpi(result); + } + + /// + /// Calculates the shared context key K from the given parameters. + /// + /// The user's password salt. + /// The username to authenticate with. + /// The password to authenticate with. + /// The client's ephemeral public key. + /// The server's ephemeral public key. + /// The client's private key. + /// The generator sent by the server. + /// The safe prime modulus sent by the + /// server. + /// The negotiated hash algorithm to use + /// for the calculations. + /// The shared context key K as a "multi-precision + /// integer". + /// + /// A = Client Public Key + /// B = Server Public Key + /// N = Safe Prime Modulus + /// U = Username + /// p = Password + /// s = User's Password Salt + /// a = Client Private Key + /// g = Generator + /// K = Shared Public Key + /// + public static Mpi ComputeSharedKey(byte[] salt, string username, + string password, Mpi clientPublicKey, Mpi serverPublicKey, + Mpi clientPrivateKey, Mpi generator, Mpi safePrimeModulus, + HashAlgorithm hashAlgorithm) { + // u = H(A | B) + byte[] u = hashAlgorithm.ComputeHash(new ByteBuilder() + .Append(clientPublicKey.ToBytes()) + .Append(serverPublicKey.ToBytes()) + .ToArray()); + // x = H(s | H(U | ":" | p)) + byte[] up = hashAlgorithm.ComputeHash( + Encoding.UTF8.GetBytes(username + ":" + password)), + sup = new ByteBuilder().Append(salt).Append(up).ToArray(), + x = hashAlgorithm.ComputeHash(sup); + // S = ((B - (3 * g ^ x)) ^ (a + u * x)) % N + Mpi _u = new Mpi(u), _x = new Mpi(x); + ts.TraceInformation("ComputeSharedKey: _u = " + _u.Value.ToString("X")); + ts.TraceInformation("ComputeSharedKey: _x = " + _x.Value.ToString("X")); + // base = B - (3 * (g ^ x)) + + BigInteger _base = BigInteger.Subtract(serverPublicKey.Value, + BigInteger.Multiply(new BigInteger(3), + BigInteger.ModPow(generator.Value, _x.Value, safePrimeModulus.Value)) % + safePrimeModulus.Value); + if (_base.Sign < 0) + _base = BigInteger.Add(_base, safePrimeModulus.Value); + ts.TraceInformation("ComputeSharedKey: _base = " + _base.ToString("X")); + + // Alternative way to calculate base; This is not being used in actual calculations + // but still here to ease debugging. + BigInteger gx = BigInteger.ModPow(generator.Value, _x.Value, safePrimeModulus.Value), + gx3 = BigInteger.Multiply(new BigInteger(3), gx) % safePrimeModulus.Value; + ts.TraceInformation("ComputeSharedKey: gx = " + gx.ToString("X")); + BigInteger @base = BigInteger.Subtract(serverPublicKey.Value, gx3) % safePrimeModulus.Value; + if (@base.Sign < 0) + @base = BigInteger.Add(@base, safePrimeModulus.Value); + ts.TraceInformation("ComputeSharedKey: @base = " + @base.ToString("X")); + + // exp = a + u * x + BigInteger exp = BigInteger.Add(clientPrivateKey.Value, + BigInteger.Multiply(_u.Value, _x.Value)), + S = BigInteger.ModPow(_base, exp, safePrimeModulus.Value); + ts.TraceInformation("ComputeSharedKey: exp = " + exp.ToString("X")); + ts.TraceInformation("ComputeSharedKey: S = " + S.ToString("X")); + + // K = H(S) + return new Mpi(hashAlgorithm.ComputeHash(new Mpi(S).ToBytes())); + } + + /// + /// Computes the client evidence from the given parameters. + /// + /// The safe prime modulus sent by the + /// server. + /// The generator sent by the server. + /// The username to authenticate with. + /// The client's password salt. + /// The client's ephemeral public key. + /// The server's ephemeral public key. + /// The shared context key. + /// The authorization identity. + /// The raw options string as received from the + /// server. + /// The message digest algorithm to use for + /// calculating the client proof. + /// The client proof as an array of bytes. + public static byte[] ComputeClientProof(Mpi safePrimeModulus, Mpi generator, + string username, byte[] salt, Mpi clientPublicKey, Mpi serverPublicKey, + Mpi sharedKey, string authId, string options, HashAlgorithm hashAlgorithm) { + byte[] N = safePrimeModulus.ToBytes(), g = generator.ToBytes(), + U = Encoding.UTF8.GetBytes(username), s = salt, + A = clientPublicKey.ToBytes(), B = serverPublicKey.ToBytes(), + K = sharedKey.ToBytes(), I = Encoding.UTF8.GetBytes(authId), + L = Encoding.UTF8.GetBytes(options); + HashAlgorithm H = hashAlgorithm; + // The proof is calculated as follows: + // + // H( bytes(H( bytes(N) )) ^ bytes( H( bytes(g) )) + // | bytes(H( bytes(U) )) + // | bytes(s) + // | bytes(A) + // | bytes(B) + // | bytes(K) + // | bytes(H( bytes(I) )) + // | bytes(H( bytes(L) )) + // ) + byte[] seq = new ByteBuilder() + .Append(Xor(H.ComputeHash(N), H.ComputeHash(g))) + .Append(H.ComputeHash(U)) + .Append(s) + .Append(A) + .Append(B) + .Append(K) + .Append(H.ComputeHash(I)) + .Append(H.ComputeHash(L)) + .ToArray(); + return H.ComputeHash(seq); + } + + /// + /// Computes the server evidence from the given parameters. + /// + /// The client's ephemeral public key. + /// + /// The shared context key. + /// The authorization identity. + /// The raw options string as sent by the + /// client. + /// The session id sent by the server. + /// The time-to-live value for the session id sent + /// by the server. + /// The message digest algorithm to use for + /// calculating the server proof. + /// The server proof as an array of bytes. + public static byte[] ComputeServerProof(Mpi clientPublicKey, byte[] clientProof, + Mpi sharedKey, string authId, string options, string sid, uint ttl, + HashAlgorithm hashAlgorithm) { + byte[] A = clientPublicKey.ToBytes(), M1 = clientProof, + K = sharedKey.ToBytes(), I = Encoding.UTF8.GetBytes(authId), + o = Encoding.UTF8.GetBytes(options), _sid = Encoding.UTF8.GetBytes(sid); + HashAlgorithm H = hashAlgorithm; + // The proof is calculated as follows: + // + // H( bytes(A) + // | bytes(M1) + // | bytes(K) + // | bytes(H( bytes(I) )) + // | bytes(H( bytes(o) )) + // | bytes(sid) + // | ttl + // ) + byte[] seq = new ByteBuilder() + .Append(A) + .Append(M1) + .Append(K) + .Append(H.ComputeHash(I)) + .Append(H.ComputeHash(o)) + .Append(_sid) + .Append(ttl, true) + .ToArray(); + return H.ComputeHash(seq); + } + + /// + /// Applies the exclusive-or operation to combine the specified byte array + /// a with the specified byte array b. + /// + /// The first byte array. + /// The second byte array. + /// An array of bytes of the same length as the input arrays + /// containing the result of the exclusive-or operation. + /// Thrown if either argument is + /// null. + /// Thrown if the input arrays + /// are not of the same length. + static byte[] Xor(byte[] a, byte[] b) { + a.ThrowIfNull("a"); + b.ThrowIfNull("b"); + if (a.Length != b.Length) + throw new InvalidOperationException(); + byte[] ret = new byte[a.Length]; + for (int i = 0; i < a.Length; i++) { + ret[i] = (byte) (a[i] ^ b[i]); + } + return ret; + } + + #region Recommended Modulus values + /// + /// Recommended values for the safe prime modulus (Refer to Appendix A. + /// "Modulus and Generator Values" of the IETF SRP draft). + /// + static string[] moduli = new string[] { + "115B8B692E0E045692CF280B436735C77A5A9E8A9E7ED56C965F87DB5B2A2ECE3", + "8025363296FB943FCE54BE717E0E2958A02A9672EF561953B2BAA3BAACC3ED5754" + + "EB764C7AB7184578C57D5949CCB41B", + "D4C7F8A2B32C11B8FBA9581EC4BA4F1B04215642EF7355E37C0FC0443EF756EA2C" + + "6B8EEB755A1C723027663CAA265EF785B8FF6A9B35227A52D86633DBDFCA43", + "C94D67EB5B1A2346E8AB422FC6A0EDAEDA8C7F894C9EEEC42F9ED250FD7F0046E5" + + "AF2CF73D6B2FA26BB08033DA4DE322E144E7A8E9B12A0E4637F6371F34A2071C4B" + + "3836CBEEAB15034460FAA7ADF483", + "B344C7C4F8C495031BB4E04FF8F84EE95008163940B9558276744D91F7CC9F4026" + + "53BE7147F00F576B93754BCDDF71B636F2099E6FFF90E79575F3D0DE694AFF737D" + + "9BE9713CEF8D837ADA6380B1093E94B6A529A8C6C2BE33E0867C60C3262B", + "EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D6" + + "74DF7496EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7" + + "D7B46154D6B6CE8EF4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC" + + "3C05726CC02FD4CBF4976EAA9AFD5138FE8376435B9FC6", + "D77946826E811914B39401D56A0A7843A8E7575D738C672A090AB1187D690DC438" + + "72FC06A7B6A43F3B95BEAEC7DF04B9D242EBDC481111283216CE816E004B786C5F" + + "CE856780D41837D95AD787A50BBE90BD3A9C98AC0F5FC0DE744B1CDE1891690894" + + "BC1F65E00DE15B4B2AA6D87100C9ECC2527E45EB849DEB14BB2049B163EA04187F" + + "D27C1BD9C7958CD40CE7067A9C024F9B7C5A0B4F5003686161F0605B", + "9DEF3CAFB939277AB1F12A8617A47BBBDBA51DF499AC4C80BEEEA9614B19CC4D5F" + + "4F5F556E27CBDE51C6A94BE4607A291558903BA0D0F84380B655BB9A22E8DCDF02" + + "8A7CEC67F0D08134B1C8B97989149B609E0BE3BAB63D47548381DBC5B1FC764E3F" + + "4B53DD9DA1158BFD3E2B9C8CF56EDF019539349627DB2FD53D24B7C48665772E43" + + "7D6C7F8CE442734AF7CCB7AE837C264AE3A9BEB87F8A2FE9B8B5292E5A021FFF5E" + + "91479E8CE7A28C2442C6F315180F93499A234DCF76E3FED135F9BB", + "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A3" + + "7329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E808" + + "3969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F979" + + "93EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A" + + "23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524" + + "B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE" + + "53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7A" + + "E435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73" + }; + #endregion + } +} diff --git a/Mechanisms/Srp/Mpi.cs b/Mechanisms/Srp/Mpi.cs new file mode 100644 index 0000000..ee02745 --- /dev/null +++ b/Mechanisms/Srp/Mpi.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using System.Numerics; + +namespace S22.Sasl.Mechanisms.Srp { + /// + /// Represents a "multi-precision integer" (MPI) as is described in the + /// SRP specification (3.2 Multi-Precision Integers, p.5). + /// + /// Multi-Precision Integers, or MPIs, are positive integers used + /// to hold large integers used in cryptographic computations. + internal class Mpi { + /// + /// The underlying BigInteger instance used to represent this + /// "multi-precision integer". + /// + public BigInteger Value { + get; + set; + } + + /// + /// Creates a new "multi-precision integer" from the specified array + /// of bytes. + /// + /// A big-endian sequence of bytes forming the + /// integer value of the multi-precision integer. + public Mpi(byte[] data) { + byte[] b = new byte[data.Length]; + Array.Copy(data.Reverse().ToArray(), b, data.Length); + ByteBuilder builder = new ByteBuilder().Append(b); + // We append a null byte to the buffer which ensures the most + // significant bit will never be set and the big integer value + // always be positive. + if (b.Last() != 0) + builder.Append(0); + Value = new BigInteger(builder.ToArray()); + + } + + /// + /// Creates a new "multi-precision integer" from the specified BigInteger + /// instance. + /// + /// The BigInteger instance to initialize the MPI + /// with. + public Mpi(BigInteger value) + : this(value.ToByteArray().Reverse().ToArray()) { + } + + /// + /// Returns a sequence of bytes in big-endian order forming the integer + /// value of this "multi-precision integer" instance. + /// + /// Returns a sequence of bytes in big-endian order representing + /// this "multi-precision integer" instance. + public byte[] ToBytes() { + byte[] b = Value.ToByteArray().Reverse().ToArray(); + // Strip off the 0 byte. + if (b[0] == 0) + return b.Skip(1).ToArray(); + return b; + } + + /// + /// Serializes the "multi-precision integer" into a sequence of bytes + /// according to the requirements of the SRP specification. + /// + /// A big-endian sequence of bytes representing the integer + /// value of the MPI. + public byte[] Serialize() { + // MPI's expect a big-endian sequence of bytes forming the integer + // value, whereas BigInteger uses little-endian. + byte[] data = ToBytes(); + ushort length = Convert.ToUInt16(data.Length); + + return new ByteBuilder() + .Append(length, true) + .Append(data) + .ToArray(); + } + } +} diff --git a/Mechanisms/Srp/OctetSequence.cs b/Mechanisms/Srp/OctetSequence.cs new file mode 100644 index 0000000..d02b661 --- /dev/null +++ b/Mechanisms/Srp/OctetSequence.cs @@ -0,0 +1,47 @@ +using System; + +namespace S22.Sasl.Mechanisms.Srp { + /// + /// Represents an "octet-sequence" as is described in the SRP specification + /// (3.3 Octet sequences, p.6). + /// + internal class OctetSequence { + /// + /// The underlying byte array forming this instance of the OctetSequence + /// class. + /// + public byte[] Value { + get; + set; + } + + /// + /// Creates a new instance of the OctetSequence class using the specified + /// byte array. + /// + /// The sequence of bytes to initialize this instance + /// of the OctetSequence class with. + public OctetSequence(byte[] sequence) { + Value = sequence; + } + + /// + /// Serializes this instance of the OctetSequence class into a sequence of + /// bytes according to the requirements of the SRP specification. + /// + /// A sequence of bytes representing this instance of the + /// OctetSequence class. + /// Thrown if the length of the byte + /// sequence exceeds the maximum number of bytes allowed as per SRP + /// specification. + /// SRP specification imposes a limit of 255 bytes on the + /// length of the underlying byte array. + public byte[] Serialize() { + byte length = Convert.ToByte(Value.Length); + return new ByteBuilder() + .Append(length) + .Append(Value) + .ToArray(); + } + } +} diff --git a/Mechanisms/Srp/ServerMessage1.cs b/Mechanisms/Srp/ServerMessage1.cs new file mode 100644 index 0000000..4faf929 --- /dev/null +++ b/Mechanisms/Srp/ServerMessage1.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Specialized; +using System.IO; + +namespace S22.Sasl.Mechanisms.Srp { + /// + /// Represents the first message sent by the server in response to an + /// initial client-response. + /// + internal class ServerMessage1 { + /// + /// The safe prime modulus sent by the server. + /// + public Mpi SafePrimeModulus { + get; + set; + } + + /// + /// The generator sent by the server. + /// + public Mpi Generator { + get; + set; + } + + /// + /// The user's password salt. + /// + public byte[] Salt { + get; + set; + } + + /// + /// The server's ephemeral public key. + /// + public Mpi PublicKey { + get; + set; + } + + /// + /// The options list indicating available security services. + /// + public NameValueCollection Options { + get; + set; + } + + /// + /// The raw options as received from the server. + /// + public string RawOptions { + get; + set; + } + + /// + /// Deserializes a new instance of the ServerMessage1 class from the + /// specified buffer of bytes. + /// + /// The byte buffer to deserialize the ServerMessage1 + /// instance from. + /// An instance of the ServerMessage1 class deserialized from the + /// specified byte array. + /// Thrown if the byte buffer does not + /// contain valid data. + public static ServerMessage1 Deserialize(byte[] buffer) { + using (var ms = new MemoryStream(buffer)) { + using (var r = new BinaryReader(ms)) { + uint bufferLength = r.ReadUInt32(true); + // We don't support re-using previous sessions. + byte reuse = r.ReadByte(); + if (reuse != 0) { + throw new FormatException("Unexpected re-use parameter value: " + + reuse); + } + Mpi N = r.ReadMpi(); + Mpi g = r.ReadMpi(); + OctetSequence salt = r.ReadOs(); + Mpi B = r.ReadMpi(); + Utf8String L = r.ReadUtf8String(); + return new ServerMessage1() { + Generator = g, + PublicKey = B, + Salt = salt.Value, + SafePrimeModulus = N, + Options = ParseOptions(L.Value), + RawOptions = L.Value + }; + } + } + } + + /// + /// Parses the options string sent by the server. + /// + /// A comma-delimited options string. + /// An initialized instance of the NameValueCollection class + /// containing the parsed server options. + public static NameValueCollection ParseOptions(string s) { + NameValueCollection coll = new NameValueCollection(); + string[] parts = s.Split(','); + foreach (string p in parts) { + int index = p.IndexOf('='); + if (index < 0) { + coll.Add(p, "true"); + } else { + string name = p.Substring(0, index), value = p.Substring(index + 1); + coll.Add(name, value); + } + } + return coll; + } + } +} diff --git a/Mechanisms/Srp/ServerMessage2.cs b/Mechanisms/Srp/ServerMessage2.cs new file mode 100644 index 0000000..0d5bc72 --- /dev/null +++ b/Mechanisms/Srp/ServerMessage2.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; + +namespace S22.Sasl.Mechanisms.Srp { + /// + /// Represents the second message sent by the server as part of the SRP + /// authentication exchange. + /// + internal class ServerMessage2 { + /// + /// The evidence which proves to the client server-knowledge of the shared + /// context key. + /// + public byte[] Proof { + get; + set; + } + + /// + /// The initial vector the client will use to set up its encryption + /// context, if confidentiality protection will be employed. + /// + public byte[] InitialVector { + get; + set; + } + + /// + /// The session identifier the server has given to this session. + /// + public string SessionId { + get; + set; + } + + /// + /// The time period for which this session's parameters may be re-usable. + /// + public uint Ttl { + get; + set; + } + + /// + /// Deserializes a new instance of the ServerMessage2 class from the + /// specified buffer of bytes. + /// + /// The byte buffer to deserialize the ServerMessage2 + /// instance from. + /// An instance of the ServerMessage2 class deserialized from the + /// specified byte array. + /// Thrown if the byte buffer does not + /// contain valid data. + public static ServerMessage2 Deserialize(byte[] buffer) { + using (var ms = new MemoryStream(buffer)) { + using (var r = new BinaryReader(ms)) { + uint bufferLength = r.ReadUInt32(true); + OctetSequence M2 = r.ReadOs(), + sIV = r.ReadOs(); + Utf8String sid = r.ReadUtf8String(); + uint ttl = r.ReadUInt32(true); + return new ServerMessage2() { + Proof = M2.Value, + InitialVector = sIV.Value, + SessionId = sid.Value, + Ttl = ttl + }; + } + } + } + } +} diff --git a/Mechanisms/Srp/Utf8String.cs b/Mechanisms/Srp/Utf8String.cs new file mode 100644 index 0000000..76675cf --- /dev/null +++ b/Mechanisms/Srp/Utf8String.cs @@ -0,0 +1,45 @@ +using System; +using System.Text; + +namespace S22.Sasl.Mechanisms.Srp { + /// + /// Represents an UTF-8 string as is described in the SRP specification + /// (3.5 Text, p.6). + /// + internal class Utf8String { + /// + /// The value of the UTF-8 string. + /// + public string Value; + + /// + /// Creates a new instance of the Utf8String class using the specified + /// string value. + /// + /// The string to initialize the Utf8String instance + /// with. + public Utf8String(string s) { + Value = s; + } + + /// + /// Serializes this instance of the Utf8String class into a sequence of + /// bytes according to the requirements of the SRP specification. + /// + /// A sequence of bytes representing this instance of the + /// Utf8String class. + /// Thrown if the string value exceeds + /// the maximum number of bytes allowed as per SRP specification. + /// SRP specification imposes a limit of 65535 bytes on the + /// string data after it has been encoded into a sequence of bytes + /// using an encoding of UTF-8. + public byte[] Serialize() { + byte[] b = Encoding.UTF8.GetBytes(Value); + ushort length = Convert.ToUInt16(b.Length); + return new ByteBuilder() + .Append(length, true) + .Append(b) + .ToArray(); + } + } +} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9ea4708 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("S22.Sasl")] +[assembly: AssemblyDescription("A library implementing the Authentication and Security Layer (SASL)")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("S22.Sasl")] +[assembly: AssemblyCopyright("Copyright © Torben Könke 2013-2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f38154ca-7889-483f-b51c-a7ee49997843")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..5fe7675 --- /dev/null +++ b/Readme.md @@ -0,0 +1,70 @@ +### Introduction + +This repository contains a .NET assembly implementing the "Authentication and Security Layer" (SASL) +framework. SASL specifies a protocol for authentication and optional establishment of a security +layer between client and server applications and is used by internet protocols such as IMAP, POP3, +SMTP, XMPP and others. + + +### Usage & Examples + +To use the library add the S22.Sasl.dll assembly to your project references in Visual Studio. Here's +a simple example which instantiates a new instance of the Digest-Md5 authentication mechanism and +demonstrates how it can be used to perform authentication. + + using System; + using S22.Sasl; + + namespace Test { + class Program { + static void Main(string[] args) { + SaslMechanism m = SaslFactory.Create("Digest-Md5"); + + // Add properties needed by authentication mechanism. + m.Properties.Add("Username", "Foo"); + m.Properties.Add("Password", "Bar"); + + while(!m.IsCompleted) + { + byte[] serverChallenge = GetDataFromServer(...); + byte[] clientResponse = m.ComputeResponse(serverChallenge); + + SendMyDataToServer(clientResponse); + } + } + } + } + + +### Features + +The library supports the following authentication mechanisms: + * Plain + * Cram-Md5 + * NTLM + * NTLMv2 + * OAuth + * OAuth 2.0 + * Digest-Md5 + * Scram-Sha-1 + * SRP + +Custom SASL Security Providers can be implemented through a simple plugin +mechanism. + + +### Credits + +This library is copyright © 2013-2014 Torben Könke. + + + +### License + +This library is released under the [MIT license](https://github.com/smiley22/S22.Sasl/blob/master/License.md). + + +### Bug reports + +Please send your bug reports to [smileytwentytwo@gmail.com](mailto:smileytwentytwo@gmail.com) or create a new +issue on the GitHub project homepage. diff --git a/S22.Sasl.csproj b/S22.Sasl.csproj new file mode 100644 index 0000000..e5dc754 --- /dev/null +++ b/S22.Sasl.csproj @@ -0,0 +1,106 @@ + + + + + Debug + AnyCPU + {B860646A-13A2-47D9-9790-4719A91BF35B} + Library + Properties + S22.Sasl + S22.Sasl + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\S22.Sasl.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\..\..\..\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/S22.Sasl.sln b/S22.Sasl.sln new file mode 100644 index 0000000..ddc932a --- /dev/null +++ b/S22.Sasl.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S22.Sasl", "S22.Sasl.csproj", "{B860646A-13A2-47D9-9790-4719A91BF35B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B860646A-13A2-47D9-9790-4719A91BF35B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B860646A-13A2-47D9-9790-4719A91BF35B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B860646A-13A2-47D9-9790-4719A91BF35B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B860646A-13A2-47D9-9790-4719A91BF35B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/SaslConfiguration.cs b/SaslConfiguration.cs new file mode 100644 index 0000000..7825bbe --- /dev/null +++ b/SaslConfiguration.cs @@ -0,0 +1,175 @@ +using System.Configuration; + +namespace S22.Sasl { + /// + /// Represents the sasl section within a configuration + /// file. + /// + public class SaslConfigurationSection : ConfigurationSection { + /// + /// The saslProviders section contains a collection of + /// saslProvider elements. + /// + [ConfigurationProperty("saslProviders", IsRequired = false, + IsKey = false, IsDefaultCollection = true)] + public SaslProviderCollection SaslProviders { + get { + return ((SaslProviderCollection) base["saslProviders"]); + } + set { + base["saslProviders"] = value; + } + } + } + + /// + /// Represents a saslProvider configuration element within the + /// saslProviders section of a configuration file. + /// + [ConfigurationCollection(typeof(SaslProvider), + CollectionType=ConfigurationElementCollectionType.BasicMapAlternate)] + public class SaslProviderCollection : ConfigurationElementCollection { + /// + /// The name of the configuration element. + /// + internal const string PropertyName = "saslProvider"; + + /// + /// Gets the name used to identify this collection of elements + /// in the configuration file. + /// + protected override string ElementName { + get { return PropertyName; } + } + + /// + /// Returns the SaslProvider instance for the saslProvider + /// element with the specified name. + /// + /// The name of the saslProvider element to + /// retrieve. + /// The SaslProvider instance with the specified name + /// or null. + public new SaslProvider this[string name] { + get { + return (SaslProvider) BaseGet(name); + } + } + + /// + /// Returns the SaslProvider instance for the saslProvider + /// element at the specified index. + /// + /// The index of the saslProvider element + /// to retrieve. + /// The SaslProvider instance with the specified + /// index. + /// Thrown if the + /// index is less than 0 or if there is no SaslProvider instance + /// at the specified index. + public SaslProvider this[int index] { + get { + return (SaslProvider) BaseGet(index); + } + } + + /// + /// Gets the collection type of the SaslProviderCollection. + /// + public override ConfigurationElementCollectionType CollectionType { + get { + return ConfigurationElementCollectionType.BasicMapAlternate; + } + } + + /// + /// Indicates whether the specified System.Configuration.ConfigurationElement + /// exists in the SaslProviderCollection. + /// + /// The name of the element to verify. + /// Returns true if the element exists in the collection, + /// otherwise false. + protected override bool IsElementName(string elementName) { + return elementName == PropertyName; + } + + /// + /// Creates a new instance of the SaslProvider class. + /// + /// A new instance of the SaslProvider class. + protected override ConfigurationElement CreateNewElement() { + return new SaslProvider(); + } + + /// + /// Gets the element key for the specified SaslProvider element. + /// + /// A SaslProvider element to retrieve the + /// element key for. + /// The unique element key of the specified SaslProvider + /// instance. + protected override object GetElementKey(ConfigurationElement element) { + return ((SaslProvider) element).Name; + } + } + + /// + /// Represents a saslProvider section within the saslProviders + /// section of a configuration file. + /// + public class SaslProvider : ConfigurationSection { + /// + /// The name of the saslProvider. This attribute must be unique in + /// that no two saslProvider elements exists that have the same + /// name attribute. + /// + [ConfigurationProperty("name", IsRequired = true)] + public string Name { + get { + return (string) base["name"]; + } + set { + base["name"] = value; + } + } + + /// + /// The type name of the SaslMechanism exposed by the + /// saslProvider. + /// + [ConfigurationProperty("type", IsRequired = true)] + public string Type { + get { + return (string) base["type"]; + } + set { + base["type"] = value; + } + } + + /// + /// Retrieves the setting with the specified name for this saslProvider. + /// + /// The name of the setting to retrieve. + /// The value of the setting with the specified name or null + /// if the setting could not be found. + public new string this[string name] { + get { + if (Settings[name] != null) + return Settings[name].Value; + return null; + } + } + + /// + /// Represents a collection of arbitrary name-value pairs which can be + /// added to the saslProvider element. + /// + [ConfigurationProperty("", IsDefaultCollection = true)] + public NameValueConfigurationCollection Settings { + get { + return (NameValueConfigurationCollection) base[""]; + } + } + } +} diff --git a/SaslException.cs b/SaslException.cs new file mode 100644 index 0000000..63d645f --- /dev/null +++ b/SaslException.cs @@ -0,0 +1,41 @@ +using System; +using System.Runtime.Serialization; + +namespace S22.Sasl { + /// + /// The exception is thrown when a Sasl-related error or unexpected condition occurs. + /// + [Serializable()] + internal class SaslException : Exception { + /// + /// Initializes a new instance of the SaslException class + /// + public SaslException() : base() { } + /// + /// Initializes a new instance of the SaslException class with its message + /// string set to . + /// + /// A description of the error. The content of message is intended + /// to be understood by humans. + public SaslException(string message) : base(message) { } + /// + /// Initializes a new instance of the SaslException class with its message + /// string set to and a reference to the inner exception that + /// is the cause of this exception. + /// + /// A description of the error. The content of message is intended + /// to be understood by humans. + /// The exception that is the cause of the current exception. + public SaslException(string message, Exception inner) : base(message, inner) { } + /// + /// Initializes a new instance of the SaslException class with the specified + /// serialization and context information. + /// + /// An object that holds the serialized object data about the exception + /// being thrown. + /// An object that contains contextual information about the source + /// or destination. + protected SaslException(System.Runtime.Serialization.SerializationInfo info, StreamingContext context) + : base(info, context) { } + } +} diff --git a/SaslFactory.cs b/SaslFactory.cs new file mode 100644 index 0000000..53bb942 --- /dev/null +++ b/SaslFactory.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; + +namespace S22.Sasl { + /// + /// A factory class for producing instances of Sasl mechanisms. + /// + internal static class SaslFactory { + /// + /// A dictionary of Sasl mechanisms registered with the factory class. + /// + static Dictionary Mechanisms { + get; + set; + } + + /// + /// Creates an instance of the Sasl mechanism with the specified + /// name. + /// + /// The name of the Sasl mechanism of which an + /// instance will be created. + /// An instance of the Sasl mechanism with the specified name. + /// The name parameter is null. + /// A Sasl mechanism with the + /// specified name is not registered with Sasl.SaslFactory. + public static SaslMechanism Create(string name) { + name.ThrowIfNull("name"); + if (!Mechanisms.ContainsKey(name)) { + throw new SaslException("A Sasl mechanism with the specified name " + + "is not registered with Sasl.SaslFactory."); + } + Type t = Mechanisms[name]; + object o = Activator.CreateInstance(t, true); + return o as SaslMechanism; + } + + /// + /// Registers a Sasl mechanism with the factory using the specified name. + /// + /// The name with which to register the Sasl mechanism + /// with the factory class. + /// The type of the class implementing the Sasl mechanism. + /// The implementing class must be a subclass of Sasl.SaslMechanism. + /// The name parameter or the t + /// parameter is null. + /// The class represented by the + /// specified type does not derive from Sasl.SaslMechanism. + /// The Sasl mechanism could not be + /// registered with the factory. Refer to the inner exception for error + /// details. + public static void Add(string name, Type t) { + name.ThrowIfNull("name"); + t.ThrowIfNull("t"); + if (!t.IsSubclassOf(typeof(SaslMechanism))) { + throw new ArgumentException("The type t must be a subclass " + + "of Sasl.SaslMechanism"); + } + try { + Mechanisms.Add(name, t); + } catch (Exception e) { + throw new SaslException("Registration of Sasl mechanism failed.", e); + } + } + + /// + /// Static class constructor. Initializes static properties. + /// + static SaslFactory() { + Mechanisms = new Dictionary( + StringComparer.InvariantCultureIgnoreCase); + + // Could be moved to App.config to support SASL "plug-in" mechanisms. + var list = new Dictionary() { + { "PLAIN", typeof(Sasl.Mechanisms.SaslPlain) }, + { "DIGEST-MD5", typeof(Sasl.Mechanisms.SaslDigestMd5) }, + { "SCRAM-SHA-1", typeof(Sasl.Mechanisms.SaslScramSha1) }, + }; + foreach (string key in list.Keys) + Mechanisms.Add(key, list[key]); + } + } +} diff --git a/SaslMechanism.cs b/SaslMechanism.cs new file mode 100644 index 0000000..5b62b6b --- /dev/null +++ b/SaslMechanism.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System; + +namespace S22.Sasl { + /// + /// The abstract base class from which all classes implementing a Sasl + /// authentication mechanism must derive. + /// + internal abstract class SaslMechanism { + /// + /// IANA name of the authentication mechanism. + /// + public abstract string Name { + get; + } + + /// + /// True if the authentication exchange between client and server + /// has been completed. + /// + public abstract bool IsCompleted { + get; + } + + /// + /// True if the mechanism requires initiation by the client. + /// + public abstract bool HasInitial { + get; + } + + /// + /// A map of mechanism-specific properties which are needed by the + /// authentication mechanism to compute it's challenge-responses. + /// + public Dictionary Properties { + get; + private set; + } + + /// + /// Computes the client response to a challenge sent by the server. + /// + /// + /// The client response to the specified challenge. + protected abstract byte[] ComputeResponse(byte[] challenge); + + + /// + /// + internal SaslMechanism() { + Properties = new Dictionary(); + } + + /// + /// Retrieves the base64-encoded client response for the specified + /// base64-encoded challenge sent by the server. + /// + /// A base64-encoded string representing a challenge + /// sent by the server. + /// A base64-encoded string representing the client response to the + /// server challenge. + /// The IMAP, POP3 and SMTP authentication commands expect challenges + /// and responses to be base64-encoded. This method automatically decodes the + /// server challenge before passing it to the Sasl implementation and + /// encodes the client response to a base64-string before returning it to the + /// caller. + /// The client response could not be retrieved. + /// Refer to the inner exception for error details. + public string GetResponse(string challenge) { + try { + byte[] data = String.IsNullOrEmpty(challenge) ? new byte[0] : + Convert.FromBase64String(challenge); + byte[] response = ComputeResponse(data); + return Convert.ToBase64String(response); + } catch (Exception e) { + throw new SaslException("The challenge-response could not be " + + "retrieved.", e); + } + } + + /// + /// Retrieves the client response for the specified server challenge. + /// + /// A byte array containing the challenge sent by + /// the server. + /// An array of bytes representing the client response to the + /// server challenge. + public byte[] GetResponse(byte[] challenge) { + return ComputeResponse(challenge); + } + } +} diff --git a/Tests/CramMd5Test.cs b/Tests/CramMd5Test.cs new file mode 100644 index 0000000..8fa14c5 --- /dev/null +++ b/Tests/CramMd5Test.cs @@ -0,0 +1,28 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using S22.Sasl.Mechanisms; +using System.Text; + +namespace S22.Sasl.Test { + /// + /// Contains unit tests for the SASL CRAM-MD5 authentication mechanism. + /// + [TestClass] + public class CramMd5Test { + /// + /// Verifies the various parts of a sample authentication exchange + /// directly taken from RFC 2195 ("A.1.1. Example 1", p. 6). + /// + [TestMethod] + [TestCategory("Cram-Md5")] + public void VerifyAuthenticationExchange() { + SaslMechanism m = new SaslCramMd5("joe", "tanstaaftanstaaf"); + + string initialServer = "<1896.697170952@postoffice.example.net>", + expectedResponse = "joe 3dbc88f0624776a737b39093f6eb6427"; + + string initialResponse = Encoding.ASCII.GetString( + m.GetResponse(Encoding.ASCII.GetBytes(initialServer))); + Assert.AreEqual(expectedResponse, initialResponse); + } + } +} diff --git a/Tests/DigestMd5Test.cs b/Tests/DigestMd5Test.cs new file mode 100644 index 0000000..abb0372 --- /dev/null +++ b/Tests/DigestMd5Test.cs @@ -0,0 +1,36 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using S22.Sasl.Mechanisms; +using System; +using System.Text; + +namespace S22.Sasl.Test { + /// + /// Contains unit tests for the SASL DIGEST-MD5 authentication mechanism. + /// + [TestClass] + public class DigestMd5Test { + /// + /// Verifies the various parts of a sample authentication exchange + /// directly taken from RFC 2831 ("4 Example", p. 17-18). + /// + [TestMethod] + [TestCategory("Digest-Md5")] + public void VerifyAuthenticationExchange() { + SaslMechanism m = new SaslDigestMd5("chris", "secret", "OA6MHXh6VqTrRk"); + string initialServer = "realm=\"elwood.innosoft.com\",nonce=\"OA6MG9" + + "tEQGm2hh\",qop=\"auth\",algorithm=md5-sess,charset=utf-8", + expectedResponse = "username=\"chris\",realm=\"elwood.innosoft.com\"," + + "nonce=\"OA6MG9tEQGm2hh\",nc=00000001,cnonce=\"OA6MHXh6VqTrRk\"," + + "digest-uri=\"imap/elwood.innosoft.com\"," + + "response=d388dad90d4bbd760a152321f2143af7,qop=auth"; + + string initialResponse = Encoding.ASCII.GetString( + m.GetResponse(Encoding.ASCII.GetBytes(initialServer))); + Assert.AreEqual(expectedResponse, initialResponse); + string finalResponse = Encoding.ASCII.GetString( + m.GetResponse(Encoding.ASCII.GetBytes("rspauth=ea40f60335c427b5" + + "527b84dbabcdfffd"))); + Assert.AreEqual(String.Empty, finalResponse); + } + } +} diff --git a/Tests/NtlmTest.cs b/Tests/NtlmTest.cs new file mode 100644 index 0000000..8bbbccf --- /dev/null +++ b/Tests/NtlmTest.cs @@ -0,0 +1,295 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using S22.Sasl.Mechanisms; +using S22.Sasl.Mechanisms.Ntlm; +using System; +using System.Linq; +using System.Text; + +namespace S22.Sasl.Test { + /// + /// Contains unit tests for the NTLM Sasl mechanism. + /// + [TestClass] + public class NtmlTest { + /// + /// Serializes an NTLM type 1 message and ensures the + /// serialized byte array is identical to expected byte + /// array. + /// + [TestMethod] + [TestCategory("NTLM")] + public void SerializeType1Message() { + Type1Message msg = new Type1Message("myDomain", "myWorkstation"); + byte[] serialized = msg.Serialize(); + Assert.IsTrue(type1Message.SequenceEqual(serialized)); + } + + /// + /// Deserializes an NTLM type 2 message and ensures the + /// deserialized instance contains valid data. + /// + [TestMethod] + [TestCategory("NTLM")] + public void DeserializeType2Message() { + Type2Message msg = Type2Message.Deserialize(type2MessageVersion2); + + byte[] expectedChallenge = new byte[] { + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef + }; + Flags expectedFlags = Flags.NegotiateUnicode | + Flags.NegotiateNTLM | Flags.TargetTypeDomain | + Flags.NegotiateTargetInfo; + Assert.AreEqual(Type2Version.Version2, msg.Version); + Assert.AreEqual(expectedFlags, msg.Flags); + Assert.IsTrue(expectedChallenge.SequenceEqual(msg.Challenge)); + Assert.AreEqual(0, msg.Context); + Assert.AreEqual("DOMAIN", msg.TargetName); + Assert.AreEqual("DOMAIN", + msg.TargetInformation.DomainName); + Assert.AreEqual("SERVER", + msg.TargetInformation.ServerName); + Assert.AreEqual("domain.com", + msg.TargetInformation.DnsDomainName); + Assert.AreEqual("server.domain.com", + msg.TargetInformation.DnsHostname); + } + + /// + /// Deserializes an NTLM type 2 version 3 message and ensures the + /// deserialized instance contains valid data. + /// + [TestMethod] + [TestCategory("NTLM")] + public void DeserializeType2Version3Message() { + Type2Message msg = Type2Message.Deserialize(type2MessageVersion3); + + byte[] expectedChallenge = new byte[] { + 0xA6, 0xBC, 0xAF, 0x32, 0xA5, 0x51, 0x36, 0x65 + }; + Assert.AreEqual(Type2Version.Version3, msg.Version); + Assert.AreEqual(42009093, (int)msg.Flags); + Assert.IsTrue(expectedChallenge.SequenceEqual(msg.Challenge)); + Assert.AreEqual(0, msg.Context); + Assert.AreEqual("LOCALHOST", msg.TargetName); + Assert.AreEqual("LOCALHOST", + msg.TargetInformation.DomainName); + Assert.AreEqual("VMWARE-5T5GC9PU", + msg.TargetInformation.ServerName); + Assert.AreEqual("localhost", + msg.TargetInformation.DnsDomainName); + Assert.AreEqual("vmware-5t5gc9pu.localhost", + msg.TargetInformation.DnsHostname); + Assert.AreEqual(3790, msg.OSVersion.BuildNumber); + Assert.AreEqual(5, msg.OSVersion.MajorVersion); + Assert.AreEqual(2, msg.OSVersion.MinorVersion); + } + + /// + /// Serializes an NTLM type 3 message and ensures the + /// serialized byte array is identical to expected byte + /// array. + /// + [TestMethod] + [TestCategory("NTLM")] + public void SerializeType3Message() { + Type2Message m2 = Type2Message.Deserialize(type2MessageVersion3); + // Compute the challenge response + Type3Message msg = new Type3Message("Testuser", "Testpassword", + m2.Challenge, "MyWorkstation"); + byte[] serialized = msg.Serialize(); + + Assert.IsTrue(type3Message.SequenceEqual(serialized)); + } + + /// + /// Verifies the various parts of a sample authentication exchange + /// (server challenge generated by MS Exchange Server 2003). + /// + [TestMethod] + [TestCategory("NTLM")] + public void VerifyAuthenticationExchange() { + SaslMechanism m = new SaslNtlm("TEST", "TEST"); + + byte[] initialResponse = m.GetResponse(new byte[0]); + Assert.IsTrue(initialResponse.SequenceEqual(expectedInitial)); + byte[] finalResponse = m.GetResponse(serverChallenge); + Assert.IsTrue(finalResponse.SequenceEqual(expectedFinal)); + } + + /// + /// Verifies the various parts of a sample authentication exchange + /// (server challenge generated by the dovecot IMAP server). + /// + [TestMethod] + [TestCategory("NTLM")] + public void VerifyAnotherAuthenticationExchange() { + SaslMechanism m = new SaslNtlm("test", "test"); + + byte[] initialResponse = m.GetResponse(new byte[0]); + Assert.IsTrue(initialResponse.SequenceEqual(expectedInitial)); + byte[] finalResponse = m.GetResponse(anotherServerChallenge); + Assert.IsTrue(finalResponse.SequenceEqual(anotherExpectedFinal)); + } + + #region NTLM Type 1 message + static byte[] type1Message = new byte[] { + 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x05, 0x32, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x0D, 0x00, 0x0D, 0x00, 0x30, 0x00, 0x00, 0x00, 0x06, + 0x01, 0xB1, 0x1D, 0x00, 0x00, 0x00, 0x0F, 0x6D, 0x79, 0x44, 0x6F, + 0x6D, 0x61, 0x69, 0x6E, 0x6D, 0x79, 0x57, 0x6F, 0x72, 0x6B, 0x73, + 0x74, 0x61, 0x74, 0x69, 0x6F, 0x6E + }; + #endregion + + #region NTLM Type 2 message (Version 2) + static byte[] type2MessageVersion2 = new byte[] { + 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 + }; + #endregion + + #region NTLM Type 2 message (Version 3) + static byte[] type2MessageVersion3 = new byte[] { + 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x12, 0x00, 0x12, 0x00, 0x38, 0x00, 0x00, 0x00, 0x05, 0x02, + 0x81, 0x02, 0xA6, 0xBC, 0xAF, 0x32, 0xA5, 0x51, 0x36, 0x65, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9E, 0x00, 0x9E, 0x00, + 0x4A, 0x00, 0x00, 0x00, 0x05, 0x02, 0xCE, 0x0E, 0x00, 0x00, 0x00, + 0x0F, 0x4C, 0x00, 0x4F, 0x00, 0x43, 0x00, 0x41, 0x00, 0x4C, 0x00, + 0x48, 0x00, 0x4F, 0x00, 0x53, 0x00, 0x54, 0x00, 0x02, 0x00, 0x12, + 0x00, 0x4C, 0x00, 0x4F, 0x00, 0x43, 0x00, 0x41, 0x00, 0x4C, 0x00, + 0x48, 0x00, 0x4F, 0x00, 0x53, 0x00, 0x54, 0x00, 0x01, 0x00, 0x1E, + 0x00, 0x56, 0x00, 0x4D, 0x00, 0x57, 0x00, 0x41, 0x00, 0x52, 0x00, + 0x45, 0x00, 0x2D, 0x00, 0x35, 0x00, 0x54, 0x00, 0x35, 0x00, 0x47, + 0x00, 0x43, 0x00, 0x39, 0x00, 0x50, 0x00, 0x55, 0x00, 0x04, 0x00, + 0x12, 0x00, 0x6C, 0x00, 0x6F, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6C, + 0x00, 0x68, 0x00, 0x6F, 0x00, 0x73, 0x00, 0x74, 0x00, 0x03, 0x00, + 0x32, 0x00, 0x76, 0x00, 0x6D, 0x00, 0x77, 0x00, 0x61, 0x00, 0x72, + 0x00, 0x65, 0x00, 0x2D, 0x00, 0x35, 0x00, 0x74, 0x00, 0x35, 0x00, + 0x67, 0x00, 0x63, 0x00, 0x39, 0x00, 0x70, 0x00, 0x75, 0x00, 0x2E, + 0x00, 0x6C, 0x00, 0x6F, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6C, 0x00, + 0x68, 0x00, 0x6F, 0x00, 0x73, 0x00, 0x74, 0x00, 0x05, 0x00, 0x12, + 0x00, 0x6C, 0x00, 0x6F, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6C, 0x00, + 0x68, 0x00, 0x6F, 0x00, 0x73, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, + 0x00 + }; + #endregion + + #region NTLM Type 3 message + static byte[] type3Message = new byte[] { + 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x18, 0x00, 0x48, 0x00, 0x00, 0x00, 0x18, 0x00, + 0x18, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x78, 0x00, 0x00, 0x00, + 0x1A, 0x00, 0x1A, 0x00, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, + 0xB1, 0x1D, 0x00, 0x00, 0x00, 0x0F, 0xF6, 0x0C, 0x93, 0x17, 0x97, + 0x1C, 0x44, 0x9A, 0xAF, 0xBF, 0xC6, 0xD9, 0x44, 0xC9, 0x06, 0x2E, + 0x47, 0x6F, 0xCD, 0x57, 0xBC, 0x42, 0xD2, 0xEC, 0xBC, 0x85, 0xC7, + 0x73, 0x00, 0xAA, 0x9F, 0xEB, 0x6A, 0xF3, 0x02, 0x6C, 0xF7, 0x91, + 0x8D, 0x15, 0xF3, 0xE2, 0xB3, 0x84, 0xDE, 0x46, 0xBE, 0xDB, 0x54, + 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x75, 0x00, 0x73, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x4D, 0x00, 0x79, 0x00, 0x57, 0x00, 0x6F, + 0x00, 0x72, 0x00, 0x6B, 0x00, 0x73, 0x00, 0x74, 0x00, 0x61, 0x00, + 0x74, 0x00, 0x69, 0x00, 0x6F, 0x00, 0x6E, 0x00 + }; + #endregion + + #region Authentication Exchange + static byte[] expectedInitial = new byte[] { + 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x05, 0x32, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x0B, 0x00, 0x0B, 0x00, 0x2E, 0x00, 0x00, 0x00, 0x06, + 0x01, 0xB1, 0x1D, 0x00, 0x00, 0x00, 0x0F, 0x64, 0x6F, 0x6D, 0x61, + 0x69, 0x6E, 0x77, 0x6F, 0x72, 0x6B, 0x73, 0x74, 0x61, 0x74, 0x69, + 0x6F, 0x6E + }; + + static byte[] serverChallenge = new byte[] { + 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x12, 0x00, 0x12, 0x00, 0x38, 0x00, 0x00, 0x00, 0x05, 0x02, + 0x81, 0x02, 0x82, 0x9F, 0x92, 0xC1, 0x22, 0x63, 0x99, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9E, 0x00, 0x9E, 0x00, + 0x4A, 0x00, 0x00, 0x00, 0x05, 0x02, 0xCE, 0x0E, 0x00, 0x00, 0x00, + 0x0F, 0x4C, 0x00, 0x4F, 0x00, 0x43, 0x00, 0x41, 0x00, 0x4C, 0x00, + 0x48, 0x00, 0x4F, 0x00, 0x53, 0x00, 0x54, 0x00, 0x02, 0x00, 0x12, + 0x00, 0x4C, 0x00, 0x4F, 0x00, 0x43, 0x00, 0x41, 0x00, 0x4C, 0x00, + 0x48, 0x00, 0x4F, 0x00, 0x53, 0x00, 0x54, 0x00, 0x01, 0x00, 0x1E, + 0x00, 0x56, 0x00, 0x4D, 0x00, 0x57, 0x00, 0x41, 0x00, 0x52, 0x00, + 0x45, 0x00, 0x2D, 0x00, 0x35, 0x00, 0x54, 0x00, 0x35, 0x00, 0x47, + 0x00, 0x43, 0x00, 0x39, 0x00, 0x50, 0x00, 0x55, 0x00, 0x04, 0x00, + 0x12, 0x00, 0x6C, 0x00, 0x6F, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6C, + 0x00, 0x68, 0x00, 0x6F, 0x00, 0x73, 0x00, 0x74, 0x00, 0x03, 0x00, + 0x32, 0x00, 0x76, 0x00, 0x6D, 0x00, 0x77, 0x00, 0x61, 0x00, 0x72, + 0x00, 0x65, 0x00, 0x2D, 0x00, 0x35, 0x00, 0x74, 0x00, 0x35, 0x00, + 0x67, 0x00, 0x63, 0x00, 0x39, 0x00, 0x70, 0x00, 0x75, 0x00, 0x2E, + 0x00, 0x6C, 0x00, 0x6F, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6C, 0x00, + 0x68, 0x00, 0x6F, 0x00, 0x73, 0x00, 0x74, 0x00, 0x05, 0x00, 0x12, + 0x00, 0x6C, 0x00, 0x6F, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6C, 0x00, + 0x68, 0x00, 0x6F, 0x00, 0x73, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, + 0x00 + }; + + static byte[] expectedFinal = new byte[] { + 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x18, 0x00, 0x48, 0x00, 0x00, 0x00, 0x18, 0x00, + 0x18, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x78, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x16, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x96, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, + 0xB1, 0x1D, 0x00, 0x00, 0x00, 0x0F, 0x16, 0xF6, 0xC6, 0x49, 0x65, + 0xD6, 0x73, 0x14, 0x50, 0xD1, 0x52, 0x56, 0x43, 0x94, 0x04, 0x8B, + 0x89, 0xCD, 0xEF, 0x41, 0x22, 0x75, 0x1A, 0x4E, 0x50, 0xEF, 0x89, + 0x1C, 0x1D, 0x8E, 0xAC, 0x10, 0xDD, 0xED, 0x7C, 0x35, 0xE5, 0x62, + 0xC8, 0x75, 0x75, 0x5E, 0x10, 0xA5, 0x43, 0x44, 0x26, 0x70, 0x54, + 0x00, 0x45, 0x00, 0x53, 0x00, 0x54, 0x00, 0x57, 0x00, 0x6F, 0x00, + 0x72, 0x00, 0x6B, 0x00, 0x73, 0x00, 0x74, 0x00, 0x61, 0x00, 0x74, + 0x00, 0x69, 0x00, 0x6F, 0x00, 0x6E, 0x00 + }; + #endregion + + #region Another Authentication Exchange + static byte[] anotherServerChallenge = new byte[] { + 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x05, 0x02, + 0x82, 0x00, 0x78, 0x35, 0x52, 0x30, 0xD2, 0xCA, 0xD9, 0xB8, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x14, 0x00, + 0x3C, 0x00, 0x00, 0x00, 0x64, 0x00, 0x65, 0x00, 0x62, 0x00, 0x69, + 0x00, 0x61, 0x00, 0x6E, 0x00, 0x03, 0x00, 0x0C, 0x00, 0x64, 0x00, + 0x65, 0x00, 0x62, 0x00, 0x69, 0x00, 0x61, 0x00, 0x6E, 0x00, 0x00, + 0x00, 0x00, 0x00 + }; + + static byte[] anotherExpectedFinal = new byte[] { + 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x18, 0x00, 0x48, 0x00, 0x00, 0x00, 0x18, 0x00, + 0x18, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x78, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x16, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x96, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, + 0xB1, 0x1D, 0x00, 0x00, 0x00, 0x0F, 0x4E, 0xE9, 0xDD, 0x7B, 0x84, + 0x62, 0x62, 0x67, 0x64, 0xD2, 0xD2, 0x11, 0xC3, 0xEF, 0xD3, 0xC1, + 0x32, 0x35, 0x15, 0xB8, 0x34, 0xAB, 0x95, 0xFD, 0x3D, 0xD2, 0xAA, + 0x04, 0xC2, 0x6D, 0x11, 0xEC, 0x3E, 0x22, 0xE4, 0x25, 0x73, 0x18, + 0xBB, 0x3D, 0x1E, 0x45, 0x52, 0xA1, 0x39, 0xB7, 0x66, 0x3B, 0x74, + 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x57, 0x00, 0x6F, 0x00, + 0x72, 0x00, 0x6B, 0x00, 0x73, 0x00, 0x74, 0x00, 0x61, 0x00, 0x74, + 0x00, 0x69, 0x00, 0x6F, 0x00, 0x6E, 0x00 + }; + #endregion + } +} \ No newline at end of file diff --git a/Tests/OAuth2Test.cs b/Tests/OAuth2Test.cs new file mode 100644 index 0000000..b273a9c --- /dev/null +++ b/Tests/OAuth2Test.cs @@ -0,0 +1,28 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using S22.Sasl.Mechanisms; +using System.Text; + +namespace S22.Sasl.Test { + /// + /// Contains unit tests for the SASL XOAUTH2 authentication mechanism. + /// + [TestClass] + public class OAuth2Test { + /// + /// Verifies the various parts of a sample authentication exchange + /// directly taken from Google's "XOAUTH2 Mechanism" document + /// ("Initial Client Response"). + /// + [TestMethod] + [TestCategory("OAuth2")] + public void VerifyAuthenticationExchange() { + SaslMechanism m = new SaslOAuth2("someuser@example.com", + "vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg=="); + string expectedResponse = "user=someuser@example.com\u0001" + + "auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\u0001\u0001"; + string initialResponse = Encoding.ASCII.GetString( + m.GetResponse(new byte[0])); + Assert.AreEqual(expectedResponse, initialResponse); + } + } +} diff --git a/Tests/PlainTest.cs b/Tests/PlainTest.cs new file mode 100644 index 0000000..5f32442 --- /dev/null +++ b/Tests/PlainTest.cs @@ -0,0 +1,26 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using S22.Sasl.Mechanisms; +using System.Text; + +namespace S22.Sasl.Test { + /// + /// Contains unit tests for the SASL PLAIN authentication mechanism. + /// + [TestClass] + public class PlainTest { + /// + /// Verifies the various parts of a sample authentication exchange + /// directly taken from RFC 4616 ("4. Examples", p. 5). + /// + [TestMethod] + [TestCategory("Plain authentication")] + public void VerfiyAuthenticationExchange() { + SaslMechanism m = new SaslPlain("tim", "tanstaaftanstaaf"); + + string expectedResponse = "\0tim\0tanstaaftanstaaf"; + string initialResponse = Encoding.ASCII.GetString( + m.GetResponse(new byte[0])); + Assert.AreEqual(expectedResponse, initialResponse); + } + } +} diff --git a/Tests/ScramSha1Test.cs b/Tests/ScramSha1Test.cs new file mode 100644 index 0000000..e1781d1 --- /dev/null +++ b/Tests/ScramSha1Test.cs @@ -0,0 +1,103 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using S22.Sasl.Mechanisms; +using System; +using System.Text; +using System.Text.RegularExpressions; + +namespace S22.Sasl.Test { + /// + /// Contains unit tests for the SASL SCRAM-SHA-1 authentication mechanism. + /// + [TestClass] + public class ScramSha1Test { + /// + /// Verifies the syntax of the client-first-message sent by the client to + /// initiate authentication. + /// + [TestMethod] + [TestCategory("Scram-Sha-1")] + public void VerifyClientFirstMessage() { + SaslMechanism m = new SaslScramSha1("Foo", "Bar"); + string clientInitial = Encoding.UTF8.GetString( + m.GetResponse(new byte[0])); + // Verify the syntax of the client-first-message. + bool valid = Regex.IsMatch(clientInitial, + "^[nyp],(a=[^,]+)?,(m=[^,]+,)?n=[^,]+,(r=[^,]+)(,.*)?"); + Assert.IsTrue(valid); + } + + /// + /// Sends the client an illegal nonce value and verifies the client + /// subsequently raises an exception. + /// + [TestMethod] + [TestCategory("Scram-Sha-1")] + [ExpectedException(typeof(SaslException))] + public void TamperedNonce() { + SaslMechanism m = new SaslScramSha1("Foo", "Bar"); + // Skip the initial client response. + m.GetResponse(new byte[0]); + // Hand the client a server-first-message containing a nonce which is + // missing the mandatory client-nonce part. + byte[] serverFirst = Encoding.UTF8.GetBytes("r=123456789,s=MTIzNDU2" + + "Nzg5,i=4096"); + // This should raise an exception. + m.GetResponse(serverFirst); + } + + /// + /// Verifies the various parts of a sample authentication exchange + /// directly taken from RFC 5802 ("SCRAM Authentication Exchange", p. 8). + /// + [TestMethod] + [TestCategory("Scram-Sha-1")] + public void VerifyAuthenticationExchange() { + string username = "user", password = "pencil", + cnonce = "fyko+d2lbbFgONRv9qkxdawL"; + SaslMechanism s = new SaslScramSha1(username, password, cnonce); + string initialResponse = Encoding.UTF8.GetString( + s.GetResponse(new byte[0])); + // Verify the syntax of the client-first-message. + Match m = Regex.Match(initialResponse, + "^[nyp],(a=[^,]+)?,(m=[^,]+,)?n=([^,]+),r=([^,]+)(,.*)?"); + Assert.IsTrue(m.Success); + Assert.AreEqual(username, m.Groups[3].ToString()); + Assert.AreEqual(cnonce, m.Groups[4].ToString()); + // Hand the client the server-first-message. + byte[] serverFirst = Encoding.UTF8.GetBytes( + "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92," + + "i=4096"); + string clientFinal = Encoding.UTF8.GetString( + s.GetResponse(serverFirst)); + string expectedClientFinal = "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfc" + + "NHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts="; + Assert.AreEqual(expectedClientFinal, clientFinal); + // Hand the client the server-last-message. + byte[] serverLast = Encoding.UTF8.GetBytes("v=rmF9pqV8S7suAoZWja4dJ" + + "RkFsKQ="); + clientFinal = Encoding.UTF8.GetString(s.GetResponse(serverLast)); + Assert.AreEqual(String.Empty, clientFinal); + } + + /// + /// Helper method for conveniently converting the specified string to + /// Base64 using a decoding of UTF-8. + /// + /// The string to base64-encode. + /// A base64-encoded string. + string ToBase64(string s) { + return Convert.ToBase64String(Encoding.UTF8.GetBytes(s)); + } + + /// + /// Helper method for conveniently decoding the specified base64-encoded + /// string using a decoding of UTF-8. + /// + /// The base64-encoded string to decode. + /// A string constructed from the base64-decoded sequence + /// of bytes. + string FromBase64(string s) { + return Encoding.UTF8.GetString(Convert.FromBase64String(s)); + } + } +} diff --git a/Tests/SrpTest.cs b/Tests/SrpTest.cs new file mode 100644 index 0000000..fb17c54 --- /dev/null +++ b/Tests/SrpTest.cs @@ -0,0 +1,449 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using S22.Sasl.Mechanisms; +using S22.Sasl.Mechanisms.Srp; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; + +namespace S22.Sasl.Test { + /// + /// Contains unit tests for the SASL SRP authentication mechanism. + /// + [TestClass] + public class SrpTest { + /// + /// Serializes an instance of the ClientMessage1 class and verifies the + /// serialized byte sequence is identical to the pre-computed expected + /// byte sequence. + /// + [TestMethod] + [TestCategory("Srp")] + public void SerializeClientFirstMessage() { + byte[] expected = new byte[] { + 0x00, 0x00, 0x00, 0x11, 0x00, 0x04, 0x74, 0x65, + 0x73, 0x74, 0x00, 0x06, 0x61, 0x75, 0x74, 0x68, + 0x49, 0x64, 0x00, 0x00, 0x00 + }; + ClientMessage1 m = new ClientMessage1("test", "authId"); + + Assert.IsTrue(m.Serialize().SequenceEqual(expected)); + } + + /// + /// Serializes an instance of the ClientMessage2 class and verifies the + /// serialized byte sequence is identical to the pre-computed expected + /// byte sequence. + /// + [TestMethod] + [TestCategory("Srp")] + public void SerializeClientSecondMessage() { + BigInteger key = BigInteger.Parse(clientPublicKey, + NumberStyles.HexNumber); + Mpi _publicKey = new Mpi(key); + + ClientMessage2 m = new ClientMessage2(_publicKey, clientProof); + m.InitialVector = clientInitialVector; + foreach (KeyValuePair p in clientOptions) + m.Options.Add(p.Key, p.Value); + + byte[] serialized = m.Serialize(); + Assert.IsTrue(serialized.SequenceEqual(expectedClientMessage2)); + + } + + /// + /// Deserializes a byte sequence into an instance of the ServerMessage1 + /// class and verifies the instance fields contain the expected values. + /// + [TestMethod] + [TestCategory("Srp")] + public void DeserializeServerFirstMessage() { + ServerMessage1 m = ServerMessage1.Deserialize(serverMessage1); + BigInteger expectedGenerator = new BigInteger(2), + expectedModulus = BigInteger.Parse(expectedSafePrimeModulus, + NumberStyles.HexNumber), + _expectedPublicKey = BigInteger.Parse(expectedPublicKey, + NumberStyles.HexNumber); + Assert.AreEqual(expectedGenerator, m.Generator.Value); + Assert.AreEqual(expectedModulus, m.SafePrimeModulus.Value); + Assert.AreEqual(_expectedPublicKey, m.PublicKey.Value); + Assert.IsTrue(m.Salt.SequenceEqual(expectedSalt)); + Assert.AreEqual(expectedOptions, m.RawOptions); + + Assert.AreEqual(expectedParsedOptions.Count, m.Options.Count); + foreach (KeyValuePair pair in expectedParsedOptions) + Assert.AreEqual(pair.Value, m.Options[pair.Key]); + } + + /// + /// Deserializes a byte sequence into an instance of the ServerMessage2 + /// class and verifies the instance fields contain the expected values. + /// + [TestMethod] + [TestCategory("Srp")] + public void DeserializeServerSecondMessage() { + ServerMessage2 m = ServerMessage2.Deserialize(serverMessage2); + + Assert.IsTrue(m.Proof.SequenceEqual(expectedServerProof)); + Assert.IsTrue(m.InitialVector.SequenceEqual(expectedInitialVector)); + Assert.AreEqual(String.Empty, m.SessionId); + Assert.AreEqual(0, m.Ttl); + } + + /// + /// Verifies the various parts of a sample authentication exchange + /// (Challenge generated by the Cyrus Sasl library). + /// + /// The exchange was generated with the authorization id + /// (authId) set to the same value as the username. + [TestMethod] + [TestCategory("Srp")] + public void VerifyAuthenticationExchange() { + byte[] privateKey = new byte[] { + 0xAB, 0x1A, 0x11, 0x07, 0xDF, 0x5D, 0x91, 0xC5, + 0xD6, 0x21, 0x47, 0x06, 0x41, 0xD7, 0x04, 0x63 + }; + SaslMechanism m = new SaslSrp("test@debian", "test", privateKey); + // Ensure the expected client initial-response is generated. + byte[] clientResponse = m.GetResponse(new byte[0]); + Assert.IsTrue(clientResponse.SequenceEqual(expectedClientFirst)); + + // Hand the server-challenge to the client and verify the expected + // client-response is generated. + clientResponse = m.GetResponse(serverFirst); + Assert.IsTrue(clientResponse.SequenceEqual(expectedClientSecond)); + + // Finally, hand the server-evidence to the client and verify the client + // responds with the empty string which concludes authentication. + clientResponse = m.GetResponse(serverSecond); + Assert.AreEqual(0, clientResponse.Length); + } + + #region Server Message 1 + static byte[] serverMessage1 = new byte[] { + 0x00, 0x00, 0x02, 0xF9, 0x00, 0x01, 0x00, 0xAC, 0x6B, 0xDB, 0x41, + 0x32, 0x4A, 0x9A, 0x9B, 0xF1, 0x66, 0xDE, 0x5E, 0x13, 0x89, 0x58, + 0x2F, 0xAF, 0x72, 0xB6, 0x65, 0x19, 0x87, 0xEE, 0x07, 0xFC, 0x31, + 0x92, 0x94, 0x3D, 0xB5, 0x60, 0x50, 0xA3, 0x73, 0x29, 0xCB, 0xB4, + 0xA0, 0x99, 0xED, 0x81, 0x93, 0xE0, 0x75, 0x77, 0x67, 0xA1, 0x3D, + 0xD5, 0x23, 0x12, 0xAB, 0x4B, 0x03, 0x31, 0x0D, 0xCD, 0x7F, 0x48, + 0xA9, 0xDA, 0x04, 0xFD, 0x50, 0xE8, 0x08, 0x39, 0x69, 0xED, 0xB7, + 0x67, 0xB0, 0xCF, 0x60, 0x95, 0x17, 0x9A, 0x16, 0x3A, 0xB3, 0x66, + 0x1A, 0x05, 0xFB, 0xD5, 0xFA, 0xAA, 0xE8, 0x29, 0x18, 0xA9, 0x96, + 0x2F, 0x0B, 0x93, 0xB8, 0x55, 0xF9, 0x79, 0x93, 0xEC, 0x97, 0x5E, + 0xEA, 0xA8, 0x0D, 0x74, 0x0A, 0xDB, 0xF4, 0xFF, 0x74, 0x73, 0x59, + 0xD0, 0x41, 0xD5, 0xC3, 0x3E, 0xA7, 0x1D, 0x28, 0x1E, 0x44, 0x6B, + 0x14, 0x77, 0x3B, 0xCA, 0x97, 0xB4, 0x3A, 0x23, 0xFB, 0x80, 0x16, + 0x76, 0xBD, 0x20, 0x7A, 0x43, 0x6C, 0x64, 0x81, 0xF1, 0xD2, 0xB9, + 0x07, 0x87, 0x17, 0x46, 0x1A, 0x5B, 0x9D, 0x32, 0xE6, 0x88, 0xF8, + 0x77, 0x48, 0x54, 0x45, 0x23, 0xB5, 0x24, 0xB0, 0xD5, 0x7D, 0x5E, + 0xA7, 0x7A, 0x27, 0x75, 0xD2, 0xEC, 0xFA, 0x03, 0x2C, 0xFB, 0xDB, + 0xF5, 0x2F, 0xB3, 0x78, 0x61, 0x60, 0x27, 0x90, 0x04, 0xE5, 0x7A, + 0xE6, 0xAF, 0x87, 0x4E, 0x73, 0x03, 0xCE, 0x53, 0x29, 0x9C, 0xCC, + 0x04, 0x1C, 0x7B, 0xC3, 0x08, 0xD8, 0x2A, 0x56, 0x98, 0xF3, 0xA8, + 0xD0, 0xC3, 0x82, 0x71, 0xAE, 0x35, 0xF8, 0xE9, 0xDB, 0xFB, 0xB6, + 0x94, 0xB5, 0xC8, 0x03, 0xD8, 0x9F, 0x7A, 0xE4, 0x35, 0xDE, 0x23, + 0x6D, 0x52, 0x5F, 0x54, 0x75, 0x9B, 0x65, 0xE3, 0x72, 0xFC, 0xD6, + 0x8E, 0xF2, 0x0F, 0xA7, 0x11, 0x1F, 0x9E, 0x4A, 0xFF, 0x73, 0x00, + 0x01, 0x02, 0x10, 0x0E, 0xC3, 0x6A, 0x9E, 0xA3, 0x39, 0x7C, 0xE8, + 0x2D, 0x0E, 0xAC, 0x18, 0xA7, 0xD4, 0xCD, 0x16, 0x01, 0x00, 0x9B, + 0x49, 0x67, 0xB7, 0xA0, 0x7C, 0x12, 0xDB, 0x49, 0x21, 0x63, 0xC8, + 0x20, 0x4F, 0xF2, 0xBE, 0x5A, 0x49, 0xA8, 0xC9, 0x3E, 0xE8, 0x08, + 0xE5, 0x04, 0x38, 0x0A, 0x26, 0x55, 0x1E, 0x50, 0x61, 0xE2, 0x45, + 0x81, 0xBA, 0x68, 0x9B, 0x6F, 0x87, 0x61, 0x14, 0xCA, 0x73, 0x27, + 0xB4, 0x0F, 0xBD, 0x79, 0xD7, 0xD5, 0x4D, 0x3C, 0xB8, 0xAD, 0x60, + 0x25, 0x80, 0x32, 0xFD, 0xD6, 0x0F, 0xA9, 0x2D, 0x44, 0xC0, 0x82, + 0xCB, 0xE5, 0x1C, 0x83, 0xFE, 0x21, 0x3B, 0x71, 0x42, 0x44, 0x74, + 0xB7, 0xFA, 0xB2, 0xB9, 0x0E, 0xB5, 0x6C, 0x54, 0x97, 0xFA, 0x11, + 0x0D, 0xD7, 0x7C, 0x72, 0x2F, 0x65, 0x47, 0x07, 0x95, 0x06, 0x05, + 0x27, 0x2E, 0xEE, 0x74, 0xDE, 0x3E, 0xD9, 0xC9, 0xE5, 0x32, 0x85, + 0xE4, 0xA1, 0x41, 0xD0, 0xEB, 0x1F, 0x07, 0xBE, 0xD4, 0x9F, 0x58, + 0x11, 0x3B, 0x9D, 0xC2, 0x9B, 0x0B, 0xF8, 0x7E, 0x92, 0xD3, 0xF2, + 0x31, 0xC5, 0xE3, 0x47, 0x10, 0x11, 0xDE, 0xA6, 0x82, 0x61, 0x46, + 0xBE, 0x84, 0x67, 0xA8, 0x7C, 0x9E, 0xED, 0xD5, 0x67, 0x73, 0x61, + 0xCA, 0x04, 0xD7, 0x0F, 0x25, 0x0D, 0xD7, 0x78, 0xC1, 0x36, 0xEE, + 0xB9, 0x1D, 0x97, 0x54, 0xEC, 0x14, 0xFF, 0xB0, 0xDE, 0x65, 0xF6, + 0x74, 0xDE, 0x1C, 0xF9, 0x90, 0x59, 0xAE, 0x62, 0x23, 0x52, 0xFA, + 0x6F, 0x1D, 0x03, 0x28, 0x6F, 0xB5, 0x60, 0x0E, 0x0C, 0xA0, 0x7F, + 0x19, 0x5C, 0xB2, 0x11, 0x5A, 0x67, 0xA5, 0xD9, 0x7B, 0x37, 0xEE, + 0x74, 0xB6, 0x58, 0x8B, 0xC1, 0x33, 0x6D, 0x2A, 0x24, 0x16, 0xEF, + 0x93, 0x60, 0x80, 0x49, 0xD1, 0x56, 0x36, 0x41, 0x46, 0x44, 0x02, + 0x49, 0xA8, 0xE2, 0xF9, 0x93, 0x7F, 0xB8, 0x33, 0xB0, 0x8E, 0x41, + 0x82, 0x96, 0x63, 0x8C, 0x11, 0x75, 0x57, 0xE6, 0xA2, 0xF5, 0xCB, + 0xCB, 0xA0, 0x00, 0xDE, 0x6D, 0x64, 0x61, 0x3D, 0x53, 0x48, 0x41, + 0x2D, 0x31, 0x2C, 0x72, 0x65, 0x70, 0x6C, 0x61, 0x79, 0x5F, 0x64, + 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2C, 0x69, 0x6E, + 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x3D, 0x48, 0x4D, 0x41, + 0x43, 0x2D, 0x53, 0x48, 0x41, 0x2D, 0x31, 0x2C, 0x69, 0x6E, 0x74, + 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x3D, 0x48, 0x4D, 0x41, 0x43, + 0x2D, 0x52, 0x49, 0x50, 0x45, 0x4D, 0x44, 0x2D, 0x31, 0x36, 0x30, + 0x2C, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x3D, + 0x48, 0x4D, 0x41, 0x43, 0x2D, 0x4D, 0x44, 0x35, 0x2C, 0x63, 0x6F, + 0x6E, 0x66, 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x69, + 0x74, 0x79, 0x3D, 0x44, 0x45, 0x53, 0x2C, 0x63, 0x6F, 0x6E, 0x66, + 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x69, 0x74, 0x79, + 0x3D, 0x33, 0x44, 0x45, 0x53, 0x2C, 0x63, 0x6F, 0x6E, 0x66, 0x69, + 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x69, 0x74, 0x79, 0x3D, + 0x41, 0x45, 0x53, 0x2C, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x64, 0x65, + 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x69, 0x74, 0x79, 0x3D, 0x42, 0x6C, + 0x6F, 0x77, 0x66, 0x69, 0x73, 0x68, 0x2C, 0x63, 0x6F, 0x6E, 0x66, + 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x69, 0x74, 0x79, + 0x3D, 0x43, 0x41, 0x53, 0x54, 0x2D, 0x31, 0x32, 0x38, 0x2C, 0x6D, + 0x61, 0x78, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x73, 0x69, 0x7A, + 0x65, 0x3D, 0x32, 0x30, 0x34, 0x38 + }; + #endregion + + #region Server Message 2 + static byte[] serverMessage2 = new byte[] { + 0x00, 0x00, 0x00, 0x2C, 0x14, 0xEF, 0xC0, 0x2A, 0xD0, 0x1F, 0xCB, + 0x35, 0x8C, 0x0F, 0xC9, 0xF7, 0x2A, 0x35, 0xE5, 0x92, 0xDC, 0x15, + 0x7A, 0x00, 0x6D, 0x10, 0x8C, 0x6E, 0x44, 0x75, 0xD6, 0xF0, 0x95, + 0x4B, 0xD5, 0xBF, 0x89, 0xA1, 0xDD, 0x52, 0x4D, 0x97, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + }; + #endregion + + #region Client Message 2 + static byte[] clientProof = new byte[] { + 0xA5, 0x84, 0xC6, 0x97, 0x07, 0x46, 0xFE, 0x80, 0xE5, 0x2D, 0xBB, + 0x03, 0xF2, 0x3E, 0xC8, 0x10, 0xAE, 0xC1, 0xE3, 0x88 + }; + + static byte[] clientInitialVector = new byte[] { + 0x02, 0x3A, 0x90, 0x6C, 0x28, 0xEE, 0xB8, 0x37, 0x9F, 0xC1, 0x15, + 0x62, 0xAE, 0x60, 0x41, 0xBF + }; + + static string clientPublicKey = "08A1FE2384AFB6214C1428692B564E11D" + + "07BE4F242E458A54EB96A19366CDF531F8B52A3B7E942B09B4C44A3477E4769" + + "CB900FC1862D4C29913EC9464B31D50EB07111152E4B503F2EB180628EE0036" + + "DB8FC97EAE16B450FEA3B49C60F5AD59C25D2EED1C1DF9026782F513445279A" + + "FB8B63E7C89AAE1A17AD1BF5E1A53ACACDDD0005AD8CF745B59969A29A22FF5" + + "40C151D3361F636D624B267DE80310B9FE49BC1DE9981084C2830084026D4D2" + + "EC5932C5F817FF87CB911DD3FA05710966E484D6A75C502E4BB9854478C6F97" + + "B7EE77999F5C2E5138B8F289F4A2DCA3FA9CEB045B2DEDB05E768A3AA416CF9" + + "14B7F96B7F2C6AF00C750D60F754554EA171972"; + + static Dictionary clientOptions = + new Dictionary() { + { "mda", "SHA-1" }, + { "replay_detection", "true" }, + { "integrity", "HMAC-SHA-1" }, + { "confidentiality", "AES" }, + { "maxbuffersize", "2048" } + }; + + static byte[] expectedClientMessage2 = new byte[] { + 0x00, 0x00, 0x01, 0x80, 0x01, 0x00, 0x8A, 0x1F, 0xE2, 0x38, 0x4A, + 0xFB, 0x62, 0x14, 0xC1, 0x42, 0x86, 0x92, 0xB5, 0x64, 0xE1, 0x1D, + 0x07, 0xBE, 0x4F, 0x24, 0x2E, 0x45, 0x8A, 0x54, 0xEB, 0x96, 0xA1, + 0x93, 0x66, 0xCD, 0xF5, 0x31, 0xF8, 0xB5, 0x2A, 0x3B, 0x7E, 0x94, + 0x2B, 0x09, 0xB4, 0xC4, 0x4A, 0x34, 0x77, 0xE4, 0x76, 0x9C, 0xB9, + 0x00, 0xFC, 0x18, 0x62, 0xD4, 0xC2, 0x99, 0x13, 0xEC, 0x94, 0x64, + 0xB3, 0x1D, 0x50, 0xEB, 0x07, 0x11, 0x11, 0x52, 0xE4, 0xB5, 0x03, + 0xF2, 0xEB, 0x18, 0x06, 0x28, 0xEE, 0x00, 0x36, 0xDB, 0x8F, 0xC9, + 0x7E, 0xAE, 0x16, 0xB4, 0x50, 0xFE, 0xA3, 0xB4, 0x9C, 0x60, 0xF5, + 0xAD, 0x59, 0xC2, 0x5D, 0x2E, 0xED, 0x1C, 0x1D, 0xF9, 0x02, 0x67, + 0x82, 0xF5, 0x13, 0x44, 0x52, 0x79, 0xAF, 0xB8, 0xB6, 0x3E, 0x7C, + 0x89, 0xAA, 0xE1, 0xA1, 0x7A, 0xD1, 0xBF, 0x5E, 0x1A, 0x53, 0xAC, + 0xAC, 0xDD, 0xD0, 0x00, 0x5A, 0xD8, 0xCF, 0x74, 0x5B, 0x59, 0x96, + 0x9A, 0x29, 0xA2, 0x2F, 0xF5, 0x40, 0xC1, 0x51, 0xD3, 0x36, 0x1F, + 0x63, 0x6D, 0x62, 0x4B, 0x26, 0x7D, 0xE8, 0x03, 0x10, 0xB9, 0xFE, + 0x49, 0xBC, 0x1D, 0xE9, 0x98, 0x10, 0x84, 0xC2, 0x83, 0x00, 0x84, + 0x02, 0x6D, 0x4D, 0x2E, 0xC5, 0x93, 0x2C, 0x5F, 0x81, 0x7F, 0xF8, + 0x7C, 0xB9, 0x11, 0xDD, 0x3F, 0xA0, 0x57, 0x10, 0x96, 0x6E, 0x48, + 0x4D, 0x6A, 0x75, 0xC5, 0x02, 0xE4, 0xBB, 0x98, 0x54, 0x47, 0x8C, + 0x6F, 0x97, 0xB7, 0xEE, 0x77, 0x99, 0x9F, 0x5C, 0x2E, 0x51, 0x38, + 0xB8, 0xF2, 0x89, 0xF4, 0xA2, 0xDC, 0xA3, 0xFA, 0x9C, 0xEB, 0x04, + 0x5B, 0x2D, 0xED, 0xB0, 0x5E, 0x76, 0x8A, 0x3A, 0xA4, 0x16, 0xCF, + 0x91, 0x4B, 0x7F, 0x96, 0xB7, 0xF2, 0xC6, 0xAF, 0x00, 0xC7, 0x50, + 0xD6, 0x0F, 0x75, 0x45, 0x54, 0xEA, 0x17, 0x19, 0x72, 0x14, 0xA5, + 0x84, 0xC6, 0x97, 0x07, 0x46, 0xFE, 0x80, 0xE5, 0x2D, 0xBB, 0x03, + 0xF2, 0x3E, 0xC8, 0x10, 0xAE, 0xC1, 0xE3, 0x88, 0x00, 0x56, 0x6D, + 0x64, 0x61, 0x3D, 0x53, 0x48, 0x41, 0x2D, 0x31, 0x2C, 0x72, 0x65, + 0x70, 0x6C, 0x61, 0x79, 0x5F, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, + 0x69, 0x6F, 0x6E, 0x2C, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x72, 0x69, + 0x74, 0x79, 0x3D, 0x48, 0x4D, 0x41, 0x43, 0x2D, 0x53, 0x48, 0x41, + 0x2D, 0x31, 0x2C, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x64, 0x65, 0x6E, + 0x74, 0x69, 0x61, 0x6C, 0x69, 0x74, 0x79, 0x3D, 0x41, 0x45, 0x53, + 0x2C, 0x6D, 0x61, 0x78, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x73, + 0x69, 0x7A, 0x65, 0x3D, 0x32, 0x30, 0x34, 0x38, 0x10, 0x02, 0x3A, + 0x90, 0x6C, 0x28, 0xEE, 0xB8, 0x37, 0x9F, 0xC1, 0x15, 0x62, 0xAE, + 0x60, 0x41, 0xBF + }; + #endregion + + #region Expected values + static string expectedSafePrimeModulus = + "0AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37" + + "329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E808396" + + "9EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC9" + + "75EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801" + + "676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA" + + "77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041" + + "C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525" + + "F54759B65E372FCD68EF20FA7111F9E4AFF73"; + + static string expectedPublicKey = + "09B4967B7A07C12DB492163C8204FF2BE5A49A8C93EE808E504380A26551E5061E24" + + "581BA689B6F876114CA7327B40FBD79D7D54D3CB8AD60258032FDD60FA92D44C082C" + + "BE51C83FE213B71424474B7FAB2B90EB56C5497FA110DD77C722F654707950605272" + + "EEE74DE3ED9C9E53285E4A141D0EB1F07BED49F58113B9DC29B0BF87E92D3F231C5E" + + "3471011DEA6826146BE8467A87C9EEDD5677361CA04D70F250DD778C136EEB91D975" + + "4EC14FFB0DE65F674DE1CF99059AE622352FA6F1D03286FB5600E0CA07F195CB2115" + + "A67A5D97B37EE74B6588BC1336D2A2416EF93608049D156364146440249A8E2F9937" + + "FB833B08E418296638C117557E6A2F5CBCBA0"; + + static byte[] expectedSalt = new byte[] { + 0x0E, 0xC3, 0x6A, 0x9E, 0xA3, 0x39, 0x7C, 0xE8, 0x2D, 0x0E, 0xAC, 0x18, + 0xA7, 0xD4, 0xCD, 0x16 + }; + + static string expectedOptions = "mda=SHA-1,replay_detection,integrity=H" + + "MAC-SHA-1,integrity=HMAC-RIPEMD-160,integrity=HMAC-MD5,confidentiali" + + "ty=DES,confidentiality=3DES,confidentiality=AES,confidentiality=Blow" + + "fish,confidentiality=CAST-128,maxbuffersize=2048"; + + static Dictionary expectedParsedOptions = + new Dictionary() { + { "mda", "SHA-1" }, + { "replay_detection", "true" }, + { "integrity", "HMAC-SHA-1,HMAC-RIPEMD-160,HMAC-MD5" }, + { "confidentiality", "DES,3DES,AES,Blowfish,CAST-128" }, + { "maxbuffersize", "2048" } + }; + + static byte[] expectedServerProof = new byte[] { + 0xEF, 0xC0, 0x2A, 0xD0, 0x1F, 0xCB, 0x35, 0x8C, 0x0F, 0xC9, 0xF7, 0x2A, + 0x35, 0xE5, 0x92, 0xDC, 0x15, 0x7A, 0x00, 0x6D + }; + + static byte[] expectedInitialVector = new byte[] { + 0x8C, 0x6E, 0x44, 0x75, 0xD6, 0xF0, 0x95, 0x4B, 0xD5, 0xBF, 0x89, 0xA1, + 0xDD, 0x52, 0x4D, 0x97 + }; + #endregion + + #region Authentication Exchange + static byte[] serverFirst = new byte[] { + 0x00, 0x00, 0x02, 0xF9, 0x00, 0x01, 0x00, 0xAC, 0x6B, 0xDB, 0x41, + 0x32, 0x4A, 0x9A, 0x9B, 0xF1, 0x66, 0xDE, 0x5E, 0x13, 0x89, 0x58, + 0x2F, 0xAF, 0x72, 0xB6, 0x65, 0x19, 0x87, 0xEE, 0x07, 0xFC, 0x31, + 0x92, 0x94, 0x3D, 0xB5, 0x60, 0x50, 0xA3, 0x73, 0x29, 0xCB, 0xB4, + 0xA0, 0x99, 0xED, 0x81, 0x93, 0xE0, 0x75, 0x77, 0x67, 0xA1, 0x3D, + 0xD5, 0x23, 0x12, 0xAB, 0x4B, 0x03, 0x31, 0x0D, 0xCD, 0x7F, 0x48, + 0xA9, 0xDA, 0x04, 0xFD, 0x50, 0xE8, 0x08, 0x39, 0x69, 0xED, 0xB7, + 0x67, 0xB0, 0xCF, 0x60, 0x95, 0x17, 0x9A, 0x16, 0x3A, 0xB3, 0x66, + 0x1A, 0x05, 0xFB, 0xD5, 0xFA, 0xAA, 0xE8, 0x29, 0x18, 0xA9, 0x96, + 0x2F, 0x0B, 0x93, 0xB8, 0x55, 0xF9, 0x79, 0x93, 0xEC, 0x97, 0x5E, + 0xEA, 0xA8, 0x0D, 0x74, 0x0A, 0xDB, 0xF4, 0xFF, 0x74, 0x73, 0x59, + 0xD0, 0x41, 0xD5, 0xC3, 0x3E, 0xA7, 0x1D, 0x28, 0x1E, 0x44, 0x6B, + 0x14, 0x77, 0x3B, 0xCA, 0x97, 0xB4, 0x3A, 0x23, 0xFB, 0x80, 0x16, + 0x76, 0xBD, 0x20, 0x7A, 0x43, 0x6C, 0x64, 0x81, 0xF1, 0xD2, 0xB9, + 0x07, 0x87, 0x17, 0x46, 0x1A, 0x5B, 0x9D, 0x32, 0xE6, 0x88, 0xF8, + 0x77, 0x48, 0x54, 0x45, 0x23, 0xB5, 0x24, 0xB0, 0xD5, 0x7D, 0x5E, + 0xA7, 0x7A, 0x27, 0x75, 0xD2, 0xEC, 0xFA, 0x03, 0x2C, 0xFB, 0xDB, + 0xF5, 0x2F, 0xB3, 0x78, 0x61, 0x60, 0x27, 0x90, 0x04, 0xE5, 0x7A, + 0xE6, 0xAF, 0x87, 0x4E, 0x73, 0x03, 0xCE, 0x53, 0x29, 0x9C, 0xCC, + 0x04, 0x1C, 0x7B, 0xC3, 0x08, 0xD8, 0x2A, 0x56, 0x98, 0xF3, 0xA8, + 0xD0, 0xC3, 0x82, 0x71, 0xAE, 0x35, 0xF8, 0xE9, 0xDB, 0xFB, 0xB6, + 0x94, 0xB5, 0xC8, 0x03, 0xD8, 0x9F, 0x7A, 0xE4, 0x35, 0xDE, 0x23, + 0x6D, 0x52, 0x5F, 0x54, 0x75, 0x9B, 0x65, 0xE3, 0x72, 0xFC, 0xD6, + 0x8E, 0xF2, 0x0F, 0xA7, 0x11, 0x1F, 0x9E, 0x4A, 0xFF, 0x73, 0x00, + 0x01, 0x02, 0x10, 0x5A, 0x32, 0xE8, 0xDD, 0x4A, 0x5C, 0x5E, 0x77, + 0x08, 0x20, 0xF9, 0xC7, 0x00, 0xA6, 0xB6, 0xCD, 0x01, 0x00, 0x29, + 0x2B, 0x33, 0x8B, 0xE2, 0xD0, 0xF0, 0xBA, 0x4E, 0xED, 0x64, 0x69, + 0x4A, 0xDA, 0x31, 0xB2, 0xBD, 0x8A, 0x6F, 0x26, 0x4C, 0xD7, 0xC1, + 0x59, 0xA5, 0xBD, 0xA9, 0xB2, 0x20, 0x71, 0xE4, 0x93, 0xC9, 0x3B, + 0x5F, 0xA5, 0x08, 0x13, 0xF4, 0x1E, 0xEF, 0x98, 0x26, 0xED, 0x65, + 0xAD, 0xC9, 0xA5, 0x57, 0x78, 0x65, 0x22, 0x6C, 0x2E, 0x66, 0x02, + 0xDC, 0x35, 0x7A, 0xC0, 0x28, 0x0F, 0xAF, 0x23, 0x7D, 0xDD, 0x4B, + 0xB4, 0x8E, 0x6F, 0xDD, 0xFD, 0xAA, 0xDE, 0x23, 0xAC, 0xF0, 0xCB, + 0xCC, 0x83, 0xDC, 0xFC, 0x1B, 0xF0, 0x0B, 0x10, 0x12, 0x06, 0x86, + 0x29, 0xAC, 0xEF, 0x7F, 0x15, 0xB4, 0xF4, 0x85, 0x22, 0x6B, 0x01, + 0xD7, 0x1F, 0xC1, 0x16, 0x3C, 0x73, 0xCC, 0x5D, 0x8B, 0xCC, 0x22, + 0x6C, 0x92, 0x5A, 0x1A, 0x5D, 0x11, 0x6E, 0xD5, 0x83, 0xFC, 0xD1, + 0xC1, 0x5E, 0x0E, 0xAD, 0x3F, 0x16, 0x50, 0xE3, 0x6A, 0x44, 0x70, + 0x04, 0x29, 0x9A, 0x23, 0x61, 0xC5, 0x2A, 0x3C, 0x3A, 0x26, 0x01, + 0xF9, 0x64, 0x01, 0x77, 0x38, 0xF6, 0x0B, 0x33, 0x0C, 0x33, 0x8F, + 0x29, 0x57, 0x6F, 0xFE, 0x3D, 0x6D, 0xE7, 0x52, 0x59, 0x11, 0xE5, + 0x2B, 0xDD, 0x37, 0x68, 0x1F, 0x57, 0x42, 0xCC, 0x10, 0xAC, 0x9D, + 0x23, 0x2A, 0x21, 0xB9, 0x68, 0xBA, 0x98, 0xDC, 0xBD, 0xDD, 0x1A, + 0x99, 0xE5, 0x4C, 0x5B, 0x99, 0xC9, 0xCA, 0xFE, 0xB9, 0x1E, 0x94, + 0xD3, 0x13, 0x30, 0xC1, 0xEF, 0xA1, 0xDB, 0xF6, 0x4F, 0x77, 0x6A, + 0xA1, 0x98, 0x9B, 0xAC, 0xAF, 0x9F, 0xDB, 0xEC, 0x06, 0xB7, 0xC2, + 0x13, 0x46, 0xD3, 0x79, 0x73, 0xA4, 0x21, 0x6B, 0x8F, 0x49, 0xEC, + 0xE4, 0xF6, 0x2C, 0xC5, 0xA8, 0xBC, 0x46, 0x94, 0x87, 0x77, 0x21, + 0x76, 0xD9, 0x1A, 0xD4, 0x95, 0x92, 0x64, 0x54, 0xE4, 0xC8, 0x3F, + 0x92, 0xBF, 0x00, 0xDE, 0x6D, 0x64, 0x61, 0x3D, 0x53, 0x48, 0x41, + 0x2D, 0x31, 0x2C, 0x72, 0x65, 0x70, 0x6C, 0x61, 0x79, 0x5F, 0x64, + 0x65, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2C, 0x69, 0x6E, + 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x3D, 0x48, 0x4D, 0x41, + 0x43, 0x2D, 0x53, 0x48, 0x41, 0x2D, 0x31, 0x2C, 0x69, 0x6E, 0x74, + 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x3D, 0x48, 0x4D, 0x41, 0x43, + 0x2D, 0x52, 0x49, 0x50, 0x45, 0x4D, 0x44, 0x2D, 0x31, 0x36, 0x30, + 0x2C, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x3D, + 0x48, 0x4D, 0x41, 0x43, 0x2D, 0x4D, 0x44, 0x35, 0x2C, 0x63, 0x6F, + 0x6E, 0x66, 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x69, + 0x74, 0x79, 0x3D, 0x44, 0x45, 0x53, 0x2C, 0x63, 0x6F, 0x6E, 0x66, + 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x69, 0x74, 0x79, + 0x3D, 0x33, 0x44, 0x45, 0x53, 0x2C, 0x63, 0x6F, 0x6E, 0x66, 0x69, + 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x69, 0x74, 0x79, 0x3D, + 0x41, 0x45, 0x53, 0x2C, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x64, 0x65, + 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x69, 0x74, 0x79, 0x3D, 0x42, 0x6C, + 0x6F, 0x77, 0x66, 0x69, 0x73, 0x68, 0x2C, 0x63, 0x6F, 0x6E, 0x66, + 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x69, 0x74, 0x79, + 0x3D, 0x43, 0x41, 0x53, 0x54, 0x2D, 0x31, 0x32, 0x38, 0x2C, 0x6D, + 0x61, 0x78, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x73, 0x69, 0x7A, + 0x65, 0x3D, 0x32, 0x30, 0x34, 0x38 + }; + + static byte[] expectedClientFirst = new byte[] { + 0x00, 0x00, 0x00, 0x1D, 0x00, 0x0B, 0x74, 0x65, 0x73, 0x74, 0x40, + 0x64, 0x65, 0x62, 0x69, 0x61, 0x6E, 0x00, 0x0B, 0x74, 0x65, 0x73, + 0x74, 0x40, 0x64, 0x65, 0x62, 0x69, 0x61, 0x6E, 0x00, 0x00, 0x00 + }; + + static byte[] expectedClientSecond = new byte[] { + 0x00, 0x00, 0x01, 0x23, 0x01, 0x00, 0x1A, 0x2B, 0x50, 0xE8, 0x91, + 0xB7, 0xE4, 0x6C, 0x6D, 0x30, 0x4F, 0x8D, 0x20, 0x85, 0xA0, 0xB5, + 0xD6, 0x1F, 0xD6, 0x40, 0xAF, 0xEF, 0x78, 0xDE, 0xDA, 0xC6, 0x6A, + 0x90, 0xB2, 0xD8, 0xBC, 0x56, 0x6B, 0xE6, 0xD4, 0x07, 0xBF, 0x8B, + 0xD5, 0x8C, 0xDD, 0xE5, 0xA4, 0xC9, 0xF3, 0xAA, 0x25, 0xDC, 0x4F, + 0x4A, 0x99, 0x9D, 0x17, 0x6E, 0xDF, 0xC4, 0x23, 0x8C, 0x48, 0x4C, + 0x66, 0xC5, 0x66, 0x94, 0x36, 0xF2, 0x3C, 0xF7, 0xC2, 0x51, 0x2B, + 0xD6, 0xA7, 0x2C, 0xD9, 0x2B, 0xC8, 0x16, 0xA9, 0xDE, 0x9E, 0x3D, + 0xFB, 0xA4, 0xAA, 0x8F, 0x43, 0x5F, 0x90, 0xAF, 0x4B, 0xA9, 0xE3, + 0x39, 0x63, 0xA4, 0x4F, 0x50, 0x27, 0x63, 0x3B, 0x37, 0x6D, 0x3F, + 0xEB, 0xE1, 0x92, 0xCA, 0x78, 0xE4, 0x59, 0xD3, 0x8C, 0xD3, 0xFC, + 0xCA, 0x62, 0xC9, 0x0C, 0x28, 0xD1, 0x83, 0x44, 0x78, 0x89, 0xF8, + 0x48, 0xAA, 0xCD, 0x51, 0x17, 0x71, 0x31, 0x53, 0x28, 0xD6, 0x44, + 0x56, 0x23, 0xDB, 0x99, 0x90, 0x4B, 0xA9, 0xFD, 0x7D, 0xB0, 0x80, + 0xB7, 0xFC, 0x28, 0x88, 0x31, 0x9C, 0x1D, 0x2F, 0xD0, 0xCF, 0xA9, + 0x3E, 0x92, 0x4E, 0x95, 0xDC, 0xAD, 0x12, 0xB6, 0xB4, 0x51, 0x53, + 0x3E, 0xF5, 0x8D, 0xD1, 0x8B, 0xD2, 0x4C, 0x16, 0x79, 0x46, 0x13, + 0x2F, 0x25, 0x80, 0x96, 0x53, 0x0E, 0x08, 0xEA, 0x8D, 0xC3, 0x58, + 0xB7, 0x7C, 0xDC, 0x62, 0x1D, 0x37, 0xD4, 0x90, 0x35, 0xD4, 0x5E, + 0x8B, 0x16, 0xBE, 0x2B, 0xB7, 0xD8, 0x5B, 0xD9, 0x0C, 0xDC, 0x6B, + 0x46, 0x46, 0xFD, 0x15, 0x3F, 0x17, 0x90, 0xC4, 0xAB, 0x92, 0x5B, + 0x00, 0xE9, 0xB8, 0x97, 0x10, 0xEF, 0xF4, 0x35, 0x32, 0xAC, 0x01, + 0xDB, 0x81, 0x33, 0xA5, 0x64, 0x79, 0xDE, 0x45, 0x93, 0x38, 0xC0, + 0x19, 0x5B, 0x82, 0x47, 0xBD, 0xDC, 0x52, 0x80, 0xC1, 0x14, 0xA8, + 0xDC, 0x11, 0x00, 0xED, 0x94, 0xA9, 0x0F, 0xC5, 0x2A, 0x15, 0xC2, + 0x01, 0x6F, 0xA7, 0xB7, 0xBF, 0x74, 0x7E, 0x43, 0x00, 0x09, 0x6D, + 0x64, 0x61, 0x3D, 0x53, 0x48, 0x41, 0x2D, 0x31, 0x00 + }; + + static byte[] serverSecond = new byte[] { + 0x00, 0x00, 0x00, 0x2C, 0x14, 0xC7, 0x40, 0x3C, 0x3A, 0xB3, 0x5D, + 0xB4, 0xB4, 0xD4, 0x28, 0x99, 0xC2, 0x0A, 0x0E, 0x04, 0xD2, 0x7C, + 0xF2, 0x87, 0x98, 0x10, 0x0E, 0x46, 0x0B, 0x63, 0x0E, 0x80, 0xE6, + 0x6A, 0xDF, 0xD4, 0xCF, 0xA0, 0x88, 0x1A, 0xFC, 0x67, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + }; + #endregion + } +}