Initial Commit

This commit is contained in:
smiley22 2014-01-06 09:27:48 +01:00
commit 235d86668c
49 changed files with 6174 additions and 0 deletions

22
.gitattributes vendored Normal file
View File

@ -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

167
.gitignore vendored Normal file
View File

@ -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

55
Extensions.cs Normal file
View File

@ -0,0 +1,55 @@
using System;
namespace S22.Sasl {
internal static class Extensions {
/// <summary>
/// Adds a couple of useful extensions to reference types.
/// </summary>
/// <summary>
/// Throws an ArgumentNullException if the given data item is null.
/// </summary>
/// <param name="data">The item to check for nullity.</param>
/// <param name="name">The name to use when throwing an
/// exception, if necessary</param>
public static void ThrowIfNull<T>(this T data, string name)
where T : class {
if (data == null)
throw new ArgumentNullException(name);
}
/// <summary>
/// Throws an ArgumentNullException if the given data item is null.
/// </summary>
/// <param name="data">The item to check for nullity.</param>
public static void ThrowIfNull<T>(this T data)
where T : class {
if (data == null)
throw new ArgumentNullException();
}
/// <summary>
/// Throws an ArgumentException if the given string is null or
/// empty.
/// </summary>
/// <param name="data">The string to check for nullity and
/// emptiness.</param>
public static void ThrowIfNullOrEmpty(this string data) {
if (String.IsNullOrEmpty(data))
throw new ArgumentException();
}
/// <summary>
/// Throws an ArgumentException if the given string is null or
/// empty.
/// </summary>
/// <param name="data">The string to check for nullity and
/// emptiness.</param>
/// <param name="name">The name to use when throwing an
/// exception, if necessary</param>
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");
}
}
}

22
License.md Normal file
View File

@ -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.

195
Mechanisms/ByteBuilder.cs Normal file
View File

@ -0,0 +1,195 @@
using System;
using System.Text;
namespace S22.Sasl {
/// <summary>
/// A utility class modeled after the BCL StringBuilder to simplify
/// building binary-data messages.
/// </summary>
internal class ByteBuilder {
/// <summary>
/// The actual byte buffer.
/// </summary>
byte[] buffer = new byte[1024];
/// <summary>
/// The current position in the buffer.
/// </summary>
int position = 0;
/// <summary>
/// The length of the underlying data buffer.
/// </summary>
public int Length {
get {
return position;
}
}
/// <summary>
/// Resizes the internal byte buffer.
/// </summary>
/// <param name="amount">Amount in bytes by which to increase the
/// size of the buffer.</param>
void Resize(int amount = 1024) {
byte[] newBuffer = new byte[buffer.Length + amount];
Array.Copy(buffer, newBuffer, buffer.Length);
buffer = newBuffer;
}
/// <summary>
/// Appends one or several byte values to this instance.
/// </summary>
/// <param name="values">Byte values to append.</param>
/// <returns>A reference to the calling instance.</returns>
public ByteBuilder Append(params byte[] values) {
if ((position + values.Length) >= buffer.Length)
Resize();
foreach (byte b in values)
buffer[position++] = b;
return this;
}
/// <summary>
/// Appends the specified number of bytes from the specified buffer
/// starting at the specified offset to this instance.
/// </summary>
/// <param name="buffer">The buffer to append bytes from.</param>
/// <param name="offset">The offset into the buffert at which to start
/// reading bytes from.</param>
/// <param name="count">The number of bytes to read from the buffer.</param>
/// <returns>A reference to the calling instance.</returns>
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;
}
/// <summary>
/// Appends the specified 32-bit integer value to this instance.
/// </summary>
/// <param name="value">A 32-bit integer value to append.</param>
/// <param name="bigEndian">Set this to true, to append the value as
/// big-endian.</param>
/// <returns>A reference to the calling instance.</returns>
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;
}
/// <summary>
/// Appends the specified 16-bit short value to this instance.
/// </summary>
/// <param name="value">A 16-bit short value to append.</param>
/// <param name="bigEndian">Set this to true, to append the value as
/// big-endian.</param>
/// <returns>A reference to the calling instance.</returns>
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;
}
/// <summary>
/// Appends the specified 16-bit unsigend short value to this instance.
/// </summary>
/// <param name="value">A 16-bit unsigend short value to append.</param>
/// <param name="bigEndian">Set this to true, to append the value as
/// big-endian.</param>
/// <returns>A reference to the calling instance.</returns>
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;
}
/// <summary>
/// Appends the specified 32-bit unsigned integer value to this instance.
/// </summary>
/// <param name="value">A 32-bit unsigned integer value to append.</param>
/// <param name="bigEndian">Set this to true, to append the value as
/// big-endian.</param>
/// <returns>A reference to the calling instance.</returns>
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;
}
/// <summary>
/// Appends the specified 64-bit integer value to this instance.
/// </summary>
/// <param name="value">A 64-bit integer value to append.</param>
/// <param name="bigEndian">Set this to true, to append the value as
/// big-endian.</param>
/// <returns>A reference to the calling instance.</returns>
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;
}
/// <summary>
/// Appends the specified string using the specified encoding to this
/// instance.
/// </summary>
/// <param name="value">The string vale to append.</param>
/// <param name="encoding">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.</param>
/// <returns>A reference to the calling instance.</returns>
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;
}
/// <summary>
/// Returns the ByteBuilder's content as an array of bytes.
/// </summary>
/// <returns>An array of bytes.</returns>
public byte[] ToArray() {
// Fixme: Do this properly.
byte[] b = new byte[position];
Array.Copy(buffer, b, position);
return b;
}
/// <summary>
/// Removes all bytes from the current ByteBuilder instance.
/// </summary>
public void Clear() {
buffer = new byte[1024];
position = 0;
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.IO;
using System.Text;
namespace S22.Sasl.Mechanisms.Ntlm {
/// <summary>
/// Adds extension methods to the BinaryReader class to simplify the
/// deserialization of NTLM messages.
/// </summary>
internal static class BinaryReaderExtensions {
/// <summary>
/// Reads an ASCII-string of the specified length from this instance.
/// </summary>
/// <param name="reader">Extension method for the BinaryReader class.</param>
/// <param name="count">The number of bytes to read from the underlying
/// stream.</param>
/// <returns>A string decoded from the bytes read from the underlying
/// stream using the ASCII character set.</returns>
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');
}
}
}

139
Mechanisms/Ntlm/Flags.cs Normal file
View File

@ -0,0 +1,139 @@
using System;
namespace S22.Sasl.Mechanisms.Ntlm {
/// <summary>
/// The NTLM flags which are contained in a bitfield within the header of
/// an NTLM message.
/// </summary>
[Flags]
internal enum Flags {
/// <summary>
/// Indicates that Unicode strings are supported for use in security
/// buffer data.
/// </summary>
NegotiateUnicode = 0x00000001,
/// <summary>
/// Indicates that OEM strings are supported for use in security
/// buffer data.
/// </summary>
NegotiateOEM = 0x00000002,
/// <summary>
/// Requests that the server's authentication realm be included in
/// the Type 2 message.
/// </summary>
RequestTarget = 0x00000004,
/// <summary>
/// Specifies that authenticated communication between the client and
/// server should carry a digital signature (message integrity).
/// </summary>
NegotiateSign = 0x00000010,
/// <summary>
/// Specifies that authenticated communication between the client and
/// server should be encrypted (message confidentiality).
/// </summary>
NegotiateSeal = 0x00000020,
/// <summary>
/// Indicates that datagram authentication is being used.
/// </summary>
NegotiateDatagramStyle = 0x00000040,
/// <summary>
/// Indicates that the Lan Manager Session Key should be used for signing
/// and sealing authenticated communications.
/// </summary>
NegotiateLanManagerKey = 0x00000080,
/// <summary>
/// This flag's usage has not been identified.
/// </summary>
NegotiateNetware = 0x00000100,
/// <summary>
/// Indicates that NTLM authentication is being used.
/// </summary>
NegotiateNTLM = 0x00000200,
/// <summary>
/// Sent by the client in the Type 3 message to indicate that an anonymous
/// context has been established. This also affects the response fields.
/// </summary>
NegotiateAnonymous = 0x00000800,
/// <summary>
/// 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.
/// </summary>
NegotiateDomainSupplied = 0x00001000,
/// <summary>
/// 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.
/// </summary>
NegotiateWorkstationSupplied = 0x00002000,
/// <summary>
/// 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.
/// </summary>
NegotiateLocalCall = 0x00004000,
/// <summary>
/// Indicates that authenticated communication between the client and
/// server should be signed with a "dummy" signature.
/// </summary>
NegotiateAlwaysSign = 0x00008000,
/// <summary>
/// Sent by the server in the Type 2 message to indicate that the target
/// authentication realm is a domain.
/// </summary>
TargetTypeDomain = 0x00010000,
/// <summary>
/// Sent by the server in the Type 2 message to indicate that the target
/// authentication realm is a server.
/// </summary>
TargetTypeServer = 0x00020000,
/// <summary>
/// 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.
/// </summary>
TargetTypeShare = 0x00040000,
/// <summary>
/// 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.
/// </summary>
NegotiateNTLM2Key = 0x00080000,
/// <summary>
/// This flag's usage has not been identified.
/// </summary>
RequestInitResponse = 0x00100000,
/// <summary>
/// This flag's usage has not been identified.
/// </summary>
RequestAcceptResponse = 0x00200000,
/// <summary>
/// This flag's usage has not been identified.
/// </summary>
RequestNonNTSessionKey = 0x00400000,
/// <summary>
/// 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.
/// </summary>
NegotiateTargetInfo = 0x00800000,
/// <summary>
/// Indicates that 128-bit encryption is supported.
/// </summary>
Negotiate128 = 0x20000000,
/// <summary>
/// Indicates that the client will provide an encrypted master key in the
/// "Session Key" field of the Type 3 message.
/// </summary>
NegotiateKeyExchange = 0x40000000,
/// <summary>
/// Indicates that 56-bit encryption is supported.
/// </summary>
Negotiate56
}
}

View File

@ -0,0 +1,91 @@

namespace S22.Sasl.Mechanisms.Ntlm {
/// <summary>
/// Represents the data contained in the target information block of an
/// NTLM type 2 message.
/// </summary>
internal class Type2TargetInformation {
/// <summary>
/// The server name.
/// </summary>
public string ServerName {
get;
set;
}
/// <summary>
/// The domain name.
/// </summary>
public string DomainName {
get;
set;
}
/// <summary>
/// The fully-qualified DNS host name.
/// </summary>
public string DnsHostname {
get;
set;
}
/// <summary>
/// The fully-qualified DNS domain name.
/// </summary>
public string DnsDomainName {
get;
set;
}
}
/// <summary>
/// Describes the different versions of the Type 2 message that have
/// been observed.
/// </summary>
internal enum Type2Version {
/// <summary>
/// The version is unknown.
/// </summary>
Unknown = 0,
/// <summary>
/// This form is seen in older Win9x-based systems.
/// </summary>
Version1 = 32,
/// <summary>
/// This form is seen in most out-of-box shipping versions of Windows.
/// </summary>
Version2 = 48,
/// <summary>
/// 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.
/// </summary>
Version3 = 56,
}
/// <summary>
/// Indicates the type of data in Type 2 target information blocks.
/// </summary>
internal enum Type2InformationType {
/// <summary>
/// Signals the end of the target information block.
/// </summary>
TerminatorBlock = 0,
/// <summary>
/// The data in the information block contains the server name.
/// </summary>
ServerName = 1,
/// <summary>
/// The data in the information block contains the domain name.
/// </summary>
DomainName = 2,
/// <summary>
/// The data in the information block contains the DNS hostname.
/// </summary>
DnsHostname = 3,
/// <summary>
/// The data in the information block contans the DNS domain name.
/// </summary>
DnsDomainName = 4
}
}

146
Mechanisms/Ntlm/MD4.cs Normal file
View File

@ -0,0 +1,146 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
namespace S22.Sasl.Mechanisms.Ntlm {
/// <summary>
/// Computes the MD4 hash value for the input data.
/// Courtesy of Keith Wood.
/// </summary>
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<byte> 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<byte> Bytes(byte[] bytes, int offset, int length) {
for (int i = offset; i < length; i++) {
yield return bytes[i];
}
}
private IEnumerable<byte> 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<byte> Repeat(byte value, int count) {
for (int i = 0; i < count; i++) {
yield return value;
}
}
private IEnumerable<byte> 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);
}
}
}
}

View File

@ -0,0 +1,23 @@

namespace S22.Sasl.Mechanisms.Ntlm {
/// <summary>
/// Describes the different types of NTLM messages.
/// </summary>
internal enum MessageType {
/// <summary>
/// An NTLM type 1 message is the initial client response to the
/// server.
/// </summary>
Type1 = 0x01,
/// <summary>
/// An NTLM type 2 message is the challenge sent by the server in
/// response to an NTLM type 1 message.
/// </summary>
Type2 = 0x02,
/// <summary>
/// An NTLM type 3 message is the challenge response sent by the client
/// in response to an NTLM type 2 message.
/// </summary>
Type3 = 0x03
}
}

View File

@ -0,0 +1,67 @@

