using System;
using System.Collections.Generic;
using System.Text;

namespace LumiSoft.Net.AUTH
{
    /// <summary>
    /// This class represents SASL DIGEST-MD5 authentication <b>digest-response</b>. Defined in RFC 2831.
    /// </summary>
    public class AUTH_SASL_DigestMD5_Response
    {
        private AUTH_SASL_DigestMD5_Challenge m_pChallenge = null;
        private string                        m_UserName   = null;
        private string                        m_Password   = null;
        private string                        m_Realm      = null;
        private string                        m_Nonce      = null;
        private string                        m_Cnonce     = null;
        private int                           m_NonceCount = 0;
        private string                        m_Qop        = null;
        private string                        m_DigestUri  = null;
        private string                        m_Response   = null;
        private string                        m_Charset    = null;
        private string                        m_Cipher     = null;
        private string                        m_Authzid    = null;

        /// <summary>
        /// Default constructor.
        /// </summary>
        /// <param name="challenge">Client challenge.</param>
        /// <param name="realm">Realm value. This must be one value of the challenge Realm.</param>
        /// <param name="userName">User name.</param>
        /// <param name="password">User password.</param>
        /// <param name="cnonce">Client nonce value.</param>
        /// <param name="nonceCount">Nonce count. One-based client authentication attempt number. Normally this value is 1.</param>
        /// <param name="qop">Indicates what "quality of protection" the client accepted. This must be one value of the challenge QopOptions.</param>
        /// <param name="digestUri">Digest URI.</param>
        /// <exception cref="ArgumentNullException">Is raised when <b>challenge</b>,<b>realm</b>,<b>password</b>,<b>nonce</b>,<b>qop</b> or <b>digestUri</b> is null reference.</exception>
        public AUTH_SASL_DigestMD5_Response(AUTH_SASL_DigestMD5_Challenge challenge,string realm,string userName,string password,string cnonce,int nonceCount,string qop,string digestUri)
        {    
            if(challenge == null){
                throw new ArgumentNullException("challenge");
            }
            if(realm == null){
                throw new ArgumentNullException("realm");
            }
            if(userName == null){
                throw new ArgumentNullException("userName");
            }
            if(password == null){
                throw new ArgumentNullException("password");
            }
            if(cnonce == null){
                throw new ArgumentNullException("cnonce");
            }
            if(qop == null){
                throw new ArgumentNullException("qop");
            }
            if(digestUri == null){
                throw new ArgumentNullException("digestUri");
            }

            m_pChallenge = challenge;
            m_Realm      = realm;
            m_UserName   = userName;
            m_Password   = password;
            m_Nonce      = m_pChallenge.Nonce;
            m_Cnonce     = cnonce;
            m_NonceCount = nonceCount;
            m_Qop        = qop;
            m_DigestUri  = digestUri;                        
            m_Response   = CalculateResponse(userName,password);
            m_Charset    = challenge.Charset;
        }

        /// <summary>
        /// Internal parse constructor.
        /// </summary>
        private AUTH_SASL_DigestMD5_Response()
        {
        }


        #region static method Parse

