mirror of
https://github.com/FabInfra/S22.Sasl.git
synced 2025-03-12 06:41:52 +01:00
Initial Commit
This commit is contained in:
commit
235d86668c
22
.gitattributes
vendored
Normal file
22
.gitattributes
vendored
Normal 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
167
.gitignore
vendored
Normal 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
55
Extensions.cs
Normal 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
22
License.md
Normal 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
195
Mechanisms/ByteBuilder.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
Mechanisms/Ntlm/Extensions.cs
Normal file
31
Mechanisms/Ntlm/Extensions.cs
Normal 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
139
Mechanisms/Ntlm/Flags.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
91
Mechanisms/Ntlm/Helpers.cs
Normal file
91
Mechanisms/Ntlm/Helpers.cs
Normal 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
146
Mechanisms/Ntlm/MD4.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
Mechanisms/Ntlm/MessageType.cs
Normal file
23
Mechanisms/Ntlm/MessageType.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
67
Mechanisms/Ntlm/OSVersion.cs
Normal file
67
Mechanisms/Ntlm/OSVersion.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
295
Mechanisms/Ntlm/Responses.cs
Normal file
295
Mechanisms/Ntlm/Responses.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
Mechanisms/Ntlm/SecurityBuffer.cs
Normal file
78
Mechanisms/Ntlm/SecurityBuffer.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
136
Mechanisms/Ntlm/Type1Message.cs
Normal file
136
Mechanisms/Ntlm/Type1Message.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
225
Mechanisms/Ntlm/Type2Message.cs
Normal file
225
Mechanisms/Ntlm/Type2Message.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
268
Mechanisms/Ntlm/Type3Message.cs
Normal file
268
Mechanisms/Ntlm/Type3Message.cs
Normal 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
124
Mechanisms/SaslCramMd5.cs
Normal 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
281
Mechanisms/SaslDigestMd5.cs
Normal 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
161
Mechanisms/SaslNtlm.cs
Normal 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
70
Mechanisms/SaslNtlmv2.cs
Normal 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
93
Mechanisms/SaslOAuth.cs
Normal 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
138
Mechanisms/SaslOAuth2.cs
Normal 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
119
Mechanisms/SaslPlain.cs
Normal 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
368
Mechanisms/SaslScramSha1.cs
Normal 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
287
Mechanisms/SaslSrp.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
95
Mechanisms/Srp/ClientMessage1.cs
Normal file
95
Mechanisms/Srp/ClientMessage1.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
115
Mechanisms/Srp/ClientMessage2.cs
Normal file
115
Mechanisms/Srp/ClientMessage2.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
Mechanisms/Srp/Extensions.cs
Normal file
89
Mechanisms/Srp/Extensions.cs
Normal 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
321
Mechanisms/Srp/Helper.cs
Normal 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
83
Mechanisms/Srp/Mpi.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
Mechanisms/Srp/OctetSequence.cs
Normal file
47
Mechanisms/Srp/OctetSequence.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
117
Mechanisms/Srp/ServerMessage1.cs
Normal file
117
Mechanisms/Srp/ServerMessage1.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
Mechanisms/Srp/ServerMessage2.cs
Normal file
72
Mechanisms/Srp/ServerMessage2.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
Mechanisms/Srp/Utf8String.cs
Normal file
45
Mechanisms/Srp/Utf8String.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
Properties/AssemblyInfo.cs
Normal file
36
Properties/AssemblyInfo.cs
Normal 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
70
Readme.md
Normal 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
106
S22.Sasl.csproj
Normal 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
20
S22.Sasl.sln
Normal 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
175
SaslConfiguration.cs
Normal 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
41
SaslException.cs
Normal 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
83
SaslFactory.cs
Normal 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
93
SaslMechanism.cs
Normal 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
28
Tests/CramMd5Test.cs
Normal 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
36
Tests/DigestMd5Test.cs
Normal 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
295
Tests/NtlmTest.cs
Normal 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
28
Tests/OAuth2Test.cs
Normal 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
26
Tests/PlainTest.cs
Normal 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
103
Tests/ScramSha1Test.cs
Normal 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
449
Tests/SrpTest.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user