namespace S22.Sasl.Mechanisms.Ntlm {
/// <summary>
/// Indicates the version and build number of the operating system.
/// </summary>
internal class OSVersion {
/// <summary>
/// The major version number of the operating system.
/// </summary>
public byte MajorVersion {
get;
set;
}
/// <summary>
/// The minor version number of the operating system.
/// </summary>
public byte MinorVersion {
get;
set;
}
/// <summary>
/// The build number of the operating system.
/// </summary>
public short BuildNumber {
get;
set;
}
/// <summary>
/// Default constructor.
/// </summary>
public OSVersion() {
}
/// <summary>
/// Creates a new instance of the OSVersion class using the specified
/// values.
/// </summary>
/// <param name="majorVersion">The major version of the operating
/// system.</param>
/// <param name="minorVersion">The minor version of the operating
/// system.</param>
/// <param name="buildNumber">The build number of the operating systen.</param>
public OSVersion(byte majorVersion, byte minorVersion, short buildNumber) {
MajorVersion = majorVersion;
MinorVersion = minorVersion;
BuildNumber = buildNumber;
}
/// <summary>
/// Serializes this instance of the OSVersion class to an array of
/// bytes.
/// </summary>
/// <returns>An array of bytes representing this instance of the OSVersion
/// class.</returns>
public byte[] Serialize() {
return new ByteBuilder()
.Append(MajorVersion)
.Append(MinorVersion)
.Append(BuildNumber)
.Append(0, 0, 0, 0x0F)
.ToArray();
}
}
}

View File

@ -0,0 +1,295 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace S22.Sasl.Mechanisms.Ntlm {
/// <summary>
/// Contains methods for calculating the various Type 3 challenge
/// responses.
/// </summary>
internal static class Responses {
/// <summary>
/// Computes the LM-response to the challenge sent as part of an
/// NTLM type 2 message.
/// </summary>
/// <param name="challenge">The challenge sent by the server.</param>
/// <param name="password">The user account password.</param>
/// <returns>An array of bytes representing the response to the
/// specified challenge.</returns>
internal static byte[] ComputeLMResponse(byte[] challenge,
string password) {
byte[] lmHash = LMHash(password);
return LMResponse(lmHash, challenge);
}
/// <summary>
/// Computes the NTLM-response to the challenge sent as part of an
/// NTLM type 2 message.
/// </summary>
/// <param name="challenge">The challenge sent by the server.</param>
/// <param name="password">The user account password.</param>
/// <returns>An array of bytes representing the response to the
/// specified challenge.</returns>
internal static byte[] ComputeNtlmResponse(byte[] challenge,
string password) {
byte[] ntlmHash = NtlmHash(password);
return LMResponse(ntlmHash, challenge);
}
/// <summary>
/// Computes the NTLMv2-response to the challenge sent as part of an
/// NTLM type 2 message.
/// </summary>
/// <param name="target">The name of the authentication target.</param>
/// <param name="username">The user account name to authenticate with.</param>
/// <param name="password">The user account password.</param>
/// <param name="targetInformation">The target information block from
/// the NTLM type 2 message.</param>
/// <param name="challenge">The challenge sent by the server.</param>
/// <param name="clientNonce">A random 8-byte client nonce.</param>
/// <returns>An array of bytes representing the response to the
/// specified challenge.</returns>
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);
}
/// <summary>
/// Computes the LMv2-response to the challenge sent as part of an
/// NTLM type 2 message.
/// </summary>
/// <param name="target">The name of the authentication target.</param>
/// <param name="username">The user account to authenticate with.</param>
/// <param name="password">The user account password.</param>
/// <param name="challenge">The challenge sent by the server.</param>
/// <param name="clientNonce">A random 8-byte client nonce.</param>
/// <returns>An array of bytes representing the response to the
/// specified challenge.</returns>
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);
}
/// <summary>
/// Creates the LM Hash of the specified password.
/// </summary>
/// <param name="password">The password to create the LM Hash of.</param>
/// <returns>The LM Hash of the given password, used in the calculation
/// of the LM Response.</returns>
/// <exception cref="ArgumentNullException">Thrown if the password argument
/// is null.</exception>
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;
}
}
/// <summary>
/// Creates a DES encryption key from the specified key material.
/// </summary>
/// <param name="bytes">The key material to create the DES encryption
/// key from.</param>
/// <param name="offset">An offset into the byte array at which to
/// extract the key material from.</param>
/// <returns>A 56-bit DES encryption key as an array of bytes.</returns>
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);
}
/// <summary>
/// Applies odd parity to the specified byte array.
/// </summary>
/// <param name="bytes">The byte array to apply odd parity to.</param>
/// <returns>A reference to the byte array.</returns>
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;
}
/// <summary>
/// Creates the LM Response from the specified hash and Type 2 challenge.
/// </summary>
/// <param name="hash">An LM or NTLM hash.</param>
/// <param name="challenge">The server challenge from the Type 2
/// message.</param>
/// <returns>The challenge response as an array of bytes.</returns>
/// <exception cref="ArgumentNullException">Thrown if the hash or the
/// challenge parameter is null.</exception>
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;
}
}
/// <summary>
/// Creates the NTLM Hash of the specified password.
/// </summary>
/// <param name="password">The password to create the NTLM hash of.</param>
/// <returns>The NTLM hash for the specified password.</returns>
/// <exception cref="ArgumentNullException">Thrown if the password
/// parameter is null.</exception>
private static byte[] NtlmHash(String password) {
password.ThrowIfNull("password");
byte[] data = Encoding.Unicode.GetBytes(password);
using (MD4 md4 = new MD4()) {
return md4.ComputeHash(data);
}
}
/// <summary>
/// Creates the NTLMv2 Hash of the specified target, username
/// and password values.
/// </summary>
/// <param name="target">The name of the authentication target as is
/// specified in the target name field of the NTLM type 3 message.</param>
/// <param name="username">The user account name.</param>
/// <param name="password">The password for the user account.</param>
/// <returns>The NTLMv2 hash for the specified input values.</returns>
/// <exception cref="ArgumentNullException">Thrown if the username or
/// the password parameter is null.</exception>
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));
}
/// <summary>
/// Returns the current time as the number of tenths of a microsecond
/// since January 1, 1601.
/// </summary>
/// <returns>The current time as the number of tenths of a microsecond
/// since January 1, 1601.</returns>
private static long GetTimestamp() {
return DateTime.Now.ToFileTimeUtc();
}
/// <summary>
/// Creates the "blob" data block which is part of the NTLMv2 challenge
/// response.
/// </summary>
/// <param name="targetInformation">The target information block from
/// the NTLM type 2 message.</param>
/// <param name="clientNonce">A random 8-byte client nonce.</param>
/// <returns>The blob, used in the calculation of the NTLMv2 Response.</returns>
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();
}
/// <summary>
/// Creates the LMv2 Response from the given NTLMv2 hash, client data, and
/// Type 2 challenge.
/// </summary>
/// <param name="hash">The NTLMv2 Hash.</param>
/// <param name="clientData">The client data (blob or client nonce).</param>
/// <param name="challenge">The server challenge from the Type 2 message.</param>
/// <returns>The response which is either for NTLMv2 or LMv2, depending
/// on the client data.</returns>
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();
}
}
}
}

View File

@ -0,0 +1,78 @@
using System;
namespace S22.Sasl.Mechanisms.Ntlm {
/// <summary>
/// Represents an NTLM security buffer, which is a structure used to point
/// to a buffer of binary data within an NTLM message.
/// </summary>
internal class SecurityBuffer {
/// <summary>
/// The length of the buffer content in bytes (may be zero).
/// </summary>
public short Length {
get;
private set;
}
/// <summary>
/// The allocated space for the buffer in bytes (typically the same as
/// the length).
/// </summary>
public short AllocatedSpace {
get {
return Length;
}
}
/// <summary>
/// The offset from the beginning of the NTLM message to the start of
/// the buffer, in bytes.
/// </summary>
public int Offset {
get;
private set;
}
/// <summary>
/// Creates a new instance of the SecurityBuffer class using the specified
/// values.
/// </summary>
/// <param name="length">The length of the buffer described by this instance
/// of the SecurityBuffer class.</param>
/// <param name="offset">The offset at which the buffer starts, in bytes.</param>
/// <exception cref="OverflowException">Thrown if the length value exceeds
/// the maximum value allowed. The security buffer structure stores the
/// length value as a 2-byte short value.</exception>
public SecurityBuffer(int length, int offset) {
Length = Convert.ToInt16(length);
Offset = offset;
}
/// <summary>
/// Creates a new instance of the SecurityBuffer class using the specified
/// values.
/// </summary>
/// <param name="data">The data of the buffer described by this instance
/// of the SecurityBuffer class.</param>
/// <param name="offset">The offset at which the buffer starts, in bytes.</param>
/// <exception cref="OverflowException">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.</exception>
public SecurityBuffer(byte[] data, int offset)
: this(data.Length, offset) {
}
/// <summary>
/// Serializes this instance of the SecurityBuffer into an array of bytes.
/// </summary>
/// <returns>A byte array representing this instance of the SecurityBuffer
/// class.</returns>
public byte[] Serialize() {
return new ByteBuilder()
.Append(Length)
.Append(AllocatedSpace)
.Append(Offset)
.ToArray();
}
}
}

View File

@ -0,0 +1,136 @@
using System;
using System.Text;
namespace S22.Sasl.Mechanisms.Ntlm {
/// <summary>
/// Represents an NTLM Type 1 Message.
/// </summary>
internal class Type1Message {
/// <summary>
/// The NTLM message signature which is always "NTLMSSP".
/// </summary>
static readonly string signature = "NTLMSSP";
/// <summary>
/// The NTML message type which is always 1 for an NTLM Type 1 message.
/// </summary>
static readonly MessageType type = MessageType.Type1;
/// <summary>
/// The NTLM flags set on this instance.
/// </summary>
internal Flags Flags {
get;
set;
}
/// <summary>
/// The supplied domain name as an array of bytes in the ASCII
/// range.
/// </summary>
byte[] domain {
get;
set;
}
/// <summary>
/// The offset within the message where the domain name data starts.
/// </summary>
int domainOffset {
get {
// We send a version 3 NTLM type 1 message.
return 40;
}
}
/// <summary>
/// The supplied workstation name as an array of bytes in the
/// ASCII range.
/// </summary>
byte[] workstation {
get;
set;
}
/// <summary>
/// The offset within the message where the workstation name data starts.
/// </summary>
int workstationOffset {
get {
return domainOffset + domain.Length;
}
}
/// <summary>
/// The length of the supplied workstation name as a 16-bit short value.
/// </summary>
short workstationLength {
get {
return Convert.ToInt16(workstation.Length);
}
}
/// <summary>
/// Contains information about the client's OS version.
/// </summary>
OSVersion OSVersion {
get;
set;
}
/// <summary>
/// Creates a new instance of the Type1Message class using the specified
/// domain and workstation names.
/// </summary>
/// <param name="domain">The domain in which the client's workstation has
/// membership.</param>
/// <param name="workstation">The client's workstation name.</param>
/// <exception cref="ArgumentNullException">Thrown if the domain or the
/// workstation parameter is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the domain
/// or the workstation name exceeds the maximum allowed string
/// length.</exception>
/// <remarks>The domain as well as the workstation name is restricted
/// to ASCII characters and must not be longer than 65536 characters.
/// </remarks>
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);
}
/// <summary>
/// Serializes this instance of the Type1 class to an array of bytes.
/// </summary>
/// <returns>An array of bytes representing this instance of the Type1
/// class.</returns>
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();
}
}
}

View File

