using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace S22.Sasl.Mechanisms {
///
/// Implements the Sasl Cram-Md5 authentication method as described in
/// RFC 2831.
///
internal class SaslDigestMd5 : SaslMechanism {
bool Completed = false;
///
/// The client nonce value used during authentication.
///
string Cnonce = GenerateCnonce();
///
/// Cram-Md5 involves several steps.
///
int Step = 0;
///
/// The server sends the first message in the authentication exchange.
///
public override bool HasInitial {
get {
return false;
}
}
///
/// True if the authentication exchange between client and server
/// has been completed.
///
public override bool IsCompleted {
get {
return Completed;
}
}
///
/// The IANA name for the Digest-Md5 authentication mechanism as described
/// in RFC 2195.
///
public override string Name {
get {
return "DIGEST-MD5";
}
}
///
/// The username to authenticate with.
///
string Username {
get {
return Properties.ContainsKey("Username") ?
Properties["Username"] as string : null;
}
set {
Properties["Username"] = value;
}
}
///
/// The password to authenticate with.
///
string Password {
get {
return Properties.ContainsKey("Password") ?
Properties["Password"] as string : null;
}
set {
Properties["Password"] = value;
}
}
///
/// Private constructor for use with Sasl.SaslFactory.
///
private SaslDigestMd5() {
// Nothing to do here.
}
///
/// Internal constructor used for unit testing.
///
/// The username to authenticate with.
/// The plaintext password to authenticate
/// with.
/// The client nonce value to use.
/// Thrown if the username
/// or the password parameter is null.
/// Thrown if the username
/// parameter is empty.
internal SaslDigestMd5(string username, string password, string cnonce)
: this(username, password) {
Cnonce = cnonce;
}
///
/// Creates and initializes a new instance of the SaslDigestMd5 class
/// using the specified username and password.
///
/// The username to authenticate with.
/// The plaintext password to authenticate
/// with.
/// Thrown if the username
/// or the password parameter is null.
/// Thrown if the username
/// parameter is empty.
public SaslDigestMd5(string username, string password) {
username.ThrowIfNull("username");
if (username == String.Empty)
throw new ArgumentException("The username must not be empty.");
password.ThrowIfNull("password");
Username = username;
Password = password;
}
///
/// Computes the client response to the specified Digest-Md5 challenge.
///
/// The challenge sent by the server
/// The response to the Digest-Md5 challenge.
/// Thrown if the response could not
/// be computed.
protected override byte[] ComputeResponse(byte[] challenge) {
if (Step == 1)
Completed = true;
// If authentication succeeded, the server responds with another
// challenge (which we ignore) which the client must acknowledge
// with a CRLF.
byte[] ret = Step == 0 ? ComputeDigestResponse(challenge) :
new byte[0];
Step = Step + 1;
return ret;
}
private byte[] ComputeDigestResponse(byte[] challenge) {
// Precondition: Ensure username and password are not null and
// username is not empty.
if (String.IsNullOrEmpty(Username) || Password == null) {
throw new SaslException("The username must not be null or empty and " +
"the password must not be null.");
}
// Parse the challenge-string and construct the "response-value" from it.
string decoded = Encoding.ASCII.GetString(challenge);
NameValueCollection fields = ParseDigestChallenge(decoded);
string digestUri = "imap/" + fields["realm"];
string responseValue = ComputeDigestResponseValue(fields, Cnonce, digestUri,
Username, Password);
// Create the challenge-response string.
string[] directives = new string[] {
// We don't use UTF-8 in the current implementation.
//"charset=utf-8",
"username=" + Dquote(Username),
"realm=" + Dquote(fields["realm"]),
"nonce="+ Dquote(fields["nonce"]),
"nc=00000001",
"cnonce=" + Dquote(Cnonce),
"digest-uri=" + Dquote(digestUri),
"response=" + responseValue,
"qop=" + fields["qop"]
};
string challengeResponse = String.Join(",", directives);
// Finally, return the response as a byte array.
return Encoding.ASCII.GetBytes(challengeResponse);
}
///
/// Parses the challenge string sent by the server in response to a Digest-Md5
/// authentication request.
///
/// The challenge sent by the server as part of
/// "Step One" of the Digest-Md5 authentication mechanism.
/// An initialized NameValueCollection instance made up of the
/// attribute/value pairs contained in the challenge.
/// Thrown if the challenge parameter
/// is null.
/// Refer to RFC 2831 section 2.1.1 for a detailed description of the
/// format of the challenge sent by the server.
private static NameValueCollection ParseDigestChallenge(string challenge) {
challenge.ThrowIfNull("challenge");
NameValueCollection coll = new NameValueCollection();
string[] parts = challenge.Split(',');
foreach (string p in parts) {
string[] kv = p.Split(new char[] { '=' }, 2);
if (kv.Length == 2)
coll.Add(kv[0], kv[1].Trim('"'));
}
return coll;
}
///
/// Computes the "response-value" hex-string which is part of the
/// Digest-MD5 challenge-response.
///
/// A collection containing the attributes
/// and values of the challenge sent by the server.
/// The cnonce value to use for computing
/// the response-value.
/// The "digest-uri" string to use for
/// computing the response-value.
/// The username to use for computing the
/// response-value.
/// The password to use for computing the
/// response-value.
/// A string containing a hash-value which is part of the
/// response sent by the client.
/// Refer to RFC 2831, section 2.1.2.1 for a detailed
/// description of the computation of the response-value.
private static string ComputeDigestResponseValue(NameValueCollection challenge,
string cnonce, string digestUri, string username, string password) {
// The username, realm and password are encoded with ISO-8859-1
// (Compare RFC 2831, p. 10).
Encoding enc = Encoding.GetEncoding("ISO-8859-1");
string ncValue = "00000001", realm = challenge["realm"];
// Construct A1.
using (var md5p = new MD5CryptoServiceProvider()) {
byte[] data = enc.GetBytes(username + ":" + realm + ":" + password);
data = md5p.ComputeHash(data);
string A1 = enc.GetString(data) + ":" + challenge["nonce"] + ":" +
cnonce;
// Construct A2.
string A2 = "AUTHENTICATE:" + digestUri;
if (!"auth".Equals(challenge["qop"]))
A2 = A2 + ":00000000000000000000000000000000";
string ret = MD5(A1, enc) + ":" + challenge["nonce"] + ":" + ncValue +
":" + cnonce + ":" + challenge["qop"] + ":" + MD5(A2, enc);
return MD5(ret, enc);
}
}
///
/// Calculates the MD5 hash value for the specified string.
///
/// The string to calculate the MD5 hash value for.
/// The encoding to employ for encoding the
/// characters in the specified string into a sequence of bytes for
/// which the MD5 hash will be calculated.
/// An MD5 hash as a 32-character hex-string.
/// Thrown if the input string
/// is null.
private static string MD5(string s, Encoding encoding = null) {
if (s == null)
throw new ArgumentNullException("s");
if (encoding == null)
encoding = Encoding.UTF8;
byte[] data = encoding.GetBytes(s);
byte[] hash = (new MD5CryptoServiceProvider()).ComputeHash(data);
StringBuilder builder = new StringBuilder();
foreach (byte h in hash)
builder.Append(h.ToString("x2"));
return builder.ToString();
}
///
/// Encloses the specified string in double-quotes.
///
/// The string to enclose in double-quote characters.
/// The enclosed string.
private static string Dquote(string s) {
return "\"" + s + "\"";
}
///
/// Generates a random cnonce-value which is a "client-specified data string
/// which must be different each time a digest-response is sent".
///
/// A random "cnonce-value" string.
private static string GenerateCnonce() {
return Guid.NewGuid().ToString("N").Substring(0, 16);
}
}
}