| /* |
| * |
| * 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 |