@ -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 {
/// <summary>
/// Represents an NTLM Type 2 Message.
/// </summary>
internal class Type2Message {
/// <summary>
/// The NTLM message signature which is always "NTLMSSP".
/// </summary>
static readonly string signature = "NTLMSSP";
/// <summary>
/// The NTML message type which is always 2 for an NTLM Type 2 message.
/// </summary>
static readonly MessageType type = MessageType.Type2;
/// <summary>
/// The challenge is an 8-byte block of random data.
/// </summary>
public byte[] Challenge {
get;
private set;
}
/// <summary>
/// The target name of the authentication target.
/// </summary>
public string TargetName {
get;
private set;
}
/// <summary>
/// The NTLM flags set on this message.
/// </summary>
public Flags Flags {
get;
private set;
}
/// <summary>
/// The SSPI context handle when a local call is being made,
/// otherwise null.
/// </summary>
public Int64 Context {
get;
private set;
}
/// <summary>
/// Contains the data present in the OS version structure.
/// </summary>
public OSVersion OSVersion {
get;
private set;
}
/// <summary>
/// The version of this Type 2 message instance.
/// </summary>
public Type2Version Version {
get;
private set;
}
/// <summary>
/// Contains the data present in the target information block.
/// </summary>
public Type2TargetInformation TargetInformation {
get;
private set;
}
/// <summary>
/// Contains the raw data present in the target information block.
/// </summary>
public byte[] RawTargetInformation {
get;
private set;
}
/// <summary>
/// Private constructor.
/// </summary>
private Type2Message() {
TargetInformation = new Type2TargetInformation();
OSVersion = new OSVersion();
}
/// <summary>
/// Deserializes a Type 2 message instance from the specified buffer
/// of bytes.
/// </summary>
/// <param name="buffer">The buffer containing a sequence of bytes
/// representing an NTLM Type 2 message.</param>
/// <returns>An initialized instance of the Type2 class.</returns>
/// <exception cref="SerializationException">Thrown if an error occurs
/// during deserialization of the Type 2 message.</exception>
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);
}
}
/// <summary>
/// Determines the version of an NTLM type 2 message.
/// </summary>
/// <param name="targetOffset">The target offset field of the NTLM
/// type 2 message.</param>
/// <returns>A value from the Type2Version enumeration.</returns>
static Type2Version GetType2Version(int targetOffset) {
var dict = new Dictionary<int, Type2Version>() {
{ 32, Type2Version.Version1 },
{ 48, Type2Version.Version2 },
{ 56, Type2Version.Version3 }
};
return dict.ContainsKey(targetOffset) ? dict[targetOffset] :
Type2Version.Unknown;
}
/// <summary>
/// Reads the OS information data present in version 3 of an NTLM
/// type 2 message from the specified BinaryReader.
/// </summary>
/// <param name="r">The BinaryReader instance to read from.</param>
/// <returns>An initialized instance of the OSVersion class.</returns>
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;
}
/// <summary>
/// Reads the target information data present in version 2 and 3 of
/// an NTLM type 2 message from the specified BinaryReader.
/// </summary>
/// <param name="r">The BinaryReader instance to read from.</param>
/// <returns>An initialized instance of the Type2TargetInformation
/// class.</returns>
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;
}
/// <summary>
/// Retrieves the target name from the specified byte array.
/// </summary>
/// <param name="data">A byte array containing the target name.</param>
/// <param name="isUnicode">If true the target name will be decoded
/// using UTF-16 unicode encoding.</param>
/// <returns></returns>
static string GetTargetName(byte[] data, bool isUnicode) {
Encoding enc = isUnicode ? Encoding.Unicode : Encoding.ASCII;
return enc.GetString(data);
}
}
}

View File

@ -0,0 +1,268 @@
using System;
using System.Text;
namespace S22.Sasl.Mechanisms.Ntlm {
/// <summary>
/// Represents an NTLM Type 3 Message.
/// </summary>
internal class Type3Message {
/// <summary>
/// The NTLM message signature which is always "NTLMSSP".
/// </summary>
static readonly string signature = "NTLMSSP";
/// <summary>
/// The NTML message type which is always 3 for an NTLM Type 3 message.
/// </summary>
static readonly MessageType type = MessageType.Type3;
/// <summary>
/// The NTLM flags set on this instance.
/// </summary>
public Flags Flags {
get;
set;
}
/// <summary>
/// The "Lan Manager" challenge response.
/// </summary>
byte[] LMResponse {
get;
set;
}
/// <summary>
/// The offset at which the LM challenge response data starts.
/// </summary>
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;
}
}
/// <summary>
/// The NTLM challenge response.
/// </summary>
byte[] NtlmResponse {
get;
set;
}
/// <summary>
/// The offset at which the NTLM challenge response data starts.
/// </summary>
int NtlmOffset {
get {
return LMOffset + LMResponse.Length;
}
}
/// <summary>
/// The authentication realm in which the authenticating account
/// has membership.
/// </summary>
byte[] targetName {
get;
set;
}
/// <summary>
/// The offset at which the target name data starts.
/// </summary>
int targetOffset {
get {
return NtlmOffset + NtlmResponse.Length;
}
}
/// <summary>
/// The authenticating account name.
/// </summary>
byte[] username {
get;
set;
}
/// <summary>
/// The offset at which the username data starts.
/// </summary>
int usernameOffset {
get {
return targetOffset + targetName.Length;
}
}
/// <summary>
/// The client workstation's name.
/// </summary>
byte[] workstation {
get;
set;
}
/// <summary>
/// The offset at which the client workstation's name data starts.
/// </summary>
int workstationOffset {
get {
return usernameOffset + username.Length;
}
}
/// <summary>
/// The session key value which is used by the session security mechanism
/// during key exchange.
/// </summary>
byte[] sessionKey {
get;
set;
}
/// <summary>
/// The offset at which the session key data starts.
/// </summary>
int sessionKeyOffset {
get {
return workstationOffset + workstation.Length;
}
}
/// <summary>
/// Contains the data present in the OS version structure.
/// </summary>
OSVersion OSVersion {
get;
set;
}
/// <summary>
/// The encoding used for transmitting the contents of the various
/// security buffers.
/// </summary>
Encoding encoding {
get;
set;
}
/// <summary>
/// Creates a new instance of an NTLM type 3 message using the specified
/// values.
/// </summary>
/// <param name="username">The Windows account name to use for
/// authentication.</param>
/// <param name="password">The Windows account password to use for
/// authentication.</param>
/// <param name="challenge">The challenge received from the server as part
/// of the NTLM type 2 message.</param>
/// <param name="workstation">The client's workstation name.</param>
/// <param name="ntlmv2">Set to true to send an NTLMv2 challenge
/// response.</param>
/// <param name="targetName">The authentication realm in which the
/// authenticating account has membership.</param>
/// <param name="targetInformation">The target information block from
/// the NTLM type 2 message.</param>
/// <remarks>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.</remarks>
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)
{
}
/// <summary>
/// Creates a new instance of an NTLM type 3 message using the specified
/// values.
/// </summary>
/// <param name="username">The Windows account name to use for
/// authentication.</param>
/// <param name="password">The Windows account password to use for
/// authentication.</param>
/// <param name="challenge">The challenge received from the server as part
/// of the NTLM type 2 message.</param>
/// <param name="useUnicode">Set this to true, if Unicode encoding has been
/// negotiated between client and server.</param>
/// <param name="workstation">The client's workstation name.</param>
/// <param name="ntlmv2">Set to true to send an NTLMv2 challenge
/// response.</param>
/// <param name="targetName">The authentication realm in which the
/// authenticating account has membership.</param>
/// <param name="targetInformation">The target information block from
/// the NTLM type 2 message.</param>
/// <remarks>The target name is a domain name for domain accounts, or
/// a server name for local machine accounts.</remarks>
/// <exception cref="ArgumentNullException">Thrown if the username, password
/// or challenge parameters are null.</exception>
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);
}
/// <summary>
/// Serializes this instance of the Type3 class to an array of bytes.
/// </summary>
/// <returns>An array of bytes representing this instance of the Type3
/// class.</returns>
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();
}
/// <summary>
/// Returns a random 8-byte cnonce value.
/// </summary>
/// <returns>A random 8-byte cnonce value.</returns>
private static byte[] GetCNonce() {
byte[] b = new byte[8];
new Random().NextBytes(b);
return b;
}
}
}

124
Mechanisms/SaslCramMd5.cs Normal file
View File

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace S22.Sasl.Mechanisms {
/// <summary>
/// Implements the Sasl Cram-Md5 authentication method as described in
/// RFC 2195.
/// </summary>
internal class SaslCramMd5 : SaslMechanism {
bool Completed = false;
/// <summary>
/// Server sends the first message in the authentication exchange.
/// </summary>
public override bool HasInitial {
get {
return false;
}
}
/// <summary>
/// True if the authentication exchange between client and server
/// has been completed.
/// </summary>
public override bool IsCompleted {
get {
return Completed;
}
}
/// <summary>
/// The IANA name for the Cram-Md5 authentication mechanism as described
/// in RFC 2195.
/// </summary>
public override string Name {
get {
return "CRAM-MD5";
}
}
/// <summary>
/// The username to authenticate with.
/// </summary>
string Username {
get {
return Properties.ContainsKey("Username") ?
Properties["Username"] as string : null;
}
set {
Properties["Username"] = value;
}
}
/// <summary>
/// The password to authenticate with.
/// </summary>
string Password {
get {
return Properties.ContainsKey("Password") ?
Properties["Password"] as string : null;
}
set {
Properties["Password"] = value;
}
}
/// <summary>
/// Private constructor for use with Sasl.SaslFactory.
/// </summary>
private SaslCramMd5() {
// Nothing to do here.
}
/// <summary>
/// Creates and initializes a new instance of the SaslCramMd5 class
/// using the specified username and password.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="password">The plaintext password to authenticate
/// with.</param>
/// <exception cref="ArgumentNullException">Thrown if the username
/// or the password parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the username
/// parameter is empty.</exception>
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;
}
/// <summary>
/// Computes the client response to the specified Cram-Md5 challenge.
/// </summary>
/// <param name="challenge">The challenge sent by the server</param>
/// <returns>The response to the Cram-Md5 challenge.</returns>
/// <exception cref="SaslException">Thrown if the response could not
/// be computed.</exception>
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);
}
}
}

281
Mechanisms/SaslDigestMd5.cs Normal file
View File

@ -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 {
/// <summary>
/// Implements the Sasl Cram-Md5 authentication method as described in
/// RFC 2831.
/// </summary>
internal class SaslDigestMd5 : SaslMechanism {
bool Completed = false;
/// <summary>
/// The client nonce value used during authentication.
/// </summary>
string Cnonce = GenerateCnonce();
/// <summary>
/// Cram-Md5 involves several steps.
/// </summary>
int Step = 0;
/// <summary>
/// The server sends the first message in the authentication exchange.
/// </summary>
public override bool HasInitial {
get {
return false;
}
}
/// <summary>
/// True if the authentication exchange between client and server
/// has been completed.
/// </summary>
public override bool IsCompleted {
get {
return Completed;
}
}
/// <summary>
/// The IANA name for the Digest-Md5 authentication mechanism as described
/// in RFC 2195.
/// </summary>
public override string Name {
get {
return "DIGEST-MD5";
}
}
/// <summary>
/// The username to authenticate with.
/// </summary>
string Username {
get {
return Properties.ContainsKey("Username") ?
Properties["Username"] as string : null;
}
set {
Properties["Username"] = value;
}
}
/// <summary>
/// The password to authenticate with.
/// </summary>
string Password {
get {
return Properties.ContainsKey("Password") ?
Properties["Password"] as string : null;
}
set {
Properties["Password"] = value;
}
}
/// <summary>
/// Private constructor for use with Sasl.SaslFactory.
/// </summary>
private SaslDigestMd5() {
// Nothing to do here.
}
/// <summary>
/// Internal constructor used for unit testing.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="password">The plaintext password to authenticate
/// with.</param>
/// <param name="cnonce">The client nonce value to use.</param>
/// <exception cref="ArgumentNullException">Thrown if the username
/// or the password parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the username
/// parameter is empty.</exception>
internal SaslDigestMd5(string username, string password, string cnonce)
: this(username, password) {
Cnonce = cnonce;
}
/// <summary>
/// Creates and initializes a new instance of the SaslDigestMd5 class
/// using the specified username and password.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="password">The plaintext password to authenticate
/// with.</param>
/// <exception cref="ArgumentNullException">Thrown if the username
/// or the password parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the username
/// parameter is empty.</exception>
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;
}
/// <summary>
/// Computes the client response to the specified Digest-Md5 challenge.
/// </summary>
/// <param name="challenge">The challenge sent by the server</param>
/// <returns>The response to the Digest-Md5 challenge.</returns>
/// <exception cref="SaslException">Thrown if the response could not
/// be computed.</exception>
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);
}
/// <summary>
/// Parses the challenge string sent by the server in response to a Digest-Md5
/// authentication request.
/// </summary>
/// <param name="challenge">The challenge sent by the server as part of
/// "Step One" of the Digest-Md5 authentication mechanism.</param>
/// <returns>An initialized NameValueCollection instance made up of the
/// attribute/value pairs contained in the challenge.</returns>
/// <exception cref="ArgumentNullException">Thrown if the challenge parameter
/// is null.</exception>
/// <remarks>Refer to RFC 2831 section 2.1.1 for a detailed description of the
/// format of the challenge sent by the server.</remarks>
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;
}
/// <summary>
/// Computes the "response-value" hex-string which is part of the
/// Digest-MD5 challenge-response.
/// </summary>
/// <param name="challenge">A collection containing the attributes
/// and values of the challenge sent by the server.</param>
/// <param name="cnonce">The cnonce value to use for computing
/// the response-value.</param>
/// <param name="digestUri">The "digest-uri" string to use for
/// computing the response-value.</param>
/// <param name="username">The username to use for computing the
/// response-value.</param>
/// <param name="password">The password to use for computing the
/// response-value.</param>
/// <returns>A string containing a hash-value which is part of the
/// response sent by the client.</returns>
/// <remarks>Refer to RFC 2831, section 2.1.2.1 for a detailed
/// description of the computation of the response-value.</remarks>
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);
}
}
/// <summary>
/// Calculates the MD5 hash value for the specified string.
/// </summary>
/// <param name="s">The string to calculate the MD5 hash value for.</param>
/// <param name="encoding">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.</param>
/// <returns>An MD5 hash as a 32-character hex-string.</returns>
/// <exception cref="ArgumentException">Thrown if the input string
/// is null.</exception>
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();
}
/// <summary>
/// Encloses the specified string in double-quotes.
/// </summary>
/// <param name="s">The string to enclose in double-quote characters.</param>
/// <returns>The enclosed string.</returns>
private static string Dquote(string s) {
return "\"" + s + "\"";
}
/// <summary>
/// Generates a random cnonce-value which is a "client-specified data string
/// which must be different each time a digest-response is sent".
/// </summary>
/// <returns>A random "cnonce-value" string.</returns>
private static string GenerateCnonce() {
return Guid.NewGuid().ToString("N").Substring(0, 16);
}
}
}

