mirror of
https://github.com/FabInfra/S22.Sasl.git
synced 2025-03-11 14:21:44 +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