/* * * 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 { /// /// Implements the DIGEST MD5 authentication mechanism /// as outlined in RFC 2831 /// 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 // /// /// Process the first challenge from the server /// and calculate a response /// /// The server issued challenge /// Client response 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()); } /// /// Process the second server challenge /// /// Server issued challenge /// The client response 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]; } /// /// Calculate the response field of the client response /// /// The user name /// The realm /// The user's password /// Server nonce value /// Client nonce count (always 1) /// Quality of Protection /// Digest-URI /// The value for the response field 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 /// /// Available QOP options in the DIGEST scheme /// public sealed class DigestQop { public const string Auth = "auth"; public const string AuthInt = "auth-int"; public const string AuthConf = "auth-conf"; } // class DigestQop /// /// Represents and parses a digest server challenge /// 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 /// /// Represents and knows how to write a /// digest client response /// 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