161
Mechanisms/SaslNtlm.cs Normal file
View File

@ -0,0 +1,161 @@
using S22.Sasl.Mechanisms.Ntlm;
using System;
namespace S22.Sasl.Mechanisms {
/// <summary>
/// Implements the Sasl NTLM authentication method which is used in various
/// Microsoft network protocol implementations.
/// </summary>
/// <remarks>Implemented with the help of the excellent documentation on
/// NTLM composed by Eric Glass.</remarks>
internal class SaslNtlm : SaslMechanism {
protected bool completed = false;
/// <summary>
/// NTLM involves several steps.
/// </summary>
protected int step = 0;
/// <summary>
/// Client sends the first message in the authentication exchange.
/// </summary>
public override bool HasInitial {
get {
return true;
}
}
/// <summary>
/// True if the authentication exchange between client and server
/// has been completed.
/// </summary>
public override bool IsCompleted {
get {
return completed;
}
}
/// <summary>
/// The IANA name for the NTLM authentication mechanism.
/// </summary>
public override string Name {
get {
return "NTLM";
}
}
/// <summary>
/// The username to authenticate with.
/// </summary>
protected string Username {
get {
return Properties.ContainsKey("Username") ?
Properties["Username"] as string : null;
}
set {
Properties["Username"] = value;
}
}
/// <summary>
/// The password to authenticate with.
/// </summary>
protected string Password {
get {
return Properties.ContainsKey("Password") ?
Properties["Password"] as string : null;
}
set {
Properties["Password"] = value;
}
}
/// <summary>
/// Private constructor for use with Sasl.SaslFactory.
/// </summary>
protected SaslNtlm() {
// Nothing to do here.
}
/// <summary>
/// Creates and initializes a new instance of the SaslNtlm class
/// using the specified username and password.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="password">The plaintext password to authenticate
/// with.</param>
/// <exception cref="ArgumentNullException">Thrown if the username
/// or the password parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the username
/// parameter is empty.</exception>
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;
}
/// <summary>
/// Computes the client response to the specified NTLM challenge.
/// </summary>
/// <param name="challenge">The challenge sent by the server</param>
/// <returns>The response to the NTLM challenge.</returns>
/// <exception cref="SaslException">Thrown if the response could not
/// be computed.</exception>
protected override byte[] ComputeResponse(byte[] challenge) {
if (step == 1)
completed = true;
byte[] ret = step == 0 ? ComputeInitialResponse(challenge) :
ComputeChallengeResponse(challenge);
step = step + 1;
return ret;
}
/// <summary>
/// Computes the initial client response to an NTLM challenge.
/// </summary>
/// <param name="challenge">The challenge sent by the server. Since
/// NTLM expects an initial client response, this will usually be
/// empty.</param>
/// <returns>The initial response to the NTLM challenge.</returns>
/// <exception cref="SaslException">Thrown if the response could not
/// be computed.</exception>
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);
}
}
/// <summary>
/// Computes the actual challenge response to an NTLM challenge
/// which is sent as part of an NTLM type 2 message.
/// </summary>
/// <param name="challenge">The challenge sent by the server.</param>
/// <returns>The response to the NTLM challenge.</returns>
/// <exception cref="SaslException">Thrown if the challenge
/// response could not be computed.</exception>
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);
}
}
}
}

70
Mechanisms/SaslNtlmv2.cs Normal file
View File

@ -0,0 +1,70 @@
using S22.Sasl.Mechanisms.Ntlm;
using System;
namespace S22.Sasl.Mechanisms {
/// <summary>
/// Implements the Sasl NTLMv2 authentication method which addresses
/// some of the security issues present in NTLM version 1.
/// </summary>
internal class SaslNtlmv2 : SaslNtlm {
/// <summary>
/// Private constructor for use with Sasl.SaslFactory.
/// </summary>
protected SaslNtlmv2() {
// Nothing to do here.
}
/// <summary>
/// Creates and initializes a new instance of the SaslNtlmv2 class
/// using the specified username and password.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="password">The plaintext password to authenticate
/// with.</param>
/// <exception cref="ArgumentNullException">Thrown if the username
/// or the password parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the username
/// parameter is empty.</exception>
public SaslNtlmv2(string username, string password)
: base(username, password) {
}
/// <summary>
/// Computes the client response to the specified NTLM challenge.
/// </summary>
/// <param name="challenge">The challenge sent by the server</param>
/// <returns>The response to the NTLM challenge.</returns>
/// <exception cref="SaslException">Thrown if the response could not
/// be computed.</exception>
protected override byte[] ComputeResponse(byte[] challenge) {
if (step == 1)
completed = true;
byte[] ret = step == 0 ? ComputeInitialResponse(challenge) :
ComputeChallengeResponse(challenge);
step = step + 1;
return ret;
}
/// <summary>
/// Computes the actual challenge response to an NTLM challenge
/// which is sent as part of an NTLM type 2 message.
/// </summary>
/// <param name="challenge">The challenge sent by the server.</param>
/// <returns>The response to the NTLM challenge.</returns>
/// <exception cref="SaslException">Thrown if the challenge
/// response could not be computed.</exception>
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);
}
}
}
}

93
Mechanisms/SaslOAuth.cs Normal file
View File

@ -0,0 +1,93 @@
using System;
using System.Text;
namespace S22.Sasl.Mechanisms {
/// <summary>
/// Implements the Sasl OAuth authentication method.
/// </summary>
internal class SaslOAuth : SaslMechanism {
bool Completed = false;
/// <summary>
/// Client sends the first message in the authentication exchange.
/// </summary>
public override bool HasInitial {
get {
return true;
}
}
/// <summary>
/// True if the authentication exchange between client and server
/// has been completed.
/// </summary>
public override bool IsCompleted {
get {
return Completed;
}
}
/// <summary>
/// The IANA name for the OAuth authentication mechanism.
/// </summary>
public override string Name {
get {
return "XOAUTH";
}
}
/// <summary>
/// The access token to authenticate with.
/// </summary>
string AccessToken {
get {
return Properties.ContainsKey("AccessToken") ?
Properties["AccessToken"] as string : null;
}
set {
Properties["AccessToken"] = value;
}
}
/// <summary>
/// Private constructor for use with Sasl.SaslFactory.
/// </summary>
private SaslOAuth() {
// Nothing to do here.
}
/// <summary>
/// Creates and initializes a new instance of the SaslOAuth class
/// using the specified username and password.
/// </summary>
/// <param name="accessToken">The username to authenticate with.</param>
/// <exception cref="ArgumentNullException">Thrown if the accessToken
/// parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the accessToken
/// parameter is empty.</exception>
public SaslOAuth(string accessToken) {
accessToken.ThrowIfNull("accessToken");
if (accessToken == String.Empty)
throw new ArgumentException("The access token must not be empty.");
AccessToken = accessToken;
}
/// <summary>
/// Computes the client response for a OAuth challenge.
/// </summary>
/// <param name="challenge">The challenge sent by the server.</param>
/// <returns>The response to the OAuth challenge.</returns>
/// <exception cref="SaslException">Thrown if the response could not
/// be computed.</exception>
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);
}
}
}

138
Mechanisms/SaslOAuth2.cs Normal file
View File

@ -0,0 +1,138 @@
using System;
using System.Text;
namespace S22.Sasl.Mechanisms {
/// <summary>
/// Implements the Sasl OAuth 2.0 authentication method.
/// </summary>
internal class SaslOAuth2 : SaslMechanism {
bool Completed = false;
/// <summary>
/// The server sends an error response in case authentication fails
/// which must be acknowledged.
/// </summary>
int Step = 0;
/// <summary>
/// Client sends the first message in the authentication exchange.
/// </summary>
public override bool HasInitial {
get {
return true;
}
}
/// <summary>
/// True if the authentication exchange between client and server
/// has been completed.
/// </summary>
public override bool IsCompleted {
get {
return Completed;
}
}
/// <summary>
/// The IANA name for the OAuth 2.0 authentication mechanism.
/// </summary>
public override string Name {
get {
return "XOAUTH2";
}
}
/// <summary>
/// The username to authenticate with.
/// </summary>
string Username {
get {
return Properties.ContainsKey("Username") ?
Properties["Username"] as string : null;
}
set {
Properties["Username"] = value;
}
}
/// <summary>
/// The access token to authenticate with.
/// </summary>
string AccessToken {
get {
return Properties.ContainsKey("AccessToken") ?
Properties["AccessToken"] as string : null;
}
set {
Properties["AccessToken"] = value;
}
}
/// <summary>
/// Private constructor for use with Sasl.SaslFactory.
/// </summary>
private SaslOAuth2() {
// Nothing to do here.
}
/// <summary>
/// Creates and initializes a new instance of the SaslOAuth class
/// using the specified username and password.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="accessToken">The username to authenticate with.</param>
/// <exception cref="ArgumentNullException">Thrown if the username
/// or the accessToken parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the username or
/// the accessToken parameter is empty.</exception>
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;
}
/// <summary>
/// Computes the client response to an XOAUTH2 challenge.
/// </summary>
/// <param name="challenge">The challenge sent by the server.</param>
/// <returns>The response to the OAuth2 challenge.</returns>
/// <exception cref="SaslException">Thrown if the response could not
/// be computed.</exception>
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;
}
/// <summary>
/// Computes the initial client response to an XOAUTH2 challenge.
/// </summary>
/// <param name="challenge">The challenge sent by the server.</param>
/// <returns>The response to the OAuth2 challenge.</returns>
/// <exception cref="SaslException">Thrown if the response could not
/// be computed.</exception>
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);
}
}
}

119
Mechanisms/SaslPlain.cs Normal file
View File

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace S22.Sasl.Mechanisms {
/// <summary>
/// Implements the Sasl Plain authentication method as described in
/// RFC 4616.
/// </summary>
internal class SaslPlain : SaslMechanism {
bool Completed = false;
/// <summary>
/// Sasl Plain just sends one initial response.
/// </summary>
public override bool HasInitial {
get {
return true;
}
}
/// <summary>
/// True if the authentication exchange between client and server
/// has been completed.
/// </summary>
public override bool IsCompleted {
get {
return Completed;
}
}
/// <summary>
/// The IANA name for the Plain authentication mechanism as described
/// in RFC 4616.
/// </summary>
public override string Name {
get {
return "PLAIN";
}
}
/// <summary>
/// The username to authenticate with.
/// </summary>
string Username {
get {
return Properties.ContainsKey("Username") ?
Properties["Username"] as string : null;
}
set {
Properties["Username"] = value;
}
}
/// <summary>
/// The plain-text password to authenticate with.
/// </summary>
string Password {
get {
return Properties.ContainsKey("Password") ?
Properties["Password"] as string : null;
}
set {
Properties["Password"] = value;
}
}
/// <summary>
/// Private constructor for use with Sasl.SaslFactory.
/// </summary>
private SaslPlain() {
// Nothing to do here.
}
/// <summary>
/// Creates and initializes a new instance of the SaslPlain class
/// using the specified username and password.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="password">The plaintext password to authenticate
/// with.</param>
/// <exception cref="ArgumentNullException">Thrown if the username
/// or the password parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the username
/// parameter is empty.</exception>
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;
}
/// <summary>
/// Computes the client response for a plain-challenge.
/// </summary>
/// <param name="challenge">The challenge sent by the server. For the
/// "plain" mechanism this will usually be empty.</param>
/// <returns>The response for the "plain"-challenge.</returns>
/// <exception cref="SaslException">Thrown if the response could not
/// be computed.</exception>
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);
}
}
}

368
Mechanisms/SaslScramSha1.cs Normal file
View File