        /// <summary>
        /// Parses DIGEST-MD5 response from response-string.
        /// </summary>
        /// <param name="digestResponse">Response string.</param>
        /// <returns>Returns DIGEST-MD5 response.</returns>
        /// <exception cref="ArgumentNullException">Is raised when <b>digestResponse</b> isnull reference.</exception>
        /// <exception cref="ParseException">Is raised when response parsing + validation fails.</exception>
        public static AUTH_SASL_DigestMD5_Response Parse(string digestResponse)
        {
            if(digestResponse == null){
                throw new ArgumentNullException(digestResponse);
            }

            /* RFC 2831 2.1.2.
                The client makes note of the "digest-challenge" and then responds
                with a string formatted and computed according to the rules for a
                "digest-response" defined as follows:

                digest-response  = 1#( username | realm | nonce | cnonce |
                                       nonce-count | qop | digest-uri | response |
                                       maxbuf | charset | cipher | authzid |
                                       auth-param )

                username         = "username" "=" <"> username-value <">
                username-value   = qdstr-val
                cnonce           = "cnonce" "=" <"> cnonce-value <">
                cnonce-value     = qdstr-val
                nonce-count      = "nc" "=" nc-value
                nc-value         = 8LHEX
                qop              = "qop" "=" qop-value
                digest-uri       = "digest-uri" "=" <"> digest-uri-value <">
                digest-uri-value  = serv-type "/" host [ "/" serv-name ]
                serv-type        = 1*ALPHA
                host             = 1*( ALPHA | DIGIT | "-" | "." )
                serv-name        = host
                response         = "response" "=" response-value
                response-value   = 32LHEX
                LHEX             = "0" | "1" | "2" | "3" |
                                   "4" | "5" | "6" | "7" |
                                   "8" | "9" | "a" | "b" |
                                   "c" | "d" | "e" | "f"
                cipher           = "cipher" "=" cipher-value
                authzid          = "authzid" "=" <"> authzid-value <">
                authzid-value    = qdstr-val
            */    
            
            AUTH_SASL_DigestMD5_Response retVal = new AUTH_SASL_DigestMD5_Response();

            // Set default values.
            retVal.m_Realm = "";

            string[] parameters = TextUtils.SplitQuotedString(digestResponse,',');
            foreach(string parameter in parameters){
                string[] name_value = parameter.Split(new char[]{'='},2);
                string   name       = name_value[0].Trim();

                if(name_value.Length == 2){
                    if(name.ToLower() == "username"){
                        retVal.m_UserName = TextUtils.UnQuoteString(name_value[1]);
                    }
                    else if(name.ToLower() == "realm"){
                        retVal.m_Realm = TextUtils.UnQuoteString(name_value[1]);
                    }
                    else if(name.ToLower() == "nonce"){            
                        retVal.m_Nonce = TextUtils.UnQuoteString(name_value[1]);
                    }
                    else if(name.ToLower() == "cnonce"){
                        retVal.m_Cnonce = TextUtils.UnQuoteString(name_value[1]);
                    }
                    else if(name.ToLower() == "nc"){
                        retVal.m_NonceCount = Int32.Parse(TextUtils.UnQuoteString(name_value[1]),System.Globalization.NumberStyles.HexNumber);
                    }
                    else if(name.ToLower() == "qop"){
                        retVal.m_Qop = TextUtils.UnQuoteString(name_value[1]);
                    }
                    else if(name.ToLower() == "digest-uri"){
                        retVal.m_DigestUri = TextUtils.UnQuoteString(name_value[1]);
                    }
                    else if(name.ToLower() == "response"){
                        retVal.m_Response = TextUtils.UnQuoteString(name_value[1]);
                    }
                    else if(name.ToLower() == "charset"){
                        retVal.m_Charset = TextUtils.UnQuoteString(name_value[1]);
                    }
                    else if(name.ToLower() == "cipher"){
                        retVal.m_Cipher = TextUtils.UnQuoteString(name_value[1]);
                    }
                    else if(name.ToLower() == "authzid"){
                        retVal.m_Authzid = TextUtils.UnQuoteString(name_value[1]);
                    }
                }
            }

            /* Validate required fields.
                Per RFC 2831 2.1.2. Only [username nonce cnonce nc response] parameters are required.
            */
            if(string.IsNullOrEmpty(retVal.UserName)){
                throw new ParseException("The response-string doesn't contain required parameter 'username' value.");
            }
            if(string.IsNullOrEmpty(retVal.Nonce)){
                throw new ParseException("The response-string doesn't contain required parameter 'nonce' value.");
            }
            if(string.IsNullOrEmpty(retVal.Cnonce)){
                throw new ParseException("The response-string doesn't contain required parameter 'cnonce' value.");
            }            
            if(retVal.NonceCount < 1){
                throw new ParseException("The response-string doesn't contain required parameter 'nc' value.");
            }
            if(string.IsNullOrEmpty(retVal.Response)){
                throw new ParseException("The response-string doesn't contain required parameter 'response' value.");
            }

            return retVal;
        }

        #endregion


        #region method Authenticate

        /// <summary>
        /// Authenticates user.
        /// </summary>
        /// <param name="userName">User name.</param>
        /// <param name="password">Password.</param>
        /// <returns>Returns true if user authenticated, otherwise false.</returns>
        /// <exception cref="ArgumentNullException">Is raised when <b>userName</b> or <b>password</b> is null reference.</exception>
        public bool Authenticate(string userName,string password)
        {
            if(userName == null){
                throw new ArgumentNullException("userName");
            }
            if(password == null){
                throw new ArgumentNullException("password");
            }

            if(this.Response == CalculateResponse(userName,password)){
                return true;
            }

            return false;
        }

        #endregion


        #region method ToResponse

