576 lines
16 KiB
C#
576 lines
16 KiB
C#
/*
|
|
*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Specialized;
|
|
using System.Globalization;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace Apache.Qpid.Sasl.Mechanisms
|
|
{
|
|
|
|
/// <summary>
|
|
/// Implements the DIGEST MD5 authentication mechanism
|
|
/// as outlined in RFC 2831
|
|
/// </summary>
|
|
public class DigestSaslClient : SaslClient
|
|
{
|
|
public const string Mechanism = "DIGEST-MD5";
|
|
private static readonly MD5 _md5 = new MD5CryptoServiceProvider();
|
|
private int _state;
|
|
private string _cnonce;
|
|
private Encoding _encoding = Encoding.UTF8;
|
|
|
|
public string Cnonce
|
|
{
|
|
get { return _cnonce; }
|
|
set { _cnonce = value; }
|
|
}
|
|
|
|
public DigestSaslClient(
|
|
string authid, string serverName, string protocol,
|
|
IDictionary properties, ISaslCallbackHandler handler)
|
|
: base(authid, serverName, protocol, properties, handler)
|
|
{
|
|
_cnonce = Guid.NewGuid().ToString("N");
|
|
}
|
|
|
|
#region ISaslClient Implementation
|
|
//
|
|
// ISaslClient Implementation
|
|
//
|
|
|
|
public override string MechanismName
|
|
{
|
|
get { return Mechanism; }
|
|
}
|
|
|
|
public override bool HasInitialResponse
|
|
{
|
|
get { return false; }
|
|
}
|
|
|
|
public override byte[] EvaluateChallenge(byte[] challenge)
|
|
{
|
|
if ( challenge == null || challenge.Length <= 0 )
|
|
throw new ArgumentNullException("challenge");
|
|
|
|
switch ( _state++ )
|
|
{
|
|
case 0: return OnInitialChallenge(challenge);
|
|
case 1: return OnFinalResponse(challenge);
|
|
}
|
|
throw new SaslException("Invalid State for authentication");
|
|
}
|
|
|
|
#endregion // ISaslClient Implementation
|
|
|
|
|
|
#region Private Methods
|
|
//
|
|
// Private Methods
|
|
//
|
|
|
|
/// <summary>
|
|
/// Process the first challenge from the server
|
|
/// and calculate a response
|
|
/// </summary>
|
|
/// <param name="challenge">The server issued challenge</param>
|
|
/// <returns>Client response</returns>
|
|
private byte[] OnInitialChallenge(byte[] challenge)
|
|
{
|
|
DigestChallenge dch =
|
|
DigestChallenge.Parse(_encoding.GetString(challenge));
|
|
// validate input challenge
|
|
if ( dch.Nonce == null || dch.Nonce.Length == 0 )
|
|
throw new SaslException("Nonce value missing in server challenge");
|
|
if ( dch.Algorithm != "md5-sess" )
|
|
throw new SaslException("Invalid or missing algorithm value in server challenge");
|
|
|
|
|
|
NameCallback nameCB = new NameCallback(AuthorizationId);
|
|
PasswordCallback pwdCB = new PasswordCallback();
|
|
RealmCallback realmCB = new RealmCallback(dch.Realm);
|
|
ISaslCallback[] callbacks = { nameCB, pwdCB, realmCB };
|
|
Handler.Handle(callbacks);
|
|
|
|
DigestResponse response = new DigestResponse();
|
|
response.Username = nameCB.Text;
|
|
response.Realm = realmCB.Text;
|
|
response.Nonce = dch.Nonce;
|
|
response.Cnonce = Cnonce;
|
|
response.NonceCount = 1;
|
|
response.Qop = DigestQop.Auth; // only auth supported for now
|
|
response.DigestUri = Protocol.ToLower() + "/" + ServerName;
|
|
response.MaxBuffer = dch.MaxBuffer;
|
|
response.Charset = dch.Charset;
|
|
response.Cipher = null; // not supported for now
|
|
response.Authzid = AuthorizationId;
|
|
response.AuthParam = dch.AuthParam;
|
|
|
|
response.Response = CalculateResponse(
|
|
nameCB.Text, realmCB.Text, pwdCB.Text,
|
|
dch.Nonce, response.NonceCount, response.Qop, response.DigestUri
|
|
);
|
|
|
|
return _encoding.GetBytes(response.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process the second server challenge
|
|
/// </summary>
|
|
/// <param name="challenge">Server issued challenge</param>
|
|
/// <returns>The client response</returns>
|
|
private byte[] OnFinalResponse(byte[] challenge)
|
|
{
|
|
DigestChallenge dch =
|
|
DigestChallenge.Parse(_encoding.GetString(challenge));
|
|
|
|
if ( dch.Rspauth == null || dch.Rspauth.Length == 0 )
|
|
throw new SaslException("Expected 'rspauth' in server challenge not found");
|
|
|
|
SetComplete();
|
|
return new byte[0];
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Calculate the response field of the client response
|
|
/// </summary>
|
|
/// <param name="username">The user name</param>
|
|
/// <param name="realm">The realm</param>
|
|
/// <param name="passwd">The user's password</param>
|
|
/// <param name="nonce">Server nonce value</param>
|
|
/// <param name="nc">Client nonce count (always 1)</param>
|
|
/// <param name="qop">Quality of Protection</param>
|
|
/// <param name="digestUri">Digest-URI</param>
|
|
/// <returns>The value for the response field</returns>
|
|
private string CalculateResponse(
|
|
string username, string realm, string passwd,
|
|
string nonce, int nc, string qop, string digestUri
|
|
)
|
|
{
|
|
string a1 = CalcHexA1(username, realm, passwd, nonce);
|
|
string a2 = CalcHexA2(digestUri, qop);
|
|
|
|
string ncs = nc.ToString("x8", CultureInfo.InvariantCulture);
|
|
StringBuilder prekd = new StringBuilder();
|
|
prekd.Append(a1).Append(':').Append(nonce).Append(':')
|
|
.Append(ncs).Append(':').Append(Cnonce)
|
|
.Append(':').Append(qop).Append(':').Append(a2);
|
|
|
|
return ToHex(CalcH(_encoding.GetBytes(prekd.ToString())));
|
|
}
|
|
|
|
private string CalcHexA1(
|
|
string username, string realm,
|
|
string passwd, string nonce
|
|
)
|
|
{
|
|
bool hasAuthId = AuthorizationId != null && AuthorizationId.Length > 0;
|
|
|
|
string premd = username + ":" + realm + ":" + passwd;
|
|
byte[] temp1 = CalcH(_encoding.GetBytes(premd));
|
|
|
|
|
|
int a1len = 16 + 1 + nonce.Length + 1 + Cnonce.Length;
|
|
if ( hasAuthId )
|
|
a1len += 1 + AuthorizationId.Length;
|
|
|
|
byte[] buffer = new byte[a1len];
|
|
Array.Copy(temp1, buffer, temp1.Length);
|
|
|
|
string p2 = ":" + nonce + ":" + Cnonce;
|
|
if ( hasAuthId )
|
|
p2 += ":" + AuthorizationId;
|
|
|
|
byte[] temp2 = _encoding.GetBytes(p2);
|
|
Array.Copy(temp2, 0, buffer, 16, temp2.Length);
|
|
|
|
return ToHex(CalcH(buffer));
|
|
}
|
|
|
|
private string CalcHexA2(string digestUri, string qop)
|
|
{
|
|
string a2 = "AUTHENTICATE:" + digestUri;
|
|
if ( qop != DigestQop.Auth )
|
|
a2 += ":00000000000000000000000000000000";
|
|
return ToHex(CalcH(_encoding.GetBytes(a2)));
|
|
}
|
|
|
|
private static byte[] CalcH(byte[] value)
|
|
{
|
|
return _md5.ComputeHash(value);
|
|
}
|
|
|
|
#endregion // Private Methods
|
|
|
|
|
|
} // class DigestSaslClient
|
|
|
|
|
|
/// <summary>
|
|
/// Available QOP options in the DIGEST scheme
|
|
/// </summary>
|
|
public sealed class DigestQop
|
|
{
|
|
public const string Auth = "auth";
|
|
public const string AuthInt = "auth-int";
|
|
public const string AuthConf = "auth-conf";
|
|
} // class DigestQop
|
|
|
|
|
|
/// <summary>
|
|
/// Represents and parses a digest server challenge
|
|
/// </summary>
|
|
public class DigestChallenge
|
|
{
|
|
private string _realm = "localhost";
|
|
private string _nonce;
|
|
private string[] _qopOptions = { DigestQop.Auth };
|
|
private bool _stale;
|
|
private int _maxBuffer = 65536;
|
|
private string _charset = "ISO 8859-1";
|
|
private string _algorithm;
|
|
private string[] _cipherOptions;
|
|
private string _authParam;
|
|
private string _rspauth;
|
|
|
|
#region Properties
|
|
//
|
|
// Properties
|
|
//
|
|
|
|
public string Realm
|
|
{
|
|
get { return _realm; }
|
|
}
|
|
|
|
public string Nonce
|
|
{
|
|
get { return _nonce; }
|
|
}
|
|
|
|
public string[] QopOptions
|
|
{
|
|
get { return _qopOptions; }
|
|
}
|
|
|
|
public bool Stale
|
|
{
|
|
get { return _stale; }
|
|
}
|
|
|
|
public int MaxBuffer
|
|
{
|
|
get { return _maxBuffer; }
|
|
set { _maxBuffer = value; }
|
|
}
|
|
|
|
public string Charset
|
|
{
|
|
get { return _charset; }
|
|
}
|
|
|
|
public string Algorithm
|
|
{
|
|
get { return _algorithm; }
|
|
}
|
|
|
|
public string[] CipherOptions
|
|
{
|
|
get { return _cipherOptions; }
|
|
}
|
|
|
|
public string AuthParam
|
|
{
|
|
get { return _authParam; }
|
|
}
|
|
|
|
public string Rspauth
|
|
{
|
|
get { return _rspauth; }
|
|
}
|
|
|
|
#endregion // Properties
|
|
|
|
public static DigestChallenge Parse(string challenge)
|
|
{
|
|
DigestChallenge parsed = new DigestChallenge();
|
|
StringDictionary parts = ParseParameters(challenge);
|
|
foreach ( string optname in parts.Keys )
|
|
{
|
|
switch ( optname )
|
|
{
|
|
case "realm":
|
|
parsed._realm = parts[optname];
|
|
break;
|
|
case "nonce":
|
|
parsed._nonce = parts[optname];
|
|
break;
|
|
case "qop-options":
|
|
parsed._qopOptions = GetOptions(parts[optname]);
|
|
break;
|
|
case "cipher-opts":
|
|
parsed._cipherOptions = GetOptions(parts[optname]);
|
|
break;
|
|
case "stale":
|
|
parsed._stale = Convert.ToBoolean(parts[optname], CultureInfo.InvariantCulture);
|
|
break;
|
|
case "maxbuf":
|
|
parsed._maxBuffer = Convert.ToInt32(parts[optname], CultureInfo.InvariantCulture);
|
|
break;
|
|
case "charset":
|
|
parsed._charset = parts[optname];
|
|
break;
|
|
case "algorithm":
|
|
parsed._algorithm = parts[optname];
|
|
break;
|
|
case "auth-param":
|
|
parsed._authParam = parts[optname];
|
|
break;
|
|
case "rspauth":
|
|
parsed._rspauth = parts[optname];
|
|
break;
|
|
}
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
|
|
public static StringDictionary ParseParameters(string source)
|
|
{
|
|
if ( source == null )
|
|
throw new ArgumentNullException("source");
|
|
|
|
StringDictionary ret = new StringDictionary();
|
|
|
|
string remaining = source.Trim();
|
|
while ( remaining.Length > 0 )
|
|
{
|
|
int equals = remaining.IndexOf('=');
|
|
if ( equals < 0 )
|
|
break;
|
|
|
|
string optname = remaining.Substring(0, equals).Trim();
|
|
remaining = remaining.Substring(equals + 1);
|
|
|
|
string value = ParseQuoted(ref remaining);
|
|
ret[optname] = value.Trim();
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
private static string ParseQuoted(ref string str)
|
|
{
|
|
string ns = str.TrimStart();
|
|
|
|
int start = 0;
|
|
bool quoted = ns[0] == '\"';
|
|
if ( quoted ) start++;
|
|
bool inquotes = quoted;
|
|
bool escaped = false;
|
|
|
|
int pos = start;
|
|
for ( ; pos < ns.Length; pos++ )
|
|
{
|
|
if ( !inquotes && ns[pos] == ',' )
|
|
break;
|
|
|
|
// at end of quotes?
|
|
if ( quoted && !escaped && ns[pos] == '\"' )
|
|
inquotes = false;
|
|
// is this char an escape for the next one?
|
|
escaped = inquotes && ns[pos] == '\\';
|
|
}
|
|
// pos has end of string
|
|
string value = ns.Substring(start, pos-start).Trim();
|
|
if ( quoted )
|
|
{
|
|
// remove trailing quote
|
|
value = value.Substring(0, value.Length - 1);
|
|
}
|
|
str = ns.Substring(pos < ns.Length-1 ? pos+1 : pos);
|
|
return value;
|
|
}
|
|
|
|
private static string[] GetOptions(string value)
|
|
{
|
|
return value.Split(' ');
|
|
}
|
|
|
|
} // class DigestChallenge
|
|
|
|
|
|
/// <summary>
|
|
/// Represents and knows how to write a
|
|
/// digest client response
|
|
/// </summary>
|
|
public class DigestResponse
|
|
{
|
|
private string _username;
|
|
private string _realm;
|
|
private string _nonce;
|
|
private string _cnonce;
|
|
private int _nonceCount;
|
|
private string _qop;
|
|
private string _digestUri;
|
|
private string _response;
|
|
private int _maxBuffer;
|
|
private string _charset;
|
|
private string _cipher;
|
|
private string _authzid;
|
|
private string _authParam;
|
|
|
|
#region Properties
|
|
//
|
|
// Properties
|
|
//
|
|
|
|
public string Username
|
|
{
|
|
get { return _username; }
|
|
set { _username = value; }
|
|
}
|
|
|
|
public string Realm
|
|
{
|
|
get { return _realm; }
|
|
set { _realm = value; }
|
|
}
|
|
|
|
public string Nonce
|
|
{
|
|
get { return _nonce; }
|
|
set { _nonce = value; }
|
|
}
|
|
|
|
public string Cnonce
|
|
{
|
|
get { return _cnonce; }
|
|
set { _cnonce = value; }
|
|
}
|
|
|
|
public int NonceCount
|
|
{
|
|
get { return _nonceCount; }
|
|
set { _nonceCount = value; }
|
|
}
|
|
|
|
public string Qop
|
|
{
|
|
get { return _qop; }
|
|
set { _qop = value; }
|
|
}
|
|
|
|
public string DigestUri
|
|
{
|
|
get { return _digestUri; }
|
|
set { _digestUri = value; }
|
|
}
|
|
|
|
public string Response
|
|
{
|
|
get { return _response; }
|
|
set { _response = value; }
|
|
}
|
|
|
|
public int MaxBuffer
|
|
{
|
|
get { return _maxBuffer; }
|
|
set { _maxBuffer = value; }
|
|
}
|
|
|
|
public string Charset
|
|
{
|
|
get { return _charset; }
|
|
set { _charset = value; }
|
|
}
|
|
|
|
public string Cipher
|
|
{
|
|
get { return _cipher; }
|
|
set { _cipher = value; }
|
|
}
|
|
|
|
public string Authzid
|
|
{
|
|
get { return _authzid; }
|
|
set { _authzid = value; }
|
|
}
|
|
|
|
public string AuthParam
|
|
{
|
|
get { return _authParam; }
|
|
set { _authParam = value; }
|
|
}
|
|
|
|
#endregion // Properties
|
|
|
|
|
|
public override string ToString()
|
|
{
|
|
StringBuilder buffer = new StringBuilder();
|
|
Pair(buffer, "username", Username, true);
|
|
Pair(buffer, "realm", Realm, true);
|
|
Pair(buffer, "nonce", Nonce, true);
|
|
Pair(buffer, "cnonce", Cnonce, true);
|
|
string nc = NonceCount.ToString("x8", CultureInfo.InvariantCulture);
|
|
Pair(buffer, "nc", nc, false);
|
|
Pair(buffer, "qop", Qop, false);
|
|
Pair(buffer, "digest-uri", DigestUri, true);
|
|
Pair(buffer, "response", Response, true);
|
|
string maxBuffer = MaxBuffer.ToString(CultureInfo.InvariantCulture);
|
|
Pair(buffer, "maxbuf", maxBuffer, false);
|
|
Pair(buffer, "charset", Charset, false);
|
|
Pair(buffer, "cipher", Cipher, false);
|
|
Pair(buffer, "authzid", Authzid, true);
|
|
Pair(buffer, "auth-param", AuthParam, true);
|
|
|
|
return buffer.ToString().TrimEnd(',');
|
|
}
|
|
|
|
private static void Pair(StringBuilder buffer, string name, string value, bool quoted)
|
|
{
|
|
if ( value != null && value.Length > 0 )
|
|
{
|
|
buffer.Append(name);
|
|
buffer.Append('=');
|
|
if ( quoted )
|
|
{
|
|
buffer.Append('\"');
|
|
buffer.Append(value.Replace("\"", "\\\""));
|
|
buffer.Append('\"');
|
|
} else
|
|
{
|
|
buffer.Append(value);
|
|
}
|
|
buffer.Append(',');
|
|
}
|
|
}
|
|
|
|
} // class DigestResponse
|
|
|
|
} // namespace Apache.Qpid.Sasl.Mechanisms
|