@ -0,0 +1,368 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace S22.Sasl.Mechanisms {
/// <summary>
/// Implements the Sasl SCRAM-SHA-1 authentication method as described in
/// RFC 5802.
/// </summary>
internal class SaslScramSha1 : SaslMechanism {
bool Completed = false;
/// <summary>
/// The client nonce value used during authentication.
/// </summary>
string Cnonce = GenerateCnonce();
/// <summary>
/// Scram-Sha-1 involves several steps.
/// </summary>
int Step = 0;
/// <summary>
/// The salted password. This is needed for client authentication and later
/// on again for verifying the server signature.
/// </summary>
byte[] SaltedPassword;
/// <summary>
/// The auth message is part of the authentication exchange and is needed for
/// authentication as well as for verifying the server signature.
/// </summary>
string AuthMessage;
/// <summary>
/// Client sends the first message in the authentication exchange.
/// </summary>
public override bool HasInitial {
get {
return true;
}
}
/// <summary>
/// True if the authentication exchange between client and server
/// has been completed.
/// </summary>
public override bool IsCompleted {
get {
return Completed;
}
}
/// <summary>
/// The IANA name for the Scram-Sha-1 authentication mechanism as described
/// in RFC 5802.
/// </summary>
public override string Name {
get {
return "SCRAM-SHA-1";
}
}
/// <summary>
/// The username to authenticate with.
/// </summary>
string Username {
get {
return Properties.ContainsKey("Username") ?
Properties["Username"] as string : null;
}
set {
Properties["Username"] = value;
}
}
/// <summary>
/// The password to authenticate with.
/// </summary>
string Password {
get {
return Properties.ContainsKey("Password") ?
Properties["Password"] as string : null;
}
set {
Properties["Password"] = value;
}
}
/// <summary>
/// Private constructor for use with Sasl.SaslFactory.
/// </summary>
private SaslScramSha1() {
// Nothing to do here.
}
/// <summary>
/// Internal constructor used for unit testing.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="password">The plaintext password to authenticate
/// with.</param>
/// <param name="cnonce">The client nonce value to use.</param>
/// <exception cref="ArgumentNullException">Thrown if the username
/// or the password parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the username
/// parameter is empty.</exception>
internal SaslScramSha1(string username, string password, string cnonce)
: this(username, password) {
Cnonce = cnonce;
}
/// <summary>
/// Creates and initializes a new instance of the SaslScramSha1
/// class using the specified username and password.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="password">The plaintext password to authenticate
/// with.</param>
/// <exception cref="ArgumentNullException">Thrown if the username
/// or the password parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the username
/// parameter is empty.</exception>
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;
}
/// <summary>
/// Computes the client response to the specified SCRAM-SHA-1 challenge.
/// </summary>
/// <param name="challenge">The challenge sent by the server</param>
/// <returns>The response to the SCRAM-SHA-1 challenge.</returns>
/// <exception cref="SaslException">Thrown if the response could not
/// be computed.</exception>
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;
}
/// <summary>
/// Computes the initial response sent by the client to the server.
/// </summary>
/// <returns>An array of bytes containing the initial client
/// response.</returns>
private byte[] ComputeInitialResponse() {
// We don't support channel binding.
return Encoding.UTF8.GetBytes("n,,n=" + SaslPrep(Username) + ",r=" +
Cnonce);
}
/// <summary>
/// Computes the "client-final-message" which completes the authentication
/// process.
/// </summary>
/// <param name="challenge">The "server-first-message" challenge received
/// from the server in response to the initial client response.</param>
/// <returns>An array of bytes containing the client's challenge
/// response.</returns>
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));
}
/// <summary>
/// Verifies the nonce value sent by the server.
/// </summary>
/// <param name="nonce">The nonce value sent by the server as part of the
/// server-first-message.</param>
/// <returns>True if the nonce value is valid, otherwise false.</returns>
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);
}
/// <summary>
/// Verifies the server signature which is sent by the server as the final
/// step of the authentication process.
/// </summary>
/// <param name="challenge">The server signature as a base64-encoded
/// string.</param>
/// <returns>The client's response to the server. This will be an empty
/// byte array if verification was successful, or the '*' SASL cancellation
/// token.</returns>
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("*");
}
/// <summary>
/// Parses the "server-first-message" received from the server.
/// </summary>
/// <param name="challenge">The challenge received from the server.</param>
/// <returns>A collection of key/value pairs contained extracted from
/// the server message.</returns>
/// <exception cref="ArgumentNullException">Thrown if the message parameter
/// is null.</exception>
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;
}
/// <summary>
/// Computes the "Hi()"-formula which is part of the client's response
/// to the server challenge.
/// </summary>
/// <param name="password">The supplied password to use.</param>
/// <param name="salt">The salt received from the server.</param>
/// <param name="count">The iteration count.</param>
/// <returns>An array of bytes containing the result of the
/// computation of the "Hi()"-formula.</returns>
/// <remarks>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)</remarks>
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);
}
}
/// <summary>
/// Applies the HMAC keyed hash algorithm using the specified key to
/// the specified input data.
/// </summary>
/// <param name="key">The key to use for initializing the HMAC
/// provider.</param>
/// <param name="data">The input to compute the hashcode for.</param>
/// <returns>The hashcode of the specified data input.</returns>
private byte[] HMAC(byte[] key, byte[] data) {
using (var hmac = new HMACSHA1(key)) {
return hmac.ComputeHash(data);
}
}
/// <summary>
/// Applies the HMAC keyed hash algorithm using the specified key to
/// the specified input string.
/// </summary>
/// <param name="key">The key to use for initializing the HMAC
/// provider.</param>
/// <param name="data">The input string to compute the hashcode for.</param>
/// <returns>The hashcode of the specified string.</returns>
private byte[] HMAC(byte[] key, string data) {
return HMAC(key, Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// Applies the cryptographic hash function SHA-1 to the specified data
/// array.
/// </summary>
/// <param name="data">The data array to apply the hash function to.</param>
/// <returns>The hash value for the specified byte array.</returns>
private byte[] H(byte[] data) {
using (var sha1 = new SHA1Managed()) {
return sha1.ComputeHash(data);
}
}
/// <summary>
/// Applies the exclusive-or operation to combine the specified byte array
/// a with the specified byte array b.
/// </summary>
/// <param name="a">The first byte array.</param>
/// <param name="b">The second byte array.</param>
/// <returns>An array of bytes of the same length as the input arrays
/// containing the result of the exclusive-or operation.</returns>
/// <exception cref="ArgumentNullException">Thrown if either argument is
/// null.</exception>
/// <exception cref="InvalidOperationException">Thrown if the input arrays
/// are not of the same length.</exception>
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;
}
/// <summary>
/// Generates a random cnonce-value which is a "client-specified data string
/// which must be different each time a digest-response is sent".
/// </summary>
/// <returns>A random "cnonce-value" string.</returns>
private static string GenerateCnonce() {
return Guid.NewGuid().ToString("N").Substring(0, 16);
}
/// <summary>
/// Prepares the specified string as is described in RFC 5802.
/// </summary>
/// <param name="s">A string value.</param>
/// <returns>A "Saslprepped" string.</returns>
private static string SaslPrep(string s) {
// Fixme: Do this properly?
return s
.Replace("=", "=3D")
.Replace(",", "=2C");
}
}
}

287
Mechanisms/SaslSrp.cs Normal file
View File

@ -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 {
/// <summary>
/// Implements the Sasl Secure Remote Password (SRP) authentication
/// mechanism as is described in the IETF SRP 08 draft.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal class SaslSrp : SaslMechanism {
bool Completed = false;
/// <summary>
/// SRP involves several steps.
/// </summary>
int Step = 0;
/// <summary>
/// The negotiated hash algorithm which will be used to perform any
/// message digest calculations.
/// </summary>
HashAlgorithm HashAlgorithm;
/// <summary>
/// The public key computed as part of the authentication exchange.
/// </summary>
Mpi PublicKey;
/// <summary>
/// The client's private key used for calculating the client evidence.
/// </summary>
Mpi PrivateKey = Helper.GenerateClientPrivateKey();
/// <summary>
/// The secret key shared between client and server.
/// </summary>
Mpi SharedKey;
/// <summary>
/// The client evidence calculated as part of the authentication exchange.
/// </summary>
byte[] ClientProof;
/// <summary>
/// The options chosen by the client, picked from the list of options
/// advertised by the server.
/// </summary>
string Options;
/// <summary>
/// Client sends the first message in the authentication exchange.
/// </summary>
public override bool HasInitial {
get {
return true;
}
}
/// <summary>
/// True if the authentication exchange between client and server
/// has been completed.
/// </summary>
public override bool IsCompleted {
get {
return Completed;
}
}
/// <summary>
/// The IANA name for the SRP authentication mechanism.
/// </summary>
public override string Name {
get {
return "SRP";
}
}
/// <summary>
/// The username to authenticate with.
/// </summary>
string Username {
get {
return Properties.ContainsKey("Username") ?
Properties["Username"] as string : null;
}
set {
Properties["Username"] = value;
}
}
/// <summary>
/// The password to authenticate with.
/// </summary>
string Password {
get {
return Properties.ContainsKey("Password") ?
Properties["Password"] as string : null;
}
set {
Properties["Password"] = value;
}
}
/// <summary>
/// The authorization id (userid in draft jargon).
/// </summary>
string AuthId {
get {
return Properties.ContainsKey("AuthId") ?
Properties["AuthId"] as string : Username;
}
set {
Properties["AuthId"] = value;
}
}
/// <summary>
/// Private constructor for use with Sasl.SaslFactory.
/// </summary>
private SaslSrp() {
// Nothing to do here.
}
/// <summary>
/// Internal constructor used for unit testing.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="password">The plaintext password to authenticate
/// with.</param>
/// <param name="privateKey">The client private key to use.</param>
/// <exception cref="ArgumentNullException">Thrown if the username
/// or the password parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the username
/// parameter is empty.</exception>
internal SaslSrp(string username, string password, byte[] privateKey)
: this(username, password) {
PrivateKey = new Mpi(privateKey);
}
/// <summary>
/// Creates and initializes a new instance of the SaslSrp class using
/// the specified username and password.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="password">The plaintext password to authenticate
/// with.</param>
/// <exception cref="ArgumentNullException">Thrown if the username
/// or the password parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown if the username
/// parameter is empty.</exception>
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;
}
/// <summary>
/// Computes the client response to the specified SRP challenge.
/// </summary>
/// <param name="challenge">The challenge sent by the server</param>
/// <returns>The response to the SRP challenge.</returns>
/// <exception cref="SaslException">Thrown if the response could not
/// be computed.</exception>
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;
}
/// <summary>
/// Computes the initial response sent by the client to the server.
/// </summary>
/// <returns>An array of bytes containing the initial client
/// response.</returns>
private byte[] ComputeInitialResponse() {
return new ClientMessage1(Username, AuthId).Serialize();
}
/// <summary>
/// Computes the client response containing the client's public key and
/// evidence.
/// </summary>
/// <param name="challenge">The challenge containing the protocol elements
/// received from the server in response to the initial client
/// response.</param>
/// <returns>An array of bytes containing the client's challenge
/// response.</returns>
/// <exception cref="SaslException">Thrown if the server specified any
/// mandatory options which are not supported.</exception>
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();
}
/// <summary>
/// Verifies the server signature which is sent by the server as the final
/// step of the authentication process.
/// </summary>
/// <param name="challenge">The server signature as a base64-encoded
/// string.</param>
/// <returns>The client's response to the server. This will be an empty
/// byte array if verification was successful, or the '*' SASL cancellation
/// token.</returns>
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("*");
}
/// <summary>
/// Selects a message digest algorithm from the specified list of
/// supported algorithms.
/// </summary>
/// <returns>A tuple containing the name of the selected message digest
/// algorithm as well as the type.</returns>
/// <exception cref="NotSupportedException">Thrown if none of the algorithms
/// specified in the list parameter is supported.</exception>
private Tuple<string, Type> SelectHashAlgorithm(string list) {
string[] supported = list.Split(',');
var l = new Dictionary<string, Type>(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<string, Type> p in l) {
if (supported.Contains(p.Key, StringComparer.InvariantCultureIgnoreCase))
return new Tuple<string, Type>(p.Key, p.Value);
}
throw new NotSupportedException();
}
}
}

View File

@ -0,0 +1,95 @@
using System;
namespace S22.Sasl.Mechanisms.Srp {
/// <summary>
/// Represents the initial client-response sent to the server to initiate
/// the authentication exchange.
/// </summary>
internal class ClientMessage1 {
/// <summary>
/// The username to authenticate with.
/// </summary>
/// <remarks>SRP specification imposes a limit of 65535 bytes
/// on this field.</remarks>
public string Username {
get;
set;
}
/// <summary>
/// The authorization identity to authenticate with.
/// </summary>
/// <remarks>SRP specification imposes a limit of 65535 bytes
/// on this field.</remarks>
public string AuthId {
get;
set;
}
/// <summary>
/// The session identifier of a previous session whose parameters the
/// client wishes to re-use.
/// </summary>
/// <remarks>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.</remarks>
public string SessionId {
get;
set;
}
/// <summary>
/// The client's nonce used in deriving a new shared context key from
/// the shared context key of the previous session.
/// </summary>
/// <remarks>SRP specification imposes a limit of 255 bytes on this
/// field. If not needed, it must be set to an empty byte array.</remarks>
public byte[] ClientNonce {
get;
set;
}
/// <summary>
/// Creates a new instance of the ClientMessage1 class using the specified
/// username.
/// </summary>
/// <param name="username">The username to authenticate with.</param>
/// <param name="authId">The authorization id to authenticate with.</param>
/// <exception cref="ArgumentNullException">Thrown if the username parameter
/// is null.</exception>
public ClientMessage1(string username, string authId = null) {
username.ThrowIfNull("username");
Username = username;
AuthId = authId ?? String.Empty;
SessionId = String.Empty;
ClientNonce = new byte[0];
}
/// <summary>
/// Serializes this instance of the ClientMessage1 class into a sequence of
/// bytes according to the requirements of the SRP specification.
/// </summary>
/// <returns>A sequence of bytes representing this instance of the
/// ClientMessage1 class.</returns>
/// <exception cref="OverflowException">Thrown if the cummultative length
/// of the serialized data fields exceeds the maximum number of bytes
/// allowed as per SRP specification.</exception>
/// <remarks>SRP specification imposes a limit of 2,147,483,643 bytes on
/// the serialized data.</remarks>
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();
}
}
}

