summaryrefslogtreecommitdiff
path: root/qpid/dotnet/Qpid.Sasl/Mechanisms/DigestSaslClient.cs
diff options
context:
space:
mode:
Diffstat (limited to 'qpid/dotnet/Qpid.Sasl/Mechanisms/DigestSaslClient.cs')
-rw-r--r--qpid/dotnet/Qpid.Sasl/Mechanisms/DigestSaslClient.cs576
1 files changed, 576 insertions, 0 deletions
diff --git a/qpid/dotnet/Qpid.Sasl/Mechanisms/DigestSaslClient.cs b/qpid/dotnet/Qpid.Sasl/Mechanisms/DigestSaslClient.cs
new file mode 100644
index 0000000000..79843587c7
--- /dev/null
+++ b/qpid/dotnet/Qpid.Sasl/Mechanisms/DigestSaslClient.cs
@@ -0,0 +1,576 @@
+/*
+ *
+ * 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