        /// <summary>
        /// Creates digest response for challenge.
        /// </summary>
        /// <returns>Returns digest response.</returns>
        public string ToResponse()
        {
            /* RFC 2831 2.1.2.
                The client makes note of the "digest-challenge" and then responds
                with a string formatted and computed according to the rules for a
                "digest-response" defined as follows:

                digest-response  = 1#( username | realm | nonce | cnonce |
                                       nonce-count | qop | digest-uri | response |
                                       maxbuf | charset | cipher | authzid |
                                       auth-param )

                username         = "username" "=" <"> username-value <">
                username-value   = qdstr-val
                cnonce           = "cnonce" "=" <"> cnonce-value <">
                cnonce-value     = qdstr-val
                nonce-count      = "nc" "=" nc-value
                nc-value         = 8LHEX
                qop              = "qop" "=" qop-value
                digest-uri       = "digest-uri" "=" <"> digest-uri-value <">
                digest-uri-value  = serv-type "/" host [ "/" serv-name ]
                serv-type        = 1*ALPHA
                host             = 1*( ALPHA | DIGIT | "-" | "." )
                serv-name        = host
                response         = "response" "=" response-value
                response-value   = 32LHEX
                LHEX             = "0" | "1" | "2" | "3" |
                                   "4" | "5" | "6" | "7" |
                                   "8" | "9" | "a" | "b" |
                                   "c" | "d" | "e" | "f"
                cipher           = "cipher" "=" cipher-value
                authzid          = "authzid" "=" <"> authzid-value <">
                authzid-value    = qdstr-val
            */

            StringBuilder retVal = new StringBuilder();
            retVal.Append("username=\"" + this.UserName + "\"");
            retVal.Append(",realm=\"" + this.Realm + "\"");
            retVal.Append(",nonce=\"" + this.Nonce + "\"");
            retVal.Append(",cnonce=\"" + this.Cnonce + "\"");
            retVal.Append(",nc=" + this.NonceCount.ToString("x8"));
            retVal.Append(",qop=" + this.Qop);
            retVal.Append(",digest-uri=\"" + this.DigestUri + "\"");
            retVal.Append(",response=" + this.Response);
            if(!string.IsNullOrEmpty(this.Charset)){
                retVal.Append(",charset=" + this.Charset);
            }
            if(!string.IsNullOrEmpty(this.Cipher)){
                retVal.Append(",cipher=\"" + this.Cipher + "\"");
            }
            if(!string.IsNullOrEmpty(this.Authzid)){
                retVal.Append(",authzid=\"" + this.Authzid + "\"");
            }
            // auth-param

            return retVal.ToString();
        }

        #endregion

        #region method ToRspauthResponse

        /// <summary>
        /// Creates <b>response-auth</b> response for client.
        /// </summary>
        /// <returns>Returns <b>response-auth</b> response.</returns>
        public string ToRspauthResponse(string userName,string password)
        {
            /* RFC 2831 2.1.3.
                The server receives and validates the "digest-response". The server
                checks that the nonce-count is "00000001". If it supports subsequent
                authentication (see section 2.2), it saves the value of the nonce and
                the nonce-count. It sends a message formatted as follows:

                    response-auth = "rspauth" "=" response-value

                where response-value is calculated as above, using the values sent in
                step two, except that if qop is "auth", then A2 is

                    A2 = { ":", digest-uri-value }

                And if qop is "auth-int" or "auth-conf" then A2 is

                    A2 = { ":", digest-uri-value, ":00000000000000000000000000000000" }

                Compared to its use in HTTP, the following Digest directives in the
                "digest-response" are unused:

                    nextnonce
                    qop
                    cnonce
                    nonce-count
             
                response-value  =
                    HEX( KD ( HEX(H(A1)),
                        { nonce-value, ":" nc-value, ":", cnonce-value, ":", qop-value, ":", HEX(H(A2)) }))
            */

            byte[] a2 = null;
            if(string.IsNullOrEmpty(this.Qop) || this.Qop.ToLower() == "auth"){
                a2 = Encoding.UTF8.GetBytes(":" + this.DigestUri);
            }
            else if(this.Qop.ToLower() == "auth-int" || this.Qop.ToLower() == "auth-conf"){
                a2 = Encoding.UTF8.GetBytes(":" + this.DigestUri + ":00000000000000000000000000000000");
            }            

            if(this.Qop.ToLower() == "auth"){
                // RFC 2831 2.1.2.1.
                // response-value = HEX(KD(HEX(H(A1)),{nonce-value,":" nc-value,":",cnonce-value,":",qop-value,":",HEX(H(A2))}))

                return "rspauth=" + hex(kd(hex(h(a1(userName,password))),m_Nonce + ":" + this.NonceCount.ToString("x8") + ":" + this.Cnonce + ":" + this.Qop + ":" + hex(h(a2))));
            }
            else{
                throw new ArgumentException("Invalid 'qop' value '" + this.Qop + "'.");
            }            
        }