View File

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
namespace S22.Sasl.Mechanisms.Srp {
/// <summary>
/// Represents the second client-response sent to the server as part of
/// the SRP authentication exchange.
/// </summary>
internal class ClientMessage2 {
/// <summary>
/// The client's ephemeral public key.
/// </summary>
public Mpi PublicKey {
get;
set;
}
/// <summary>
/// The evidence which proves to the server client-knowledge of the shared
/// context key.
/// </summary>
public byte[] Proof {
get;
set;
}
/// <summary>
/// The options list indicating the security services chosen by the client.
/// </summary>
public NameValueCollection Options {
get;
private set;
}
/// <summary>
/// The initial vector the server will use to set up its encryption
/// context, if confidentiality protection will be employed.
/// </summary>
public byte[] InitialVector {
get;
set;
}
/// <summary>
/// Creates and initializes a new instance of the ClientMessage2 class.
/// </summary>
private ClientMessage2() {
Options = new NameValueCollection();
InitialVector = new byte[0];
}
/// <summary>
/// Creates and initializes a new instance of the ClientMessage2 class using
/// the specified public key and client proof.
/// </summary>
/// <param name="publicKey">The client's public key.</param>
/// <param name="proof">The calculated client proof.</param>
/// <exception cref="ArgumentNullException">Thrown if either the public key
/// or the proof parameter is null.</exception>
public ClientMessage2(Mpi publicKey, byte[] proof)
: this() {
publicKey.ThrowIfNull("publicKey");
proof.ThrowIfNull("proof");
PublicKey = publicKey;
Proof = proof;
}
/// <summary>
/// Serializes this instance of the ClientMessage2 class into a sequence of
/// bytes according to the requirements of the SRP specification.
/// </summary>
/// <returns>A sequence of bytes representing this instance of the
/// ClientMessage2 class.</returns>
/// <exception cref="OverflowException">Thrown if the cummultative length
/// of the serialized data fields exceeds the maximum number of bytes
/// allowed as per SRP specification.</exception>
/// <remarks>SRP specification imposes a limit of 2,147,483,643 bytes on
/// the serialized data.</remarks>
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();
}
/// <summary>
/// Serializes the client's options collection into a comma-seperated
/// options string.
/// </summary>
/// <returns>A comma-seperated string containing the client's chosen
/// options.</returns>
public string BuildOptionsString() {
List<string> list = new List<string>();
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());
}
}
}

View File

@ -0,0 +1,89 @@
using System;
using System.IO;
using System.Text;
using System.Linq;
namespace S22.Sasl.Mechanisms.Srp {
/// <summary>
/// Adds extension methods to the BinaryReader class to simplify the
/// deserialization of SRP messages.
/// </summary>
internal static class BinaryReaderExtensions {
/// <summary>
/// Reads an unsigned integer value from the underlying stream,
/// optionally using big endian byte ordering.
/// </summary>
/// <param name="reader">Extension method for BinaryReader.</param>
/// <param name="bigEndian">Set to true to interpret the integer value
/// as big endian value.</param>
/// <returns>The 32-byte unsigned integer value read from the underlying
/// stream.</returns>
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;
}
/// <summary>
/// Reads an unsigned short value from the underlying stream, optionally
/// using big endian byte ordering.
/// </summary>
/// <param name="reader">Extension method for BinaryReader.</param>
/// <param name="bigEndian">Set to true to interpret the short value
/// as big endian value.</param>
/// <returns>The 16-byte unsigned short value read from the underlying
/// stream.</returns>
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;
}
/// <summary>
/// Reads a "multi-precision integer" from this instance.
/// </summary>
/// <param name="reader">Extension method for the BinaryReader class.</param>
/// <returns>An instance of the Mpi class decoded from the bytes read
/// from the underlying stream.</returns>
public static Mpi ReadMpi(this BinaryReader reader) {
ushort length = reader.ReadUInt16(true);
byte[] data = reader.ReadBytes(length);
return new Mpi(data);
}
/// <summary>
/// Reads an "octet-sequence" from this instance.
/// </summary>
/// <param name="reader">Extension method for the BinaryReader class.</param>
/// <returns>An instance of the OctetSequence class decoded from the bytes
/// read from the underlying stream.</returns>
public static OctetSequence ReadOs(this BinaryReader reader) {
byte length = reader.ReadByte();
byte[] data = reader.ReadBytes(length);
return new OctetSequence(data);
}
/// <summary>
/// Reads an UTF-8 string from this instance.
/// </summary>
/// <param name="reader">Extension method for the BinaryReader class.</param>
/// <returns>An instance of the Utf8String class decoded from the bytes
/// read from the underlying stream.</returns>
public static Utf8String ReadUtf8String(this BinaryReader reader) {
ushort length = reader.ReadUInt16(true);
byte[] data = reader.ReadBytes(length);
return new Utf8String(Encoding.UTF8.GetString(data));
}
}
}

321
Mechanisms/Srp/Helper.cs Normal file
View File

@ -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 {
/// <summary>
/// Contains helper methods for calculating the various components of the
/// SRP authentication exchange.
/// </summary>
internal static class Helper {
/// <summary>
/// The trace source used for informational and debug messages.
/// </summary>
static TraceSource ts = new TraceSource("S22.Imap.Sasl.Srp");
/// <summary>
/// Determines whether the specified modulus is valid.
/// </summary>
/// <param name="N">The modulus to validate.</param>
/// <returns>True if the specified modulus is valid, otherwise
/// false.</returns>
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;
}
/// <summary>
/// Determines whether the specified generator is valid.
/// </summary>
/// <param name="g">The generator to validate.</param>
/// <returns>True if the specified generator is valid, otherwise
/// false.</returns>
public static bool IsValidGenerator(Mpi g) {
return BigInteger.Compare(new BigInteger(2), g.Value) == 0;
}
/// <summary>
/// Generates a random "multi-precision integer" which will act as the
/// client's private key.
/// </summary>
/// <returns>The client's ephemeral private key as a "multi-precision
/// integer".</returns>
public static Mpi GenerateClientPrivateKey() {
using (var rng = new RNGCryptoServiceProvider()) {
byte[] data = new byte[16];
rng.GetBytes(data);
return new Mpi(data);
}
}
/// <summary>
/// Calculates the client's ephemeral public key.
/// </summary>
/// <param name="generator">The generator sent by the server.</param>
/// <param name="safePrimeModulus">The safe prime modulus sent by
/// the server.</param>
/// <param name="privateKey">The client's private key.</param>
/// <returns>The client's ephemeral public key as a
/// "multi-precision integer".</returns>
/// <remarks>
/// A = Client Public Key
/// g = Generator
/// a = Client Private Key
/// N = Safe Prime Modulus
/// </remarks>
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);
}
/// <summary>
/// Calculates the shared context key K from the given parameters.
/// </summary>
/// <param name="salt">The user's password salt.</param>
/// <param name="username">The username to authenticate with.</param>
/// <param name="password">The password to authenticate with.</param>
/// <param name="clientPublicKey">The client's ephemeral public key.</param>
/// <param name="serverPublicKey">The server's ephemeral public key.</param>
/// <param name="clientPrivateKey">The client's private key.</param>
/// <param name="generator">The generator sent by the server.</param>
/// <param name="safePrimeModulus">The safe prime modulus sent by the
/// server.</param>
/// <param name="hashAlgorithm">The negotiated hash algorithm to use
/// for the calculations.</param>
/// <returns>The shared context key K as a "multi-precision
/// integer".</returns>
/// <remarks>
/// 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
/// </remarks>
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()));
}
/// <summary>
/// Computes the client evidence from the given parameters.
/// </summary>
/// <param name="safePrimeModulus">The safe prime modulus sent by the
/// server.</param>
/// <param name="generator">The generator sent by the server.</param>
/// <param name="username">The username to authenticate with.</param>
/// <param name="salt">The client's password salt.</param>
/// <param name="clientPublicKey">The client's ephemeral public key.</param>
/// <param name="serverPublicKey">The server's ephemeral public key.</param>
/// <param name="sharedKey">The shared context key.</param>
/// <param name="authId">The authorization identity.</param>
/// <param name="options">The raw options string as received from the
/// server.</param>
/// <param name="hashAlgorithm">The message digest algorithm to use for
/// calculating the client proof.</param>
/// <returns>The client proof as an array of bytes.</returns>
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);
}
/// <summary>
/// Computes the server evidence from the given parameters.
/// </summary>
/// <param name="clientPublicKey">The client's ephemeral public key.</param>
/// <param name="clientProof"></param>
/// <param name="sharedKey">The shared context key.</param>
/// <param name="authId">The authorization identity.</param>
/// <param name="options">The raw options string as sent by the
/// client.</param>
/// <param name="sid">The session id sent by the server.</param>
/// <param name="ttl">The time-to-live value for the session id sent
/// by the server.</param>
/// <param name="hashAlgorithm">The message digest algorithm to use for
/// calculating the server proof.</param>
/// <returns>The server proof as an array of bytes.</returns>
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);
}
/// <summary>
/// Applies the exclusive-or operation to combine the specified byte array
/// a with the specified byte array b.
/// </summary>
/// <param name="a">The first byte array.</param>
/// <param name="b">The second byte array.</param>
/// <returns>An array of bytes of the same length as the input arrays
/// containing the result of the exclusive-or operation.</returns>
/// <exception cref="ArgumentNullException">Thrown if either argument is
/// null.</exception>
/// <exception cref="InvalidOperationException">Thrown if the input arrays
/// are not of the same length.</exception>
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
/// <summary>
/// Recommended values for the safe prime modulus (Refer to Appendix A.
/// "Modulus and Generator Values" of the IETF SRP draft).
/// </summary>
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
}
}

83
Mechanisms/Srp/Mpi.cs Normal file
View File

@ -0,0 +1,83 @@
using System;
using System.Linq;
using System.Numerics;
namespace S22.Sasl.Mechanisms.Srp {
/// <summary>
/// Represents a "multi-precision integer" (MPI) as is described in the
/// SRP specification (3.2 Multi-Precision Integers, p.5).
/// </summary>
/// <remarks>Multi-Precision Integers, or MPIs, are positive integers used
/// to hold large integers used in cryptographic computations.</remarks>
internal class Mpi {
/// <summary>
/// The underlying BigInteger instance used to represent this
/// "multi-precision integer".
/// </summary>
public BigInteger Value {
get;
set;
}
/// <summary>
/// Creates a new "multi-precision integer" from the specified array
/// of bytes.
/// </summary>
/// <param name="data">A big-endian sequence of bytes forming the
/// integer value of the multi-precision integer.</param>
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());
}
/// <summary>
/// Creates a new "multi-precision integer" from the specified BigInteger
/// instance.
/// </summary>
/// <param name="value">The BigInteger instance to initialize the MPI
/// with.</param>
public Mpi(BigInteger value)
: this(value.ToByteArray().Reverse().ToArray()) {
}
/// <summary>
/// Returns a sequence of bytes in big-endian order forming the integer
/// value of this "multi-precision integer" instance.
/// </summary>
/// <returns>Returns a sequence of bytes in big-endian order representing
/// this "multi-precision integer" instance.</returns>
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;
}
/// <summary>
/// Serializes the "multi-precision integer" into a sequence of bytes
/// according to the requirements of the SRP specification.
/// </summary>
/// <returns>A big-endian sequence of bytes representing the integer
/// value of the MPI.</returns>
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();
}
}
}

View File

@ -0,0 +1,47 @@
using System;
namespace S22.Sasl.Mechanisms.Srp {
/// <summary>
/// Represents an "octet-sequence" as is described in the SRP specification
/// (3.3 Octet sequences, p.6).
/// </summary>
internal class OctetSequence {
/// <summary>
/// The underlying byte array forming this instance of the OctetSequence
/// class.
/// </summary>
public byte[] Value {
get;
set;
}
/// <summary>
/// Creates a new instance of the OctetSequence class using the specified
/// byte array.
/// </summary>
/// <param name="sequence">The sequence of bytes to initialize this instance
/// of the OctetSequence class with.</param>
public OctetSequence(byte[] sequence) {
Value = sequence;
}
/// <summary>
/// Serializes this instance of the OctetSequence class into a sequence of
/// bytes according to the requirements of the SRP specification.
/// </summary>
/// <returns>A sequence of bytes representing this instance of the
/// OctetSequence class.</returns>
/// <exception cref="OverflowException">Thrown if the length of the byte
/// sequence exceeds the maximum number of bytes allowed as per SRP
/// specification.</exception>
/// <remarks>SRP specification imposes a limit of 255 bytes on the
/// length of the underlying byte array.</remarks>
public byte[] Serialize() {
byte length = Convert.ToByte(Value.Length);
return new ByteBuilder()
.Append(length)
.Append(Value)
.ToArray();
}
}
}

View File

@ -0,0 +1,117 @@
using System;
using System.Collections.Specialized;
using System.IO;
namespace S22.Sasl.Mechanisms.Srp {
/// <summary>
/// Represents the first message sent by the server in response to an
/// initial client-response.
/// </summary>
internal class ServerMessage1 {
/// <summary>
/// The safe prime modulus sent by the server.
/// </summary>
public Mpi SafePrimeModulus {
get;
set;
}
/// <summary>
/// The generator sent by the server.
/// </summary>
public Mpi Generator {
get;
set;
}
/// <summary>
/// The user's password salt.
/// </summary>
public byte[] Salt {
get;
set;
}
/// <summary>
/// The server's ephemeral public key.
/// </summary>
public Mpi PublicKey {
get;
set;
}
/// <summary>
/// The options list indicating available security services.
/// </summary>
public NameValueCollection Options {
get;
set;
}
/// <summary>
/// The raw options as received from the server.
/// </summary>
public string RawOptions {
get;
set;
}
/// <summary>
/// Deserializes a new instance of the ServerMessage1 class from the
/// specified buffer of bytes.
/// </summary>
/// <param name="buffer">The byte buffer to deserialize the ServerMessage1
/// instance from.</param>
/// <returns>An instance of the ServerMessage1 class deserialized from the
/// specified byte array.</returns>
/// <exception cref="FormatException">Thrown if the byte buffer does not
/// contain valid data.</exception>
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
};
}
}
}
/// <summary>
/// Parses the options string sent by the server.
/// </summary>
/// <param name="s">A comma-delimited options string.</param>
/// <returns>An initialized instance of the NameValueCollection class
/// containing the parsed server options.</returns>
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;
}
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.IO;
namespace S22.Sasl.Mechanisms.Srp {
/// <summary>
/// Represents the second message sent by the server as part of the SRP
/// authentication exchange.
/// </summary>
internal class ServerMessage2 {
/// <summary>
/// The evidence which proves to the client server-knowledge of the shared
/// context key.
/// </summary>
public byte[] Proof {
get;
set;
}
/// <summary>
/// The initial vector the client will use to set up its encryption
/// context, if confidentiality protection will be employed.
/// </summary>
public byte[] InitialVector {
get;
set;
}
/// <summary>
/// The session identifier the server has given to this session.
/// </summary>
public string SessionId {
get;
set;
}
/// <summary>
/// The time period for which this session's parameters may be re-usable.
/// </summary>
public uint Ttl {
get;
set;
}
/// <summary>
/// Deserializes a new instance of the ServerMessage2 class from the
/// specified buffer of bytes.
/// </summary>
/// <param name="buffer">The byte buffer to deserialize the ServerMessage2
/// instance from.</param>
/// <returns>An instance of the ServerMessage2 class deserialized from the
/// specified byte array.</returns>
/// <exception cref="FormatException">Thrown if the byte buffer does not
/// contain valid data.</exception>
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
};
}
}
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Text;
namespace S22.Sasl.Mechanisms.Srp {
/// <summary>
/// Represents an UTF-8 string as is described in the SRP specification
/// (3.5 Text, p.6).
/// </summary>
internal class Utf8String {
/// <summary>
/// The value of the UTF-8 string.
/// </summary>
public string Value;
/// <summary>
/// Creates a new instance of the Utf8String class using the specified
/// string value.
/// </summary>
/// <param name="s">The string to initialize the Utf8String instance
/// with.</param>
public Utf8String(string s) {
Value = s;
}
/// <summary>
/// Serializes this instance of the Utf8String class into a sequence of
/// bytes according to the requirements of the SRP specification.
/// </summary>
/// <returns>A sequence of bytes representing this instance of the
/// Utf8String class.</returns>
/// <exception cref="OverflowException">Thrown if the string value exceeds
/// the maximum number of bytes allowed as per SRP specification.</exception>
/// <remarks>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.</remarks>
public byte[] Serialize() {
byte[] b = Encoding.UTF8.GetBytes(Value);
ushort length = Convert.ToUInt16(b.Length);
return new ByteBuilder()
.Append(length, true)
.Append(b)
.ToArray();
}
}
}

View File

@ -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")]

70
Readme.md Normal file
View File

@ -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.

106
S22.Sasl.csproj Normal file
View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{B860646A-13A2-47D9-9790-4719A91BF35B}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>S22.Sasl</RootNamespace>
<AssemblyName>S22.Sasl</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>bin\Debug\S22.Sasl.XML</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\..\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Extensions.cs" />
<Compile Include="Mechanisms\ByteBuilder.cs" />
<Compile Include="Mechanisms\Ntlm\Extensions.cs" />
<Compile Include="Mechanisms\Ntlm\Flags.cs" />
<Compile Include="Mechanisms\Ntlm\Helpers.cs" />
<Compile Include="Mechanisms\Ntlm\MD4.cs" />
<Compile Include="Mechanisms\Ntlm\MessageType.cs" />
<Compile Include="Mechanisms\Ntlm\OSVersion.cs" />
<Compile Include="Mechanisms\Ntlm\Responses.cs" />
<Compile Include="Mechanisms\Ntlm\SecurityBuffer.cs" />
<Compile Include="Mechanisms\Ntlm\Type1Message.cs" />
<Compile Include="Mechanisms\Ntlm\Type2Message.cs" />
<Compile Include="Mechanisms\Ntlm\Type3Message.cs" />
<Compile Include="Mechanisms\SaslCramMd5.cs" />
<Compile Include="Mechanisms\SaslDigestMd5.cs" />
<Compile Include="Mechanisms\SaslNtlm.cs" />
<Compile Include="Mechanisms\SaslNtlmv2.cs" />
<Compile Include="Mechanisms\SaslOAuth.cs" />
<Compile Include="Mechanisms\SaslOAuth2.cs" />
<Compile Include="Mechanisms\SaslPlain.cs" />
<Compile Include="Mechanisms\SaslScramSha1.cs" />
<Compile Include="Mechanisms\SaslSrp.cs" />
<Compile Include="Mechanisms\Srp\ClientMessage1.cs" />
<Compile Include="Mechanisms\Srp\ClientMessage2.cs" />
<Compile Include="Mechanisms\Srp\Extensions.cs" />
<Compile Include="Mechanisms\Srp\Helper.cs" />
<Compile Include="Mechanisms\Srp\Mpi.cs" />
<Compile Include="Mechanisms\Srp\OctetSequence.cs" />
<Compile Include="Mechanisms\Srp\ServerMessage1.cs" />
<Compile Include="Mechanisms\Srp\ServerMessage2.cs" />
<Compile Include="Mechanisms\Srp\Utf8String.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SaslConfiguration.cs" />
<Compile Include="SaslException.cs" />
<Compile Include="SaslFactory.cs" />
<Compile Include="SaslMechanism.cs" />
<Compile Include="Tests\CramMd5Test.cs" />
<Compile Include="Tests\DigestMd5Test.cs" />
<Compile Include="Tests\NtlmTest.cs" />
<Compile Include="Tests\OAuth2Test.cs" />
<Compile Include="Tests\PlainTest.cs" />
<Compile Include="Tests\ScramSha1Test.cs" />
<Compile Include="Tests\SrpTest.cs" />
</ItemGroup>
<ItemGroup>
<None Include="License.md" />
<None Include="Readme.md" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

20
S22.Sasl.sln Normal file
View File

@ -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

175
SaslConfiguration.cs Normal file
View File

@ -0,0 +1,175 @@
using System.Configuration;
namespace S22.Sasl {
/// <summary>
/// Represents the sasl section within a configuration
/// file.
/// </summary>
public class SaslConfigurationSection : ConfigurationSection {
/// <summary>
/// The saslProviders section contains a collection of
/// saslProvider elements.
/// </summary>
[ConfigurationProperty("saslProviders", IsRequired = false,
IsKey = false, IsDefaultCollection = true)]
public SaslProviderCollection SaslProviders {
get {
return ((SaslProviderCollection) base["saslProviders"]);
}
set {
base["saslProviders"] = value;
}
}
}
/// <summary>
/// Represents a saslProvider configuration element within the
/// saslProviders section of a configuration file.
/// </summary>
[ConfigurationCollection(typeof(SaslProvider),
CollectionType=ConfigurationElementCollectionType.BasicMapAlternate)]
public class SaslProviderCollection : ConfigurationElementCollection {
/// <summary>
/// The name of the configuration element.
/// </summary>
internal const string PropertyName = "saslProvider";
/// <summary>
/// Gets the name used to identify this collection of elements
/// in the configuration file.
/// </summary>
protected override string ElementName {
get { return PropertyName; }
}
/// <summary>
/// Returns the SaslProvider instance for the saslProvider
/// element with the specified name.
/// </summary>
/// <param name="name">The name of the saslProvider element to
/// retrieve.</param>
/// <returns>The SaslProvider instance with the specified name
/// or null.</returns>
public new SaslProvider this[string name] {
get {
return (SaslProvider) BaseGet(name);
}
}
/// <summary>
/// Returns the SaslProvider instance for the saslProvider
/// element at the specified index.
/// </summary>
/// <param name="index">The index of the saslProvider element
/// to retrieve.</param>
/// <returns>The SaslProvider instance with the specified
/// index.</returns>
/// <exception cref="ConfigurationErrorsException">Thrown if the
/// index is less than 0 or if there is no SaslProvider instance
/// at the specified index.</exception>
public SaslProvider this[int index] {
get {
return (SaslProvider) BaseGet(index);
}
}
/// <summary>
/// Gets the collection type of the SaslProviderCollection.
/// </summary>
public override ConfigurationElementCollectionType CollectionType {
get {
return ConfigurationElementCollectionType.BasicMapAlternate;
}
}
/// <summary>
/// Indicates whether the specified System.Configuration.ConfigurationElement
/// exists in the SaslProviderCollection.
/// </summary>
/// <param name="elementName">The name of the element to verify.</param>
/// <returns>Returns true if the element exists in the collection,
/// otherwise false.</returns>
protected override bool IsElementName(string elementName) {
return elementName == PropertyName;
}
/// <summary>
/// Creates a new instance of the SaslProvider class.
/// </summary>
/// <returns>A new instance of the SaslProvider class.</returns>
protected override ConfigurationElement CreateNewElement() {
return new SaslProvider();
}
/// <summary>
/// Gets the element key for the specified SaslProvider element.
/// </summary>
/// <param name="element">A SaslProvider element to retrieve the
/// element key for.</param>
/// <returns>The unique element key of the specified SaslProvider
/// instance.</returns>
protected override object GetElementKey(ConfigurationElement element) {
return ((SaslProvider) element).Name;
}
}
/// <summary>
/// Represents a saslProvider section within the saslProviders
/// section of a configuration file.
/// </summary>
public class SaslProvider : ConfigurationSection {
/// <summary>
/// The name of the saslProvider. This attribute must be unique in
/// that no two saslProvider elements exists that have the same
/// name attribute.
/// </summary>
[ConfigurationProperty("name", IsRequired = true)]
public string Name {
get {
return (string) base["name"];
}
set {
base["name"] = value;
}
}
/// <summary>
/// The type name of the SaslMechanism exposed by the
/// saslProvider.
/// </summary>
[ConfigurationProperty("type", IsRequired = true)]
public string Type {
get {
return (string) base["type"];
}
set {
base["type"] = value;
}
}
/// <summary>
/// Retrieves the setting with the specified name for this saslProvider.
/// </summary>
/// <param name="name">The name of the setting to retrieve.</param>
/// <returns>The value of the setting with the specified name or null
/// if the setting could not be found.</returns>
public new string this[string name] {
get {
if (Settings[name] != null)
return Settings[name].Value;
return null;
}
}
/// <summary>
/// Represents a collection of arbitrary name-value pairs which can be
/// added to the saslProvider element.
/// </summary>
[ConfigurationProperty("", IsDefaultCollection = true)]
public NameValueConfigurationCollection Settings {
get {
return (NameValueConfigurationCollection) base[""];
}
}
}
}

41
SaslException.cs Normal file
View File