        #endregion


        #region method CalculateResponse

        /// <summary>
        /// Calculates digest response.
        /// </summary>
        /// <param name="userName">User name.</param>
        /// <param name="password">Password.</param>
        /// <returns>Returns digest response.</returns>
        private string CalculateResponse(string userName,string password)
        {
            /* RFC 2831.2.1.2.1.
                The definition of "response-value" above indicates the encoding for
                its value -- 32 lower case hex characters. The following definitions
                show how the value is computed.

                Although qop-value and components of digest-uri-value may be
                case-insensitive, the case which the client supplies in step two is
                preserved for the purpose of computing and verifying the
                response-value.

                response-value  =
                    HEX( KD ( HEX(H(A1)),
                        { nonce-value, ":" nc-value, ":", cnonce-value, ":", qop-value, ":", HEX(H(A2)) }))

                If authzid is specified, then A1 is

                    A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
                        ":", nonce-value, ":", cnonce-value, ":", authzid-value }

                If authzid is not specified, then A1 is

                    A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
                        ":", nonce-value, ":", cnonce-value }

                The "username-value", "realm-value" and "passwd" are encoded
                according to the value of the "charset" directive. If "charset=UTF-8"
                is present, and all the characters of either "username-value" or
                "passwd" are in the ISO 8859-1 character set, then it must be
                converted to ISO 8859-1 before being hashed. This is so that
                authentication databases that store the hashed username, realm and
                password (which is common) can be shared compatibly with HTTP, which
                specifies ISO 8859-1. A sample implementation of this conversion is
                in section 8.

                If the "qop" directive's value is "auth", then A2 is:

                    A2       = { "AUTHENTICATE:", digest-uri-value }

                If the "qop" value is "auth-int" or "auth-conf" then A2 is:

                    A2       = { "AUTHENTICATE:", digest-uri-value,
                                ":00000000000000000000000000000000" }

                Note that "AUTHENTICATE:" must be in upper case, and the second
                string constant is a string with a colon followed by 32 zeros.

                These apparently strange values of A2 are for compatibility with
                HTTP; they were arrived at by setting "Method" to "AUTHENTICATE" and
                the hash of the entity body to zero in the HTTP digest calculation of
                A2.

                Also, in the HTTP usage of Digest, several directives in the

                "digest-challenge" sent by the server have to be returned by the
                client in the "digest-response". These are:

                    opaque
                    algorithm

                These directives are not needed when Digest is used as a SASL
                mechanism (i.e., MUST NOT be sent, and MUST be ignored if received).
            */
                        
            if(string.IsNullOrEmpty(this.Qop) || this.Qop.ToLower() == "auth"){
                // RFC 2831 2.1.2.1.
                // response-value = HEX(KD(HEX(H(A1)),{nonce-value,":" nc-value,":",cnonce-value,":",qop-value,":",HEX(H(A2))}))

                return hex(kd(hex(h(a1(userName,password))),m_Nonce + ":" + this.NonceCount.ToString("x8") + ":" + this.Cnonce + ":" + this.Qop + ":" + hex(h(a2()))));
            }
            else{
                throw new ArgumentException("Invalid 'qop' value '" + this.Qop + "'.");
            }
        }

        #endregion

        #region method a1

        /// <summary>
        /// Calculates A1 value.
        /// </summary>
        /// <param name="userName">User name.</param>
        /// <param name="password">Password.</param>
        /// <returns>Returns A1 value.</returns>
        private byte[] a1(string userName,string password)
        {   
            /* RFC 2831 2.1.2.1.
                If authzid is specified, then A1 is

                A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
                      ":", nonce-value, ":", cnonce-value, ":", authzid-value }

                If authzid is not specified, then A1 is

                A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
                      ":", nonce-value, ":", cnonce-value
             
                NOTE: HTTP MD5 RFC 2617 supports more algorithms. SASL requires md5-sess.
            */
  
            if(string.IsNullOrEmpty(this.Authzid)){
                byte[] user_realm_pwd = h(Encoding.UTF8.GetBytes(userName + ":" + this.Realm + ":" + password));
                byte[] nonce_cnonce   = Encoding.UTF8.GetBytes(":" + m_Nonce + ":" + this.Cnonce);

                byte[] retVal = new byte[user_realm_pwd.Length + nonce_cnonce.Length];
                Array.Copy(user_realm_pwd,0,retVal,0,user_realm_pwd.Length);
                Array.Copy(nonce_cnonce,0,retVal,user_realm_pwd.Length,nonce_cnonce.Length);

                return retVal;
            }
            else{
                byte[] user_realm_pwd       = h(Encoding.UTF8.GetBytes(userName + ":" + this.Realm + ":" + password));
                byte[] nonce_cnonce_authzid = Encoding.UTF8.GetBytes(":" + m_Nonce + ":" + this.Cnonce + ":" + this.Authzid);

                byte[] retVal = new byte[user_realm_pwd.Length + nonce_cnonce_authzid.Length];
                Array.Copy(user_realm_pwd,0,retVal,0,user_realm_pwd.Length);
                Array.Copy(nonce_cnonce_authzid,0,retVal,user_realm_pwd.Length,nonce_cnonce_authzid.Length);

                return retVal;
            }
        }

        #endregion

        #region method a2

        /// <summary>
        /// Calculates A2 value.
        /// </summary>
        /// <returns>Returns A2 value.</returns>
        private byte[] a2()
        {
            /* RFC 2831 2.1.2.1.
                If the "qop" directive's value is "auth", then A2 is:

                    A2       = { "AUTHENTICATE:", digest-uri-value }

                If the "qop" value is "auth-int" or "auth-conf" then A2 is:

                    A2       = { "AUTHENTICATE:", digest-uri-value, ":00000000000000000000000000000000" }

                Note that "AUTHENTICATE:" must be in upper case, and the second
                string constant is a string with a colon followed by 32 zeros.
             
                RFC 2617(HTTP MD5) 3.2.2.3.
                    A2       = Method ":" digest-uri-value ":" H(entity-body)

                NOTE: In SASL entity-body hash always "00000000000000000000000000000000".
            */

            if(string.IsNullOrEmpty(this.Qop) || this.Qop.ToLower() == "auth"){
                return Encoding.UTF8.GetBytes("AUTHENTICATE:" + this.DigestUri);
            }
            else if(this.Qop.ToLower() == "auth-int" || this.Qop.ToLower() == "auth-conf"){
                return Encoding.UTF8.GetBytes("AUTHENTICATE:" + this.DigestUri + ":00000000000000000000000000000000");
            }
            else{
                throw new ArgumentException("Invalid 'qop' value '" + this.Qop + "'.");
            }
        }

        #endregion

        #region method h

        /// <summary>
        /// Computes MD5 hash.
        /// </summary>
        /// <param name="value">Value to process.</param>
        /// <returns>Return MD5 hash.</returns>
        private byte[] h(byte[] value)
        {
            using(System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider()){

                return md5.ComputeHash(value);
            }
        }

        #endregion

        #region method kd

        private byte[] kd(string secret,string data)
        {
            // KD(secret, data) = H(concat(secret, ":", data))

            return h(Encoding.UTF8.GetBytes(secret + ":" + data));
        }

        #endregion

        #region method hex

        /// <summary>
        /// Converts value to hex string.
        /// </summary>
        /// <param name="value">Value to convert.</param>
        /// <returns>Returns hex string.</returns>
        private string hex(byte[] value)
        {
            return Net_Utils.ToHex(value);
        }

        #endregion


        #region Properties implementation
                
        /// <summary>
        /// Gets user name.
        /// </summary>
        public string UserName
        {
            get{ return m_UserName; }
        }

        /// <summary>
        /// Gets realm(domain) name.
        /// </summary>
        public string Realm
        {
            get{ return m_Realm; }
        }

        /// <summary>
        /// Gets nonce value.
        /// </summary>
        public string Nonce
        {
            get{ return m_Nonce; }
        }

        /// <summary>
        /// Gets cnonce value.
        /// </summary>
        public string Cnonce
        {
            get{ return m_Cnonce; }
        }

        /// <summary>
        /// Gets nonce count.
        /// </summary>
        public int NonceCount
        {
            get{ return m_NonceCount; }
        }

        /// <summary>
        /// Gets "quality of protection" value.
        /// </summary>
        public string Qop
        {
            get{ return m_Qop; }
        }

        /// <summary>
        /// Gets digest URI value.
        /// </summary>
        public string DigestUri
        {
            get{ return m_DigestUri; }
        }

        /// <summary>
        /// Gets response value.
        /// </summary>
        public string Response
        {
            get{ return m_Response; }
        }

        /// <summary>
        /// Gets charset value.
        /// </summary>
        public string Charset
        {
            get{ return m_Charset; }
        }

        /// <summary>
        /// Gets cipher value.
        /// </summary>
        public string Cipher
        {
            get{ return m_Cipher; }
        }

        /// <summary>
        /// Gets authorization ID.
        /// </summary>
        public string Authzid
        {
            get{ return m_Authzid; }
        }

        #endregion
    }
}