@ -0,0 +1,41 @@
using System;
using System.Runtime.Serialization;
namespace S22.Sasl {
/// <summary>
/// The exception is thrown when a Sasl-related error or unexpected condition occurs.
/// </summary>
[Serializable()]
internal class SaslException : Exception {
/// <summary>
/// Initializes a new instance of the SaslException class
/// </summary>
public SaslException() : base() { }
/// <summary>
/// Initializes a new instance of the SaslException class with its message
/// string set to <paramref name="message"/>.
/// </summary>
/// <param name="message">A description of the error. The content of message is intended
/// to be understood by humans.</param>
public SaslException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the SaslException class with its message
/// string set to <paramref name="message"/> and a reference to the inner exception that
/// is the cause of this exception.
/// </summary>
/// <param name="message">A description of the error. The content of message is intended
/// to be understood by humans.</param>
/// <param name="inner">The exception that is the cause of the current exception.</param>
public SaslException(string message, Exception inner) : base(message, inner) { }
/// <summary>
/// Initializes a new instance of the SaslException class with the specified
/// serialization and context information.
/// </summary>
/// <param name="info">An object that holds the serialized object data about the exception
/// being thrown. </param>
/// <param name="context">An object that contains contextual information about the source
/// or destination. </param>
protected SaslException(System.Runtime.Serialization.SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
}

83
SaslFactory.cs Normal file
View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
namespace S22.Sasl {
/// <summary>
/// A factory class for producing instances of Sasl mechanisms.
/// </summary>
internal static class SaslFactory {
/// <summary>
/// A dictionary of Sasl mechanisms registered with the factory class.
/// </summary>
static Dictionary<string, Type> Mechanisms {
get;
set;
}
/// <summary>
/// Creates an instance of the Sasl mechanism with the specified
/// name.
/// </summary>
/// <param name="name">The name of the Sasl mechanism of which an
/// instance will be created.</param>
/// <returns>An instance of the Sasl mechanism with the specified name.</returns>
/// <exception cref="ArgumentNullException">The name parameter is null.</exception>
/// <exception cref="SaslException">A Sasl mechanism with the
/// specified name is not registered with Sasl.SaslFactory.</exception>
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;
}
/// <summary>
/// Registers a Sasl mechanism with the factory using the specified name.
/// </summary>
/// <param name="name">The name with which to register the Sasl mechanism
/// with the factory class.</param>
/// <param name="t">The type of the class implementing the Sasl mechanism.
/// The implementing class must be a subclass of Sasl.SaslMechanism.</param>
/// <exception cref="ArgumentNullException">The name parameter or the t
/// parameter is null.</exception>
/// <exception cref="ArgumentException">The class represented by the
/// specified type does not derive from Sasl.SaslMechanism.</exception>
/// <exception cref="SaslException">The Sasl mechanism could not be
/// registered with the factory. Refer to the inner exception for error
/// details.</exception>
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);
}
}
/// <summary>
/// Static class constructor. Initializes static properties.
/// </summary>
static SaslFactory() {
Mechanisms = new Dictionary<string, Type>(
StringComparer.InvariantCultureIgnoreCase);
// Could be moved to App.config to support SASL "plug-in" mechanisms.
var list = new Dictionary<string, Type>() {
{ "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]);
}
}
}

93
SaslMechanism.cs Normal file
View File

@ -0,0 +1,93 @@
using System.Collections.Generic;
using System;
namespace S22.Sasl {
/// <summary>
/// The abstract base class from which all classes implementing a Sasl
/// authentication mechanism must derive.
/// </summary>
internal abstract class SaslMechanism {
/// <summary>
/// IANA name of the authentication mechanism.
/// </summary>
public abstract string Name {
get;
}
/// <summary>
/// True if the authentication exchange between client and server
/// has been completed.
/// </summary>
public abstract bool IsCompleted {
get;
}
/// <summary>
/// True if the mechanism requires initiation by the client.
/// </summary>
public abstract bool HasInitial {
get;
}
/// <summary>
/// A map of mechanism-specific properties which are needed by the
/// authentication mechanism to compute it's challenge-responses.
/// </summary>
public Dictionary<string, object> Properties {
get;
private set;
}
/// <summary>
/// Computes the client response to a challenge sent by the server.
/// </summary>
/// <param name="challenge"></param>
/// <returns>The client response to the specified challenge.</returns>
protected abstract byte[] ComputeResponse(byte[] challenge);
/// <summary>
/// </summary>
internal SaslMechanism() {
Properties = new Dictionary<string, object>();
}
/// <summary>
/// Retrieves the base64-encoded client response for the specified
/// base64-encoded challenge sent by the server.
/// </summary>
/// <param name="challenge">A base64-encoded string representing a challenge
/// sent by the server.</param>
/// <returns>A base64-encoded string representing the client response to the
/// server challenge.</returns>
/// <remarks>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.</remarks>
/// <exception cref="SaslException">The client response could not be retrieved.
/// Refer to the inner exception for error details.</exception>
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);
}
}
/// <summary>
/// Retrieves the client response for the specified server challenge.
/// </summary>
/// <param name="challenge">A byte array containing the challenge sent by
/// the server.</param>
/// <returns>An array of bytes representing the client response to the
/// server challenge.</returns>
public byte[] GetResponse(byte[] challenge) {
return ComputeResponse(challenge);
}
}
}

28
Tests/CramMd5Test.cs Normal file
View File

@ -0,0 +1,28 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S22.Sasl.Mechanisms;
using System.Text;
namespace S22.Sasl.Test {
/// <summary>
/// Contains unit tests for the SASL CRAM-MD5 authentication mechanism.
/// </summary>
[TestClass]
public class CramMd5Test {
/// <summary>
/// Verifies the various parts of a sample authentication exchange
/// directly taken from RFC 2195 ("A.1.1. Example 1", p. 6).
/// </summary>
[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<string>(expectedResponse, initialResponse);
}
}
}

36
Tests/DigestMd5Test.cs Normal file
View File

@ -0,0 +1,36 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S22.Sasl.Mechanisms;
using System;
using System.Text;
namespace S22.Sasl.Test {
/// <summary>
/// Contains unit tests for the SASL DIGEST-MD5 authentication mechanism.
/// </summary>
[TestClass]
public class DigestMd5Test {
/// <summary>
/// Verifies the various parts of a sample authentication exchange
/// directly taken from RFC 2831 ("4 Example", p. 17-18).
/// </summary>
[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<string>(expectedResponse, initialResponse);
string finalResponse = Encoding.ASCII.GetString(
m.GetResponse(Encoding.ASCII.GetBytes("rspauth=ea40f60335c427b5" +
"527b84dbabcdfffd")));
Assert.AreEqual<string>(String.Empty, finalResponse);
}
}
}

295
Tests/NtlmTest.cs Normal file
View File

@ -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 {
/// <summary>
/// Contains unit tests for the NTLM Sasl mechanism.
/// </summary>
[TestClass]
public class NtmlTest {
/// <summary>
/// Serializes an NTLM type 1 message and ensures the
/// serialized byte array is identical to expected byte
/// array.
/// </summary>
[TestMethod]
[TestCategory("NTLM")]
public void SerializeType1Message() {
Type1Message msg = new Type1Message("myDomain", "myWorkstation");
byte[] serialized = msg.Serialize();
Assert.IsTrue(type1Message.SequenceEqual(serialized));
}
/// <summary>
/// Deserializes an NTLM type 2 message and ensures the
/// deserialized instance contains valid data.
/// </summary>
[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>(Type2Version.Version2, msg.Version);
Assert.AreEqual<Flags>(expectedFlags, msg.Flags);
Assert.IsTrue(expectedChallenge.SequenceEqual(msg.Challenge));
Assert.AreEqual<long>(0, msg.Context);
Assert.AreEqual<string>("DOMAIN", msg.TargetName);
Assert.AreEqual<string>("DOMAIN",
msg.TargetInformation.DomainName);
Assert.AreEqual<string>("SERVER",
msg.TargetInformation.ServerName);
Assert.AreEqual<string>("domain.com",
msg.TargetInformation.DnsDomainName);
Assert.AreEqual<string>("server.domain.com",
msg.TargetInformation.DnsHostname);
}
/// <summary>
/// Deserializes an NTLM type 2 version 3 message and ensures the
/// deserialized instance contains valid data.
/// </summary>
[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>(Type2Version.Version3, msg.Version);
Assert.AreEqual<int>(42009093, (int)msg.Flags);
Assert.IsTrue(expectedChallenge.SequenceEqual(msg.Challenge));
Assert.AreEqual<long>(0, msg.Context);
Assert.AreEqual<string>("LOCALHOST", msg.TargetName);
Assert.AreEqual<string>("LOCALHOST",
msg.TargetInformation.DomainName);
Assert.AreEqual<string>("VMWARE-5T5GC9PU",
msg.TargetInformation.ServerName);
Assert.AreEqual<string>("localhost",
msg.TargetInformation.DnsDomainName);
Assert.AreEqual<string>("vmware-5t5gc9pu.localhost",
msg.TargetInformation.DnsHostname);
Assert.AreEqual<short>(3790, msg.OSVersion.BuildNumber);
Assert.AreEqual<short>(5, msg.OSVersion.MajorVersion);
Assert.AreEqual<short>(2, msg.OSVersion.MinorVersion);
}
/// <summary>
/// Serializes an NTLM type 3 message and ensures the
/// serialized byte array is identical to expected byte
/// array.
/// </summary>
[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));
}
/// <summary>
/// Verifies the various parts of a sample authentication exchange
/// (server challenge generated by MS Exchange Server 2003).
/// </summary>
[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));
}
/// <summary>
/// Verifies the various parts of a sample authentication exchange
/// (server challenge generated by the dovecot IMAP server).
/// </summary>
[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
}
}

28
Tests/OAuth2Test.cs Normal file
View File

@ -0,0 +1,28 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S22.Sasl.Mechanisms;
using System.Text;
namespace S22.Sasl.Test {
/// <summary>
/// Contains unit tests for the SASL XOAUTH2 authentication mechanism.
/// </summary>
[TestClass]
public class OAuth2Test {
/// <summary>
/// Verifies the various parts of a sample authentication exchange
/// directly taken from Google's "XOAUTH2 Mechanism" document
/// ("Initial Client Response").
/// </summary>
[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<string>(expectedResponse, initialResponse);
}
}
}

26
Tests/PlainTest.cs Normal file
View File

@ -0,0 +1,26 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S22.Sasl.Mechanisms;
using System.Text;
namespace S22.Sasl.Test {
/// <summary>
/// Contains unit tests for the SASL PLAIN authentication mechanism.
/// </summary>
[TestClass]
public class PlainTest {
/// <summary>
/// Verifies the various parts of a sample authentication exchange
/// directly taken from RFC 4616 ("4. Examples", p. 5).
/// </summary>
[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<string>(expectedResponse, initialResponse);
}
}
}

103
Tests/ScramSha1Test.cs Normal file
View File

@ -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 {
/// <summary>
/// Contains unit tests for the SASL SCRAM-SHA-1 authentication mechanism.
/// </summary>
[TestClass]
public class ScramSha1Test {
/// <summary>
/// Verifies the syntax of the client-first-message sent by the client to
/// initiate authentication.
/// </summary>
[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);
}
/// <summary>
/// Sends the client an illegal nonce value and verifies the client
/// subsequently raises an exception.
/// </summary>
[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);
}
/// <summary>
/// Verifies the various parts of a sample authentication exchange
/// directly taken from RFC 5802 ("SCRAM Authentication Exchange", p. 8).
/// </summary>
[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<string>(username, m.Groups[3].ToString());
Assert.AreEqual<string>(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<string>(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>(String.Empty, clientFinal);
}
/// <summary>
/// Helper method for conveniently converting the specified string to
/// Base64 using a decoding of UTF-8.
/// </summary>
/// <param name="s">The string to base64-encode.</param>
/// <returns>A base64-encoded string.</returns>
string ToBase64(string s) {
return Convert.ToBase64String(Encoding.UTF8.GetBytes(s));
}
/// <summary>
/// Helper method for conveniently decoding the specified base64-encoded
/// string using a decoding of UTF-8.
/// </summary>
/// <param name="s">The base64-encoded string to decode.</param>
/// <returns>A string constructed from the base64-decoded sequence
/// of bytes.</returns>
string FromBase64(string s) {
return Encoding.UTF8.GetString(Convert.FromBase64String(s));
}
}
}

449
Tests/SrpTest.cs Normal file
View File

@ -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 {
/// <summary>
/// Contains unit tests for the SASL SRP authentication mechanism.
/// </summary>
[TestClass]
public class SrpTest {
/// <summary>
/// Serializes an instance of the ClientMessage1 class and verifies the
/// serialized byte sequence is identical to the pre-computed expected
/// byte sequence.
/// </summary>
[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));
}
/// <summary>
/// Serializes an instance of the ClientMessage2 class and verifies the
/// serialized byte sequence is identical to the pre-computed expected
/// byte sequence.
/// </summary>
[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<string, string> p in clientOptions)
m.Options.Add(p.Key, p.Value);
byte[] serialized = m.Serialize();
Assert.IsTrue(serialized.SequenceEqual(expectedClientMessage2));
}
/// <summary>
/// Deserializes a byte sequence into an instance of the ServerMessage1
/// class and verifies the instance fields contain the expected values.
/// </summary>
[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<BigInteger>(expectedGenerator, m.Generator.Value);
Assert.AreEqual<BigInteger>(expectedModulus, m.SafePrimeModulus.Value);
Assert.AreEqual<BigInteger>(_expectedPublicKey, m.PublicKey.Value);
Assert.IsTrue(m.Salt.SequenceEqual(expectedSalt));
Assert.AreEqual<string>(expectedOptions, m.RawOptions);
Assert.AreEqual<int>(expectedParsedOptions.Count, m.Options.Count);
foreach (KeyValuePair<string, string> pair in expectedParsedOptions)
Assert.AreEqual<string>(pair.Value, m.Options[pair.Key]);
}
/// <summary>
/// Deserializes a byte sequence into an instance of the ServerMessage2
/// class and verifies the instance fields contain the expected values.
/// </summary>
[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>(String.Empty, m.SessionId);
Assert.AreEqual<uint>(0, m.Ttl);
}
/// <summary>
/// Verifies the various parts of a sample authentication exchange
/// (Challenge generated by the Cyrus Sasl library).
/// </summary>
/// <remarks>The exchange was generated with the authorization id
/// (authId) set to the same value as the username.</remarks>
[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<int>(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<string, string> clientOptions =
new Dictionary<string, string>() {
{ "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<string, string> expectedParsedOptions =
new Dictionary<string, string>() {
{ "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
}
}